comfy-env 0.1.14__py3-none-any.whl → 0.1.16__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 (51) hide show
  1. comfy_env/__init__.py +115 -62
  2. comfy_env/cli.py +89 -319
  3. comfy_env/config/__init__.py +18 -8
  4. comfy_env/config/parser.py +21 -122
  5. comfy_env/config/types.py +37 -70
  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 +88 -0
  16. comfy_env/install.py +163 -249
  17. comfy_env/isolation/__init__.py +33 -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 +2 -2
  22. comfy_env/isolation/wrap.py +149 -409
  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.16.dist-info/METADATA +279 -0
  30. comfy_env-0.1.16.dist-info/RECORD +36 -0
  31. comfy_env/cache.py +0 -331
  32. comfy_env/errors.py +0 -293
  33. comfy_env/nodes.py +0 -187
  34. comfy_env/pixi/__init__.py +0 -48
  35. comfy_env/pixi/core.py +0 -588
  36. comfy_env/pixi/cuda_detection.py +0 -303
  37. comfy_env/pixi/platform/__init__.py +0 -21
  38. comfy_env/pixi/platform/base.py +0 -96
  39. comfy_env/pixi/platform/darwin.py +0 -53
  40. comfy_env/pixi/platform/linux.py +0 -68
  41. comfy_env/pixi/platform/windows.py +0 -284
  42. comfy_env/pixi/resolver.py +0 -198
  43. comfy_env/prestartup.py +0 -192
  44. comfy_env/workers/__init__.py +0 -38
  45. comfy_env/workers/tensor_utils.py +0 -188
  46. comfy_env-0.1.14.dist-info/METADATA +0 -291
  47. comfy_env-0.1.14.dist-info/RECORD +0 -33
  48. /comfy_env/{workers → isolation/workers}/base.py +0 -0
  49. {comfy_env-0.1.14.dist-info → comfy_env-0.1.16.dist-info}/WHEEL +0 -0
  50. {comfy_env-0.1.14.dist-info → comfy_env-0.1.16.dist-info}/entry_points.txt +0 -0
  51. {comfy_env-0.1.14.dist-info → comfy_env-0.1.16.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,405 +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()
21
+
22
+ def _env_name(dir_name: str) -> str:
23
+ return f"_env_{dir_name.lower().replace('-', '_').lstrip('comfyui_')}"
46
24
 
47
25
 
48
- def _get_isolated_python_version(env_dir: Path) -> Optional[str]:
49
- """Get Python version from isolated environment."""
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
- if sys.version_info >= (3, 11):
178
- import tomllib
179
- else:
180
- import tomli as tomllib
181
- with open(marker_path, "rb") as f:
182
- marker = tomllib.load(f)
183
- env_path = marker.get("env", {}).get("path")
184
- if env_path:
185
- env_dir = Path(env_path)
186
- if env_dir.exists():
187
- result = _get_paths_from_env(env_dir)
188
- if result[0]:
189
- return result
190
- except Exception:
191
- pass # Fall through to other options
192
-
193
- # 2. Check _env_<name> directory (local)
194
- env_name = get_env_name(node_dir.name)
195
- env_dir = node_dir / env_name
196
- if env_dir.exists():
197
- result = _get_paths_from_env(env_dir)
198
- if result[0]:
199
- return result
200
-
201
- # 3. Fallback: Check old .pixi/envs/default (for backward compat)
202
- pixi_env = node_dir / ".pixi" / "envs" / "default"
203
- if pixi_env.exists():
204
- result = _get_paths_from_env(pixi_env)
205
- if result[0]:
206
- return result
207
-
208
- # 4. Check .venv directory
209
- venv_dir = node_dir / ".venv"
210
- if venv_dir.exists():
211
- if sys.platform == "win32":
212
- site_packages = venv_dir / "Lib" / "site-packages"
213
- else:
214
- pattern = str(venv_dir / "lib" / "python*" / "site-packages")
215
- matches = glob.glob(pattern)
216
- site_packages = Path(matches[0]) if matches else None
217
- if site_packages and site_packages.exists():
218
- return site_packages, None # venvs don't have separate lib
219
-
220
- return None, None
221
-
222
-
223
- def _find_env_dir(node_dir: Path) -> Optional[Path]:
224
- """
225
- Find the environment directory (for cache key).
226
-
227
- Fallback order:
228
- 1. Marker file -> central cache
229
- 2. _env_<name> (local)
230
- 3. .pixi/envs/default (old pixi)
231
- 4. .venv
232
- """
233
- # 1. Check marker file -> central cache
234
- marker_path = node_dir / ".comfy-env-marker.toml"
235
- if marker_path.exists():
236
- try:
237
- if sys.version_info >= (3, 11):
238
- import tomllib
239
- else:
240
- import tomli as tomllib
241
- with open(marker_path, "rb") as f:
242
- marker = tomllib.load(f)
243
- env_path = marker.get("env", {}).get("path")
244
- if env_path:
245
- env_dir = Path(env_path)
246
- if env_dir.exists():
247
- return env_dir
248
- except Exception:
249
- pass
250
-
251
- # 2. Check _env_<name> first
252
- env_name = get_env_name(node_dir.name)
253
- env_dir = node_dir / env_name
254
- if env_dir.exists():
255
- return env_dir
256
-
257
- # 3. Fallback to old .pixi path
258
- pixi_env = node_dir / ".pixi" / "envs" / "default"
259
- if pixi_env.exists():
260
- return pixi_env
261
-
262
- # 4. Check .venv
263
- venv_dir = node_dir / ".venv"
264
- if venv_dir.exists():
265
- return venv_dir
266
-
267
- return None
268
-
269
-
270
- def _find_custom_node_root(nodes_dir: Path) -> Optional[Path]:
271
- """
272
- Find the custom node root (direct child of custom_nodes/).
273
-
274
- Uses folder_paths to find custom_nodes directories, then finds
275
- which one is an ancestor of nodes_dir.
276
-
277
- Example: /path/custom_nodes/ComfyUI-UniRig/nodes/nodes_gpu
278
- -> returns /path/custom_nodes/ComfyUI-UniRig
279
- """
280
- try:
281
- import folder_paths
282
- custom_nodes_dirs = folder_paths.get_folder_paths("custom_nodes")
283
- except (ImportError, KeyError):
284
- return None
285
-
286
- for cn_dir in custom_nodes_dirs:
287
- cn_path = Path(cn_dir)
288
- try:
289
- rel = nodes_dir.relative_to(cn_path)
290
- if rel.parts:
291
- return cn_path / rel.parts[0]
292
- except ValueError:
293
- continue
294
-
295
- return None
296
-
297
-
298
- def _wrap_node_class(
299
- cls: type,
300
- env_dir: Path,
301
- working_dir: Path,
302
- sys_path: list[str],
303
- lib_path: Optional[str] = None,
304
- env_vars: Optional[dict] = None,
305
- ) -> type:
306
- """
307
- Wrap a node class so its FUNCTION method runs in the isolated environment.
308
-
309
- Args:
310
- cls: The node class to wrap
311
- env_dir: Path to the isolated environment directory
312
- working_dir: Working directory for the worker
313
- sys_path: Additional paths to add to sys.path in the worker
314
- lib_path: Path to add to LD_LIBRARY_PATH for conda libraries
315
-
316
- Returns:
317
- The wrapped class (modified in place)
318
- """
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:
319
103
  func_name = getattr(cls, "FUNCTION", None)
320
- if not func_name:
321
- 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
322
107
 
323
- original_method = getattr(cls, func_name, None)
324
- if original_method is None:
325
- return cls
326
-
327
- # Get source file for the class
328
108
  try:
329
- source_file = Path(inspect.getfile(cls)).resolve()
330
- except (TypeError, OSError):
331
- # Can't get source file, skip wrapping
332
- 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__
333
113
 
334
- # Compute relative module path from working_dir
335
- # e.g., /path/to/nodes/io/load_mesh.py -> nodes.io.load_mesh
336
- try:
337
- relative_path = source_file.relative_to(working_dir)
338
- # Convert path to module: nodes/io/load_mesh.py -> nodes.io.load_mesh
339
- module_name = str(relative_path.with_suffix("")).replace("/", ".").replace("\\", ".")
340
- except ValueError:
341
- # File not under working_dir, use stem as fallback
342
- module_name = source_file.stem
343
-
344
- @wraps(original_method)
114
+ @wraps(original)
345
115
  def proxy(self, **kwargs):
346
- if _DEBUG:
347
- print(f"[comfy-env] PROXY CALLED: {cls.__name__}.{func_name}", flush=True)
348
- print(f"[comfy-env] kwargs keys: {list(kwargs.keys())}", flush=True)
349
-
350
116
  worker = _get_worker(env_dir, working_dir, sys_path, lib_path, env_vars)
351
- if _DEBUG:
352
- print(f"[comfy-env] worker alive: {worker.is_alive()}", flush=True)
353
-
354
- # Clone tensors for IPC if needed
355
117
  try:
356
- from ..workers.tensor_utils import prepare_for_ipc_recursive
357
-
118
+ from .tensor_utils import prepare_for_ipc_recursive
358
119
  kwargs = {k: prepare_for_ipc_recursive(v) for k, v in kwargs.items()}
359
- except ImportError:
360
- pass # No torch available, skip cloning
120
+ except ImportError: pass
361
121
 
362
- if _DEBUG:
363
- print(f"[comfy-env] calling worker.call_method...", flush=True)
364
122
  result = worker.call_method(
365
- module_name=module_name,
366
- class_name=cls.__name__,
367
- method_name=func_name,
123
+ module_name=module_name, class_name=cls.__name__, method_name=func_name,
368
124
  self_state=self.__dict__.copy() if hasattr(self, "__dict__") else None,
369
- kwargs=kwargs,
370
- timeout=600.0,
125
+ kwargs=kwargs, timeout=600.0,
371
126
  )
372
- if _DEBUG:
373
- print(f"[comfy-env] call_method returned", flush=True)
374
127
 
375
- # Clone result tensors
376
128
  try:
377
- from ..workers.tensor_utils import prepare_for_ipc_recursive
378
-
129
+ from .tensor_utils import prepare_for_ipc_recursive
379
130
  result = prepare_for_ipc_recursive(result)
380
- except ImportError:
381
- pass
382
-
131
+ except ImportError: pass
383
132
  return result
384
133
 
385
- # Replace the method
386
134
  setattr(cls, func_name, proxy)
387
-
388
- # Mark as isolated for debugging
389
135
  cls._comfy_env_isolated = True
390
-
391
136
  return cls
392
137
 
393
138
 
394
- def wrap_isolated_nodes(
395
- node_class_mappings: Dict[str, type],
396
- nodes_dir: Path,
397
- ) -> Dict[str, type]:
398
- """
399
- Wrap nodes from a directory that has a comfy-env.toml.
139
+ def wrap_nodes() -> None:
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":
142
+ return
400
143
 
401
- This is the directory-based isolation API. Call it for each subdirectory
402
- of nodes/ that has a comfy-env.toml.
144
+ frame = inspect.stack()[1]
145
+ caller_module = inspect.getmodule(frame.frame)
146
+ if not caller_module: return
403
147
 
404
- Args:
405
- node_class_mappings: The NODE_CLASS_MAPPINGS dict from the nodes in this dir.
406
- nodes_dir: The directory containing comfy-env.toml and the node files.
148
+ mappings = getattr(caller_module, "NODE_CLASS_MAPPINGS", None)
149
+ if not mappings: return
407
150
 
408
- Returns:
409
- The same dict with node classes wrapped for isolation.
151
+ pkg_dir = Path(frame.filename).resolve().parent
152
+ config_files = list(pkg_dir.rglob("comfy-env.toml"))
153
+ if not config_files: return
410
154
 
411
- Example:
412
- # __init__.py
413
- from comfy_env import wrap_isolated_nodes
414
- from pathlib import Path
155
+ try:
156
+ import folder_paths
157
+ comfyui_base = folder_paths.base_path
158
+ except ImportError:
159
+ comfyui_base = None
415
160
 
416
- NODE_CLASS_MAPPINGS = {}
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
417
166
 
418
- # Native nodes (no isolation)
419
- from .nodes.main import NODE_CLASS_MAPPINGS as main_nodes
420
- NODE_CLASS_MAPPINGS.update(main_nodes)
167
+ env_vars = {}
168
+ try:
169
+ import tomli
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)
421
174
 
422
- # Isolated nodes (has comfy-env.toml)
423
- from .nodes.cgal import NODE_CLASS_MAPPINGS as cgal_nodes
424
- NODE_CLASS_MAPPINGS.update(
425
- wrap_isolated_nodes(cgal_nodes, Path(__file__).parent / "nodes/cgal")
426
- )
427
- """
428
- # Skip if running inside worker subprocess
429
- if os.environ.get("COMFYUI_ISOLATION_WORKER") == "1":
175
+ envs.append({"dir": cf.parent, "env_dir": env_dir, "sp": sp, "lib": lib, "env_vars": env_vars})
176
+
177
+ wrapped = 0
178
+ for name, cls in mappings.items():
179
+ if not hasattr(cls, "FUNCTION"): continue
180
+ try:
181
+ src = Path(inspect.getfile(cls)).resolve()
182
+ except (TypeError, OSError): continue
183
+
184
+ for e in envs:
185
+ try:
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
190
+ break
191
+ except ValueError: continue
192
+
193
+ if wrapped: print(f"[comfy-env] Wrapped {wrapped} nodes")
194
+
195
+
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":
430
199
  return node_class_mappings
431
200
 
432
- # Get ComfyUI base path from folder_paths (canonical source)
433
201
  try:
434
202
  import folder_paths
435
203
  comfyui_base = folder_paths.base_path
@@ -437,59 +205,31 @@ def wrap_isolated_nodes(
437
205
  comfyui_base = None
438
206
 
439
207
  nodes_dir = Path(nodes_dir).resolve()
440
-
441
- # Check for comfy-env.toml
442
- config_file = nodes_dir / "comfy-env.toml"
443
- if not config_file.exists():
444
- 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}")
445
211
  return node_class_mappings
446
212
 
447
- # Read env_vars from comfy-env.toml
448
213
  env_vars = {}
449
214
  try:
450
- if sys.version_info >= (3, 11):
451
- import tomllib
452
- else:
453
- import tomli as tomllib
454
- with open(config_file, "rb") as f:
455
- config = tomllib.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 # Ignore errors reading config
460
-
461
- # Set COMFYUI_BASE for worker to find ComfyUI modules
462
- if comfyui_base:
463
- env_vars["COMFYUI_BASE"] = str(comfyui_base)
464
-
465
- # Find environment directory and paths
466
- env_dir = _find_env_dir(nodes_dir)
467
- site_packages, lib_dir = _find_env_paths(nodes_dir)
215
+ import tomli
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)
468
220
 
