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 CHANGED
@@ -1,4 +1,9 @@
1
- __version__ = "0.0.14"
1
+ from importlib.metadata import version, PackageNotFoundError
2
+
3
+ try:
4
+ __version__ = version("comfy-env")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.0.0-dev" # Fallback for editable installs
2
7
 
3
8
  from .env.config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq, CondaConfig
4
9
  from .env.config_file import (
@@ -31,8 +36,8 @@ from .ipc.worker import BaseWorker, register
31
36
  from .decorator import isolated, shutdown_all_processes
32
37
 
33
38
  # New in-place installation API
34
- from .install import install, verify_installation, setup
35
- from .resolver import RuntimeEnv, WheelResolver
39
+ from .install import install, verify_installation
40
+ from .resolver import RuntimeEnv
36
41
 
37
42
  # Pixi integration (for conda packages)
38
43
  from .pixi import (
@@ -82,7 +87,6 @@ __all__ = [
82
87
  "install",
83
88
  "verify_installation",
84
89
  "RuntimeEnv",
85
- "WheelResolver",
86
90
  # Pixi integration (for conda packages)
87
91
  "ensure_pixi",
88
92
  "get_pixi_path",
comfy_env/cli.py CHANGED
@@ -10,7 +10,6 @@ Provides the `comfy-env` command with subcommands:
10
10
 
11
11
  Usage:
12
12
  comfy-env install
13
- comfy-env install --isolated
14
13
  comfy-env install --dry-run
15
14
 
16
15
  comfy-env info
@@ -54,21 +53,11 @@ def main(args: Optional[List[str]] = None) -> int:
54
53
  type=str,
55
54
  help="Path to config file (default: auto-discover)",
56
55
  )
57
- install_parser.add_argument(
58
- "--isolated",
59
- action="store_true",
60
- help="Create isolated venv instead of installing in-place",
61
- )
62
56
  install_parser.add_argument(
63
57
  "--dry-run",
64
58
  action="store_true",
65
59
  help="Show what would be installed without installing",
66
60
  )
67
- install_parser.add_argument(
68
- "--verify",
69
- action="store_true",
70
- help="Verify wheel URLs exist before installing",
71
- )
72
61
  install_parser.add_argument(
73
62
  "--dir", "-d",
74
63
  type=str,
@@ -108,11 +97,6 @@ def main(args: Optional[List[str]] = None) -> int:
108
97
  type=str,
109
98
  help="Path to config file",
110
99
  )
111
- resolve_parser.add_argument(
112
- "--verify",
113
- action="store_true",
114
- help="Verify URLs exist (HTTP HEAD check)",
115
- )
116
100
 
117
101
  # doctor command
118
102
  doctor_parser = subparsers.add_parser(
@@ -175,16 +159,13 @@ def cmd_install(args) -> int:
175
159
  """Handle install command."""
176
160
  from .install import install
177
161
 
178
- mode = "isolated" if args.isolated else "inplace"
179
162
  node_dir = Path(args.dir) if args.dir else Path.cwd()
180
163
 
181
164
  try:
182
165
  install(
183
166
  config=args.config,
184
- mode=mode,
185
167
  node_dir=node_dir,
186
168
  dry_run=args.dry_run,
187
- verify_wheels=args.verify,
188
169
  )
189
170
  return 0
190
171
  except FileNotFoundError as e:
@@ -233,18 +214,15 @@ def cmd_info(args) -> int:
233
214
 
234
215
  def cmd_resolve(args) -> int:
235
216
  """Handle resolve command."""
236
- from .resolver import RuntimeEnv, WheelResolver, parse_wheel_requirement
237
- from .registry import PACKAGE_REGISTRY
217
+ from .resolver import RuntimeEnv, parse_wheel_requirement
218
+ from .registry import PACKAGE_REGISTRY, get_cuda_short2
238
219
  from .env.config_file import discover_env_config, load_env_from_file
239
220
 
240
221
  env = RuntimeEnv.detect()
241
- resolver = WheelResolver()
242
-
243
222
  packages = []
244
223
 
245
224
  # Get packages from args or config
246
225
  if args.all or (not args.packages and args.config):
247
- # Load from config
248
226
  if args.config:
249
227
  config = load_env_from_file(Path(args.config))
250
228
  else:
@@ -264,52 +242,57 @@ def cmd_resolve(args) -> int:
264
242
  print(f"Resolving wheels for: {env}")
265
243
  print("=" * 60)
266
244
 
245
+ # Build template variables
246
+ vars_dict = env.as_dict()
247
+ if env.cuda_version:
248
+ vars_dict["cuda_short2"] = get_cuda_short2(env.cuda_version)
249
+
267
250
  all_ok = True
268
251
  for pkg_spec in packages:
269
252
  package, version = parse_wheel_requirement(pkg_spec)
270
- if version is None:
271
- print(f" {package}: No version specified, skipping")
272
- continue
273
-
274
253
  pkg_lower = package.lower()
254
+
275
255
  try:
276
- # Check if package is in registry with github_release method
277
256
  if pkg_lower in PACKAGE_REGISTRY:
278
- registry_config = PACKAGE_REGISTRY[pkg_lower]
279
- method = registry_config.get("method")
280
-
281
- if method == "github_release":
282
- # Resolve URL from registry sources
283
- url = _resolve_github_release_url(package, version, env, registry_config)
284
- status = "OK" if args.verify else "resolved"
285
- print(f" {package}=={version}: {status}")
257
+ config = PACKAGE_REGISTRY[pkg_lower]
258
+
259
+ if "wheel_template" in config:
260
+ # Direct wheel URL template
261
+ effective_version = version or config.get("default_version")
262
+ if not effective_version:
263
+ print(f" {package}: No version specified (no default in registry)")
264
+ all_ok = False
265
+ continue
266
+
267
+ vars_dict["version"] = effective_version
268
+ url = _substitute_template(config["wheel_template"], vars_dict)
269
+ print(f" {package}=={effective_version}: resolved")
286
270
  print(f" {url}")
271
+
272
+ elif "package_name" in config:
273
+ # PyPI variant (e.g., spconv-cu124)
274
+ pkg_name = _substitute_template(config["package_name"], vars_dict)
275
+ pkg_spec = f"{pkg_name}=={version}" if version else pkg_name
276
+ print(f" {package}: installs as {pkg_spec} from PyPI")
277
+
287
278
  else:
288
- # For other methods, just show what method will be used
289
- print(f" {package}=={version}: uses {method} method")
290
- if "index_url" in registry_config:
291
- index_url = _substitute_template(registry_config["index_url"], env)
292
- print(f" index: {index_url}")
293
- elif "package_template" in registry_config:
294
- pkg_name = _substitute_template(registry_config["package_template"], env)
295
- print(f" installs as: {pkg_name}")
279
+ print(f" {package}: no wheel_template or package_name in registry")
280
+ all_ok = False
296
281
  else:
297
- # Fall back to WheelResolver
298
- url = resolver.resolve(package, version, env, verify=args.verify)
299
- status = "OK" if args.verify else "resolved"
300
- print(f" {package}=={version}: {status}")
301
- print(f" {url}")
282
+ print(f" {package}: NOT in registry")
283
+ print(f" Add to [wheel_sources] in comfy-env.toml:")
284
+ print(f' {package} = "https://example.com/{package}-{{version}}+cu{{cuda_short}}-{{py_tag}}-{{platform}}.whl"')
285
+ all_ok = False
286
+
302
287
  except Exception as e:
303
- print(f" {package}=={version}: FAILED")
304
- _print_wheel_not_found_error(package, version, env, e)
288
+ print(f" {package}: FAILED - {e}")
305
289
  all_ok = False
306
290
 
307
291
  return 0 if all_ok else 1
308
292
 
309
293
 
310
- def _substitute_template(template: str, env) -> str:
311
- """Substitute environment variables into a URL template."""
312
- vars_dict = env.as_dict()
294
+ def _substitute_template(template: str, vars_dict: dict) -> str:
295
+ """Substitute {var} placeholders in template."""
313
296
  result = template
314
297
  for key, value in vars_dict.items():
315
298
  if value is not None:
@@ -317,70 +300,6 @@ def _substitute_template(template: str, env) -> str:
317
300
  return result
318
301
 
319
302
 
320
- def _resolve_github_release_url(package: str, version: str, env, config: dict) -> str:
321
- """Resolve URL for github_release method packages."""
322
- sources = config.get("sources", [])
323
- if not sources:
324
- raise ValueError(f"No sources configured for {package}")
325
-
326
- # Build template variables
327
- vars_dict = env.as_dict()
328
- vars_dict["version"] = version
329
- vars_dict["py_tag"] = f"cp{env.python_short}"
330
- if env.cuda_version:
331
- vars_dict["cuda_major"] = env.cuda_version.split(".")[0]
332
-
333
- # Filter sources by platform
334
- current_platform = env.platform_tag
335
- compatible_sources = [
336
- s for s in sources
337
- if current_platform in s.get("platforms", [])
338
- ]
339
-
340
- if not compatible_sources:
341
- available = set()
342
- for s in sources:
343
- available.update(s.get("platforms", []))
344
- raise ValueError(
345
- f"No {package} wheels for platform {current_platform}. "
346
- f"Available: {', '.join(sorted(available))}"
347
- )
348
-
349
- # Return URL from first compatible source
350
- source = compatible_sources[0]
351
- url_template = source.get("url_template", "")
352
- url = url_template
353
- for key, value in vars_dict.items():
354
- if value is not None:
355
- url = url.replace(f"{{{key}}}", str(value))
356
-
357
- return url
358
-
359
-
360
- def _print_wheel_not_found_error(package: str, version: str, env, error: Exception) -> None:
361
- """Print a formatted error message for wheel not found."""
362
- from .errors import WheelNotFoundError
363
-
364
- if isinstance(error, WheelNotFoundError):
365
- print(f" CUDA wheel not found: {package}=={version}")
366
- print()
367
- print("+------------------------------------------------------------------+")
368
- print("| CUDA Wheel Not Found |")
369
- print("+------------------------------------------------------------------+")
370
- print(f"| Package: {package}=={version:<46} |")
371
- print(f"| Requested: cu{env.cuda_short}-torch{env.torch_mm}-{env.python_short}-{env.platform_tag:<17} |")
372
- print("| |")
373
- print(f"| Reason: {error.reason:<54} |")
374
- print("| |")
375
- print("| Suggestions: |")
376
- print(f"| 1. Check if wheel exists: comfy-env resolve {package:<15} |")
377
- print(f"| 2. Build wheel locally: comfy-env build {package:<18} |")
378
- print("| |")
379
- print("+------------------------------------------------------------------+")
380
- else:
381
- print(f" {error}")
382
-
383
-
384
303
  def cmd_doctor(args) -> int:
385
304
  """Handle doctor command."""
386
305
  from .install import verify_installation
@@ -409,7 +328,6 @@ def cmd_doctor(args) -> int:
409
328
  packages = (config.requirements or []) + (config.no_deps_requirements or [])
410
329
 
411
330
  if packages:
412
- # Extract package names from specs
413
331
  pkg_names = []
414
332
  for pkg in packages:
415
333
  name = pkg.split("==")[0].split(">=")[0].split("[")[0]
@@ -429,55 +347,67 @@ def cmd_doctor(args) -> int:
429
347
 
430
348
  def cmd_list_packages(args) -> int:
431
349
  """Handle list-packages command."""
432
- from .registry import PACKAGE_REGISTRY, list_packages
350
+ from .registry import PACKAGE_REGISTRY
433
351
 
434
352
  if args.json:
435
353
  import json
436
354
  result = {}
437
355
  for name, config in PACKAGE_REGISTRY.items():
438
356
  result[name] = {
439
- "method": config["method"],
440
357
  "description": config.get("description", ""),
441
358
  }
442
- if "index_url" in config:
443
- result[name]["index_url"] = config["index_url"]
444
- if "package_template" in config:
445
- result[name]["package_template"] = config["package_template"]
359
+ if "wheel_template" in config:
360
+ result[name]["wheel_template"] = config["wheel_template"]
361
+ if "package_name" in config:
362
+ result[name]["package_name"] = config["package_name"]
363
+ if "default_version" in config:
364
+ result[name]["default_version"] = config["default_version"]
446
365
  print(json.dumps(result, indent=2))
447
366
  return 0
448
367
 
449
368
  print("Built-in CUDA Package Registry")
450
369
  print("=" * 60)
451
370
  print()
452
- print("These packages can be installed without specifying wheel_sources.")
453
- print("Just add them to your comfy-env.toml:")
371
+ print("These packages can be installed by adding them to comfy-env.toml:")
454
372
  print()
455
373
  print(" [cuda]")
456
- print(" torch-scatter = \"2.1.2\"")
457
- print(" torch-cluster = \"1.6.3\"")
374
+ print(' nvdiffrast = "0.4.0"')
375
+ print(' torch-scatter = "2.1.2"')
376
+ print()
377
+ print("Or override with custom wheel source:")
378
+ print()
379
+ print(" [wheel_sources]")
380
+ print(' nvdiffrast = "https://my-mirror.com/nvdiffrast-{version}+cu{cuda_short}-{py_tag}-{platform}.whl"')
458
381
  print()
459
382
  print("-" * 60)
460
383
 
461
- # Group packages by method
462
- by_method = {}
384
+ # Group by type
385
+ wheel_template_packages = []
386
+ package_name_packages = []
387
+
463
388
  for name, config in PACKAGE_REGISTRY.items():
464
- method = config["method"]
465
- if method not in by_method:
466
- by_method[method] = []
467
- by_method[method].append((name, config))
468
-
469
- method_labels = {
470
- "index": "PEP 503 Index (pip --extra-index-url)",
471
- "github_index": "GitHub Pages (pip --find-links)",
472
- "pypi_variant": "PyPI with CUDA variant names",
473
- }
474
-
475
- for method, packages in by_method.items():
476
- print(f"\n{method_labels.get(method, method)}:")
477
- for name, config in sorted(packages):
478
- desc = config.get("description", "")
389
+ desc = config.get("description", "")
390
+ default = config.get("default_version", "")
391
+ if "wheel_template" in config:
392
+ wheel_template_packages.append((name, desc, default))
393
+ elif "package_name" in config:
394
+ package_name_packages.append((name, desc, config["package_name"]))
395
+
396
+ if wheel_template_packages:
397
+ print("\nDirect wheel URL packages:")
398
+ for name, desc, default in sorted(wheel_template_packages):
399
+ version_info = f" (default: {default})" if default else ""
400
+ print(f" {name:20} - {desc}{version_info}")
401
+
402
+ if package_name_packages:
403
+ print("\nPyPI variant packages:")
404
+ for name, desc, pkg_template in sorted(package_name_packages):
479
405
  print(f" {name:20} - {desc}")
406
+ print(f" installs as: {pkg_template}")
480
407
 
408
+ print()
409
+ print("Template variables: {version}, {cuda_short}, {torch_mm}, {py_tag}, {platform}")
410
+ print("See README for full documentation on writing wheel templates.")
481
411
  print()
482
412
  return 0
483
413
 
comfy_env/env/config.py CHANGED
@@ -68,12 +68,14 @@ class EnvManagerConfig:
68
68
  [envname.packages] - Regular packages for isolated env
69
69
  [node_reqs] - Node dependencies
70
70
  [tools] - External tools (e.g., blender = "4.2")
71
+ [wheel_sources] - Custom wheel URL templates (override registry)
71
72
  """
72
73
  system: SystemConfig = field(default_factory=SystemConfig)
73
74
  local: LocalConfig = field(default_factory=LocalConfig)
74
75
  envs: Dict[str, "IsolatedEnv"] = field(default_factory=dict)
75
76
  node_reqs: List[NodeReq] = field(default_factory=list)
76
77
  tools: Dict[str, ToolConfig] = field(default_factory=dict)
78
+ wheel_sources: Dict[str, str] = field(default_factory=dict) # package -> wheel_template URL
77
79
 
78
80
  @property
79
81
  def has_system(self) -> bool:
@@ -219,7 +219,7 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
219
219
 
220
220
  # Handle pytorch version - auto-derive if "auto" or not specified
221
221
  pytorch_version = env_section.get("pytorch_version") or env_section.get("pytorch")
222
- if pytorch_version == "auto" or (pytorch_version is None and cuda):
222
+ if pytorch_version == "auto" or pytorch_version is None:
223
223
  pytorch_version = _get_default_pytorch_version(cuda)
224
224
 
225
225
  if pytorch_version:
@@ -332,7 +332,7 @@ def _substitute_vars(s: str, variables: Dict[str, str]) -> str:
332
332
  # =============================================================================
333
333
 
334
334
  # Reserved table names that are NOT isolated environments
335
- RESERVED_TABLES = {"local", "node_reqs", "env", "packages", "sources", "cuda", "variables", "worker", "tools", "system"}
335
+ RESERVED_TABLES = {"local", "node_reqs", "env", "packages", "sources", "cuda", "variables", "worker", "tools", "system", "wheel_sources"}
336
336
 
337
337
 
338
338
  def load_config(
@@ -415,6 +415,7 @@ def _parse_full_config(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig
415
415
  [envname.cuda] - CUDA packages for env
416
416
  [envname.packages] - Packages for env
417
417
  [node_reqs] - Node dependencies
418
+ [wheel_sources] - Custom wheel URL templates
418
419
 
419
420
  Also supports simple format ([env] + [packages]) for backward compatibility.
420
421
  """
@@ -431,6 +432,7 @@ def _parse_full_config(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig
431
432
  envs = _parse_env_sections(data, base_dir)
432
433
  node_reqs = _parse_node_reqs(data.get("node_reqs", {}))
433
434
  tools = _parse_tools_section(data.get("tools", {}))
435
+ wheel_sources = _parse_wheel_sources_section(data.get("wheel_sources", {}))
434
436
 
435
437
  return EnvManagerConfig(
436
438
  system=system,
@@ -438,6 +440,7 @@ def _parse_full_config(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig
438
440
  envs=envs,
439
441
  node_reqs=node_reqs,
440
442
  tools=tools,
443
+ wheel_sources=wheel_sources,
441
444
  )
442
445
 
443
446
 
@@ -656,6 +659,24 @@ def _parse_system_section(system_data: Dict[str, Any]) -> SystemConfig:
656
659
  )
657
660
 
658
661
 
662
+ def _parse_wheel_sources_section(wheel_sources_data: Dict[str, Any]) -> Dict[str, str]:
663
+ """Parse [wheel_sources] section.
664
+
665
+ Supports:
666
+ [wheel_sources]
667
+ nvdiffrast = "https://example.com/nvdiffrast-{version}+cu{cuda_short}-{py_tag}-{platform}.whl"
668
+ my-package = "https://my-server.com/my-package-{version}.whl"
669
+
670
+ Returns:
671
+ Dict mapping package name (lowercase) to wheel URL template
672
+ """
673
+ wheel_sources = {}
674
+ for name, url_template in wheel_sources_data.items():
675
+ if isinstance(url_template, str):
676
+ wheel_sources[name.lower()] = url_template
677
+ return wheel_sources
678
+
679
+
659
680
  def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
660
681
  """Convert simple config format to full EnvManagerConfig.
661
682
 
@@ -665,9 +686,10 @@ def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerC
665
686
  # Parse using simple parser to get IsolatedEnv
666
687
  simple_env = _parse_config(data, base_dir)
667
688
 
668
- # Parse tools and system sections (shared between simple and full format)
689
+ # Parse tools, system, and wheel_sources sections (shared between simple and full format)
669
690
  tools = _parse_tools_section(data.get("tools", {}))
670
691
  system = _parse_system_section(data.get("system", {}))
692
+ wheel_sources = _parse_wheel_sources_section(data.get("wheel_sources", {}))
671
693
 
672
694
  # Check if this has explicit env settings (isolated venv) vs just CUDA packages (local install)
673
695
  env_section = data.get("env", {})
@@ -681,6 +703,7 @@ def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerC
681
703
  envs={simple_env.name: simple_env},
682
704
  node_reqs=[],
683
705
  tools=tools,
706
+ wheel_sources=wheel_sources,
684
707
  )
685
708
  else:
686
709
  # Local CUDA packages only (no isolated venv)
@@ -701,4 +724,5 @@ def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerC
701
724
  envs={},
702
725
  node_reqs=[],
703
726
  tools=tools,
727
+ wheel_sources=wheel_sources,
704
728
  )
comfy_env/env/manager.py CHANGED
@@ -22,7 +22,6 @@ from .security import (
22
22
  )
23
23
  from ..registry import PACKAGE_REGISTRY, is_registered, get_cuda_short2
24
24
  from ..resolver import RuntimeEnv, parse_wheel_requirement
25
- from ..index_resolver import resolve_wheel_from_index
26
25
 
27
26
 
28
27
  class IsolatedEnvManager:
@@ -373,7 +372,6 @@ class IsolatedEnvManager:
373
372
  vars_dict = runtime_env.as_dict()
374
373
  if env.cuda:
375
374
  vars_dict["cuda_short2"] = get_cuda_short2(env.cuda)
376
- # Add py_tag for wheel filename templates (e.g., cp310)
377
375
  vars_dict["py_tag"] = f"cp{env.python.replace('.', '')}"
378
376
 
379
377
  for req in env.no_deps_requirements:
@@ -382,142 +380,45 @@ class IsolatedEnvManager:
382
380
 
383
381
  if pkg_lower in PACKAGE_REGISTRY:
384
382
  config = PACKAGE_REGISTRY[pkg_lower]
385
- method = config["method"]
386
- self.log(f" Installing {package} ({method})...")
387
-
388
- if method == "index":
389
- # PEP 503 index - try to resolve exact wheel URL first
390
- index_url = self._substitute_template(config["index_url"], vars_dict)
391
- pkg_spec = f"{package}=={version}" if version else package
392
- # Try to resolve exact wheel URL from index
393
- wheel_url = resolve_wheel_from_index(index_url, package, vars_dict, version)
394
- if wheel_url:
395
- # Install from resolved URL directly (guarantees we get what we resolved)
396
- self.log(f" Wheel: {wheel_url}")
397
- result = subprocess.run(
398
- pip_args + ["--no-deps", wheel_url],
399
- capture_output=True, text=True,
400
- )
401
- else:
402
- # Fallback to index-based resolution
403
- self.log(f" Index: {index_url}")
404
- self.log(f" Package: {pkg_spec}")
405
- result = subprocess.run(
406
- pip_args + ["--extra-index-url", index_url, "--no-deps", pkg_spec],
407
- capture_output=True, text=True,
408
- )
409
-
410
- elif method in ("github_index", "find_links"):
411
- # GitHub Pages or generic find-links
412
- index_url = self._substitute_template(config["index_url"], vars_dict)
413
- pkg_spec = f"{package}=={version}" if version else package
414
- # Try to resolve exact wheel URL from find-links page
415
- wheel_url = resolve_wheel_from_index(index_url, package, vars_dict, version)
416
- if wheel_url:
417
- # Install from resolved URL directly (guarantees we get what we resolved)
418
- self.log(f" Wheel: {wheel_url}")
419
- result = subprocess.run(
420
- pip_args + ["--no-deps", wheel_url],
421
- capture_output=True, text=True,
422
- )
423
- else:
424
- # Fallback to find-links based resolution
425
- self.log(f" Find-links: {index_url}")
426
- self.log(f" Package: {pkg_spec}")
427
- result = subprocess.run(
428
- pip_args + ["--find-links", index_url, "--no-deps", pkg_spec],
429
- capture_output=True, text=True,
430
- )
431
-
432
- elif method == "pypi_variant":
433
- # Transform package name based on CUDA version
434
- actual_package = self._substitute_template(config["package_template"], vars_dict)
435
- pkg_spec = f"{actual_package}=={version}" if version else actual_package
436
- self.log(f" PyPI variant: {pkg_spec}")
383
+
384
+ if "wheel_template" in config:
385
+ # Direct wheel URL from template
386
+ effective_version = version or config.get("default_version")
387
+ if not effective_version:
388
+ raise RuntimeError(f"Package {package} requires version (no default in registry)")
389
+
390
+ vars_dict["version"] = effective_version
391
+ wheel_url = self._substitute_template(config["wheel_template"], vars_dict)
392
+ self.log(f" Installing {package}=={effective_version}...")
393
+ self.log(f" URL: {wheel_url}")
437
394
  result = subprocess.run(
438
- pip_args + ["--no-deps", pkg_spec],
395
+ pip_args + ["--no-deps", wheel_url],
439
396
  capture_output=True, text=True,
440
397
  )
441
-
442
- elif method == "github_release":
443
- # Direct wheel URL from GitHub releases
444
- release_vars = vars_dict.copy()
445
- release_vars["version"] = version or ""
446
- self._install_from_github_release(
447
- package, version, release_vars, config, pip_args
448
- )
449
- continue # Already handled
450
-
451
- else:
452
- # Unknown method - try regular install
453
- self.log(f" Unknown method '{method}', trying regular install")
454
- pkg_spec = f"{package}=={version}" if version else package
398
+ if result.returncode != 0:
399
+ raise RuntimeError(f"Failed to install {package}: {result.stderr}")
400
+
401
+ elif "package_name" in config:
402
+ # PyPI variant (e.g., spconv-cu124)
403
+ pkg_name = self._substitute_template(config["package_name"], vars_dict)
404
+ pkg_spec = f"{pkg_name}=={version}" if version else pkg_name
405
+ self.log(f" Installing {package} as {pkg_spec}...")
455
406
  result = subprocess.run(
456
- pip_args + ["--no-deps", pkg_spec],
407
+ pip_args + [pkg_spec],
457
408
  capture_output=True, text=True,
458
409
  )
410
+ if result.returncode != 0:
411
+ raise RuntimeError(f"Failed to install {package}: {result.stderr}")
412
+
413
+ else:
414
+ raise RuntimeError(f"Package {package} in registry but missing wheel_template or package_name")
459
415
 
460
- if result.returncode != 0:
461
- raise RuntimeError(f"Failed to install {package}: {result.stderr}")
462
416
  else:
463
- # Not in registry - try regular pip install (e.g., spconv-cu126)
464
- self.log(f" Installing {package} (PyPI)...")
465
- pkg_spec = f"{package}=={version}" if version else package
466
- result = subprocess.run(
467
- pip_args + ["--no-deps", pkg_spec],
468
- capture_output=True, text=True,
417
+ # Not in registry - error
418
+ raise RuntimeError(
419
+ f"Package {package} not found in registry. "
420
+ f"Add wheel_template to [wheel_sources] in comfy-env.toml"
469
421
  )
470
- if result.returncode != 0:
471
- raise RuntimeError(f"Failed to install {package}: {result.stderr}")
472
-
473
- def _install_from_github_release(
474
- self,
475
- package: str,
476
- version: str,
477
- vars_dict: dict,
478
- config: dict,
479
- pip_args: list,
480
- ) -> None:
481
- """Install package from GitHub release wheels with fallback sources."""
482
- import platform as plat
483
- import sys
484
- # Use consistent platform tags (win_amd64, linux_x86_64, etc.)
485
- if sys.platform == 'win32':
486
- machine = plat.machine().lower()
487
- current_platform = 'win_amd64' if machine in ('amd64', 'x86_64') else f'win_{machine}'
488
- elif sys.platform == 'darwin':
489
- current_platform = f"macosx_{plat.machine()}"
490
- else:
491
- current_platform = f"linux_{plat.machine()}"
492
-
493
- sources = config.get("sources", [])
494
- errors = []
495
-
496
- for source in sources:
497
- # Check platform compatibility
498
- platforms = source.get("platforms", [])
499
- if platforms and not any(p in current_platform for p in platforms):
500
- continue
501
-
502
- url_template = source["url_template"]
503
- url = self._substitute_template(url_template, vars_dict)
504
-
505
- self.log(f" Trying {source.get('name', 'unknown')}: {url}")
506
- result = subprocess.run(
507
- pip_args + ["--no-deps", url],
508
- capture_output=True, text=True,
509
- )
510
-
511
- if result.returncode == 0:
512
- return # Success!
513
-
514
- errors.append(f"{source.get('name', 'unknown')}: {result.stderr.strip()}")
515
-
516
- # All sources failed
517
- raise RuntimeError(
518
- f"Failed to install {package}=={version} from any source:\n"
519
- + "\n".join(errors)
520
- )
521
422
 
522
423
  def _substitute_template(self, template: str, vars_dict: dict) -> str:
523
424
  """Substitute template variables with environment values."""