"""Install the pinned ComfyUI runtime without overwriting application code.""" from __future__ import annotations import os import shutil import subprocess import sys from pathlib import Path import yaml from core.settings import CATEGORY_TO_DIR_MAP, INPUT_DIR, OUTPUT_DIR APP_DIR = Path(__file__).resolve().parents[1] LOCK_FILE = APP_DIR / "vendor.lock.yaml" VENDOR_DIR = APP_DIR / "_vendor" CUSTOM_NODES_DIR = APP_DIR / "custom_nodes" def _bounded_env_int(name: str, default: int, minimum: int, maximum: int) -> int: try: value = int(os.getenv(name, str(default))) except ValueError: value = default return max(minimum, min(maximum, value)) GIT_TIMEOUT_SECONDS = _bounded_env_int( "IMAGEGEN_GIT_TIMEOUT_SECONDS", 180, 30, 900 ) GIT_NETWORK_ATTEMPTS = _bounded_env_int("IMAGEGEN_GIT_ATTEMPTS", 2, 1, 5) def _run_git(*args: str, cwd: Path | None = None) -> str: attempts = GIT_NETWORK_ATTEMPTS if args and args[0] == "fetch" else 1 for attempt in range(1, attempts + 1): try: completed = subprocess.run( ["git", *args], cwd=str(cwd) if cwd else None, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=GIT_TIMEOUT_SECONDS, ) return completed.stdout.strip() except subprocess.TimeoutExpired as exc: if attempt == attempts: raise RuntimeError( f"Git 操作超过 {GIT_TIMEOUT_SECONDS} 秒:git {args[0]}。" "可稍后重启,或设置 COMFYUI_PATH 使用本地 checkout。" ) from exc print(f"⚠️ Git {args[0]} 超时,正在重试({attempt}/{attempts})…") except subprocess.CalledProcessError: if attempt == attempts: raise print(f"⚠️ Git {args[0]} 失败,正在重试({attempt}/{attempts})…") raise RuntimeError("Unreachable git retry state") def _ensure_pinned_repo(name: str, url: str, revision: str, destination: Path) -> None: if destination.exists() and not (destination / ".git").is_dir(): raise RuntimeError( f"{name} 目录已存在但不是 Git 仓库:{destination}。" "请移走该目录后重新启动。" ) if not destination.exists(): destination.parent.mkdir(parents=True, exist_ok=True) print(f"--- [Vendor] Cloning pinned {name} ---") partial = destination.with_name(f"{destination.name}.partial") last_error = None for attempt in range(1, GIT_NETWORK_ATTEMPTS + 1): if partial.exists(): shutil.rmtree(partial) try: _run_git( "clone", "--filter=blob:none", "--no-checkout", url, str(partial) ) partial.replace(destination) last_error = None break except (subprocess.CalledProcessError, RuntimeError) as exc: last_error = exc if attempt < GIT_NETWORK_ATTEMPTS: print(f"⚠️ {name} clone 失败,正在重试({attempt}/{GIT_NETWORK_ATTEMPTS})…") if last_error is not None: raise RuntimeError( f"无法拉取 {name}。可稍后重启;本地离线运行可设置 " "COMFYUI_PATH,并按需设置 IMAGEGEN_SKIP_CUSTOM_NODES=1。" ) from last_error current = "" try: current = _run_git("rev-parse", "HEAD", cwd=destination) except (subprocess.CalledProcessError, RuntimeError): pass if current != revision: print(f"--- [Vendor] Checking out {name} @ {revision[:12]} ---") _run_git("fetch", "--depth", "1", "origin", revision, cwd=destination) _run_git("checkout", "--detach", "--force", "FETCH_HEAD", cwd=destination) actual = _run_git("rev-parse", "HEAD", cwd=destination) if actual != revision: raise RuntimeError( f"{name} 版本不匹配:期望 {revision},实际 {actual}。" ) print(f"✅ {name} ready @ {actual[:12]}") def _load_lock() -> dict: with LOCK_FILE.open("r", encoding="utf-8") as handle: data = yaml.safe_load(handle) or {} if "comfyui" not in data: raise RuntimeError(f"Missing comfyui entry in {LOCK_FILE}") return data def initialize_comfyui() -> Path: """Prepare pinned sources and make ComfyUI importable. Set ``COMFYUI_PATH`` to use an existing local checkout. This is the recommended offline/local-development route. """ lock = _load_lock() configured_path = os.getenv("COMFYUI_PATH", "").strip() if configured_path: comfyui_path = Path(configured_path).expanduser().resolve() if not (comfyui_path / "nodes.py").is_file(): raise RuntimeError(f"COMFYUI_PATH is not a ComfyUI checkout: {comfyui_path}") else: comfyui_path = VENDOR_DIR / "ComfyUI" comfy = lock["comfyui"] _ensure_pinned_repo("ComfyUI", comfy["url"], comfy["revision"], comfyui_path) CUSTOM_NODES_DIR.mkdir(parents=True, exist_ok=True) if os.getenv("IMAGEGEN_SKIP_CUSTOM_NODES", "0").lower() not in {"1", "true", "yes"}: for name, spec in (lock.get("custom_nodes") or {}).items(): _ensure_pinned_repo(name, spec["url"], spec["revision"], CUSTOM_NODES_DIR / name) # ComfyUI contains a top-level `utils` package. The application uses the # collision-free `imagegen_utils` package, so ComfyUI can safely come first. comfyui_str = str(comfyui_path) if comfyui_str not in sys.path: sys.path.insert(0, comfyui_str) for relative_path in CATEGORY_TO_DIR_MAP.values(): (APP_DIR / relative_path).mkdir(parents=True, exist_ok=True) (APP_DIR / INPUT_DIR).mkdir(parents=True, exist_ok=True) (APP_DIR / OUTPUT_DIR).mkdir(parents=True, exist_ok=True) import folder_paths for category, directory in CATEGORY_TO_DIR_MAP.items(): folder_paths.add_model_folder_path( category, str(Path(directory).resolve()), is_default=True ) folder_paths.set_input_directory(str(Path(INPUT_DIR).resolve())) folder_paths.set_output_directory(str(Path(OUTPUT_DIR).resolve())) import comfy.model_management # noqa: F401 print(f"✅ ComfyUI initialized from isolated path: {comfyui_path}") return comfyui_path