SuperAPI AI Directory

comfy-build-pins

Coding & development

View repository

Updated 10-15 20:14

About this skill

comfy-build-pins

The depth behind comfy-build's pins rule. The parent skill already carries the default — cut the first build with pipDependencies emptied — and that is the right answer most of the time. Read this when you need to justify keeping a line, or when a build failed on a conflict.

Why an inherited freeze is the usual first failure

init fills pipDependencies with the whole pip freeze, behind a two-line comment header naming the platform it was captured on. The builder applies every line as a pip override, and an override replaces what every package declared rather than capping it — a user line for a package the torch stack also names replaces it in place. Left alone, a freeze taken on macOS with Python 3.13 forces those exact versions onto a linux Python 3.12 build.

So the freeze is evidence about the environment the workflow ran in, not a set of pins for the environment being built. Emptying it lets the build resolve the packs' own requirements against the base image's torch, which is what you want.

Empty is the default, not a rule that outranks what you can already see. The reading below exists to avoid buying a conflict, and cutting empty after finding one buys it anyway. A conflict you can state in a sentence goes into cut one, disclosed.

What to delete

Delete rather than curate: torch, torchvision, torchaudio, triton, xformers, every nvidia-*, comfyui-frontend-package, comfyui-manager, comfyui-embedded-docs, and any wheel that only exists on the source OS (pywin32, pyobjc*).

A torch pin is the worst of these: pinning one member of that stack replaces the base image's line for it and releases the other two, so one pin silently un-pins two.

What to keep, and the rules for anything you keep

Keep a line only when you can name why — a pack's own docs demand a version and nothing else supplies it, or a named failure in comfy-build-failures tells you to. Then:

  • numpy and scipy are one axis. Pin one and you have chosen for the other, so pin both, to versions released for each other.

  • Two packages providing one import are one axis too. opencv-python and opencv-python-headless both install cv2, so pin both to the same version number. Resolve the competing names together to get that number rather than recalling one:

    printf 'opencv-python\nopencv-python-headless\n' > pair.txt
    <install>/.venv/bin/uv pip compile pair.txt --python-version <py> --python-platform linux
    
  • An override forces a version, it never adds a package. Pinning something nothing requires installs nothing, and the build says so.

Predict the conflict instead of buying it

A build takes minutes to tell you two packages disagree. Most of that answer is sitting in text files, so look before you cut.

cat <install>/requirements.txt <install>/custom_nodes/*/requirements.txt > declared.txt 2>/dev/null

A hand-authored definition can do this too — clone the packs. The requirements files are not on this machine, but every pack in the definition names a repository or a registry slug that resolves to one, and concatenating their requirements.txt with core's reproduces what the builder resolves. It found the real blocker in one port before a single build was spent:

# core, plus one shallow clone per pack in customNodes
git clone --depth 1 -b <baseComfyVersion> https://github.com/comfyanonymous/ComfyUI core

# per pack, when gitRef is a branch or tag:
git clone --depth 1 -b <gitRef> https://github.com/<org>/<repo> packs/<name>

# per pack, when gitRef is a commit SHA — `--branch` does NOT accept one
# ("fatal: Remote branch <sha> not found in upstream origin"), so fetch it:
git init -q packs/<name>
git -C packs/<name> fetch -q --depth 1 https://github.com/<org>/<repo> <gitRef>
git -C packs/<name> checkout -q FETCH_HEAD

cat core/requirements.txt packs/*/requirements.txt > declared.txt

Clone each pack at the ref the definition pins, not its HEAD — the same distinction comfy-build-authoring draws about latest_version: a gitRef entry means that commit, and HEAD's requirements are a different pack's. For a registryVersion entry there is no ref to clone; take that pack's requirements from its published archive, or say in the disclosure that its constraints went unread.

Then strip the deletions listed above (torch, torchvision, torchaudio, triton, xformers, nvidia-*, comfyui-frontend-package, …) and resolve.

Pass the platform tag the build actually uses, because the tag decides which conflict surfaces first. On the same input, the default manylinux_2_28 reported an open3d wheel-tag problem — a red herring — while manylinux_2_34 reported the real blocker:

uv pip compile declared_clean.txt --python-version 3.12 --python-platform x86_64-manylinux_2_34

That blocker was Imath>=3.1.0, unsatisfiable: PyPI's imath has only 0.0.1 and 0.0.2, and the Imath module ships inside the OpenEXR package the same pack already requires. Which is the second thing worth knowing:

pip install -r req.txt || true in a source script is a smell. It hides every constraint failure the builder will enforce, so a source environment can run happily for months carrying a requirement that has never once been satisfied — which is exactly how that Imath line survived unnoticed.

requirements.txt is the only file the build reads. It resolves ComfyUI's own plus one per pack, so a dependency declared only in a pyproject.toml is never installed on its account, and a pyproject constraint you find on disk is not one the build applies: one real pack asked for a bare timm in requirements.txt and timm==0.6.13 in its pyproject.toml. A pack shipping no requirements.txt declares nothing and gets whatever the others pulled in, which is the shape behind declared custom nodes failed to import naming a module nothing asked for.

Three shapes in that text are worth a build each:

  • Two names for one import. opencv-python and opencv-python-headless both install cv2; pillow and the abandoned pil both answer to PIL. A real install had four packs asking for both cv2 names. One loses, and whichever loses, something breaks. A failing import names the module, never the pip package, so pick between them from what the packs declare and not from the log.
  • A ceiling on a shared package. A line like opencv-python-headless[ffmpeg]<=4.7.0.72 holds everyone at a 2023 build. That single line is the most common cause of a failed first build here, because that wheel predates NumPy 2 and aborts at import under it.
  • A pack pinning far below what the install runs. Compare a pin against the freeze init captured. timm==0.6.13 under an install running 1.0.28 is a pack untouched in two years, and that gap is the pin to write.

Ignore torch, torchvision and torchaudio in all of this. The build owns them and they always differ.

When a resolver is available, confirm it

The transitive answer needs one, and uv is usually already in the install:

<install>/.venv/bin/uv pip compile declared.txt --python-version <py> --python-platform linux -o resolved.txt

<py> is the base image's Python, which comfy build refs base-images names — read it rather than assuming, since the catalog moves. That command needs the sign-in, and so does the cut, so this is the point to sign in. If the user would rather not yet, resolve against the install's own Python and say in the disclosure that the build's Python is unconfirmed. A refusal to resolve is the clearest possible finding: the error names both sides. Plain pip cannot do this reliably for another platform, so do not force it.

A warning is a finding too. uv reporting that a package has no extra by the name a pack asked for — say [ffmpeg] on a pinned wheel — corroborates that the pin is old enough to have moved on. Read warnings, not only the exit status.

A clean resolve is not an all-clear. The ceiling case satisfies every constraint: the six-pack install that failed resolves to numpy==2.5.2 with opencv-python-headless==4.7.0.72 without complaint. Take a refusal as a finding and a success as nothing learned about the three shapes above.

When there is no resolver, offer to install one and say plainly what it is for. If the user would rather not, say the check was the reading above only, and that the build is now the first thing that will disagree with you.

What none of this can see

  • A binary compiled against another version. Every constraint is satisfied and the pack still aborts with numpy.core.multiarray failed to import.
  • Install scripts. Packs run their own at build time, outside the lock, so the final environment is not the one you resolved.

Back to comfy skills show comfy-build for the disclosure and the cut. comfy skills show comfy-build-failures reads a build that already failed.