comfy-env 0.0.65__py3-none-any.whl → 0.0.66__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 +70 -122
- comfy_env/cli.py +78 -7
- comfy_env/config/__init__.py +19 -0
- comfy_env/config/parser.py +151 -0
- comfy_env/config/types.py +64 -0
- comfy_env/install.py +83 -361
- comfy_env/isolation/__init__.py +9 -0
- comfy_env/isolation/wrap.py +351 -0
- comfy_env/nodes.py +2 -2
- comfy_env/pixi/__init__.py +48 -0
- comfy_env/pixi/core.py +356 -0
- comfy_env/{resolver.py → pixi/resolver.py} +1 -14
- comfy_env/prestartup.py +60 -0
- comfy_env/templates/comfy-env-instructions.txt +30 -87
- comfy_env/templates/comfy-env.toml +69 -128
- comfy_env/workers/__init__.py +21 -32
- comfy_env/workers/base.py +1 -1
- comfy_env/workers/{torch_mp.py → mp.py} +47 -14
- comfy_env/workers/{venv.py → subprocess.py} +397 -443
- {comfy_env-0.0.65.dist-info → comfy_env-0.0.66.dist-info}/METADATA +2 -1
- comfy_env-0.0.66.dist-info/RECORD +34 -0
- comfy_env/decorator.py +0 -700
- comfy_env/env/__init__.py +0 -46
- comfy_env/env/config.py +0 -191
- comfy_env/env/config_file.py +0 -706
- comfy_env/env/manager.py +0 -636
- comfy_env/env/security.py +0 -267
- comfy_env/ipc/__init__.py +0 -55
- comfy_env/ipc/bridge.py +0 -476
- comfy_env/ipc/protocol.py +0 -265
- comfy_env/ipc/tensor.py +0 -371
- comfy_env/ipc/torch_bridge.py +0 -401
- comfy_env/ipc/transport.py +0 -318
- comfy_env/ipc/worker.py +0 -221
- comfy_env/isolation.py +0 -310
- comfy_env/pixi.py +0 -760
- comfy_env/stub_imports.py +0 -270
- comfy_env/stubs/__init__.py +0 -1
- comfy_env/stubs/comfy/__init__.py +0 -6
- comfy_env/stubs/comfy/model_management.py +0 -58
- comfy_env/stubs/comfy/utils.py +0 -29
- comfy_env/stubs/folder_paths.py +0 -71
- comfy_env/workers/pool.py +0 -241
- comfy_env-0.0.65.dist-info/RECORD +0 -48
- /comfy_env/{env/cuda_gpu_detection.py → pixi/cuda_detection.py} +0 -0
- /comfy_env/{env → pixi}/platform/__init__.py +0 -0
- /comfy_env/{env → pixi}/platform/base.py +0 -0
- /comfy_env/{env → pixi}/platform/darwin.py +0 -0
- /comfy_env/{env → pixi}/platform/linux.py +0 -0
- /comfy_env/{env → pixi}/platform/windows.py +0 -0
- /comfy_env/{registry.py → pixi/registry.py} +0 -0
- /comfy_env/{wheel_sources.yml → pixi/wheel_sources.yml} +0 -0
- {comfy_env-0.0.65.dist-info → comfy_env-0.0.66.dist-info}/WHEEL +0 -0
- {comfy_env-0.0.65.dist-info → comfy_env-0.0.66.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.0.65.dist-info → comfy_env-0.0.66.dist-info}/licenses/LICENSE +0 -0
comfy_env/env/manager.py
DELETED
|
@@ -1,636 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
IsolatedEnvManager - Creates and manages isolated Python environments.
|
|
3
|
-
|
|
4
|
-
Uses uv for fast environment creation and package installation.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import os
|
|
8
|
-
import subprocess
|
|
9
|
-
import shutil
|
|
10
|
-
import sys
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Optional, Callable
|
|
13
|
-
|
|
14
|
-
from .config import IsolatedEnv
|
|
15
|
-
from .platform import get_platform, PlatformProvider
|
|
16
|
-
from .cuda_gpu_detection import detect_cuda_version
|
|
17
|
-
from .security import (
|
|
18
|
-
normalize_env_name,
|
|
19
|
-
validate_dependency,
|
|
20
|
-
validate_dependencies,
|
|
21
|
-
validate_path_within_root,
|
|
22
|
-
validate_wheel_url,
|
|
23
|
-
)
|
|
24
|
-
from ..registry import PACKAGE_REGISTRY, is_registered, get_cuda_short2
|
|
25
|
-
from ..resolver import RuntimeEnv, parse_wheel_requirement
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class IsolatedEnvManager:
|
|
29
|
-
"""
|
|
30
|
-
Manages isolated Python environments for ComfyUI nodes.
|
|
31
|
-
|
|
32
|
-
This class handles:
|
|
33
|
-
- Creating Python virtual environments using uv
|
|
34
|
-
- Installing PyTorch with correct CUDA version
|
|
35
|
-
- Installing packages from requirements or wheel sources
|
|
36
|
-
- Caching environments (same config = reuse existing)
|
|
37
|
-
- Platform-specific handling (Windows DLLs, etc.)
|
|
38
|
-
|
|
39
|
-
Example:
|
|
40
|
-
manager = IsolatedEnvManager(base_dir=Path("./"))
|
|
41
|
-
|
|
42
|
-
env = IsolatedEnv(
|
|
43
|
-
name="my-node",
|
|
44
|
-
python="3.10",
|
|
45
|
-
cuda="12.8",
|
|
46
|
-
requirements=["torch==2.8.0", "nvdiffrast"],
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
# Create environment and install dependencies
|
|
50
|
-
env_path = manager.setup(env)
|
|
51
|
-
|
|
52
|
-
# Get Python executable path
|
|
53
|
-
python_exe = manager.get_python(env)
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
def __init__(
|
|
57
|
-
self,
|
|
58
|
-
base_dir: Path,
|
|
59
|
-
log_callback: Optional[Callable[[str], None]] = None,
|
|
60
|
-
):
|
|
61
|
-
"""
|
|
62
|
-
Initialize environment manager.
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
base_dir: Directory where environments will be created
|
|
66
|
-
log_callback: Optional callback for logging (default: print)
|
|
67
|
-
"""
|
|
68
|
-
self.base_dir = Path(base_dir)
|
|
69
|
-
self.platform: PlatformProvider = get_platform()
|
|
70
|
-
self.log = log_callback or print
|
|
71
|
-
|
|
72
|
-
# Check platform compatibility
|
|
73
|
-
is_compatible, error = self.platform.check_prerequisites()
|
|
74
|
-
if not is_compatible:
|
|
75
|
-
raise RuntimeError(f"Platform incompatible: {error}")
|
|
76
|
-
|
|
77
|
-
def _find_uv(self) -> Optional[Path]:
|
|
78
|
-
"""Find uv executable in PATH or current Python environment."""
|
|
79
|
-
# First check system PATH
|
|
80
|
-
uv_path = shutil.which("uv")
|
|
81
|
-
if uv_path:
|
|
82
|
-
return Path(uv_path)
|
|
83
|
-
|
|
84
|
-
# Check current Python environment's Scripts/bin folder
|
|
85
|
-
import sys
|
|
86
|
-
if sys.platform == "win32":
|
|
87
|
-
candidates = [
|
|
88
|
-
Path(sys.prefix) / "Scripts" / "uv.exe",
|
|
89
|
-
Path(sys.base_prefix) / "Scripts" / "uv.exe",
|
|
90
|
-
]
|
|
91
|
-
else:
|
|
92
|
-
candidates = [
|
|
93
|
-
Path(sys.prefix) / "bin" / "uv",
|
|
94
|
-
Path(sys.base_prefix) / "bin" / "uv",
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
for candidate in candidates:
|
|
98
|
-
if candidate.exists():
|
|
99
|
-
return candidate
|
|
100
|
-
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
def _run_uv(self, args: list, env_dir: Optional[Path] = None, **kwargs) -> subprocess.CompletedProcess:
|
|
104
|
-
"""Run uv command."""
|
|
105
|
-
uv = self._find_uv()
|
|
106
|
-
if not uv:
|
|
107
|
-
raise RuntimeError(
|
|
108
|
-
"uv not found. Please install it:\n"
|
|
109
|
-
" curl -LsSf https://astral.sh/uv/install.sh | sh\n"
|
|
110
|
-
" # or on Windows: powershell -c \"irm https://astral.sh/uv/install.ps1 | iex\""
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
cmd = [str(uv)] + args
|
|
114
|
-
return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
|
|
115
|
-
|
|
116
|
-
def get_env_dir(self, env: IsolatedEnv) -> Path:
|
|
117
|
-
"""Get the environment directory path for a config."""
|
|
118
|
-
# Validate environment name to prevent directory traversal
|
|
119
|
-
safe_name = normalize_env_name(env.name)
|
|
120
|
-
env_dir = env.get_default_env_dir(self.base_dir)
|
|
121
|
-
|
|
122
|
-
# Ensure the resulting path is within base_dir
|
|
123
|
-
validate_path_within_root(env_dir, self.base_dir)
|
|
124
|
-
|
|
125
|
-
return env_dir
|
|
126
|
-
|
|
127
|
-
def get_python(self, env: IsolatedEnv) -> Path:
|
|
128
|
-
"""Get the Python executable path for an environment."""
|
|
129
|
-
env_dir = self.get_env_dir(env)
|
|
130
|
-
return self.platform.get_env_paths(env_dir, env.python).python
|
|
131
|
-
|
|
132
|
-
def get_pip(self, env: IsolatedEnv) -> Path:
|
|
133
|
-
"""Get the pip executable path for an environment."""
|
|
134
|
-
env_dir = self.get_env_dir(env)
|
|
135
|
-
return self.platform.get_env_paths(env_dir, env.python).pip
|
|
136
|
-
|
|
137
|
-
def exists(self, env: IsolatedEnv) -> bool:
|
|
138
|
-
"""Check if environment already exists."""
|
|
139
|
-
python_exe = self.get_python(env)
|
|
140
|
-
return python_exe.exists()
|
|
141
|
-
|
|
142
|
-
def is_ready(self, env: IsolatedEnv, verify_packages: Optional[list] = None) -> bool:
|
|
143
|
-
"""
|
|
144
|
-
Check if environment is ready to use.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
env: Environment configuration
|
|
148
|
-
verify_packages: Optional list of packages to verify (e.g., ["torch", "numpy"])
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
True if environment exists and packages are importable
|
|
152
|
-
"""
|
|
153
|
-
python_exe = self.get_python(env)
|
|
154
|
-
if not python_exe.exists():
|
|
155
|
-
return False
|
|
156
|
-
|
|
157
|
-
if verify_packages:
|
|
158
|
-
imports = ", ".join(verify_packages)
|
|
159
|
-
try:
|
|
160
|
-
result = subprocess.run(
|
|
161
|
-
[str(python_exe), "-c", f"import {imports}"],
|
|
162
|
-
capture_output=True,
|
|
163
|
-
text=True,
|
|
164
|
-
timeout=30
|
|
165
|
-
)
|
|
166
|
-
return result.returncode == 0
|
|
167
|
-
except (subprocess.SubprocessError, OSError):
|
|
168
|
-
return False
|
|
169
|
-
|
|
170
|
-
return True
|
|
171
|
-
|
|
172
|
-
def create_venv(self, env: IsolatedEnv) -> Path:
|
|
173
|
-
"""
|
|
174
|
-
Create a virtual environment using uv.
|
|
175
|
-
|
|
176
|
-
Args:
|
|
177
|
-
env: Environment configuration
|
|
178
|
-
|
|
179
|
-
Returns:
|
|
180
|
-
Path to the environment directory
|
|
181
|
-
"""
|
|
182
|
-
env_dir = self.get_env_dir(env)
|
|
183
|
-
|
|
184
|
-
if env_dir.exists():
|
|
185
|
-
self.log(f"Environment already exists: {env_dir}")
|
|
186
|
-
return env_dir
|
|
187
|
-
|
|
188
|
-
self.log(f"Creating environment: {env_dir}")
|
|
189
|
-
self.log(f" Python: {env.python}")
|
|
190
|
-
|
|
191
|
-
result = self._run_uv([
|
|
192
|
-
"venv",
|
|
193
|
-
str(env_dir),
|
|
194
|
-
"--python", env.python,
|
|
195
|
-
])
|
|
196
|
-
|
|
197
|
-
if result.returncode != 0:
|
|
198
|
-
raise RuntimeError(f"Failed to create venv: {result.stderr}")
|
|
199
|
-
|
|
200
|
-
self.log(f"Environment created successfully")
|
|
201
|
-
return env_dir
|
|
202
|
-
|
|
203
|
-
def install_pytorch(self, env: IsolatedEnv) -> None:
|
|
204
|
-
"""
|
|
205
|
-
Install PyTorch with appropriate CUDA version.
|
|
206
|
-
|
|
207
|
-
Args:
|
|
208
|
-
env: Environment configuration
|
|
209
|
-
"""
|
|
210
|
-
cuda_version = env.cuda
|
|
211
|
-
if cuda_version is None:
|
|
212
|
-
# Auto-detect CUDA version
|
|
213
|
-
cuda_version = detect_cuda_version()
|
|
214
|
-
if cuda_version:
|
|
215
|
-
self.log(f"Auto-detected CUDA version: {cuda_version}")
|
|
216
|
-
else:
|
|
217
|
-
self.log("No GPU detected, installing CPU-only PyTorch")
|
|
218
|
-
|
|
219
|
-
python_exe = self.get_python(env)
|
|
220
|
-
|
|
221
|
-
# Determine PyTorch index URL
|
|
222
|
-
if cuda_version:
|
|
223
|
-
cuda_short = cuda_version.replace(".", "")
|
|
224
|
-
index_url = f"https://download.pytorch.org/whl/cu{cuda_short}"
|
|
225
|
-
self.log(f"Installing PyTorch with CUDA {cuda_version}")
|
|
226
|
-
else:
|
|
227
|
-
index_url = "https://download.pytorch.org/whl/cpu"
|
|
228
|
-
self.log("Installing PyTorch (CPU)")
|
|
229
|
-
|
|
230
|
-
# Build uv pip command
|
|
231
|
-
uv = self._find_uv()
|
|
232
|
-
pip_args = [
|
|
233
|
-
str(uv), "pip", "install",
|
|
234
|
-
"--python", str(python_exe),
|
|
235
|
-
"--index-url", index_url,
|
|
236
|
-
]
|
|
237
|
-
|
|
238
|
-
# Install torch + torchvision together from same index to ensure ABI compatibility
|
|
239
|
-
if env.pytorch_version:
|
|
240
|
-
pip_args.append(f"torch=={env.pytorch_version}")
|
|
241
|
-
else:
|
|
242
|
-
pip_args.append("torch")
|
|
243
|
-
|
|
244
|
-
# Always install torchvision from the same index - uv will resolve matching version
|
|
245
|
-
pip_args.append("torchvision")
|
|
246
|
-
|
|
247
|
-
result = subprocess.run(pip_args, capture_output=True, text=True)
|
|
248
|
-
|
|
249
|
-
if result.returncode != 0:
|
|
250
|
-
raise RuntimeError(f"Failed to install PyTorch: {result.stderr}")
|
|
251
|
-
|
|
252
|
-
self.log("PyTorch + torchvision installed successfully")
|
|
253
|
-
|
|
254
|
-
def install_requirements(
|
|
255
|
-
self,
|
|
256
|
-
env: IsolatedEnv,
|
|
257
|
-
extra_args: Optional[list] = None,
|
|
258
|
-
) -> None:
|
|
259
|
-
"""
|
|
260
|
-
Install requirements for an environment.
|
|
261
|
-
|
|
262
|
-
Args:
|
|
263
|
-
env: Environment configuration
|
|
264
|
-
extra_args: Extra pip arguments
|
|
265
|
-
"""
|
|
266
|
-
python_exe = self.get_python(env)
|
|
267
|
-
|
|
268
|
-
# Merge platform-specific requirements
|
|
269
|
-
if sys.platform == 'win32':
|
|
270
|
-
platform_reqs = env.windows_requirements
|
|
271
|
-
platform_name = 'Windows'
|
|
272
|
-
elif sys.platform == 'darwin':
|
|
273
|
-
platform_reqs = env.darwin_requirements
|
|
274
|
-
platform_name = 'macOS'
|
|
275
|
-
else:
|
|
276
|
-
platform_reqs = env.linux_requirements
|
|
277
|
-
platform_name = 'Linux'
|
|
278
|
-
|
|
279
|
-
all_requirements = list(env.requirements) + list(platform_reqs)
|
|
280
|
-
|
|
281
|
-
if platform_reqs:
|
|
282
|
-
self.log(f"Including {len(platform_reqs)} {platform_name}-specific packages")
|
|
283
|
-
|
|
284
|
-
if not all_requirements and not env.requirements_file:
|
|
285
|
-
self.log("No requirements to install")
|
|
286
|
-
return
|
|
287
|
-
|
|
288
|
-
# Validate requirements for security
|
|
289
|
-
if all_requirements:
|
|
290
|
-
validate_dependencies(all_requirements)
|
|
291
|
-
|
|
292
|
-
# Validate wheel sources
|
|
293
|
-
for wheel_source in env.wheel_sources:
|
|
294
|
-
validate_wheel_url(wheel_source)
|
|
295
|
-
|
|
296
|
-
# Validate index URLs
|
|
297
|
-
for index_url in env.index_urls:
|
|
298
|
-
validate_wheel_url(index_url)
|
|
299
|
-
|
|
300
|
-
uv = self._find_uv()
|
|
301
|
-
pip_args = [str(uv), "pip", "install", "--python", str(python_exe)]
|
|
302
|
-
|
|
303
|
-
# Add wheel sources as --find-links
|
|
304
|
-
for wheel_source in env.wheel_sources:
|
|
305
|
-
pip_args.extend(["--find-links", wheel_source])
|
|
306
|
-
|
|
307
|
-
# Add extra index URLs
|
|
308
|
-
for index_url in env.index_urls:
|
|
309
|
-
pip_args.extend(["--extra-index-url", index_url])
|
|
310
|
-
|
|
311
|
-
# Add extra args
|
|
312
|
-
if extra_args:
|
|
313
|
-
pip_args.extend(extra_args)
|
|
314
|
-
|
|
315
|
-
# Install from requirements file
|
|
316
|
-
if env.requirements_file and env.requirements_file.exists():
|
|
317
|
-
self.log(f"Installing from {env.requirements_file}")
|
|
318
|
-
result = subprocess.run(
|
|
319
|
-
pip_args + ["-r", str(env.requirements_file)],
|
|
320
|
-
capture_output=True,
|
|
321
|
-
text=True,
|
|
322
|
-
)
|
|
323
|
-
if result.returncode != 0:
|
|
324
|
-
raise RuntimeError(f"Failed to install requirements: {result.stderr}")
|
|
325
|
-
|
|
326
|
-
# Install no-deps requirements first (e.g., CUDA extensions with conflicting metadata)
|
|
327
|
-
# For packages in registry, resolve proper wheel URLs
|
|
328
|
-
if env.no_deps_requirements:
|
|
329
|
-
self.log(f"Installing {len(env.no_deps_requirements)} CUDA packages")
|
|
330
|
-
self._install_cuda_packages(env, pip_args)
|
|
331
|
-
|
|
332
|
-
# Install individual requirements (including platform-specific)
|
|
333
|
-
if all_requirements:
|
|
334
|
-
self.log(f"Installing {len(all_requirements)} packages")
|
|
335
|
-
result = subprocess.run(
|
|
336
|
-
pip_args + all_requirements,
|
|
337
|
-
capture_output=True,
|
|
338
|
-
text=True,
|
|
339
|
-
)
|
|
340
|
-
if result.returncode != 0:
|
|
341
|
-
raise RuntimeError(f"Failed to install packages: {result.stderr}")
|
|
342
|
-
|
|
343
|
-
self.log("Requirements installed successfully")
|
|
344
|
-
|
|
345
|
-
def _install_cuda_packages(self, env: IsolatedEnv, pip_args: list) -> None:
|
|
346
|
-
"""Install CUDA packages using registry-based wheel resolution."""
|
|
347
|
-
import platform as plat
|
|
348
|
-
|
|
349
|
-
# Build RuntimeEnv from IsolatedEnv config
|
|
350
|
-
os_name = plat.system().lower()
|
|
351
|
-
if os_name == 'darwin':
|
|
352
|
-
platform_tag = f"macosx_{plat.mac_ver()[0].replace('.', '_')}_{plat.machine()}"
|
|
353
|
-
elif os_name == 'windows':
|
|
354
|
-
platform_tag = f"win_{plat.machine().lower()}"
|
|
355
|
-
else:
|
|
356
|
-
platform_tag = f"linux_{plat.machine()}"
|
|
357
|
-
|
|
358
|
-
cuda_short = env.cuda.replace(".", "") if env.cuda else None
|
|
359
|
-
torch_short = env.pytorch_version.replace(".", "") if env.pytorch_version else None
|
|
360
|
-
torch_mm = "".join(env.pytorch_version.split(".")[:2]) if env.pytorch_version else None
|
|
361
|
-
|
|
362
|
-
runtime_env = RuntimeEnv(
|
|
363
|
-
os_name=os_name,
|
|
364
|
-
platform_tag=platform_tag,
|
|
365
|
-
python_version=env.python,
|
|
366
|
-
python_short=env.python.replace(".", ""),
|
|
367
|
-
cuda_version=env.cuda,
|
|
368
|
-
cuda_short=cuda_short,
|
|
369
|
-
torch_version=env.pytorch_version,
|
|
370
|
-
torch_short=torch_short,
|
|
371
|
-
torch_mm=torch_mm,
|
|
372
|
-
)
|
|
373
|
-
vars_dict = runtime_env.as_dict()
|
|
374
|
-
if env.cuda:
|
|
375
|
-
vars_dict["cuda_short2"] = get_cuda_short2(env.cuda)
|
|
376
|
-
vars_dict["py_tag"] = f"cp{env.python.replace('.', '')}"
|
|
377
|
-
|
|
378
|
-
for req in env.no_deps_requirements:
|
|
379
|
-
package, version = parse_wheel_requirement(req)
|
|
380
|
-
pkg_lower = package.lower()
|
|
381
|
-
|
|
382
|
-
if pkg_lower in PACKAGE_REGISTRY:
|
|
383
|
-
config = PACKAGE_REGISTRY[pkg_lower]
|
|
384
|
-
|
|
385
|
-
if "wheel_template" in config:
|
|
386
|
-
# Direct wheel URL from template
|
|
387
|
-
template = config["wheel_template"]
|
|
388
|
-
|
|
389
|
-
# Only require version if template uses {version}
|
|
390
|
-
effective_version = None
|
|
391
|
-
if "{version}" in template:
|
|
392
|
-
effective_version = version or config.get("default_version")
|
|
393
|
-
if not effective_version:
|
|
394
|
-
raise RuntimeError(f"Package {package} requires version (no default in registry)")
|
|
395
|
-
vars_dict["version"] = effective_version
|
|
396
|
-
|
|
397
|
-
wheel_url = self._substitute_template(template, vars_dict)
|
|
398
|
-
version_str = f"=={effective_version}" if effective_version else ""
|
|
399
|
-
self.log(f" Installing {package}{version_str}...")
|
|
400
|
-
self.log(f" URL: {wheel_url}")
|
|
401
|
-
result = subprocess.run(
|
|
402
|
-
pip_args + ["--no-deps", wheel_url],
|
|
403
|
-
capture_output=True, text=True,
|
|
404
|
-
)
|
|
405
|
-
if result.returncode != 0:
|
|
406
|
-
raise RuntimeError(f"Failed to install {package}: {result.stderr}")
|
|
407
|
-
|
|
408
|
-
elif "find_links" in config:
|
|
409
|
-
# find_links: pip resolves wheel from index URL
|
|
410
|
-
find_links_url = config["find_links"]
|
|
411
|
-
effective_version = version or config.get("default_version")
|
|
412
|
-
pkg_spec = f"{package}=={effective_version}" if effective_version else package
|
|
413
|
-
self.log(f" Installing {pkg_spec} from {find_links_url}...")
|
|
414
|
-
result = subprocess.run(
|
|
415
|
-
pip_args + ["--no-deps", "--find-links", find_links_url, pkg_spec],
|
|
416
|
-
capture_output=True, text=True,
|
|
417
|
-
)
|
|
418
|
-
if result.returncode != 0:
|
|
419
|
-
raise RuntimeError(f"Failed to install {package}: {result.stderr}")
|
|
420
|
-
|
|
421
|
-
elif "package_name" in config:
|
|
422
|
-
# PyPI variant (e.g., spconv-cu124)
|
|
423
|
-
pkg_name = self._substitute_template(config["package_name"], vars_dict)
|
|
424
|
-
pkg_spec = f"{pkg_name}=={version}" if version else pkg_name
|
|
425
|
-
self.log(f" Installing {package} as {pkg_spec}...")
|
|
426
|
-
result = subprocess.run(
|
|
427
|
-
pip_args + [pkg_spec],
|
|
428
|
-
capture_output=True, text=True,
|
|
429
|
-
)
|
|
430
|
-
if result.returncode != 0:
|
|
431
|
-
raise RuntimeError(f"Failed to install {package}: {result.stderr}")
|
|
432
|
-
|
|
433
|
-
else:
|
|
434
|
-
raise RuntimeError(f"Package {package} in registry but missing wheel_template, find_links, or package_name")
|
|
435
|
-
|
|
436
|
-
else:
|
|
437
|
-
# Not in registry - error
|
|
438
|
-
raise RuntimeError(
|
|
439
|
-
f"Package {package} not found in registry. "
|
|
440
|
-
f"Add wheel_template to [wheel_sources] in comfy-env.toml"
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
def _substitute_template(self, template: str, vars_dict: dict) -> str:
|
|
444
|
-
"""Substitute template variables with environment values."""
|
|
445
|
-
result = template
|
|
446
|
-
for key, value in vars_dict.items():
|
|
447
|
-
if value is not None:
|
|
448
|
-
result = result.replace(f"{{{key}}}", str(value))
|
|
449
|
-
return result
|
|
450
|
-
|
|
451
|
-
def _install_comfy_env(self, env: IsolatedEnv) -> None:
|
|
452
|
-
"""Install comfy-env package (needed for BaseWorker)."""
|
|
453
|
-
python_exe = self.get_python(env)
|
|
454
|
-
uv = self._find_uv()
|
|
455
|
-
|
|
456
|
-
self.log("Installing comfy-env (for worker support)...")
|
|
457
|
-
|
|
458
|
-
# Check for local wheel in COMFY_LOCAL_WHEELS directory (set by comfy-test --local)
|
|
459
|
-
install_target = "comfy-env @ git+https://github.com/PozzettiAndrea/comfy-env"
|
|
460
|
-
wheels_dir = os.environ.get("COMFY_LOCAL_WHEELS")
|
|
461
|
-
self.log(f" COMFY_LOCAL_WHEELS={wheels_dir}")
|
|
462
|
-
if wheels_dir:
|
|
463
|
-
wheels_path = Path(wheels_dir)
|
|
464
|
-
self.log(f" Wheels dir exists: {wheels_path.exists()}")
|
|
465
|
-
if wheels_path.exists():
|
|
466
|
-
local_wheels = list(wheels_path.glob("comfy_env-*.whl"))
|
|
467
|
-
self.log(f" Found wheels: {[w.name for w in local_wheels]}")
|
|
468
|
-
if local_wheels:
|
|
469
|
-
install_target = str(local_wheels[0])
|
|
470
|
-
self.log(f" Using local wheel: {local_wheels[0].name}")
|
|
471
|
-
|
|
472
|
-
result = subprocess.run(
|
|
473
|
-
[str(uv), "pip", "install", "--python", str(python_exe),
|
|
474
|
-
"--upgrade", "--no-cache", install_target],
|
|
475
|
-
capture_output=True,
|
|
476
|
-
text=True,
|
|
477
|
-
)
|
|
478
|
-
if result.returncode != 0:
|
|
479
|
-
# Non-fatal - might already be installed or network issues
|
|
480
|
-
self.log(f"Warning: Failed to install comfy-env: {result.stderr}")
|
|
481
|
-
else:
|
|
482
|
-
self.log("comfy-env installed")
|
|
483
|
-
|
|
484
|
-
def _setup_windows_deps(self, env: IsolatedEnv, env_dir: Path) -> None:
|
|
485
|
-
"""
|
|
486
|
-
Set up Windows-specific dependencies (VC++ runtime, DLL paths).
|
|
487
|
-
|
|
488
|
-
This:
|
|
489
|
-
1. Installs msvc-runtime package (provides VC++ DLLs)
|
|
490
|
-
2. Sets up the Lib/x64/vc17/bin directory structure for opencv
|
|
491
|
-
"""
|
|
492
|
-
python_exe = self.get_python(env)
|
|
493
|
-
uv = self._find_uv()
|
|
494
|
-
|
|
495
|
-
# Install msvc-runtime package to get VC++ DLLs
|
|
496
|
-
self.log("Installing VC++ runtime (msvc-runtime)...")
|
|
497
|
-
result = subprocess.run(
|
|
498
|
-
[str(uv), "pip", "install", "--python", str(python_exe), "msvc-runtime"],
|
|
499
|
-
capture_output=True,
|
|
500
|
-
text=True,
|
|
501
|
-
)
|
|
502
|
-
if result.returncode != 0:
|
|
503
|
-
self.log(f"Warning: Failed to install msvc-runtime: {result.stderr}")
|
|
504
|
-
else:
|
|
505
|
-
self.log("msvc-runtime installed")
|
|
506
|
-
|
|
507
|
-
# Set up opencv DLL paths (copies DLLs to Lib/x64/vc17/bin)
|
|
508
|
-
self.log("Setting up opencv DLL paths...")
|
|
509
|
-
success, msg = self.platform.setup_opencv_dll_paths(env_dir)
|
|
510
|
-
if success:
|
|
511
|
-
if msg:
|
|
512
|
-
self.log(f" {msg}")
|
|
513
|
-
else:
|
|
514
|
-
self.log(f"Warning: {msg}")
|
|
515
|
-
|
|
516
|
-
def ensure_system_deps(self) -> bool:
|
|
517
|
-
"""
|
|
518
|
-
Ensure system-level dependencies are installed (Windows only).
|
|
519
|
-
|
|
520
|
-
On Windows, this checks for Media Foundation and installs if missing.
|
|
521
|
-
Returns True if all deps are available, False otherwise.
|
|
522
|
-
"""
|
|
523
|
-
if self.platform.name != 'windows':
|
|
524
|
-
return True
|
|
525
|
-
|
|
526
|
-
# Check and install Media Foundation if needed
|
|
527
|
-
success, error = self.platform.ensure_media_foundation(self.log)
|
|
528
|
-
if not success:
|
|
529
|
-
self.log(f"WARNING: {error}")
|
|
530
|
-
self.log("Some packages (like opencv) may not work correctly.")
|
|
531
|
-
return False
|
|
532
|
-
|
|
533
|
-
return True
|
|
534
|
-
|
|
535
|
-
def setup(
|
|
536
|
-
self,
|
|
537
|
-
env: IsolatedEnv,
|
|
538
|
-
install_pytorch: bool = True,
|
|
539
|
-
verify_packages: Optional[list] = None,
|
|
540
|
-
) -> Path:
|
|
541
|
-
"""
|
|
542
|
-
Create environment and install all dependencies.
|
|
543
|
-
|
|
544
|
-
This is the main entry point for setting up an isolated environment.
|
|
545
|
-
|
|
546
|
-
Args:
|
|
547
|
-
env: Environment configuration
|
|
548
|
-
install_pytorch: Whether to install PyTorch
|
|
549
|
-
verify_packages: Packages to verify after installation
|
|
550
|
-
|
|
551
|
-
Returns:
|
|
552
|
-
Path to the environment directory
|
|
553
|
-
"""
|
|
554
|
-
self.log("=" * 50)
|
|
555
|
-
self.log(f"Setting up isolated environment: {env.name}")
|
|
556
|
-
self.log("=" * 50)
|
|
557
|
-
|
|
558
|
-
# Ensure system-level deps (Media Foundation on Windows)
|
|
559
|
-
self.ensure_system_deps()
|
|
560
|
-
|
|
561
|
-
# Check if already ready
|
|
562
|
-
if self.is_ready(env, verify_packages):
|
|
563
|
-
self.log("Environment already ready, skipping setup")
|
|
564
|
-
return self.get_env_dir(env)
|
|
565
|
-
|
|
566
|
-
# Create virtual environment
|
|
567
|
-
env_dir = self.create_venv(env)
|
|
568
|
-
|
|
569
|
-
# Install PyTorch
|
|
570
|
-
if install_pytorch and (env.cuda is not None or detect_cuda_version()):
|
|
571
|
-
self.install_pytorch(env)
|
|
572
|
-
|
|
573
|
-
# Install comfy-env (needed for BaseWorker in workers)
|
|
574
|
-
self._install_comfy_env(env)
|
|
575
|
-
|
|
576
|
-
# Install other requirements
|
|
577
|
-
self.install_requirements(env)
|
|
578
|
-
|
|
579
|
-
# Windows: Install VC++ runtime and set up DLL paths
|
|
580
|
-
if self.platform.name == 'windows':
|
|
581
|
-
self._setup_windows_deps(env, env_dir)
|
|
582
|
-
|
|
583
|
-
# Verify installation
|
|
584
|
-
if verify_packages:
|
|
585
|
-
if self.is_ready(env, verify_packages):
|
|
586
|
-
self.log("Verification passed!")
|
|
587
|
-
else:
|
|
588
|
-
raise RuntimeError(f"Verification failed: could not import {verify_packages}")
|
|
589
|
-
|
|
590
|
-
self.log("=" * 50)
|
|
591
|
-
self.log("Setup complete!")
|
|
592
|
-
self.log(f"Python: {self.get_python(env)}")
|
|
593
|
-
self.log("=" * 50)
|
|
594
|
-
|
|
595
|
-
return env_dir
|
|
596
|
-
|
|
597
|
-
def delete(self, env: IsolatedEnv) -> bool:
|
|
598
|
-
"""
|
|
599
|
-
Delete an environment.
|
|
600
|
-
|
|
601
|
-
Args:
|
|
602
|
-
env: Environment configuration
|
|
603
|
-
|
|
604
|
-
Returns:
|
|
605
|
-
True if deleted, False if didn't exist
|
|
606
|
-
"""
|
|
607
|
-
env_dir = self.get_env_dir(env)
|
|
608
|
-
|
|
609
|
-
if not env_dir.exists():
|
|
610
|
-
return False
|
|
611
|
-
|
|
612
|
-
self.log(f"Deleting environment: {env_dir}")
|
|
613
|
-
self.platform.rmtree_robust(env_dir)
|
|
614
|
-
self.log("Environment deleted")
|
|
615
|
-
return True
|
|
616
|
-
|
|
617
|
-
def repair(
|
|
618
|
-
self,
|
|
619
|
-
env: IsolatedEnv,
|
|
620
|
-
install_pytorch: bool = True,
|
|
621
|
-
verify_packages: Optional[list] = None,
|
|
622
|
-
) -> Path:
|
|
623
|
-
"""
|
|
624
|
-
Delete and recreate an environment.
|
|
625
|
-
|
|
626
|
-
Args:
|
|
627
|
-
env: Environment configuration
|
|
628
|
-
install_pytorch: Whether to install PyTorch
|
|
629
|
-
verify_packages: Packages to verify after installation
|
|
630
|
-
|
|
631
|
-
Returns:
|
|
632
|
-
Path to the new environment directory
|
|
633
|
-
"""
|
|
634
|
-
self.log("Repairing environment (delete + recreate)")
|
|
635
|
-
self.delete(env)
|
|
636
|
-
return self.setup(env, install_pytorch, verify_packages)
|