comfy-env 0.1.15__py3-none-any.whl → 0.1.17__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.
Files changed (50) hide show
  1. comfy_env/__init__.py +117 -40
  2. comfy_env/cli.py +122 -311
  3. comfy_env/config/__init__.py +12 -4
  4. comfy_env/config/parser.py +30 -79
  5. comfy_env/config/types.py +37 -0
  6. comfy_env/detection/__init__.py +77 -0
  7. comfy_env/detection/cuda.py +61 -0
  8. comfy_env/detection/gpu.py +230 -0
  9. comfy_env/detection/platform.py +70 -0
  10. comfy_env/detection/runtime.py +103 -0
  11. comfy_env/environment/__init__.py +53 -0
  12. comfy_env/environment/cache.py +141 -0
  13. comfy_env/environment/libomp.py +41 -0
  14. comfy_env/environment/paths.py +38 -0
  15. comfy_env/environment/setup.py +91 -0
  16. comfy_env/install.py +134 -331
  17. comfy_env/isolation/__init__.py +32 -2
  18. comfy_env/isolation/tensor_utils.py +83 -0
  19. comfy_env/isolation/workers/__init__.py +16 -0
  20. comfy_env/{workers → isolation/workers}/mp.py +1 -1
  21. comfy_env/{workers → isolation/workers}/subprocess.py +1 -1
  22. comfy_env/isolation/wrap.py +128 -509
  23. comfy_env/packages/__init__.py +60 -0
  24. comfy_env/packages/apt.py +36 -0
  25. comfy_env/packages/cuda_wheels.py +97 -0
  26. comfy_env/packages/node_dependencies.py +77 -0
  27. comfy_env/packages/pixi.py +85 -0
  28. comfy_env/packages/toml_generator.py +88 -0
  29. comfy_env-0.1.17.dist-info/METADATA +225 -0
  30. comfy_env-0.1.17.dist-info/RECORD +36 -0
  31. comfy_env/cache.py +0 -203
  32. comfy_env/nodes.py +0 -187
  33. comfy_env/pixi/__init__.py +0 -48
  34. comfy_env/pixi/core.py +0 -587
  35. comfy_env/pixi/cuda_detection.py +0 -303
  36. comfy_env/pixi/platform/__init__.py +0 -21
  37. comfy_env/pixi/platform/base.py +0 -96
  38. comfy_env/pixi/platform/darwin.py +0 -53
  39. comfy_env/pixi/platform/linux.py +0 -68
  40. comfy_env/pixi/platform/windows.py +0 -284
  41. comfy_env/pixi/resolver.py +0 -198
  42. comfy_env/prestartup.py +0 -208
  43. comfy_env/workers/__init__.py +0 -38
  44. comfy_env/workers/tensor_utils.py +0 -188
  45. comfy_env-0.1.15.dist-info/METADATA +0 -291
  46. comfy_env-0.1.15.dist-info/RECORD +0 -31
  47. /comfy_env/{workers → isolation/workers}/base.py +0 -0
  48. {comfy_env-0.1.15.dist-info → comfy_env-0.1.17.dist-info}/WHEEL +0 -0
  49. {comfy_env-0.1.15.dist-info → comfy_env-0.1.17.dist-info}/entry_points.txt +0 -0
  50. {comfy_env-0.1.15.dist-info → comfy_env-0.1.17.dist-info}/licenses/LICENSE +0 -0
@@ -1,28 +1,7 @@
1
- """
2
- Process isolation for ComfyUI node packs.
3
-
4
- This module provides wrap_isolated_nodes() which wraps node classes
5
- to run their FUNCTION methods in an isolated Python environment.
6
-
7
- Usage:
8
- # In your node pack's __init__.py:
9
- from pathlib import Path
10
- from comfy_env import wrap_isolated_nodes
11
-
12
- NODE_CLASS_MAPPINGS = {}
13
-
14
- # Main nodes (no isolation)
15
- from .nodes.main import NODE_CLASS_MAPPINGS as main_nodes
16
- NODE_CLASS_MAPPINGS.update(main_nodes)
17
-
18
- # Isolated nodes (has comfy-env.toml in that directory)
19
- from .nodes.isolated import NODE_CLASS_MAPPINGS as isolated_nodes
20
- NODE_CLASS_MAPPINGS.update(
21
- wrap_isolated_nodes(isolated_nodes, Path(__file__).parent / "nodes/isolated")
22
- )
23
- """
1
+ """Process isolation for ComfyUI nodes - wraps FUNCTION methods to run in isolated env."""
24
2
 
