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