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.
- comfy_env/__init__.py +70 -122
- comfy_env/cli.py +78 -7
- comfy_env/config/__init__.py +19 -0
- comfy_env/config/parser.py +151 -0
- comfy_env/config/types.py +64 -0
- comfy_env/install.py +83 -361
- comfy_env/isolation/__init__.py +9 -0
- comfy_env/isolation/wrap.py +351 -0
- comfy_env/nodes.py +2 -2
- comfy_env/pixi/__init__.py +48 -0
- comfy_env/pixi/core.py +356 -0
- comfy_env/{resolver.py → pixi/resolver.py} +1 -14
- comfy_env/prestartup.py +60 -0
- comfy_env/templates/comfy-env-instructions.txt +30 -87
- comfy_env/templates/comfy-env.toml +68 -136
- comfy_env/workers/__init__.py +21 -32
- comfy_env/workers/base.py +1 -1
- comfy_env/workers/{torch_mp.py → mp.py} +47 -14
- comfy_env/workers/{venv.py → subprocess.py} +405 -441
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/METADATA +2 -1
- comfy_env-0.0.66.dist-info/RECORD +34 -0
- comfy_env/decorator.py +0 -700
- comfy_env/env/__init__.py +0 -47
- comfy_env/env/config.py +0 -201
- comfy_env/env/config_file.py +0 -740
- comfy_env/env/manager.py +0 -636
- comfy_env/env/security.py +0 -267
- comfy_env/ipc/__init__.py +0 -55
- comfy_env/ipc/bridge.py +0 -476
- comfy_env/ipc/protocol.py +0 -265
- comfy_env/ipc/tensor.py +0 -371
- comfy_env/ipc/torch_bridge.py +0 -401
- comfy_env/ipc/transport.py +0 -318
- comfy_env/ipc/worker.py +0 -221
- comfy_env/isolation.py +0 -310
- comfy_env/pixi.py +0 -760
- comfy_env/stub_imports.py +0 -270
- comfy_env/stubs/__init__.py +0 -1
- comfy_env/stubs/comfy/__init__.py +0 -6
- comfy_env/stubs/comfy/model_management.py +0 -58
- comfy_env/stubs/comfy/utils.py +0 -29
- comfy_env/stubs/folder_paths.py +0 -71
- comfy_env/workers/pool.py +0 -241
- comfy_env-0.0.64.dist-info/RECORD +0 -48
- /comfy_env/{env/cuda_gpu_detection.py → pixi/cuda_detection.py} +0 -0
- /comfy_env/{env → pixi}/platform/__init__.py +0 -0
- /comfy_env/{env → pixi}/platform/base.py +0 -0
- /comfy_env/{env → pixi}/platform/darwin.py +0 -0
- /comfy_env/{env → pixi}/platform/linux.py +0 -0
- /comfy_env/{env → pixi}/platform/windows.py +0 -0
- /comfy_env/{registry.py → pixi/registry.py} +0 -0
- /comfy_env/{wheel_sources.yml → pixi/wheel_sources.yml} +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/WHEEL +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.0.64.dist-info → comfy_env-0.0.66.dist-info}/licenses/LICENSE +0 -0
comfy_env/env/config_file.py
DELETED
|
@@ -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
|
-
)
|