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.
- {comfy_env-0.1.12 → comfy_env-0.1.14}/PKG-INFO +1 -1
- {comfy_env-0.1.12 → comfy_env-0.1.14}/pyproject.toml +1 -1
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/__init__.py +25 -0
- comfy_env-0.1.14/src/comfy_env/cache.py +331 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/isolation/wrap.py +77 -21
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/core.py +44 -9
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/prestartup.py +38 -10
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/workers/mp.py +51 -11
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/workers/subprocess.py +5 -5
- {comfy_env-0.1.12 → comfy_env-0.1.14}/.github/workflows/ci.yml +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/.github/workflows/publish.yml +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/.gitignore +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/LICENSE +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/README.md +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/cli.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/config/__init__.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/config/parser.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/config/types.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/errors.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/install.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/isolation/__init__.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/nodes.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/__init__.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/cuda_detection.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/platform/__init__.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/platform/base.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/platform/darwin.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/platform/linux.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/platform/windows.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/pixi/resolver.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/templates/comfy-env-instructions.txt +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/templates/comfy-env.toml +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/workers/__init__.py +0 -0
- {comfy_env-0.1.12 → comfy_env-0.1.14}/src/comfy_env/workers/base.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
"""
|
|
199
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
540
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 -
|
|
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
|
|
256
|
-
|
|
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
|
-
#
|
|
261
|
-
|
|
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
|
-
#
|
|
268
|
-
|
|
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
|
-
#
|
|
703
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|