comfy-env 0.0.64__py3-none-any.whl → 0.0.66__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. comfy_env/__init__.py +70 -122
  2. comfy_env/cli.py +78 -7
  3. comfy_env/config/__init__.py +19 -0
  4. comfy_env/config/parser.py +151 -0
  5. comfy_env/config/types.py +64 -0
  6. comfy_env/install.py +83 -361
  7. comfy_env/isolation/__init__.py +9 -0
  8. comfy_env/isolation/wrap.py +351 -0
  9. comfy_env/nodes.py +2 -2
  10. comfy_env/pixi/__init__.py +48 -0
  11. comfy_env/pixi/core.py +356 -0
  12. comfy_env/{resolver.py → pixi/resolver.py} +1 -14
  13. comfy_env/prestartup.py +60 -0
  14. comfy_env/templates/comfy-env-instructions.txt +30 -87
  15. comfy_env/templates/comfy-env.toml +68 -136
  16. comfy_env/workers/__init__.py +21 -32
  17. comfy_env/workers/base.py +1 -1
  18. comfy_env/workers/{torch_mp.py → mp.py} +47 -14
  19. comfy_env/workers/{venv.py → subprocess.py} +405 -441
  20. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/METADATA +2 -1
  21. comfy_env-0.0.66.dist-info/RECORD +34 -0
  22. comfy_env/decorator.py +0 -700
  23. comfy_env/env/__init__.py +0 -47
  24. comfy_env/env/config.py +0 -201
  25. comfy_env/env/config_file.py +0 -740
  26. comfy_env/env/manager.py +0 -636
  27. comfy_env/env/security.py +0 -267
  28. comfy_env/ipc/__init__.py +0 -55
  29. comfy_env/ipc/bridge.py +0 -476
  30. comfy_env/ipc/protocol.py +0 -265
  31. comfy_env/ipc/tensor.py +0 -371
  32. comfy_env/ipc/torch_bridge.py +0 -401
  33. comfy_env/ipc/transport.py +0 -318
  34. comfy_env/ipc/worker.py +0 -221
  35. comfy_env/isolation.py +0 -310
  36. comfy_env/pixi.py +0 -760
  37. comfy_env/stub_imports.py +0 -270
  38. comfy_env/stubs/__init__.py +0 -1
  39. comfy_env/stubs/comfy/__init__.py +0 -6
  40. comfy_env/stubs/comfy/model_management.py +0 -58
  41. comfy_env/stubs/comfy/utils.py +0 -29
  42. comfy_env/stubs/folder_paths.py +0 -71
  43. comfy_env/workers/pool.py +0 -241
  44. comfy_env-0.0.64.dist-info/RECORD +0 -48
  45. /comfy_env/{env/cuda_gpu_detection.py → pixi/cuda_detection.py} +0 -0
  46. /comfy_env/{env → pixi}/platform/__init__.py +0 -0
  47. /comfy_env/{env → pixi}/platform/base.py +0 -0
  48. /comfy_env/{env → pixi}/platform/darwin.py +0 -0
  49. /comfy_env/{env → pixi}/platform/linux.py +0 -0
  50. /comfy_env/{env → pixi}/platform/windows.py +0 -0
  51. /comfy_env/{registry.py → pixi/registry.py} +0 -0
  52. /comfy_env/{wheel_sources.yml → pixi/wheel_sources.yml} +0 -0
  53. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/WHEEL +0 -0
  54. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/entry_points.txt +0 -0
  55. {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/licenses/LICENSE +0 -0
@@ -1,740 +0,0 @@
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, SystemConfig, ToolConfig, CondaConfig
67
- from .cuda_gpu_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" # Full: 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 full 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
- Full:
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:
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 = [...] > [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 old format: [packages] with requirements/no_deps lists
262
-
263
- has_old_keys = any(k in packages_section for k in ["requirements", "no_deps", "requirements_file"])
264
-
265
- if has_old_keys:
266
- # Old 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", "tools", "system", "wheel_sources"}
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.
344
-
345
- Supports both full schema (named envs) and simple 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_full_config(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_full_config(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
408
- """
409
- Parse TOML data into EnvManagerConfig.
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
- [wheel_sources] - Custom wheel URL templates
419
-
420
- Also supports simple format ([env] + [packages]) for backward compatibility.
421
- """
422
- # Detect if this is full schema or simple format
423
- is_full = "local" in data or _has_named_env(data)
424
-
425
- if not is_full:
426
- # Simple format - convert to full structure
427
- return _convert_simple_to_full(data, base_dir)
428
-
429
- # Parse full schema
430
- system = _parse_system_section(data.get("system", {}))
431
- local = _parse_local_section(data.get("local", {}))
432
- envs = _parse_env_sections(data, base_dir)
433
- node_reqs = _parse_node_reqs(data.get("node_reqs", {}))
434
- tools = _parse_tools_section(data.get("tools", {}))
435
- wheel_sources = _parse_wheel_sources_section(data.get("wheel_sources", {}))
436
-
437
- return EnvManagerConfig(
438
- system=system,
439
- local=local,
440
- envs=envs,
441
- node_reqs=node_reqs,
442
- tools=tools,
443
- wheel_sources=wheel_sources,
444
- )
445
-
446
-
447
- def _has_named_env(data: Dict[str, Any]) -> bool:
448
- """Check if data has any named environment tables (not reserved names)."""
449
- env_indicators = ["python", "cuda", "pytorch", "cuda_version", "pytorch_version"]
450
- for key in data.keys():
451
- if key not in RESERVED_TABLES and isinstance(data[key], dict):
452
- # Check if it looks like an env definition
453
- section = data[key]
454
- if any(k in section for k in env_indicators):
455
- return True
456
- return False
457
-
458
-
459
- def _parse_local_section(local_data: Dict[str, Any]) -> LocalConfig:
460
- """Parse [local] section."""
461
- cuda_packages = {}
462
- requirements = []
463
-
464
- # [local.cuda] - CUDA packages
465
- cuda_section = local_data.get("cuda", {})
466
- if isinstance(cuda_section, dict):
467
- for pkg, ver in cuda_section.items():
468
- cuda_packages[pkg] = ver if ver and ver != "*" else ""
469
-
470
- # [local.packages] - regular packages
471
- packages_section = local_data.get("packages", {})
472
- if isinstance(packages_section, dict):
473
- requirements = packages_section.get("requirements", [])
474
- elif isinstance(packages_section, list):
475
- requirements = packages_section
476
-
477
- return LocalConfig(
478
- cuda_packages=cuda_packages,
479
- requirements=requirements,
480
- )
481
-
482
-
483
- def _parse_env_sections(data: Dict[str, Any], base_dir: Path) -> Dict[str, IsolatedEnv]:
484
- """Parse named environment sections."""
485
- envs = {}
486
- env_indicators = ["python", "cuda", "pytorch", "cuda_version", "pytorch_version"]
487
-
488
- for key, value in data.items():
489
- if key in RESERVED_TABLES:
490
- continue
491
- if not isinstance(value, dict):
492
- continue
493
-
494
- # Check if this looks like an env definition
495
- if not any(k in value for k in env_indicators):
496
- continue
497
-
498
- env = _parse_single_env(key, value, base_dir)
499
- envs[key] = env
500
-
501
- return envs
502
-
503
-
504
- def _parse_single_env(name: str, env_data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
505
- """Parse a single isolated environment section."""
506
- # Get basic env config
507
- python = env_data.get("python", "3.10")
508
- # Support both "cuda" and "cuda_version" field names (cuda_version avoids conflict with [envname.cuda] table)
509
- cuda = env_data.get("cuda_version") or env_data.get("cuda", "auto")
510
- pytorch = env_data.get("pytorch_version") or env_data.get("pytorch", "auto")
511
-
512
- # Handle auto-detection
513
- if cuda == "auto":
514
- cuda = detect_cuda_version()
515
- elif cuda in ("null", "none", None):
516
- cuda = None
517
-
518
- if pytorch == "auto":
519
- pytorch = _get_default_pytorch_version(cuda)
520
-
521
- # Parse [envname.conda] - conda packages (uses pixi backend)
522
- conda_section = env_data.get("conda", {})
523
- conda_config = None
524
- if isinstance(conda_section, dict) and ("channels" in conda_section or "packages" in conda_section):
525
- # This is a conda config with channels/packages
526
- channels = conda_section.get("channels", [])
527
- packages = conda_section.get("packages", [])
528
- if packages:
529
- conda_config = CondaConfig(channels=channels, packages=packages)
530
-
531
- # Parse [envname.cuda] - CUDA packages (separate from conda)
532
- cuda_section = env_data.get("cuda", {})
533
- no_deps_requirements = []
534
- if isinstance(cuda_section, dict):
535
- # Skip if this looks like a conda section (has channels/packages keys)
536
- if not ("channels" in cuda_section or "packages" in cuda_section):
537
- for pkg, ver in cuda_section.items():
538
- if ver == "*" or ver == "":
539
- no_deps_requirements.append(pkg)
540
- else:
541
- no_deps_requirements.append(f"{pkg}=={ver}")
542
-
543
- # Parse [envname.packages] - regular packages
544
- packages_section = env_data.get("packages", {})
545
- requirements = []
546
- if isinstance(packages_section, dict):
547
- requirements = packages_section.get("requirements", [])
548
- elif isinstance(packages_section, list):
549
- requirements = packages_section
550
-
551
- # Parse platform-specific packages [envname.packages.windows], etc.
552
- windows_reqs = []
553
- linux_reqs = []
554
- darwin_reqs = []
555
-
556
- if isinstance(packages_section, dict):
557
- win_section = packages_section.get("windows", {})
558
- if isinstance(win_section, dict):
559
- windows_reqs = win_section.get("requirements", [])
560
- elif isinstance(win_section, list):
561
- windows_reqs = win_section
562
-
563
- linux_section = packages_section.get("linux", {})
564
- if isinstance(linux_section, dict):
565
- linux_reqs = linux_section.get("requirements", [])
566
- elif isinstance(linux_section, list):
567
- linux_reqs = linux_section
568
-
569
- darwin_section = packages_section.get("darwin", {})
570
- if isinstance(darwin_section, dict):
571
- darwin_reqs = darwin_section.get("requirements", [])
572
- elif isinstance(darwin_section, list):
573
- darwin_reqs = darwin_section
574
-
575
- # Parse isolated flag for runtime process isolation
576
- isolated = env_data.get("isolated", False)
577
-
578
- # Parse environment variables section [envname.env]
579
- env_vars = {}
580
- env_section = env_data.get("env", {})
581
- if isinstance(env_section, dict):
582
- # Filter to only string/numeric values (avoid nested tables like cuda/packages)
583
- env_vars = {k: str(v) for k, v in env_section.items() if isinstance(v, (str, int, float))}
584
-
585
- return IsolatedEnv(
586
- name=name,
587
- python=python,
588
- cuda=cuda,
589
- pytorch_version=pytorch,
590
- requirements=requirements,
591
- no_deps_requirements=no_deps_requirements,
592
- windows_requirements=windows_reqs,
593
- linux_requirements=linux_reqs,
594
- darwin_requirements=darwin_reqs,
595
- conda=conda_config,
596
- isolated=isolated,
597
- env_vars=env_vars,
598
- )
599
-
600
-
601
- def _parse_node_reqs(node_reqs_data: Dict[str, Any]) -> List[NodeReq]:
602
- """Parse [node_reqs] section."""
603
- reqs = []
604
-
605
- for name, value in node_reqs_data.items():
606
- if isinstance(value, str):
607
- # Simple format: VideoHelperSuite = "Kosinkadink/ComfyUI-VideoHelperSuite"
608
- reqs.append(NodeReq(name=name, repo=value))
609
- elif isinstance(value, dict):
610
- # Extended format: VideoHelperSuite = { repo = "..." }
611
- repo = value.get("repo", "")
612
- reqs.append(NodeReq(name=name, repo=repo))
613
-
614
- return reqs
615
-
616
-
617
- def _parse_tools_section(tools_data: Dict[str, Any]) -> Dict[str, ToolConfig]:
618
- """Parse [tools] section.
619
-
620
- Supports:
621
- [tools]
622
- blender = "4.2"
623
-
624
- Or extended:
625
- [tools.blender]
626
- version = "4.2"
627
- install_dir = "/custom/path"
628
- """
629
- tools = {}
630
-
631
- for name, value in tools_data.items():
632
- if isinstance(value, str):
633
- # Simple format: blender = "4.2"
634
- tools[name] = ToolConfig(name=name, version=value)
635
- elif isinstance(value, dict):
636
- # Extended format: [tools.blender] with version, install_dir
637
- version = value.get("version", "latest")
638
- install_dir = value.get("install_dir")
639
- if install_dir:
640
- install_dir = Path(install_dir)
641
- tools[name] = ToolConfig(name=name, version=version, install_dir=install_dir)
642
-
643
- return tools
644
-
645
-
646
- def _parse_system_section(system_data: Dict[str, Any]) -> SystemConfig:
647
- """Parse [system] section.
648
-
649
- Supports:
650
- [system]
651
- linux = ["libgl1", "libopengl0"]
652
- darwin = ["mesa"] # future
653
- windows = ["vcredist"] # future
654
- """
655
- linux = system_data.get("linux", [])
656
- darwin = system_data.get("darwin", [])
657
- windows = system_data.get("windows", [])
658
-
659
- # Ensure all are lists
660
- if not isinstance(linux, list):
661
- linux = [linux] if linux else []
662
- if not isinstance(darwin, list):
663
- darwin = [darwin] if darwin else []
664
- if not isinstance(windows, list):
665
- windows = [windows] if windows else []
666
-
667
- return SystemConfig(
668
- linux=linux,
669
- darwin=darwin,
670
- windows=windows,
671
- )
672
-
673
-
674
- def _parse_wheel_sources_section(wheel_sources_data: Dict[str, Any]) -> Dict[str, str]:
675
- """Parse [wheel_sources] section.
676
-
677
- Supports:
678
- [wheel_sources]
679
- nvdiffrast = "https://example.com/nvdiffrast-{version}+cu{cuda_short}-{py_tag}-{platform}.whl"
680
- my-package = "https://my-server.com/my-package-{version}.whl"
681
-
682
- Returns:
683
- Dict mapping package name (lowercase) to wheel URL template
684
- """
685
- wheel_sources = {}
686
- for name, url_template in wheel_sources_data.items():
687
- if isinstance(url_template, str):
688
- wheel_sources[name.lower()] = url_template
689
- return wheel_sources
690
-
691
-
692
- def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
693
- """Convert simple config format to full EnvManagerConfig.
694
-
695
- Simple configs have [env] and [packages] sections but no named environments.
696
- This converts them to the full format with a single named environment.
697
- """
698
- # Parse using simple parser to get IsolatedEnv
699
- simple_env = _parse_config(data, base_dir)
700
-
701
- # Parse tools, system, and wheel_sources sections (shared between simple and full format)
702
- tools = _parse_tools_section(data.get("tools", {}))
703
- system = _parse_system_section(data.get("system", {}))
704
- wheel_sources = _parse_wheel_sources_section(data.get("wheel_sources", {}))
705
-
706
- # Check if this has explicit env settings (isolated venv) vs just CUDA packages (local install)
707
- env_section = data.get("env", {})
708
- has_explicit_env = bool(env_section.get("name") or env_section.get("python"))
709
-
710
- if has_explicit_env:
711
- # Isolated venv config
712
- return EnvManagerConfig(
713
- system=system,
714
- local=LocalConfig(),
715
- envs={simple_env.name: simple_env},
716
- node_reqs=[],
717
- tools=tools,
718
- wheel_sources=wheel_sources,
719
- )
720
- else:
721
- # Local CUDA packages only (no isolated venv)
722
- cuda_packages = {}
723
- for req in simple_env.no_deps_requirements:
724
- if "==" in req:
725
- pkg, ver = req.split("==", 1)
726
- cuda_packages[pkg] = ver
727
- else:
728
- cuda_packages[req] = ""
729
-
730
- return EnvManagerConfig(
731
- system=system,
732
- local=LocalConfig(
733
- cuda_packages=cuda_packages,
734
- requirements=simple_env.requirements,
735
- ),
736
- envs={},
737
- node_reqs=[],
738
- tools=tools,
739
- wheel_sources=wheel_sources,
740
- )