comfy-env 0.0.40__py3-none-any.whl → 0.0.42__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 -4
- comfy_env/cli.py +77 -147
- comfy_env/env/config.py +2 -0
- comfy_env/env/config_file.py +27 -3
- comfy_env/env/manager.py +30 -129
- comfy_env/install.py +115 -497
- comfy_env/registry.py +48 -9
- comfy_env/resolver.py +10 -187
- comfy_env/wheel_sources.yml +30 -85
- {comfy_env-0.0.40.dist-info → comfy_env-0.0.42.dist-info}/METADATA +118 -46
- {comfy_env-0.0.40.dist-info → comfy_env-0.0.42.dist-info}/RECORD +14 -15
- comfy_env/index_resolver.py +0 -132
- {comfy_env-0.0.40.dist-info → comfy_env-0.0.42.dist-info}/WHEEL +0 -0
- {comfy_env-0.0.40.dist-info → comfy_env-0.0.42.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.0.40.dist-info → comfy_env-0.0.42.dist-info}/licenses/LICENSE +0 -0
comfy_env/install.py
CHANGED
|
@@ -12,7 +12,7 @@ Example:
|
|
|
12
12
|
install()
|
|
13
13
|
|
|
14
14
|
# In-place with explicit config
|
|
15
|
-
install(config="comfy-env.toml"
|
|
15
|
+
install(config="comfy-env.toml")
|
|
16
16
|
|
|
17
17
|
# Isolated environment
|
|
18
18
|
install(config="comfy-env.toml", mode="isolated")
|
|
@@ -23,16 +23,15 @@ import shutil
|
|
|
23
23
|
import subprocess
|
|
24
24
|
import sys
|
|
25
25
|
from pathlib import Path
|
|
26
|
-
from typing import
|
|
26
|
+
from typing import Callable, Dict, List, Optional, Set, Union
|
|
27
27
|
|
|
28
28
|
from .env.config import IsolatedEnv, LocalConfig, NodeReq, SystemConfig
|
|
29
29
|
from .env.config_file import load_config, discover_config
|
|
30
30
|
from .env.manager import IsolatedEnvManager
|
|
31
|
-
from .errors import CUDANotFoundError,
|
|
31
|
+
from .errors import CUDANotFoundError, InstallError
|
|
32
32
|
from .pixi import pixi_install
|
|
33
33
|
from .registry import PACKAGE_REGISTRY, get_cuda_short2
|
|
34
|
-
from .resolver import RuntimeEnv,
|
|
35
|
-
from .index_resolver import resolve_wheel_from_index
|
|
34
|
+
from .resolver import RuntimeEnv, parse_wheel_requirement
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def _install_system_packages(
|
|
@@ -64,32 +63,21 @@ def _install_system_packages(
|
|
|
64
63
|
log(f" Would install: {', '.join(packages)}")
|
|
65
64
|
return True
|
|
66
65
|
|
|
67
|
-
# Check if apt-get is available
|
|
68
66
|
if not shutil.which("apt-get"):
|
|
69
67
|
log(" Warning: apt-get not found. Cannot install system packages.")
|
|
70
68
|
log(f" Please install manually: {', '.join(packages)}")
|
|
71
|
-
return True
|
|
69
|
+
return True
|
|
72
70
|
|
|
73
|
-
# Check if we can use sudo
|
|
74
71
|
sudo_available = shutil.which("sudo") is not None
|
|
75
72
|
|
|
76
73
|
try:
|
|
77
74
|
if sudo_available:
|
|
78
|
-
# Try with sudo
|
|
79
75
|
log(" Running apt-get update...")
|
|
80
|
-
|
|
81
|
-
["sudo", "apt-get", "update"],
|
|
82
|
-
capture_output=True,
|
|
83
|
-
text=True,
|
|
84
|
-
)
|
|
85
|
-
if update_result.returncode != 0:
|
|
86
|
-
log(f" Warning: apt-get update failed: {update_result.stderr.strip()}")
|
|
87
|
-
# Continue anyway - packages might already be cached
|
|
76
|
+
subprocess.run(["sudo", "apt-get", "update"], capture_output=True, text=True)
|
|
88
77
|
|
|
89
78
|
log(f" Installing: {', '.join(packages)}")
|
|
90
|
-
install_cmd = ["sudo", "apt-get", "install", "-y"] + packages
|
|
91
79
|
install_result = subprocess.run(
|
|
92
|
-
|
|
80
|
+
["sudo", "apt-get", "install", "-y"] + packages,
|
|
93
81
|
capture_output=True,
|
|
94
82
|
text=True,
|
|
95
83
|
)
|
|
@@ -97,38 +85,30 @@ def _install_system_packages(
|
|
|
97
85
|
if install_result.returncode != 0:
|
|
98
86
|
log(f" Warning: apt-get install failed: {install_result.stderr.strip()}")
|
|
99
87
|
log(f" Please install manually: sudo apt-get install {' '.join(packages)}")
|
|
100
|
-
return True # Don't fail - just warn
|
|
101
88
|
else:
|
|
102
89
|
log(" System packages installed successfully.")
|
|
103
|
-
return True
|
|
104
90
|
else:
|
|
105
|
-
log(" Warning: sudo not available.
|
|
91
|
+
log(" Warning: sudo not available.")
|
|
106
92
|
log(f" Please install manually: sudo apt-get install {' '.join(packages)}")
|
|
107
|
-
return True # Don't fail - just warn
|
|
108
93
|
|
|
109
94
|
except Exception as e:
|
|
110
95
|
log(f" Warning: Failed to install system packages: {e}")
|
|
111
96
|
log(f" Please install manually: sudo apt-get install {' '.join(packages)}")
|
|
112
|
-
|
|
97
|
+
|
|
98
|
+
return True
|
|
113
99
|
|
|
114
100
|
elif platform == "darwin":
|
|
115
101
|
packages = system_config.darwin
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
log(f"System packages for macOS: {', '.join(packages)}")
|
|
120
|
-
log(" Note: macOS system package installation not yet implemented.")
|
|
121
|
-
log(f" Please install manually with Homebrew: brew install {' '.join(packages)}")
|
|
102
|
+
if packages:
|
|
103
|
+
log(f"System packages for macOS: {', '.join(packages)}")
|
|
104
|
+
log(f" Please install manually: brew install {' '.join(packages)}")
|
|
122
105
|
return True
|
|
123
106
|
|
|
124
107
|
elif platform == "win32":
|
|
125
108
|
packages = system_config.windows
|
|
126
|
-
if
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
log(f"System packages for Windows: {', '.join(packages)}")
|
|
130
|
-
log(" Note: Windows system package installation not yet implemented.")
|
|
131
|
-
log(f" Please install manually.")
|
|
109
|
+
if packages:
|
|
110
|
+
log(f"System packages for Windows: {', '.join(packages)}")
|
|
111
|
+
log(" Please install manually.")
|
|
132
112
|
return True
|
|
133
113
|
|
|
134
114
|
return True
|
|
@@ -140,23 +120,10 @@ def _install_node_dependencies(
|
|
|
140
120
|
log: Callable[[str], None],
|
|
141
121
|
dry_run: bool = False,
|
|
142
122
|
) -> bool:
|
|
143
|
-
"""
|
|
144
|
-
Install node dependencies (other ComfyUI custom nodes).
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
node_reqs: List of NodeReq objects from [node_reqs] config section.
|
|
148
|
-
node_dir: Directory of the current node (used to find custom_nodes/).
|
|
149
|
-
log: Logging callback.
|
|
150
|
-
dry_run: If True, show what would be installed without installing.
|
|
151
|
-
|
|
152
|
-
Returns:
|
|
153
|
-
True if installation succeeded or no dependencies needed.
|
|
154
|
-
"""
|
|
123
|
+
"""Install node dependencies (other ComfyUI custom nodes)."""
|
|
155
124
|
from .nodes import install_node_deps
|
|
156
125
|
|
|
157
|
-
# Detect custom_nodes directory (parent of current node)
|
|
158
126
|
custom_nodes_dir = node_dir.parent
|
|
159
|
-
|
|
160
127
|
log(f"\nInstalling {len(node_reqs)} node dependencies...")
|
|
161
128
|
|
|
162
129
|
if dry_run:
|
|
@@ -166,79 +133,49 @@ def _install_node_dependencies(
|
|
|
166
133
|
log(f" {req.name}: {status}")
|
|
167
134
|
return True
|
|
168
135
|
|
|
169
|
-
# Track visited nodes to prevent cycles
|
|
170
|
-
# Start with current node's directory name
|
|
171
136
|
visited: Set[str] = {node_dir.name}
|
|
172
|
-
|
|
173
137
|
install_node_deps(node_reqs, custom_nodes_dir, log, visited)
|
|
174
|
-
|
|
175
138
|
return True
|
|
176
139
|
|
|
177
140
|
|
|
178
141
|
def install(
|
|
179
|
-
config: Optional[Union[str, Path]] = None,
|
|
180
|
-
node_dir: Optional[Path] = None,
|
|
181
142
|
log_callback: Optional[Callable[[str], None]] = None,
|
|
182
143
|
dry_run: bool = False,
|
|
183
|
-
verify_wheels: bool = False,
|
|
184
|
-
**kwargs, # Accept but ignore deprecated 'mode' parameter
|
|
185
144
|
) -> bool:
|
|
186
145
|
"""
|
|
187
|
-
Install dependencies from
|
|
146
|
+
Install dependencies from comfy-env.toml, auto-discovered from caller's directory.
|
|
188
147
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
- [envname] without python → inplace (installs into current env)
|
|
193
|
-
- [local.cuda] → local (installs into current env)
|
|
148
|
+
Example:
|
|
149
|
+
from comfy_env import install
|
|
150
|
+
install()
|
|
194
151
|
|
|
195
152
|
Args:
|
|
196
|
-
config: Path to config file. If None, auto-discovers in node_dir.
|
|
197
|
-
node_dir: Directory to search for config. Defaults to current directory.
|
|
198
153
|
log_callback: Optional callback for logging. Defaults to print.
|
|
199
154
|
dry_run: If True, show what would be installed without installing.
|
|
200
|
-
verify_wheels: If True, verify wheel URLs exist before installing.
|
|
201
155
|
|
|
202
156
|
Returns:
|
|
203
157
|
True if installation succeeded.
|
|
204
|
-
|
|
205
|
-
Raises:
|
|
206
|
-
FileNotFoundError: If config file not found.
|
|
207
|
-
WheelNotFoundError: If required wheel cannot be resolved.
|
|
208
|
-
InstallError: If installation fails.
|
|
209
|
-
|
|
210
|
-
Example:
|
|
211
|
-
# Simple usage - auto-discover config
|
|
212
|
-
install()
|
|
213
|
-
|
|
214
|
-
# Explicit config file
|
|
215
|
-
install(config="comfy-env.toml")
|
|
216
|
-
|
|
217
|
-
# Dry run to see what would be installed
|
|
218
|
-
install(dry_run=True)
|
|
219
158
|
"""
|
|
159
|
+
# Auto-discover caller's directory
|
|
160
|
+
frame = inspect.stack()[1]
|
|
161
|
+
caller_file = frame.filename
|
|
162
|
+
node_dir = Path(caller_file).parent.resolve()
|
|
163
|
+
|
|
220
164
|
log = log_callback or print
|
|
221
|
-
node_dir = Path(node_dir) if node_dir else Path.cwd()
|
|
222
165
|
|
|
223
|
-
|
|
224
|
-
full_config = _load_full_config(config, node_dir)
|
|
166
|
+
full_config = _load_full_config(None, node_dir)
|
|
225
167
|
if full_config is None:
|
|
226
168
|
raise FileNotFoundError(
|
|
227
|
-
"No
|
|
228
|
-
"Create comfy-env.toml
|
|
169
|
+
f"No comfy-env.toml found in {node_dir}. "
|
|
170
|
+
"Create comfy-env.toml to define dependencies."
|
|
229
171
|
)
|
|
230
172
|
|
|
231
|
-
# Install node dependencies first (other ComfyUI custom nodes)
|
|
232
|
-
# These may have their own system packages and Python packages
|
|
233
173
|
if full_config.node_reqs:
|
|
234
174
|
_install_node_dependencies(full_config.node_reqs, node_dir, log, dry_run)
|
|
235
175
|
|
|
236
|
-
# Install system packages (apt, brew, etc.)
|
|
237
|
-
# These need to be installed before Python packages that depend on them
|
|
238
176
|
if full_config.has_system:
|
|
239
177
|
_install_system_packages(full_config.system, log, dry_run)
|
|
240
178
|
|
|
241
|
-
# Get environment config
|
|
242
179
|
env_config = full_config.default_env
|
|
243
180
|
if env_config is None and not full_config.has_local:
|
|
244
181
|
log("No packages to install")
|
|
@@ -247,30 +184,26 @@ def install(
|
|
|
247
184
|
if env_config:
|
|
248
185
|
log(f"Found configuration: {env_config.name}")
|
|
249
186
|
|
|
250
|
-
# Check if environment uses conda packages (pixi backend)
|
|
251
187
|
if env_config and env_config.uses_conda:
|
|
252
|
-
log(
|
|
188
|
+
log("Environment uses conda packages - using pixi backend")
|
|
253
189
|
return pixi_install(env_config, node_dir, log, dry_run)
|
|
254
190
|
|
|
255
|
-
#
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
# - only [local.cuda] → local (installs into current env)
|
|
191
|
+
# Get user wheel_sources overrides
|
|
192
|
+
user_wheel_sources = full_config.wheel_sources if hasattr(full_config, 'wheel_sources') else {}
|
|
193
|
+
|
|
259
194
|
if env_config:
|
|
260
195
|
if env_config.python:
|
|
261
196
|
return _install_isolated(env_config, node_dir, log, dry_run)
|
|
262
197
|
else:
|
|
263
|
-
return _install_inplace(env_config, node_dir, log, dry_run,
|
|
198
|
+
return _install_inplace(env_config, node_dir, log, dry_run, user_wheel_sources)
|
|
264
199
|
elif full_config.has_local:
|
|
265
|
-
|
|
266
|
-
return _install_local(full_config.local, node_dir, log, dry_run)
|
|
200
|
+
return _install_local(full_config.local, node_dir, log, dry_run, user_wheel_sources)
|
|
267
201
|
else:
|
|
268
202
|
return True
|
|
269
203
|
|
|
270
204
|
|
|
271
205
|
def _load_full_config(config: Optional[Union[str, Path]], node_dir: Path):
|
|
272
206
|
"""Load full EnvManagerConfig (includes tools)."""
|
|
273
|
-
from .env.config import EnvManagerConfig
|
|
274
207
|
if config is not None:
|
|
275
208
|
config_path = Path(config)
|
|
276
209
|
if not config_path.is_absolute():
|
|
@@ -307,54 +240,45 @@ def _install_inplace(
|
|
|
307
240
|
node_dir: Path,
|
|
308
241
|
log: Callable[[str], None],
|
|
309
242
|
dry_run: bool,
|
|
310
|
-
|
|
243
|
+
user_wheel_sources: Dict[str, str],
|
|
311
244
|
) -> bool:
|
|
312
|
-
"""Install in-place into current environment
|
|
245
|
+
"""Install in-place into current environment."""
|
|
313
246
|
log("Installing in-place mode")
|
|
314
247
|
|
|
315
|
-
# Install MSVC runtime on Windows (required for CUDA/PyTorch native extensions)
|
|
316
248
|
if sys.platform == "win32":
|
|
317
249
|
log("Installing MSVC runtime for Windows...")
|
|
318
250
|
if not dry_run:
|
|
319
251
|
_pip_install(["msvc-runtime"], no_deps=False, log=log)
|
|
320
252
|
|
|
321
|
-
# Detect runtime environment
|
|
322
253
|
env = RuntimeEnv.detect()
|
|
323
254
|
log(f"Detected environment: {env}")
|
|
324
255
|
|
|
325
|
-
# Check CUDA requirement
|
|
326
256
|
if not env.cuda_version:
|
|
327
|
-
cuda_packages =
|
|
257
|
+
cuda_packages = env_config.no_deps_requirements or []
|
|
328
258
|
if cuda_packages:
|
|
329
259
|
raise CUDANotFoundError(package=", ".join(cuda_packages))
|
|
330
260
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
regular_packages = _get_regular_packages(env_config)
|
|
334
|
-
|
|
335
|
-
# Legacy wheel sources from config (for packages not in registry)
|
|
336
|
-
legacy_wheel_sources = env_config.wheel_sources or []
|
|
261
|
+
cuda_packages = env_config.no_deps_requirements or []
|
|
262
|
+
regular_packages = env_config.requirements or []
|
|
337
263
|
|
|
338
264
|
if dry_run:
|
|
339
265
|
log("\nDry run - would install:")
|
|
340
266
|
for req in cuda_packages:
|
|
341
267
|
package, version = parse_wheel_requirement(req)
|
|
342
|
-
|
|
343
|
-
log(f" {package}: {
|
|
268
|
+
url = _resolve_wheel_url(package, version, env, user_wheel_sources)
|
|
269
|
+
log(f" {package}: {url[:80]}...")
|
|
344
270
|
if regular_packages:
|
|
345
271
|
log(" Regular packages:")
|
|
346
272
|
for pkg in regular_packages:
|
|
347
273
|
log(f" {pkg}")
|
|
348
274
|
return True
|
|
349
275
|
|
|
350
|
-
# Install CUDA packages using appropriate method per package
|
|
351
276
|
if cuda_packages:
|
|
352
277
|
log(f"\nInstalling {len(cuda_packages)} CUDA packages...")
|
|
353
278
|
for req in cuda_packages:
|
|
354
279
|
package, version = parse_wheel_requirement(req)
|
|
355
|
-
_install_cuda_package(package, version, env,
|
|
280
|
+
_install_cuda_package(package, version, env, user_wheel_sources, log)
|
|
356
281
|
|
|
357
|
-
# Install regular packages
|
|
358
282
|
if regular_packages:
|
|
359
283
|
log(f"\nInstalling {len(regular_packages)} regular packages...")
|
|
360
284
|
_pip_install(regular_packages, no_deps=False, log=log)
|
|
@@ -368,25 +292,22 @@ def _install_local(
|
|
|
368
292
|
node_dir: Path,
|
|
369
293
|
log: Callable[[str], None],
|
|
370
294
|
dry_run: bool,
|
|
295
|
+
user_wheel_sources: Dict[str, str],
|
|
371
296
|
) -> bool:
|
|
372
|
-
"""Install local packages into current environment
|
|
297
|
+
"""Install local packages into current environment."""
|
|
373
298
|
log("Installing local packages into host environment")
|
|
374
299
|
|
|
375
|
-
# Install MSVC runtime on Windows (required for CUDA/PyTorch native extensions)
|
|
376
300
|
if sys.platform == "win32":
|
|
377
301
|
log("Installing MSVC runtime for Windows...")
|
|
378
302
|
if not dry_run:
|
|
379
303
|
_pip_install(["msvc-runtime"], no_deps=False, log=log)
|
|
380
304
|
|
|
381
|
-
# Detect runtime environment
|
|
382
305
|
env = RuntimeEnv.detect()
|
|
383
306
|
log(f"Detected environment: {env}")
|
|
384
307
|
|
|
385
|
-
# Check CUDA requirement
|
|
386
308
|
if not env.cuda_version and local_config.cuda_packages:
|
|
387
309
|
raise CUDANotFoundError(package=", ".join(local_config.cuda_packages.keys()))
|
|
388
310
|
|
|
389
|
-
# Convert cuda_packages dict to list of specs
|
|
390
311
|
cuda_packages = []
|
|
391
312
|
for pkg, ver in local_config.cuda_packages.items():
|
|
392
313
|
if ver:
|
|
@@ -404,14 +325,12 @@ def _install_local(
|
|
|
404
325
|
log(f" {pkg}")
|
|
405
326
|
return True
|
|
406
327
|
|
|
407
|
-
# Install CUDA packages
|
|
408
328
|
if cuda_packages:
|
|
409
329
|
log(f"\nInstalling {len(cuda_packages)} CUDA packages...")
|
|
410
330
|
for req in cuda_packages:
|
|
411
331
|
package, version = parse_wheel_requirement(req)
|
|
412
|
-
_install_cuda_package(package, version, env,
|
|
332
|
+
_install_cuda_package(package, version, env, user_wheel_sources, log)
|
|
413
333
|
|
|
414
|
-
# Install regular packages
|
|
415
334
|
if local_config.requirements:
|
|
416
335
|
log(f"\nInstalling {len(local_config.requirements)} regular packages...")
|
|
417
336
|
_pip_install(local_config.requirements, no_deps=False, log=log)
|
|
@@ -420,335 +339,101 @@ def _install_local(
|
|
|
420
339
|
return True
|
|
421
340
|
|
|
422
341
|
|
|
423
|
-
def
|
|
342
|
+
def _resolve_wheel_url(
|
|
424
343
|
package: str,
|
|
425
344
|
version: Optional[str],
|
|
426
345
|
env: RuntimeEnv,
|
|
427
|
-
|
|
428
|
-
) ->
|
|
429
|
-
"""
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if pkg_lower in PACKAGE_REGISTRY:
|
|
433
|
-
config = PACKAGE_REGISTRY[pkg_lower]
|
|
434
|
-
method = config["method"]
|
|
435
|
-
index_url = _substitute_template(config.get("index_url", ""), env)
|
|
436
|
-
|
|
437
|
-
if method == "index":
|
|
438
|
-
version_info = ""
|
|
439
|
-
if "version_template" in config:
|
|
440
|
-
resolved_version = _substitute_template(config["version_template"], env)
|
|
441
|
-
version_info = f" (version {resolved_version})"
|
|
442
|
-
return {"method": method, "description": f"from index {index_url}{version_info}"}
|
|
443
|
-
elif method == "github_index":
|
|
444
|
-
return {"method": method, "description": f"from {index_url}"}
|
|
445
|
-
elif method == "find_links":
|
|
446
|
-
return {"method": method, "description": f"from {index_url}"}
|
|
447
|
-
elif method == "pypi_variant":
|
|
448
|
-
vars_dict = env.as_dict()
|
|
449
|
-
if env.cuda_version:
|
|
450
|
-
vars_dict["cuda_short2"] = get_cuda_short2(env.cuda_version)
|
|
451
|
-
actual_pkg = _substitute_template(config["package_template"], vars_dict)
|
|
452
|
-
return {"method": method, "description": f"as {actual_pkg} from PyPI"}
|
|
453
|
-
elif method == "github_release":
|
|
454
|
-
sources = config.get("sources", [])
|
|
455
|
-
source_names = [s.get("name", "unknown") for s in sources]
|
|
456
|
-
return {"method": method, "description": f"from GitHub ({', '.join(source_names)})"}
|
|
457
|
-
elif legacy_wheel_sources:
|
|
458
|
-
return {"method": "legacy", "description": f"from config wheel_sources"}
|
|
459
|
-
else:
|
|
460
|
-
return {"method": "pypi", "description": "from PyPI"}
|
|
461
|
-
|
|
346
|
+
user_wheel_sources: Dict[str, str],
|
|
347
|
+
) -> str:
|
|
348
|
+
"""
|
|
349
|
+
Resolve wheel URL for a CUDA package.
|
|
462
350
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
log: Callable[[str], None],
|
|
469
|
-
) -> None:
|
|
470
|
-
"""Install a single CUDA package using the appropriate method from registry."""
|
|
351
|
+
Resolution order:
|
|
352
|
+
1. User's [wheel_sources] in comfy-env.toml (highest priority)
|
|
353
|
+
2. Built-in wheel_sources.yml registry
|
|
354
|
+
3. Error if not found
|
|
355
|
+
"""
|
|
471
356
|
pkg_lower = package.lower()
|
|
357
|
+
vars_dict = _build_template_vars(env, version)
|
|
358
|
+
|
|
359
|
+
# 1. Check user overrides first
|
|
360
|
+
if pkg_lower in user_wheel_sources:
|
|
361
|
+
template = user_wheel_sources[pkg_lower]
|
|
362
|
+
return _substitute_template(template, vars_dict)
|
|
472
363
|
|
|
473
|
-
# Check
|
|
364
|
+
# 2. Check built-in registry
|
|
474
365
|
if pkg_lower in PACKAGE_REGISTRY:
|
|
475
366
|
config = PACKAGE_REGISTRY[pkg_lower]
|
|
476
|
-
method = config["method"]
|
|
477
|
-
|
|
478
|
-
if method == "index":
|
|
479
|
-
# PEP 503 index - try to resolve exact wheel URL first
|
|
480
|
-
index_url = _substitute_template(config["index_url"], env)
|
|
481
|
-
# Check for version_template (e.g., detectron2 with embedded torch/cuda version)
|
|
482
|
-
if "version_template" in config:
|
|
483
|
-
resolved_version = _substitute_template(config["version_template"], env)
|
|
484
|
-
pkg_spec = f"{package}=={resolved_version}"
|
|
485
|
-
else:
|
|
486
|
-
pkg_spec = f"{package}=={version}" if version else package
|
|
487
|
-
log(f" Installing {package} (index)...")
|
|
488
|
-
|
|
489
|
-
# Resolve version: use provided version, default_version, or None
|
|
490
|
-
effective_version = version if version and version != "*" else config.get("default_version")
|
|
491
|
-
|
|
492
|
-
# Check for wheel_template first (direct URL construction, no index parsing)
|
|
493
|
-
if "wheel_template" in config and effective_version:
|
|
494
|
-
vars_dict = env.as_dict()
|
|
495
|
-
vars_dict["version"] = effective_version
|
|
496
|
-
wheel_url = _substitute_template(config["wheel_template"], vars_dict)
|
|
497
|
-
log(f" Wheel: {wheel_url}")
|
|
498
|
-
_pip_install([wheel_url], no_deps=True, log=log)
|
|
499
|
-
else:
|
|
500
|
-
# Try to resolve exact wheel URL from index
|
|
501
|
-
actual_version = resolved_version if "version_template" in config else version
|
|
502
|
-
vars_dict = env.as_dict()
|
|
503
|
-
wheel_url = resolve_wheel_from_index(index_url, package, vars_dict, actual_version)
|
|
504
|
-
if wheel_url:
|
|
505
|
-
# Install from resolved URL directly (guarantees we get what we resolved)
|
|
506
|
-
log(f" Wheel: {wheel_url}")
|
|
507
|
-
_pip_install([wheel_url], no_deps=True, log=log)
|
|
508
|
-
else:
|
|
509
|
-
raise InstallError(
|
|
510
|
-
f"Failed to resolve wheel URL for {package} from index {index_url}. "
|
|
511
|
-
"No matching wheel found and PyPI fallback is disabled.",
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
elif method == "github_index":
|
|
515
|
-
# GitHub Pages index - try to resolve exact wheel URL first
|
|
516
|
-
index_url = _substitute_template(config["index_url"], env)
|
|
517
|
-
pkg_spec = f"{package}=={version}" if version else package
|
|
518
|
-
log(f" Installing {package} (github_index)...")
|
|
519
|
-
# Try to resolve exact wheel URL from find-links page
|
|
520
|
-
vars_dict = env.as_dict()
|
|
521
|
-
wheel_url = resolve_wheel_from_index(index_url, package, vars_dict, version)
|
|
522
|
-
if wheel_url:
|
|
523
|
-
# Install from resolved URL directly (guarantees we get what we resolved)
|
|
524
|
-
log(f" Wheel: {wheel_url}")
|
|
525
|
-
_pip_install([wheel_url], no_deps=True, log=log)
|
|
526
|
-
else:
|
|
527
|
-
# Fallback to find-links based resolution
|
|
528
|
-
log(f" Find-links: {index_url}")
|
|
529
|
-
log(f" Package: {pkg_spec}")
|
|
530
|
-
_pip_install_with_find_links(pkg_spec, index_url, log)
|
|
531
|
-
|
|
532
|
-
elif method == "find_links":
|
|
533
|
-
# Generic find-links (e.g., PyG) - try to resolve exact wheel URL first
|
|
534
|
-
index_url = _substitute_template(config["index_url"], env)
|
|
535
|
-
pkg_spec = f"{package}=={version}" if version else package
|
|
536
|
-
log(f" Installing {package} (find_links)...")
|
|
537
|
-
# Try to resolve exact wheel URL from find-links page
|
|
538
|
-
vars_dict = env.as_dict()
|
|
539
|
-
wheel_url = resolve_wheel_from_index(index_url, package, vars_dict, version)
|
|
540
|
-
if wheel_url:
|
|
541
|
-
# Install from resolved URL directly (guarantees we get what we resolved)
|
|
542
|
-
log(f" Wheel: {wheel_url}")
|
|
543
|
-
_pip_install([wheel_url], no_deps=True, log=log)
|
|
544
|
-
else:
|
|
545
|
-
# Fallback to find-links based resolution
|
|
546
|
-
log(f" Find-links: {index_url}")
|
|
547
|
-
log(f" Package: {pkg_spec}")
|
|
548
|
-
_pip_install_with_find_links(pkg_spec, index_url, log)
|
|
549
|
-
|
|
550
|
-
elif method == "pypi_variant":
|
|
551
|
-
# Transform package name based on CUDA version
|
|
552
|
-
vars_dict = env.as_dict()
|
|
553
|
-
if env.cuda_version:
|
|
554
|
-
vars_dict["cuda_short2"] = get_cuda_short2(env.cuda_version)
|
|
555
|
-
actual_package = _substitute_template(config["package_template"], vars_dict)
|
|
556
|
-
pkg_spec = f"{actual_package}=={version}" if version else actual_package
|
|
557
|
-
log(f" Installing {package} (pypi_variant)...")
|
|
558
|
-
log(f" PyPI variant: {pkg_spec}")
|
|
559
|
-
_pip_install([pkg_spec], no_deps=False, log=log)
|
|
560
|
-
|
|
561
|
-
elif method == "github_release":
|
|
562
|
-
# Direct wheel URL from GitHub releases with fallback sources
|
|
563
|
-
_install_from_github_release(package, version, env, config, log)
|
|
564
|
-
|
|
565
|
-
elif legacy_wheel_sources:
|
|
566
|
-
# Fall back to legacy wheel sources from config
|
|
567
|
-
log(f" Installing {package} from config wheel_sources...")
|
|
568
|
-
resolver = WheelResolver()
|
|
569
|
-
if version:
|
|
570
|
-
try:
|
|
571
|
-
url = resolver.resolve(package, version, env, verify=False)
|
|
572
|
-
_pip_install([url], no_deps=True, log=log)
|
|
573
|
-
except WheelNotFoundError:
|
|
574
|
-
# Try with find-links
|
|
575
|
-
pkg_spec = f"{package}=={version}"
|
|
576
|
-
for source in legacy_wheel_sources:
|
|
577
|
-
source_url = _substitute_template(source, env)
|
|
578
|
-
try:
|
|
579
|
-
_pip_install_with_find_links(pkg_spec, source_url, log)
|
|
580
|
-
return
|
|
581
|
-
except InstallError:
|
|
582
|
-
continue
|
|
583
|
-
raise WheelNotFoundError(
|
|
584
|
-
package=package,
|
|
585
|
-
version=version,
|
|
586
|
-
env=env,
|
|
587
|
-
tried_urls=legacy_wheel_sources,
|
|
588
|
-
reason="Not found in any wheel source",
|
|
589
|
-
)
|
|
590
|
-
else:
|
|
591
|
-
# Package not in registry - try regular pip install (e.g., spconv-cu126)
|
|
592
|
-
log(f" Installing {package} from PyPI...")
|
|
593
|
-
pkg_spec = f"{package}=={version}" if version else package
|
|
594
|
-
_pip_install([pkg_spec], no_deps=False, log=log)
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
def _substitute_template(template: str, env_or_dict: Union[RuntimeEnv, Dict[str, str]]) -> str:
|
|
598
|
-
"""Substitute template variables with runtime environment values."""
|
|
599
|
-
if isinstance(env_or_dict, dict):
|
|
600
|
-
vars_dict = env_or_dict.copy()
|
|
601
|
-
else:
|
|
602
|
-
vars_dict = env_or_dict.as_dict()
|
|
603
|
-
# Add py_minor for pytorch3d URL pattern
|
|
604
|
-
if env_or_dict.python_version:
|
|
605
|
-
vars_dict["py_minor"] = env_or_dict.python_version.split(".")[-1]
|
|
606
|
-
|
|
607
|
-
result = template
|
|
608
|
-
for key, value in vars_dict.items():
|
|
609
|
-
if value is not None:
|
|
610
|
-
result = result.replace(f"{{{key}}}", str(value))
|
|
611
|
-
return result
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def _pip_install_with_index(
|
|
615
|
-
package: str,
|
|
616
|
-
index_url: str,
|
|
617
|
-
log: Callable[[str], None],
|
|
618
|
-
) -> None:
|
|
619
|
-
"""Install package using pip with --extra-index-url."""
|
|
620
|
-
pip_cmd = _get_pip_command()
|
|
621
|
-
args = pip_cmd + ["install", "--extra-index-url", index_url, package]
|
|
622
|
-
|
|
623
|
-
log(f" Running: pip install --extra-index-url ... {package}")
|
|
624
|
-
result = subprocess.run(args, capture_output=True, text=True)
|
|
625
367
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
368
|
+
# wheel_template: direct URL
|
|
369
|
+
if "wheel_template" in config:
|
|
370
|
+
effective_version = version or config.get("default_version")
|
|
371
|
+
if not effective_version:
|
|
372
|
+
raise InstallError(f"Package {package} requires version (no default in registry)")
|
|
373
|
+
vars_dict["version"] = effective_version
|
|
374
|
+
return _substitute_template(config["wheel_template"], vars_dict)
|
|
632
375
|
|
|
376
|
+
# package_name: PyPI variant (e.g., spconv-cu124)
|
|
377
|
+
if "package_name" in config:
|
|
378
|
+
pkg_name = _substitute_template(config["package_name"], vars_dict)
|
|
379
|
+
return f"pypi:{pkg_name}" # Special marker for PyPI install
|
|
633
380
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
pip_cmd = _get_pip_command()
|
|
641
|
-
args = pip_cmd + ["install", "--find-links", find_links_url, package]
|
|
642
|
-
|
|
643
|
-
log(f" Running: pip install --find-links ... {package}")
|
|
644
|
-
result = subprocess.run(args, capture_output=True, text=True)
|
|
645
|
-
|
|
646
|
-
if result.returncode != 0:
|
|
647
|
-
raise InstallError(
|
|
648
|
-
f"Failed to install {package}",
|
|
649
|
-
exit_code=result.returncode,
|
|
650
|
-
stderr=result.stderr,
|
|
651
|
-
)
|
|
381
|
+
raise InstallError(
|
|
382
|
+
f"Package {package} not found in registry or user wheel_sources.\n"
|
|
383
|
+
f"Add it to [wheel_sources] in your comfy-env.toml:\n\n"
|
|
384
|
+
f"[wheel_sources]\n"
|
|
385
|
+
f'{package} = "https://example.com/{package}-{{version}}+cu{{cuda_short}}-{{py_tag}}-{{platform}}.whl"'
|
|
386
|
+
)
|
|
652
387
|
|
|
653
388
|
|
|
654
|
-
def
|
|
389
|
+
def _install_cuda_package(
|
|
655
390
|
package: str,
|
|
656
391
|
version: Optional[str],
|
|
657
392
|
env: RuntimeEnv,
|
|
658
|
-
|
|
393
|
+
user_wheel_sources: Dict[str, str],
|
|
659
394
|
log: Callable[[str], None],
|
|
660
395
|
) -> None:
|
|
661
|
-
"""
|
|
396
|
+
"""
|
|
397
|
+
Install a single CUDA package.
|
|
662
398
|
|
|
663
|
-
|
|
664
|
-
sources for different platforms (Linux: Dao-AILab, mjun0812; Windows: bdashore3).
|
|
399
|
+
Uses wheel_template for direct URL or package_name for PyPI variants.
|
|
665
400
|
"""
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
)
|
|
401
|
+
url_or_marker = _resolve_wheel_url(package, version, env, user_wheel_sources)
|
|
402
|
+
|
|
403
|
+
if url_or_marker.startswith("pypi:"):
|
|
404
|
+
# PyPI variant package (e.g., spconv-cu124)
|
|
405
|
+
pkg_name = url_or_marker[5:] # Strip "pypi:" prefix
|
|
406
|
+
pkg_spec = f"{pkg_name}=={version}" if version else pkg_name
|
|
407
|
+
log(f" Installing {package} as {pkg_spec} from PyPI...")
|
|
408
|
+
_pip_install([pkg_spec], no_deps=False, log=log)
|
|
409
|
+
else:
|
|
410
|
+
# Direct wheel URL
|
|
411
|
+
log(f" Installing {package}...")
|
|
412
|
+
log(f" URL: {url_or_marker}")
|
|
413
|
+
_pip_install([url_or_marker], no_deps=True, log=log)
|
|
670
414
|
|
|
671
|
-
sources = config.get("sources", [])
|
|
672
|
-
if not sources:
|
|
673
|
-
raise InstallError(f"No sources configured for {package}")
|
|
674
415
|
|
|
675
|
-
|
|
416
|
+
def _build_template_vars(env: RuntimeEnv, version: Optional[str] = None) -> Dict[str, str]:
|
|
417
|
+
"""Build template variables dict from RuntimeEnv."""
|
|
676
418
|
vars_dict = env.as_dict()
|
|
677
|
-
vars_dict["version"] = version
|
|
678
419
|
|
|
679
|
-
|
|
680
|
-
|
|
420
|
+
if version:
|
|
421
|
+
vars_dict["version"] = version
|
|
681
422
|
|
|
682
|
-
# Add
|
|
423
|
+
# Add cuda_short2 for spconv (e.g., "124" not "1240")
|
|
683
424
|
if env.cuda_version:
|
|
684
|
-
vars_dict["
|
|
685
|
-
|
|
686
|
-
# Filter sources by platform
|
|
687
|
-
current_platform = env.platform_tag
|
|
688
|
-
compatible_sources = [
|
|
689
|
-
s for s in sources
|
|
690
|
-
if current_platform in s.get("platforms", [])
|
|
691
|
-
]
|
|
692
|
-
|
|
693
|
-
if not compatible_sources:
|
|
694
|
-
available = set()
|
|
695
|
-
for s in sources:
|
|
696
|
-
available.update(s.get("platforms", []))
|
|
697
|
-
raise InstallError(
|
|
698
|
-
f"No {package} wheels available for platform {current_platform}. "
|
|
699
|
-
f"Available platforms: {', '.join(sorted(available))}"
|
|
700
|
-
)
|
|
701
|
-
|
|
702
|
-
# Try each source in order
|
|
703
|
-
errors = []
|
|
704
|
-
for source in compatible_sources:
|
|
705
|
-
source_name = source.get("name", "unknown")
|
|
706
|
-
url_template = source.get("url_template", "")
|
|
707
|
-
|
|
708
|
-
# Substitute template variables
|
|
709
|
-
url = url_template
|
|
710
|
-
for key, value in vars_dict.items():
|
|
711
|
-
if value is not None:
|
|
712
|
-
url = url.replace(f"{{{key}}}", str(value))
|
|
713
|
-
|
|
714
|
-
log(f" Trying {source_name}: {package}=={version}...")
|
|
715
|
-
log(f" Resolved wheel to: {url}")
|
|
716
|
-
|
|
717
|
-
try:
|
|
718
|
-
pip_cmd = _get_pip_command()
|
|
719
|
-
args = pip_cmd + ["install", "--no-deps", url]
|
|
720
|
-
|
|
721
|
-
result = subprocess.run(args, capture_output=True, text=True)
|
|
722
|
-
|
|
723
|
-
if result.returncode == 0:
|
|
724
|
-
log(f" Successfully installed from {source_name}")
|
|
725
|
-
return
|
|
726
|
-
else:
|
|
727
|
-
error_msg = result.stderr.strip().split('\n')[-1] if result.stderr else "Unknown error"
|
|
728
|
-
errors.append(f"{source_name}: {error_msg}")
|
|
729
|
-
log(f" Failed: {error_msg[:80]}...")
|
|
730
|
-
|
|
731
|
-
except Exception as e:
|
|
732
|
-
errors.append(f"{source_name}: {str(e)}")
|
|
733
|
-
log(f" Error: {str(e)[:80]}...")
|
|
734
|
-
|
|
735
|
-
# All sources failed
|
|
736
|
-
raise InstallError(
|
|
737
|
-
f"Failed to install {package}=={version} from any source.\n"
|
|
738
|
-
f"Tried sources:\n" + "\n".join(f" - {e}" for e in errors)
|
|
739
|
-
)
|
|
740
|
-
|
|
425
|
+
vars_dict["cuda_short2"] = get_cuda_short2(env.cuda_version)
|
|
741
426
|
|
|
742
|
-
|
|
743
|
-
"""Extract CUDA packages that need wheel resolution."""
|
|
744
|
-
# For now, treat no_deps_requirements as CUDA packages
|
|
745
|
-
# In future, could parse from [packages.cuda] section
|
|
746
|
-
return env_config.no_deps_requirements or []
|
|
427
|
+
return vars_dict
|
|
747
428
|
|
|
748
429
|
|
|
749
|
-
def
|
|
750
|
-
"""
|
|
751
|
-
|
|
430
|
+
def _substitute_template(template: str, vars_dict: Dict[str, str]) -> str:
|
|
431
|
+
"""Substitute {var} placeholders in template with values from vars_dict."""
|
|
432
|
+
result = template
|
|
433
|
+
for key, value in vars_dict.items():
|
|
434
|
+
if value is not None:
|
|
435
|
+
result = result.replace(f"{{{key}}}", str(value))
|
|
436
|
+
return result
|
|
752
437
|
|
|
753
438
|
|
|
754
439
|
def _pip_install(
|
|
@@ -756,18 +441,7 @@ def _pip_install(
|
|
|
756
441
|
no_deps: bool = False,
|
|
757
442
|
log: Callable[[str], None] = print,
|
|
758
443
|
) -> None:
|
|
759
|
-
"""
|
|
760
|
-
Install packages using pip.
|
|
761
|
-
|
|
762
|
-
Args:
|
|
763
|
-
packages: List of packages or URLs to install.
|
|
764
|
-
no_deps: If True, use --no-deps flag.
|
|
765
|
-
log: Logging callback.
|
|
766
|
-
|
|
767
|
-
Raises:
|
|
768
|
-
InstallError: If pip install fails.
|
|
769
|
-
"""
|
|
770
|
-
# Prefer uv if available for speed
|
|
444
|
+
"""Install packages using pip (prefers uv if available)."""
|
|
771
445
|
pip_cmd = _get_pip_command()
|
|
772
446
|
|
|
773
447
|
args = pip_cmd + ["install"]
|
|
@@ -777,11 +451,7 @@ def _pip_install(
|
|
|
777
451
|
|
|
778
452
|
log(f"Running: {' '.join(args[:3])}... ({len(packages)} packages)")
|
|
779
453
|
|
|
780
|
-
result = subprocess.run(
|
|
781
|
-
args,
|
|
782
|
-
capture_output=True,
|
|
783
|
-
text=True,
|
|
784
|
-
)
|
|
454
|
+
result = subprocess.run(args, capture_output=True, text=True)
|
|
785
455
|
|
|
786
456
|
if result.returncode != 0:
|
|
787
457
|
raise InstallError(
|
|
@@ -793,12 +463,9 @@ def _pip_install(
|
|
|
793
463
|
|
|
794
464
|
def _get_pip_command() -> List[str]:
|
|
795
465
|
"""Get the pip command to use (prefers uv if available)."""
|
|
796
|
-
# Check for uv
|
|
797
466
|
uv_path = shutil.which("uv")
|
|
798
467
|
if uv_path:
|
|
799
468
|
return [uv_path, "pip"]
|
|
800
|
-
|
|
801
|
-
# Fall back to pip
|
|
802
469
|
return [sys.executable, "-m", "pip"]
|
|
803
470
|
|
|
804
471
|
|
|
@@ -806,65 +473,16 @@ def verify_installation(
|
|
|
806
473
|
packages: List[str],
|
|
807
474
|
log: Callable[[str], None] = print,
|
|
808
475
|
) -> bool:
|
|
809
|
-
"""
|
|
810
|
-
Verify that packages are importable.
|
|
811
|
-
|
|
812
|
-
Args:
|
|
813
|
-
packages: List of package names to verify.
|
|
814
|
-
log: Logging callback.
|
|
815
|
-
|
|
816
|
-
Returns:
|
|
817
|
-
True if all packages are importable.
|
|
818
|
-
"""
|
|
476
|
+
"""Verify that packages are importable."""
|
|
819
477
|
all_ok = True
|
|
820
478
|
for package in packages:
|
|
821
|
-
# Convert package name to import name
|
|
822
479
|
import_name = package.replace("-", "_").split("[")[0]
|
|
823
|
-
|
|
824
480
|
try:
|
|
825
481
|
__import__(import_name)
|
|
826
482
|
log(f" {package}: OK")
|
|
827
483
|
except ImportError as e:
|
|
828
484
|
log(f" {package}: FAILED ({e})")
|
|
829
485
|
all_ok = False
|
|
830
|
-
|
|
831
486
|
return all_ok
|
|
832
487
|
|
|
833
488
|
|
|
834
|
-
def setup(
|
|
835
|
-
log_callback: Optional[Callable[[str], None]] = None,
|
|
836
|
-
dry_run: bool = False,
|
|
837
|
-
) -> bool:
|
|
838
|
-
"""
|
|
839
|
-
One-liner setup that auto-discovers config from caller's directory.
|
|
840
|
-
|
|
841
|
-
This is the simplest way to install dependencies - just call setup()
|
|
842
|
-
from your install.py and it will find the comfy-env.toml in the same
|
|
843
|
-
directory as the calling script.
|
|
844
|
-
|
|
845
|
-
Example:
|
|
846
|
-
# install.py (entire file)
|
|
847
|
-
from comfy_env import setup
|
|
848
|
-
setup()
|
|
849
|
-
|
|
850
|
-
Args:
|
|
851
|
-
log_callback: Optional callback for logging. Defaults to print.
|
|
852
|
-
dry_run: If True, show what would be installed without installing.
|
|
853
|
-
|
|
854
|
-
Returns:
|
|
855
|
-
True if installation succeeded.
|
|
856
|
-
|
|
857
|
-
Raises:
|
|
858
|
-
FileNotFoundError: If no config file found.
|
|
859
|
-
InstallError: If installation fails.
|
|
860
|
-
"""
|
|
861
|
-
# Get the caller's directory by inspecting the stack
|
|
862
|
-
frame = inspect.stack()[1]
|
|
863
|
-
caller_file = frame.filename
|
|
864
|
-
caller_dir = Path(caller_file).parent.resolve()
|
|
865
|
-
|
|
866
|
-
return install(
|
|
867
|
-
node_dir=caller_dir,
|
|
868
|
-
log_callback=log_callback,
|
|
869
|
-
dry_run=dry_run,
|
|
870
|
-
)
|