25
3
  import atexit
4
+ import glob
26
5
  import inspect
27
6
  import os
28
7
  import sys
@@ -31,529 +10,194 @@ from functools import wraps
31
10
  from pathlib import Path
32
11
  from typing import Any, Dict, Optional
33
12
 
34
- # Debug logging (set COMFY_ENV_DEBUG=1 to enable)
35
13
  _DEBUG = os.environ.get("COMFY_ENV_DEBUG", "").lower() in ("1", "true", "yes")
14
+ _workers: Dict[str, Any] = {}
15
+ _workers_lock = threading.Lock()
36
16
 
37
17
 
38
- def get_env_name(dir_name: str) -> str:
39
- """Convert directory name to env name: ComfyUI-UniRig -> _env_unirig"""
40
- name = dir_name.lower().replace("-", "_").lstrip("comfyui_")
41
- return f"_env_{name}"
18
+ def _is_enabled() -> bool:
19
+ return os.environ.get("USE_COMFY_ENV", "1").lower() not in ("0", "false", "no", "off")
42
20
 
43
- # Global worker cache (one per isolated environment)
44
- _workers: Dict[str, Any] = {}
45
- _workers_lock = threading.Lock()
46
21
 
22
+ def _env_name(dir_name: str) -> str:
23
+ return f"_env_{dir_name.lower().replace('-', '_').lstrip('comfyui_')}"
47
24
 
