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:
co-authored by
Claude Opus 5
parent
1af51e034c
commit
5cbae311b6
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user