Skip to content

Build Standalone Environment #17

Build Standalone Environment

Build Standalone Environment #17

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 }}