comfy-env 0.0.8__py3-none-any.whl → 0.0.14__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 +1 -1
- comfy_env/cli.py +103 -5
- comfy_env/env/config.py +4 -0
- comfy_env/env/config_file.py +62 -32
- comfy_env/env/detection.py +25 -18
- comfy_env/env/manager.py +26 -9
- comfy_env/errors.py +0 -32
- comfy_env/install.py +1 -0
- comfy_env/ipc/bridge.py +0 -36
- comfy_env/registry.py +17 -178
- comfy_env/resolver.py +6 -17
- comfy_env/stubs/folder_paths.py +4 -0
- comfy_env/wheel_sources.yml +141 -0
- {comfy_env-0.0.8.dist-info → comfy_env-0.0.14.dist-info}/METADATA +2 -1
- {comfy_env-0.0.8.dist-info → comfy_env-0.0.14.dist-info}/RECORD +18 -18
- comfy_env/runner.py +0 -273
- {comfy_env-0.0.8.dist-info → comfy_env-0.0.14.dist-info}/WHEEL +0 -0
- {comfy_env-0.0.8.dist-info → comfy_env-0.0.14.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.0.8.dist-info → comfy_env-0.0.14.dist-info}/licenses/LICENSE +0 -0
comfy_env/__init__.py
CHANGED
|
@@ -40,7 +40,7 @@ This package provides:
|
|
|
40
40
|
The @isolated decorator and WorkerBridge are still available.
|
|
41
41
|
"""
|
|
42
42
|
|
|
43
|
-
__version__ = "0.0.
|
|
43
|
+
__version__ = "0.0.11"
|
|
44
44
|
|
|
45
45
|
from .env.config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq
|
|
46
46
|
from .env.config_file import (
|
comfy_env/cli.py
CHANGED
|
@@ -234,6 +234,7 @@ def cmd_info(args) -> int:
|
|
|
234
234
|
def cmd_resolve(args) -> int:
|
|
235
235
|
"""Handle resolve command."""
|
|
236
236
|
from .resolver import RuntimeEnv, WheelResolver, parse_wheel_requirement
|
|
237
|
+
from .registry import PACKAGE_REGISTRY
|
|
237
238
|
from .env.config_file import discover_env_config, load_env_from_file
|
|
238
239
|
|
|
239
240
|
env = RuntimeEnv.detect()
|
|
@@ -270,19 +271,116 @@ def cmd_resolve(args) -> int:
|
|
|
270
271
|
print(f" {package}: No version specified, skipping")
|
|
271
272
|
continue
|
|
272
273
|
|
|
274
|
+
pkg_lower = package.lower()
|
|
273
275
|
try:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
# Check if package is in registry with github_release method
|
|
277
|
+
if pkg_lower in PACKAGE_REGISTRY:
|
|
278
|
+
registry_config = PACKAGE_REGISTRY[pkg_lower]
|
|
279
|
+
method = registry_config.get("method")
|
|
280
|
+
|
|
281
|
+
if method == "github_release":
|
|
282
|
+
# Resolve URL from registry sources
|
|
283
|
+
url = _resolve_github_release_url(package, version, env, registry_config)
|
|
284
|
+
status = "OK" if args.verify else "resolved"
|
|
285
|
+
print(f" {package}=={version}: {status}")
|
|
286
|
+
print(f" {url}")
|
|
287
|
+
else:
|
|
288
|
+
# For other methods, just show what method will be used
|
|
289
|
+
print(f" {package}=={version}: uses {method} method")
|
|
290
|
+
if "index_url" in registry_config:
|
|
291
|
+
index_url = _substitute_template(registry_config["index_url"], env)
|
|
292
|
+
print(f" index: {index_url}")
|
|
293
|
+
elif "package_template" in registry_config:
|
|
294
|
+
pkg_name = _substitute_template(registry_config["package_template"], env)
|
|
295
|
+
print(f" installs as: {pkg_name}")
|
|
296
|
+
else:
|
|
297
|
+
# Fall back to WheelResolver
|
|
298
|
+
url = resolver.resolve(package, version, env, verify=args.verify)
|
|
299
|
+
status = "OK" if args.verify else "resolved"
|
|
300
|
+
print(f" {package}=={version}: {status}")
|
|
301
|
+
print(f" {url}")
|
|
278
302
|
except Exception as e:
|
|
279
303
|
print(f" {package}=={version}: FAILED")
|
|
280
|
-
|
|
304
|
+
_print_wheel_not_found_error(package, version, env, e)
|
|
281
305
|
all_ok = False
|
|
282
306
|
|
|
283
307
|
return 0 if all_ok else 1
|
|
284
308
|
|
|
285
309
|
|
|
310
|
+
def _substitute_template(template: str, env) -> str:
|
|
311
|
+
"""Substitute environment variables into a URL template."""
|
|
312
|
+
vars_dict = env.as_dict()
|
|
313
|
+
result = template
|
|
314
|
+
for key, value in vars_dict.items():
|
|
315
|
+
if value is not None:
|
|
316
|
+
result = result.replace(f"{{{key}}}", str(value))
|
|
317
|
+
return result
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _resolve_github_release_url(package: str, version: str, env, config: dict) -> str:
|
|
321
|
+
"""Resolve URL for github_release method packages."""
|
|
322
|
+
sources = config.get("sources", [])
|
|
323
|
+
if not sources:
|
|
324
|
+
raise ValueError(f"No sources configured for {package}")
|
|
325
|
+
|
|
326
|
+
# Build template variables
|
|
327
|
+
vars_dict = env.as_dict()
|
|
328
|
+
vars_dict["version"] = version
|
|
329
|
+
vars_dict["py_tag"] = f"cp{env.python_short}"
|
|
330
|
+
if env.cuda_version:
|
|
331
|
+
vars_dict["cuda_major"] = env.cuda_version.split(".")[0]
|
|
332
|
+
|
|
333
|
+
# Filter sources by platform
|
|
334
|
+
current_platform = env.platform_tag
|
|
335
|
+
compatible_sources = [
|
|
336
|
+
s for s in sources
|
|
337
|
+
if current_platform in s.get("platforms", [])
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
if not compatible_sources:
|
|
341
|
+
available = set()
|
|
342
|
+
for s in sources:
|
|
343
|
+
available.update(s.get("platforms", []))
|
|
344
|
+
raise ValueError(
|
|
345
|
+
f"No {package} wheels for platform {current_platform}. "
|
|
346
|
+
f"Available: {', '.join(sorted(available))}"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Return URL from first compatible source
|
|
350
|
+
source = compatible_sources[0]
|
|
351
|
+
url_template = source.get("url_template", "")
|
|
352
|
+
url = url_template
|
|
353
|
+
for key, value in vars_dict.items():
|
|
354
|
+
if value is not None:
|
|
355
|
+
url = url.replace(f"{{{key}}}", str(value))
|
|
356
|
+
|
|
357
|
+
return url
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _print_wheel_not_found_error(package: str, version: str, env, error: Exception) -> None:
|
|
361
|
+
"""Print a formatted error message for wheel not found."""
|
|
362
|
+
from .errors import WheelNotFoundError
|
|
363
|
+
|
|
364
|
+
if isinstance(error, WheelNotFoundError):
|
|
365
|
+
print(f" CUDA wheel not found: {package}=={version}")
|
|
366
|
+
print()
|
|
367
|
+
print("+------------------------------------------------------------------+")
|
|
368
|
+
print("| CUDA Wheel Not Found |")
|
|
369
|
+
print("+------------------------------------------------------------------+")
|
|
370
|
+
print(f"| Package: {package}=={version:<46} |")
|
|
371
|
+
print(f"| Requested: cu{env.cuda_short}-torch{env.torch_mm}-{env.python_short}-{env.platform_tag:<17} |")
|
|
372
|
+
print("| |")
|
|
373
|
+
print(f"| Reason: {error.reason:<54} |")
|
|
374
|
+
print("| |")
|
|
375
|
+
print("| Suggestions: |")
|
|
376
|
+
print(f"| 1. Check if wheel exists: comfy-env resolve {package:<15} |")
|
|
377
|
+
print(f"| 2. Build wheel locally: comfy-env build {package:<18} |")
|
|
378
|
+
print("| |")
|
|
379
|
+
print("+------------------------------------------------------------------+")
|
|
380
|
+
else:
|
|
381
|
+
print(f" {error}")
|
|
382
|
+
|
|
383
|
+
|
|
286
384
|
def cmd_doctor(args) -> int:
|
|
287
385
|
"""Handle doctor command."""
|
|
288
386
|
from .install import verify_installation
|
comfy_env/env/config.py
CHANGED
|
@@ -98,6 +98,10 @@ class IsolatedEnv:
|
|
|
98
98
|
cuda: Optional[str] = None
|
|
99
99
|
requirements: list[str] = field(default_factory=list)
|
|
100
100
|
no_deps_requirements: list[str] = field(default_factory=list) # Install with --no-deps
|
|
101
|
+
# Platform-specific requirements (merged at install time)
|
|
102
|
+
windows_requirements: list[str] = field(default_factory=list)
|
|
103
|
+
linux_requirements: list[str] = field(default_factory=list)
|
|
104
|
+
darwin_requirements: list[str] = field(default_factory=list)
|
|
101
105
|
requirements_file: Optional[Path] = None
|
|
102
106
|
wheel_sources: list[str] = field(default_factory=list)
|
|
103
107
|
index_urls: list[str] = field(default_factory=list)
|
comfy_env/env/config_file.py
CHANGED
|
@@ -168,7 +168,7 @@ def _get_default_pytorch_version(cuda_version: Optional[str]) -> str:
|
|
|
168
168
|
- CUDA 12.8 (Turing+): PyTorch 2.8.0
|
|
169
169
|
"""
|
|
170
170
|
if cuda_version == "12.4":
|
|
171
|
-
return "2.5.1" #
|
|
171
|
+
return "2.5.1" # Full: Pascal GPUs
|
|
172
172
|
return "2.8.0" # Modern: Turing through Blackwell
|
|
173
173
|
|
|
174
174
|
|
|
@@ -176,7 +176,7 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
|
|
|
176
176
|
"""
|
|
177
177
|
Parse TOML data into IsolatedEnv.
|
|
178
178
|
|
|
179
|
-
Supports both simplified and
|
|
179
|
+
Supports both simplified and full config formats:
|
|
180
180
|
|
|
181
181
|
Simplified (CUDA packages only):
|
|
182
182
|
[packages]
|
|
@@ -186,7 +186,7 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
|
|
|
186
186
|
Or as list:
|
|
187
187
|
packages = ["torch-scatter==2.1.2", "torch-cluster==1.6.3"]
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
Full:
|
|
190
190
|
[env]
|
|
191
191
|
name = "my-node"
|
|
192
192
|
[packages]
|
|
@@ -233,7 +233,7 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
|
|
|
233
233
|
variables.setdefault("pytorch_mm", pytorch_mm)
|
|
234
234
|
|
|
235
235
|
# Parse CUDA packages - support multiple formats
|
|
236
|
-
# Priority: [cuda] section > cuda = [...] >
|
|
236
|
+
# Priority: [cuda] section > cuda = [...] > [packages] section
|
|
237
237
|
no_deps_requirements = []
|
|
238
238
|
requirements = []
|
|
239
239
|
|
|
@@ -258,12 +258,12 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
|
|
|
258
258
|
|
|
259
259
|
elif isinstance(packages_section, dict):
|
|
260
260
|
# Check for simplified format: [packages] with key=value pairs
|
|
261
|
-
# vs
|
|
261
|
+
# vs old format: [packages] with requirements/no_deps lists
|
|
262
262
|
|
|
263
|
-
|
|
263
|
+
has_old_keys = any(k in packages_section for k in ["requirements", "no_deps", "requirements_file"])
|
|
264
264
|
|
|
265
|
-
if
|
|
266
|
-
#
|
|
265
|
+
if has_old_keys:
|
|
266
|
+
# Old format
|
|
267
267
|
raw_requirements = packages_section.get("requirements", [])
|
|
268
268
|
requirements = [_substitute_vars(req, variables) for req in raw_requirements]
|
|
269
269
|
|
|
@@ -340,9 +340,9 @@ def load_config(
|
|
|
340
340
|
base_dir: Optional[Path] = None,
|
|
341
341
|
) -> EnvManagerConfig:
|
|
342
342
|
"""
|
|
343
|
-
Load full EnvManagerConfig from a TOML file
|
|
343
|
+
Load full EnvManagerConfig from a TOML file.
|
|
344
344
|
|
|
345
|
-
Supports both
|
|
345
|
+
Supports both full schema (named envs) and simple format (auto-detected).
|
|
346
346
|
|
|
347
347
|
Args:
|
|
348
348
|
path: Path to the TOML config file
|
|
@@ -373,7 +373,7 @@ def load_config(
|
|
|
373
373
|
with open(path, "rb") as f:
|
|
374
374
|
data = tomllib.load(f)
|
|
375
375
|
|
|
376
|
-
return
|
|
376
|
+
return _parse_full_config(data, base_dir)
|
|
377
377
|
|
|
378
378
|
|
|
379
379
|
def discover_config(
|
|
@@ -404,9 +404,9 @@ def discover_config(
|
|
|
404
404
|
return None
|
|
405
405
|
|
|
406
406
|
|
|
407
|
-
def
|
|
407
|
+
def _parse_full_config(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
|
|
408
408
|
"""
|
|
409
|
-
Parse TOML data into EnvManagerConfig
|
|
409
|
+
Parse TOML data into EnvManagerConfig.
|
|
410
410
|
|
|
411
411
|
Schema:
|
|
412
412
|
[local.cuda] - CUDA packages for host
|
|
@@ -416,16 +416,16 @@ def _parse_config_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
|
|
|
416
416
|
[envname.packages] - Packages for env
|
|
417
417
|
[node_reqs] - Node dependencies
|
|
418
418
|
|
|
419
|
-
Also supports
|
|
419
|
+
Also supports simple format ([env] + [packages]) for backward compatibility.
|
|
420
420
|
"""
|
|
421
|
-
# Detect if this is
|
|
422
|
-
|
|
421
|
+
# Detect if this is full schema or simple format
|
|
422
|
+
is_full = "local" in data or _has_named_env(data)
|
|
423
423
|
|
|
424
|
-
if not
|
|
425
|
-
#
|
|
426
|
-
return
|
|
424
|
+
if not is_full:
|
|
425
|
+
# Simple format - convert to full structure
|
|
426
|
+
return _convert_simple_to_full(data, base_dir)
|
|
427
427
|
|
|
428
|
-
# Parse
|
|
428
|
+
# Parse full schema
|
|
429
429
|
local = _parse_local_section(data.get("local", {}))
|
|
430
430
|
envs = _parse_env_sections(data, base_dir)
|
|
431
431
|
node_reqs = _parse_node_reqs(data.get("node_reqs", {}))
|
|
@@ -529,6 +529,30 @@ def _parse_single_env(name: str, env_data: Dict[str, Any], base_dir: Path) -> Is
|
|
|
529
529
|
elif isinstance(packages_section, list):
|
|
530
530
|
requirements = packages_section
|
|
531
531
|
|
|
532
|
+
# Parse platform-specific packages [envname.packages.windows], etc.
|
|
533
|
+
windows_reqs = []
|
|
534
|
+
linux_reqs = []
|
|
535
|
+
darwin_reqs = []
|
|
536
|
+
|
|
537
|
+
if isinstance(packages_section, dict):
|
|
538
|
+
win_section = packages_section.get("windows", {})
|
|
539
|
+
if isinstance(win_section, dict):
|
|
540
|
+
windows_reqs = win_section.get("requirements", [])
|
|
541
|
+
elif isinstance(win_section, list):
|
|
542
|
+
windows_reqs = win_section
|
|
543
|
+
|
|
544
|
+
linux_section = packages_section.get("linux", {})
|
|
545
|
+
if isinstance(linux_section, dict):
|
|
546
|
+
linux_reqs = linux_section.get("requirements", [])
|
|
547
|
+
elif isinstance(linux_section, list):
|
|
548
|
+
linux_reqs = linux_section
|
|
549
|
+
|
|
550
|
+
darwin_section = packages_section.get("darwin", {})
|
|
551
|
+
if isinstance(darwin_section, dict):
|
|
552
|
+
darwin_reqs = darwin_section.get("requirements", [])
|
|
553
|
+
elif isinstance(darwin_section, list):
|
|
554
|
+
darwin_reqs = darwin_section
|
|
555
|
+
|
|
532
556
|
return IsolatedEnv(
|
|
533
557
|
name=name,
|
|
534
558
|
python=python,
|
|
@@ -536,6 +560,9 @@ def _parse_single_env(name: str, env_data: Dict[str, Any], base_dir: Path) -> Is
|
|
|
536
560
|
pytorch_version=pytorch,
|
|
537
561
|
requirements=requirements,
|
|
538
562
|
no_deps_requirements=no_deps_requirements,
|
|
563
|
+
windows_requirements=windows_reqs,
|
|
564
|
+
linux_requirements=linux_reqs,
|
|
565
|
+
darwin_requirements=darwin_reqs,
|
|
539
566
|
)
|
|
540
567
|
|
|
541
568
|
|
|
@@ -555,27 +582,30 @@ def _parse_node_reqs(node_reqs_data: Dict[str, Any]) -> List[NodeReq]:
|
|
|
555
582
|
return reqs
|
|
556
583
|
|
|
557
584
|
|
|
558
|
-
def
|
|
559
|
-
"""Convert
|
|
560
|
-
# Parse using legacy parser to get IsolatedEnv
|
|
561
|
-
legacy_env = _parse_config(data, base_dir)
|
|
585
|
+
def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
|
|
586
|
+
"""Convert simple config format to full EnvManagerConfig.
|
|
562
587
|
|
|
563
|
-
|
|
564
|
-
|
|
588
|
+
Simple configs have [env] and [packages] sections but no named environments.
|
|
589
|
+
This converts them to the full format with a single named environment.
|
|
590
|
+
"""
|
|
591
|
+
# Parse using simple parser to get IsolatedEnv
|
|
592
|
+
simple_env = _parse_config(data, base_dir)
|
|
593
|
+
|
|
594
|
+
# Check if this has explicit env settings (isolated venv) vs just CUDA packages (local install)
|
|
565
595
|
env_section = data.get("env", {})
|
|
566
596
|
has_explicit_env = bool(env_section.get("name") or env_section.get("python"))
|
|
567
597
|
|
|
568
598
|
if has_explicit_env:
|
|
569
|
-
#
|
|
599
|
+
# Isolated venv config
|
|
570
600
|
return EnvManagerConfig(
|
|
571
601
|
local=LocalConfig(),
|
|
572
|
-
envs={
|
|
602
|
+
envs={simple_env.name: simple_env},
|
|
573
603
|
node_reqs=[],
|
|
574
604
|
)
|
|
575
605
|
else:
|
|
576
|
-
#
|
|
606
|
+
# Local CUDA packages only (no isolated venv)
|
|
577
607
|
cuda_packages = {}
|
|
578
|
-
for req in
|
|
608
|
+
for req in simple_env.no_deps_requirements:
|
|
579
609
|
if "==" in req:
|
|
580
610
|
pkg, ver = req.split("==", 1)
|
|
581
611
|
cuda_packages[pkg] = ver
|
|
@@ -585,8 +615,8 @@ def _convert_legacy_to_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerCon
|
|
|
585
615
|
return EnvManagerConfig(
|
|
586
616
|
local=LocalConfig(
|
|
587
617
|
cuda_packages=cuda_packages,
|
|
588
|
-
requirements=
|
|
618
|
+
requirements=simple_env.requirements,
|
|
589
619
|
),
|
|
590
620
|
envs={},
|
|
591
621
|
node_reqs=[],
|
|
592
|
-
)
|
|
622
|
+
)
|
comfy_env/env/detection.py
CHANGED
|
@@ -7,10 +7,16 @@ vs older GPUs which use CUDA 12.4.
|
|
|
7
7
|
This runs BEFORE PyTorch is installed, so we use nvidia-smi directly.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import os
|
|
10
11
|
import subprocess
|
|
11
12
|
from typing import List, Dict, Optional
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
# Environment variable to override CUDA version detection
|
|
16
|
+
# Useful for CI environments without GPU
|
|
17
|
+
CUDA_VERSION_ENV_VAR = "COMFY_ENV_CUDA_VERSION"
|
|
18
|
+
|
|
19
|
+
|
|
14
20
|
def detect_gpu_info() -> List[Dict[str, str]]:
|
|
15
21
|
"""
|
|
16
22
|
Detect GPU name and compute capability using nvidia-smi.
|
|
@@ -86,22 +92,6 @@ def is_blackwell_gpu(name: str, compute_cap: str) -> bool:
|
|
|
86
92
|
return False
|
|
87
93
|
|
|
88
94
|
|
|
89
|
-
def needs_cuda_128() -> bool:
|
|
90
|
-
"""
|
|
91
|
-
Check if any detected GPU requires CUDA 12.8.
|
|
92
|
-
|
|
93
|
-
Returns:
|
|
94
|
-
True if Blackwell GPU detected, False otherwise.
|
|
95
|
-
"""
|
|
96
|
-
gpus = detect_gpu_info()
|
|
97
|
-
|
|
98
|
-
for gpu in gpus:
|
|
99
|
-
if is_blackwell_gpu(gpu["name"], gpu["compute_cap"]):
|
|
100
|
-
return True
|
|
101
|
-
|
|
102
|
-
return False
|
|
103
|
-
|
|
104
|
-
|
|
105
95
|
def is_legacy_gpu(compute_cap: str) -> bool:
|
|
106
96
|
"""
|
|
107
97
|
Check if GPU is Pascal or older (requires legacy CUDA/PyTorch).
|
|
@@ -126,7 +116,12 @@ def detect_cuda_version() -> Optional[str]:
|
|
|
126
116
|
Returns:
|
|
127
117
|
"12.4" for Pascal or older (compute < 7.5),
|
|
128
118
|
"12.8" for Turing or newer (compute >= 7.5),
|
|
129
|
-
None if no GPU detected.
|
|
119
|
+
None if no GPU detected and no env var override.
|
|
120
|
+
|
|
121
|
+
Environment Variable Override:
|
|
122
|
+
Set COMFY_ENV_CUDA_VERSION to override auto-detection.
|
|
123
|
+
Useful for CI environments without GPU.
|
|
124
|
+
Example: COMFY_ENV_CUDA_VERSION=12.8
|
|
130
125
|
|
|
131
126
|
GPU Architecture Reference:
|
|
132
127
|
- Pascal (GTX 10xx, P100): compute 6.0-6.1 → CUDA 12.4
|
|
@@ -138,6 +133,15 @@ def detect_cuda_version() -> Optional[str]:
|
|
|
138
133
|
"""
|
|
139
134
|
gpus = detect_gpu_info()
|
|
140
135
|
if not gpus:
|
|
136
|
+
# No GPU detected - check for env var override
|
|
137
|
+
override = os.environ.get(CUDA_VERSION_ENV_VAR)
|
|
138
|
+
if override:
|
|
139
|
+
# Normalize format (e.g., "12.8" or "128" -> "12.8")
|
|
140
|
+
override = override.strip()
|
|
141
|
+
if override and '.' not in override and len(override) >= 2:
|
|
142
|
+
# Convert "128" -> "12.8"
|
|
143
|
+
override = f"{override[:-1]}.{override[-1]}"
|
|
144
|
+
return override
|
|
141
145
|
return None
|
|
142
146
|
|
|
143
147
|
# Check if any GPU is legacy (Pascal or older)
|
|
@@ -159,7 +163,10 @@ def get_gpu_summary() -> str:
|
|
|
159
163
|
gpus = detect_gpu_info()
|
|
160
164
|
|
|
161
165
|
if not gpus:
|
|
162
|
-
|
|
166
|
+
override = os.environ.get(CUDA_VERSION_ENV_VAR)
|
|
167
|
+
if override:
|
|
168
|
+
return f"No NVIDIA GPU detected (using {CUDA_VERSION_ENV_VAR}={override})"
|
|
169
|
+
return f"No NVIDIA GPU detected (set {CUDA_VERSION_ENV_VAR} to override)"
|
|
163
170
|
|
|
164
171
|
lines = []
|
|
165
172
|
for i, gpu in enumerate(gpus):
|
comfy_env/env/manager.py
CHANGED
|
@@ -6,6 +6,7 @@ Uses uv for fast environment creation and package installation.
|
|
|
6
6
|
|
|
7
7
|
import subprocess
|
|
8
8
|
import shutil
|
|
9
|
+
import sys
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Optional, Callable
|
|
11
12
|
|
|
@@ -263,13 +264,29 @@ class IsolatedEnvManager:
|
|
|
263
264
|
"""
|
|
264
265
|
python_exe = self.get_python(env)
|
|
265
266
|
|
|
266
|
-
|
|
267
|
+
# Merge platform-specific requirements
|
|
268
|
+
if sys.platform == 'win32':
|
|
269
|
+
platform_reqs = env.windows_requirements
|
|
270
|
+
platform_name = 'Windows'
|
|
271
|
+
elif sys.platform == 'darwin':
|
|
272
|
+
platform_reqs = env.darwin_requirements
|
|
273
|
+
platform_name = 'macOS'
|
|
274
|
+
else:
|
|
275
|
+
platform_reqs = env.linux_requirements
|
|
276
|
+
platform_name = 'Linux'
|
|
277
|
+
|
|
278
|
+
all_requirements = list(env.requirements) + list(platform_reqs)
|
|
279
|
+
|
|
280
|
+
if platform_reqs:
|
|
281
|
+
self.log(f"Including {len(platform_reqs)} {platform_name}-specific packages")
|
|
282
|
+
|
|
283
|
+
if not all_requirements and not env.requirements_file:
|
|
267
284
|
self.log("No requirements to install")
|
|
268
285
|
return
|
|
269
286
|
|
|
270
287
|
# Validate requirements for security
|
|
271
|
-
if
|
|
272
|
-
validate_dependencies(
|
|
288
|
+
if all_requirements:
|
|
289
|
+
validate_dependencies(all_requirements)
|
|
273
290
|
|
|
274
291
|
# Validate wheel sources
|
|
275
292
|
for wheel_source in env.wheel_sources:
|
|
@@ -311,11 +328,11 @@ class IsolatedEnvManager:
|
|
|
311
328
|
self.log(f"Installing {len(env.no_deps_requirements)} CUDA packages")
|
|
312
329
|
self._install_cuda_packages(env, pip_args)
|
|
313
330
|
|
|
314
|
-
# Install individual requirements
|
|
315
|
-
if
|
|
316
|
-
self.log(f"Installing {len(
|
|
331
|
+
# Install individual requirements (including platform-specific)
|
|
332
|
+
if all_requirements:
|
|
333
|
+
self.log(f"Installing {len(all_requirements)} packages")
|
|
317
334
|
result = subprocess.run(
|
|
318
|
-
pip_args +
|
|
335
|
+
pip_args + all_requirements,
|
|
319
336
|
capture_output=True,
|
|
320
337
|
text=True,
|
|
321
338
|
)
|
|
@@ -458,7 +475,7 @@ class IsolatedEnvManager:
|
|
|
458
475
|
url_template = source["url_template"]
|
|
459
476
|
url = self._substitute_template(url_template, vars_dict)
|
|
460
477
|
|
|
461
|
-
self.log(f" Trying {source.get('name', 'unknown')}: {url
|
|
478
|
+
self.log(f" Trying {source.get('name', 'unknown')}: {url}")
|
|
462
479
|
result = subprocess.run(
|
|
463
480
|
pip_args + ["--no-deps", url],
|
|
464
481
|
capture_output=True, text=True,
|
|
@@ -467,7 +484,7 @@ class IsolatedEnvManager:
|
|
|
467
484
|
if result.returncode == 0:
|
|
468
485
|
return # Success!
|
|
469
486
|
|
|
470
|
-
errors.append(f"{source.get('name', 'unknown')}: {result.stderr
|
|
487
|
+
errors.append(f"{source.get('name', 'unknown')}: {result.stderr.strip()}")
|
|
471
488
|
|
|
472
489
|
# All sources failed
|
|
473
490
|
raise RuntimeError(
|
comfy_env/errors.py
CHANGED
|
@@ -291,35 +291,3 @@ class InstallError(EnvManagerError):
|
|
|
291
291
|
|
|
292
292
|
details = "\n".join(details_parts) if details_parts else None
|
|
293
293
|
super().__init__(message, details)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def format_environment_mismatch(
|
|
297
|
-
expected: "RuntimeEnv",
|
|
298
|
-
actual: "RuntimeEnv",
|
|
299
|
-
) -> str:
|
|
300
|
-
"""
|
|
301
|
-
Format a message explaining environment mismatch.
|
|
302
|
-
|
|
303
|
-
Used when the current environment doesn't match what's needed.
|
|
304
|
-
"""
|
|
305
|
-
mismatches = []
|
|
306
|
-
|
|
307
|
-
if expected.cuda_version != actual.cuda_version:
|
|
308
|
-
mismatches.append(
|
|
309
|
-
f" CUDA: expected {expected.cuda_version}, got {actual.cuda_version}"
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
if expected.torch_version != actual.torch_version:
|
|
313
|
-
mismatches.append(
|
|
314
|
-
f" PyTorch: expected {expected.torch_version}, got {actual.torch_version}"
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
if expected.python_version != actual.python_version:
|
|
318
|
-
mismatches.append(
|
|
319
|
-
f" Python: expected {expected.python_version}, got {actual.python_version}"
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
if not mismatches:
|
|
323
|
-
return "Environment matches expected configuration"
|
|
324
|
-
|
|
325
|
-
return "Environment mismatch:\n" + "\n".join(mismatches)
|
comfy_env/install.py
CHANGED
comfy_env/ipc/bridge.py
CHANGED
|
@@ -48,10 +48,6 @@ class WorkerBridge:
|
|
|
48
48
|
result = bridge.call("process", image=my_image)
|
|
49
49
|
"""
|
|
50
50
|
|
|
51
|
-
# Singleton instances by environment hash
|
|
52
|
-
_instances: Dict[str, "WorkerBridge"] = {}
|
|
53
|
-
_instances_lock = threading.Lock()
|
|
54
|
-
|
|
55
51
|
def __init__(
|
|
56
52
|
self,
|
|
57
53
|
env: IsolatedEnv,
|
|
@@ -81,38 +77,6 @@ class WorkerBridge:
|
|
|
81
77
|
self._process_lock = threading.Lock()
|
|
82
78
|
self._stderr_thread: Optional[threading.Thread] = None
|
|
83
79
|
|
|
84
|
-
@classmethod
|
|
85
|
-
def get_instance(
|
|
86
|
-
cls,
|
|
87
|
-
env: IsolatedEnv,
|
|
88
|
-
worker_script: Path,
|
|
89
|
-
base_dir: Optional[Path] = None,
|
|
90
|
-
log_callback: Optional[Callable[[str], None]] = None,
|
|
91
|
-
) -> "WorkerBridge":
|
|
92
|
-
"""
|
|
93
|
-
Get or create a singleton bridge instance for an environment.
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
env: Isolated environment configuration
|
|
97
|
-
worker_script: Path to the worker Python script
|
|
98
|
-
base_dir: Base directory for environments
|
|
99
|
-
log_callback: Optional callback for logging
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
WorkerBridge instance (reused if same env hash)
|
|
103
|
-
"""
|
|
104
|
-
env_hash = env.get_env_hash()
|
|
105
|
-
|
|
106
|
-
with cls._instances_lock:
|
|
107
|
-
if env_hash not in cls._instances:
|
|
108
|
-
cls._instances[env_hash] = cls(
|
|
109
|
-
env=env,
|
|
110
|
-
worker_script=worker_script,
|
|
111
|
-
base_dir=base_dir,
|
|
112
|
-
log_callback=log_callback,
|
|
113
|
-
)
|
|
114
|
-
return cls._instances[env_hash]
|
|
115
|
-
|
|
116
80
|
@classmethod
|
|
117
81
|
def from_config_file(
|
|
118
82
|
cls,
|