comfy-env 0.0.18__py3-none-any.whl → 0.0.21__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/decorator.py +28 -9
- comfy_env/env/config.py +19 -0
- comfy_env/env/config_file.py +36 -3
- comfy_env/install.py +104 -12
- comfy_env/workers/torch_mp.py +204 -1
- {comfy_env-0.0.18.dist-info → comfy_env-0.0.21.dist-info}/METADATA +2 -2
- {comfy_env-0.0.18.dist-info → comfy_env-0.0.21.dist-info}/RECORD +10 -11
- comfy_env/tools.py +0 -221
- {comfy_env-0.0.18.dist-info → comfy_env-0.0.21.dist-info}/WHEEL +0 -0
- {comfy_env-0.0.18.dist-info → comfy_env-0.0.21.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.0.18.dist-info → comfy_env-0.0.21.dist-info}/licenses/LICENSE +0 -0
comfy_env/decorator.py
CHANGED
|
@@ -113,6 +113,31 @@ def _clone_tensor_if_needed(obj: Any, smart_clone: bool = True) -> Any:
|
|
|
113
113
|
return obj
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
def _find_node_package_dir(source_file: Path) -> Path:
|
|
117
|
+
"""
|
|
118
|
+
Find the node package root directory by searching for comfy-env.toml.
|
|
119
|
+
|
|
120
|
+
Walks up from the source file's directory until it finds a config file,
|
|
121
|
+
or falls back to heuristics if not found.
|
|
122
|
+
"""
|
|
123
|
+
from .env.config_file import CONFIG_FILE_NAMES
|
|
124
|
+
|
|
125
|
+
current = source_file.parent
|
|
126
|
+
|
|
127
|
+
# Walk up the directory tree looking for config file
|
|
128
|
+
while current != current.parent: # Stop at filesystem root
|
|
129
|
+
for config_name in CONFIG_FILE_NAMES:
|
|
130
|
+
if (current / config_name).exists():
|
|
131
|
+
return current
|
|
132
|
+
current = current.parent
|
|
133
|
+
|
|
134
|
+
# Fallback: use old heuristic if no config found
|
|
135
|
+
node_dir = source_file.parent
|
|
136
|
+
if node_dir.name == "nodes":
|
|
137
|
+
return node_dir.parent
|
|
138
|
+
return node_dir
|
|
139
|
+
|
|
140
|
+
|
|
116
141
|
# ---------------------------------------------------------------------------
|
|
117
142
|
# Worker Management
|
|
118
143
|
# ---------------------------------------------------------------------------
|
|
@@ -262,10 +287,7 @@ def isolated(
|
|
|
262
287
|
# Get source file info for sys.path setup
|
|
263
288
|
source_file = Path(inspect.getfile(cls))
|
|
264
289
|
node_dir = source_file.parent
|
|
265
|
-
|
|
266
|
-
node_package_dir = node_dir.parent
|
|
267
|
-
else:
|
|
268
|
-
node_package_dir = node_dir
|
|
290
|
+
node_package_dir = _find_node_package_dir(source_file)
|
|
269
291
|
|
|
270
292
|
# Build sys.path for worker
|
|
271
293
|
sys_path_additions = [str(node_dir)]
|
|
@@ -367,13 +389,10 @@ def isolated(
|
|
|
367
389
|
call_kwargs = {k: _clone_tensor_if_needed(v) for k, v in call_kwargs.items()}
|
|
368
390
|
|
|
369
391
|
# Get module name for import in worker
|
|
392
|
+
# Note: ComfyUI uses full filesystem paths as module names for custom nodes.
|
|
393
|
+
# The worker's _execute_method_call handles this by using file-based imports.
|
|
370
394
|
module_name = cls.__module__
|
|
371
395
|
|
|
372
|
-
# Handle ComfyUI's dynamic import which can set __module__ to a path
|
|
373
|
-
if module_name.startswith('/') or module_name.startswith('\\'):
|
|
374
|
-
# Module name is a filesystem path - use the source file stem instead
|
|
375
|
-
module_name = source_file.stem
|
|
376
|
-
|
|
377
396
|
# Call worker using appropriate method
|
|
378
397
|
if worker_config.python is None:
|
|
379
398
|
# TorchMPWorker - use call_method protocol (avoids pickle issues)
|
comfy_env/env/config.py
CHANGED
|
@@ -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."""
|
comfy_env/env/config_file.py
CHANGED
|
@@ -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,
|
comfy_env/install.py
CHANGED
|
@@ -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
|
comfy_env/workers/torch_mp.py
CHANGED
|
@@ -106,6 +106,190 @@ def _worker_loop(queue_in, queue_out, sys_path_additions=None):
|
|
|
106
106
|
break
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
class PathBasedModuleFinder:
|
|
110
|
+
"""
|
|
111
|
+
Meta path finder that handles ComfyUI's path-based module names.
|
|
112
|
+
|
|
113
|
+
ComfyUI uses full filesystem paths as module names for custom nodes.
|
|
114
|
+
This finder intercepts imports of such modules and loads them from disk.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def find_spec(self, fullname, path, target=None):
|
|
118
|
+
import importlib.util
|
|
119
|
+
import os
|
|
120
|
+
|
|
121
|
+
# Only handle path-based module names (starting with /)
|
|
122
|
+
if not fullname.startswith('/'):
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
# Parse the module name to find base path and submodule parts
|
|
126
|
+
parts = fullname.split('.')
|
|
127
|
+
base_path = parts[0]
|
|
128
|
+
submodule_parts = parts[1:] if len(parts) > 1 else []
|
|
129
|
+
|
|
130
|
+
# Walk through parts to find where path ends and module begins
|
|
131
|
+
for i, part in enumerate(submodule_parts):
|
|
132
|
+
test_path = os.path.join(base_path, part)
|
|
133
|
+
if os.path.exists(test_path):
|
|
134
|
+
base_path = test_path
|
|
135
|
+
else:
|
|
136
|
+
# Remaining parts are module names
|
|
137
|
+
submodule_parts = submodule_parts[i:]
|
|
138
|
+
break
|
|
139
|
+
else:
|
|
140
|
+
# All parts were path components
|
|
141
|
+
submodule_parts = []
|
|
142
|
+
|
|
143
|
+
# Determine the file to load
|
|
144
|
+
if submodule_parts:
|
|
145
|
+
# We're importing a submodule
|
|
146
|
+
current_path = base_path
|
|
147
|
+
for part in submodule_parts[:-1]:
|
|
148
|
+
current_path = os.path.join(current_path, part)
|
|
149
|
+
|
|
150
|
+
submod = submodule_parts[-1]
|
|
151
|
+
submod_file = os.path.join(current_path, submod + '.py')
|
|
152
|
+
submod_pkg = os.path.join(current_path, submod, '__init__.py')
|
|
153
|
+
|
|
154
|
+
if os.path.exists(submod_file):
|
|
155
|
+
return importlib.util.spec_from_file_location(fullname, submod_file)
|
|
156
|
+
elif os.path.exists(submod_pkg):
|
|
157
|
+
return importlib.util.spec_from_file_location(
|
|
158
|
+
fullname, submod_pkg,
|
|
159
|
+
submodule_search_locations=[os.path.join(current_path, submod)]
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
# Top-level path-based module
|
|
163
|
+
if os.path.isdir(base_path):
|
|
164
|
+
init_path = os.path.join(base_path, "__init__.py")
|
|
165
|
+
if os.path.exists(init_path):
|
|
166
|
+
return importlib.util.spec_from_file_location(
|
|
167
|
+
fullname, init_path,
|
|
168
|
+
submodule_search_locations=[base_path]
|
|
169
|
+
)
|
|
170
|
+
elif os.path.isfile(base_path):
|
|
171
|
+
return importlib.util.spec_from_file_location(fullname, base_path)
|
|
172
|
+
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Global flag to track if we've installed the finder
|
|
177
|
+
_path_finder_installed = False
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _ensure_path_finder_installed():
|
|
181
|
+
"""Install the PathBasedModuleFinder if not already installed."""
|
|
182
|
+
import sys
|
|
183
|
+
global _path_finder_installed
|
|
184
|
+
if not _path_finder_installed:
|
|
185
|
+
sys.meta_path.insert(0, PathBasedModuleFinder())
|
|
186
|
+
_path_finder_installed = True
|
|
187
|
+
logger.debug("[comfy_env] Installed PathBasedModuleFinder for path-based module names")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _load_path_based_module(module_name: str):
|
|
191
|
+
"""
|
|
192
|
+
Load a module that has a filesystem path as its name.
|
|
193
|
+
|
|
194
|
+
ComfyUI uses full filesystem paths as module names for custom nodes.
|
|
195
|
+
This function handles that case by using file-based imports.
|
|
196
|
+
"""
|
|
197
|
+
import importlib.util
|
|
198
|
+
import os
|
|
199
|
+
import sys
|
|
200
|
+
|
|
201
|
+
# Check if it's already in sys.modules
|
|
202
|
+
if module_name in sys.modules:
|
|
203
|
+
return sys.modules[module_name]
|
|
204
|
+
|
|
205
|
+
# Check if module_name contains submodule parts (e.g., "/path/to/pkg.submod.subsubmod")
|
|
206
|
+
# In this case, we need to load the parent packages first
|
|
207
|
+
if '.' in module_name:
|
|
208
|
+
parts = module_name.split('.')
|
|
209
|
+
# Find where the path ends and module parts begin
|
|
210
|
+
# The path part won't exist as a directory when combined with module parts
|
|
211
|
+
base_path = parts[0]
|
|
212
|
+
submodule_parts = []
|
|
213
|
+
|
|
214
|
+
for i, part in enumerate(parts[1:], 1):
|
|
215
|
+
test_path = os.path.join(base_path, part)
|
|
216
|
+
if os.path.exists(test_path):
|
|
217
|
+
base_path = test_path
|
|
218
|
+
else:
|
|
219
|
+
# This and remaining parts are module names, not path components
|
|
220
|
+
submodule_parts = parts[i:]
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
if submodule_parts:
|
|
224
|
+
# Load parent package first
|
|
225
|
+
parent_module = _load_path_based_module(base_path)
|
|
226
|
+
|
|
227
|
+
# Now load submodules
|
|
228
|
+
current_module = parent_module
|
|
229
|
+
current_name = base_path
|
|
230
|
+
for submod in submodule_parts:
|
|
231
|
+
current_name = f"{current_name}.{submod}"
|
|
232
|
+
if current_name in sys.modules:
|
|
233
|
+
current_module = sys.modules[current_name]
|
|
234
|
+
else:
|
|
235
|
+
# Try to import as attribute or load from file
|
|
236
|
+
if hasattr(current_module, submod):
|
|
237
|
+
current_module = getattr(current_module, submod)
|
|
238
|
+
else:
|
|
239
|
+
# Try to load the submodule file
|
|
240
|
+
if hasattr(current_module, '__path__'):
|
|
241
|
+
for parent_path in current_module.__path__:
|
|
242
|
+
submod_file = os.path.join(parent_path, submod + '.py')
|
|
243
|
+
submod_pkg = os.path.join(parent_path, submod, '__init__.py')
|
|
244
|
+
if os.path.exists(submod_file):
|
|
245
|
+
spec = importlib.util.spec_from_file_location(current_name, submod_file)
|
|
246
|
+
current_module = importlib.util.module_from_spec(spec)
|
|
247
|
+
current_module.__package__ = f"{base_path}.{'.'.join(submodule_parts[:-1])}" if len(submodule_parts) > 1 else base_path
|
|
248
|
+
sys.modules[current_name] = current_module
|
|
249
|
+
spec.loader.exec_module(current_module)
|
|
250
|
+
break
|
|
251
|
+
elif os.path.exists(submod_pkg):
|
|
252
|
+
spec = importlib.util.spec_from_file_location(current_name, submod_pkg,
|
|
253
|
+
submodule_search_locations=[os.path.dirname(submod_pkg)])
|
|
254
|
+
current_module = importlib.util.module_from_spec(spec)
|
|
255
|
+
sys.modules[current_name] = current_module
|
|
256
|
+
spec.loader.exec_module(current_module)
|
|
257
|
+
break
|
|
258
|
+
else:
|
|
259
|
+
raise ModuleNotFoundError(f"Cannot find submodule {submod} in {current_name}")
|
|
260
|
+
return current_module
|
|
261
|
+
|
|
262
|
+
# Simple path-based module (no submodule parts)
|
|
263
|
+
if os.path.isdir(module_name):
|
|
264
|
+
init_path = os.path.join(module_name, "__init__.py")
|
|
265
|
+
submodule_search_locations = [module_name]
|
|
266
|
+
else:
|
|
267
|
+
init_path = module_name
|
|
268
|
+
submodule_search_locations = None
|
|
269
|
+
|
|
270
|
+
if not os.path.exists(init_path):
|
|
271
|
+
raise ModuleNotFoundError(f"Cannot find module at path: {module_name}")
|
|
272
|
+
|
|
273
|
+
spec = importlib.util.spec_from_file_location(
|
|
274
|
+
module_name,
|
|
275
|
+
init_path,
|
|
276
|
+
submodule_search_locations=submodule_search_locations
|
|
277
|
+
)
|
|
278
|
+
module = importlib.util.module_from_spec(spec)
|
|
279
|
+
|
|
280
|
+
# Set up package attributes for relative imports
|
|
281
|
+
if os.path.isdir(module_name):
|
|
282
|
+
module.__path__ = [module_name]
|
|
283
|
+
module.__package__ = module_name
|
|
284
|
+
else:
|
|
285
|
+
module.__package__ = module_name.rsplit('.', 1)[0] if '.' in module_name else ''
|
|
286
|
+
|
|
287
|
+
sys.modules[module_name] = module
|
|
288
|
+
spec.loader.exec_module(module)
|
|
289
|
+
|
|
290
|
+
return module
|
|
291
|
+
|
|
292
|
+
|
|
109
293
|
def _execute_method_call(module_name: str, class_name: str, method_name: str,
|
|
110
294
|
self_state: dict, kwargs: dict) -> Any:
|
|
111
295
|
"""
|
|
@@ -114,9 +298,28 @@ def _execute_method_call(module_name: str, class_name: str, method_name: str,
|
|
|
114
298
|
This function imports the class fresh and calls the original (un-decorated) method.
|
|
115
299
|
"""
|
|
116
300
|
import importlib
|
|
301
|
+
import os
|
|
302
|
+
import sys
|
|
117
303
|
|
|
118
304
|
# Import the module
|
|
119
|
-
|
|
305
|
+
logger.debug(f"Attempting to import module_name={module_name}")
|
|
306
|
+
|
|
307
|
+
# Check if module_name is a filesystem path (ComfyUI uses paths as module names)
|
|
308
|
+
# This happens because ComfyUI's load_custom_node uses the full path as sys_module_name
|
|
309
|
+
if module_name.startswith('/') or (os.sep in module_name and not module_name.startswith('.')):
|
|
310
|
+
# Check if the base path exists to confirm it's a path-based module
|
|
311
|
+
base_path = module_name.split('.')[0] if '.' in module_name else module_name
|
|
312
|
+
if os.path.exists(base_path):
|
|
313
|
+
logger.debug(f"Detected path-based module name, using file-based import")
|
|
314
|
+
# Install the meta path finder to handle relative imports within the package
|
|
315
|
+
_ensure_path_finder_installed()
|
|
316
|
+
module = _load_path_based_module(module_name)
|
|
317
|
+
else:
|
|
318
|
+
# Doesn't look like a valid path, try standard import
|
|
319
|
+
module = importlib.import_module(module_name)
|
|
320
|
+
else:
|
|
321
|
+
# Standard module name - use importlib.import_module
|
|
322
|
+
module = importlib.import_module(module_name)
|
|
120
323
|
cls = getattr(module, class_name)
|
|
121
324
|
|
|
122
325
|
# Create instance with proper __slots__ handling
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comfy-env
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.21
|
|
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
|
|
@@ -126,7 +126,7 @@ comfy-env resolve nvdiffrast==0.4.0
|
|
|
126
126
|
comfy-env doctor
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
##
|
|
129
|
+
## Configurations
|
|
130
130
|
|
|
131
131
|
### comfy-env.toml
|
|
132
132
|
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
comfy_env/__init__.py,sha256=76gIAh7qFff_v_bAolXVzuWzcgvD3bp-yQGCNzba_Iw,3287
|
|
2
2
|
comfy_env/cli.py,sha256=X-GCQMP0mtMcE3ZgkT-VLQ4Gq3UUvcb_Ux_NClEFhgI,15975
|
|
3
|
-
comfy_env/decorator.py,sha256=
|
|
3
|
+
comfy_env/decorator.py,sha256=6JCKwLHaZtOLVDexs_gh_-NtS2ZK0V7nGCPqkyeYEAA,16688
|
|
4
4
|
comfy_env/errors.py,sha256=8hN8NDlo8oBUdapc-eT3ZluigI5VBzfqsSBvQdfWlz4,9943
|
|
5
|
-
comfy_env/install.py,sha256=
|
|
5
|
+
comfy_env/install.py,sha256=txjQh5mdtFt0uQL7682LWgH0-311TflunaKv-n0XlYM,24510
|
|
6
6
|
comfy_env/registry.py,sha256=uFCtGmWYvwGCqObXgzmArX7o5JsFNsHXxayofk3m6no,2569
|
|
7
7
|
comfy_env/resolver.py,sha256=l-AnmCE1puG6CvdpDB-KrsfG_cn_3uO2DryYizUnG_4,12474
|
|
8
|
-
comfy_env/tools.py,sha256=mFNB_uq64ON5hlreH_0wTLONahDo3pBHxhQYTcTHxXE,6554
|
|
9
8
|
comfy_env/env/__init__.py,sha256=imQdoQEQvrRT-QDtyNpFlkVbm2fBzgACdpQwRPd09fI,1157
|
|
10
|
-
comfy_env/env/config.py,sha256=
|
|
11
|
-
comfy_env/env/config_file.py,sha256
|
|
9
|
+
comfy_env/env/config.py,sha256=r1olWRFbXzSSU24Zkn_TVFVujaf_lEX6LAYEgfBYo3U,6765
|
|
10
|
+
comfy_env/env/config_file.py,sha256=-NdK6i0LKKGK6uNqXKvBlQGnwdHpMCaNkYpbxLDAgNY,22918
|
|
12
11
|
comfy_env/env/cuda_gpu_detection.py,sha256=YLuXUdWg6FeKdNyLlQAHPlveg4rTenXJ2VbeAaEi9QE,9755
|
|
13
12
|
comfy_env/env/manager.py,sha256=bbV1MpURNGuBJ1sSWg_2oSU0J-dW-FhBCuHHHQxgrSM,24785
|
|
14
13
|
comfy_env/env/security.py,sha256=dNSitAnfBNVdvxgBBntYw33AJaCs_S1MHb7KJhAVYzM,8171
|
|
@@ -30,11 +29,11 @@ comfy_env/workers/__init__.py,sha256=IKZwOvrWOGqBLDUIFAalg4CdqzJ_YnAdxo2Ha7gZTJ0
|
|
|
30
29
|
comfy_env/workers/base.py,sha256=ZILYXlvGCWuCZXmjKqfG8VeD19ihdYaASdlbasl2BMo,2312
|
|
31
30
|
comfy_env/workers/pool.py,sha256=MtjeOWfvHSCockq8j1gfnxIl-t01GSB79T5N4YB82Lg,6956
|
|
32
31
|
comfy_env/workers/tensor_utils.py,sha256=TCuOAjJymrSbkgfyvcKtQ_KbVWTqSwP9VH_bCaFLLq8,6409
|
|
33
|
-
comfy_env/workers/torch_mp.py,sha256=
|
|
32
|
+
comfy_env/workers/torch_mp.py,sha256=4YSNPn7hALrvMVbkO4RkTeFTcc0lhfLMk5QTWjY4PHw,22134
|
|
34
33
|
comfy_env/workers/venv.py,sha256=_ekHfZPqBIPY08DjqiXm6cTBQH4DrbxRWR3AAv3mit8,31589
|
|
35
34
|
comfy_env/wheel_sources.yml,sha256=nSZ8XB_I5JXQGB7AgC6lHs_IXMd9Kcno10artNL8BKw,7775
|
|
36
|
-
comfy_env-0.0.
|
|
37
|
-
comfy_env-0.0.
|
|
38
|
-
comfy_env-0.0.
|
|
39
|
-
comfy_env-0.0.
|
|
40
|
-
comfy_env-0.0.
|
|
35
|
+
comfy_env-0.0.21.dist-info/METADATA,sha256=UvyKIRRV4zWGCvxgQu-Og_Y7wNC--tzcBCFab4PvAYw,5400
|
|
36
|
+
comfy_env-0.0.21.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
37
|
+
comfy_env-0.0.21.dist-info/entry_points.txt,sha256=J4fXeqgxU_YenuW_Zxn_pEL7J-3R0--b6MS5t0QmAr0,49
|
|
38
|
+
comfy_env-0.0.21.dist-info/licenses/LICENSE,sha256=E68QZMMpW4P2YKstTZ3QU54HRQO8ecew09XZ4_Vn870,1093
|
|
39
|
+
comfy_env-0.0.21.dist-info/RECORD,,
|
comfy_env/tools.py
DELETED
|
@@ -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
|