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/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", mode="inplace")
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 Any, Callable, Dict, List, Optional, Set, Union
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, DependencyError, InstallError, WheelNotFoundError
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, WheelResolver, parse_wheel_requirement
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 # Don't fail - just warn
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
- update_result = subprocess.run(
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
- install_cmd,
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. Cannot install system packages automatically.")
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
- return True # Don't fail - just warn
97
+
98
+ return True
113
99
 
114
100
  elif platform == "darwin":
115
101
  packages = system_config.darwin
116
- if not packages:
117
- return True
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 not packages:
127
- return True
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 a comfy-env.toml configuration.
146
+ Install dependencies from comfy-env.toml, auto-discovered from caller's directory.
188
147
 
189
- This is the main entry point for installing CUDA dependencies.
190
- Mode is auto-detected from config:
191
- - [envname] with python = "X.X" → isolated (creates separate venv)
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
- # Load full configuration (includes tools)
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 configuration file found. "
228
- "Create comfy-env.toml or specify path explicitly."
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(f"Environment uses conda packages - using pixi backend")
188
+ log("Environment uses conda packages - using pixi backend")
253
189
  return pixi_install(env_config, node_dir, log, dry_run)
254
190
 
255
- # Auto-detect mode based on config:
256
- # - env_config.python set isolated (creates separate venv)
257
- # - env_config without python → inplace (installs into current env)
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, verify_wheels)
198
+ return _install_inplace(env_config, node_dir, log, dry_run, user_wheel_sources)
264
199
  elif full_config.has_local:
265
- # Handle [local.cuda] and [local.packages] without isolated env
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
- verify_wheels: bool,
243
+ user_wheel_sources: Dict[str, str],
311
244
  ) -> bool:
312
- """Install in-place into current environment using the package registry."""
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 = _get_cuda_packages(env_config)
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
- # Get packages to install
332
- cuda_packages = _get_cuda_packages(env_config)
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
- install_info = _get_install_info(package, version, env, legacy_wheel_sources)
343
- log(f" {package}: {install_info['description']}")
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, legacy_wheel_sources, log)
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 (no isolated venv)."""
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, [], log)
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 _get_install_info(
342
+ def _resolve_wheel_url(
424
343
  package: str,
425
344
  version: Optional[str],
426
345
  env: RuntimeEnv,
427
- legacy_wheel_sources: List[str],
428
- ) -> Dict[str, str]:
429
- """Get installation info for a package (for dry-run output)."""
430
- pkg_lower = package.lower()
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
- def _install_cuda_package(
464
- package: str,
465
- version: Optional[str],
466
- env: RuntimeEnv,
467
- legacy_wheel_sources: List[str],
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 if package is in registry
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
- if result.returncode != 0:
627
- raise InstallError(
628
- f"Failed to install {package}",
629
- exit_code=result.returncode,
630
- stderr=result.stderr,
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
- def _pip_install_with_find_links(
635
- package: str,
636
- find_links_url: str,
637
- log: Callable[[str], None],
638
- ) -> None:
639
- """Install package using pip with --find-links."""
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 _install_from_github_release(
389
+ def _install_cuda_package(
655
390
  package: str,
656
391
  version: Optional[str],
657
392
  env: RuntimeEnv,
658
- config: Dict[str, Any],
393
+ user_wheel_sources: Dict[str, str],
659
394
  log: Callable[[str], None],
660
395
  ) -> None:
661
- """Install package from GitHub release wheels with fallback sources.
396
+ """
397
+ Install a single CUDA package.
662
398
 
663
- This method handles packages like flash-attn that have multiple wheel
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
- if not version:
667
- raise InstallError(
668
- f"Package {package} requires explicit version for github_release method"
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
- # Build template variables
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
- # Add py_tag (e.g., "cp310")
680
- vars_dict["py_tag"] = f"cp{env.python_short}"
420
+ if version:
421
+ vars_dict["version"] = version
681
422
 
682
- # Add cuda_major (e.g., "12") for Dao-AILab URL pattern
423
+ # Add cuda_short2 for spconv (e.g., "124" not "1240")
683
424
  if env.cuda_version:
684
- vars_dict["cuda_major"] = env.cuda_version.split(".")[0]
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
- def _get_cuda_packages(env_config: IsolatedEnv) -> List[str]:
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 _get_regular_packages(env_config: IsolatedEnv) -> List[str]:
750
- """Extract regular pip packages."""
751
- return env_config.requirements or []
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
- )