Build Standalone Environment #17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Standalone Environment | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| comfyui_ref: | |
| description: "ComfyUI branch/tag (leave empty for latest stable release)" | |
| required: false | |
| default: "" | |
| release_tag: | |
| description: "Tag for the GitHub Release to upload to" | |
| required: true | |
| default: "standalone-v0.1.0" | |
| env: | |
| PYTHON_VERSION: "3.13.12" | |
| PBS_RELEASE: "20260211" | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Windows | |
| - id: win-nvidia | |
| os: windows-latest | |
| python_platform: x86_64-pc-windows-msvc | |
| vendor_requirements: requirements-nvidia.txt | |
| # - id: win-nvidia-cu128 | |
| # os: windows-latest | |
| # python_platform: x86_64-pc-windows-msvc | |
| # vendor_requirements: requirements-nvidia-cu128.txt | |
| # - id: win-nvidia-cu126 | |
| # os: windows-latest | |
| # python_platform: x86_64-pc-windows-msvc | |
| # vendor_requirements: requirements-nvidia-cu126.txt | |
| - id: win-intel-xpu | |
| os: windows-latest | |
| python_platform: x86_64-pc-windows-msvc | |
| vendor_requirements: requirements-intel.txt | |
| # AMD universal ROCm 7.2 package only provides Python 3.12 wheels | |
| - id: win-amd | |
| os: windows-latest | |
| python_platform: x86_64-pc-windows-msvc | |
| vendor_requirements: requirements-amd-win.txt | |
| python_version: "3.12.12" | |
| - id: win-cpu | |
| os: windows-latest | |
| python_platform: x86_64-pc-windows-msvc | |
| vendor_requirements: requirements-cpu.txt | |
| # Linux | |
| - id: linux-nvidia | |
| os: ubuntu-latest | |
| python_platform: x86_64-unknown-linux-gnu | |
| vendor_requirements: requirements-nvidia.txt | |
| # macOS | |
| - id: mac-mps | |
| os: macos-latest | |
| python_platform: aarch64-apple-darwin | |
| vendor_requirements: requirements-mac.txt | |
| runs-on: ${{ matrix.os }} | |
| name: Build ${{ matrix.id }} | |
| steps: | |
| - name: Checkout launcher repo | |
| uses: actions/checkout@v4 | |
| - name: Resolve ComfyUI ref | |
| id: comfyui | |
| shell: bash | |
| run: | | |
| REF="${{ inputs.comfyui_ref }}" | |
| if [[ -z "$REF" ]]; then | |
| MAX_ATTEMPTS=$([[ "${{ runner.os }}" == "macOS" ]] && echo 6 || echo 3) | |
| for i in $(seq 1 $MAX_ATTEMPTS); do | |
| REF=$(curl -sf --retry 3 -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/Comfy-Org/ComfyUI/releases/latest 2>/dev/null | jq -r '.tag_name // empty' 2>/dev/null) || true | |
| if [[ -n "$REF" ]]; then break; fi | |
| echo "Attempt $i/$MAX_ATTEMPTS failed, retrying in 15s..." | |
| sleep 15 | |
| done | |
| if [[ -z "$REF" ]]; then | |
| echo "::error::Failed to resolve latest ComfyUI release tag" | |
| exit 1 | |
| fi | |
| echo "Auto-detected latest stable release: $REF" | |
| fi | |
| echo "ref=$REF" >> "$GITHUB_OUTPUT" | |
| - name: Download python-build-standalone | |
| shell: bash | |
| run: | | |
| PYTHON_VER="${{ matrix.python_version || env.PYTHON_VERSION }}" | |
| PBS_TAG="${{ env.PBS_RELEASE }}" | |
| ARCHIVE_NAME="cpython-${PYTHON_VER}+${PBS_TAG}-${{ matrix.python_platform }}-install_only.tar.gz" | |
| DOWNLOAD_URL="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_TAG}/${ARCHIVE_NAME}" | |
| echo "Downloading: ${DOWNLOAD_URL}" | |
| curl -L -o python-standalone.tar.gz "$DOWNLOAD_URL" | |
| mkdir python-extract | |
| tar -xzf python-standalone.tar.gz -C python-extract | |
| mv python-extract/python standalone-env | |
| - name: Clone ComfyUI | |
| shell: bash | |
| run: | | |
| REF="${{ steps.comfyui.outputs.ref }}" | |
| MAX_ATTEMPTS=$([[ "${{ runner.os }}" == "macOS" ]] && echo 6 || echo 3) | |
| for i in $(seq 1 $MAX_ATTEMPTS); do | |
| git clone --depth 1 --branch "$REF" https://github.com/Comfy-Org/ComfyUI.git ComfyUI && break | |
| echo "Clone attempt $i/$MAX_ATTEMPTS failed, retrying in 15s..." | |
| rm -rf ComfyUI | |
| sleep 15 | |
| done | |
| if [ ! -d "ComfyUI" ]; then | |
| echo "::error::Failed to clone ComfyUI after $MAX_ATTEMPTS attempts" | |
| exit 1 | |
| fi | |
| - name: Fetch and prepare requirements | |
| shell: bash | |
| run: | | |
| # Build final requirements: vendor-specific + ComfyUI deps + manager deps | |
| cp "${{ matrix.vendor_requirements }}" requirements_final.txt | |
| echo "-r ComfyUI/requirements.txt" >> requirements_final.txt | |
| echo "-r ComfyUI/manager_requirements.txt" >> requirements_final.txt | |
| echo "=== requirements_final.txt ===" | |
| cat requirements_final.txt | |
| - name: Install dependencies into standalone Python | |
| shell: bash | |
| run: | | |
| if [[ "${{ runner.os }}" == "Windows" ]]; then | |
| PYTHON="standalone-env/python.exe" | |
| else | |
| PYTHON="standalone-env/bin/python3" | |
| chmod +x "$PYTHON" | |
| fi | |
| "$PYTHON" -m ensurepip --upgrade 2>/dev/null || true | |
| "$PYTHON" -m pip install --upgrade pip | |
| "$PYTHON" -m pip install --no-cache-dir -r requirements_final.txt pygit2 | |
| - name: Bundle uv | |
| shell: bash | |
| run: | | |
| if [[ "${{ runner.os }}" == "Windows" ]]; then | |
| UV_ARCHIVE="uv-x86_64-pc-windows-msvc.zip" | |
| curl -L -o uv.zip "https://github.com/astral-sh/uv/releases/latest/download/${UV_ARCHIVE}" | |
| unzip uv.zip -d uv-extract | |
| cp uv-extract/uv.exe standalone-env/ | |
| elif [[ "${{ runner.os }}" == "macOS" ]]; then | |
| UV_ARCHIVE="uv-aarch64-apple-darwin.tar.gz" | |
| curl -L -o uv.tar.gz "https://github.com/astral-sh/uv/releases/latest/download/${UV_ARCHIVE}" | |
| mkdir -p uv-extract && tar -xzf uv.tar.gz -C uv-extract --strip-components=1 | |
| cp uv-extract/uv standalone-env/bin/ | |
| else | |
| UV_ARCHIVE="uv-x86_64-unknown-linux-gnu.tar.gz" | |
| curl -L -o uv.tar.gz "https://github.com/astral-sh/uv/releases/latest/download/${UV_ARCHIVE}" | |
| mkdir -p uv-extract && tar -xzf uv.tar.gz -C uv-extract --strip-components=1 | |
| cp uv-extract/uv standalone-env/bin/ | |
| fi | |
| rm -rf uv.zip uv.tar.gz uv-extract | |
| - name: Strip unnecessary files | |
| shell: bash | |
| run: | | |
| if [[ "${{ runner.os }}" == "Windows" ]]; then | |
| SITE_PACKAGES="standalone-env/Lib/site-packages" | |
| else | |
| SITE_PACKAGES=$(find standalone-env/lib -name site-packages -type d | head -1) | |
| fi | |
| echo "Stripping from: $SITE_PACKAGES" | |
| # Report test directory sizes before stripping | |
| echo "=== Test directory sizes ===" | |
| for test_dir in \ | |
| "$SITE_PACKAGES/torch/test" \ | |
| "$SITE_PACKAGES/transformers/tests" \ | |
| "$SITE_PACKAGES/numpy/tests" \ | |
| "$SITE_PACKAGES/PIL/tests" \ | |
| "$SITE_PACKAGES/scipy/tests"; do | |
| if [ -d "$test_dir" ]; then | |
| echo "$(du -sh "$test_dir")" | |
| else | |
| echo "$test_dir: not found" | |
| fi | |
| done | |
| echo "===========================" | |
| # Remove unnecessary lib files (Windows .lib, Linux/Mac .a) | |
| rm -f "$SITE_PACKAGES/torch/lib/"*.lib 2>/dev/null || true | |
| find "$SITE_PACKAGES" -name "*.a" -delete 2>/dev/null || true | |
| # Remove C++ headers and cmake files | |
| rm -rf "$SITE_PACKAGES/torch/include" 2>/dev/null || true | |
| rm -rf "$SITE_PACKAGES/torch/share" 2>/dev/null || true | |
| # Remove unused torch components | |
| rm -rf "$SITE_PACKAGES/caffe2" 2>/dev/null || true | |
| rm -rf "$SITE_PACKAGES/torch/_inductor/autoheuristic/datasets" 2>/dev/null || true | |
| rm -rf "$SITE_PACKAGES/torch/test" 2>/dev/null || true | |
| # Remove __pycache__ and .pyc files | |
| find standalone-env -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true | |
| find standalone-env -name "*.pyc" -delete 2>/dev/null || true | |
| # Strip debug symbols from shared libraries (Linux/Mac) | |
| if [[ "${{ runner.os }}" != "Windows" ]]; then | |
| find standalone-env -name "*.so" -exec strip --strip-debug {} \; 2>/dev/null || true | |
| find standalone-env -name "*.dylib" -exec strip -x {} \; 2>/dev/null || true | |
| fi | |
| echo "=== Largest directories in site-packages ===" | |
| du -sh "$SITE_PACKAGES"/*/ 2>/dev/null | sort -rh | head -20 | |
| echo "=== Final environment size ===" | |
| du -sh standalone-env | |
| - name: Smoke test stripped environment | |
| shell: bash | |
| run: | | |
| if [[ "${{ runner.os }}" == "Windows" ]]; then | |
| PYTHON="standalone-env/python.exe" | |
| else | |
| PYTHON="standalone-env/bin/python3" | |
| fi | |
| echo "=== Verifying core imports ===" | |
| "$PYTHON" -c " | |
| import torch | |
| print(f'torch {torch.__version__}') | |
| import tqdm | |
| print(f'tqdm {tqdm.__version__}') | |
| import transformers | |
| print(f'transformers {transformers.__version__}') | |
| import pygit2 | |
| print(f'pygit2 {pygit2.__version__}') | |
| print('All core imports OK') | |
| " | |
| - name: Generate build manifest | |
| shell: bash | |
| run: | | |
| COMFYUI_COMMIT=$(git -C ComfyUI rev-parse HEAD) | |
| if [[ "${{ runner.os }}" == "Windows" ]]; then | |
| PYTHON="standalone-env/python.exe" | |
| UV_VERSION=$(standalone-env/uv.exe --version | awk '{print $2}') | |
| else | |
| PYTHON="standalone-env/bin/python3" | |
| UV_VERSION=$(standalone-env/bin/uv --version | awk '{print $2}') | |
| fi | |
| PYTHON_VER="${{ matrix.python_version || env.PYTHON_VERSION }}" | |
| TORCH_VERSION=$("$PYTHON" -c "import torch; print(torch.__version__)") | |
| TORCHVISION_VERSION=$("$PYTHON" -c "import torchvision; print(torchvision.__version__)" 2>/dev/null || echo "N/A") | |
| TORCHAUDIO_VERSION=$("$PYTHON" -c "import torchaudio; print(torchaudio.__version__)" 2>/dev/null || echo "N/A") | |
| BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) | |
| ARCHIVE_BASE="comfyui-standalone-${{ matrix.id }}-${{ steps.comfyui.outputs.ref }}" | |
| "$PYTHON" -c " | |
| import json, pathlib | |
| vendor_req = pathlib.Path('${{ matrix.vendor_requirements }}').read_text().strip() | |
| comfyui_req = pathlib.Path('ComfyUI/requirements.txt').read_text().strip() | |
| manager_req = pathlib.Path('ComfyUI/manager_requirements.txt').read_text().strip() | |
| manifest = { | |
| 'id': '${{ matrix.id }}', | |
| 'version': '${{ github.ref_type == 'tag' && github.ref_name || inputs.release_tag }}', | |
| 'archive_base': '${ARCHIVE_BASE}', | |
| 'files': [], | |
| 'comfyui_ref': '${{ steps.comfyui.outputs.ref }}', | |
| 'comfyui_commit': '${COMFYUI_COMMIT}', | |
| 'python_version': '${PYTHON_VER}', | |
| 'torch_version': '${TORCH_VERSION}', | |
| 'torchvision_version': '${TORCHVISION_VERSION}', | |
| 'torchaudio_version': '${TORCHAUDIO_VERSION}', | |
| 'pbs_release': '${{ env.PBS_RELEASE }}', | |
| 'uv_version': '${UV_VERSION}', | |
| 'vendor_requirements': '${{ matrix.vendor_requirements }}', | |
| 'vendor_requirements_content': vendor_req, | |
| 'comfyui_requirements_content': comfyui_req, | |
| 'manager_requirements_content': manager_req, | |
| 'build_date': '${BUILD_DATE}', | |
| } | |
| print(json.dumps(manifest, indent=2)) | |
| " > manifest.json | |
| echo "=== manifest.json (pre-archive) ===" | |
| cat manifest.json | |
| - name: Package archive | |
| shell: bash | |
| run: | | |
| ARCHIVE_BASE="comfyui-standalone-${{ matrix.id }}-${{ steps.comfyui.outputs.ref }}" | |
| if [[ "${{ runner.os }}" == "macOS" ]]; then | |
| tar -czf "${ARCHIVE_BASE}.tar.gz" standalone-env ComfyUI manifest.json "${{ matrix.vendor_requirements }}" | |
| else | |
| 7z a -t7z -m0=lzma2 -mx=3 -mfb=32 -md=16m -ms=on -mf=off \ | |
| -v1900m \ | |
| "${ARCHIVE_BASE}.7z" standalone-env ComfyUI manifest.json "${{ matrix.vendor_requirements }}" | |
| fi | |
| - name: Populate manifest files list | |
| shell: bash | |
| run: | | |
| ARCHIVE_BASE="comfyui-standalone-${{ matrix.id }}-${{ steps.comfyui.outputs.ref }}" | |
| FILES=$(ls -1 "${ARCHIVE_BASE}".* 2>/dev/null | sort) | |
| echo "=== Archive files ===" | |
| ls -lh ${ARCHIVE_BASE}.* 2>/dev/null | |
| python3 -c " | |
| import json | |
| files = '''${FILES}'''.strip().split('\n') | |
| files = [f.strip() for f in files if f.strip()] | |
| with open('manifest.json', 'r') as fh: | |
| manifest = json.load(fh) | |
| manifest['files'] = files | |
| with open('manifest.json', 'w') as fh: | |
| json.dump(manifest, fh, indent=2) | |
| " | |
| echo "=== manifest.json ===" | |
| cat manifest.json | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: manifest-${{ matrix.id }} | |
| path: manifest.json | |
| - name: Upload to release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ github.ref_type == 'tag' && github.ref_name || inputs.release_tag }} | |
| files: | | |
| comfyui-standalone-${{ matrix.id }}-${{ steps.comfyui.outputs.ref }}.* | |
| fail_on_unmatched_files: false | |
| draft: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| publish-manifests: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| name: Publish manifests | |
| steps: | |
| - name: Download all manifests | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: manifest-* | |
| - name: Combine manifests | |
| shell: bash | |
| run: | | |
| python -c " | |
| import json, glob | |
| manifests = [] | |
| for f in sorted(glob.glob('manifest-*/manifest.json')): | |
| with open(f) as fh: | |
| manifests.append(json.load(fh)) | |
| print(json.dumps(manifests, indent=2)) | |
| " > manifests.json | |
| echo "=== manifests.json ===" | |
| cat manifests.json | |
| - name: Upload manifests to release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ github.ref_type == 'tag' && github.ref_name || inputs.release_tag }} | |
| files: manifests.json | |
| fail_on_unmatched_files: false | |
| draft: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |