comfy-env 0.1.12__tar.gz → 0.1.14__tar.gz

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 (35) hide show
  1. {comfy_env-0.1.12 → comfy_env-0.1.14}/PKG-INFO +1 -1
  2. {comfy_env-0.1.12 → comfy_env-0.1.14}/pyproject.toml +1 -1
  3. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/__init__.py +25 -0
  4. comfy_env-0.1.14/src/comfy_env/cache.py +331 -0
  5. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/isolation/wrap.py +77 -21
  6. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/core.py +44 -9
  7. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/prestartup.py +38 -10
  8. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/workers/mp.py +51 -11
  9. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/workers/subprocess.py +5 -5
  10. {comfy_env-0.1.12 → comfy_env-0.1.14}/.github/workflows/ci.yml +0 -0
  11. {comfy_env-0.1.12 → comfy_env-0.1.14}/.github/workflows/publish.yml +0 -0
  12. {comfy_env-0.1.12 → comfy_env-0.1.14}/.gitignore +0 -0
  13. {comfy_env-0.1.12 → comfy_env-0.1.14}/LICENSE +0 -0
  14. {comfy_env-0.1.12 → comfy_env-0.1.14}/README.md +0 -0
  15. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/cli.py +0 -0
  16. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/config/__init__.py +0 -0
  17. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/config/parser.py +0 -0
  18. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/config/types.py +0 -0
  19. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/errors.py +0 -0
  20. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/install.py +0 -0
  21. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/isolation/__init__.py +0 -0
  22. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/nodes.py +0 -0
  23. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/__init__.py +0 -0
  24. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/cuda_detection.py +0 -0
  25. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/platform/__init__.py +0 -0
  26. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/platform/base.py +0 -0
  27. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/platform/darwin.py +0 -0
  28. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/platform/linux.py +0 -0
  29. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/platform/windows.py +0 -0
  30. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/resolver.py +0 -0
  31. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/templates/comfy-env-instructions.txt +0 -0
  32. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/templates/comfy-env.toml +0 -0
  33. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/workers/__init__.py +0 -0
  34. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/workers/base.py +0 -0
  35. {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/workers/tensor_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfy-env
3
- Version: 0.1.12
3
+ Version: 0.1.14
4
4
  Summary: Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation
5
5
  Project-URL: Homepage, https://github.com/PozzettiAndrea/comfy-env
6
6
  Project-URL: Repository, https://github.com/PozzettiAndrea/comfy-env
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "comfy-env"
3
- version = "0.1.12"
3
+ version = "0.1.14"
4
4
  description = "Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -56,6 +56,15 @@ from .install import install, verify_installation
56
56
  # Prestartup helpers
57
57
  from .prestartup import setup_env
58
58
 
59
+ # Cache management
60
+ from .cache import (
61
+ get_cache_dir,
62
+ cleanup_orphaned_envs,
63
+ resolve_env_path,
64
+ CACHE_DIR,
65
+ MARKER_FILE,
66
+ )
67
+
59
68
  # Errors
60
69
  from .errors import (
61
70
  EnvManagerError,
@@ -105,4 +114,20 @@ __all__ = [
105
114
  "DependencyError",
106
115
  "CUDANotFoundError",
107
116
  "InstallError",
117
+ # Cache
118
+ "get_cache_dir",
119
+ "cleanup_orphaned_envs",
120
+ "resolve_env_path",
121
+ "CACHE_DIR",
122
+ "MARKER_FILE",
108
123
  ]
124
+
125
+ # Run orphan cleanup once on module load (silently)
126
+ def _run_startup_cleanup():
127
+ """Clean orphaned envs on startup. Runs silently, never fails startup."""
128
+ try:
129
+ cleanup_orphaned_envs(log=lambda x: None) # Silent
130
+ except Exception:
131
+ pass # Never fail startup due to cleanup
132
+
133
+ _run_startup_cleanup()
@@ -0,0 +1,331 @@
1
+ """
2
+ Central environment cache management for comfy-env.
3
+
4
+ Stores environments in ~/.comfy-env/envs/<nodename>_<subfolder>_<hash>/
5
+ to avoid Windows MAX_PATH (260 char) issues.
6
+
7
+ Marker files in node folders link to central envs and enable orphan cleanup.
8
+ """
9
+
10
+ import hashlib
11
+ import os
12
+ import shutil
13
+ import sys
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from typing import Optional, Tuple, Callable
17
+
18
+ # Import version
19
+ try:
20
+ from . import __version__
21
+ except ImportError:
22
+ __version__ = "0.0.0-dev"
23
+
24
+ # Lazy import tomli/tomllib
25
+ def _get_tomli():
26
+ if sys.version_info >= (3, 11):
27
+ import tomllib
28
+ return tomllib
29
+ else:
30
+ try:
31
+ import tomli
32
+ return tomli
33
+ except ImportError:
34
+ return None
35
+
36
+ def _get_tomli_w():
37
+ try:
38
+ import tomli_w
39
+ return tomli_w
40
+ except ImportError:
41
+ return None
42
+
43
+
44
+ # Constants
45
+ CACHE_DIR = Path.home() / ".comfy-env" / "envs"
46
+ MARKER_FILE = ".comfy-env-marker.toml"
47
+ METADATA_FILE = ".comfy-env-metadata.toml"
48
+
49
+
50
+ def get_cache_dir() -> Path:
51
+ """Get central cache directory, create if needed."""
52
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
53
+ return CACHE_DIR
54
+
55
+
56
+ def compute_config_hash(config_path: Path) -> str:
57
+ """Compute hash of comfy-env.toml content (first 8 chars of SHA256)."""
58
+ content = config_path.read_bytes()
59
+ return hashlib.sha256(content).hexdigest()[:8]
60
+
61
+
62
+ def sanitize_name(name: str) -> str:
63
+ """Sanitize a name for use in filesystem paths."""
64
+ # Lowercase and replace problematic chars
65
+ name = name.lower()
66
+ for prefix in ("comfyui-", "comfyui_"):
67
+ if name.startswith(prefix):
68
+ name = name[len(prefix):]
69
+ return name.replace("-", "_").replace(" ", "_")
70
+
71
+
72
+ def get_env_name(node_dir: Path, config_path: Path) -> str:
73
+ """
74
+ Generate env name: <nodename>_<subfolder>_<hash>
75
+
76
+ Args:
77
+ node_dir: The custom node directory (e.g., custom_nodes/ComfyUI-UniRig)
78
+ config_path: Path to comfy-env.toml
79
+
80
+ Examples:
81
+ - ComfyUI-UniRig/nodes/comfy-env.toml -> unirig_nodes_a1b2c3d4
82
+ - ComfyUI-Pack/comfy-env.toml -> pack__f5e6d7c8 (double underscore = no subfolder)
83
+ """
84
+ # Get node name
85
+ node_name = sanitize_name(node_dir.name)
86
+
87
+ # Get subfolder (relative path from node_dir to config parent)
88
+ config_parent = config_path.parent
89
+ if config_parent == node_dir:
90
+ subfolder = ""
91
+ else:
92
+ try:
93
+ rel_path = config_parent.relative_to(node_dir)
94
+ subfolder = rel_path.as_posix().replace("/", "_")
95
+ except ValueError:
96
+ # config_path not under node_dir - use parent folder name
97
+ subfolder = sanitize_name(config_parent.name)
98
+
99
+ # Compute hash
100
+ config_hash = compute_config_hash(config_path)
101
+
102
+ return f"{node_name}_{subfolder}_{config_hash}"
103
+
104
+
105
+ def get_central_env_path(node_dir: Path, config_path: Path) -> Path:
106
+ """Get path to central environment for this config."""
107
+ env_name = get_env_name(node_dir, config_path)
108
+ return get_cache_dir() / env_name
109
+
110
+
111
+ def write_marker(config_path: Path, env_path: Path) -> None:
112
+ """
113
+ Write marker file linking node to central env.
114
+
115
+ Args:
116
+ config_path: Path to comfy-env.toml
117
+ env_path: Path to central environment
118
+ """
119
+ tomli_w = _get_tomli_w()
120
+ if not tomli_w:
121
+ # Fallback to manual TOML writing
122
+ marker_path = config_path.parent / MARKER_FILE
123
+ content = f'''[env]
124
+ name = "{env_path.name}"
125
+ path = "{env_path}"
126
+ config_hash = "{compute_config_hash(config_path)}"
127
+ created = "{datetime.now().isoformat()}"
128
+ comfy_env_version = "{__version__}"
129
+ '''
130
+ marker_path.write_text(content)
131
+ return
132
+
133
+ marker_path = config_path.parent / MARKER_FILE
134
+ marker_data = {
135
+ "env": {
136
+ "name": env_path.name,
137
+ "path": str(env_path),
138
+ "config_hash": compute_config_hash(config_path),
139
+ "created": datetime.now().isoformat(),
140
+ "comfy_env_version": __version__,
141
+ }
142
+ }
143
+ marker_path.write_text(tomli_w.dumps(marker_data))
144
+
145
+
146
+ def write_env_metadata(env_path: Path, marker_path: Path) -> None:
147
+ """
148
+ Write metadata file in env for orphan detection.
149
+
150
+ Args:
151
+ env_path: Path to central environment
152
+ marker_path: Path to marker file in node folder
153
+ """
154
+ tomli_w = _get_tomli_w()
155
+ metadata_path = env_path / METADATA_FILE
156
+
157
+ if not tomli_w:
158
+ # Fallback to manual TOML writing
159
+ content = f'''marker_path = "{marker_path}"
160
+ created = "{datetime.now().isoformat()}"
161
+ '''
162
+ metadata_path.write_text(content)
163
+ return
164
+
165
+ metadata = {
166
+ "marker_path": str(marker_path),
167
+ "created": datetime.now().isoformat(),
168
+ }
169
+ metadata_path.write_text(tomli_w.dumps(metadata))
170
+
171
+
172
+ def read_marker(marker_path: Path) -> Optional[dict]:
173
+ """
174
+ Read marker file, return None if invalid/missing.
175
+
176
+ Args:
177
+ marker_path: Path to .comfy-env-marker.toml
178
+
179
+ Returns:
180
+ Parsed marker data or None
181
+ """
182
+ if not marker_path.exists():
183
+ return None
184
+
185
+ tomli = _get_tomli()
186
+ if not tomli:
187
+ return None
188
+
189
+ try:
190
+ with open(marker_path, "rb") as f:
191
+ return tomli.load(f)
192
+ except Exception:
193
+ return None
194
+
195
+
196
+ def read_env_metadata(env_path: Path) -> Optional[dict]:
197
+ """
198
+ Read metadata file from env, return None if invalid/missing.
199
+
200
+ Args:
201
+ env_path: Path to central environment
202
+
203
+ Returns:
204
+ Parsed metadata or None
205
+ """
206
+ metadata_path = env_path / METADATA_FILE
207
+ if not metadata_path.exists():
208
+ return None
209
+
210
+ tomli = _get_tomli()
211
+ if not tomli:
212
+ return None
213
+
214
+ try:
215
+ with open(metadata_path, "rb") as f:
216
+ return tomli.load(f)
217
+ except Exception:
218
+ return None
219
+
220
+
221
+ def resolve_env_path(node_dir: Path) -> Tuple[Optional[Path], Optional[Path], Optional[Path]]:
222
+ """
223
+ Resolve environment path with fallback chain.
224
+
225
+ Args:
226
+ node_dir: Directory containing comfy-env.toml or marker file
227
+
228
+ Returns:
229
+ (env_path, site_packages, lib_dir) or (None, None, None)
230
+
231
+ Fallback order:
232
+ 1. Marker file -> central cache
233
+ 2. _env_<name> (current location)
234
+ 3. .pixi/envs/default (old pixi)
235
+ 4. .venv (venv support)
236
+ """
237
+ # 1. Check marker file -> central cache
238
+ marker_path = node_dir / MARKER_FILE
239
+ marker = read_marker(marker_path)
240
+ if marker and "env" in marker:
241
+ env_path = Path(marker["env"]["path"])
242
+ if env_path.exists():
243
+ return _get_env_paths(env_path)
244
+
245
+ # 2. Check _env_<name>
246
+ node_name = sanitize_name(node_dir.name)
247
+ env_name = f"_env_{node_name}"
248
+ local_env = node_dir / env_name
249
+ if local_env.exists():
250
+ return _get_env_paths(local_env)
251
+
252
+ # 3. Check .pixi/envs/default
253
+ pixi_env = node_dir / ".pixi" / "envs" / "default"
254
+ if pixi_env.exists():
255
+ return _get_env_paths(pixi_env)
256
+
257
+ # 4. Check .venv
258
+ venv_dir = node_dir / ".venv"
259
+ if venv_dir.exists():
260
+ return _get_env_paths(venv_dir)
261
+
262
+ return None, None, None
263
+
264
+
265
+ def _get_env_paths(env_path: Path) -> Tuple[Path, Optional[Path], Optional[Path]]:
266
+ """
267
+ Get site-packages and lib paths from an environment.
268
+
269
+ Args:
270
+ env_path: Path to environment root
271
+
272
+ Returns:
273
+ (env_path, site_packages, lib_dir)
274
+ """
275
+ import glob
276
+
277
+ if sys.platform == "win32":
278
+ site_packages = env_path / "Lib" / "site-packages"
279
+ lib_dir = env_path / "Library" / "bin"
280
+ else:
281
+ # Linux/Mac: lib/python*/site-packages
282
+ matches = glob.glob(str(env_path / "lib" / "python*" / "site-packages"))
283
+ site_packages = Path(matches[0]) if matches else None
284
+ lib_dir = env_path / "lib"
285
+
286
+ return env_path, site_packages, lib_dir
287
+
288
+
289
+ def cleanup_orphaned_envs(log: Callable[[str], None] = print) -> int:
290
+ """
291
+ Scan central cache and remove orphaned environments.
292
+
293
+ An env is orphaned if its marker file no longer exists
294
+ (meaning the node was deleted).
295
+
296
+ Args:
297
+ log: Logging function
298
+
299
+ Returns:
300
+ Number of envs cleaned up
301
+ """
302
+ cache_dir = get_cache_dir()
303
+ if not cache_dir.exists():
304
+ return 0
305
+
306
+ cleaned = 0
307
+ for env_dir in cache_dir.iterdir():
308
+ if not env_dir.is_dir():
309
+ continue
310
+
311
+ # Skip if no metadata (might be manually created or old format)
312
+ metadata = read_env_metadata(env_dir)
313
+ if not metadata:
314
+ continue
315
+
316
+ # Check if marker file still exists
317
+ marker_path_str = metadata.get("marker_path", "")
318
+ if not marker_path_str:
319
+ continue
320
+
321
+ marker_path = Path(marker_path_str)
322
+ if not marker_path.exists():
323
+ # Marker gone = node was deleted = orphan
324
+ log(f"[comfy-env] Cleaning orphaned env: {env_dir.name}")
325
+ try:
326
+ shutil.rmtree(env_dir)
327
+ cleaned += 1
328
+ except Exception as e:
329
+ log(f"[comfy-env] Failed to cleanup {env_dir.name}: {e}")
330
+
331
+ return cleaned
@@ -36,7 +36,7 @@ _DEBUG = os.environ.get("COMFY_ENV_DEBUG", "").lower() in ("1", "true", "yes")
36
36
 
37
37
 
38
38
  def get_env_name(dir_name: str) -> str:
39
- """Convert directory name to env name: ComfyUI-UniRig _env_unirig"""
39
+ """Convert directory name to env name: ComfyUI-UniRig -> _env_unirig"""
40
40
  name = dir_name.lower().replace("-", "_").lstrip("comfyui_")
41
41
  return f"_env_{name}"
42
42
 
@@ -145,15 +145,19 @@ def _find_env_paths(node_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
145
145
  """
146
146
  Find site-packages and lib directories for the isolated environment.
147
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
+
148
154
  Returns:
149
155
  (site_packages, lib_dir) - lib_dir is for LD_LIBRARY_PATH
150
156
  """
151
157
  import glob
152
158
 
153
- # Check _env_<name> directory first (new pattern)
154
- env_name = get_env_name(node_dir.name)
155
- env_dir = node_dir / env_name
156
- if env_dir.exists():
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."""
157
161
  if sys.platform == "win32":
158
162
  site_packages = env_dir / "Lib" / "site-packages"
159
163
  lib_dir = env_dir / "Library" / "bin"
@@ -163,23 +167,45 @@ def _find_env_paths(node_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
163
167
  site_packages = Path(matches[0]) if matches else None
164
168
  lib_dir = env_dir / "lib"
165
169
  if site_packages and site_packages.exists():
166
- return site_packages, lib_dir if lib_dir.exists() else None
170
+ return site_packages, lib_dir if lib_dir and lib_dir.exists() else None
171
+ return None, None
167
172
 
168
- # Fallback: Check old .pixi/envs/default (for backward compat)
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)
169
202
  pixi_env = node_dir / ".pixi" / "envs" / "default"
170
203
  if pixi_env.exists():
171
- if sys.platform == "win32":
172
- site_packages = pixi_env / "Lib" / "site-packages"
173
- lib_dir = pixi_env / "Library" / "bin"
174
- else:
175
- pattern = str(pixi_env / "lib" / "python*" / "site-packages")
176
- matches = glob.glob(pattern)
177
- site_packages = Path(matches[0]) if matches else None
178
- lib_dir = pixi_env / "lib"
179
- if site_packages and site_packages.exists():
180
- return site_packages, lib_dir if lib_dir.exists() else None
204
+ result = _get_paths_from_env(pixi_env)
205
+ if result[0]:
206
+ return result
181
207
 
182
- # Check .venv directory
208
+ # 4. Check .venv directory
183
209
  venv_dir = node_dir / ".venv"
184
210
  if venv_dir.exists():
185
211
  if sys.platform == "win32":
@@ -195,19 +221,49 @@ def _find_env_paths(node_dir: Path) -> tuple[Optional[Path], Optional[Path]]:
195
221
 
196
222
 
197
223
  def _find_env_dir(node_dir: Path) -> Optional[Path]:
198
- """Find the environment directory (for cache key)."""
199
- # Check _env_<name> first
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
200
252
  env_name = get_env_name(node_dir.name)
201
253
  env_dir = node_dir / env_name
202
254
  if env_dir.exists():
203
255
  return env_dir
204
- # Fallback to old paths
256
+
257
+ # 3. Fallback to old .pixi path
205
258
  pixi_env = node_dir / ".pixi" / "envs" / "default"
206
259
  if pixi_env.exists():
207
260
  return pixi_env
261
+
262
+ # 4. Check .venv
208
263
  venv_dir = node_dir / ".venv"
209
264
  if venv_dir.exists():
210
265
  return venv_dir
266
+
211
267
  return None
212
268
 
213
269
 
@@ -282,7 +282,7 @@ def ensure_pixi(
282
282
 
283
283
 
284
284
  def get_env_name(dir_name: str) -> str:
285
- """Convert directory name to env name: ComfyUI-UniRig _env_unirig"""
285
+ """Convert directory name to env name: ComfyUI-UniRig -> _env_unirig"""
286
286
  name = dir_name.lower().replace("-", "_").lstrip("comfyui_")
287
287
  return f"_env_{name}"
288
288
 
@@ -534,20 +534,55 @@ def pixi_install(
534
534
 
535
535
  log("CUDA packages installed")
536
536
 
537
- # Move environment from .pixi/envs/default to _env_<name>
537
+ # Move environment from .pixi/envs/default to central cache
538
+ from ..cache import (
539
+ get_central_env_path, write_marker, write_env_metadata,
540
+ MARKER_FILE, get_cache_dir
541
+ )
542
+
538
543
  old_env = node_dir / ".pixi" / "envs" / "default"
539
- env_name = get_env_name(node_dir.name)
540
- new_env = node_dir / env_name
544
+ config_path = node_dir / "comfy-env.toml"
545
+
546
+ # Determine the main node directory (for naming)
547
+ # If node_dir is custom_nodes/NodeName/subdir, main_node_dir is custom_nodes/NodeName
548
+ # If node_dir is custom_nodes/NodeName, main_node_dir is custom_nodes/NodeName
549
+ if node_dir.parent.name == "custom_nodes":
550
+ main_node_dir = node_dir
551
+ else:
552
+ # Walk up to find custom_nodes parent
553
+ main_node_dir = node_dir
554
+ for parent in node_dir.parents:
555
+ if parent.parent.name == "custom_nodes":
556
+ main_node_dir = parent
557
+ break
558
+
559
+ # Get central env path
560
+ central_env = get_central_env_path(main_node_dir, config_path)
541
561
 
542
562
  if old_env.exists():
543
- if new_env.exists():
544
- shutil.rmtree(new_env) # Clean old env
545
- shutil.move(str(old_env), str(new_env))
546
- # Clean up .pixi directory (keep pixi.toml and pixi.lock)
563
+ # Ensure cache directory exists
564
+ get_cache_dir()
565
+
566
+ # Remove old central env if exists
567
+ if central_env.exists():
568
+ shutil.rmtree(central_env)
569
+
570
+ # Move to central cache
571
+ shutil.move(str(old_env), str(central_env))
572
+
573
+ # Write marker file in node directory
574
+ write_marker(config_path, central_env)
575
+
576
+ # Write metadata in env for orphan detection
577
+ marker_path = config_path.parent / MARKER_FILE
578
+ write_env_metadata(central_env, marker_path)
579
+
580
+ # Clean up .pixi directory
547
581
  pixi_dir = node_dir / ".pixi"
548
582
  if pixi_dir.exists():
549
583
  shutil.rmtree(pixi_dir)
550
- log(f"Moved environment to {new_env}")
584
+
585
+ log(f"Environment created at: {central_env}")
551
586
 
552
587
  log("Installation complete!")
553
588
  return True
@@ -12,7 +12,7 @@ from typing import Optional, Dict
12
12
 
13
13
 
14
14
  def get_env_name(dir_name: str) -> str:
15
- """Convert directory name to env name: ComfyUI-UniRig _env_unirig"""
15
+ """Convert directory name to env name: ComfyUI-UniRig -> _env_unirig"""
16
16
  name = dir_name.lower().replace("-", "_").lstrip("comfyui_")
17
17
  return f"_env_{name}"
18
18
 
@@ -127,15 +127,43 @@ def setup_env(node_dir: Optional[str] = None) -> None:
127
127
  for key, value in env_vars.items():
128
128
  os.environ[key] = value
129
129
 
130
- # Check _env_<name> first, then fallback to old .pixi path
131
- env_name = get_env_name(os.path.basename(node_dir))
132
- pixi_env = os.path.join(node_dir, env_name)
133
-
134
- if not os.path.exists(pixi_env):
135
- # Fallback to old .pixi path
136
- pixi_env = os.path.join(node_dir, ".pixi", "envs", "default")
137
- if not os.path.exists(pixi_env):
138
- return # No environment found
130
+ # Resolve environment path with fallback chain:
131
+ # 1. Marker file -> central cache
132
+ # 2. _env_<name> (current local)
133
+ # 3. .pixi/envs/default (old pixi)
134
+ pixi_env = None
135
+
136
+ # 1. Check marker file -> central cache
137
+ marker_path = os.path.join(node_dir, ".comfy-env-marker.toml")
138
+ if os.path.exists(marker_path):
139
+ try:
140
+ if sys.version_info >= (3, 11):
141
+ import tomllib
142
+ else:
143
+ import tomli as tomllib
144
+ with open(marker_path, "rb") as f:
145
+ marker = tomllib.load(f)
146
+ env_path = marker.get("env", {}).get("path")
147
+ if env_path and os.path.exists(env_path):
148
+ pixi_env = env_path
149
+ except Exception:
150
+ pass # Fall through to other options
151
+
152
+ # 2. Check _env_<name> (local)
153
+ if not pixi_env:
154
+ env_name = get_env_name(os.path.basename(node_dir))
155
+ local_env = os.path.join(node_dir, env_name)
156
+ if os.path.exists(local_env):
157
+ pixi_env = local_env
158
+
159
+ # 3. Fallback to old .pixi path
160
+ if not pixi_env:
161
+ old_pixi = os.path.join(node_dir, ".pixi", "envs", "default")
162
+ if os.path.exists(old_pixi):
163
+ pixi_env = old_pixi
164
+
165
+ if not pixi_env:
166
+ return # No environment found
139
167
 
140
168
  if sys.platform == "win32":
141
169
  # Windows: add to PATH for DLL loading
@@ -29,6 +29,7 @@ from queue import Empty as QueueEmpty
29
29
  from typing import Any, Callable, Optional
30
30
 
31
31
  from .base import Worker, WorkerError
32
+ from .tensor_utils import prepare_for_ipc_recursive, keep_tensors_recursive
32
33
 
33
34
  logger = logging.getLogger("comfy_env")
34
35
 
@@ -40,8 +41,20 @@ _SHUTDOWN = object()
40
41
  _CALL_METHOD = "call_method"
41
42
 
42
43
 
44
+ def _can_use_cuda_ipc():
45
+ """
46
+ Check if CUDA IPC is available.
47
+
48
+ CUDA IPC works with native allocator but breaks with cudaMallocAsync.
49
+ If no backend is specified, CUDA IPC should work (PyTorch default is native).
50
+ """
51
+ import os
52
+ conf = os.environ.get('PYTORCH_CUDA_ALLOC_CONF', '')
53
+ return 'cudaMallocAsync' not in conf
54
+
55
+
43
56
  # ---------------------------------------------------------------------------
44
- # Tensor file transfer - avoids CUDA IPC issues with cudaMallocAsync
57
+ # Tensor file transfer - fallback for cudaMallocAsync (CUDA IPC doesn't work)
45
58
  # ---------------------------------------------------------------------------
46
59
 
47
60
  def _save_tensors_to_files(obj, file_registry=None):
@@ -252,20 +265,31 @@ def _worker_loop(queue_in, queue_out, sys_path_additions=None, lib_path=None, en
252
265
  # Handle method call protocol
253
266
  if isinstance(item, tuple) and len(item) == 6 and item[0] == _CALL_METHOD:
254
267
  _, module_name, class_name, method_name, self_state, kwargs = item
255
- # Load tensors from files (saved by host to avoid cudaMallocAsync IPC issues)
256
- kwargs = _load_tensors_from_files(kwargs)
268
+ # Load tensors from files if using file-based transfer
269
+ if not _can_use_cuda_ipc():
270
+ kwargs = _load_tensors_from_files(kwargs)
257
271
  result = _execute_method_call(
258
272
  module_name, class_name, method_name, self_state, kwargs
259
273
  )
260
- # Save tensors to files to avoid CUDA IPC issues with cudaMallocAsync
261
- result = _save_tensors_to_files(result)
274
+ # Handle result based on allocator
275
+ if _can_use_cuda_ipc():
276
+ keep_tensors_recursive(result)
277
+ else:
278
+ result = _save_tensors_to_files(result)
262
279
  queue_out.put(("ok", result))
263
280
  else:
264
281
  # Direct function call (legacy)
265
282
  func, args, kwargs = item
283
+ # Load tensors from files if using file-based transfer
284
+ if not _can_use_cuda_ipc():
285
+ args = tuple(_load_tensors_from_files(a) for a in args)
286
+ kwargs = _load_tensors_from_files(kwargs)
266
287
  result = func(*args, **kwargs)
267
- # Save tensors to files to avoid CUDA IPC issues with cudaMallocAsync
268
- result = _save_tensors_to_files(result)
288
+ # Handle result based on allocator
289
+ if _can_use_cuda_ipc():
290
+ keep_tensors_recursive(result)
291
+ else:
292
+ result = _save_tensors_to_files(result)
269
293
  queue_out.put(("ok", result))
270
294
 
271
295
  except Exception as e:
@@ -661,6 +685,16 @@ class MPWorker(Worker):
661
685
  """
662
686
  self._ensure_started()
663
687
 
688
+ # Handle tensors based on allocator
689
+ if _can_use_cuda_ipc():
690
+ # CUDA IPC - zero copy (works with native allocator)
691
+ kwargs = {k: prepare_for_ipc_recursive(v) for k, v in kwargs.items()}
692
+ args = tuple(prepare_for_ipc_recursive(a) for a in args)
693
+ else:
694
+ # File-based transfer (fallback for cudaMallocAsync)
695
+ kwargs = _save_tensors_to_files(kwargs)
696
+ args = tuple(_save_tensors_to_files(a) for a in args)
697
+
664
698
  # Send work item
665
699
  self._queue_in.put((func, args, kwargs))
666
700
 
@@ -699,8 +733,13 @@ class MPWorker(Worker):
699
733
  """
700
734
  self._ensure_started()
701
735
 
702
- # Save tensors to files to avoid CUDA IPC issues with cudaMallocAsync
703
- kwargs = _save_tensors_to_files(kwargs)
736
+ # Handle tensors based on allocator
737
+ if _can_use_cuda_ipc():
738
+ # CUDA IPC - zero copy (works with native allocator)
739
+ kwargs = prepare_for_ipc_recursive(kwargs)
740
+ else:
741
+ # File-based transfer (fallback for cudaMallocAsync)
742
+ kwargs = _save_tensors_to_files(kwargs)
704
743
 
705
744
  # Send method call request using protocol
706
745
  self._queue_in.put((
@@ -728,8 +767,9 @@ class MPWorker(Worker):
728
767
 
729
768
  # Handle response
730
769
  if status == "ok":
731
- # Load tensors from temp files
732
- result = _load_tensors_from_files(result)
770
+ # Load tensors from temp files if using file-based transfer
771
+ if not _can_use_cuda_ipc():
772
+ result = _load_tensors_from_files(result)
733
773
  return result
734
774
  elif status == "error":
735
775
  msg, tb = result
@@ -211,7 +211,7 @@ def _to_shm(obj, registry, visited=None):
211
211
 
212
212
  t = type(obj).__name__
213
213
 
214
- # numpy array direct shared memory
214
+ # numpy array -> direct shared memory
215
215
  if t == 'ndarray':
216
216
  arr = np.ascontiguousarray(obj)
217
217
  block = shm.SharedMemory(create=True, size=arr.nbytes)
@@ -221,14 +221,14 @@ def _to_shm(obj, registry, visited=None):
221
221
  visited[obj_id] = result
222
222
  return result
223
223
 
224
- # torch.Tensor convert to numpy shared memory (with marker to restore type)
224
+ # torch.Tensor -> convert to numpy -> shared memory (with marker to restore type)
225
225
  if t == 'Tensor':
226
226
  arr = obj.detach().cpu().numpy()
227
227
  result = _to_shm(arr, registry, visited)
228
228
  result["__was_tensor__"] = True
229
229
  return result
230
230
 
231
- # trimesh.Trimesh pickle shared memory (preserves visual, metadata, normals)
231
+ # trimesh.Trimesh -> pickle -> shared memory (preserves visual, metadata, normals)
232
232
  if t == 'Trimesh':
233
233
  import pickle
234
234
  mesh_bytes = pickle.dumps(obj)
@@ -245,7 +245,7 @@ def _to_shm(obj, registry, visited=None):
245
245
  visited[obj_id] = result
246
246
  return result
247
247
 
248
- # Path string
248
+ # Path -> string
249
249
  from pathlib import PurePath
250
250
  if isinstance(obj, PurePath):
251
251
  return str(obj)
@@ -530,7 +530,7 @@ def _to_shm(obj, registry, visited=None):
530
530
  result["__was_tensor__"] = True
531
531
  return result
532
532
 
533
- # trimesh.Trimesh pickle shared memory (preserves visual, metadata, normals)
533
+ # trimesh.Trimesh -> pickle -> shared memory (preserves visual, metadata, normals)
534
534
  if t == 'Trimesh':
535
535
  import pickle
536
536
  mesh_bytes = pickle.dumps(obj)
File without changes
File without changes
File without changes