comfy-env 0.1.14__py3-none-any.whl → 0.1.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- comfy_env/__init__.py +8 -30
- comfy_env/cache.py +11 -139
- comfy_env/cli.py +9 -11
- comfy_env/config/__init__.py +8 -10
- comfy_env/config/parser.py +28 -75
- comfy_env/install.py +141 -25
- comfy_env/isolation/__init__.py +2 -1
- comfy_env/isolation/wrap.py +136 -15
- comfy_env/nodes.py +1 -1
- comfy_env/pixi/core.py +1 -2
- comfy_env/prestartup.py +31 -15
- comfy_env/workers/subprocess.py +1 -1
- {comfy_env-0.1.14.dist-info → comfy_env-0.1.15.dist-info}/METADATA +2 -2
- comfy_env-0.1.15.dist-info/RECORD +31 -0
- comfy_env/config/types.py +0 -70
- comfy_env/errors.py +0 -293
- comfy_env-0.1.14.dist-info/RECORD +0 -33
- {comfy_env-0.1.14.dist-info → comfy_env-0.1.15.dist-info}/WHEEL +0 -0
- {comfy_env-0.1.14.dist-info → comfy_env-0.1.15.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.1.14.dist-info → comfy_env-0.1.15.dist-info}/licenses/LICENSE +0 -0
comfy_env/__init__.py
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
comfy-env: Environment management for ComfyUI custom nodes.
|
|
3
|
-
|
|
4
|
-
All dependencies go through pixi for unified management.
|
|
5
|
-
|
|
6
|
-
Main APIs:
|
|
7
|
-
- install(): Install dependencies from comfy-env.toml
|
|
8
|
-
- wrap_isolated_nodes(): Wrap nodes for subprocess isolation
|
|
9
|
-
"""
|
|
1
|
+
"""Environment management for ComfyUI custom nodes."""
|
|
10
2
|
|
|
11
3
|
from importlib.metadata import version, PackageNotFoundError
|
|
12
4
|
|
|
@@ -48,13 +40,13 @@ from .workers import (
|
|
|
48
40
|
)
|
|
49
41
|
|
|
50
42
|
# Isolation
|
|
51
|
-
from .isolation import wrap_isolated_nodes
|
|
43
|
+
from .isolation import wrap_isolated_nodes, wrap_nodes
|
|
52
44
|
|
|
53
45
|
# Install API
|
|
54
|
-
from .install import install, verify_installation
|
|
46
|
+
from .install import install, verify_installation, USE_COMFY_ENV_VAR
|
|
55
47
|
|
|
56
48
|
# Prestartup helpers
|
|
57
|
-
from .prestartup import setup_env
|
|
49
|
+
from .prestartup import setup_env, copy_files
|
|
58
50
|
|
|
59
51
|
# Cache management
|
|
60
52
|
from .cache import (
|
|
@@ -65,24 +57,17 @@ from .cache import (
|
|
|
65
57
|
MARKER_FILE,
|
|
66
58
|
)
|
|
67
59
|
|
|
68
|
-
# Errors
|
|
69
|
-
from .errors import (
|
|
70
|
-
EnvManagerError,
|
|
71
|
-
ConfigError,
|
|
72
|
-
WheelNotFoundError,
|
|
73
|
-
DependencyError,
|
|
74
|
-
CUDANotFoundError,
|
|
75
|
-
InstallError,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
60
|
__all__ = [
|
|
79
61
|
# Install API
|
|
80
62
|
"install",
|
|
81
63
|
"verify_installation",
|
|
64
|
+
"USE_COMFY_ENV_VAR",
|
|
82
65
|
# Prestartup
|
|
83
66
|
"setup_env",
|
|
67
|
+
"copy_files",
|
|
84
68
|
# Isolation
|
|
85
69
|
"wrap_isolated_nodes",
|
|
70
|
+
"wrap_nodes",
|
|
86
71
|
# Config
|
|
87
72
|
"ComfyEnvConfig",
|
|
88
73
|
"NodeReq",
|
|
@@ -107,13 +92,6 @@ __all__ = [
|
|
|
107
92
|
"WorkerError",
|
|
108
93
|
"MPWorker",
|
|
109
94
|
"SubprocessWorker",
|
|
110
|
-
# Errors
|
|
111
|
-
"EnvManagerError",
|
|
112
|
-
"ConfigError",
|
|
113
|
-
"WheelNotFoundError",
|
|
114
|
-
"DependencyError",
|
|
115
|
-
"CUDANotFoundError",
|
|
116
|
-
"InstallError",
|
|
117
95
|
# Cache
|
|
118
96
|
"get_cache_dir",
|
|
119
97
|
"cleanup_orphaned_envs",
|
|
@@ -124,7 +102,7 @@ __all__ = [
|
|
|
124
102
|
|
|
125
103
|
# Run orphan cleanup once on module load (silently)
|
|
126
104
|
def _run_startup_cleanup():
|
|
127
|
-
"""Clean orphaned envs on startup.
|
|
105
|
+
"""Clean orphaned envs on startup."""
|
|
128
106
|
try:
|
|
129
107
|
cleanup_orphaned_envs(log=lambda x: None) # Silent
|
|
130
108
|
except Exception:
|
comfy_env/cache.py
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
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
|
-
"""
|
|
1
|
+
"""Central environment cache management."""
|
|
9
2
|
|
|
10
3
|
import hashlib
|
|
11
4
|
import os
|
|
@@ -21,24 +14,8 @@ try:
|
|
|
21
14
|
except ImportError:
|
|
22
15
|
__version__ = "0.0.0-dev"
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
17
|
+
import tomli
|
|
18
|
+
import tomli_w
|
|
42
19
|
|
|
43
20
|
|
|
44
21
|
# Constants
|
|
@@ -70,17 +47,7 @@ def sanitize_name(name: str) -> str:
|
|
|
70
47
|
|
|
71
48
|
|
|
72
49
|
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
|
-
"""
|
|
50
|
+
"""Generate env name: <nodename>_<subfolder>_<hash>."""
|
|
84
51
|
# Get node name
|
|
85
52
|
node_name = sanitize_name(node_dir.name)
|
|
86
53
|
|
|
@@ -109,27 +76,7 @@ def get_central_env_path(node_dir: Path, config_path: Path) -> Path:
|
|
|
109
76
|
|
|
110
77
|
|
|
111
78
|
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
|
-
|
|
79
|
+
"""Write marker file linking node to central env."""
|
|
133
80
|
marker_path = config_path.parent / MARKER_FILE
|
|
134
81
|
marker_data = {
|
|
135
82
|
"env": {
|
|
@@ -144,24 +91,8 @@ comfy_env_version = "{__version__}"
|
|
|
144
91
|
|
|
145
92
|
|
|
146
93
|
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()
|
|
94
|
+
"""Write metadata file for orphan detection."""
|
|
155
95
|
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
96
|
metadata = {
|
|
166
97
|
"marker_path": str(marker_path),
|
|
167
98
|
"created": datetime.now().isoformat(),
|
|
@@ -170,22 +101,9 @@ created = "{datetime.now().isoformat()}"
|
|
|
170
101
|
|
|
171
102
|
|
|
172
103
|
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
|
-
"""
|
|
104
|
+
"""Read marker file, return None if invalid/missing."""
|
|
182
105
|
if not marker_path.exists():
|
|
183
106
|
return None
|
|
184
|
-
|
|
185
|
-
tomli = _get_tomli()
|
|
186
|
-
if not tomli:
|
|
187
|
-
return None
|
|
188
|
-
|
|
189
107
|
try:
|
|
190
108
|
with open(marker_path, "rb") as f:
|
|
191
109
|
return tomli.load(f)
|
|
@@ -194,23 +112,10 @@ def read_marker(marker_path: Path) -> Optional[dict]:
|
|
|
194
112
|
|
|
195
113
|
|
|
196
114
|
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
|
-
"""
|
|
115
|
+
"""Read metadata file from env, return None if invalid/missing."""
|
|
206
116
|
metadata_path = env_path / METADATA_FILE
|
|
207
117
|
if not metadata_path.exists():
|
|
208
118
|
return None
|
|
209
|
-
|
|
210
|
-
tomli = _get_tomli()
|
|
211
|
-
if not tomli:
|
|
212
|
-
return None
|
|
213
|
-
|
|
214
119
|
try:
|
|
215
120
|
with open(metadata_path, "rb") as f:
|
|
216
121
|
return tomli.load(f)
|
|
@@ -219,21 +124,7 @@ def read_env_metadata(env_path: Path) -> Optional[dict]:
|
|
|
219
124
|
|
|
220
125
|
|
|
221
126
|
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
|
-
"""
|
|
127
|
+
"""Resolve environment path. Returns (env_path, site_packages, lib_dir)."""
|
|
237
128
|
# 1. Check marker file -> central cache
|
|
238
129
|
marker_path = node_dir / MARKER_FILE
|
|
239
130
|
marker = read_marker(marker_path)
|
|
@@ -263,15 +154,7 @@ def resolve_env_path(node_dir: Path) -> Tuple[Optional[Path], Optional[Path], Op
|
|
|
263
154
|
|
|
264
155
|
|
|
265
156
|
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
|
-
"""
|
|
157
|
+
"""Get site-packages and lib paths from an environment."""
|
|
275
158
|
import glob
|
|
276
159
|
|
|
277
160
|
if sys.platform == "win32":
|
|
@@ -287,18 +170,7 @@ def _get_env_paths(env_path: Path) -> Tuple[Path, Optional[Path], Optional[Path]
|
|
|
287
170
|
|
|
288
171
|
|
|
289
172
|
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
|
-
"""
|
|
173
|
+
"""Remove orphaned environments. Returns count cleaned."""
|
|
302
174
|
cache_dir = get_cache_dir()
|
|
303
175
|
if not cache_dir.exists():
|
|
304
176
|
return 0
|
comfy_env/cli.py
CHANGED
|
@@ -309,7 +309,7 @@ def cmd_info(args) -> int:
|
|
|
309
309
|
def cmd_doctor(args) -> int:
|
|
310
310
|
"""Handle doctor command."""
|
|
311
311
|
from .install import verify_installation
|
|
312
|
-
from .config.parser import
|
|
312
|
+
from .config.parser import load_config, discover_config
|
|
313
313
|
|
|
314
314
|
print("Running diagnostics...")
|
|
315
315
|
print("=" * 40)
|
|
@@ -325,21 +325,19 @@ def cmd_doctor(args) -> int:
|
|
|
325
325
|
if args.package:
|
|
326
326
|
packages = [args.package]
|
|
327
327
|
elif args.config:
|
|
328
|
-
config =
|
|
328
|
+
config = load_config(Path(args.config))
|
|
329
329
|
if config:
|
|
330
|
-
|
|
330
|
+
# Get packages from pypi-dependencies
|
|
331
|
+
pypi_deps = config.pixi_passthrough.get("pypi-dependencies", {})
|
|
332
|
+
packages = list(pypi_deps.keys()) + config.cuda_packages
|
|
331
333
|
else:
|
|
332
|
-
config =
|
|
334
|
+
config = discover_config(Path.cwd())
|
|
333
335
|
if config:
|
|
334
|
-
|
|
336
|
+
pypi_deps = config.pixi_passthrough.get("pypi-dependencies", {})
|
|
337
|
+
packages = list(pypi_deps.keys()) + config.cuda_packages
|
|
335
338
|
|
|
336
339
|
if packages:
|
|
337
|
-
|
|
338
|
-
for pkg in packages:
|
|
339
|
-
name = pkg.split("==")[0].split(">=")[0].split("[")[0]
|
|
340
|
-
pkg_names.append(name)
|
|
341
|
-
|
|
342
|
-
all_ok = verify_installation(pkg_names)
|
|
340
|
+
all_ok = verify_installation(packages)
|
|
343
341
|
if all_ok:
|
|
344
342
|
print("\nAll packages verified!")
|
|
345
343
|
return 0
|
comfy_env/config/__init__.py
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Config parsing for comfy-env.
|
|
1
|
+
"""Config parsing for comfy-env."""
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
from .parser import (
|
|
4
|
+
ComfyEnvConfig,
|
|
5
|
+
NodeReq,
|
|
6
|
+
load_config,
|
|
7
|
+
discover_config,
|
|
8
|
+
CONFIG_FILE_NAME,
|
|
9
|
+
)
|
|
10
10
|
|
|
11
11
|
__all__ = [
|
|
12
|
-
# Types
|
|
13
12
|
"ComfyEnvConfig",
|
|
14
13
|
"NodeReq",
|
|
15
|
-
# Parser
|
|
16
14
|
"load_config",
|
|
17
15
|
"discover_config",
|
|
18
16
|
"CONFIG_FILE_NAME",
|
comfy_env/config/parser.py
CHANGED
|
@@ -1,100 +1,53 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
comfy-env.toml is a superset of pixi.toml. Custom sections we handle:
|
|
4
|
-
- python = "3.11" - Python version for isolated envs
|
|
5
|
-
- [cuda] packages = [...] - CUDA packages (triggers find-links + PyTorch detection)
|
|
6
|
-
- [node_reqs] - Other ComfyUI nodes to clone
|
|
7
|
-
|
|
8
|
-
Everything else passes through to pixi.toml directly.
|
|
9
|
-
|
|
10
|
-
Example config:
|
|
11
|
-
|
|
12
|
-
python = "3.11"
|
|
13
|
-
|
|
14
|
-
[cuda]
|
|
15
|
-
packages = ["cumesh"]
|
|
16
|
-
|
|
17
|
-
[dependencies]
|
|
18
|
-
mesalib = "*"
|
|
19
|
-
cgal = "*"
|
|
20
|
-
|
|
21
|
-
[pypi-dependencies]
|
|
22
|
-
numpy = ">=1.21.0,<2"
|
|
23
|
-
trimesh = { version = ">=4.0.0", extras = ["easy"] }
|
|
24
|
-
|
|
25
|
-
[target.linux-64.pypi-dependencies]
|
|
26
|
-
embreex = "*"
|
|
1
|
+
"""
|
|
2
|
+
Configuration parsing for comfy-env.
|
|
27
3
|
|
|
28
|
-
|
|
29
|
-
SomeNode = "owner/repo"
|
|
4
|
+
Loads comfy-env.toml (a superset of pixi.toml) and provides typed config objects.
|
|
30
5
|
"""
|
|
31
6
|
|
|
32
7
|
import copy
|
|
33
8
|
import sys
|
|
9
|
+
from dataclasses import dataclass, field
|
|
34
10
|
from pathlib import Path
|
|
35
11
|
from typing import Optional, Dict, Any, List
|
|
12
|
+
import tomli
|
|
36
13
|
|
|
37
|
-
#
|
|
38
|
-
if sys.version_info >= (3, 11):
|
|
39
|
-
import tomllib
|
|
40
|
-
else:
|
|
41
|
-
try:
|
|
42
|
-
import tomli as tomllib
|
|
43
|
-
except ImportError:
|
|
44
|
-
tomllib = None # type: ignore
|
|
45
|
-
|
|
46
|
-
from .types import ComfyEnvConfig, NodeReq
|
|
47
|
-
|
|
48
|
-
|
|
14
|
+
# --- Types&Constants ---
|
|
49
15
|
CONFIG_FILE_NAME = "comfy-env.toml"
|
|
50
16
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
17
|
+
@dataclass
|
|
18
|
+
class NodeReq:
|
|
19
|
+
"""A node dependency (another ComfyUI custom node)."""
|
|
20
|
+
name: str
|
|
21
|
+
repo: str # GitHub repo, e.g., "owner/repo"
|
|
54
22
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
23
|
+
@dataclass
|
|
24
|
+
class ComfyEnvConfig:
|
|
25
|
+
"""Configuration from comfy-env.toml."""
|
|
26
|
+
python: Optional[str] = None
|
|
27
|
+
cuda_packages: List[str] = field(default_factory=list)
|
|
28
|
+
apt_packages: List[str] = field(default_factory=list)
|
|
29
|
+
env_vars: Dict[str, str] = field(default_factory=dict)
|
|
30
|
+
node_reqs: List[NodeReq] = field(default_factory=list)
|
|
31
|
+
pixi_passthrough: Dict[str, Any] = field(default_factory=dict)
|
|
58
32
|
|
|
59
|
-
|
|
60
|
-
|
|
33
|
+
@property
|
|
34
|
+
def has_cuda(self) -> bool:
|
|
35
|
+
return bool(self.cuda_packages)
|
|
36
|
+
# --- Types&Constants ---
|
|
61
37
|
|
|
62
|
-
Returns:
|
|
63
|
-
ComfyEnvConfig instance
|
|
64
|
-
|
|
65
|
-
Raises:
|
|
66
|
-
FileNotFoundError: If config file doesn't exist
|
|
67
|
-
ImportError: If tomli not installed (Python < 3.11)
|
|
68
|
-
"""
|
|
69
|
-
if tomllib is None:
|
|
70
|
-
raise ImportError(
|
|
71
|
-
"TOML parsing requires tomli for Python < 3.11. "
|
|
72
|
-
"Install with: pip install tomli"
|
|
73
|
-
)
|
|
74
38
|
|
|
39
|
+
def load_config(path: Path) -> ComfyEnvConfig:
|
|
40
|
+
"""Load config from a TOML file."""
|
|
75
41
|
path = Path(path)
|
|
76
42
|
if not path.exists():
|
|
77
43
|
raise FileNotFoundError(f"Config file not found: {path}")
|
|
78
|
-
|
|
79
44
|
with open(path, "rb") as f:
|
|
80
|
-
data =
|
|
81
|
-
|
|
45
|
+
data = tomli.load(f)
|
|
82
46
|
return _parse_config(data)
|
|
83
47
|
|
|
84
48
|
|
|
85
49
|
def discover_config(node_dir: Path) -> Optional[ComfyEnvConfig]:
|
|
86
|
-
"""
|
|
87
|
-
Find and load comfy-env.toml from a directory.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
node_dir: Directory to search
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
ComfyEnvConfig if found, None otherwise
|
|
94
|
-
"""
|
|
95
|
-
if tomllib is None:
|
|
96
|
-
return None
|
|
97
|
-
|
|
50
|
+
"""Find and load comfy-env.toml from a directory."""
|
|
98
51
|
config_path = Path(node_dir) / CONFIG_FILE_NAME
|
|
99
52
|
if config_path.exists():
|
|
100
53
|
return load_config(config_path)
|
comfy_env/install.py
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Installation API for comfy-env.
|
|
3
|
-
|
|
4
|
-
Example:
|
|
5
|
-
from comfy_env import install
|
|
6
|
-
install() # Auto-discovers comfy-env.toml and installs
|
|
7
|
-
"""
|
|
1
|
+
"""Installation API for comfy-env."""
|
|
8
2
|
|
|
9
3
|
import inspect
|
|
4
|
+
import os
|
|
10
5
|
from pathlib import Path
|
|
11
6
|
from typing import Callable, List, Optional, Set, Union
|
|
12
7
|
|
|
13
|
-
from .config.
|
|
14
|
-
|
|
8
|
+
from .config.parser import ComfyEnvConfig, NodeReq, load_config, discover_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Environment variable to disable comfy-env isolation
|
|
12
|
+
USE_COMFY_ENV_VAR = "USE_COMFY_ENV"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _is_comfy_env_enabled() -> bool:
|
|
16
|
+
"""Check if isolation is enabled."""
|
|
17
|
+
val = os.environ.get(USE_COMFY_ENV_VAR, "1").lower()
|
|
18
|
+
return val not in ("0", "false", "no", "off")
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
def install(
|
|
@@ -20,18 +24,7 @@ def install(
|
|
|
20
24
|
log_callback: Optional[Callable[[str], None]] = None,
|
|
21
25
|
dry_run: bool = False,
|
|
22
26
|
) -> bool:
|
|
23
|
-
"""
|
|
24
|
-
Install dependencies from comfy-env.toml.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
config: Optional path to comfy-env.toml. Auto-discovered if not provided.
|
|
28
|
-
node_dir: Optional node directory. Auto-discovered from caller if not provided.
|
|
29
|
-
log_callback: Optional callback for logging. Defaults to print.
|
|
30
|
-
dry_run: If True, show what would be installed without installing.
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
True if installation succeeded.
|
|
34
|
-
"""
|
|
27
|
+
"""Install dependencies from comfy-env.toml."""
|
|
35
28
|
# Auto-discover caller's directory if not provided
|
|
36
29
|
if node_dir is None:
|
|
37
30
|
frame = inspect.stack()[1]
|
|
@@ -67,11 +60,17 @@ def install(
|
|
|
67
60
|
if cfg.node_reqs:
|
|
68
61
|
_install_node_dependencies(cfg.node_reqs, node_dir, log, dry_run)
|
|
69
62
|
|
|
70
|
-
#
|
|
71
|
-
|
|
63
|
+
# Check if isolation is enabled
|
|
64
|
+
if _is_comfy_env_enabled():
|
|
65
|
+
# Install everything via pixi (isolated environment)
|
|
66
|
+
_install_via_pixi(cfg, node_dir, log, dry_run)
|
|
72
67
|
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
# Auto-discover and install isolated subdirectory environments
|
|
69
|
+
_install_isolated_subdirs(node_dir, log, dry_run)
|
|
70
|
+
else:
|
|
71
|
+
# Install directly to host Python (no isolation)
|
|
72
|
+
log("\n[comfy-env] Isolation disabled (USE_COMFY_ENV=0)")
|
|
73
|
+
_install_to_host_python(cfg, node_dir, log, dry_run)
|
|
75
74
|
|
|
76
75
|
log("\nInstallation complete!")
|
|
77
76
|
return True
|
|
@@ -291,6 +290,123 @@ def _install_via_pixi(
|
|
|
291
290
|
pixi_install(cfg, node_dir, log)
|
|
292
291
|
|
|
293
292
|
|
|
293
|
+
def _install_to_host_python(
|
|
294
|
+
cfg: ComfyEnvConfig,
|
|
295
|
+
node_dir: Path,
|
|
296
|
+
log: Callable[[str], None],
|
|
297
|
+
dry_run: bool,
|
|
298
|
+
) -> None:
|
|
299
|
+
"""Install packages directly to host Python (no isolation)."""
|
|
300
|
+
import shutil
|
|
301
|
+
import subprocess
|
|
302
|
+
import sys
|
|
303
|
+
|
|
304
|
+
from .pixi import CUDA_WHEELS_INDEX, find_wheel_url
|
|
305
|
+
from .pixi.cuda_detection import get_recommended_cuda_version
|
|
306
|
+
|
|
307
|
+
# Collect packages to install
|
|
308
|
+
pypi_deps = cfg.pixi_passthrough.get("pypi-dependencies", {})
|
|
309
|
+
conda_deps = cfg.pixi_passthrough.get("dependencies", {})
|
|
310
|
+
|
|
311
|
+
# Warn about conda dependencies (can't install without pixi)
|
|
312
|
+
# Filter out 'python' and 'pip' which are meta-dependencies
|
|
313
|
+
real_conda_deps = {k: v for k, v in conda_deps.items() if k not in ("python", "pip")}
|
|
314
|
+
if real_conda_deps:
|
|
315
|
+
log(f"\n[warning] Cannot install conda packages without isolation:")
|
|
316
|
+
for pkg in real_conda_deps:
|
|
317
|
+
log(f" - {pkg}")
|
|
318
|
+
log(" Set USE_COMFY_ENV=1 to enable isolated environments")
|
|
319
|
+
|
|
320
|
+
# Nothing to install?
|
|
321
|
+
if not pypi_deps and not cfg.cuda_packages:
|
|
322
|
+
log("No packages to install")
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
# Build pip install command
|
|
326
|
+
pip_packages = []
|
|
327
|
+
|
|
328
|
+
# Add pypi dependencies
|
|
329
|
+
for pkg, spec in pypi_deps.items():
|
|
330
|
+
if isinstance(spec, str):
|
|
331
|
+
if spec == "*":
|
|
332
|
+
pip_packages.append(pkg)
|
|
333
|
+
else:
|
|
334
|
+
pip_packages.append(f"{pkg}{spec}")
|
|
335
|
+
elif isinstance(spec, dict):
|
|
336
|
+
version = spec.get("version", "*")
|
|
337
|
+
extras = spec.get("extras", [])
|
|
338
|
+
if extras:
|
|
339
|
+
pkg_with_extras = f"{pkg}[{','.join(extras)}]"
|
|
340
|
+
else:
|
|
341
|
+
pkg_with_extras = pkg
|
|
342
|
+
if version == "*":
|
|
343
|
+
pip_packages.append(pkg_with_extras)
|
|
344
|
+
else:
|
|
345
|
+
pip_packages.append(f"{pkg_with_extras}{version}")
|
|
346
|
+
|
|
347
|
+
log(f"\nInstalling to host Python ({sys.executable}):")
|
|
348
|
+
if pip_packages:
|
|
349
|
+
log(f" PyPI packages: {len(pip_packages)}")
|
|
350
|
+
if cfg.cuda_packages:
|
|
351
|
+
log(f" CUDA packages: {', '.join(cfg.cuda_packages)}")
|
|
352
|
+
|
|
353
|
+
if dry_run:
|
|
354
|
+
if pip_packages:
|
|
355
|
+
log(f" Would install: {', '.join(pip_packages)}")
|
|
356
|
+
log("\n(dry run - no changes made)")
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
# Use uv if available, otherwise pip
|
|
360
|
+
use_uv = shutil.which("uv") is not None
|
|
361
|
+
|
|
362
|
+
# Install regular PyPI packages
|
|
363
|
+
if pip_packages:
|
|
364
|
+
if use_uv:
|
|
365
|
+
cmd = ["uv", "pip", "install", "--python", sys.executable] + pip_packages
|
|
366
|
+
else:
|
|
367
|
+
cmd = [sys.executable, "-m", "pip", "install"] + pip_packages
|
|
368
|
+
|
|
369
|
+
log(f" Running: {' '.join(cmd[:4])}...")
|
|
370
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
371
|
+
if result.returncode != 0:
|
|
372
|
+
log(f" [error] pip install failed: {result.stderr.strip()[:200]}")
|
|
373
|
+
else:
|
|
374
|
+
log(f" Installed {len(pip_packages)} package(s)")
|
|
375
|
+
|
|
376
|
+
# Install CUDA packages from cuda-wheels
|
|
377
|
+
if cfg.cuda_packages:
|
|
378
|
+
cuda_version = get_recommended_cuda_version()
|
|
379
|
+
if not cuda_version:
|
|
380
|
+
log(" [warning] No CUDA detected, skipping CUDA packages")
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
# Get torch version for wheel matching
|
|
384
|
+
cuda_mm = ".".join(cuda_version.split(".")[:2])
|
|
385
|
+
from .pixi.core import CUDA_TORCH_MAP
|
|
386
|
+
torch_version = CUDA_TORCH_MAP.get(cuda_mm, "2.8")
|
|
387
|
+
|
|
388
|
+
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
389
|
+
log(f" CUDA {cuda_version}, PyTorch {torch_version}, Python {py_version}")
|
|
390
|
+
|
|
391
|
+
for package in cfg.cuda_packages:
|
|
392
|
+
wheel_url = find_wheel_url(package, torch_version, cuda_version, py_version)
|
|
393
|
+
if not wheel_url:
|
|
394
|
+
log(f" [error] No wheel found for {package}")
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
log(f" Installing {package}...")
|
|
398
|
+
if use_uv:
|
|
399
|
+
cmd = ["uv", "pip", "install", "--python", sys.executable, "--no-deps", wheel_url]
|
|
400
|
+
else:
|
|
401
|
+
cmd = [sys.executable, "-m", "pip", "install", "--no-deps", wheel_url]
|
|
402
|
+
|
|
403
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
404
|
+
if result.returncode != 0:
|
|
405
|
+
log(f" [error] Failed to install {package}: {result.stderr.strip()[:200]}")
|
|
406
|
+
else:
|
|
407
|
+
log(f" Installed {package}")
|
|
408
|
+
|
|
409
|
+
|
|
294
410
|
def _install_isolated_subdirs(
|
|
295
411
|
node_dir: Path,
|
|
296
412
|
log: Callable[[str], None],
|