comfy-env 0.1.14__py3-none-any.whl → 0.1.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. comfy_env/__init__.py +115 -62
  2. comfy_env/cli.py +89 -319
  3. comfy_env/config/__init__.py +18 -8
  4. comfy_env/config/parser.py +21 -122
  5. comfy_env/config/types.py +37 -70
  6. comfy_env/detection/__init__.py +77 -0
  7. comfy_env/detection/cuda.py +61 -0
  8. comfy_env/detection/gpu.py +230 -0
  9. comfy_env/detection/platform.py +70 -0
  10. comfy_env/detection/runtime.py +103 -0
  11. comfy_env/environment/__init__.py +53 -0
  12. comfy_env/environment/cache.py +141 -0
  13. comfy_env/environment/libomp.py +41 -0
  14. comfy_env/environment/paths.py +38 -0
  15. comfy_env/environment/setup.py +88 -0
  16. comfy_env/install.py +163 -249
  17. comfy_env/isolation/__init__.py +33 -2
  18. comfy_env/isolation/tensor_utils.py +83 -0
  19. comfy_env/isolation/workers/__init__.py +16 -0
  20. comfy_env/{workers → isolation/workers}/mp.py +1 -1
  21. comfy_env/{workers → isolation/workers}/subprocess.py +2 -2
  22. comfy_env/isolation/wrap.py +149 -409
  23. comfy_env/packages/__init__.py +60 -0
  24. comfy_env/packages/apt.py +36 -0
  25. comfy_env/packages/cuda_wheels.py +97 -0
  26. comfy_env/packages/node_dependencies.py +77 -0
  27. comfy_env/packages/pixi.py +85 -0
  28. comfy_env/packages/toml_generator.py +88 -0
  29. comfy_env-0.1.16.dist-info/METADATA +279 -0
  30. comfy_env-0.1.16.dist-info/RECORD +36 -0
  31. comfy_env/cache.py +0 -331
  32. comfy_env/errors.py +0 -293
  33. comfy_env/nodes.py +0 -187
  34. comfy_env/pixi/__init__.py +0 -48
  35. comfy_env/pixi/core.py +0 -588
  36. comfy_env/pixi/cuda_detection.py +0 -303
  37. comfy_env/pixi/platform/__init__.py +0 -21
  38. comfy_env/pixi/platform/base.py +0 -96
  39. comfy_env/pixi/platform/darwin.py +0 -53
  40. comfy_env/pixi/platform/linux.py +0 -68
  41. comfy_env/pixi/platform/windows.py +0 -284
  42. comfy_env/pixi/resolver.py +0 -198
  43. comfy_env/prestartup.py +0 -192
  44. comfy_env/workers/__init__.py +0 -38
  45. comfy_env/workers/tensor_utils.py +0 -188
  46. comfy_env-0.1.14.dist-info/METADATA +0 -291
  47. comfy_env-0.1.14.dist-info/RECORD +0 -33
  48. /comfy_env/{workers → isolation/workers}/base.py +0 -0
  49. {comfy_env-0.1.14.dist-info → comfy_env-0.1.16.dist-info}/WHEEL +0 -0
  50. {comfy_env-0.1.14.dist-info → comfy_env-0.1.16.dist-info}/entry_points.txt +0 -0
  51. {comfy_env-0.1.14.dist-info → comfy_env-0.1.16.dist-info}/licenses/LICENSE +0 -0
