feat(entrypoint): fail-fast when no compatible NVIDIA GPU, mirror Manager’s dependency install steps, and harden permissions for Manager operations

- Add an early runtime check that exits cleanly when no compatible NVIDIA GPU is detected, preventing unnecessary installs and builds on hosts without GPUs, which matches the repo’s requirement to target recent-gen NVIDIA GPUs and avoids work on GitHub runners. 
- Mirror ComfyUI-Manager’s dependency behavior for custom nodes by: installing requirements*.txt and requirements/*.txt, building nodes with pyproject.toml using pip, and invoking node-provided install.py scripts when present, aligning with documented custom-node install flows. 
- Enforce user-level pip installs (PIP_USER=1) and ensure /usr/local site-packages trees are owned and writable by the runtime user; this resolves permission-denied errors seen when Manager updates or removes packages (e.g., numpy __pycache__), improving reliability of Manager-driven installs and uninstalls.
This commit is contained in:
clsferguson 2025-09-29 22:36:35 -06:00 committed by GitHub
parent f6d49f33b7
commit b0b95e5cc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -13,13 +13,12 @@ SAGE_ATTENTION_BUILT_FLAG="$SAGE_ATTENTION_DIR/.built"
PERMISSIONS_SET_FLAG="$BASE_DIR/.permissions_set"
FIRST_RUN_FLAG="$BASE_DIR/.first_run_done"
# Sage flags:
# - SAGE_ATTENTION_AVAILABLE: set by script to indicate built/importable
# - FORCE_SAGE_ATTENTION=1: force app to enable at startup (default 0)
# --- logging ---
log() { echo "[$(date '+%H:%M:%S')] $1"; }
# Make newly created files group-writable (helps in shared volumes)
umask 0002
# --- build parallelism (single knob) ---
# Public knob: SAGE_MAX_JOBS. If unset, pick RAM/CPU heuristic.
decide_build_jobs() {
@ -48,6 +47,26 @@ for i in range(c):
" 2>/dev/null
}
# Determine if there is a compatible NVIDIA GPU (>= sm_75, i.e., 16-series/Turing and newer)
gpu_is_compatible() {
python - <<'PY' 2>/dev/null
import sys
try:
import torch
if not torch.cuda.is_available():
sys.exit(2)
ok=False
for i in range(torch.cuda.device_count()):
p=torch.cuda.get_device_properties(i)
cc=float(f"{p.major}.{p.minor}")
if cc >= 7.5:
ok=True
sys.exit(0 if ok else 3)
except Exception:
sys.exit(4)
PY
}
# Derive arch list directly from Torch; optional +PTX via SAGE_PTX_FALLBACK=1
compute_arch_list_from_torch() {
python - <<'PY' 2>/dev/null
@ -165,7 +184,6 @@ build_sage_attention_mixed() {
local jobs; jobs="$(decide_build_jobs)"
log "Using MAX_JOBS=${jobs} for SageAttention build"
# Set MAX_JOBS only for this build call to avoid leaking globally
if MAX_JOBS="${jobs}" python -m pip install --user --no-build-isolation .; then
echo "$SAGE_STRATEGY|$TORCH_CUDA_ARCH_LIST" > "$SAGE_ATTENTION_BUILT_FLAG"
log "SageAttention built successfully"
@ -203,7 +221,6 @@ setup_sage_attention() {
if ! detect_gpu_generations; then log "No GPUs detected, skipping SageAttention setup"; return 0; fi
determine_sage_strategy
# Resolve arch list early
export TORCH_CUDA_ARCH_LIST="${SAGE_ARCH_LIST_OVERRIDE:-$(compute_arch_list_from_torch)}"
if [ -z "$TORCH_CUDA_ARCH_LIST" ]; then
local tmp=""
@ -245,61 +262,110 @@ if [ "$(id -u)" = "0" ]; then
mkdir -p "/home/${APP_USER}"
for d in "$BASE_DIR" "/home/$APP_USER"; do [ -e "$d" ] && chown -R "${APP_USER}:${APP_GROUP}" "$d" || true; done
# Discover both system and user site dirs and make them writable by the runtime user
readarray -t PY_PATHS < <(python - <<'PY'
import sys, sysconfig, os, datetime
import sys, sysconfig, os, site, datetime
def log(m): print(f"[bootstrap:python {datetime.datetime.now().strftime('%H:%M:%S')}] {m}", file=sys.stderr, flush=True)
log("Determining writable Python install targets via sysconfig.get_paths()")
log("Determining writable Python install targets via sysconfig.get_paths(), site.getsitepackages(), and site.getusersitepackages()")
seen=set()
for k in ("purelib","platlib","scripts","include","platinclude","data"):
v = sysconfig.get_paths().get(k)
if v: print(v); log(f"emit {k} -> {v}")
if v and v.startswith("/usr/local") and v not in seen:
print(v); seen.add(v); log(f"emit {k} -> {v}")
for v in (site.getusersitepackages(),):
if v and v not in seen:
print(v); seen.add(v); log(f"emit usersite -> {v}")
for v in site.getsitepackages():
if v and v.startswith("/usr/local") and v not in seen:
print(v); seen.add(v); log(f"emit sitepkg -> {v}")
d = sysconfig.get_paths().get("data")
if d:
share=os.path.join(d,"share"); man1=os.path.join(share,"man","man1")
print(share); print(man1); log(f"emit wheel data dirs -> {share}, {man1}")
for v in (share, man1):
if v and v.startswith("/usr/local") and v not in seen:
print(v); seen.add(v); log(f"emit wheel data -> {v}")
PY
)
for d in "${PY_PATHS[@]}"; do
case "$d" in
/usr/local|/usr/local/*) mkdir -p "$d" || true; chown -R "${APP_USER}:${APP_GROUP}" "$d" || true; chmod -R u+rwX,g+rwX "$d" || true ;;
*) : ;;
esac
[ -n "$d" ] || continue
mkdir -p "$d" || true
chown -R "${APP_USER}:${APP_GROUP}" "$d" || true
chmod -R u+rwX,g+rwX "$d" || true
done
# Also ensure the main site-packages tree is writable if present (guards numpy uninstall/upgrade)
if [ -d "/usr/local/lib/python3.12/site-packages" ]; then
chown -R "${APP_USER}:${APP_GROUP}" /usr/local/lib/python3.12/site-packages || true
chmod -R u+rwX,g+rwX /usr/local/lib/python3.12/site-packages || true
fi
touch "$PERMISSIONS_SET_FLAG"; chown "${APP_USER}:${APP_GROUP}" "$PERMISSIONS_SET_FLAG"
log "User permissions configured"
else log "User permissions already configured, skipping..."; fi
else
log "User permissions already configured, skipping..."
fi
exec runuser -u "${APP_USER}" -- "$0" "$@"
fi
# --- SageAttention setup ---
# From here on, running as $APP_USER
# Favor user installs everywhere to avoid touching system packages
export PATH="$HOME/.local/bin:$PATH"
pyver="$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
export PYTHONPATH="$HOME/.local/lib/python${pyver}/site-packages:${PYTHONPATH:-}"
export PIP_USER=1
export PIP_PREFER_BINARY=1
# Abort early if no compatible NVIDIA GPU (>= sm_75) is present
if ! gpu_is_compatible; then
log "No compatible NVIDIA GPU detected (compute capability 7.5+ required). Shutting down container."
# Exit 0 to avoid restart loops in some runtimes
exit 0
fi
# --- SageAttention setup (runs only if compatible GPU is present) ---
setup_sage_attention
# --- ComfyUI-Manager sync ---
if [ -d "$CUSTOM_NODES_DIR/ComfyUI-Manager/.git" ]; then
log "Updating ComfyUI-Manager"; git -C "$CUSTOM_NODES_DIR/ComfyUI-Manager" fetch --depth 1 origin || true
git -C "$CUSTOM_NODES_DIR/ComfyUI-Manager" reset --hard origin/HEAD || true; git -C "$CUSTOM_NODES_DIR/ComfyUI-Manager" clean -fdx || true
log "Updating ComfyUI-Manager"
git -C "$CUSTOM_NODES_DIR/ComfyUI-Manager" fetch --depth 1 origin || true
git -C "$CUSTOM_NODES_DIR/ComfyUI-Manager" reset --hard origin/HEAD || true
git -C "$CUSTOM_NODES_DIR/ComfyUI-Manager" clean -fdx || true
elif [ ! -d "$CUSTOM_NODES_DIR/ComfyUI-Manager" ]; then
log "Installing ComfyUI-Manager"; git clone --depth 1 https://github.com/ltdrdata/ComfyUI-Manager.git "$CUSTOM_NODES_DIR/ComfyUI-Manager" || true
log "Installing ComfyUI-Manager"
git clone --depth 1 https://github.com/ltdrdata/ComfyUI-Manager.git "$CUSTOM_NODES_DIR/ComfyUI-Manager" || true
fi
# --- PATH/PYTHONPATH ---
export PATH="$HOME/.local/bin:$PATH"
pyver="$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
export PYTHONPATH="$HOME/.local/lib/python${pyver}/site-packages:${PYTHONPATH:-}"
# --- first-run install of custom_nodes ---
if [ ! -f "$FIRST_RUN_FLAG" ] || [ "${COMFY_FORCE_INSTALL:-0}" = "1" ]; then
if [ "${COMFY_AUTO_INSTALL:-1}" = "1" ]; then
log "First run or forced; installing custom node dependencies..."
# 1) Install requirements files (Manager-like behavior)
while IFS= read -r -d '' req; do
log "python -m pip install --user --upgrade -r $req"
log "python -m pip install --user --upgrade --upgrade-strategy only-if-needed -r $req"
python -m pip install --no-cache-dir --user --upgrade --upgrade-strategy only-if-needed -r "$req" || true
done < <(find "$CUSTOM_NODES_DIR" -maxdepth 3 -type f \( -iname 'requirements.txt' -o -iname 'requirements-*.txt' -o -path '*/requirements/*.txt' \) -print0)
# 2) Install from pyproject (editable build avoided to mimic Managers typical install)
while IFS= read -r -d '' pjt; do
d="$(dirname "$pjt")"; log "python -m pip install --user . in $d"
d="$(dirname "$pjt")"
log "python -m pip install --user . in $d"
(cd "$d" && python -m pip install --no-cache-dir --user .) || true
done < <(find "$CUSTOM_NODES_DIR" -maxdepth 2 -type f -iname 'pyproject.toml' -not -path '*/ComfyUI-Manager/*' -print0)
# 3) Run node-provided install.py if present (Manager runs install scripts; mirror that)
while IFS= read -r -d '' inst; do
d="$(dirname "$inst")"
log "Running node install script: $inst"
(cd "$d" && python "$inst") || true
done < <(find "$CUSTOM_NODES_DIR" -maxdepth 2 -type f -iname 'install.py' -not -path '*/ComfyUI-Manager/*' -print0)
python -m pip check || true
else log "COMFY_AUTO_INSTALL=0; skipping dependency install"; fi
else
log "COMFY_AUTO_INSTALL=0; skipping dependency install"
fi
touch "$FIRST_RUN_FLAG"
else
log "Not first run; skipping custom_nodes dependency install"
@ -316,8 +382,12 @@ else
fi
cd "$BASE_DIR"
if [ $# -eq 0 ]; then exec python main.py --listen 0.0.0.0 $COMFYUI_ARGS
if [ $# -eq 0 ]; then
exec python main.py --listen 0.0.0.0 $COMFYUI_ARGS
else
if [ "$1" = "python" ] && [ "${2:-}" = "main.py" ]; then shift 2; exec python main.py $COMFYUI_ARGS "$@"
else exec "$@"; fi
if [ "$1" = "python" ] && [ "${2:-}" = "main.py" ]; then
shift 2; exec python main.py $COMFYUI_ARGS "$@"
else
exec "$@"
fi
fi