diff --git a/movie_editor/backend/git_update.py b/movie_editor/backend/git_update.py index f3ccfb7d..b922a3db 100644 --- a/movie_editor/backend/git_update.py +++ b/movie_editor/backend/git_update.py @@ -188,6 +188,12 @@ def install_requirements(timeout: int = 900) -> dict: req = REPO_ROOT / REQUIREMENTS if not req.is_file(): return {"ran": False, "ok": True, "detail": "no requirements.txt in this checkout"} + # What is installed BEFORE, so the update can say what it changed. An update that + # silently moves a shared dependency is the worst kind: ComfyUI is full of compiled + # extensions (torch, comfy-kitchen, comfy-aimdo, onnxruntime, opencv) and a numpy or + # transformers bump under them does not raise — it segfaults, or corrupts memory, hours + # later, with nothing in the log connecting it to the update that caused it. + before = _pip_freeze() cmd = [sys.executable, "-m", "pip", "install", "--disable-pip-version-check", "-r", str(req)] try: @@ -204,7 +210,49 @@ def install_requirements(timeout: int = 900) -> dict: return {"ran": True, "ok": False, "detail": f"pip install failed — run it yourself:\n" f" {sys.executable} -m pip install -r {req}\n\n{tail}"} - return {"ran": True, "ok": True, "detail": (proc.stdout or "").strip()[-800:]} + changed = _pip_diff(before, _pip_freeze()) + if changed: + print("[FunPack update] pip changed these packages: " + ", ".join(changed)) + return {"ran": True, "ok": True, "changed": changed, + "detail": (("Changed: " + ", ".join(changed) + "\n\n") if changed else "") + + (proc.stdout or "").strip()[-800:]} + + +def _pip_freeze() -> dict: + """{name: version} for the environment ComfyUI is running in. {} if pip cannot be read — + an unreadable freeze must never block the install it was only meant to describe.""" + try: + out = subprocess.run([sys.executable, "-m", "pip", "freeze", + "--disable-pip-version-check"], + capture_output=True, text=True, timeout=120) + if out.returncode != 0: + return {} + found = {} + for line in (out.stdout or "").splitlines(): + if "==" in line: + name, _, ver = line.partition("==") + found[name.strip().lower()] = ver.strip() + return found + except Exception: # noqa: BLE001 + return {} + + +def _pip_diff(before: dict, after: dict) -> list[str]: + """Human-readable list of what moved. Empty when nothing did, or when either side is + unknown — reporting every package as "new" because the freeze failed would be worse + than saying nothing.""" + if not before or not after: + return [] + out = [] + for name, ver in sorted(after.items()): + was = before.get(name) + if was is None: + out.append(f"{name} {ver} (new)") + elif was != ver: + out.append(f"{name} {was} -> {ver}") + for name in sorted(set(before) - set(after)): + out.append(f"{name} {before[name]} (removed)") + return out def pull(branch: str | None = None, *, install_deps: bool = False) -> dict: diff --git a/movie_editor/tests/test_git_update.py b/movie_editor/tests/test_git_update.py index 290e21a5..29388c42 100644 --- a/movie_editor/tests/test_git_update.py +++ b/movie_editor/tests/test_git_update.py @@ -219,17 +219,65 @@ def test_install_uses_the_running_interpreter(monkeypatch, tmp_path): whatever else is on it — installing into the wrong environment succeeds and changes nothing.""" from movie_editor.backend import git_update as gu - seen = {} + calls = [] def fake_run(cmd, **kw): - seen["cmd"] = cmd + calls.append(cmd) return types.SimpleNamespace(returncode=0, stdout="ok", stderr="") monkeypatch.setattr(gu.subprocess, "run", fake_run) out = gu.install_requirements() assert out["ran"] and out["ok"] - assert seen["cmd"][0] == sys.executable - assert seen["cmd"][1:4] == ["-m", "pip", "install"] + # freeze / install / freeze — every one of them through THIS interpreter. + assert all(c[0] == sys.executable and c[1:3] == ["-m", "pip"] for c in calls) + install = [c for c in calls if c[3] == "install"] + assert len(install) == 1 + + +def test_the_install_reports_which_packages_moved(monkeypatch): + """An update that silently bumps a shared dependency is the worst kind: ComfyUI is full + of compiled extensions, and a numpy or transformers change under them does not raise — + it segfaults hours later with nothing connecting it to the update.""" + from movie_editor.backend import git_update as gu + freezes = iter(["numpy==1.26.4\ntransformers==4.44.0\n", + "numpy==2.1.0\ntransformers==5.0.1\ngguf==0.10.0\n"]) + + def fake_run(cmd, **kw): + if cmd[3] == "freeze": + return types.SimpleNamespace(returncode=0, stdout=next(freezes), stderr="") + return types.SimpleNamespace(returncode=0, stdout="ok", stderr="") + + monkeypatch.setattr(gu.subprocess, "run", fake_run) + changed = gu.install_requirements()["changed"] + assert "numpy 1.26.4 -> 2.1.0" in changed + assert "transformers 4.44.0 -> 5.0.1" in changed + assert "gguf 0.10.0 (new)" in changed + + +def test_an_unreadable_freeze_reports_nothing_rather_than_everything(monkeypatch): + """Reporting every package as new because pip freeze failed is worse than silence.""" + from movie_editor.backend import git_update as gu + + def fake_run(cmd, **kw): + if cmd[3] == "freeze": + return types.SimpleNamespace(returncode=1, stdout="", stderr="boom") + return types.SimpleNamespace(returncode=0, stdout="ok", stderr="") + + monkeypatch.setattr(gu.subprocess, "run", fake_run) + out = gu.install_requirements() + assert out["ok"] and out["changed"] == [] + + +def test_nothing_changed_reports_nothing(monkeypatch): + from movie_editor.backend import git_update as gu + same = "numpy==2.1.0\ngguf==0.10.0\n" + + def fake_run(cmd, **kw): + return types.SimpleNamespace( + returncode=0, stdout=(same if cmd[3] == "freeze" else "ok"), stderr="") + + monkeypatch.setattr(gu.subprocess, "run", fake_run) + assert gu.install_requirements()["changed"] == [] def test_a_failed_install_reports_the_command_and_never_raises(monkeypatch): diff --git a/requirements.txt b/requirements.txt index e6861fbf..c3377240 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,9 @@ +# Version floors ONLY where FunPack needs one, and never a bare upgrade of anything the +# rest of ComfyUI compiles against. `pip install -r` upgrades a package whose floor is not +# met — on a rental full of native extensions (torch, comfy-kitchen, comfy-aimdo, +# onnxruntime, opencv) a numpy or transformers bump under them does not raise, it segfaults +# hours later. The updater now prints exactly which packages moved; check that line first +# when a machine starts misbehaving after an update. transformers>=5.0.0 huggingface_hub safetensors