comfy-env 0.0.48__py3-none-any.whl → 0.0.50__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 +7 -0
- comfy_env/decorator.py +256 -2
- comfy_env/env/config.py +2 -0
- comfy_env/env/config_file.py +4 -0
- comfy_env/install.py +38 -104
- comfy_env/isolation.py +297 -0
- comfy_env/pixi.py +124 -24
- comfy_env/stub_imports.py +310 -0
- comfy_env/templates/comfy-env-instructions.txt +103 -0
- comfy_env/templates/comfy-env.toml +186 -0
- comfy_env/wheel_sources.yml +1 -1
- comfy_env/workers/torch_mp.py +5 -1
- comfy_env/workers/venv.py +94 -3
- {comfy_env-0.0.48.dist-info → comfy_env-0.0.50.dist-info}/METADATA +62 -4
- {comfy_env-0.0.48.dist-info → comfy_env-0.0.50.dist-info}/RECORD +18 -14
- {comfy_env-0.0.48.dist-info → comfy_env-0.0.50.dist-info}/WHEEL +0 -0
- {comfy_env-0.0.48.dist-info → comfy_env-0.0.50.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.0.48.dist-info → comfy_env-0.0.50.dist-info}/licenses/LICENSE +0 -0
comfy_env/__init__.py
CHANGED
|
@@ -34,6 +34,8 @@ from .env.security import (
|
|
|
34
34
|
from .ipc.bridge import WorkerBridge
|
|
35
35
|
from .ipc.worker import BaseWorker, register
|
|
36
36
|
from .decorator import isolated, shutdown_all_processes
|
|
37
|
+
from .isolation import enable_isolation
|
|
38
|
+
from .stub_imports import setup_isolated_imports, cleanup_stubs
|
|
37
39
|
|
|
38
40
|
# New in-place installation API
|
|
39
41
|
from .install import install, verify_installation
|
|
@@ -144,6 +146,11 @@ __all__ = [
|
|
|
144
146
|
# Legacy Decorator API
|
|
145
147
|
"isolated",
|
|
146
148
|
"shutdown_all_processes",
|
|
149
|
+
# New: Enable isolation for entire node pack
|
|
150
|
+
"enable_isolation",
|
|
151
|
+
# Import stubbing for isolated packages
|
|
152
|
+
"setup_isolated_imports",
|
|
153
|
+
"cleanup_stubs",
|
|
147
154
|
]
|
|
148
155
|
|
|
149
156
|
# Add torch-based IPC if available
|
comfy_env/decorator.py
CHANGED
|
@@ -41,7 +41,7 @@ import time
|
|
|
41
41
|
from dataclasses import dataclass
|
|
42
42
|
from functools import wraps
|
|
43
43
|
from pathlib import Path
|
|
44
|
-
from typing import Any, Callable, Dict, List, Optional, Union
|
|
44
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
45
45
|
|
|
46
46
|
logger = logging.getLogger("comfy_env")
|
|
47
47
|
|
|
@@ -178,7 +178,10 @@ def _get_or_create_worker(config: WorkerConfig, log_fn: Callable):
|
|
|
178
178
|
# Same Python - use TorchMPWorker (fast, zero-copy)
|
|
179
179
|
from .workers import TorchMPWorker
|
|
180
180
|
log_fn(f"Creating TorchMPWorker (same Python, zero-copy tensors)")
|
|
181
|
-
worker = TorchMPWorker(
|
|
181
|
+
worker = TorchMPWorker(
|
|
182
|
+
name=config.env_name,
|
|
183
|
+
sys_path=config.sys_path,
|
|
184
|
+
)
|
|
182
185
|
else:
|
|
183
186
|
# Different Python - use PersistentVenvWorker
|
|
184
187
|
from .workers.venv import PersistentVenvWorker
|
|
@@ -444,3 +447,254 @@ def isolated(
|
|
|
444
447
|
return cls
|
|
445
448
|
|
|
446
449
|
return decorator
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# ---------------------------------------------------------------------------
|
|
453
|
+
# The @auto_isolate Decorator (Function-level)
|
|
454
|
+
# ---------------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
def _parse_import_error(e: ImportError) -> Optional[str]:
|
|
457
|
+
"""Extract the module name from an ImportError."""
|
|
458
|
+
# Python's ImportError has a 'name' attribute with the module name
|
|
459
|
+
if hasattr(e, 'name') and e.name:
|
|
460
|
+
return e.name
|
|
461
|
+
|
|
462
|
+
# Fallback: parse from message "No module named 'xxx'"
|
|
463
|
+
msg = str(e)
|
|
464
|
+
if "No module named" in msg:
|
|
465
|
+
# Extract 'xxx' from "No module named 'xxx'" or "No module named 'xxx.yyy'"
|
|
466
|
+
import re
|
|
467
|
+
match = re.search(r"No module named ['\"]([^'\"\.]+)", msg)
|
|
468
|
+
if match:
|
|
469
|
+
return match.group(1)
|
|
470
|
+
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _find_env_for_module(
|
|
475
|
+
module_name: str,
|
|
476
|
+
source_file: Path,
|
|
477
|
+
) -> Optional[Tuple[str, Path, Path]]:
|
|
478
|
+
"""
|
|
479
|
+
Find which isolated environment contains the given module.
|
|
480
|
+
|
|
481
|
+
Searches comfy-env.toml configs starting from the source file's directory,
|
|
482
|
+
looking for the module in cuda packages, requirements, etc.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
module_name: The module that failed to import (e.g., "cumesh")
|
|
486
|
+
source_file: Path to the source file containing the function
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Tuple of (env_name, python_path, node_dir) or None if not found
|
|
490
|
+
"""
|
|
491
|
+
from .env.config_file import discover_config, CONFIG_FILE_NAMES
|
|
492
|
+
|
|
493
|
+
# Normalize module name (cumesh, pytorch3d, etc.)
|
|
494
|
+
module_lower = module_name.lower().replace("-", "_").replace(".", "_")
|
|
495
|
+
|
|
496
|
+
# Search for config file starting from source file's directory
|
|
497
|
+
node_dir = source_file.parent
|
|
498
|
+
while node_dir != node_dir.parent:
|
|
499
|
+
for config_name in CONFIG_FILE_NAMES:
|
|
500
|
+
config_path = node_dir / config_name
|
|
501
|
+
if config_path.exists():
|
|
502
|
+
# Found a config, check if it has our module
|
|
503
|
+
config = discover_config(node_dir)
|
|
504
|
+
if config is None:
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
# Check all environments in the config
|
|
508
|
+
for env_name, env_config in config.envs.items():
|
|
509
|
+
# Check cuda/no_deps_requirements
|
|
510
|
+
if env_config.no_deps_requirements:
|
|
511
|
+
for req in env_config.no_deps_requirements:
|
|
512
|
+
req_name = req.split("==")[0].split(">=")[0].split("<")[0].strip()
|
|
513
|
+
req_lower = req_name.lower().replace("-", "_")
|
|
514
|
+
if req_lower == module_lower:
|
|
515
|
+
# Found it! Get the python path
|
|
516
|
+
env_path = node_dir / f"_env_{env_name}"
|
|
517
|
+
if not env_path.exists():
|
|
518
|
+
# Try pixi path
|
|
519
|
+
env_path = node_dir / ".pixi" / "envs" / "default"
|
|
520
|
+
|
|
521
|
+
if env_path.exists():
|
|
522
|
+
python_path = env_path / "bin" / "python"
|
|
523
|
+
if not python_path.exists():
|
|
524
|
+
python_path = env_path / "Scripts" / "python.exe"
|
|
525
|
+
if python_path.exists():
|
|
526
|
+
return (env_name, python_path, node_dir)
|
|
527
|
+
|
|
528
|
+
# Check regular requirements too
|
|
529
|
+
if env_config.requirements:
|
|
530
|
+
for req in env_config.requirements:
|
|
531
|
+
req_name = req.split("==")[0].split(">=")[0].split("<")[0].split("[")[0].strip()
|
|
532
|
+
req_lower = req_name.lower().replace("-", "_")
|
|
533
|
+
if req_lower == module_lower:
|
|
534
|
+
env_path = node_dir / f"_env_{env_name}"
|
|
535
|
+
if not env_path.exists():
|
|
536
|
+
env_path = node_dir / ".pixi" / "envs" / "default"
|
|
537
|
+
|
|
538
|
+
if env_path.exists():
|
|
539
|
+
python_path = env_path / "bin" / "python"
|
|
540
|
+
if not python_path.exists():
|
|
541
|
+
python_path = env_path / "Scripts" / "python.exe"
|
|
542
|
+
if python_path.exists():
|
|
543
|
+
return (env_name, python_path, node_dir)
|
|
544
|
+
|
|
545
|
+
# Config found but module not in it, stop searching
|
|
546
|
+
break
|
|
547
|
+
|
|
548
|
+
node_dir = node_dir.parent
|
|
549
|
+
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# Cache for auto_isolate workers
|
|
554
|
+
_auto_isolate_workers: Dict[str, Any] = {}
|
|
555
|
+
_auto_isolate_lock = threading.Lock()
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _get_auto_isolate_worker(env_name: str, python_path: Path, node_dir: Path):
|
|
559
|
+
"""Get or create a worker for auto_isolate."""
|
|
560
|
+
cache_key = f"{env_name}:{python_path}"
|
|
561
|
+
|
|
562
|
+
with _auto_isolate_lock:
|
|
563
|
+
if cache_key in _auto_isolate_workers:
|
|
564
|
+
worker = _auto_isolate_workers[cache_key]
|
|
565
|
+
if worker.is_alive():
|
|
566
|
+
return worker
|
|
567
|
+
|
|
568
|
+
# Create new PersistentVenvWorker
|
|
569
|
+
from .workers.venv import PersistentVenvWorker
|
|
570
|
+
|
|
571
|
+
worker = PersistentVenvWorker(
|
|
572
|
+
python=str(python_path),
|
|
573
|
+
working_dir=node_dir,
|
|
574
|
+
sys_path=[str(node_dir)],
|
|
575
|
+
name=f"auto-{env_name}",
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
_auto_isolate_workers[cache_key] = worker
|
|
579
|
+
return worker
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def auto_isolate(func: Callable) -> Callable:
|
|
583
|
+
"""
|
|
584
|
+
Decorator that automatically runs a function in an isolated environment
|
|
585
|
+
when an ImportError occurs for a package that exists in the isolated env.
|
|
586
|
+
|
|
587
|
+
This provides seamless isolation - just write normal code with imports,
|
|
588
|
+
and if the import fails in the host environment but the package is
|
|
589
|
+
configured in comfy-env.toml, the function automatically retries in
|
|
590
|
+
the isolated environment.
|
|
591
|
+
|
|
592
|
+
Example:
|
|
593
|
+
from comfy_env import auto_isolate
|
|
594
|
+
|
|
595
|
+
@auto_isolate
|
|
596
|
+
def process_with_cumesh(mesh, target_faces):
|
|
597
|
+
import cumesh # If this fails, function retries in isolated env
|
|
598
|
+
import torch
|
|
599
|
+
|
|
600
|
+
v = torch.tensor(mesh.vertices).cuda()
|
|
601
|
+
f = torch.tensor(mesh.faces).cuda()
|
|
602
|
+
|
|
603
|
+
cm = cumesh.CuMesh()
|
|
604
|
+
cm.init(v, f)
|
|
605
|
+
cm.simplify(target_faces)
|
|
606
|
+
|
|
607
|
+
result_v, result_f = cm.read()
|
|
608
|
+
return result_v.cpu().numpy(), result_f.cpu().numpy()
|
|
609
|
+
|
|
610
|
+
How it works:
|
|
611
|
+
1. Function runs normally in the host environment
|
|
612
|
+
2. If ImportError occurs, decorator catches it
|
|
613
|
+
3. Extracts the module name from the error (e.g., "cumesh")
|
|
614
|
+
4. Searches comfy-env.toml for which env has that module
|
|
615
|
+
5. Re-runs the entire function in that isolated environment
|
|
616
|
+
6. Returns the result as if nothing happened
|
|
617
|
+
|
|
618
|
+
Benefits:
|
|
619
|
+
- Zero overhead when imports succeed (fast path)
|
|
620
|
+
- Auto-detects which environment to use from the failed import
|
|
621
|
+
- Function is the isolation boundary (clean, debuggable)
|
|
622
|
+
- Works with any import pattern (top of function, conditional, etc.)
|
|
623
|
+
|
|
624
|
+
Note:
|
|
625
|
+
Arguments and return values are serialized via torch.save/load,
|
|
626
|
+
so they should be tensors, numpy arrays, or pickle-able objects.
|
|
627
|
+
"""
|
|
628
|
+
# Get source file for environment detection
|
|
629
|
+
source_file = Path(inspect.getfile(func))
|
|
630
|
+
|
|
631
|
+
@wraps(func)
|
|
632
|
+
def wrapper(*args, **kwargs):
|
|
633
|
+
try:
|
|
634
|
+
# Fast path: try running in host environment
|
|
635
|
+
return func(*args, **kwargs)
|
|
636
|
+
|
|
637
|
+
except ImportError as e:
|
|
638
|
+
# Extract module name from error
|
|
639
|
+
module_name = _parse_import_error(e)
|
|
640
|
+
if module_name is None:
|
|
641
|
+
# Can't determine module, re-raise
|
|
642
|
+
raise
|
|
643
|
+
|
|
644
|
+
# Find which env has this module
|
|
645
|
+
env_info = _find_env_for_module(module_name, source_file)
|
|
646
|
+
if env_info is None:
|
|
647
|
+
# Module not in any known isolated env, re-raise
|
|
648
|
+
raise
|
|
649
|
+
|
|
650
|
+
env_name, python_path, node_dir = env_info
|
|
651
|
+
|
|
652
|
+
_log(env_name, f"Import '{module_name}' failed in host, retrying in isolated env...")
|
|
653
|
+
_log(env_name, f" Python: {python_path}")
|
|
654
|
+
|
|
655
|
+
# Get or create worker
|
|
656
|
+
worker = _get_auto_isolate_worker(env_name, python_path, node_dir)
|
|
657
|
+
|
|
658
|
+
# Prepare arguments - convert numpy arrays to lists for IPC
|
|
659
|
+
import numpy as np
|
|
660
|
+
|
|
661
|
+
def convert_for_ipc(obj):
|
|
662
|
+
if isinstance(obj, np.ndarray):
|
|
663
|
+
return obj.tolist()
|
|
664
|
+
elif hasattr(obj, 'vertices') and hasattr(obj, 'faces'):
|
|
665
|
+
# Trimesh-like object - convert to dict
|
|
666
|
+
return {
|
|
667
|
+
'__trimesh__': True,
|
|
668
|
+
'vertices': obj.vertices.tolist() if hasattr(obj.vertices, 'tolist') else list(obj.vertices),
|
|
669
|
+
'faces': obj.faces.tolist() if hasattr(obj.faces, 'tolist') else list(obj.faces),
|
|
670
|
+
}
|
|
671
|
+
elif isinstance(obj, (list, tuple)):
|
|
672
|
+
converted = [convert_for_ipc(x) for x in obj]
|
|
673
|
+
return type(obj)(converted) if isinstance(obj, tuple) else converted
|
|
674
|
+
elif isinstance(obj, dict):
|
|
675
|
+
return {k: convert_for_ipc(v) for k, v in obj.items()}
|
|
676
|
+
return obj
|
|
677
|
+
|
|
678
|
+
converted_args = [convert_for_ipc(arg) for arg in args]
|
|
679
|
+
converted_kwargs = {k: convert_for_ipc(v) for k, v in kwargs.items()}
|
|
680
|
+
|
|
681
|
+
# Call via worker
|
|
682
|
+
start_time = time.time()
|
|
683
|
+
|
|
684
|
+
result = worker.call_module(
|
|
685
|
+
module=source_file.stem,
|
|
686
|
+
func=func.__name__,
|
|
687
|
+
*converted_args,
|
|
688
|
+
**converted_kwargs,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
elapsed = time.time() - start_time
|
|
692
|
+
_log(env_name, f"← {func.__name__} completed in isolated env [{elapsed:.2f}s]")
|
|
693
|
+
|
|
694
|
+
return result
|
|
695
|
+
|
|
696
|
+
# Mark the function as auto-isolate enabled
|
|
697
|
+
wrapper._auto_isolate = True
|
|
698
|
+
wrapper._source_file = source_file
|
|
699
|
+
|
|
700
|
+
return wrapper
|
comfy_env/env/config.py
CHANGED
|
@@ -154,6 +154,8 @@ class IsolatedEnv:
|
|
|
154
154
|
worker_script: Optional[str] = None # e.g., "worker.py" -> worker.py
|
|
155
155
|
# Conda configuration (uses pixi backend when present)
|
|
156
156
|
conda: Optional["CondaConfig"] = None
|
|
157
|
+
# Runtime isolation - run node FUNCTION methods in isolated subprocess
|
|
158
|
+
isolated: bool = False
|
|
157
159
|
|
|
158
160
|
def __post_init__(self):
|
|
159
161
|
"""Validate and normalize configuration."""
|
comfy_env/env/config_file.py
CHANGED
|
@@ -572,6 +572,9 @@ def _parse_single_env(name: str, env_data: Dict[str, Any], base_dir: Path) -> Is
|
|
|
572
572
|
elif isinstance(darwin_section, list):
|
|
573
573
|
darwin_reqs = darwin_section
|
|
574
574
|
|
|
575
|
+
# Parse isolated flag for runtime process isolation
|
|
576
|
+
isolated = env_data.get("isolated", False)
|
|
577
|
+
|
|
575
578
|
return IsolatedEnv(
|
|
576
579
|
name=name,
|
|
577
580
|
python=python,
|
|
@@ -583,6 +586,7 @@ def _parse_single_env(name: str, env_data: Dict[str, Any], base_dir: Path) -> Is
|
|
|
583
586
|
linux_requirements=linux_reqs,
|
|
584
587
|
darwin_requirements=darwin_reqs,
|
|
585
588
|
conda=conda_config,
|
|
589
|
+
isolated=isolated,
|
|
586
590
|
)
|
|
587
591
|
|
|
588
592
|
|
comfy_env/install.py
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Installation API for comfy-env.
|
|
3
3
|
|
|
4
|
-
This module provides the main `install()` function that handles
|
|
5
|
-
-
|
|
6
|
-
-
|
|
4
|
+
This module provides the main `install()` function that handles:
|
|
5
|
+
- Named environments [envname] → pixi (isolated Python environment)
|
|
6
|
+
- Local packages [local] → uv/pip (in-place to current Python)
|
|
7
7
|
|
|
8
8
|
Example:
|
|
9
9
|
from comfy_env import install
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# Auto-discovers config and installs
|
|
12
12
|
install()
|
|
13
13
|
|
|
14
|
-
#
|
|
14
|
+
# With explicit config path
|
|
15
15
|
install(config="comfy-env.toml")
|
|
16
|
-
|
|
17
|
-
# Isolated environment
|
|
18
|
-
install(config="comfy-env.toml", mode="isolated")
|
|
19
16
|
"""
|
|
20
17
|
|
|
21
18
|
import inspect
|
|
@@ -27,7 +24,6 @@ from typing import Callable, Dict, List, Optional, Set, Union
|
|
|
27
24
|
|
|
28
25
|
from .env.config import IsolatedEnv, LocalConfig, NodeReq, SystemConfig
|
|
29
26
|
from .env.config_file import load_config, discover_config
|
|
30
|
-
from .env.manager import IsolatedEnvManager
|
|
31
27
|
from .errors import CUDANotFoundError, InstallError
|
|
32
28
|
from .pixi import pixi_install
|
|
33
29
|
from .registry import PACKAGE_REGISTRY, get_cuda_short2
|
|
@@ -139,31 +135,36 @@ def _install_node_dependencies(
|
|
|
139
135
|
|
|
140
136
|
|
|
141
137
|
def install(
|
|
138
|
+
config: Optional[Union[str, Path]] = None,
|
|
139
|
+
node_dir: Optional[Path] = None,
|
|
142
140
|
log_callback: Optional[Callable[[str], None]] = None,
|
|
143
141
|
dry_run: bool = False,
|
|
144
142
|
) -> bool:
|
|
145
143
|
"""
|
|
146
|
-
Install dependencies from comfy-env.toml
|
|
144
|
+
Install dependencies from comfy-env.toml.
|
|
147
145
|
|
|
148
146
|
Example:
|
|
149
147
|
from comfy_env import install
|
|
150
148
|
install()
|
|
151
149
|
|
|
152
150
|
Args:
|
|
151
|
+
config: Optional path to comfy-env.toml. Auto-discovered if not provided.
|
|
152
|
+
node_dir: Optional node directory. Auto-discovered from caller if not provided.
|
|
153
153
|
log_callback: Optional callback for logging. Defaults to print.
|
|
154
154
|
dry_run: If True, show what would be installed without installing.
|
|
155
155
|
|
|
156
156
|
Returns:
|
|
157
157
|
True if installation succeeded.
|
|
158
158
|
"""
|
|
159
|
-
# Auto-discover caller's directory
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
# Auto-discover caller's directory if not provided
|
|
160
|
+
if node_dir is None:
|
|
161
|
+
frame = inspect.stack()[1]
|
|
162
|
+
caller_file = frame.filename
|
|
163
|
+
node_dir = Path(caller_file).parent.resolve()
|
|
163
164
|
|
|
164
165
|
log = log_callback or print
|
|
165
166
|
|
|
166
|
-
full_config = _load_full_config(
|
|
167
|
+
full_config = _load_full_config(config, node_dir)
|
|
167
168
|
if full_config is None:
|
|
168
169
|
raise FileNotFoundError(
|
|
169
170
|
f"No comfy-env.toml found in {node_dir}. "
|
|
@@ -177,28 +178,36 @@ def install(
|
|
|
177
178
|
_install_system_packages(full_config.system, log, dry_run)
|
|
178
179
|
|
|
179
180
|
env_config = full_config.default_env
|
|
180
|
-
if env_config is None and not full_config.has_local:
|
|
181
|
-
log("No packages to install")
|
|
182
|
-
return True
|
|
183
|
-
|
|
184
|
-
if env_config:
|
|
185
|
-
log(f"Found configuration: {env_config.name}")
|
|
186
|
-
|
|
187
|
-
if env_config and env_config.uses_conda:
|
|
188
|
-
log("Environment uses conda packages - using pixi backend")
|
|
189
|
-
return pixi_install(env_config, node_dir, log, dry_run)
|
|
190
181
|
|
|
191
182
|
# Get user wheel_sources overrides
|
|
192
183
|
user_wheel_sources = full_config.wheel_sources if hasattr(full_config, 'wheel_sources') else {}
|
|
193
184
|
|
|
194
185
|
if env_config:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
186
|
+
# Named environment → always pixi
|
|
187
|
+
log(f"Found environment: {env_config.name}")
|
|
188
|
+
python_ver = env_config.python or "3.11" # Default to 3.11 if not specified
|
|
189
|
+
if not env_config.python:
|
|
190
|
+
log(f" No Python version specified, defaulting to {python_ver}")
|
|
191
|
+
env_config = IsolatedEnv(
|
|
192
|
+
name=env_config.name,
|
|
193
|
+
python=python_ver,
|
|
194
|
+
cuda=env_config.cuda,
|
|
195
|
+
pytorch=env_config.pytorch,
|
|
196
|
+
requirements=env_config.requirements,
|
|
197
|
+
no_deps_requirements=env_config.no_deps_requirements,
|
|
198
|
+
linux_requirements=env_config.linux_requirements,
|
|
199
|
+
darwin_requirements=env_config.darwin_requirements,
|
|
200
|
+
windows_requirements=env_config.windows_requirements,
|
|
201
|
+
conda=env_config.conda,
|
|
202
|
+
isolated=env_config.isolated,
|
|
203
|
+
)
|
|
204
|
+
log(f" Using pixi backend (Python {python_ver})")
|
|
205
|
+
return pixi_install(env_config, node_dir, log, dry_run)
|
|
199
206
|
elif full_config.has_local:
|
|
207
|
+
# [local] section → uv in-place install
|
|
200
208
|
return _install_local(full_config.local, node_dir, log, dry_run, user_wheel_sources)
|
|
201
209
|
else:
|
|
210
|
+
log("No packages to install")
|
|
202
211
|
return True
|
|
203
212
|
|
|
204
213
|
|
|
@@ -212,81 +221,6 @@ def _load_full_config(config: Optional[Union[str, Path]], node_dir: Path):
|
|
|
212
221
|
return discover_config(node_dir)
|
|
213
222
|
|
|
214
223
|
|
|
215
|
-
def _install_isolated(
|
|
216
|
-
env_config: IsolatedEnv,
|
|
217
|
-
node_dir: Path,
|
|
218
|
-
log: Callable[[str], None],
|
|
219
|
-
dry_run: bool,
|
|
220
|
-
) -> bool:
|
|
221
|
-
"""Install in isolated mode using IsolatedEnvManager."""
|
|
222
|
-
log(f"Installing in isolated mode: {env_config.name}")
|
|
223
|
-
|
|
224
|
-
if dry_run:
|
|
225
|
-
log("Dry run - would create isolated environment:")
|
|
226
|
-
log(f" Python: {env_config.python}")
|
|
227
|
-
log(f" CUDA: {env_config.cuda or 'auto-detect'}")
|
|
228
|
-
if env_config.requirements:
|
|
229
|
-
log(f" Requirements: {len(env_config.requirements)} packages")
|
|
230
|
-
return True
|
|
231
|
-
|
|
232
|
-
manager = IsolatedEnvManager(base_dir=node_dir, log_callback=log)
|
|
233
|
-
env_dir = manager.setup(env_config)
|
|
234
|
-
log(f"Isolated environment ready: {env_dir}")
|
|
235
|
-
return True
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def _install_inplace(
|
|
239
|
-
env_config: IsolatedEnv,
|
|
240
|
-
node_dir: Path,
|
|
241
|
-
log: Callable[[str], None],
|
|
242
|
-
dry_run: bool,
|
|
243
|
-
user_wheel_sources: Dict[str, str],
|
|
244
|
-
) -> bool:
|
|
245
|
-
"""Install in-place into current environment."""
|
|
246
|
-
log("Installing in-place mode")
|
|
247
|
-
|
|
248
|
-
if sys.platform == "win32":
|
|
249
|
-
log("Installing MSVC runtime for Windows...")
|
|
250
|
-
if not dry_run:
|
|
251
|
-
_pip_install(["msvc-runtime"], no_deps=False, log=log)
|
|
252
|
-
|
|
253
|
-
env = RuntimeEnv.detect()
|
|
254
|
-
log(f"Detected environment: {env}")
|
|
255
|
-
|
|
256
|
-
if not env.cuda_version:
|
|
257
|
-
cuda_packages = env_config.no_deps_requirements or []
|
|
258
|
-
if cuda_packages:
|
|
259
|
-
raise CUDANotFoundError(package=", ".join(cuda_packages))
|
|
260
|
-
|
|
261
|
-
cuda_packages = env_config.no_deps_requirements or []
|
|
262
|
-
regular_packages = env_config.requirements or []
|
|
263
|
-
|
|
264
|
-
if dry_run:
|
|
265
|
-
log("\nDry run - would install:")
|
|
266
|
-
for req in cuda_packages:
|
|
267
|
-
package, version = parse_wheel_requirement(req)
|
|
268
|
-
url = _resolve_wheel_url(package, version, env, user_wheel_sources)
|
|
269
|
-
log(f" {package}: {url[:80]}...")
|
|
270
|
-
if regular_packages:
|
|
271
|
-
log(" Regular packages:")
|
|
272
|
-
for pkg in regular_packages:
|
|
273
|
-
log(f" {pkg}")
|
|
274
|
-
return True
|
|
275
|
-
|
|
276
|
-
if cuda_packages:
|
|
277
|
-
log(f"\nInstalling {len(cuda_packages)} CUDA packages...")
|
|
278
|
-
for req in cuda_packages:
|
|
279
|
-
package, version = parse_wheel_requirement(req)
|
|
280
|
-
_install_cuda_package(package, version, env, user_wheel_sources, log)
|
|
281
|
-
|
|
282
|
-
if regular_packages:
|
|
283
|
-
log(f"\nInstalling {len(regular_packages)} regular packages...")
|
|
284
|
-
_pip_install(regular_packages, no_deps=False, log=log)
|
|
285
|
-
|
|
286
|
-
log("\nInstallation complete!")
|
|
287
|
-
return True
|
|
288
|
-
|
|
289
|
-
|
|
290
224
|
def _install_local(
|
|
291
225
|
local_config: LocalConfig,
|
|
292
226
|
node_dir: Path,
|