diff --git a/docs/admin-api.openapi.json b/docs/admin-api.openapi.json index 33549f4..4cfa23f 100644 --- a/docs/admin-api.openapi.json +++ b/docs/admin-api.openapi.json @@ -8626,7 +8626,7 @@ "items": { "type": "string" }, - "description": "Requirements are the PyPI packages the node needs that ComfyUI does not have, by name and version range only. They are installed beside the replica, never into ComfyUI's environment, at the versions that environment already has of anything it shares." + "description": "Requirements are the PyPI packages the node needs, by name and version range only. They are installed beside the replica, never into ComfyUI's environment: a package named here at the range named, and every other one at the version ComfyUI already runs. One named here is first on ComfyUI's path, so its range must suit ComfyUI too." } } }, diff --git a/docs/reference.md b/docs/reference.md index 9f31aa0..11486e6 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -912,10 +912,11 @@ node that changed under a model is a model that changed unasked. `[]string` -Requirements are the PyPI packages the node needs that ComfyUI does -not have, by name and version range only. They are installed beside -the replica, never into ComfyUI's environment, at the versions that -environment already has of anything it shares. +Requirements are the PyPI packages the node needs, by name and version +range only. They are installed beside the replica, never into +ComfyUI's environment: a package named here at the range named, and +every other one at the version ComfyUI already runs. One named here +is first on ComfyUI's path, so its range must suit ComfyUI too. ## Endpoint diff --git a/internal/comfy/nodes.go b/internal/comfy/nodes.go index b13f841..1e0ffb1 100644 --- a/internal/comfy/nodes.go +++ b/internal/comfy/nodes.go @@ -83,7 +83,7 @@ func installNodes(ctx context.Context, state, python string, nodes []Node, uv st return "", fmt.Errorf("reading ComfyUI's packages: %w", err) } constraints := filepath.Join(state, "constraints.txt") - if err := os.WriteFile(constraints, freeze, 0o644); err != nil { //nolint:gosec // not secret + if err := os.WriteFile(constraints, withoutNamed(freeze, reqs), 0o644); err != nil { //nolint:gosec // not secret return "", err } _ = os.RemoveAll(site) @@ -136,3 +136,32 @@ func lastLines(b []byte) string { } return strings.Join(lines, " | ") } + +// withoutNamed is ComfyUI's packages less those a node names itself: of a +// package a node asks for by name, the node's range decides — TripoSR's +// checkpoint loads with transformers 4 and not 5 — and of everything else, +// what ComfyUI runs. A node that names a package must name a range ComfyUI +// can run with too: the copy it gets is first on ComfyUI's path. +func withoutNamed(freeze []byte, reqs []string) []byte { + named := map[string]bool{} + for _, r := range reqs { + named[packageName(r)] = true + } + var out []string + for _, line := range strings.Split(string(freeze), "\n") { + if strings.TrimSpace(line) == "" || named[packageName(line)] { + continue + } + out = append(out, line) + } + return []byte(strings.Join(out, "\n") + "\n") +} + +// packageName is a requirement's package, as PyPI compares them. +func packageName(spec string) string { + s := strings.TrimSpace(spec) + if i := strings.IndexAny(s, "<>=!~;[ @"); i >= 0 { + s = s[:i] + } + return strings.NewReplacer("_", "-", ".", "-").Replace(strings.ToLower(s)) +} diff --git a/internal/comfy/progress_test.go b/internal/comfy/progress_test.go index 5e763f9..49ff825 100644 --- a/internal/comfy/progress_test.go +++ b/internal/comfy/progress_test.go @@ -181,3 +181,12 @@ func TestANodeIsPutAtItsCommit(t *testing.T) { t.Errorf("packages without a uv: %v", err) } } + +func TestANodesOwnRangeDecidesThePackagesItNames(t *testing.T) { + freeze := []byte("numpy==2.3.1\ntransformers==5.19.0\nhuggingface-hub==1.33.0\nPillow==12.3.0\n") + got := string(withoutNamed(freeze, []string{"transformers>=4.50.3,<5", "Huggingface_Hub<1", "trimesh"})) + if strings.Contains(got, "transformers") || strings.Contains(got, "huggingface") || + !strings.Contains(got, "numpy==2.3.1") || !strings.Contains(got, "Pillow==12.3.0") { + t.Errorf("constraints %q", got) + } +} diff --git a/internal/control/catalog/catalog.go b/internal/control/catalog/catalog.go index 39a3209..88544ce 100644 --- a/internal/control/catalog/catalog.go +++ b/internal/control/catalog/catalog.go @@ -564,10 +564,11 @@ type CustomNode struct { // Commit is the full commit the node is used at: a branch moves, and a // node that changed under a model is a model that changed unasked. Commit string `json:"commit" koanf:"commit"` - // Requirements are the PyPI packages the node needs that ComfyUI does - // not have, by name and version range only. They are installed beside - // the replica, never into ComfyUI's environment, at the versions that - // environment already has of anything it shares. + // Requirements are the PyPI packages the node needs, by name and version + // range only. They are installed beside the replica, never into + // ComfyUI's environment: a package named here at the range named, and + // every other one at the version ComfyUI already runs. One named here + // is first on ComfyUI's path, so its range must suit ComfyUI too. Requirements []string `json:"requirements,omitempty" koanf:"requirements"` } diff --git a/internal/docgen/reference_gen.go b/internal/docgen/reference_gen.go index cabf1ac..c38d85a 100644 --- a/internal/docgen/reference_gen.go +++ b/internal/docgen/reference_gen.go @@ -93,7 +93,7 @@ var Reference = []Block{ {Package: "catalog", Name: "CustomNode", Title: "CustomNode", Doc: "", Fields: []Field{ {Key: "repo", Type: "string", Doc: "Repo is the repository, https only.", Nested: ""}, {Key: "commit", Type: "string", Doc: "Commit is the full commit the node is used at: a branch moves, and a\nnode that changed under a model is a model that changed unasked.", Nested: ""}, - {Key: "requirements", Type: "[]string", Doc: "Requirements are the PyPI packages the node needs that ComfyUI does\nnot have, by name and version range only. They are installed beside\nthe replica, never into ComfyUI's environment, at the versions that\nenvironment already has of anything it shares.", Nested: ""}, + {Key: "requirements", Type: "[]string", Doc: "Requirements are the PyPI packages the node needs, by name and version\nrange only. They are installed beside the replica, never into\nComfyUI's environment: a package named here at the range named, and\nevery other one at the version ComfyUI already runs. One named here\nis first on ComfyUI's path, so its range must suit ComfyUI too.", Nested: ""}, }}, {Package: "config", Name: "Endpoint", Title: "Endpoint", Doc: "", Fields: []Field{ {Key: "id", Type: "string", Doc: "", Nested: ""},