comfy-env 0.1.16__py3-none-any.whl → 0.1.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
comfy_env/__init__.py CHANGED
@@ -37,10 +37,11 @@ from .isolation import wrap_isolated_nodes, wrap_nodes
37
37
  from .config import (
38
38
  ComfyEnvConfig,
39
39
  NodeDependency,
40
- NodeReq, # Backwards compatibility alias
40
+ NodeReq,
41
41
  load_config,
42
42
  discover_config,
43
43
  CONFIG_FILE_NAME,
44
+ ROOT_CONFIG_FILE_NAME,
44
45
  )
45
46
 
46
47
 
@@ -136,6 +137,7 @@ __all__ = [
136
137
  "load_config",
137
138
  "discover_config",
138
139
  "CONFIG_FILE_NAME",
140
+ "ROOT_CONFIG_FILE_NAME",
139
141
  # Detection
140
142
  "detect_cuda_version",
141
143
  "detect_cuda_environment",
comfy_env/cli.py CHANGED
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from typing import List, Optional
7
7
 
8
8
  from . import __version__
9
+ from .config import ROOT_CONFIG_FILE_NAME, CONFIG_FILE_NAME
9
10
 
10
11
 
11
12
  def main(args: Optional[List[str]] = None) -> int:
@@ -14,12 +15,13 @@ def main(args: Optional[List[str]] = None) -> int:
14
15
  sub = parser.add_subparsers(dest="command", help="Commands")
15
16
 
16
17
  # init
17
- p = sub.add_parser("init", help="Create comfy-env.toml")
18
+ p = sub.add_parser("init", help=f"Create {ROOT_CONFIG_FILE_NAME}")
18
19
  p.add_argument("--force", "-f", action="store_true", help="Overwrite existing")
20
+ p.add_argument("--isolated", action="store_true", help=f"Create {CONFIG_FILE_NAME} instead (for isolated folders)")
19
21
 
20
22
  # generate
21
- p = sub.add_parser("generate", help="Generate pixi.toml from comfy-env.toml")
22
- p.add_argument("config", type=str, help="Path to comfy-env.toml")
23
+ p = sub.add_parser("generate", help="Generate pixi.toml from config")
24
+ p.add_argument("config", type=str, help="Path to config file")
23
25
  p.add_argument("--force", "-f", action="store_true", help="Overwrite existing")
24
26
 
25
27
  # install
@@ -62,22 +64,53 @@ def main(args: Optional[List[str]] = None) -> int:
62
64
  return 1
63
65
 
64
66
 
65
- DEFAULT_CONFIG = """\
66
- # comfy-env.toml
67
+ ROOT_DEFAULT_CONFIG = """\
68
+ # comfy-env-root.toml - Main node config
69
+ # PyPI deps go in requirements.txt
70
+
67
71
  [cuda]
68
72
  packages = []
69
73
 
74
+ [apt]
75
+ packages = []
76
+
77
+ [dependencies]
78
+ # cgal = "*"
79
+
80
+ [env_vars]
81
+ # KMP_DUPLICATE_LIB_OK = "TRUE"
82
+
83
+ [node_reqs]
84
+ # ComfyUI_essentials = "cubiq/ComfyUI_essentials"
85
+ """
86
+
87
+ ISOLATED_DEFAULT_CONFIG = """\
88
+ # comfy-env.toml - Isolated folder config
89
+ python = "3.11"
90
+
91
+ [dependencies]
92
+ # cgal = "*"
93
+
70
94
  [pypi-dependencies]
71
- # example = "*"
95
+ # trimesh = { version = "*", extras = ["easy"] }
96
+
97
+ [env_vars]
98
+ # SOME_VAR = "value"
72
99
  """
73
100
 
74
101
 
75
102
  def cmd_init(args) -> int:
76
- config_path = Path.cwd() / "comfy-env.toml"
103
+ if getattr(args, 'isolated', False):
104
+ config_path = Path.cwd() / CONFIG_FILE_NAME
105
+ content = ISOLATED_DEFAULT_CONFIG
106
+ else:
107
+ config_path = Path.cwd() / ROOT_CONFIG_FILE_NAME
108
+ content = ROOT_DEFAULT_CONFIG
109
+
77
110
  if config_path.exists() and not args.force:
78
111
  print(f"Already exists: {config_path}\nUse --force to overwrite", file=sys.stderr)
79
112
  return 1
80
- config_path.write_text(DEFAULT_CONFIG)
113
+ config_path.write_text(content)
81
114
  print(f"Created {config_path}")
82
115
  return 0
83
116
 
@@ -170,7 +203,13 @@ def cmd_apt_install(args) -> int:
170
203
  print("apt-install: Linux only", file=sys.stderr)
171
204
  return 1
172
205
 
173
- config_path = Path(args.config).resolve() if args.config else Path.cwd() / "comfy-env.toml"
206
+ # Check root config first, then regular
207
+ if args.config:
208
+ config_path = Path(args.config).resolve()
209
+ else:
210
+ root_path = Path.cwd() / ROOT_CONFIG_FILE_NAME
211
+ config_path = root_path if root_path.exists() else Path.cwd() / CONFIG_FILE_NAME
212
+
174
213
  if not config_path.exists():
175
214
  print(f"Not found: {config_path}", file=sys.stderr)
176
215
  return 1
@@ -1,15 +1,12 @@
1
- """
2
- Config layer - Configuration parsing and types.
3
-
4
- Pure parsing, no side effects.
5
- """
1
+ """Config layer - Configuration parsing and types."""
6
2
 
7
3
  from .types import (
8
4
  ComfyEnvConfig,
9
5
  NodeDependency,
10
- NodeReq, # Backwards compatibility alias
6
+ NodeReq,
11
7
  )
12
8
  from .parser import (
9
+ ROOT_CONFIG_FILE_NAME,
13
10
  CONFIG_FILE_NAME,
14
11
  load_config,
15
12
  discover_config,
@@ -17,11 +14,10 @@ from .parser import (
17
14
  )
18
15
 
19
16
  __all__ = [
20
- # Types
21
17
  "ComfyEnvConfig",
22
18
  "NodeDependency",
23
- "NodeReq", # Backwards compatibility
24
- # Parsing
19
+ "NodeReq",
20
+ "ROOT_CONFIG_FILE_NAME",
25
21
  "CONFIG_FILE_NAME",
26
22
  "load_config",
27
23
  "discover_config",
@@ -8,11 +8,12 @@ import tomli
8
8
 
9
9
  from .types import ComfyEnvConfig, NodeDependency
10
10
 
11
- CONFIG_FILE_NAME = "comfy-env.toml"
11
+ ROOT_CONFIG_FILE_NAME = "comfy-env-root.toml" # Main node config
12
+ CONFIG_FILE_NAME = "comfy-env.toml" # Isolated folder config
12
13
 
13
14
 
14
15
  def load_config(path: Path) -> ComfyEnvConfig:
15
- """Load and parse comfy-env.toml."""
16
+ """Load and parse config file."""
16
17
  path = Path(path)
17
18
  if not path.exists():
18
19
  raise FileNotFoundError(f"Config file not found: {path}")
@@ -20,9 +21,14 @@ def load_config(path: Path) -> ComfyEnvConfig:
20
21
  return parse_config(tomli.load(f))
21
22
 
22
23
 
23
- def discover_config(node_dir: Path) -> Optional[ComfyEnvConfig]:
24
- """Find and load comfy-env.toml from directory."""
25
- config_path = Path(node_dir) / CONFIG_FILE_NAME
24
+ def discover_config(node_dir: Path, root: bool = True) -> Optional[ComfyEnvConfig]:
25
+ """Find and load config from directory. Checks root config first if root=True."""
26
+ node_dir = Path(node_dir)
27
+ if root:
28
+ root_path = node_dir / ROOT_CONFIG_FILE_NAME
29
+ if root_path.exists():
30
+ return load_config(root_path)
31
+ config_path = node_dir / CONFIG_FILE_NAME
26
32
  return load_config(config_path) if config_path.exists() else None
27
33
 
28
34
 
@@ -49,7 +55,6 @@ def parse_config(data: Dict[str, Any]) -> ComfyEnvConfig:
49
55
 
50
56
 
51
57
  def _parse_node_reqs(data: Dict[str, Any]) -> List[NodeDependency]:
52
- """Parse [node_reqs] section."""
53
58
  return [
54
59
  NodeDependency(name=name, repo=value if isinstance(value, str) else value.get("repo", ""))
55
60
  for name, value in data.items()
@@ -10,6 +10,7 @@ from .cache import MARKER_FILE, sanitize_name
10
10
  from .libomp import dedupe_libomp
11
11
 
12
12
  USE_COMFY_ENV_VAR = "USE_COMFY_ENV"
13
+ ROOT_CONFIG_FILE_NAME = "comfy-env-root.toml"
13
14
 
14
15
 
15
16
  def is_comfy_env_enabled() -> bool:
@@ -50,8 +51,10 @@ def setup_env(node_dir: Optional[str] = None) -> None:
50
51
  import inspect
51
52
  node_dir = str(Path(inspect.stack()[1].filename).parent)
52
53
 
53
- # Apply env vars
54
- for k, v in load_env_vars(os.path.join(node_dir, "comfy-env.toml")).items():
54
+ # Apply env vars (check root config first, then regular)
55
+ root_config = os.path.join(node_dir, ROOT_CONFIG_FILE_NAME)
56
+ config = root_config if os.path.exists(root_config) else os.path.join(node_dir, "comfy-env.toml")
57
+ for k, v in load_env_vars(config).items():
55
58
  os.environ[k] = v
56
59
 
57
60
  # Find env: marker -> _env_<name> -> .pixi
comfy_env/install.py CHANGED
@@ -5,7 +5,7 @@ import os
5
5
  from pathlib import Path
6
6
  from typing import Callable, List, Optional, Set, Union
7
7
 
8
- from .config import ComfyEnvConfig, NodeDependency, load_config, discover_config, CONFIG_FILE_NAME
8
+ from .config import ComfyEnvConfig, NodeDependency, load_config, discover_config, CONFIG_FILE_NAME, ROOT_CONFIG_FILE_NAME
9
9
 
10
10
  USE_COMFY_ENV_VAR = "USE_COMFY_ENV"
11
11
 
@@ -20,7 +20,7 @@ def install(
20
20
  log_callback: Optional[Callable[[str], None]] = None,
21
21
  dry_run: bool = False,
22
22
  ) -> bool:
23
- """Install dependencies from comfy-env.toml."""
23
+ """Install dependencies from comfy-env-root.toml or comfy-env.toml."""
24
24
  if node_dir is None:
25
25
  node_dir = Path(inspect.stack()[1].filename).parent.resolve()
26
26
 
@@ -32,10 +32,10 @@ def install(
32
32
  config_path = node_dir / config_path
33
33
  cfg = load_config(config_path)
34
34
  else:
35
- cfg = discover_config(node_dir)
35
+ cfg = discover_config(node_dir, root=True)
36
36
 
37
37
  if cfg is None:
38
- raise FileNotFoundError(f"No comfy-env.toml found in {node_dir}")
38
+ raise FileNotFoundError(f"No {ROOT_CONFIG_FILE_NAME} or {CONFIG_FILE_NAME} found in {node_dir}")
39
39
 
40
40
  if cfg.apt_packages: _install_apt_packages(cfg.apt_packages, log, dry_run)
41
41
  if cfg.env_vars: _set_persistent_env_vars(cfg.env_vars, log, dry_run)
@@ -168,8 +168,12 @@ def _install_via_pixi(cfg: ComfyEnvConfig, node_dir: Path, log: Callable[[str],
168
168
  if result.returncode != 0:
169
169
  raise RuntimeError(f"Failed: {result.stderr}")
170
170
 
171
+ # Find config file for marker
172
+ config_path = node_dir / CONFIG_FILE_NAME
173
+ if not config_path.exists():
174
+ config_path = node_dir / ROOT_CONFIG_FILE_NAME
175
+
171
176
  old_env = node_dir / ".pixi" / "envs" / "default"
172
- config_path = node_dir / "comfy-env.toml"
173
177
  main_node_dir = node_dir
174
178
  for parent in node_dir.parents:
175
179
  if parent.parent.name == "custom_nodes":
@@ -229,8 +233,9 @@ def _install_to_host_python(cfg: ComfyEnvConfig, node_dir: Path, log: Callable[[
229
233
 
230
234
 
231
235
  def _install_isolated_subdirs(node_dir: Path, log: Callable[[str], None], dry_run: bool) -> None:
236
+ """Find and install comfy-env.toml in subdirectories (isolated folders only)."""
232
237
  for config_file in node_dir.rglob(CONFIG_FILE_NAME):
233
- if config_file.parent == node_dir: continue
238
+ if config_file.parent == node_dir: continue # Skip root
234
239
  log(f"\n[isolated] {config_file.parent.relative_to(node_dir)}")
235
240
  if not dry_run:
236
241
  _install_via_pixi(load_config(config_file), config_file.parent, log, dry_run)
@@ -193,6 +193,32 @@ from multiprocessing import shared_memory as shm
193
193
  import numpy as np
194
194
 
195
195
 
196
+ def _prepare_trimesh_for_pickle(mesh):
197
+ """
198
+ Prepare a trimesh object for cross-Python-version pickling.
199
+
200
+ Trimesh attaches helper objects (ray tracer, proximity query) that may use
201
+ native extensions like embreex. These cause import errors when unpickling
202
+ on a system without those extensions. We strip them - they'll be recreated
203
+ lazily when needed.
204
+
205
+ Note: Do NOT strip _cache - trimesh needs it to function properly.
206
+ """
207
+ # Make a copy to avoid modifying the original
208
+ mesh = mesh.copy()
209
+
210
+ # Remove helper objects that may have unpickleable native code references
211
+ # These are lazily recreated on first access anyway
212
+ # Do NOT remove _cache - it's needed for trimesh to work
213
+ for attr in ('ray', '_ray', 'permutate', 'nearest'):
214
+ try:
215
+ delattr(mesh, attr)
216
+ except AttributeError:
217
+ pass
218
+
219
+ return mesh
220
+
221
+
196
222
  def _to_shm(obj, registry, visited=None):
197
223
  """
198
224
  Serialize object to shared memory. Returns JSON-safe metadata.
@@ -231,6 +257,7 @@ def _to_shm(obj, registry, visited=None):
231
257
  # trimesh.Trimesh -> pickle -> shared memory (preserves visual, metadata, normals)
232
258
  if t == 'Trimesh':
233
259
  import pickle
260
+ obj = _prepare_trimesh_for_pickle(obj)
234
261
  mesh_bytes = pickle.dumps(obj)
235
262
 
236
263
  block = shm.SharedMemory(create=True, size=len(mesh_bytes))
@@ -533,6 +560,7 @@ def _to_shm(obj, registry, visited=None):
533
560
  # trimesh.Trimesh -> pickle -> shared memory (preserves visual, metadata, normals)
534
561
  if t == 'Trimesh':
535
562
  import pickle
563
+ obj = _prepare_trimesh_for_pickle(obj)
536
564
  mesh_bytes = pickle.dumps(obj)
537
565
 
538
566
  block = shm.SharedMemory(create=True, size=len(mesh_bytes))
@@ -642,31 +670,6 @@ def _should_use_reference(obj):
642
670
  # Everything else (custom classes) - pass by reference
643
671
  return True
644
672
 
645
- def _prepare_trimesh_for_pickle(mesh):
646
- """
647
- Prepare a trimesh object for cross-Python-version pickling.
648
-
649
- Trimesh attaches helper objects (ray tracer, proximity query) that may use
650
- native extensions like embreex. These cause import errors when unpickling
651
- on a system without those extensions. We strip them - they'll be recreated
652
- lazily when needed.
653
-
654
- Note: Do NOT strip _cache - trimesh needs it to function properly.
655
- """
656
- # Make a copy to avoid modifying the original
657
- mesh = mesh.copy()
658
-
659
- # Remove helper objects that may have unpickleable native code references
660
- # These are lazily recreated on first access anyway
661
- # Do NOT remove _cache - it's needed for trimesh to work
662
- for attr in ('ray', '_ray', 'permutate', 'nearest'):
663
- try:
664
- delattr(mesh, attr)
665
- except AttributeError:
666
- pass
667
-
668
- return mesh
669
-
670
673
 
671
674
  def _serialize_result(obj, visited=None):
672
675
  """Convert result for IPC - complex objects become references."""
@@ -5,7 +5,6 @@ Handles pixi, CUDA wheels, apt packages, and node dependencies.
5
5
  """
6
6
 
7
7
  from .pixi import (
8
- PIXI_VERSION,
9
8
  ensure_pixi,
10
9
  get_pixi_path,
11
10
  get_pixi_python,
@@ -35,7 +34,6 @@ from .node_dependencies import (
35
34
 
36
35
  __all__ = [
37
36
  # Pixi package manager
38
- "PIXI_VERSION",
39
37
  "ensure_pixi",
40
38
  "get_pixi_path",
41
39
  "get_pixi_python",
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.4
2
+ Name: comfy-env
3
+ Version: 0.1.18
4
+ Summary: Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation
5
+ Project-URL: Homepage, https://github.com/PozzettiAndrea/comfy-env
6
+ Project-URL: Repository, https://github.com/PozzettiAndrea/comfy-env
7
+ Project-URL: Issues, https://github.com/PozzettiAndrea/comfy-env/issues
8
+ Author: Andrea Pozzetti
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: comfyui,cuda,environment,isolation,process,venv,wheels
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: pip>=21.0
21
+ Requires-Dist: tomli-w>=1.0.0
22
+ Requires-Dist: tomli>=2.0.0
23
+ Requires-Dist: uv>=0.4.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy; extra == 'dev'
26
+ Requires-Dist: pytest; extra == 'dev'
27
+ Requires-Dist: ruff; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # comfy-env
31
+
32
+ Environment management for ComfyUI custom nodes.
33
+
34
+ ## Quick Start
35
+
36
+ ```bash
37
+ pip install comfy-env
38
+ ```
39
+
40
+ **1. Create `comfy-env-root.toml` in your node directory:**
41
+
42
+ ```toml
43
+ [cuda]
44
+ packages = ["nvdiffrast", "pytorch3d"]
45
+
46
+ [apt]
47
+ packages = ["libgl1-mesa-glx"]
48
+
49
+ [node_reqs]
50
+ ComfyUI_essentials = "cubiq/ComfyUI_essentials"
51
+ ```
52
+
53
+ PyPI deps go in `requirements.txt` (standard ComfyUI pattern).
54
+
55
+ **2. In `install.py`:**
56
+
57
+ ```python
58
+ from comfy_env import install
59
+ install()
60
+ ```
61
+
62
+ **3. In `prestartup_script.py`:**
63
+
64
+ ```python
65
+ from comfy_env import setup_env
66
+ setup_env()
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Two Config Files
72
+
73
+ | File | Purpose |
74
+ |------|---------|
75
+ | `comfy-env-root.toml` | Main node config (root level) |
76
+ | `comfy-env.toml` | Isolated subfolder config |
77
+
78
+ ### comfy-env-root.toml (main node)
79
+
80
+ ```toml
81
+ [cuda]
82
+ packages = ["nvdiffrast", "pytorch3d"]
83
+
84
+ [apt]
85
+ packages = ["libgl1-mesa-glx"]
86
+
87
+ [dependencies]
88
+ cgal = "*"
89
+
90
+ [env_vars]
91
+ KMP_DUPLICATE_LIB_OK = "TRUE"
92
+
93
+ [node_reqs]
94
+ ComfyUI_essentials = "cubiq/ComfyUI_essentials"
95
+ ```
96
+
97
+ PyPI deps → `requirements.txt`
98
+
99
+ ### comfy-env.toml (isolated folder)
100
+
101
+ ```toml
102
+ python = "3.11"
103
+
104
+ [dependencies]
105
+ cgal = "*"
106
+
107
+ [pypi-dependencies]
108
+ trimesh = { version = "*", extras = ["easy"] }
109
+
110
+ [env_vars]
111
+ SOME_VAR = "value"
112
+ ```
113
+
114
+ ### What goes where?
115
+
116
+ | Section | Root | Isolated |
117
+ |---------|------|----------|
118
+ | `[cuda]` | ✓ | ✓ |
119
+ | `[apt]` | ✓ | ✓ |
120
+ | `[dependencies]` | ✓ | ✓ |
121
+ | `[env_vars]` | ✓ | ✓ |
122
+ | `[node_reqs]` | ✓ | ✗ |
123
+ | `python = "X.Y"` | ✗ | ✓ |
124
+ | `[pypi-dependencies]` | ✗ | ✓ |
125
+
126
+ ---
127
+
128
+ ## Process Isolation
129
+
130
+ For nodes with conflicting dependencies:
131
+
132
+ ```python
133
+ # In nodes/__init__.py
134
+ from pathlib import Path
135
+ from comfy_env import wrap_isolated_nodes
136
+
137
+ from .cgal import NODE_CLASS_MAPPINGS as cgal_mappings
138
+
139
+ NODE_CLASS_MAPPINGS = wrap_isolated_nodes(
140
+ cgal_mappings,
141
+ Path(__file__).parent / "cgal" # Has comfy-env.toml
142
+ )
143
+ ```
144
+
145
+ Each wrapped node runs in a subprocess with its own Python environment.
146
+
147
+ ---
148
+
149
+ ## CLI
150
+
151
+ ```bash
152
+ comfy-env init # Create comfy-env-root.toml
153
+ comfy-env init --isolated # Create comfy-env.toml (for subfolders)
154
+ comfy-env install # Install dependencies
155
+ comfy-env install --dry-run # Preview
156
+ comfy-env info # Show runtime info
157
+ comfy-env doctor # Verify packages
158
+ comfy-env apt-install # Install system packages
159
+ ```
160
+
161
+ ---
162
+
163
+ ## API
164
+
165
+ ### install()
166
+
167
+ ```python
168
+ from comfy_env import install
169
+ install()
170
+ ```
171
+
172
+ ### setup_env()
173
+
174
+ ```python
175
+ from comfy_env import setup_env
176
+ setup_env() # Call in prestartup_script.py
177
+ ```
178
+
179
+ ### wrap_isolated_nodes()
180
+
181
+ ```python
182
+ from comfy_env import wrap_isolated_nodes
183
+ wrapped = wrap_isolated_nodes(NODE_CLASS_MAPPINGS, node_dir)
184
+ ```
185
+
186
+ ### Detection
187
+
188
+ ```python
189
+ from comfy_env import RuntimeEnv, detect_cuda_version, detect_gpu
190
+
191
+ env = RuntimeEnv.detect()
192
+ print(env) # Python 3.11, CUDA 12.8, PyTorch 2.8.0, GPU: RTX 4090
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Example
198
+
199
+ See [ComfyUI-GeometryPack](https://github.com/PozzettiAndrea/ComfyUI-GeometryPack):
200
+
201
+ - Multiple isolated environments (CGAL, Blender, GPU)
202
+ - Per-subdirectory `comfy-env.toml`
203
+ - Different Python versions
204
+
205
+ ---
206
+
207
+ ## Why?
208
+
209
+ **Why isolation?** ComfyUI nodes share one Python. Conflicts happen when:
210
+ - Node A needs torch 2.4, Node B needs torch 2.8
211
+ - Two packages bundle incompatible libomp
212
+ - Blender API requires Python 3.11
213
+
214
+ **Why CUDA wheels?** Installing nvdiffrast normally needs CUDA toolkit + C++ compiler + 30 min compilation. [cuda-wheels](https://pozzettiandrea.github.io/cuda-wheels/) provides pre-built wheels.
215
+
216
+ **How envs work:**
217
+ - Central cache: `~/.comfy-env/envs/`
218
+ - Marker files link nodes → cached envs
219
+ - Config hash in name → changes create new envs
220
+
221
+ ---
222
+
223
+ ## License
224
+
225
+ MIT
@@ -1,8 +1,8 @@
1
- comfy_env/__init__.py,sha256=uoHq-m1274tOeinWsIpW81MkRQilHx9z8_coJSqH9qc,4641
2
- comfy_env/cli.py,sha256=abdUBItk8OkzxKBu7cKRPizO5_i5DFflvx7pYpPc7OM,6696
3
- comfy_env/install.py,sha256=BV8OfY3Rt-BiKFzpDNHfs4mfLYalbsiIMNU-y9CmLDg,10467
4
- comfy_env/config/__init__.py,sha256=noqhycE0UF4ZntV5NnWcFApppQludQDHvJOtLsQgUIo,542
5
- comfy_env/config/parser.py,sha256=79C8LIwCO4920MU7CJ08hfMWh-5Ehzr-i5XQna5K81A,1951
1
+ comfy_env/__init__.py,sha256=7R1WnVv6Rmq97bbK2Fvp1A50jkFwxIgcmfiILit9k8E,4666
2
+ comfy_env/cli.py,sha256=SWErVa3lB2ZHc7aNJJgWElWCRMsCzVLP_CrupuOk_zw,7684
3
+ comfy_env/install.py,sha256=CT3A3kIVQqXGA5nfp7jxTU_gHdsood4NsjW0tbXv7_8,10781
4
+ comfy_env/config/__init__.py,sha256=QlxIc5Hdghje6cm4FutReMO6fQK5rBu-zr36V2fjcLE,474
5
+ comfy_env/config/parser.py,sha256=bNSy8Jn4VZg3xw8pldlkEyGaaI_BmNsUEYQz-hKiXgE,2221
6
6
  comfy_env/config/types.py,sha256=vFgWeEl_p26OmcoUv0wAOlHe9GBio2isjIWl7kACKFY,1096
7
7
  comfy_env/detection/__init__.py,sha256=dH84PlSRJfs8MRJp2gp9_NX8ZzGIDHR8iZXy7_B8Ez4,1671
8
8
  comfy_env/detection/cuda.py,sha256=BOaRQOGP2yoaPCO9eqPvWBB5Us_MNo-sSbadQsIjHqM,1708
@@ -13,15 +13,15 @@ comfy_env/environment/__init__.py,sha256=WfZnyOvbI0MrDQPYTtOG2kHn0XCSCrqKcOlJcmB
13
13
  comfy_env/environment/cache.py,sha256=RGfVW2caMO0Dd1nX2otUQP0xW3pVS7iSOP4vIUAMdEA,4568
14
14
  comfy_env/environment/libomp.py,sha256=nzr3kDnRLgcf9CZ_WF4ItWskqEDS2S0geqZS43XoKig,1319
15
15
  comfy_env/environment/paths.py,sha256=5TFFAkOZXa8R3cHfVHDEFnwy6_JcHilVBOHJuy-yqR0,1129
16
- comfy_env/environment/setup.py,sha256=34To-cJX85sZ5W33dxcNosedrrICNUzVzqBcJkq4FLI,3013
16
+ comfy_env/environment/setup.py,sha256=KQgeqlEaqB_tOVhsR2RQF76-LuPud2EPtkQWUM3AJ5Y,3231
17
17
  comfy_env/isolation/__init__.py,sha256=XfMLEiBIcEzHG_k2vk9fT9GvFfmOsfbpM26czuxbdRI,800
18
18
  comfy_env/isolation/tensor_utils.py,sha256=2_f4jjylqCPaPldD1Jw-es5CyOtuF5I1ROdyEIxsg-U,2951
19
19
  comfy_env/isolation/wrap.py,sha256=K7GAkqU_Uxe717eUtPsFv5kcr_Jfbh3x79A-8vbY1nY,8592
20
20
  comfy_env/isolation/workers/__init__.py,sha256=Zp6sZSRBcb5Negqgzqs3jPjfO9T1u3nNrQhp6WqTAuc,325
21
21
  comfy_env/isolation/workers/base.py,sha256=4ZYTaQ4J0kBHCoO_OfZnsowm4rJCoqinZUaOtgkOPbw,2307
22
22
  comfy_env/isolation/workers/mp.py,sha256=ygOgx2iyLN7l5fWkKI4lqzQsDyfAAd9Gb4gTYLp7o1A,34061
23
- comfy_env/isolation/workers/subprocess.py,sha256=ML6I9IenReagP8iT0Cd2ipet6JPK1gnDbOianOuFwOw,57164
24
- comfy_env/packages/__init__.py,sha256=6PTwUfUdJDTbIw46dCiA42qk4zUe_gw29xOaklBiMMc,1193
23
+ comfy_env/isolation/workers/subprocess.py,sha256=4qKtzla0zfVvzGW6y-2PyeFiL-aSQsYxK6Qk8LUKYUQ,57262
24
+ comfy_env/packages/__init__.py,sha256=4pRCUnfcVFVgy7hkbPz9BPVXELtSFHha6L7n-hqNuZA,1155
25
25
  comfy_env/packages/apt.py,sha256=pxy3A5ZHv3X8ExCVyohODY8Fcy9ji4izIVPfYoxhqT4,1027
26
26
  comfy_env/packages/cuda_wheels.py,sha256=G_CnlwNcfeWlEU24aCVBpeqQQ05y8_02dDLBwBFNwII,3980
27
27
  comfy_env/packages/node_dependencies.py,sha256=AX_CY6j43tTY5KhyPfU7Wz6zgLAfWF0o0JkTrcNSecg,2966
@@ -29,8 +29,8 @@ comfy_env/packages/pixi.py,sha256=RPu8x5sSOLE1CYAhWMMjoQrbFGGt00fdsbqtRcTz7LQ,38
29
29
  comfy_env/packages/toml_generator.py,sha256=Vhc8F9euHhMTwH1TV6t96-D9Pjrn9jIN4e9WXrCIFE8,3414
30
30
  comfy_env/templates/comfy-env-instructions.txt,sha256=ve1RAthW7ouumU9h6DM7mIRX1MS8_Tyonq2U4tcrFu8,1031
31
31
  comfy_env/templates/comfy-env.toml,sha256=ROIqi4BlPL1MEdL1VgebfTHpdwPNYGHwWeigI9Kw-1I,4831
32
- comfy_env-0.1.16.dist-info/METADATA,sha256=SLbnLOFz5AbqS8YT9-TWslRzo4QghO2EwUtQwn2B0OE,6468
33
- comfy_env-0.1.16.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
- comfy_env-0.1.16.dist-info/entry_points.txt,sha256=J4fXeqgxU_YenuW_Zxn_pEL7J-3R0--b6MS5t0QmAr0,49
35
- comfy_env-0.1.16.dist-info/licenses/LICENSE,sha256=E68QZMMpW4P2YKstTZ3QU54HRQO8ecew09XZ4_Vn870,1093
36
- comfy_env-0.1.16.dist-info/RECORD,,
32
+ comfy_env-0.1.18.dist-info/METADATA,sha256=u07OMiP4riErJHu-44eSuN-gfL3iYh9dEDjBNDcQPLg,4808
33
+ comfy_env-0.1.18.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
+ comfy_env-0.1.18.dist-info/entry_points.txt,sha256=J4fXeqgxU_YenuW_Zxn_pEL7J-3R0--b6MS5t0QmAr0,49
35
+ comfy_env-0.1.18.dist-info/licenses/LICENSE,sha256=E68QZMMpW4P2YKstTZ3QU54HRQO8ecew09XZ4_Vn870,1093
36
+ comfy_env-0.1.18.dist-info/RECORD,,
@@ -1,279 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: comfy-env
3
- Version: 0.1.16
4
- Summary: Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation
5
- Project-URL: Homepage, https://github.com/PozzettiAndrea/comfy-env
6
- Project-URL: Repository, https://github.com/PozzettiAndrea/comfy-env
7
- Project-URL: Issues, https://github.com/PozzettiAndrea/comfy-env/issues
8
- Author: Andrea Pozzetti
9
- License: MIT
10
- License-File: LICENSE
11
- Keywords: comfyui,cuda,environment,isolation,process,venv,wheels
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: MIT License
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Programming Language :: Python :: 3.13
19
- Requires-Python: >=3.10
20
- Requires-Dist: pip>=21.0
21
- Requires-Dist: tomli-w>=1.0.0
22
- Requires-Dist: tomli>=2.0.0
23
- Requires-Dist: uv>=0.4.0
24
- Provides-Extra: dev
25
- Requires-Dist: mypy; extra == 'dev'
26
- Requires-Dist: pytest; extra == 'dev'
27
- Requires-Dist: ruff; extra == 'dev'
28
- Description-Content-Type: text/markdown
29
-
30
- # comfy-env
31
-
32
- Environment management for ComfyUI custom nodes.
33
-
34
- ## Quick Start
35
-
36
- ```bash
37
- pip install comfy-env
38
- ```
39
-
40
- **1. Create `comfy-env.toml` in your node directory:**
41
-
42
- ```toml
43
- [cuda]
44
- packages = ["nvdiffrast", "pytorch3d"]
45
-
46
- [pypi-dependencies]
47
- trimesh = { version = "*", extras = ["easy"] }
48
- ```
49
-
50
- **2. In `install.py`:**
51
-
52
- ```python
53
- from comfy_env import install
54
- install()
55
- ```
56
-
57
- **3. In `prestartup_script.py`:**
58
-
59
- ```python
60
- from comfy_env import setup_env
61
- setup_env()
62
- ```
63
-
64
- That's it. CUDA wheels install without compilation, and the environment is ready.
65
-
66
- ---
67
-
68
- ## Configuration
69
-
70
- Create `comfy-env.toml` in your node directory:
71
-
72
- ```toml
73
- # Python version for isolated environment (optional)
74
- python = "3.11"
75
-
76
- # CUDA packages from cuda-wheels index (no compilation needed)
77
- [cuda]
78
- packages = ["nvdiffrast", "pytorch3d", "flash-attn"]
79
-
80
- # System packages (Linux only)
81
- [apt]
82
- packages = ["libgl1-mesa-glx", "libglu1-mesa"]
83
-
84
- # Environment variables
85
- [env_vars]
86
- KMP_DUPLICATE_LIB_OK = "TRUE"
87
- OMP_NUM_THREADS = "1"
88
-
89
- # Dependent custom nodes to auto-install
90
- [node_reqs]
91
- ComfyUI_essentials = "cubiq/ComfyUI_essentials"
92
-
93
- # Conda packages (via pixi)
94
- [dependencies]
95
- cgal = "*"
96
-
97
- # PyPI packages
98
- [pypi-dependencies]
99
- trimesh = { version = "*", extras = ["easy"] }
100
- numpy = "*"
101
- ```
102
-
103
- ---
104
-
105
- ## Process Isolation
106
-
107
- For nodes with conflicting dependencies, use isolated execution:
108
-
109
- ```python
110
- # In nodes/__init__.py
111
- from pathlib import Path
112
- from comfy_env import wrap_isolated_nodes
113
-
114
- # Import your isolated nodes
115
- from .cgal import NODE_CLASS_MAPPINGS as cgal_mappings
116
-
117
- # Wrap them for isolated execution
118
- NODE_CLASS_MAPPINGS = wrap_isolated_nodes(
119
- cgal_mappings,
120
- Path(__file__).parent / "cgal" # Directory with comfy-env.toml
121
- )
122
- ```
123
-
124
- Each wrapped node runs in a subprocess with its own Python environment.
125
-
126
- ---
127
-
128
- ## CLI Commands
129
-
130
- ```bash
131
- # Show detected environment
132
- comfy-env info
133
-
134
- # Install dependencies
135
- comfy-env install
136
-
137
- # Preview without installing
138
- comfy-env install --dry-run
139
-
140
- # Verify packages
141
- comfy-env doctor
142
-
143
- # Install system packages
144
- comfy-env apt-install
145
- ```
146
-
147
- ---
148
-
149
- ## API Reference
150
-
151
- ### install()
152
-
153
- Install dependencies from comfy-env.toml:
154
-
155
- ```python
156
- from comfy_env import install
157
-
158
- install() # Auto-detect config
159
- install(dry_run=True) # Preview only
160
- install(config="path.toml") # Explicit config
161
- ```
162
-
163
- ### setup_env()
164
-
165
- Set up environment at ComfyUI startup:
166
-
167
- ```python
168
- from comfy_env import setup_env
169
-
170
- setup_env() # Auto-detects node directory from caller
171
- ```
172
-
173
- Sets library paths, environment variables, and injects site-packages.
174
-
175
- ### wrap_isolated_nodes()
176
-
177
- Wrap nodes for subprocess isolation:
178
-
179
- ```python
180
- from comfy_env import wrap_isolated_nodes
181
-
182
- wrapped = wrap_isolated_nodes(NODE_CLASS_MAPPINGS, node_dir)
183
- ```
184
-
185
- ### Detection
186
-
187
- ```python
188
- from comfy_env import (
189
- detect_cuda_version, # Returns "12.8", "12.4", or None
190
- detect_gpu, # Returns GPUInfo or None
191
- get_gpu_summary, # Human-readable string
192
- RuntimeEnv, # Combined runtime info
193
- )
194
-
195
- env = RuntimeEnv.detect()
196
- print(env) # Python 3.11, CUDA 12.8, PyTorch 2.8.0, GPU: RTX 4090
197
- ```
198
-
199
- ### Workers
200
-
201
- Low-level process isolation:
202
-
203
- ```python
204
- from comfy_env import MPWorker, SubprocessWorker
205
-
206
- # Same Python version (multiprocessing)
207
- worker = MPWorker()
208
- result = worker.call(my_function, arg1, arg2)
209
-
210
- # Different Python version (subprocess)
211
- worker = SubprocessWorker(python="/path/to/python")
212
- result = worker.call(my_function, arg1, arg2)
213
- ```
214
-
215
- ---
216
-
217
- ## Real Example
218
-
219
- See [ComfyUI-GeometryPack](https://github.com/PozzettiAndrea/ComfyUI-GeometryPack) for a production example with:
220
-
221
- - Multiple isolated environments (CGAL, Blender, GPU)
222
- - Per-subdirectory comfy-env.toml
223
- - Prestartup asset copying
224
- - Different Python versions (3.11 for Blender API)
225
-
226
- ---
227
-
228
- ## Architecture
229
-
230
- ### Layers
231
-
232
- ```
233
- comfy_env/
234
- ├── detection/ # Pure functions - CUDA, GPU, platform detection
235
- ├── config/ # Pure parsing - comfy-env.toml → typed config
236
- ├── environment/ # Side effects - cache, paths, setup
237
- ├── packages/ # Side effects - pixi, cuda-wheels, apt
238
- ├── isolation/ # Side effects - subprocess workers, node wrapping
239
- └── install.py # Orchestration
240
- ```
241
-
242
- ### Why Isolation?
243
-
244
- ComfyUI nodes share a single Python environment. This breaks when:
245
-
246
- 1. **Dependency conflicts**: Node A needs `torch==2.4`, Node B needs `torch==2.8`
247
- 2. **Native library conflicts**: Two packages bundle incompatible libomp
248
- 3. **Python version requirements**: Blender API requires Python 3.11
249
-
250
- Solution: Run each node group in its own subprocess with isolated dependencies.
251
-
252
- ### Why CUDA Wheels?
253
-
254
- Installing packages like `nvdiffrast` normally requires:
255
- - CUDA toolkit
256
- - C++ compiler
257
- - 30+ minutes of compilation
258
-
259
- CUDA wheels from [cuda-wheels](https://pozzettiandrea.github.io/cuda-wheels/) are pre-built for common configurations:
260
-
261
- | GPU | CUDA | PyTorch |
262
- |-----|------|---------|
263
- | Blackwell (sm_100+) | 12.8 | 2.8 |
264
- | Ada/Hopper/Ampere | 12.8 | 2.8 |
265
- | Turing | 12.8 | 2.8 |
266
- | Pascal | 12.4 | 2.4 |
267
-
268
- ### How Environments Work
269
-
270
- 1. **Central cache**: Environments stored at `~/.comfy-env/envs/`
271
- 2. **Marker files**: `.comfy-env-marker.toml` links node → env
272
- 3. **Orphan cleanup**: Envs deleted when their node is removed
273
- 4. **Hash-based naming**: Config changes create new envs
274
-
275
- ---
276
-
277
- ## License
278
-
279
- MIT