feat(update): report which packages pip moved

Freeze before and after the requirements install and print the diff.
A silent bump of numpy or transformers under ComfyUI's compiled
extensions segfaults hours later with nothing pointing at the update.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-08-23 04:10:04 +03:00
co-authored by Claude Opus 5
parent 1af51e034c
commit 5cbae311b6
3 changed files with 107 additions and 5 deletions
+49 -1
View File
@@ -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:
+52 -4
View File
@@ -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):
+6
View File
@@ -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