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.
- comfy_env/__init__.py +116 -41
- comfy_env/cli.py +89 -317
- comfy_env/config/__init__.py +18 -6
- comfy_env/config/parser.py +22 -76
- comfy_env/config/types.py +37 -0
- comfy_env/detection/__init__.py +77 -0
- comfy_env/detection/cuda.py +61 -0
- comfy_env/detection/gpu.py +230 -0
- comfy_env/detection/platform.py +70 -0
- comfy_env/detection/runtime.py +103 -0
- comfy_env/environment/__init__.py +53 -0
- comfy_env/environment/cache.py +141 -0
- comfy_env/environment/libomp.py +41 -0
- comfy_env/environment/paths.py +38 -0
- comfy_env/environment/setup.py +88 -0
- comfy_env/install.py +127 -329
- comfy_env/isolation/__init__.py +32 -2
- comfy_env/isolation/tensor_utils.py +83 -0
- comfy_env/isolation/workers/__init__.py +16 -0
- comfy_env/{workers → isolation/workers}/mp.py +1 -1
- comfy_env/{workers → isolation/workers}/subprocess.py +1 -1
- comfy_env/isolation/wrap.py +128 -509
- comfy_env/packages/__init__.py +60 -0
- comfy_env/packages/apt.py +36 -0
- comfy_env/packages/cuda_wheels.py +97 -0
- comfy_env/packages/node_dependencies.py +77 -0
- comfy_env/packages/pixi.py +85 -0
- comfy_env/packages/toml_generator.py +88 -0
- comfy_env-0.1.16.dist-info/METADATA +279 -0
- comfy_env-0.1.16.dist-info/RECORD +36 -0
- comfy_env/cache.py +0 -203
- comfy_env/nodes.py +0 -187
- comfy_env/pixi/__init__.py +0 -48
- comfy_env/pixi/core.py +0 -587
- comfy_env/pixi/cuda_detection.py +0 -303
- comfy_env/pixi/platform/__init__.py +0 -21
- comfy_env/pixi/platform/base.py +0 -96
- comfy_env/pixi/platform/darwin.py +0 -53
- comfy_env/pixi/platform/linux.py +0 -68
- comfy_env/pixi/platform/windows.py +0 -284
- comfy_env/pixi/resolver.py +0 -198
- comfy_env/prestartup.py +0 -208
- comfy_env/workers/__init__.py +0 -38
- comfy_env/workers/tensor_utils.py +0 -188
- comfy_env-0.1.15.dist-info/METADATA +0 -291
- comfy_env-0.1.15.dist-info/RECORD +0 -31
- /comfy_env/{workers → isolation/workers}/base.py +0 -0
- {comfy_env-0.1.15.dist-info → comfy_env-0.1.16.dist-info}/WHEEL +0 -0
- {comfy_env-0.1.15.dist-info → comfy_env-0.1.16.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.1.15.dist-info → comfy_env-0.1.16.dist-info}/licenses/LICENSE +0 -0
comfy_env/pixi/__init__.py
DELETED
|
@@ -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
|