comfy_env/pixi/core.py DELETED
@@ -1,588 +0,0 @@
1
- """
2
- Pixi integration for comfy-env.
3
-
4
- Pixi is a fast package manager that supports both conda and pip packages.
5
- All dependencies go through pixi for unified management.
6
-
7
- See: https://pixi.sh/
8
- """
9
-
10
- import copy
11
- import platform
12
- import re
13
- import shutil
14
- import stat
15
- import subprocess
16
- import sys
17
- import urllib.request
18
- from pathlib import Path
19
- from typing import Any, Callable, Dict, List, Optional
20
-
21
- from ..config.types import ComfyEnvConfig
22
-
23
-
24
- # Pixi download URLs by platform
25
- PIXI_URLS = {
26
- ("Linux", "x86_64"): "https://github.com/prefix-dev/pixi/releases/latest/download/pixi-x86_64-unknown-linux-musl",
27
- ("Linux", "aarch64"): "https://github.com/prefix-dev/pixi/releases/latest/download/pixi-aarch64-unknown-linux-musl",
28
- ("Darwin", "x86_64"): "https://github.com/prefix-dev/pixi/releases/latest/download/pixi-x86_64-apple-darwin",
29
- ("Darwin", "arm64"): "https://github.com/prefix-dev/pixi/releases/latest/download/pixi-aarch64-apple-darwin",
30
- ("Windows", "AMD64"): "https://github.com/prefix-dev/pixi/releases/latest/download/pixi-x86_64-pc-windows-msvc.exe",
31
- }
32
-
33
- # CUDA wheels index (includes flash-attn, PyG packages, and custom wheels)
34
- CUDA_WHEELS_INDEX = "https://pozzettiandrea.github.io/cuda-wheels/"
35
-
36
- # CUDA version -> PyTorch version mapping
37
- CUDA_TORCH_MAP = {
38
- "12.8": "2.8",
39
- "12.4": "2.4",
40
- "12.1": "2.4",
41
- }
42
-
43
- def find_wheel_url(
44
- package: str,
45
- torch_version: str,
46
- cuda_version: str,
47
- python_version: str,
48
- ) -> Optional[str]:
49
- """
50
- Query cuda-wheels index and return the direct URL for the matching wheel.
51
-
52
- This bypasses pip's version validation by providing a direct URL,
53
- which is necessary for wheels where the filename has a local version
54
- but the internal METADATA doesn't (e.g., flash-attn from mjun0812).
55
-
56
- Args:
57
- package: Package name (e.g., "flash-attn")
58
- torch_version: PyTorch version (e.g., "2.8")
59
- cuda_version: CUDA version (e.g., "12.8")
60
- python_version: Python version (e.g., "3.10")
61
-
62
- Returns:
63
- Direct URL to the wheel file, or None if no match found.
64
- """
65
- cuda_short = cuda_version.replace(".", "")[:3] # "12.8" -> "128"
66
- torch_short = torch_version.replace(".", "")[:2] # "2.8" -> "28"
67
- py_tag = f"cp{python_version.replace('.', '')}" # "3.10" -> "cp310"
68
-
69
- # Platform tag for current system
70
- if sys.platform == "linux":
71
- platform_tag = "linux_x86_64"
72
- elif sys.platform == "win32":
73
- platform_tag = "win_amd64"
74
- else:
75
- platform_tag = None # macOS doesn't typically have CUDA wheels
76
-
77
- # Local version patterns to match:
78
- # cuda-wheels style: +cu128torch28
79
- # PyG style: +pt28cu128
80
- local_patterns = [
81
- f"+cu{cuda_short}torch{torch_short}", # cuda-wheels style
82
- f"+pt{torch_short}cu{cuda_short}", # PyG style
83
- ]
84
-
85
- pkg_variants = [package, package.replace("-", "_"), package.replace("_", "-")]
86
-
87
- for pkg_dir in pkg_variants:
88
- index_url = f"{CUDA_WHEELS_INDEX}{pkg_dir}/"
89
- try:
90
- with urllib.request.urlopen(index_url, timeout=10) as resp:
91
- html = resp.read().decode("utf-8")
92
- except Exception:
93
- continue
94
-
95
- # Parse href and display name from HTML: <a href="URL">DISPLAY_NAME</a>
96
- link_pattern = re.compile(r'href="([^"]+\.whl)"[^>]*>([^<]+)</a>', re.IGNORECASE)
97
-
98
- for match in link_pattern.finditer(html):
99
- wheel_url = match.group(1)
100
- display_name = match.group(2)
101
-
102
- # Match on display name (has normalized torch28 format)
103
- matches_cuda_torch = any(p in display_name for p in local_patterns)
104
- matches_python = py_tag in display_name
105
- matches_platform = platform_tag is None or platform_tag in display_name
106
-
107
- if matches_cuda_torch and matches_python and matches_platform:
108
- # Return absolute URL
109
- if wheel_url.startswith("http"):
110
- return wheel_url
111
- # Relative URL - construct absolute
112
- return f"{CUDA_WHEELS_INDEX}{pkg_dir}/{wheel_url}"
113
-
114
- return None
115
-
116
-
117
- def find_matching_wheel(package: str, torch_version: str, cuda_version: str) -> Optional[str]:
118
- """
119
- Query cuda-wheels index to find a wheel matching the CUDA/torch version.
120
- Returns the full version spec (e.g., "flash-attn===2.8.3+cu128torch2.8") or None.
121
-
122
- Note: This is used as a fallback for packages with correct wheel metadata.
123
- For packages with mismatched metadata (like flash-attn), use find_wheel_url() instead.
124
- """
125
- cuda_short = cuda_version.replace(".", "")[:3] # "12.8" -> "128"
126
- torch_short = torch_version.replace(".", "")[:2] # "2.8" -> "28"
127
-
128
- # Try different directory name variants
129
- pkg_variants = [package, package.replace("-", "_"), package.replace("_", "-")]
130
-
131
- for pkg_dir in pkg_variants:
132
- url = f"{CUDA_WHEELS_INDEX}{pkg_dir}/"
133
- try:
134
- with urllib.request.urlopen(url, timeout=10) as resp:
135
- html = resp.read().decode("utf-8")
136
- except Exception:
137
- continue
138
-
139
- # Parse wheel filenames from href attributes
140
- # Pattern: package_name-version+localversion-cpXX-cpXX-platform.whl
141
- wheel_pattern = re.compile(
142
- r'href="[^"]*?([^"/]+\.whl)"',
143
- re.IGNORECASE
144
- )
145
-
146
- # Local version patterns to match:
147
- # cuda-wheels style: +cu128torch28
148
- # PyG style: +pt28cu128
149
- local_patterns = [
150
- f"+cu{cuda_short}torch{torch_short}", # cuda-wheels style
151
- f"+pt{torch_short}cu{cuda_short}", # PyG style
152
- ]
153
-
154
- best_match = None
155
- best_version = None
156
-
157
- for match in wheel_pattern.finditer(html):
158
- wheel_name = match.group(1)
159
- # URL decode
160
- wheel_name = wheel_name.replace("%2B", "+")
161
-
162
- # Check if wheel matches our CUDA/torch version
163
- for local_pattern in local_patterns:
164
- if local_pattern in wheel_name:
165
- # Extract version from wheel name
166
- # Format: name-version+local-cpXX-cpXX-platform.whl
167
- parts = wheel_name.split("-")
168
- if len(parts) >= 2:
169
- version_part = parts[1] # e.g., "2.8.3+cu128torch2.8"
170
- if best_version is None or version_part > best_version:
171
- best_version = version_part
172
- best_match = f"{package}==={version_part}"
173
- break
174
-
175
- if best_match:
176
- return best_match
177
-
178
- return None
179
-
180
-
181
- def get_package_spec(package: str, torch_version: str, cuda_version: str) -> str:
182
- """
183
- Get package spec with local version for CUDA/torch compatibility.
184
- Queries the index to find matching wheels dynamically.
185
- """
186
- spec = find_matching_wheel(package, torch_version, cuda_version)
187
- return spec if spec else package
188
-
189
-
190
- def get_all_find_links(package: str, torch_version: str, cuda_version: str) -> list:
191
- """Get all find-links URLs for a CUDA package."""
192
- # Try both underscore and hyphen variants since directory naming is inconsistent
193
- pkg_underscore = package.replace("-", "_")
194
- pkg_hyphen = package.replace("_", "-")
195
- urls = [f"{CUDA_WHEELS_INDEX}{package}/"]
196
- if pkg_underscore != package:
197
- urls.append(f"{CUDA_WHEELS_INDEX}{pkg_underscore}/")
198
- if pkg_hyphen != package:
199
- urls.append(f"{CUDA_WHEELS_INDEX}{pkg_hyphen}/")
200
- return urls
201
-
202
-
203
- def get_current_platform() -> str:
204
- """Get the current platform string for pixi."""
205
- if sys.platform == "linux":
206
- return "linux-64"
207
- elif sys.platform == "darwin":
208
- return "osx-arm64" if platform.machine() == "arm64" else "osx-64"
209
- elif sys.platform == "win32":
210
- return "win-64"
211
- return "linux-64"
212
-
213
-
214
- def get_pixi_path() -> Optional[Path]:
215
- """Find the pixi executable."""
216
- pixi_cmd = shutil.which("pixi")
217
- if pixi_cmd:
218
- return Path(pixi_cmd)
219
-
220
- home = Path.home()
221
- candidates = [
222
- home / ".pixi" / "bin" / "pixi",
223
- home / ".local" / "bin" / "pixi",
224
- ]
225
-
226
- if sys.platform == "win32":
227
- candidates = [p.with_suffix(".exe") for p in candidates]
228
-
229
- for candidate in candidates:
230
- if candidate.exists():
231
- return candidate
232
-
233
- return None
234
-
235
-
236
- def ensure_pixi(
237
- install_dir: Optional[Path] = None,
238
- log: Callable[[str], None] = print,
239
- ) -> Path:
240
- """Ensure pixi is installed, downloading if necessary."""
241
- existing = get_pixi_path()
242
- if existing:
243
- return existing
244
-
245
- log("Pixi not found, downloading...")
246
-
247
- if install_dir is None:
248
- install_dir = Path.home() / ".local" / "bin"
249
- install_dir.mkdir(parents=True, exist_ok=True)
250
-
251
- system = platform.system()
252
- machine = platform.machine()
253
-
254
- if machine in ("x86_64", "AMD64"):
255
- machine = "x86_64" if system != "Windows" else "AMD64"
256
- elif machine in ("arm64", "aarch64"):
257
- machine = "arm64" if system == "Darwin" else "aarch64"
258
-
259
- url_key = (system, machine)
260
- if url_key not in PIXI_URLS:
261
- raise RuntimeError(f"No pixi download for {system}/{machine}")
262
-
263
- url = PIXI_URLS[url_key]
264
- pixi_path = install_dir / ("pixi.exe" if system == "Windows" else "pixi")
265
-
266
- try:
267
- import urllib.request
268
- urllib.request.urlretrieve(url, pixi_path)
269
- except Exception as e:
270
- result = subprocess.run(
271
- ["curl", "-fsSL", "-o", str(pixi_path), url],
272
- capture_output=True, text=True,
273
- )
274
- if result.returncode != 0:
275
- raise RuntimeError(f"Failed to download pixi: {result.stderr}") from e
276
-
277
- if system != "Windows":
278
- pixi_path.chmod(pixi_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
279
-
280
- log(f"Installed pixi to: {pixi_path}")
281
- return pixi_path
282
-
283
-
284
- def get_env_name(dir_name: str) -> str:
285
- """Convert directory name to env name: ComfyUI-UniRig -> _env_unirig"""
286
- name = dir_name.lower().replace("-", "_").lstrip("comfyui_")
287
- return f"_env_{name}"
288
-
289
-
290
- def clean_pixi_artifacts(node_dir: Path, log: Callable[[str], None] = print) -> None:
291
- """Remove previous pixi installation artifacts."""
292
- for path in [node_dir / "pixi.toml", node_dir / "pixi.lock"]:
293
- if path.exists():
294
- path.unlink()
295
- pixi_dir = node_dir / ".pixi"
296
- if pixi_dir.exists():
297
- shutil.rmtree(pixi_dir)
298
- # Also clean old _env_* directories
299
- env_name = get_env_name(node_dir.name)
300
- env_dir = node_dir / env_name
301
- if env_dir.exists():
302
- shutil.rmtree(env_dir)
303
-
304
-
305
- def get_pixi_python(node_dir: Path) -> Optional[Path]:
306
- """Get path to Python in the pixi environment."""
307
- # Check new _env_<name> location first
308
- env_name = get_env_name(node_dir.name)
309
- env_dir = node_dir / env_name
310
- if not env_dir.exists():
311
- # Fallback to old .pixi path
312
- env_dir = node_dir / ".pixi" / "envs" / "default"
313
- if sys.platform == "win32":
314
- python_path = env_dir / "python.exe"
315
- else:
316
- python_path = env_dir / "bin" / "python"
317
- return python_path if python_path.exists() else None
318
-
319
-
320
- def pixi_run(
321
- command: List[str],
322
- node_dir: Path,
323
- log: Callable[[str], None] = print,
324
- ) -> subprocess.CompletedProcess:
325
- """Run a command in the pixi environment."""
326
- pixi_path = get_pixi_path()
327
- if not pixi_path:
328
- raise RuntimeError("Pixi not found")
329
- return subprocess.run(
330
- [str(pixi_path), "run"] + command,
331
- cwd=node_dir,
332
- capture_output=True,
333
- text=True,
334
- )
335
-
336
-
337
- def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
338
- """Deep merge two dicts, override wins for conflicts."""
339
- result = copy.deepcopy(base)
340
- for key, value in override.items():
341
- if key in result and isinstance(result[key], dict) and isinstance(value, dict):
342
- result[key] = _deep_merge(result[key], value)
343
- else:
344
- result[key] = copy.deepcopy(value)
345
- return result
346
-
347
-
348
- def pixi_install(
349
- cfg: ComfyEnvConfig,
350
- node_dir: Path,
351
- log: Callable[[str], None] = print,
352
- ) -> bool:
353
- """
354
- Install all packages via pixi.
355
-
356
- comfy-env.toml is a superset of pixi.toml. This function:
357
- 1. Starts with passthrough sections from comfy-env.toml
358
- 2. Adds workspace metadata (name, version, channels, platforms)
359
- 3. Adds system-requirements if needed (CUDA detection)
360
- 4. Adds CUDA find-links and PyTorch if [cuda] packages present
361
- 5. Writes combined data as pixi.toml
362
-
363
- Args:
364
- cfg: ComfyEnvConfig with packages to install.
365
- node_dir: Directory to install in.
366
- log: Logging callback.
367
-
368
- Returns:
369
- True if installation succeeded.
370
- """
371
- try:
372
- import tomli_w
373
- except ImportError:
374
- raise ImportError(
375
- "tomli-w required for writing TOML. Install with: pip install tomli-w"
376
- )
377
-
378
- from .cuda_detection import get_recommended_cuda_version
379
-
380
- # Start with passthrough data from comfy-env.toml
381
- pixi_data = copy.deepcopy(cfg.pixi_passthrough)
382
-
383
- # Detect CUDA version if CUDA packages requested
384
- cuda_version = None
385
- torch_version = None
386
- if cfg.has_cuda and sys.platform != "darwin":
387
- cuda_version = get_recommended_cuda_version()
388
- if cuda_version:
389
- cuda_mm = ".".join(cuda_version.split(".")[:2])
390
- torch_version = CUDA_TORCH_MAP.get(cuda_mm, "2.8")
391
- log(f"Detected CUDA {cuda_version} -> PyTorch {torch_version}")
392
- else:
393
- log("Warning: CUDA packages requested but no GPU detected")
394
-
395
- # Install system dependencies on Linux via apt
396
- if sys.platform == "linux" and cfg.apt_packages:
397
- log(f"Installing apt packages: {cfg.apt_packages}")
398
- subprocess.run(["sudo", "apt-get", "update"], capture_output=True)
399
- subprocess.run(
400
- ["sudo", "apt-get", "install", "-y"] + cfg.apt_packages,
401
- capture_output=True,
402
- )
403
-
404
- # Clean previous artifacts
405
- clean_pixi_artifacts(node_dir, log)
406
-
407
- # Create .pixi/config.toml to ensure inline (non-detached) environments
408
- pixi_config_dir = node_dir / ".pixi"
409
- pixi_config_dir.mkdir(parents=True, exist_ok=True)
410
- pixi_config_file = pixi_config_dir / "config.toml"
411
- pixi_config_file.write_text("detached-environments = false\n")
412
-
413
- # Ensure pixi is installed
414
- pixi_path = ensure_pixi(log=log)
415
-
416
- # Build workspace section
417
- workspace = pixi_data.get("workspace", {})
418
- workspace.setdefault("name", node_dir.name)
419
- workspace.setdefault("version", "0.1.0")
420
- workspace.setdefault("channels", ["conda-forge"])
421
- workspace.setdefault("platforms", [get_current_platform()])
422
- pixi_data["workspace"] = workspace
423
-
424
- # Build system-requirements section
425
- system_reqs = pixi_data.get("system-requirements", {})
426
- if sys.platform == "linux":
427
- system_reqs.setdefault("libc", {"family": "glibc", "version": "2.35"})
428
- if cuda_version:
429
- cuda_major = cuda_version.split(".")[0]
430
- system_reqs["cuda"] = cuda_major
431
- if system_reqs:
432
- pixi_data["system-requirements"] = system_reqs
433
-
434
- # Build dependencies section (conda packages + python + pip)
435
- dependencies = pixi_data.get("dependencies", {})
436
- if cfg.python:
437
- py_version = cfg.python
438
- log(f"Using specified Python {py_version}")
439
- else:
440
- py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
441
- dependencies.setdefault("python", f"{py_version}.*")
442
- dependencies.setdefault("pip", "*") # Always include pip
443
- pixi_data["dependencies"] = dependencies
444
-
445
- # Add pypi-options for PyTorch index (CUDA packages installed separately via pip)
446
- if cfg.has_cuda and cuda_version:
447
- pypi_options = pixi_data.get("pypi-options", {})
448
- # Add PyTorch CUDA index for torch installation
449
- cuda_short = cuda_version.replace(".", "")[:3]
450
- pytorch_index = f"https://download.pytorch.org/whl/cu{cuda_short}"
451
- extra_urls = pypi_options.get("extra-index-urls", [])
452
- if pytorch_index not in extra_urls:
453
- extra_urls.append(pytorch_index)
454
- pypi_options["extra-index-urls"] = extra_urls
455
- pixi_data["pypi-options"] = pypi_options
456
-
457
- # Build pypi-dependencies section (CUDA packages excluded - installed separately)
458
- pypi_deps = pixi_data.get("pypi-dependencies", {})
459
-
460
- # Enforce torch version if we have CUDA packages (must match cuda_packages wheels)
461
- if cfg.has_cuda and torch_version:
462
- torch_major = torch_version.split(".")[0]
463
- torch_minor = int(torch_version.split(".")[1])
464
- required_torch = f">={torch_version},<{torch_major}.{torch_minor + 1}"
465
- if "torch" in pypi_deps and pypi_deps["torch"] != required_torch:
466
- log(f"Overriding torch={pypi_deps['torch']} with {required_torch} (required for cuda_packages)")
467
- pypi_deps["torch"] = required_torch
468
-
469
- # NOTE: CUDA packages are NOT added here - they're installed with --no-deps after pixi
470
-
471
- if pypi_deps:
472
- pixi_data["pypi-dependencies"] = pypi_deps
473
-
474
- # Write pixi.toml
475
- pixi_toml = node_dir / "pixi.toml"
476
- with open(pixi_toml, "wb") as f:
477
- tomli_w.dump(pixi_data, f)
478
- log(f"Generated {pixi_toml}")
479
-
480
- # Run pixi install
481
- log("Running pixi install...")
482
- result = subprocess.run(
483
- [str(pixi_path), "install"],
484
- cwd=node_dir,
485
- capture_output=True,
486
- text=True,
487
- )
488
-
489
- if result.returncode != 0:
490
- log(f"pixi install failed:\n{result.stderr}")
491
- raise RuntimeError(f"pixi install failed: {result.stderr}")
492
-
493
- # Install CUDA packages via direct URL or find-links fallback
494
- if cfg.cuda_packages and cuda_version:
495
- log(f"Installing CUDA packages: {cfg.cuda_packages}")
496
- python_path = get_pixi_python(node_dir)
497
- if not python_path:
498
- raise RuntimeError("Could not find Python in pixi environment")
499
-
500
- # Get Python version from the pixi environment (not host Python)
501
- result = subprocess.run(
502
- [str(python_path), "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
503
- capture_output=True, text=True
504
- )
505
- if result.returncode == 0:
506
- py_version = result.stdout.strip()
507
- else:
508
- py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
509
- log(f"Warning: Could not detect pixi Python version, using host: {py_version}")
510
-
511
- for package in cfg.cuda_packages:
512
- # Find direct wheel URL (bypasses metadata validation)
513
- wheel_url = find_wheel_url(package, torch_version, cuda_version, py_version)
514
-
515
- if not wheel_url:
516
- raise RuntimeError(
517
- f"No wheel found for {package} with CUDA {cuda_version}, "
518
- f"torch {torch_version}, Python {py_version}. "
519
- f"Check cuda-wheels index."
520
- )
521
-
522
- log(f" Installing {package} from {wheel_url}")
523
- pip_cmd = [
524
- str(python_path), "-m", "pip", "install",
525
- "--no-deps",
526
- "--no-cache-dir",
527
- wheel_url,
528
- ]
529
-
530
- result = subprocess.run(pip_cmd, capture_output=True, text=True)
531
- if result.returncode != 0:
532
- log(f"CUDA package install failed for {package}:\n{result.stderr}")
533
- raise RuntimeError(f"CUDA package install failed: {result.stderr}")
534
-
535
- log("CUDA packages installed")
536
-
537
- # Move environment from .pixi/envs/default to central cache
538
- from ..cache import (
539
- get_central_env_path, write_marker, write_env_metadata,
540
- MARKER_FILE, get_cache_dir
541
- )
542
-
543
- old_env = node_dir / ".pixi" / "envs" / "default"
544
- config_path = node_dir / "comfy-env.toml"
545
-
546
- # Determine the main node directory (for naming)
547
- # If node_dir is custom_nodes/NodeName/subdir, main_node_dir is custom_nodes/NodeName
548
- # If node_dir is custom_nodes/NodeName, main_node_dir is custom_nodes/NodeName
549
- if node_dir.parent.name == "custom_nodes":
550
- main_node_dir = node_dir
551
- else:
552
- # Walk up to find custom_nodes parent
553
- main_node_dir = node_dir
554
- for parent in node_dir.parents:
555
- if parent.parent.name == "custom_nodes":
556
- main_node_dir = parent
557
- break
558
-
559
- # Get central env path
560
- central_env = get_central_env_path(main_node_dir, config_path)
561
-
562
- if old_env.exists():
563
- # Ensure cache directory exists
564
- get_cache_dir()
565
-
566
- # Remove old central env if exists
567
- if central_env.exists():
568
- shutil.rmtree(central_env)
569
-
570
- # Move to central cache
571
- shutil.move(str(old_env), str(central_env))
572
-
573
- # Write marker file in node directory
574
- write_marker(config_path, central_env)
575
-
576
- # Write metadata in env for orphan detection
577
- marker_path = config_path.parent / MARKER_FILE
578
- write_env_metadata(central_env, marker_path)
579
-
580
- # Clean up .pixi directory
581
- pixi_dir = node_dir / ".pixi"
582
- if pixi_dir.exists():
583
- shutil.rmtree(pixi_dir)
584
-
585
- log(f"Environment created at: {central_env}")
586
-
587
- log("Installation complete!")
588
- return True