comfy-env 0.0.8__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 +161 -0
- comfy_env/cli.py +388 -0
- comfy_env/decorator.py +422 -0
- comfy_env/env/__init__.py +30 -0
- comfy_env/env/config.py +144 -0
- comfy_env/env/config_file.py +592 -0
- comfy_env/env/detection.py +176 -0
- comfy_env/env/manager.py +657 -0
- comfy_env/env/platform/__init__.py +21 -0
- comfy_env/env/platform/base.py +96 -0
- comfy_env/env/platform/darwin.py +53 -0
- comfy_env/env/platform/linux.py +68 -0
- comfy_env/env/platform/windows.py +377 -0
- comfy_env/env/security.py +267 -0
- comfy_env/errors.py +325 -0
- comfy_env/install.py +539 -0
- comfy_env/ipc/__init__.py +55 -0
- comfy_env/ipc/bridge.py +512 -0
- comfy_env/ipc/protocol.py +265 -0
- comfy_env/ipc/tensor.py +371 -0
- comfy_env/ipc/torch_bridge.py +401 -0
- comfy_env/ipc/transport.py +318 -0
- comfy_env/ipc/worker.py +221 -0
- comfy_env/registry.py +252 -0
- comfy_env/resolver.py +399 -0
- comfy_env/runner.py +273 -0
- comfy_env/stubs/__init__.py +1 -0
- comfy_env/stubs/folder_paths.py +57 -0
- comfy_env/workers/__init__.py +49 -0
- comfy_env/workers/base.py +82 -0
- comfy_env/workers/pool.py +241 -0
- comfy_env/workers/tensor_utils.py +188 -0
- comfy_env/workers/torch_mp.py +375 -0
- comfy_env/workers/venv.py +903 -0
- comfy_env-0.0.8.dist-info/METADATA +228 -0
- comfy_env-0.0.8.dist-info/RECORD +39 -0
- comfy_env-0.0.8.dist-info/WHEEL +4 -0
- comfy_env-0.0.8.dist-info/entry_points.txt +2 -0
- comfy_env-0.0.8.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"""Load IsolatedEnv configuration from TOML files.
|
|
2
|
+
|
|
3
|
+
This module provides declarative configuration for isolated environments,
|
|
4
|
+
allowing custom nodes to define their requirements in a TOML file instead
|
|
5
|
+
of programmatically.
|
|
6
|
+
|
|
7
|
+
Config file: comfy-env.toml
|
|
8
|
+
|
|
9
|
+
Simplified config (recommended for CUDA packages only):
|
|
10
|
+
|
|
11
|
+
# comfy-env.toml - just list CUDA packages
|
|
12
|
+
[cuda]
|
|
13
|
+
torch-scatter = "2.1.2"
|
|
14
|
+
torch-cluster = "1.6.3"
|
|
15
|
+
spconv = "*" # latest compatible
|
|
16
|
+
|
|
17
|
+
Or as a list:
|
|
18
|
+
|
|
19
|
+
cuda = [
|
|
20
|
+
"torch-scatter==2.1.2",
|
|
21
|
+
"torch-cluster==1.6.3",
|
|
22
|
+
"spconv",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
Optional overrides:
|
|
26
|
+
|
|
27
|
+
[env]
|
|
28
|
+
cuda = "12.4" # Override auto-detection
|
|
29
|
+
pytorch = "2.5.1" # Override auto-detection
|
|
30
|
+
|
|
31
|
+
Full format:
|
|
32
|
+
|
|
33
|
+
[env]
|
|
34
|
+
name = "my-node"
|
|
35
|
+
python = "3.10"
|
|
36
|
+
cuda = "auto"
|
|
37
|
+
|
|
38
|
+
[packages]
|
|
39
|
+
requirements = ["my-package>=1.0.0"]
|
|
40
|
+
no_deps = ["torch-scatter==2.1.2"]
|
|
41
|
+
|
|
42
|
+
[sources]
|
|
43
|
+
wheel_sources = ["https://my-wheels.github.io/"]
|
|
44
|
+
|
|
45
|
+
Available auto-derived variables:
|
|
46
|
+
- {cuda_version}: Full CUDA version (e.g., "12.8")
|
|
47
|
+
- {cuda_short}: CUDA version without dot (e.g., "128")
|
|
48
|
+
- {pytorch_version}: Full PyTorch version (e.g., "2.9.1")
|
|
49
|
+
- {pytorch_short}: PyTorch version without dots (e.g., "291")
|
|
50
|
+
- {pytorch_mm}: PyTorch major.minor without dot (e.g., "29")
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
import sys
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
from typing import Optional, Dict, Any, List
|
|
56
|
+
|
|
57
|
+
# Use built-in tomllib (Python 3.11+) or tomli fallback
|
|
58
|
+
if sys.version_info >= (3, 11):
|
|
59
|
+
import tomllib
|
|
60
|
+
else:
|
|
61
|
+
try:
|
|
62
|
+
import tomli as tomllib
|
|
63
|
+
except ImportError:
|
|
64
|
+
tomllib = None # type: ignore
|
|
65
|
+
|
|
66
|
+
from .config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq
|
|
67
|
+
from .detection import detect_cuda_version
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Config file name
|
|
71
|
+
CONFIG_FILE_NAMES = [
|
|
72
|
+
"comfy-env.toml",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_env_from_file(
|
|
77
|
+
path: Path,
|
|
78
|
+
base_dir: Optional[Path] = None,
|
|
79
|
+
) -> IsolatedEnv:
|
|
80
|
+
"""
|
|
81
|
+
Load IsolatedEnv configuration from a TOML file.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
path: Path to the TOML config file
|
|
85
|
+
base_dir: Base directory for resolving relative paths (default: file's parent)
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Configured IsolatedEnv instance
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
FileNotFoundError: If config file doesn't exist
|
|
92
|
+
ValueError: If config is invalid
|
|
93
|
+
ImportError: If tomli is not installed (Python < 3.11)
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> env = load_env_from_file(Path("my_node/comfy-env.toml"))
|
|
97
|
+
>>> print(env.name)
|
|
98
|
+
'my-node'
|
|
99
|
+
"""
|
|
100
|
+
if tomllib is None:
|
|
101
|
+
raise ImportError(
|
|
102
|
+
"TOML parsing requires tomli for Python < 3.11. "
|
|
103
|
+
"Install it with: pip install tomli"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
path = Path(path)
|
|
107
|
+
if not path.exists():
|
|
108
|
+
raise FileNotFoundError(f"Config file not found: {path}")
|
|
109
|
+
|
|
110
|
+
base_dir = Path(base_dir) if base_dir else path.parent
|
|
111
|
+
|
|
112
|
+
with open(path, "rb") as f:
|
|
113
|
+
data = tomllib.load(f)
|
|
114
|
+
|
|
115
|
+
return _parse_config(data, base_dir)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def discover_env_config(
|
|
119
|
+
node_dir: Path,
|
|
120
|
+
file_names: Optional[List[str]] = None,
|
|
121
|
+
) -> Optional[IsolatedEnv]:
|
|
122
|
+
"""
|
|
123
|
+
Auto-discover and load config from a node directory.
|
|
124
|
+
|
|
125
|
+
Searches for standard config file names in order of priority.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
node_dir: Directory to search for config files
|
|
129
|
+
file_names: Custom list of file names to search (default: CONFIG_FILE_NAMES)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
IsolatedEnv if config found, None otherwise
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> env = discover_env_config(Path("my_custom_node/"))
|
|
136
|
+
>>> if env:
|
|
137
|
+
... print(f"Found config: {env.name}")
|
|
138
|
+
... else:
|
|
139
|
+
... print("No config file found")
|
|
140
|
+
"""
|
|
141
|
+
if tomllib is None:
|
|
142
|
+
# Can't parse TOML without the library
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
node_dir = Path(node_dir)
|
|
146
|
+
file_names = file_names or CONFIG_FILE_NAMES
|
|
147
|
+
|
|
148
|
+
for name in file_names:
|
|
149
|
+
config_path = node_dir / name
|
|
150
|
+
if config_path.exists():
|
|
151
|
+
return load_env_from_file(config_path, node_dir)
|
|
152
|
+
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _get_default_pytorch_version(cuda_version: Optional[str]) -> str:
|
|
157
|
+
"""
|
|
158
|
+
Get default PyTorch version based on CUDA version.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
cuda_version: CUDA version (e.g., "12.4", "12.8") or None
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
PyTorch version string
|
|
165
|
+
|
|
166
|
+
Version Mapping:
|
|
167
|
+
- CUDA 12.4 (Pascal): PyTorch 2.5.1
|
|
168
|
+
- CUDA 12.8 (Turing+): PyTorch 2.8.0
|
|
169
|
+
"""
|
|
170
|
+
if cuda_version == "12.4":
|
|
171
|
+
return "2.5.1" # Legacy: Pascal GPUs
|
|
172
|
+
return "2.8.0" # Modern: Turing through Blackwell
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
|
|
176
|
+
"""
|
|
177
|
+
Parse TOML data into IsolatedEnv.
|
|
178
|
+
|
|
179
|
+
Supports both simplified and legacy config formats:
|
|
180
|
+
|
|
181
|
+
Simplified (CUDA packages only):
|
|
182
|
+
[packages]
|
|
183
|
+
torch-scatter = "2.1.2"
|
|
184
|
+
torch-cluster = "1.6.3"
|
|
185
|
+
|
|
186
|
+
Or as list:
|
|
187
|
+
packages = ["torch-scatter==2.1.2", "torch-cluster==1.6.3"]
|
|
188
|
+
|
|
189
|
+
Legacy:
|
|
190
|
+
[env]
|
|
191
|
+
name = "my-node"
|
|
192
|
+
[packages]
|
|
193
|
+
no_deps = ["torch-scatter==2.1.2"]
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
data: Parsed TOML data
|
|
197
|
+
base_dir: Base directory for resolving relative paths
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Configured IsolatedEnv instance
|
|
201
|
+
"""
|
|
202
|
+
env_section = data.get("env", {})
|
|
203
|
+
packages_section = data.get("packages", {})
|
|
204
|
+
sources_section = data.get("sources", {})
|
|
205
|
+
worker_section = data.get("worker", {})
|
|
206
|
+
variables = dict(data.get("variables", {})) # Copy to avoid mutation
|
|
207
|
+
|
|
208
|
+
# Handle CUDA version - default to "auto" if not specified
|
|
209
|
+
cuda = env_section.get("cuda", "auto")
|
|
210
|
+
if cuda == "auto":
|
|
211
|
+
cuda = detect_cuda_version()
|
|
212
|
+
elif cuda == "null" or cuda == "none":
|
|
213
|
+
cuda = None
|
|
214
|
+
|
|
215
|
+
# Add auto-derived variables based on CUDA
|
|
216
|
+
if cuda:
|
|
217
|
+
variables.setdefault("cuda_version", cuda)
|
|
218
|
+
variables.setdefault("cuda_short", cuda.replace(".", ""))
|
|
219
|
+
|
|
220
|
+
# Handle pytorch version - auto-derive if "auto" or not specified
|
|
221
|
+
pytorch_version = env_section.get("pytorch_version") or env_section.get("pytorch")
|
|
222
|
+
if pytorch_version == "auto" or (pytorch_version is None and cuda):
|
|
223
|
+
pytorch_version = _get_default_pytorch_version(cuda)
|
|
224
|
+
|
|
225
|
+
if pytorch_version:
|
|
226
|
+
variables.setdefault("pytorch_version", pytorch_version)
|
|
227
|
+
# Add short version without dots (e.g., "2.9.1" -> "291")
|
|
228
|
+
pytorch_short = pytorch_version.replace(".", "")
|
|
229
|
+
variables.setdefault("pytorch_short", pytorch_short)
|
|
230
|
+
# Add major.minor without dot (e.g., "2.9.1" -> "29") for wheel naming
|
|
231
|
+
parts = pytorch_version.split(".")[:2]
|
|
232
|
+
pytorch_mm = "".join(parts)
|
|
233
|
+
variables.setdefault("pytorch_mm", pytorch_mm)
|
|
234
|
+
|
|
235
|
+
# Parse CUDA packages - support multiple formats
|
|
236
|
+
# Priority: [cuda] section > cuda = [...] > legacy [packages] section
|
|
237
|
+
no_deps_requirements = []
|
|
238
|
+
requirements = []
|
|
239
|
+
|
|
240
|
+
cuda_section = data.get("cuda", {})
|
|
241
|
+
|
|
242
|
+
if cuda_section:
|
|
243
|
+
# New format: [cuda] section or cuda = [...]
|
|
244
|
+
if isinstance(cuda_section, list):
|
|
245
|
+
# Format: cuda = ["torch-scatter==2.1.2", ...]
|
|
246
|
+
no_deps_requirements = [_substitute_vars(req, variables) for req in cuda_section]
|
|
247
|
+
elif isinstance(cuda_section, dict):
|
|
248
|
+
# Format: [cuda] with package = "version" pairs
|
|
249
|
+
for pkg, ver in cuda_section.items():
|
|
250
|
+
if ver == "*" or ver == "":
|
|
251
|
+
no_deps_requirements.append(pkg)
|
|
252
|
+
else:
|
|
253
|
+
no_deps_requirements.append(f"{pkg}=={ver}")
|
|
254
|
+
|
|
255
|
+
elif isinstance(packages_section, list):
|
|
256
|
+
# Legacy format: packages = ["torch-scatter==2.1.2", ...]
|
|
257
|
+
no_deps_requirements = [_substitute_vars(req, variables) for req in packages_section]
|
|
258
|
+
|
|
259
|
+
elif isinstance(packages_section, dict):
|
|
260
|
+
# Check for simplified format: [packages] with key=value pairs
|
|
261
|
+
# vs legacy format: [packages] with requirements/no_deps lists
|
|
262
|
+
|
|
263
|
+
has_legacy_keys = any(k in packages_section for k in ["requirements", "no_deps", "requirements_file"])
|
|
264
|
+
|
|
265
|
+
if has_legacy_keys:
|
|
266
|
+
# Legacy format
|
|
267
|
+
raw_requirements = packages_section.get("requirements", [])
|
|
268
|
+
requirements = [_substitute_vars(req, variables) for req in raw_requirements]
|
|
269
|
+
|
|
270
|
+
raw_no_deps = packages_section.get("no_deps", [])
|
|
271
|
+
no_deps_requirements = [_substitute_vars(req, variables) for req in raw_no_deps]
|
|
272
|
+
else:
|
|
273
|
+
# Simplified format: [packages] with package = "version" pairs
|
|
274
|
+
# All packages are CUDA packages
|
|
275
|
+
for pkg, ver in packages_section.items():
|
|
276
|
+
if ver == "*" or ver == "":
|
|
277
|
+
no_deps_requirements.append(pkg)
|
|
278
|
+
else:
|
|
279
|
+
no_deps_requirements.append(f"{pkg}=={ver}")
|
|
280
|
+
|
|
281
|
+
# Resolve requirements_file path (relative to base_dir)
|
|
282
|
+
requirements_file = None
|
|
283
|
+
if isinstance(packages_section, dict) and "requirements_file" in packages_section:
|
|
284
|
+
req_file_path = packages_section["requirements_file"]
|
|
285
|
+
requirements_file = base_dir / req_file_path
|
|
286
|
+
|
|
287
|
+
# Get wheel sources and index URLs (optional - registry handles most cases now)
|
|
288
|
+
wheel_sources = sources_section.get("wheel_sources", [])
|
|
289
|
+
index_urls = sources_section.get("index_urls", [])
|
|
290
|
+
|
|
291
|
+
# Parse worker configuration
|
|
292
|
+
worker_package = worker_section.get("package")
|
|
293
|
+
worker_script = worker_section.get("script")
|
|
294
|
+
|
|
295
|
+
return IsolatedEnv(
|
|
296
|
+
name=env_section.get("name", base_dir.name),
|
|
297
|
+
python=env_section.get("python", "3.10"),
|
|
298
|
+
cuda=cuda,
|
|
299
|
+
pytorch_version=pytorch_version,
|
|
300
|
+
requirements=requirements,
|
|
301
|
+
no_deps_requirements=no_deps_requirements,
|
|
302
|
+
requirements_file=requirements_file,
|
|
303
|
+
wheel_sources=wheel_sources,
|
|
304
|
+
index_urls=index_urls,
|
|
305
|
+
worker_package=worker_package,
|
|
306
|
+
worker_script=worker_script,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _substitute_vars(s: str, variables: Dict[str, str]) -> str:
|
|
311
|
+
"""
|
|
312
|
+
Substitute {var_name} placeholders with values from variables dict.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
s: String with placeholders like {var_name}
|
|
316
|
+
variables: Dictionary mapping variable names to values
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
String with placeholders replaced
|
|
320
|
+
|
|
321
|
+
Example:
|
|
322
|
+
>>> _substitute_vars("torch=={pytorch_version}", {"pytorch_version": "2.4.1"})
|
|
323
|
+
'torch==2.4.1'
|
|
324
|
+
"""
|
|
325
|
+
for key, value in variables.items():
|
|
326
|
+
s = s.replace(f"{{{key}}}", str(value))
|
|
327
|
+
return s
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# =============================================================================
|
|
331
|
+
# V2 Schema Parser
|
|
332
|
+
# =============================================================================
|
|
333
|
+
|
|
334
|
+
# Reserved table names that are NOT isolated environments
|
|
335
|
+
RESERVED_TABLES = {"local", "node_reqs", "env", "packages", "sources", "cuda", "variables", "worker"}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def load_config(
|
|
339
|
+
path: Path,
|
|
340
|
+
base_dir: Optional[Path] = None,
|
|
341
|
+
) -> EnvManagerConfig:
|
|
342
|
+
"""
|
|
343
|
+
Load full EnvManagerConfig from a TOML file (v2 schema).
|
|
344
|
+
|
|
345
|
+
Supports both v2 schema and legacy format (auto-detected).
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
path: Path to the TOML config file
|
|
349
|
+
base_dir: Base directory for resolving relative paths
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
EnvManagerConfig with local, envs, and node_reqs
|
|
353
|
+
|
|
354
|
+
Example:
|
|
355
|
+
>>> config = load_config(Path("comfy-env.toml"))
|
|
356
|
+
>>> if config.has_local:
|
|
357
|
+
... print(f"Local CUDA packages: {config.local.cuda_packages}")
|
|
358
|
+
>>> for name, env in config.envs.items():
|
|
359
|
+
... print(f"Isolated env: {name}")
|
|
360
|
+
"""
|
|
361
|
+
if tomllib is None:
|
|
362
|
+
raise ImportError(
|
|
363
|
+
"TOML parsing requires tomli for Python < 3.11. "
|
|
364
|
+
"Install it with: pip install tomli"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
path = Path(path)
|
|
368
|
+
if not path.exists():
|
|
369
|
+
raise FileNotFoundError(f"Config file not found: {path}")
|
|
370
|
+
|
|
371
|
+
base_dir = Path(base_dir) if base_dir else path.parent
|
|
372
|
+
|
|
373
|
+
with open(path, "rb") as f:
|
|
374
|
+
data = tomllib.load(f)
|
|
375
|
+
|
|
376
|
+
return _parse_config_v2(data, base_dir)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def discover_config(
|
|
380
|
+
node_dir: Path,
|
|
381
|
+
file_names: Optional[List[str]] = None,
|
|
382
|
+
) -> Optional[EnvManagerConfig]:
|
|
383
|
+
"""
|
|
384
|
+
Auto-discover and load EnvManagerConfig from a node directory.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
node_dir: Directory to search for config files
|
|
388
|
+
file_names: Custom list of file names to search
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
EnvManagerConfig if found, None otherwise
|
|
392
|
+
"""
|
|
393
|
+
if tomllib is None:
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
node_dir = Path(node_dir)
|
|
397
|
+
file_names = file_names or CONFIG_FILE_NAMES
|
|
398
|
+
|
|
399
|
+
for name in file_names:
|
|
400
|
+
config_path = node_dir / name
|
|
401
|
+
if config_path.exists():
|
|
402
|
+
return load_config(config_path, node_dir)
|
|
403
|
+
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _parse_config_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
|
|
408
|
+
"""
|
|
409
|
+
Parse TOML data into EnvManagerConfig (v2 schema).
|
|
410
|
+
|
|
411
|
+
Schema:
|
|
412
|
+
[local.cuda] - CUDA packages for host
|
|
413
|
+
[local.packages] - Regular packages for host
|
|
414
|
+
[envname] - Isolated env (python, cuda, pytorch)
|
|
415
|
+
[envname.cuda] - CUDA packages for env
|
|
416
|
+
[envname.packages] - Packages for env
|
|
417
|
+
[node_reqs] - Node dependencies
|
|
418
|
+
|
|
419
|
+
Also supports legacy format for backward compatibility.
|
|
420
|
+
"""
|
|
421
|
+
# Detect if this is v2 schema or legacy
|
|
422
|
+
is_v2 = "local" in data or _has_named_env(data)
|
|
423
|
+
|
|
424
|
+
if not is_v2:
|
|
425
|
+
# Legacy format - convert to v2 structure
|
|
426
|
+
return _convert_legacy_to_v2(data, base_dir)
|
|
427
|
+
|
|
428
|
+
# Parse v2 schema
|
|
429
|
+
local = _parse_local_section(data.get("local", {}))
|
|
430
|
+
envs = _parse_env_sections(data, base_dir)
|
|
431
|
+
node_reqs = _parse_node_reqs(data.get("node_reqs", {}))
|
|
432
|
+
|
|
433
|
+
return EnvManagerConfig(
|
|
434
|
+
local=local,
|
|
435
|
+
envs=envs,
|
|
436
|
+
node_reqs=node_reqs,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _has_named_env(data: Dict[str, Any]) -> bool:
|
|
441
|
+
"""Check if data has any named environment tables (not reserved names)."""
|
|
442
|
+
env_indicators = ["python", "cuda", "pytorch", "cuda_version", "pytorch_version"]
|
|
443
|
+
for key in data.keys():
|
|
444
|
+
if key not in RESERVED_TABLES and isinstance(data[key], dict):
|
|
445
|
+
# Check if it looks like an env definition
|
|
446
|
+
section = data[key]
|
|
447
|
+
if any(k in section for k in env_indicators):
|
|
448
|
+
return True
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _parse_local_section(local_data: Dict[str, Any]) -> LocalConfig:
|
|
453
|
+
"""Parse [local] section."""
|
|
454
|
+
cuda_packages = {}
|
|
455
|
+
requirements = []
|
|
456
|
+
|
|
457
|
+
# [local.cuda] - CUDA packages
|
|
458
|
+
cuda_section = local_data.get("cuda", {})
|
|
459
|
+
if isinstance(cuda_section, dict):
|
|
460
|
+
for pkg, ver in cuda_section.items():
|
|
461
|
+
cuda_packages[pkg] = ver if ver and ver != "*" else ""
|
|
462
|
+
|
|
463
|
+
# [local.packages] - regular packages
|
|
464
|
+
packages_section = local_data.get("packages", {})
|
|
465
|
+
if isinstance(packages_section, dict):
|
|
466
|
+
requirements = packages_section.get("requirements", [])
|
|
467
|
+
elif isinstance(packages_section, list):
|
|
468
|
+
requirements = packages_section
|
|
469
|
+
|
|
470
|
+
return LocalConfig(
|
|
471
|
+
cuda_packages=cuda_packages,
|
|
472
|
+
requirements=requirements,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _parse_env_sections(data: Dict[str, Any], base_dir: Path) -> Dict[str, IsolatedEnv]:
|
|
477
|
+
"""Parse named environment sections."""
|
|
478
|
+
envs = {}
|
|
479
|
+
env_indicators = ["python", "cuda", "pytorch", "cuda_version", "pytorch_version"]
|
|
480
|
+
|
|
481
|
+
for key, value in data.items():
|
|
482
|
+
if key in RESERVED_TABLES:
|
|
483
|
+
continue
|
|
484
|
+
if not isinstance(value, dict):
|
|
485
|
+
continue
|
|
486
|
+
|
|
487
|
+
# Check if this looks like an env definition
|
|
488
|
+
if not any(k in value for k in env_indicators):
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
env = _parse_single_env(key, value, base_dir)
|
|
492
|
+
envs[key] = env
|
|
493
|
+
|
|
494
|
+
return envs
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _parse_single_env(name: str, env_data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
|
|
498
|
+
"""Parse a single isolated environment section."""
|
|
499
|
+
# Get basic env config
|
|
500
|
+
python = env_data.get("python", "3.10")
|
|
501
|
+
# Support both "cuda" and "cuda_version" field names (cuda_version avoids conflict with [envname.cuda] table)
|
|
502
|
+
cuda = env_data.get("cuda_version") or env_data.get("cuda", "auto")
|
|
503
|
+
pytorch = env_data.get("pytorch_version") or env_data.get("pytorch", "auto")
|
|
504
|
+
|
|
505
|
+
# Handle auto-detection
|
|
506
|
+
if cuda == "auto":
|
|
507
|
+
cuda = detect_cuda_version()
|
|
508
|
+
elif cuda in ("null", "none", None):
|
|
509
|
+
cuda = None
|
|
510
|
+
|
|
511
|
+
if pytorch == "auto":
|
|
512
|
+
pytorch = _get_default_pytorch_version(cuda)
|
|
513
|
+
|
|
514
|
+
# Parse [envname.cuda] - CUDA packages
|
|
515
|
+
cuda_section = env_data.get("cuda", {})
|
|
516
|
+
no_deps_requirements = []
|
|
517
|
+
if isinstance(cuda_section, dict):
|
|
518
|
+
for pkg, ver in cuda_section.items():
|
|
519
|
+
if ver == "*" or ver == "":
|
|
520
|
+
no_deps_requirements.append(pkg)
|
|
521
|
+
else:
|
|
522
|
+
no_deps_requirements.append(f"{pkg}=={ver}")
|
|
523
|
+
|
|
524
|
+
# Parse [envname.packages] - regular packages
|
|
525
|
+
packages_section = env_data.get("packages", {})
|
|
526
|
+
requirements = []
|
|
527
|
+
if isinstance(packages_section, dict):
|
|
528
|
+
requirements = packages_section.get("requirements", [])
|
|
529
|
+
elif isinstance(packages_section, list):
|
|
530
|
+
requirements = packages_section
|
|
531
|
+
|
|
532
|
+
return IsolatedEnv(
|
|
533
|
+
name=name,
|
|
534
|
+
python=python,
|
|
535
|
+
cuda=cuda,
|
|
536
|
+
pytorch_version=pytorch,
|
|
537
|
+
requirements=requirements,
|
|
538
|
+
no_deps_requirements=no_deps_requirements,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _parse_node_reqs(node_reqs_data: Dict[str, Any]) -> List[NodeReq]:
|
|
543
|
+
"""Parse [node_reqs] section."""
|
|
544
|
+
reqs = []
|
|
545
|
+
|
|
546
|
+
for name, value in node_reqs_data.items():
|
|
547
|
+
if isinstance(value, str):
|
|
548
|
+
# Simple format: VideoHelperSuite = "Kosinkadink/ComfyUI-VideoHelperSuite"
|
|
549
|
+
reqs.append(NodeReq(name=name, repo=value))
|
|
550
|
+
elif isinstance(value, dict):
|
|
551
|
+
# Extended format: VideoHelperSuite = { repo = "..." }
|
|
552
|
+
repo = value.get("repo", "")
|
|
553
|
+
reqs.append(NodeReq(name=name, repo=repo))
|
|
554
|
+
|
|
555
|
+
return reqs
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _convert_legacy_to_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
|
|
559
|
+
"""Convert legacy config format to v2 EnvManagerConfig."""
|
|
560
|
+
# Parse using legacy parser to get IsolatedEnv
|
|
561
|
+
legacy_env = _parse_config(data, base_dir)
|
|
562
|
+
|
|
563
|
+
# Check if this is really just a Type 2 config (local CUDA only, no venv)
|
|
564
|
+
# Type 2 indicators: no [env] section with name, or name matches directory
|
|
565
|
+
env_section = data.get("env", {})
|
|
566
|
+
has_explicit_env = bool(env_section.get("name") or env_section.get("python"))
|
|
567
|
+
|
|
568
|
+
if has_explicit_env:
|
|
569
|
+
# This is a Type 1 config (isolated venv)
|
|
570
|
+
return EnvManagerConfig(
|
|
571
|
+
local=LocalConfig(),
|
|
572
|
+
envs={legacy_env.name: legacy_env},
|
|
573
|
+
node_reqs=[],
|
|
574
|
+
)
|
|
575
|
+
else:
|
|
576
|
+
# This is a Type 2 config (local CUDA only)
|
|
577
|
+
cuda_packages = {}
|
|
578
|
+
for req in legacy_env.no_deps_requirements:
|
|
579
|
+
if "==" in req:
|
|
580
|
+
pkg, ver = req.split("==", 1)
|
|
581
|
+
cuda_packages[pkg] = ver
|
|
582
|
+
else:
|
|
583
|
+
cuda_packages[req] = ""
|
|
584
|
+
|
|
585
|
+
return EnvManagerConfig(
|
|
586
|
+
local=LocalConfig(
|
|
587
|
+
cuda_packages=cuda_packages,
|
|
588
|
+
requirements=legacy_env.requirements,
|
|
589
|
+
),
|
|
590
|
+
envs={},
|
|
591
|
+
node_reqs=[],
|
|
592
|
+
)
|