469
- if not env_dir or not site_packages:
470
- print(f"[comfy-env] Warning: Isolated environment not found")
471
- print(f"[comfy-env] Expected: .pixi/envs/default or .venv")
472
- 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}")
473
225
  return node_class_mappings
474
226
 
475
- # Build sys.path - site-packages first, then nodes_dir
476
- # Note: isolated modules should use absolute imports (their dir is in sys.path)
477
- # Relative imports would require importing parent package which may have host-only deps
478
- sys_path = [str(site_packages), str(nodes_dir)]
479
-
480
- # lib_dir for LD_LIBRARY_PATH (conda libraries)
481
- 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
482
229
 
483
230
  print(f"[comfy-env] Wrapping {len(node_class_mappings)} nodes from {nodes_dir.name}")
484
- print(f"[comfy-env] site-packages: {site_packages}")
485
- if lib_path:
486
- print(f"[comfy-env] lib: {lib_path}")
487
- if env_vars:
488
- print(f"[comfy-env] env_vars: {', '.join(f'{k}={v}' for k, v in env_vars.items())}")
489
-
490
- # Wrap all node classes
491
- for node_name, node_cls in node_class_mappings.items():
492
- if hasattr(node_cls, "FUNCTION"):
493
- _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)
494
234
 
495
235
  return node_class_mappings