comfy-env 0.0.18__tar.gz → 0.0.19__tar.gz
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-0.0.18 → comfy_env-0.0.19}/PKG-INFO +1 -1
- {comfy_env-0.0.18 → comfy_env-0.0.19}/pyproject.toml +1 -1
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/config.py +19 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/config_file.py +36 -3
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/install.py +104 -12
- comfy_env-0.0.18/examples/basic_node/__init__.py +0 -5
- comfy_env-0.0.18/examples/basic_node/comfy-env.toml +0 -65
- comfy_env-0.0.18/examples/basic_node/nodes.py +0 -157
- comfy_env-0.0.18/examples/basic_node/worker.py +0 -79
- comfy_env-0.0.18/examples/decorator_node/__init__.py +0 -9
- comfy_env-0.0.18/examples/decorator_node/nodes.py +0 -182
- comfy_env-0.0.18/src/comfy_env/tools.py +0 -221
- {comfy_env-0.0.18 → comfy_env-0.0.19}/.github/workflows/publish.yml +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/.gitignore +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/LICENSE +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/README.md +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/__init__.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/cli.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/decorator.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/__init__.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/cuda_gpu_detection.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/manager.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/platform/__init__.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/platform/base.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/platform/darwin.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/platform/linux.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/platform/windows.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/env/security.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/errors.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/__init__.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/bridge.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/protocol.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/tensor.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/torch_bridge.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/transport.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/ipc/worker.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/registry.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/resolver.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/stubs/__init__.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/stubs/folder_paths.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/wheel_sources.yml +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/__init__.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/base.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/pool.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/tensor_utils.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/torch_mp.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/src/comfy_env/workers/venv.py +0 -0
- {comfy_env-0.0.18 → comfy_env-0.0.19}/untitled.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comfy-env
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.19
|
|
4
4
|
Summary: Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation
|
|
5
5
|
Project-URL: Homepage, https://github.com/PozzettiAndrea/comfy-env
|
|
6
6
|
Project-URL: Repository, https://github.com/PozzettiAndrea/comfy-env
|
|
@@ -5,6 +5,18 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Dict, List, Optional
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
@dataclass
|
|
9
|
+
class SystemConfig:
|
|
10
|
+
"""Configuration for system-level packages.
|
|
11
|
+
|
|
12
|
+
These are OS-level packages (apt, brew, etc.) that need to be installed
|
|
13
|
+
before Python packages can work properly.
|
|
14
|
+
"""
|
|
15
|
+
linux: List[str] = field(default_factory=list) # apt packages
|
|
16
|
+
darwin: List[str] = field(default_factory=list) # brew packages (future)
|
|
17
|
+
windows: List[str] = field(default_factory=list) # winget packages (future)
|
|
18
|
+
|
|
19
|
+
|
|
8
20
|
@dataclass
|
|
9
21
|
class LocalConfig:
|
|
10
22
|
"""Configuration for local (host environment) installs.
|
|
@@ -37,6 +49,7 @@ class EnvManagerConfig:
|
|
|
37
49
|
Full configuration parsed from comfy-env.toml.
|
|
38
50
|
|
|
39
51
|
Supports the v2 schema:
|
|
52
|
+
[system] - System-level packages (apt, brew, etc.)
|
|
40
53
|
[local.cuda] - CUDA packages for host environment
|
|
41
54
|
[local.packages] - Regular packages for host environment
|
|
42
55
|
[envname] - Isolated env definition
|
|
@@ -45,11 +58,17 @@ class EnvManagerConfig:
|
|
|
45
58
|
[node_reqs] - Node dependencies
|
|
46
59
|
[tools] - External tools (e.g., blender = "4.2")
|
|
47
60
|
"""
|
|
61
|
+
system: SystemConfig = field(default_factory=SystemConfig)
|
|
48
62
|
local: LocalConfig = field(default_factory=LocalConfig)
|
|
49
63
|
envs: Dict[str, "IsolatedEnv"] = field(default_factory=dict)
|
|
50
64
|
node_reqs: List[NodeReq] = field(default_factory=list)
|
|
51
65
|
tools: Dict[str, ToolConfig] = field(default_factory=dict)
|
|
52
66
|
|
|
67
|
+
@property
|
|
68
|
+
def has_system(self) -> bool:
|
|
69
|
+
"""Check if there are system packages to install."""
|
|
70
|
+
return bool(self.system.linux or self.system.darwin or self.system.windows)
|
|
71
|
+
|
|
53
72
|
@property
|
|
54
73
|
def has_local(self) -> bool:
|
|
55
74
|
"""Check if there are local packages to install."""
|
|
@@ -63,7 +63,7 @@ else:
|
|
|
63
63
|
except ImportError:
|
|
64
64
|
tomllib = None # type: ignore
|
|
65
65
|
|
|
66
|
-
from .config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq, ToolConfig
|
|
66
|
+
from .config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq, SystemConfig, ToolConfig
|
|
67
67
|
from .cuda_gpu_detection import detect_cuda_version
|
|
68
68
|
|
|
69
69
|
|
|
@@ -332,7 +332,7 @@ def _substitute_vars(s: str, variables: Dict[str, str]) -> str:
|
|
|
332
332
|
# =============================================================================
|
|
333
333
|
|
|
334
334
|
# Reserved table names that are NOT isolated environments
|
|
335
|
-
RESERVED_TABLES = {"local", "node_reqs", "env", "packages", "sources", "cuda", "variables", "worker", "tools"}
|
|
335
|
+
RESERVED_TABLES = {"local", "node_reqs", "env", "packages", "sources", "cuda", "variables", "worker", "tools", "system"}
|
|
336
336
|
|
|
337
337
|
|
|
338
338
|
def load_config(
|
|
@@ -426,12 +426,14 @@ def _parse_full_config(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig
|
|
|
426
426
|
return _convert_simple_to_full(data, base_dir)
|
|
427
427
|
|
|
428
428
|
# Parse full schema
|
|
429
|
+
system = _parse_system_section(data.get("system", {}))
|
|
429
430
|
local = _parse_local_section(data.get("local", {}))
|
|
430
431
|
envs = _parse_env_sections(data, base_dir)
|
|
431
432
|
node_reqs = _parse_node_reqs(data.get("node_reqs", {}))
|
|
432
433
|
tools = _parse_tools_section(data.get("tools", {}))
|
|
433
434
|
|
|
434
435
|
return EnvManagerConfig(
|
|
436
|
+
system=system,
|
|
435
437
|
local=local,
|
|
436
438
|
envs=envs,
|
|
437
439
|
node_reqs=node_reqs,
|
|
@@ -613,6 +615,34 @@ def _parse_tools_section(tools_data: Dict[str, Any]) -> Dict[str, ToolConfig]:
|
|
|
613
615
|
return tools
|
|
614
616
|
|
|
615
617
|
|
|
618
|
+
def _parse_system_section(system_data: Dict[str, Any]) -> SystemConfig:
|
|
619
|
+
"""Parse [system] section.
|
|
620
|
+
|
|
621
|
+
Supports:
|
|
622
|
+
[system]
|
|
623
|
+
linux = ["libgl1", "libopengl0"]
|
|
624
|
+
darwin = ["mesa"] # future
|
|
625
|
+
windows = ["vcredist"] # future
|
|
626
|
+
"""
|
|
627
|
+
linux = system_data.get("linux", [])
|
|
628
|
+
darwin = system_data.get("darwin", [])
|
|
629
|
+
windows = system_data.get("windows", [])
|
|
630
|
+
|
|
631
|
+
# Ensure all are lists
|
|
632
|
+
if not isinstance(linux, list):
|
|
633
|
+
linux = [linux] if linux else []
|
|
634
|
+
if not isinstance(darwin, list):
|
|
635
|
+
darwin = [darwin] if darwin else []
|
|
636
|
+
if not isinstance(windows, list):
|
|
637
|
+
windows = [windows] if windows else []
|
|
638
|
+
|
|
639
|
+
return SystemConfig(
|
|
640
|
+
linux=linux,
|
|
641
|
+
darwin=darwin,
|
|
642
|
+
windows=windows,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
616
646
|
def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
|
|
617
647
|
"""Convert simple config format to full EnvManagerConfig.
|
|
618
648
|
|
|
@@ -622,8 +652,9 @@ def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerC
|
|
|
622
652
|
# Parse using simple parser to get IsolatedEnv
|
|
623
653
|
simple_env = _parse_config(data, base_dir)
|
|
624
654
|
|
|
625
|
-
# Parse tools
|
|
655
|
+
# Parse tools and system sections (shared between simple and full format)
|
|
626
656
|
tools = _parse_tools_section(data.get("tools", {}))
|
|
657
|
+
system = _parse_system_section(data.get("system", {}))
|
|
627
658
|
|
|
628
659
|
# Check if this has explicit env settings (isolated venv) vs just CUDA packages (local install)
|
|
629
660
|
env_section = data.get("env", {})
|
|
@@ -632,6 +663,7 @@ def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerC
|
|
|
632
663
|
if has_explicit_env:
|
|
633
664
|
# Isolated venv config
|
|
634
665
|
return EnvManagerConfig(
|
|
666
|
+
system=system,
|
|
635
667
|
local=LocalConfig(),
|
|
636
668
|
envs={simple_env.name: simple_env},
|
|
637
669
|
node_reqs=[],
|
|
@@ -648,6 +680,7 @@ def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerC
|
|
|
648
680
|
cuda_packages[req] = ""
|
|
649
681
|
|
|
650
682
|
return EnvManagerConfig(
|
|
683
|
+
system=system,
|
|
651
684
|
local=LocalConfig(
|
|
652
685
|
cuda_packages=cuda_packages,
|
|
653
686
|
requirements=simple_env.requirements,
|
|
@@ -24,13 +24,111 @@ import sys
|
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
26
26
|
|
|
27
|
-
from .env.config import IsolatedEnv,
|
|
27
|
+
from .env.config import IsolatedEnv, SystemConfig
|
|
28
28
|
from .env.config_file import discover_env_config, load_env_from_file, load_config, discover_config
|
|
29
29
|
from .env.manager import IsolatedEnvManager
|
|
30
30
|
from .errors import CUDANotFoundError, DependencyError, InstallError, WheelNotFoundError
|
|
31
31
|
from .registry import PACKAGE_REGISTRY, get_cuda_short2, is_registered
|
|
32
32
|
from .resolver import RuntimeEnv, WheelResolver, parse_wheel_requirement
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _install_system_packages(
|
|
36
|
+
system_config: SystemConfig,
|
|
37
|
+
log: Callable[[str], None],
|
|
38
|
+
dry_run: bool = False,
|
|
39
|
+
) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Install system-level packages (apt, brew, etc.).
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
system_config: SystemConfig with package lists per OS.
|
|
45
|
+
log: Logging callback.
|
|
46
|
+
dry_run: If True, show what would be installed without installing.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True if installation succeeded or no packages needed.
|
|
50
|
+
"""
|
|
51
|
+
platform = sys.platform
|
|
52
|
+
|
|
53
|
+
if platform.startswith("linux"):
|
|
54
|
+
packages = system_config.linux
|
|
55
|
+
if not packages:
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
log(f"Installing {len(packages)} system package(s) via apt...")
|
|
59
|
+
|
|
60
|
+
if dry_run:
|
|
61
|
+
log(f" Would install: {', '.join(packages)}")
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
# Check if apt-get is available
|
|
65
|
+
if not shutil.which("apt-get"):
|
|
66
|
+
log(" Warning: apt-get not found. Cannot install system packages.")
|
|
67
|
+
log(f" Please install manually: {', '.join(packages)}")
|
|
68
|
+
return True # Don't fail - just warn
|
|
69
|
+
|
|
70
|
+
# Check if we can use sudo
|
|
71
|
+
sudo_available = shutil.which("sudo") is not None
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
if sudo_available:
|
|
75
|
+
# Try with sudo
|
|
76
|
+
log(" Running apt-get update...")
|
|
77
|
+
update_result = subprocess.run(
|
|
78
|
+
["sudo", "apt-get", "update"],
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
)
|
|
82
|
+
if update_result.returncode != 0:
|
|
83
|
+
log(f" Warning: apt-get update failed: {update_result.stderr.strip()}")
|
|
84
|
+
# Continue anyway - packages might already be cached
|
|
85
|
+
|
|
86
|
+
log(f" Installing: {', '.join(packages)}")
|
|
87
|
+
install_cmd = ["sudo", "apt-get", "install", "-y"] + packages
|
|
88
|
+
install_result = subprocess.run(
|
|
89
|
+
install_cmd,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if install_result.returncode != 0:
|
|
95
|
+
log(f" Warning: apt-get install failed: {install_result.stderr.strip()}")
|
|
96
|
+
log(f" Please install manually: sudo apt-get install {' '.join(packages)}")
|
|
97
|
+
return True # Don't fail - just warn
|
|
98
|
+
else:
|
|
99
|
+
log(" System packages installed successfully.")
|
|
100
|
+
return True
|
|
101
|
+
else:
|
|
102
|
+
log(" Warning: sudo not available. Cannot install system packages automatically.")
|
|
103
|
+
log(f" Please install manually: sudo apt-get install {' '.join(packages)}")
|
|
104
|
+
return True # Don't fail - just warn
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
log(f" Warning: Failed to install system packages: {e}")
|
|
108
|
+
log(f" Please install manually: sudo apt-get install {' '.join(packages)}")
|
|
109
|
+
return True # Don't fail - just warn
|
|
110
|
+
|
|
111
|
+
elif platform == "darwin":
|
|
112
|
+
packages = system_config.darwin
|
|
113
|
+
if not packages:
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
log(f"System packages for macOS: {', '.join(packages)}")
|
|
117
|
+
log(" Note: macOS system package installation not yet implemented.")
|
|
118
|
+
log(f" Please install manually with Homebrew: brew install {' '.join(packages)}")
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
elif platform == "win32":
|
|
122
|
+
packages = system_config.windows
|
|
123
|
+
if not packages:
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
log(f"System packages for Windows: {', '.join(packages)}")
|
|
127
|
+
log(" Note: Windows system package installation not yet implemented.")
|
|
128
|
+
log(f" Please install manually.")
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
return True
|
|
34
132
|
|
|
35
133
|
|
|
36
134
|
def install(
|
|
@@ -86,16 +184,10 @@ def install(
|
|
|
86
184
|
"Create comfy-env.toml or specify path explicitly."
|
|
87
185
|
)
|
|
88
186
|
|
|
89
|
-
# Install
|
|
90
|
-
#
|
|
91
|
-
if full_config.
|
|
92
|
-
|
|
93
|
-
comfyui_root = node_dir.parent.parent # custom_nodes/../.. = ComfyUI/
|
|
94
|
-
for name, tool_config in full_config.tools.items():
|
|
95
|
-
if dry_run:
|
|
96
|
-
log(f" Would install {name} {tool_config.version}")
|
|
97
|
-
else:
|
|
98
|
-
install_tool(tool_config, log, comfyui_root)
|
|
187
|
+
# Install system packages first (apt, brew, etc.)
|
|
188
|
+
# These need to be installed before Python packages that depend on them
|
|
189
|
+
if full_config.has_system:
|
|
190
|
+
_install_system_packages(full_config.system, log, dry_run)
|
|
99
191
|
|
|
100
192
|
# Get environment config
|
|
101
193
|
env_config = full_config.default_env
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# Example isolation configuration for a ComfyUI custom node.
|
|
2
|
-
#
|
|
3
|
-
# This file defines the isolated Python environment for your node.
|
|
4
|
-
# The comfyui-isolation library will auto-discover this file.
|
|
5
|
-
#
|
|
6
|
-
# Usage in your node:
|
|
7
|
-
# bridge = WorkerBridge.from_config_file(
|
|
8
|
-
# node_dir=Path(__file__).parent,
|
|
9
|
-
# worker_script=Path(__file__).parent / "worker.py",
|
|
10
|
-
# )
|
|
11
|
-
|
|
12
|
-
[env]
|
|
13
|
-
# Unique name for this environment (used for caching)
|
|
14
|
-
name = "example-node"
|
|
15
|
-
|
|
16
|
-
# Python version
|
|
17
|
-
python = "3.10"
|
|
18
|
-
|
|
19
|
-
# CUDA version: "auto" (detect), "12.4", "12.8", or null (CPU only)
|
|
20
|
-
cuda = "auto"
|
|
21
|
-
|
|
22
|
-
# Optional: specific PyTorch version (if not specified, latest from index is used)
|
|
23
|
-
# pytorch_version = "2.4.1"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
[packages]
|
|
27
|
-
# Required packages
|
|
28
|
-
requirements = [
|
|
29
|
-
"torch",
|
|
30
|
-
"torchvision",
|
|
31
|
-
"pillow",
|
|
32
|
-
"numpy",
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
# Optional: path to additional requirements.txt (relative to this file's directory)
|
|
36
|
-
# requirements_file = "requirements.txt"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
[sources]
|
|
40
|
-
# Custom wheel repositories (passed as --find-links to pip)
|
|
41
|
-
# wheel_sources = [
|
|
42
|
-
# "https://my-custom-wheels.github.io/",
|
|
43
|
-
# ]
|
|
44
|
-
|
|
45
|
-
# Extra package indexes (passed as --extra-index-url to pip)
|
|
46
|
-
# index_urls = [
|
|
47
|
-
# "https://test.pypi.org/simple/",
|
|
48
|
-
# ]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
[variables]
|
|
52
|
-
# Define variables for substitution in requirements.
|
|
53
|
-
# You can reference these with {variable_name} syntax.
|
|
54
|
-
#
|
|
55
|
-
# Auto-populated variables (no need to define):
|
|
56
|
-
# {cuda_version} - e.g., "12.4"
|
|
57
|
-
# {cuda_short} - e.g., "124"
|
|
58
|
-
# {pytorch_version} - e.g., "2.9.1"
|
|
59
|
-
# {pytorch_short} - e.g., "291" (version without dots)
|
|
60
|
-
# {pytorch_mm} - e.g., "29" (major.minor without dot, for wheel names)
|
|
61
|
-
#
|
|
62
|
-
# Example:
|
|
63
|
-
# pytorch_version = "2.4.1"
|
|
64
|
-
# pytorch3d_version = "0.7.8+5043d15pt{pytorch_version}cu{cuda_short}"
|
|
65
|
-
# flex_gemm = "0.0.1+cu{cuda_short}torch{pytorch_mm}"
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Example ComfyUI node that uses process isolation.
|
|
3
|
-
|
|
4
|
-
This demonstrates how to use comfyui-isolation in a ComfyUI custom node.
|
|
5
|
-
|
|
6
|
-
Two approaches are shown:
|
|
7
|
-
1. Programmatic configuration (IsolatedEnv directly in code)
|
|
8
|
-
2. Declarative configuration (comfy_env_reqs.toml file)
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from comfy_env import IsolatedEnv, WorkerBridge, detect_cuda_version
|
|
13
|
-
|
|
14
|
-
# Get the node's root directory
|
|
15
|
-
NODE_ROOT = Path(__file__).parent
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# ===========================================================================
|
|
19
|
-
# APPROACH 1: Programmatic Configuration (traditional)
|
|
20
|
-
# ===========================================================================
|
|
21
|
-
# Define the isolated environment configuration in code
|
|
22
|
-
ENV_CONFIG = IsolatedEnv(
|
|
23
|
-
name="example-node",
|
|
24
|
-
python="3.10",
|
|
25
|
-
cuda=detect_cuda_version(), # Auto-detect (12.8 for Blackwell, 12.4 for others)
|
|
26
|
-
requirements=[
|
|
27
|
-
"torch",
|
|
28
|
-
"torchvision",
|
|
29
|
-
"pillow",
|
|
30
|
-
"numpy",
|
|
31
|
-
],
|
|
32
|
-
# Add custom wheel sources if needed:
|
|
33
|
-
# wheel_sources=["https://my-wheels.github.io/"],
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
# Create bridge singleton
|
|
37
|
-
_bridge = None
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def get_bridge() -> WorkerBridge:
|
|
41
|
-
"""Get or create the worker bridge singleton (programmatic approach)."""
|
|
42
|
-
global _bridge
|
|
43
|
-
if _bridge is None:
|
|
44
|
-
_bridge = WorkerBridge(
|
|
45
|
-
env=ENV_CONFIG,
|
|
46
|
-
worker_script=NODE_ROOT / "worker.py",
|
|
47
|
-
base_dir=NODE_ROOT,
|
|
48
|
-
)
|
|
49
|
-
return _bridge
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# ===========================================================================
|
|
53
|
-
# APPROACH 2: Declarative Configuration (recommended for new projects)
|
|
54
|
-
# ===========================================================================
|
|
55
|
-
# Instead of the above, you can use a comfy_env_reqs.toml file:
|
|
56
|
-
#
|
|
57
|
-
# def get_bridge() -> WorkerBridge:
|
|
58
|
-
# """Get or create the worker bridge singleton (config file approach)."""
|
|
59
|
-
# global _bridge
|
|
60
|
-
# if _bridge is None:
|
|
61
|
-
# _bridge = WorkerBridge.from_config_file(
|
|
62
|
-
# node_dir=NODE_ROOT,
|
|
63
|
-
# worker_script=NODE_ROOT / "worker.py",
|
|
64
|
-
# )
|
|
65
|
-
# return _bridge
|
|
66
|
-
#
|
|
67
|
-
# This auto-discovers comfy_env_reqs.toml in the node directory.
|
|
68
|
-
# See the example file in this directory for the format.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class IsolatedProcessorNode:
|
|
72
|
-
"""
|
|
73
|
-
Example node that processes images in an isolated environment.
|
|
74
|
-
|
|
75
|
-
This node runs inference in a separate Python process with its own
|
|
76
|
-
dependencies, avoiding conflicts with ComfyUI's environment.
|
|
77
|
-
"""
|
|
78
|
-
|
|
79
|
-
@classmethod
|
|
80
|
-
def INPUT_TYPES(cls):
|
|
81
|
-
return {
|
|
82
|
-
"required": {
|
|
83
|
-
"image": ("IMAGE",),
|
|
84
|
-
"scale": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10.0}),
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
RETURN_TYPES = ("IMAGE",)
|
|
89
|
-
FUNCTION = "process"
|
|
90
|
-
CATEGORY = "example/isolation"
|
|
91
|
-
|
|
92
|
-
def process(self, image, scale):
|
|
93
|
-
"""Process image in isolated environment."""
|
|
94
|
-
import torch
|
|
95
|
-
import numpy as np
|
|
96
|
-
from PIL import Image
|
|
97
|
-
|
|
98
|
-
bridge = get_bridge()
|
|
99
|
-
|
|
100
|
-
# Ensure environment is set up (first run only)
|
|
101
|
-
bridge.ensure_environment(verify_packages=["torch"])
|
|
102
|
-
|
|
103
|
-
# Convert ComfyUI image format to PIL
|
|
104
|
-
# ComfyUI uses (B, H, W, C) float32 tensors
|
|
105
|
-
img_np = (image[0].numpy() * 255).astype(np.uint8)
|
|
106
|
-
pil_image = Image.fromarray(img_np)
|
|
107
|
-
|
|
108
|
-
# Call worker method
|
|
109
|
-
result = bridge.call("process_image", image=pil_image, scale=scale)
|
|
110
|
-
|
|
111
|
-
# Convert back to ComfyUI format
|
|
112
|
-
result_np = np.array(result).astype(np.float32) / 255.0
|
|
113
|
-
result_tensor = torch.from_numpy(result_np).unsqueeze(0)
|
|
114
|
-
|
|
115
|
-
return (result_tensor,)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class SetupIsolatedEnvNode:
|
|
119
|
-
"""
|
|
120
|
-
Node to set up the isolated environment.
|
|
121
|
-
|
|
122
|
-
Run this once to create the environment before using other nodes.
|
|
123
|
-
"""
|
|
124
|
-
|
|
125
|
-
@classmethod
|
|
126
|
-
def INPUT_TYPES(cls):
|
|
127
|
-
return {
|
|
128
|
-
"required": {
|
|
129
|
-
"trigger": ("*",), # Any input to trigger
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
RETURN_TYPES = ("STRING",)
|
|
134
|
-
FUNCTION = "setup"
|
|
135
|
-
CATEGORY = "example/isolation"
|
|
136
|
-
|
|
137
|
-
def setup(self, trigger):
|
|
138
|
-
"""Set up the isolated environment."""
|
|
139
|
-
bridge = get_bridge()
|
|
140
|
-
|
|
141
|
-
try:
|
|
142
|
-
bridge.ensure_environment(verify_packages=["torch", "numpy"])
|
|
143
|
-
return (f"Environment ready: {bridge.python_exe}",)
|
|
144
|
-
except Exception as e:
|
|
145
|
-
return (f"Setup failed: {e}",)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
# Node class mappings for ComfyUI
|
|
149
|
-
NODE_CLASS_MAPPINGS = {
|
|
150
|
-
"IsolatedProcessorExample": IsolatedProcessorNode,
|
|
151
|
-
"SetupIsolatedEnvExample": SetupIsolatedEnvNode,
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
155
|
-
"IsolatedProcessorExample": "Isolated Processor (Example)",
|
|
156
|
-
"SetupIsolatedEnvExample": "Setup Isolated Env (Example)",
|
|
157
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Example worker script for isolated environment.
|
|
3
|
-
|
|
4
|
-
This runs in the isolated Python environment and handles inference requests.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from comfy_env import BaseWorker, register
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class ExampleWorker(BaseWorker):
|
|
11
|
-
"""Example worker that processes images."""
|
|
12
|
-
|
|
13
|
-
def setup(self):
|
|
14
|
-
"""Called once when worker starts - load models here."""
|
|
15
|
-
self.log("Loading model...")
|
|
16
|
-
|
|
17
|
-
# Import heavy dependencies only in the isolated env
|
|
18
|
-
import torch
|
|
19
|
-
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
20
|
-
self.log(f"Using device: {self.device}")
|
|
21
|
-
|
|
22
|
-
# In a real node, you'd load your model here:
|
|
23
|
-
# self.model = load_my_model().to(self.device)
|
|
24
|
-
|
|
25
|
-
self.log("Model loaded!")
|
|
26
|
-
|
|
27
|
-
@register("echo")
|
|
28
|
-
def echo(self, message: str):
|
|
29
|
-
"""Simple echo for testing."""
|
|
30
|
-
return f"Echo: {message}"
|
|
31
|
-
|
|
32
|
-
@register("process_image")
|
|
33
|
-
def process_image(self, image, scale: float = 1.0):
|
|
34
|
-
"""
|
|
35
|
-
Process an image.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
image: PIL Image or numpy array
|
|
39
|
-
scale: Scale factor
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
Processed image
|
|
43
|
-
"""
|
|
44
|
-
import numpy as np
|
|
45
|
-
from PIL import Image
|
|
46
|
-
|
|
47
|
-
self.log(f"Processing image with scale={scale}")
|
|
48
|
-
|
|
49
|
-
# Convert to numpy if needed
|
|
50
|
-
if hasattr(image, 'save'): # PIL Image
|
|
51
|
-
arr = np.array(image)
|
|
52
|
-
else:
|
|
53
|
-
arr = image
|
|
54
|
-
|
|
55
|
-
# Example processing: simple brightness adjustment
|
|
56
|
-
result = np.clip(arr * scale, 0, 255).astype(np.uint8)
|
|
57
|
-
|
|
58
|
-
self.log("Processing complete")
|
|
59
|
-
return Image.fromarray(result)
|
|
60
|
-
|
|
61
|
-
@register("gpu_info")
|
|
62
|
-
def get_gpu_info(self):
|
|
63
|
-
"""Get GPU information from the isolated environment."""
|
|
64
|
-
import torch
|
|
65
|
-
|
|
66
|
-
if not torch.cuda.is_available():
|
|
67
|
-
return {"available": False}
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
"available": True,
|
|
71
|
-
"device_count": torch.cuda.device_count(),
|
|
72
|
-
"device_name": torch.cuda.get_device_name(0),
|
|
73
|
-
"memory_allocated": torch.cuda.memory_allocated(0),
|
|
74
|
-
"memory_cached": torch.cuda.memory_reserved(0),
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if __name__ == "__main__":
|
|
79
|
-
ExampleWorker().run()
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Example ComfyUI nodes using the @isolated decorator.
|
|
3
|
-
|
|
4
|
-
This demonstrates the simplest way to run node methods in isolated subprocess.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
|
8
|
-
|
|
9
|
-
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Example ComfyUI nodes using the @isolated decorator.
|
|
3
|
-
|
|
4
|
-
This demonstrates the simplest possible way to run node methods in
|
|
5
|
-
isolated subprocess environments. No separate worker file needed!
|
|
6
|
-
|
|
7
|
-
The @isolated decorator:
|
|
8
|
-
1. Extracts your method source code at class decoration time
|
|
9
|
-
2. Auto-generates a worker file on first call
|
|
10
|
-
3. Creates an isolated venv with your requirements
|
|
11
|
-
4. Intercepts method calls and forwards to the subprocess
|
|
12
|
-
5. Handles all serialization/deserialization automatically
|
|
13
|
-
|
|
14
|
-
Compare this to examples/basic_node which uses the traditional approach
|
|
15
|
-
with separate worker.py and explicit bridge management.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from comfy_env import isolated
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# ===========================================================================
|
|
22
|
-
# EXAMPLE 1: Simple inline requirements
|
|
23
|
-
# ===========================================================================
|
|
24
|
-
|
|
25
|
-
@isolated(
|
|
26
|
-
env="decorator-example",
|
|
27
|
-
requirements=["torch", "pillow", "numpy"],
|
|
28
|
-
python="3.10",
|
|
29
|
-
cuda="auto", # Auto-detect GPU (12.8 for Blackwell, 12.4 for others)
|
|
30
|
-
)
|
|
31
|
-
class SimpleIsolatedNode:
|
|
32
|
-
"""
|
|
33
|
-
Simplest possible isolated node - just add @isolated decorator!
|
|
34
|
-
|
|
35
|
-
The process() method body runs in a separate Python subprocess
|
|
36
|
-
with its own torch installation. The main ComfyUI process never
|
|
37
|
-
imports torch from this node.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
@classmethod
|
|
41
|
-
def INPUT_TYPES(cls):
|
|
42
|
-
return {
|
|
43
|
-
"required": {
|
|
44
|
-
"image": ("IMAGE",),
|
|
45
|
-
"brightness": ("FLOAT", {
|
|
46
|
-
"default": 1.0,
|
|
47
|
-
"min": 0.0,
|
|
48
|
-
"max": 2.0,
|
|
49
|
-
"step": 0.1,
|
|
50
|
-
}),
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
RETURN_TYPES = ("IMAGE",)
|
|
55
|
-
FUNCTION = "process"
|
|
56
|
-
CATEGORY = "example/decorator"
|
|
57
|
-
|
|
58
|
-
def process(self, image, brightness):
|
|
59
|
-
# Everything below runs in isolated subprocess!
|
|
60
|
-
# These imports happen in the subprocess, not ComfyUI main process
|
|
61
|
-
import torch
|
|
62
|
-
import numpy as np
|
|
63
|
-
|
|
64
|
-
# The image arrives already deserialized as a torch tensor
|
|
65
|
-
# (comfyui-isolation handles ComfyUI IMAGE format automatically)
|
|
66
|
-
|
|
67
|
-
# Apply brightness adjustment
|
|
68
|
-
result = image * brightness
|
|
69
|
-
result = torch.clamp(result, 0.0, 1.0)
|
|
70
|
-
|
|
71
|
-
return (result,)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# ===========================================================================
|
|
75
|
-
# EXAMPLE 2: Multiple isolated methods
|
|
76
|
-
# ===========================================================================
|
|
77
|
-
|
|
78
|
-
@isolated(
|
|
79
|
-
env="multi-method-example",
|
|
80
|
-
requirements=["torch", "pillow", "numpy"],
|
|
81
|
-
)
|
|
82
|
-
class MultiMethodNode:
|
|
83
|
-
"""
|
|
84
|
-
Node with multiple methods that all run in the isolated subprocess.
|
|
85
|
-
|
|
86
|
-
Use ISOLATED_METHODS to specify which methods should be isolated.
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
@classmethod
|
|
90
|
-
def INPUT_TYPES(cls):
|
|
91
|
-
return {
|
|
92
|
-
"required": {
|
|
93
|
-
"image": ("IMAGE",),
|
|
94
|
-
"operation": (["invert", "grayscale", "double"],),
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
RETURN_TYPES = ("IMAGE",)
|
|
99
|
-
FUNCTION = "process"
|
|
100
|
-
CATEGORY = "example/decorator"
|
|
101
|
-
|
|
102
|
-
# List all methods that should run in subprocess
|
|
103
|
-
ISOLATED_METHODS = ["process", "invert_image", "to_grayscale", "double_brightness"]
|
|
104
|
-
|
|
105
|
-
def process(self, image, operation):
|
|
106
|
-
import torch
|
|
107
|
-
|
|
108
|
-
if operation == "invert":
|
|
109
|
-
return self.invert_image(image)
|
|
110
|
-
elif operation == "grayscale":
|
|
111
|
-
return self.to_grayscale(image)
|
|
112
|
-
else:
|
|
113
|
-
return self.double_brightness(image)
|
|
114
|
-
|
|
115
|
-
def invert_image(self, image):
|
|
116
|
-
import torch
|
|
117
|
-
return (1.0 - image,)
|
|
118
|
-
|
|
119
|
-
def to_grayscale(self, image):
|
|
120
|
-
import torch
|
|
121
|
-
# Weighted grayscale conversion
|
|
122
|
-
weights = torch.tensor([0.299, 0.587, 0.114])
|
|
123
|
-
gray = (image[..., :3] * weights).sum(dim=-1, keepdim=True)
|
|
124
|
-
gray = gray.expand_as(image[..., :3])
|
|
125
|
-
if image.shape[-1] == 4: # Has alpha
|
|
126
|
-
gray = torch.cat([gray, image[..., 3:4]], dim=-1)
|
|
127
|
-
return (gray,)
|
|
128
|
-
|
|
129
|
-
def double_brightness(self, image):
|
|
130
|
-
import torch
|
|
131
|
-
return (torch.clamp(image * 2.0, 0.0, 1.0),)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
# ===========================================================================
|
|
135
|
-
# EXAMPLE 3: Using TOML config for complex dependencies
|
|
136
|
-
# ===========================================================================
|
|
137
|
-
|
|
138
|
-
# For complex dependencies with custom wheel sources, use a config file:
|
|
139
|
-
#
|
|
140
|
-
# @isolated(env="complex-deps", config="isolation_config.toml")
|
|
141
|
-
# class ComplexDepsNode:
|
|
142
|
-
# """Node with complex dependencies defined in TOML."""
|
|
143
|
-
#
|
|
144
|
-
# FUNCTION = "process"
|
|
145
|
-
#
|
|
146
|
-
# def process(self, image):
|
|
147
|
-
# import pytorch3d # From custom wheel index
|
|
148
|
-
# import nvdiffrast # From custom wheel index
|
|
149
|
-
# ...
|
|
150
|
-
#
|
|
151
|
-
# The isolation_config.toml would look like:
|
|
152
|
-
#
|
|
153
|
-
# [env]
|
|
154
|
-
# name = "complex-deps"
|
|
155
|
-
# python = "3.10"
|
|
156
|
-
# cuda = "auto"
|
|
157
|
-
#
|
|
158
|
-
# [packages]
|
|
159
|
-
# requirements = [
|
|
160
|
-
# "torch=={pytorch_version}",
|
|
161
|
-
# "pytorch3d==0.7.8+5043d15pt{pytorch_version}cu{cuda_short}",
|
|
162
|
-
# ]
|
|
163
|
-
#
|
|
164
|
-
# [sources]
|
|
165
|
-
# index_urls = [
|
|
166
|
-
# "https://pozzettiandrea.github.io/sam3dobjects-wheels/",
|
|
167
|
-
# ]
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
# ===========================================================================
|
|
171
|
-
# Node registration
|
|
172
|
-
# ===========================================================================
|
|
173
|
-
|
|
174
|
-
NODE_CLASS_MAPPINGS = {
|
|
175
|
-
"SimpleIsolatedExample": SimpleIsolatedNode,
|
|
176
|
-
"MultiMethodIsolatedExample": MultiMethodNode,
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
180
|
-
"SimpleIsolatedExample": "Simple Isolated (Decorator Example)",
|
|
181
|
-
"MultiMethodIsolatedExample": "Multi-Method Isolated (Decorator Example)",
|
|
182
|
-
}
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tool installers for external dependencies like Blender.
|
|
3
|
-
|
|
4
|
-
Usage in comfy-env.toml:
|
|
5
|
-
[tools]
|
|
6
|
-
blender = "4.2"
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import os
|
|
10
|
-
import platform
|
|
11
|
-
import shutil
|
|
12
|
-
import subprocess
|
|
13
|
-
import tarfile
|
|
14
|
-
import zipfile
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Callable, Optional
|
|
17
|
-
from urllib.request import urlretrieve
|
|
18
|
-
|
|
19
|
-
from .env.config import ToolConfig
|
|
20
|
-
|
|
21
|
-
# Default install location
|
|
22
|
-
DEFAULT_TOOLS_DIR = Path.home() / ".comfy-env" / "tools"
|
|
23
|
-
|
|
24
|
-
# Blender download URLs by platform and version
|
|
25
|
-
BLENDER_DOWNLOADS = {
|
|
26
|
-
"4.2": {
|
|
27
|
-
"linux": "https://download.blender.org/release/Blender4.2/blender-4.2.0-linux-x64.tar.xz",
|
|
28
|
-
"windows": "https://download.blender.org/release/Blender4.2/blender-4.2.0-windows-x64.zip",
|
|
29
|
-
"darwin": "https://download.blender.org/release/Blender4.2/blender-4.2.0-macos-arm64.dmg",
|
|
30
|
-
},
|
|
31
|
-
"4.3": {
|
|
32
|
-
"linux": "https://download.blender.org/release/Blender4.3/blender-4.3.0-linux-x64.tar.xz",
|
|
33
|
-
"windows": "https://download.blender.org/release/Blender4.3/blender-4.3.0-windows-x64.zip",
|
|
34
|
-
"darwin": "https://download.blender.org/release/Blender4.3/blender-4.3.0-macos-arm64.dmg",
|
|
35
|
-
},
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def get_platform() -> str:
|
|
40
|
-
"""Get current platform name."""
|
|
41
|
-
system = platform.system().lower()
|
|
42
|
-
if system == "linux":
|
|
43
|
-
return "linux"
|
|
44
|
-
elif system == "windows":
|
|
45
|
-
return "windows"
|
|
46
|
-
elif system == "darwin":
|
|
47
|
-
return "darwin"
|
|
48
|
-
return system
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def install_tool(
|
|
52
|
-
config: ToolConfig,
|
|
53
|
-
log: Callable[[str], None] = print,
|
|
54
|
-
base_dir: Optional[Path] = None,
|
|
55
|
-
) -> Optional[Path]:
|
|
56
|
-
"""Install a tool based on its config.
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
config: Tool configuration
|
|
60
|
-
log: Logging callback
|
|
61
|
-
base_dir: Base directory for tools. Tools install to base_dir/tools/<name>/
|
|
62
|
-
"""
|
|
63
|
-
if config.name.lower() == "blender":
|
|
64
|
-
return install_blender(config.version, log, config.install_dir or base_dir)
|
|
65
|
-
else:
|
|
66
|
-
log(f"Unknown tool: {config.name}")
|
|
67
|
-
return None
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def install_blender(
|
|
71
|
-
version: str = "4.2",
|
|
72
|
-
log: Callable[[str], None] = print,
|
|
73
|
-
base_dir: Optional[Path] = None,
|
|
74
|
-
) -> Optional[Path]:
|
|
75
|
-
"""
|
|
76
|
-
Install Blender to the specified directory.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
version: Blender version to install (e.g., "4.2")
|
|
80
|
-
log: Logging callback
|
|
81
|
-
base_dir: Base directory. Blender installs to base_dir/tools/blender/
|
|
82
|
-
If None, uses ~/.comfy-env/tools/blender/
|
|
83
|
-
|
|
84
|
-
Returns path to blender executable if successful.
|
|
85
|
-
"""
|
|
86
|
-
plat = get_platform()
|
|
87
|
-
if base_dir:
|
|
88
|
-
install_dir = base_dir / "tools" / "blender"
|
|
89
|
-
else:
|
|
90
|
-
install_dir = DEFAULT_TOOLS_DIR / "blender"
|
|
91
|
-
|
|
92
|
-
# Check if already installed
|
|
93
|
-
exe = find_blender(install_dir)
|
|
94
|
-
if exe:
|
|
95
|
-
log(f"Blender already installed: {exe}")
|
|
96
|
-
return exe
|
|
97
|
-
|
|
98
|
-
# Get download URL
|
|
99
|
-
if version not in BLENDER_DOWNLOADS:
|
|
100
|
-
log(f"Unknown Blender version: {version}. Available: {list(BLENDER_DOWNLOADS.keys())}")
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
urls = BLENDER_DOWNLOADS[version]
|
|
104
|
-
if plat not in urls:
|
|
105
|
-
log(f"Blender {version} not available for {plat}")
|
|
106
|
-
return None
|
|
107
|
-
|
|
108
|
-
url = urls[plat]
|
|
109
|
-
log(f"Downloading Blender {version} for {plat}...")
|
|
110
|
-
|
|
111
|
-
install_dir.mkdir(parents=True, exist_ok=True)
|
|
112
|
-
archive_name = url.split("/")[-1]
|
|
113
|
-
archive_path = install_dir / archive_name
|
|
114
|
-
|
|
115
|
-
try:
|
|
116
|
-
# Download
|
|
117
|
-
urlretrieve(url, archive_path)
|
|
118
|
-
log(f"Downloaded to {archive_path}")
|
|
119
|
-
|
|
120
|
-
# Extract
|
|
121
|
-
log("Extracting...")
|
|
122
|
-
if archive_name.endswith(".tar.xz"):
|
|
123
|
-
with tarfile.open(archive_path, "r:xz") as tar:
|
|
124
|
-
tar.extractall(install_dir)
|
|
125
|
-
elif archive_name.endswith(".zip"):
|
|
126
|
-
with zipfile.ZipFile(archive_path, "r") as zf:
|
|
127
|
-
zf.extractall(install_dir)
|
|
128
|
-
elif archive_name.endswith(".dmg"):
|
|
129
|
-
# macOS DMG requires special handling
|
|
130
|
-
log("macOS DMG installation not yet automated. Please install manually.")
|
|
131
|
-
archive_path.unlink()
|
|
132
|
-
return None
|
|
133
|
-
|
|
134
|
-
# Clean up archive
|
|
135
|
-
archive_path.unlink()
|
|
136
|
-
|
|
137
|
-
# Find the executable
|
|
138
|
-
exe = find_blender(install_dir)
|
|
139
|
-
if exe:
|
|
140
|
-
log(f"Blender installed: {exe}")
|
|
141
|
-
return exe
|
|
142
|
-
else:
|
|
143
|
-
log("Blender extracted but executable not found")
|
|
144
|
-
return None
|
|
145
|
-
|
|
146
|
-
except Exception as e:
|
|
147
|
-
log(f"Failed to install Blender: {e}")
|
|
148
|
-
if archive_path.exists():
|
|
149
|
-
archive_path.unlink()
|
|
150
|
-
return None
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def find_blender(search_dir: Optional[Path] = None) -> Optional[Path]:
|
|
154
|
-
"""Find Blender executable."""
|
|
155
|
-
# Check PATH first
|
|
156
|
-
blender_in_path = shutil.which("blender")
|
|
157
|
-
if blender_in_path:
|
|
158
|
-
return Path(blender_in_path)
|
|
159
|
-
|
|
160
|
-
# Check common locations
|
|
161
|
-
plat = get_platform()
|
|
162
|
-
search_paths = []
|
|
163
|
-
|
|
164
|
-
if search_dir:
|
|
165
|
-
search_paths.append(search_dir)
|
|
166
|
-
|
|
167
|
-
search_paths.append(DEFAULT_TOOLS_DIR / "blender")
|
|
168
|
-
|
|
169
|
-
if plat == "windows":
|
|
170
|
-
search_paths.extend([
|
|
171
|
-
Path(os.environ.get("ProgramFiles", "C:/Program Files")) / "Blender Foundation",
|
|
172
|
-
Path.home() / "AppData" / "Local" / "Blender Foundation",
|
|
173
|
-
])
|
|
174
|
-
elif plat == "linux":
|
|
175
|
-
search_paths.extend([
|
|
176
|
-
Path("/opt/blender"),
|
|
177
|
-
Path.home() / "blender",
|
|
178
|
-
])
|
|
179
|
-
elif plat == "darwin":
|
|
180
|
-
search_paths.append(Path("/Applications/Blender.app/Contents/MacOS"))
|
|
181
|
-
|
|
182
|
-
exe_name = "blender.exe" if plat == "windows" else "blender"
|
|
183
|
-
|
|
184
|
-
for base in search_paths:
|
|
185
|
-
if not base.exists():
|
|
186
|
-
continue
|
|
187
|
-
# Direct check
|
|
188
|
-
exe = base / exe_name
|
|
189
|
-
if exe.exists():
|
|
190
|
-
return exe
|
|
191
|
-
# Search subdirectories (for extracted archives like blender-4.2.0-linux-x64/)
|
|
192
|
-
for subdir in base.iterdir():
|
|
193
|
-
if subdir.is_dir():
|
|
194
|
-
exe = subdir / exe_name
|
|
195
|
-
if exe.exists():
|
|
196
|
-
return exe
|
|
197
|
-
|
|
198
|
-
return None
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def ensure_blender(
|
|
202
|
-
version: str = "4.2",
|
|
203
|
-
log: Callable[[str], None] = print,
|
|
204
|
-
base_dir: Optional[Path] = None,
|
|
205
|
-
) -> Optional[Path]:
|
|
206
|
-
"""Ensure Blender is installed, installing if necessary.
|
|
207
|
-
|
|
208
|
-
Args:
|
|
209
|
-
version: Blender version to install
|
|
210
|
-
log: Logging callback
|
|
211
|
-
base_dir: Base directory. Searches/installs in base_dir/tools/blender/
|
|
212
|
-
"""
|
|
213
|
-
if base_dir:
|
|
214
|
-
search_dir = base_dir / "tools" / "blender"
|
|
215
|
-
else:
|
|
216
|
-
search_dir = DEFAULT_TOOLS_DIR / "blender"
|
|
217
|
-
|
|
218
|
-
exe = find_blender(search_dir)
|
|
219
|
-
if exe:
|
|
220
|
-
return exe
|
|
221
|
-
return install_blender(version, log, base_dir)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|