48
- def _get_isolated_python_version(env_dir: Path) -> Optional[str]:
49
- """Get Python version from isolated environment."""
25
+
26
+ def _get_env_paths(env_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
27
+ """Get (site_packages, lib_dir) from env."""
50
28
  if sys.platform == "win32":
51
- python_path = env_dir / "python.exe"
29
+ sp = env_dir / "Lib" / "site-packages"
30
+ lib = env_dir / "Library" / "bin"
52
31
  else:
53
- python_path = env_dir / "bin" / "python"
32
+ matches = glob.glob(str(env_dir / "lib/python*/site-packages"))
33
+ sp = Path(matches[0]) if matches else None
34
+ lib = env_dir / "lib"
35
+ return (sp, lib) if sp and sp.exists() else (None, None)
54
36
 
55
- if not python_path.exists():
56
- return None
57
37
 
58
- import subprocess
59
- try:
60
- result = subprocess.run(
61
- [str(python_path), "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
62
- capture_output=True, text=True, timeout=5
63
- )
64
- if result.returncode == 0:
65
- return result.stdout.strip()
66
- except Exception:
67
- pass
38
+ def _find_env_dir(node_dir: Path) -> Optional[Path]:
39
+ """Find env dir: marker -> _env_<name> -> .pixi -> .venv"""
40
+ marker = node_dir / ".comfy-env-marker.toml"
41
+ if marker.exists():
42
+ try:
43
+ import tomli
44
+ with open(marker, "rb") as f:
45
+ env_path = tomli.load(f).get("env", {}).get("path")
46
+ if env_path and Path(env_path).exists():
47
+ return Path(env_path)
48
+ except Exception: pass
49
+
50
+ for candidate in [node_dir / _env_name(node_dir.name),
51
+ node_dir / ".pixi/envs/default",
52
+ node_dir / ".venv"]:
53
+ if candidate.exists(): return candidate
68
54
  return None
69
55
 
70
56
 
71
- def _get_worker(
72
- env_dir: Path,
73
- working_dir: Path,
74
- sys_path: list[str],
75
- lib_path: Optional[str] = None,
76
- env_vars: Optional[dict] = None,
77
- ):
78
- """Get or create a persistent worker for the isolated environment."""
79
- cache_key = str(env_dir)
57
+ def _get_python_version(env_dir: Path) -> Optional[str]:
58
+ python = env_dir / ("python.exe" if sys.platform == "win32" else "bin/python")
59
+ if not python.exists(): return None
60
+ try:
61
+ import subprocess
62
+ r = subprocess.run([str(python), "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
63
+ capture_output=True, text=True, timeout=5)
64
+ return r.stdout.strip() if r.returncode == 0 else None
65
+ except Exception: return None
66
+
80
67
 
68
+ def _get_worker(env_dir: Path, working_dir: Path, sys_path: list[str],
69
+ lib_path: Optional[str] = None, env_vars: Optional[dict] = None):
70
+ cache_key = str(env_dir)
81
71
  with _workers_lock:
82
- if cache_key in _workers:
83
- worker = _workers[cache_key]
84
- if worker.is_alive():
85
- return worker
86
- # Worker died, will recreate
87
-
88
- # Check if Python versions match
89
- host_version = f"{sys.version_info.major}.{sys.version_info.minor}"
90
- isolated_version = _get_isolated_python_version(env_dir)
91
-
92
- if isolated_version and isolated_version != host_version:
93
- # Different Python version - must use SubprocessWorker
94
- from ..workers.subprocess import SubprocessWorker
95
-
96
- if sys.platform == "win32":
97
- python_path = env_dir / "python.exe"
98
- else:
99
- python_path = env_dir / "bin" / "python"
100
-
101
- print(f"[comfy-env] Starting isolated worker (SubprocessWorker)")
102
- print(f"[comfy-env] Python: {python_path} ({isolated_version} vs host {host_version})")
103
-
104
- worker = SubprocessWorker(
105
- python=str(python_path),
106
- working_dir=working_dir,
107
- sys_path=sys_path,
108
- name=working_dir.name,
109
- )
110
- else:
111
- # Same Python version - use MPWorker (faster)
112
- from ..workers.mp import MPWorker
72
+ if cache_key in _workers and _workers[cache_key].is_alive():
73
+ return _workers[cache_key]
113
74
 
114
- print(f"[comfy-env] Starting isolated worker (MPWorker)")
115
- print(f"[comfy-env] Env: {env_dir}")
116
- if env_vars:
117
- print(f"[comfy-env] env_vars: {', '.join(f'{k}={v}' for k, v in env_vars.items())}")
75
+ host_ver = f"{sys.version_info.major}.{sys.version_info.minor}"
76
+ iso_ver = _get_python_version(env_dir)
118
77
 
119
- worker = MPWorker(
120
- name=working_dir.name,
121
- sys_path=sys_path,
122
- lib_path=lib_path,
123
- env_vars=env_vars,
124
- )
78
+ if iso_ver and iso_ver != host_ver:
79
+ from .workers.subprocess import SubprocessWorker
80
+ python = env_dir / ("python.exe" if sys.platform == "win32" else "bin/python")
81
+ print(f"[comfy-env] SubprocessWorker: {python} ({iso_ver} vs {host_ver})")
82
+ worker = SubprocessWorker(python=str(python), working_dir=working_dir, sys_path=sys_path, name=working_dir.name)
83
+ else:
84
+ from .workers.mp import MPWorker
85
+ print(f"[comfy-env] MPWorker: {env_dir}")
86
+ worker = MPWorker(name=working_dir.name, sys_path=sys_path, lib_path=lib_path, env_vars=env_vars)
125
87
 
126
88
  _workers[cache_key] = worker
127
89
  return worker
128
90
 
129
91
 
92
+ @atexit.register
130
93
  def _shutdown_workers():
131
- """Shutdown all cached workers. Called at exit."""
132
94
  with _workers_lock:
133
- for name, worker in _workers.items():
134
- try:
135
- worker.shutdown()
136
- except Exception:
137
- pass
95
+ for w in _workers.values():
96
+ try: w.shutdown()
97
+ except Exception: pass
138
98
  _workers.clear()
139
99
 
140
100
 
141
- atexit.register(_shutdown_workers)
142
-
143
-
144
- def _find_env_paths(node_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
145
- """
146
- Find site-packages and lib directories for the isolated environment.
147
-
148
- Fallback order:
149
- 1. Marker file -> central cache
150
- 2. _env_<name> (local)
151
- 3. .pixi/envs/default (old pixi)
152
- 4. .venv
153
-
154
- Returns:
155
- (site_packages, lib_dir) - lib_dir is for LD_LIBRARY_PATH
156
- """
157
- import glob
158
-
159
- def _get_paths_from_env(env_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
160
- """Extract site-packages and lib_dir from an env directory."""
161
- if sys.platform == "win32":
162
- site_packages = env_dir / "Lib" / "site-packages"
163
- lib_dir = env_dir / "Library" / "bin"
164
- else:
165
- pattern = str(env_dir / "lib" / "python*" / "site-packages")
166
- matches = glob.glob(pattern)
167
- site_packages = Path(matches[0]) if matches else None
168
- lib_dir = env_dir / "lib"
169
- if site_packages and site_packages.exists():
170
- return site_packages, lib_dir if lib_dir and lib_dir.exists() else None
171
- return None, None
172
-
173
- # 1. Check marker file -> central cache
174
- marker_path = node_dir / ".comfy-env-marker.toml"
175
- if marker_path.exists():
176
- try:
177
- import tomli
178
- with open(marker_path, "rb") as f:
179
- marker = tomli.load(f)
180
- env_path = marker.get("env", {}).get("path")
181
- if env_path:
182
- env_dir = Path(env_path)
183
- if env_dir.exists():
184
- result = _get_paths_from_env(env_dir)
185
- if result[0]:
186
- return result
187
- except Exception:
188
- pass # Fall through to other options
189
-
190
- # 2. Check _env_<name> directory (local)
191
- env_name = get_env_name(node_dir.name)
192
- env_dir = node_dir / env_name
193
- if env_dir.exists():
194
- result = _get_paths_from_env(env_dir)
195
- if result[0]:
196
- return result
197
-
198
- # 3. Fallback: Check old .pixi/envs/default (for backward compat)
199
- pixi_env = node_dir / ".pixi" / "envs" / "default"
200
- if pixi_env.exists():
201
- result = _get_paths_from_env(pixi_env)
202
- if result[0]:
203
- return result
204
-
205
- # 4. Check .venv directory
206
- venv_dir = node_dir / ".venv"
207
- if venv_dir.exists():
208
- if sys.platform == "win32":
209
- site_packages = venv_dir / "Lib" / "site-packages"
210
- else:
211
- pattern = str(venv_dir / "lib" / "python*" / "site-packages")
212
- matches = glob.glob(pattern)
213
- site_packages = Path(matches[0]) if matches else None
214
- if site_packages and site_packages.exists():
215
- return site_packages, None # venvs don't have separate lib
216
-
217
- return None, None
218
-
219
-
220
- def _find_env_dir(node_dir: Path) -> Optional[Path]:
221
- """
222
- Find the environment directory (for cache key).
223
-
224
- Fallback order:
225
- 1. Marker file -> central cache
226
- 2. _env_<name> (local)
227
- 3. .pixi/envs/default (old pixi)
228
- 4. .venv
229
- """
230
- # 1. Check marker file -> central cache
231
- marker_path = node_dir / ".comfy-env-marker.toml"
232
- if marker_path.exists():
233
- try:
234
- import tomli
235
- with open(marker_path, "rb") as f:
236
- marker = tomli.load(f)
237
- env_path = marker.get("env", {}).get("path")
238
- if env_path:
239
- env_dir = Path(env_path)
240
- if env_dir.exists():
241
- return env_dir
242
- except Exception:
243
- pass
244
-
245
- # 2. Check _env_<name> first
246
- env_name = get_env_name(node_dir.name)
247
- env_dir = node_dir / env_name
248
- if env_dir.exists():
249
- return env_dir
250
-
251
- # 3. Fallback to old .pixi path
252
- pixi_env = node_dir / ".pixi" / "envs" / "default"
253
- if pixi_env.exists():
254
- return pixi_env
255
-
256
- # 4. Check .venv
257
- venv_dir = node_dir / ".venv"
258
- if venv_dir.exists():
259
- return venv_dir
260
-
261
- return None
262
-
263
-
264
- def _find_custom_node_root(nodes_dir: Path) -> Optional[Path]:
265
- """
266
- Find the custom node root (direct child of custom_nodes/).
267
-
268
- Uses folder_paths to find custom_nodes directories, then finds
269
- which one is an ancestor of nodes_dir.
270
-
271
- Example: /path/custom_nodes/ComfyUI-UniRig/nodes/nodes_gpu
272
- -> returns /path/custom_nodes/ComfyUI-UniRig
273
- """
274
- try:
275
- import folder_paths
276
- custom_nodes_dirs = folder_paths.get_folder_paths("custom_nodes")
277
- except (ImportError, KeyError):
278
- return None
279
-
280
- for cn_dir in custom_nodes_dirs:
281
- cn_path = Path(cn_dir)
282
- try:
283
- rel = nodes_dir.relative_to(cn_path)
284
- if rel.parts:
285
- return cn_path / rel.parts[0]
286
- except ValueError:
287
- continue
288
-
289
- return None
290
-
291
-
292
- def _wrap_node_class(
293
- cls: type,
294
- env_dir: Path,
295
- working_dir: Path,
296
- sys_path: list[str],
297
- lib_path: Optional[str] = None,
298
- env_vars: Optional[dict] = None,
299
- ) -> type:
300
- """
301
- Wrap a node class so its FUNCTION method runs in the isolated environment.
302
-
303
- Args:
304
- cls: The node class to wrap
305
- env_dir: Path to the isolated environment directory
306
- working_dir: Working directory for the worker
307
- sys_path: Additional paths to add to sys.path in the worker
308
- lib_path: Path to add to LD_LIBRARY_PATH for conda libraries
309
-
310
- Returns:
311
- The wrapped class (modified in place)
312
- """
101
+ def _wrap_node_class(cls: type, env_dir: Path, working_dir: Path, sys_path: list[str],
102
+ lib_path: Optional[str] = None, env_vars: Optional[dict] = None) -> type:
313
103
  func_name = getattr(cls, "FUNCTION", None)
314
- if not func_name:
315
- return cls # Not a valid ComfyUI node class
104
+ if not func_name: return cls
105
+ original = getattr(cls, func_name, None)
106
+ if not original: return cls
316
107
 
317
- original_method = getattr(cls, func_name, None)
318
- if original_method is None:
319
- return cls
320
-
321
- # Get source file for the class
322
108
  try:
323
- source_file = Path(inspect.getfile(cls)).resolve()
324
- except (TypeError, OSError):
325
- # Can't get source file, skip wrapping
326
- return cls
109
+ source = Path(inspect.getfile(cls)).resolve()
110
+ module_name = str(source.relative_to(working_dir).with_suffix("")).replace("/", ".").replace("\\", ".")
111
+ except (TypeError, OSError, ValueError):
112
+ module_name = source.stem if 'source' in dir() else cls.__module__
327
113
 
328
- # Compute relative module path from working_dir
329
- # e.g., /path/to/nodes/io/load_mesh.py -> nodes.io.load_mesh
330
- try:
331
- relative_path = source_file.relative_to(working_dir)
332
- # Convert path to module: nodes/io/load_mesh.py -> nodes.io.load_mesh
333
- module_name = str(relative_path.with_suffix("")).replace("/", ".").replace("\\", ".")
334
- except ValueError:
335
- # File not under working_dir, use stem as fallback
336
- module_name = source_file.stem
337
-
338
- @wraps(original_method)
114
+ @wraps(original)
339
115
  def proxy(self, **kwargs):
340
- if _DEBUG:
341
- print(f"[comfy-env] PROXY CALLED: {cls.__name__}.{func_name}", flush=True)
342
- print(f"[comfy-env] kwargs keys: {list(kwargs.keys())}", flush=True)
343
-
344
116
  worker = _get_worker(env_dir, working_dir, sys_path, lib_path, env_vars)
345
- if _DEBUG:
346
- print(f"[comfy-env] worker alive: {worker.is_alive()}", flush=True)
347
-
348
- # Clone tensors for IPC if needed
349
117
  try:
350
- from ..workers.tensor_utils import prepare_for_ipc_recursive
351
-
118
+ from .tensor_utils import prepare_for_ipc_recursive
352
119
  kwargs = {k: prepare_for_ipc_recursive(v) for k, v in kwargs.items()}
353
- except ImportError:
354
- pass # No torch available, skip cloning
120
+ except ImportError: pass
355
121
 
356
- if _DEBUG:
357
- print(f"[comfy-env] calling worker.call_method...", flush=True)
358
122
  result = worker.call_method(
359
- module_name=module_name,
360
- class_name=cls.__name__,
361
- method_name=func_name,
123
+ module_name=module_name, class_name=cls.__name__, method_name=func_name,
362
124
  self_state=self.__dict__.copy() if hasattr(self, "__dict__") else None,
363
- kwargs=kwargs,
364
- timeout=600.0,
125
+ kwargs=kwargs, timeout=600.0,
365
126
  )
366
- if _DEBUG:
367
- print(f"[comfy-env] call_method returned", flush=True)
368
127
 
369
- # Clone result tensors
370
128
  try:
371
- from ..workers.tensor_utils import prepare_for_ipc_recursive
372
-
129
+ from .tensor_utils import prepare_for_ipc_recursive
373
130
  result = prepare_for_ipc_recursive(result)
374
- except ImportError:
375
- pass
376
-
131
+ except ImportError: pass
377
132
  return result
378
133
 
379
- # Replace the method
380
134
  setattr(cls, func_name, proxy)
381
-
382
- # Mark as isolated for debugging
383
135
  cls._comfy_env_isolated = True
384
-
385
136
  return cls
386
137
 
387
138
 
388
- def _is_comfy_env_enabled() -> bool:
389
- """Check if comfy-env isolation is enabled (default: True)."""
390
- val = os.environ.get("USE_COMFY_ENV", "1").lower()
391
- return val not in ("0", "false", "no", "off")
392
-
393
-
394
139
  def wrap_nodes() -> None:
395
- """
396
- Auto-wrap nodes for isolation. Call from your __init__.py after defining NODE_CLASS_MAPPINGS.
397
-
398
- Usage:
399
- from comfy_env import wrap_nodes
400
- wrap_nodes()
401
- """
402
- # Skip if isolation is disabled
403
- if not _is_comfy_env_enabled():
404
- print(f"[comfy-env] Isolation disabled, nodes running in main process")
140
+ """Auto-wrap nodes for isolation. Call from __init__.py after NODE_CLASS_MAPPINGS."""
141
+ if not _is_enabled() or os.environ.get("COMFYUI_ISOLATION_WORKER") == "1":
405
142
  return
406
143
 
407
- # Skip if running inside worker subprocess
408
- if os.environ.get("COMFYUI_ISOLATION_WORKER") == "1":
409
- return
410
-
411
- # Get caller's frame and module
412
144
  frame = inspect.stack()[1]
413
145
  caller_module = inspect.getmodule(frame.frame)
414
- if caller_module is None:
415
- print("[comfy-env] Warning: Could not determine caller module")
416
- return
146
+ if not caller_module: return
417
147
 
418
- # Get NODE_CLASS_MAPPINGS from caller's module
419
- node_class_mappings = getattr(caller_module, "NODE_CLASS_MAPPINGS", None)
420
- if not node_class_mappings:
421
- print("[comfy-env] Warning: No NODE_CLASS_MAPPINGS found in caller module")
422
- return
148
+ mappings = getattr(caller_module, "NODE_CLASS_MAPPINGS", None)
149
+ if not mappings: return
423
150
 
424
- # Get package root directory
425
- caller_file = Path(frame.filename).resolve()
426
- package_dir = caller_file.parent
151
+ pkg_dir = Path(frame.filename).resolve().parent
152
+ config_files = list(pkg_dir.rglob("comfy-env.toml"))
153
+ if not config_files: return
427
154
 
428
- # Find all comfy-env.toml files
429
- config_files = list(package_dir.rglob("comfy-env.toml"))
430
- if not config_files:
431
- return # No configs, nothing to wrap
432
-
433
- # Get ComfyUI base path
434
155
  try:
435
156
  import folder_paths
436
157
  comfyui_base = folder_paths.base_path
437
158
  except ImportError:
438
159
  comfyui_base = None
439
160
 
440
- # Build a map of config_dir -> env info
441
- config_envs = []
442
- for config_file in config_files:
443
- config_dir = config_file.parent
444
- env_dir = _find_env_dir(config_dir)
445
- site_packages, lib_dir = _find_env_paths(config_dir)
446
-
447
- if not env_dir or not site_packages:
448
- continue
161
+ envs = []
162
+ for cf in config_files:
163
+ env_dir = _find_env_dir(cf.parent)
164
+ sp, lib = _get_env_paths(env_dir) if env_dir else (None, None)
165
+ if not env_dir or not sp: continue
449
166
 
450
- # Read env_vars from config
451
167
  env_vars = {}
452
168
  try:
453
169
  import tomli
454
- with open(config_file, "rb") as f:
455
- config = tomli.load(f)
456
- env_vars_data = config.get("env_vars", {})
457
- env_vars = {str(k): str(v) for k, v in env_vars_data.items()}
458
- except Exception:
459
- pass
460
-
461
- if comfyui_base:
462
- env_vars["COMFYUI_BASE"] = str(comfyui_base)
463
-
464
- config_envs.append({
465
- "config_dir": config_dir,
466
- "env_dir": env_dir,
467
- "site_packages": site_packages,
468
- "lib_dir": lib_dir,
469
- "env_vars": env_vars,
470
- })
471
-
472
- if not config_envs:
473
- return
170
+ with open(cf, "rb") as f:
171
+ env_vars = {str(k): str(v) for k, v in tomli.load(f).get("env_vars", {}).items()}
172
+ except Exception: pass
173
+ if comfyui_base: env_vars["COMFYUI_BASE"] = str(comfyui_base)
474
174
 
475
- # Match nodes to configs by checking source file location
476
- wrapped_count = 0
477
- for node_name, node_cls in node_class_mappings.items():
478
- if not hasattr(node_cls, "FUNCTION"):
479
- continue
175
+ envs.append({"dir": cf.parent, "env_dir": env_dir, "sp": sp, "lib": lib, "env_vars": env_vars})
480
176
 
481
- # Get node's source file
177
+ wrapped = 0
178
+ for name, cls in mappings.items():
179
+ if not hasattr(cls, "FUNCTION"): continue
482
180
  try:
483
- source_file = Path(inspect.getfile(node_cls)).resolve()
484
- except (TypeError, OSError):
485
- continue
181
+ src = Path(inspect.getfile(cls)).resolve()
182
+ except (TypeError, OSError): continue
486
183
 
487
- # Find which config this node belongs to
488
- for env_info in config_envs:
489
- config_dir = env_info["config_dir"]
184
+ for e in envs:
490
185
  try:
491
- source_file.relative_to(config_dir)
492
- # Node is under this config dir - wrap it
493
- sys_path = [str(env_info["site_packages"]), str(config_dir)]
494
- lib_path = str(env_info["lib_dir"]) if env_info["lib_dir"] else None
495
-
496
- _wrap_node_class(
497
- node_cls,
498
- env_info["env_dir"],
499
- config_dir,
500
- sys_path,
501
- lib_path,
502
- env_info["env_vars"],
503
- )
504
- wrapped_count += 1
186
+ src.relative_to(e["dir"])
187
+ _wrap_node_class(cls, e["env_dir"], e["dir"], [str(e["sp"]), str(e["dir"])],
188
+ str(e["lib"]) if e["lib"] else None, e["env_vars"])
189
+ wrapped += 1
505
190
  break
506
- except ValueError:
507
- continue # Node not under this config dir
508
-
509
- if wrapped_count > 0:
510
- print(f"[comfy-env] Wrapped {wrapped_count} nodes for isolation")
511
-
512
-
513
- def wrap_isolated_nodes(
514
- node_class_mappings: Dict[str, type],
515
- nodes_dir: Path,
516
- ) -> Dict[str, type]:
517
- """
518
- Wrap nodes from a directory that has a comfy-env.toml.
519
-
520
- This is the directory-based isolation API. Call it for each subdirectory
521
- of nodes/ that has a comfy-env.toml.
191
+ except ValueError: continue
522
192
 
523
- Args:
524
- node_class_mappings: The NODE_CLASS_MAPPINGS dict from the nodes in this dir.
525
- nodes_dir: The directory containing comfy-env.toml and the node files.
193
+ if wrapped: print(f"[comfy-env] Wrapped {wrapped} nodes")
526
194
 
527
- Returns:
528
- The same dict with node classes wrapped for isolation.
529
195
 
530
- Example:
531
- # __init__.py
532
- from comfy_env import wrap_isolated_nodes
533
- from pathlib import Path
534
-
535
- NODE_CLASS_MAPPINGS = {}
536
-
537
- # Native nodes (no isolation)
538
- from .nodes.main import NODE_CLASS_MAPPINGS as main_nodes
539
- NODE_CLASS_MAPPINGS.update(main_nodes)
540
-
541
- # Isolated nodes (has comfy-env.toml)
542
- from .nodes.cgal import NODE_CLASS_MAPPINGS as cgal_nodes
543
- NODE_CLASS_MAPPINGS.update(
544
- wrap_isolated_nodes(cgal_nodes, Path(__file__).parent / "nodes/cgal")
545
- )
546
- """
547
- # Skip if isolation is disabled
548
- if not _is_comfy_env_enabled():
549
- print(f"[comfy-env] Isolation disabled, nodes running in main process")
550
- return node_class_mappings
551
-
552
- # Skip if running inside worker subprocess
553
- if os.environ.get("COMFYUI_ISOLATION_WORKER") == "1":
196
+ def wrap_isolated_nodes(node_class_mappings: Dict[str, type], nodes_dir: Path) -> Dict[str, type]:
197
+ """Wrap nodes from a directory with comfy-env.toml for isolation."""
198
+ if not _is_enabled() or os.environ.get("COMFYUI_ISOLATION_WORKER") == "1":
554
199
  return node_class_mappings
555
200
 
556
- # Get ComfyUI base path from folder_paths (canonical source)
557
201
  try:
558
202
  import folder_paths
559
203
  comfyui_base = folder_paths.base_path
@@ -561,56 +205,31 @@ def wrap_isolated_nodes(
561
205
  comfyui_base = None
562
206
 
563
207
  nodes_dir = Path(nodes_dir).resolve()
564
-
565
- # Check for comfy-env.toml
566
- config_file = nodes_dir / "comfy-env.toml"
567
- if not config_file.exists():
568
- print(f"[comfy-env] Warning: No comfy-env.toml in {nodes_dir}")
208
+ config = nodes_dir / "comfy-env.toml"
209
+ if not config.exists():
210
+ print(f"[comfy-env] No comfy-env.toml in {nodes_dir}")
569
211
  return node_class_mappings
570
212
 
571
- # Read env_vars from comfy-env.toml
572
213
  env_vars = {}
573
214
  try:
574
215
  import tomli
575
- with open(config_file, "rb") as f:
576
- config = tomli.load(f)
577
- env_vars_data = config.get("env_vars", {})
578
- env_vars = {str(k): str(v) for k, v in env_vars_data.items()}
579
- except Exception:
580
- pass # Ignore errors reading config
581
-
582
- # Set COMFYUI_BASE for worker to find ComfyUI modules
583
- if comfyui_base:
584
- env_vars["COMFYUI_BASE"] = str(comfyui_base)
585
-
586
- # Find environment directory and paths
587
- env_dir = _find_env_dir(nodes_dir)
588
- site_packages, lib_dir = _find_env_paths(nodes_dir)
216
+ with open(config, "rb") as f:
217
+ env_vars = {str(k): str(v) for k, v in tomli.load(f).get("env_vars", {}).items()}
218
+ except Exception: pass
219
+ if comfyui_base: env_vars["COMFYUI_BASE"] = str(comfyui_base)
589
220
 
590
- if not env_dir or not site_packages:
591
- print(f"[comfy-env] Warning: Isolated environment not found")
592
- print(f"[comfy-env] Expected: .pixi/envs/default or .venv")
593
- print(f"[comfy-env] Run 'comfy-env install' in {nodes_dir}")
221
+ env_dir = _find_env_dir(nodes_dir)
222
+ sp, lib = _get_env_paths(env_dir) if env_dir else (None, None)
223
+ if not env_dir or not sp:
224
+ print(f"[comfy-env] No env found. Run 'comfy-env install' in {nodes_dir}")
594
225
  return node_class_mappings
595
226
 
596
- # Build sys.path - site-packages first, then nodes_dir
597
- # Note: isolated modules should use absolute imports (their dir is in sys.path)
598
- # Relative imports would require importing parent package which may have host-only deps
599
- sys_path = [str(site_packages), str(nodes_dir)]
600
-
601
- # lib_dir for LD_LIBRARY_PATH (conda libraries)
602
- lib_path = str(lib_dir) if lib_dir else None
227
+ sys_path = [str(sp), str(nodes_dir)]
228
+ lib_path = str(lib) if lib else None
603
229
 
604
230
  print(f"[comfy-env] Wrapping {len(node_class_mappings)} nodes from {nodes_dir.name}")
605
- print(f"[comfy-env] site-packages: {site_packages}")
606
- if lib_path:
607
- print(f"[comfy-env] lib: {lib_path}")
608
- if env_vars:
609
- print(f"[comfy-env] env_vars: {', '.join(f'{k}={v}' for k, v in env_vars.items())}")
610
-
611
- # Wrap all node classes
612
- for node_name, node_cls in node_class_mappings.items():
613
- if hasattr(node_cls, "FUNCTION"):
614
- _wrap_node_class(node_cls, env_dir, nodes_dir, sys_path, lib_path, env_vars)
231
+ for cls in node_class_mappings.values():
232
+ if hasattr(cls, "FUNCTION"):
233
+ _wrap_node_class(cls, env_dir, nodes_dir, sys_path, lib_path, env_vars)
615
234
 
616
235
  return node_class_mappings