comfy-env 0.1.14__py3-none-any.whl → 0.1.16__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 +115 -62
- comfy_env/cli.py +89 -319
- comfy_env/config/__init__.py +18 -8
- comfy_env/config/parser.py +21 -122
- comfy_env/config/types.py +37 -70
- comfy_env/detection/__init__.py +77 -0
- comfy_env/detection/cuda.py +61 -0
- comfy_env/detection/gpu.py +230 -0
- comfy_env/detection/platform.py +70 -0
- comfy_env/detection/runtime.py +103 -0
- comfy_env/environment/__init__.py +53 -0
- comfy_env/environment/cache.py +141 -0
- comfy_env/environment/libomp.py +41 -0
- comfy_env/environment/paths.py +38 -0
- comfy_env/environment/setup.py +88 -0
- comfy_env/install.py +163 -249
- comfy_env/isolation/__init__.py +33 -2
- comfy_env/isolation/tensor_utils.py +83 -0
- comfy_env/isolation/workers/__init__.py +16 -0
- comfy_env/{workers → isolation/workers}/mp.py +1 -1
- comfy_env/{workers → isolation/workers}/subprocess.py +2 -2
- comfy_env/isolation/wrap.py +149 -409
- comfy_env/packages/__init__.py +60 -0
- comfy_env/packages/apt.py +36 -0
- comfy_env/packages/cuda_wheels.py +97 -0
- comfy_env/packages/node_dependencies.py +77 -0
- comfy_env/packages/pixi.py +85 -0
- comfy_env/packages/toml_generator.py +88 -0
- comfy_env-0.1.16.dist-info/METADATA +279 -0
- comfy_env-0.1.16.dist-info/RECORD +36 -0
- comfy_env/cache.py +0 -331
- comfy_env/errors.py +0 -293
- comfy_env/nodes.py +0 -187
- comfy_env/pixi/__init__.py +0 -48
- comfy_env/pixi/core.py +0 -588
- comfy_env/pixi/cuda_detection.py +0 -303
- comfy_env/pixi/platform/__init__.py +0 -21
- comfy_env/pixi/platform/base.py +0 -96
- comfy_env/pixi/platform/darwin.py +0 -53
- comfy_env/pixi/platform/linux.py +0 -68
- comfy_env/pixi/platform/windows.py +0 -284
- comfy_env/pixi/resolver.py +0 -198
- comfy_env/prestartup.py +0 -192
- comfy_env/workers/__init__.py +0 -38
- comfy_env/workers/tensor_utils.py +0 -188
- comfy_env-0.1.14.dist-info/METADATA +0 -291
- comfy_env-0.1.14.dist-info/RECORD +0 -33
- /comfy_env/{workers → isolation/workers}/base.py +0 -0
- {comfy_env-0.1.14.dist-info → comfy_env-0.1.16.dist-info}/WHEEL +0 -0
- {comfy_env-0.1.14.dist-info → comfy_env-0.1.16.dist-info}/entry_points.txt +0 -0
- {comfy_env-0.1.14.dist-info → comfy_env-0.1.16.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Windows platform provider implementation.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
import stat
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
|
-
import sys
|
|
10
|
-
import time
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Dict, Optional, Tuple
|
|
13
|
-
|
|
14
|
-
from .base import PlatformProvider, PlatformPaths
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class WindowsPlatformProvider(PlatformProvider):
|
|
18
|
-
"""Platform provider for Windows systems."""
|
|
19
|
-
|
|
20
|
-
@property
|
|
21
|
-
def name(self) -> str:
|
|
22
|
-
return 'windows'
|
|
23
|
-
|
|
24
|
-
@property
|
|
25
|
-
def executable_suffix(self) -> str:
|
|
26
|
-
return '.exe'
|
|
27
|
-
|
|
28
|
-
@property
|
|
29
|
-
def shared_lib_extension(self) -> str:
|
|
30
|
-
return '.dll'
|
|
31
|
-
|
|
32
|
-
def get_env_paths(self, env_dir: Path, python_version: str = "3.10") -> PlatformPaths:
|
|
33
|
-
return PlatformPaths(
|
|
34
|
-
python=env_dir / "Scripts" / "python.exe",
|
|
35
|
-
pip=env_dir / "Scripts" / "pip.exe",
|
|
36
|
-
site_packages=env_dir / "Lib" / "site-packages",
|
|
37
|
-
bin_dir=env_dir / "Scripts"
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
def check_prerequisites(self) -> Tuple[bool, Optional[str]]:
|
|
41
|
-
# Check for MSYS2/Cygwin/Git Bash
|
|
42
|
-
shell_env = self._detect_shell_environment()
|
|
43
|
-
if shell_env in ('msys2', 'cygwin', 'git-bash'):
|
|
44
|
-
return (False,
|
|
45
|
-
f"Running in {shell_env.upper()} environment.\n"
|
|
46
|
-
f"This package requires native Windows Python.\n"
|
|
47
|
-
f"Please use PowerShell, Command Prompt, or native Windows terminal.")
|
|
48
|
-
# Note: VC++ runtime is handled by msvc-runtime package, no system check needed
|
|
49
|
-
return (True, None)
|
|
50
|
-
|
|
51
|
-
def _detect_shell_environment(self) -> str:
|
|
52
|
-
"""Detect if running in MSYS2, Cygwin, Git Bash, or native Windows."""
|
|
53
|
-
msystem = os.environ.get('MSYSTEM', '')
|
|
54
|
-
if msystem:
|
|
55
|
-
if 'MINGW' in msystem:
|
|
56
|
-
return 'git-bash'
|
|
57
|
-
return 'msys2'
|
|
58
|
-
|
|
59
|
-
term = os.environ.get('TERM', '')
|
|
60
|
-
if term and 'cygwin' in term:
|
|
61
|
-
return 'cygwin'
|
|
62
|
-
|
|
63
|
-
return 'native-windows'
|
|
64
|
-
|
|
65
|
-
def make_executable(self, path: Path) -> None:
|
|
66
|
-
# No-op on Windows - executables are determined by extension
|
|
67
|
-
pass
|
|
68
|
-
|
|
69
|
-
def rmtree_robust(self, path: Path, max_retries: int = 5, delay: float = 0.5) -> bool:
|
|
70
|
-
"""
|
|
71
|
-
Windows-specific rmtree with retry logic for file locking issues.
|
|
72
|
-
|
|
73
|
-
Handles Windows file locking, read-only files, and antivirus interference.
|
|
74
|
-
"""
|
|
75
|
-
def handle_remove_readonly(func, fpath, exc):
|
|
76
|
-
"""Error handler for removing read-only files."""
|
|
77
|
-
if isinstance(exc[1], PermissionError):
|
|
78
|
-
try:
|
|
79
|
-
os.chmod(fpath, stat.S_IWRITE)
|
|
80
|
-
func(fpath)
|
|
81
|
-
except Exception:
|
|
82
|
-
raise exc[1]
|
|
83
|
-
else:
|
|
84
|
-
raise exc[1]
|
|
85
|
-
|
|
86
|
-
for attempt in range(max_retries):
|
|
87
|
-
try:
|
|
88
|
-
shutil.rmtree(path, onerror=handle_remove_readonly)
|
|
89
|
-
return True
|
|
90
|
-
except PermissionError:
|
|
91
|
-
if attempt < max_retries - 1:
|
|
92
|
-
wait_time = delay * (2 ** attempt)
|
|
93
|
-
time.sleep(wait_time)
|
|
94
|
-
else:
|
|
95
|
-
raise
|
|
96
|
-
except OSError:
|
|
97
|
-
if attempt < max_retries - 1:
|
|
98
|
-
wait_time = delay * (2 ** attempt)
|
|
99
|
-
time.sleep(wait_time)
|
|
100
|
-
else:
|
|
101
|
-
raise
|
|
102
|
-
|
|
103
|
-
return False
|
|
104
|
-
|
|
105
|
-
# =========================================================================
|
|
106
|
-
# Media Foundation Detection and Installation
|
|
107
|
-
# =========================================================================
|
|
108
|
-
|
|
109
|
-
def check_media_foundation(self) -> bool:
|
|
110
|
-
"""
|
|
111
|
-
Check if Media Foundation DLLs exist on the system.
|
|
112
|
-
|
|
113
|
-
These are required by packages like opencv-python for video/media support.
|
|
114
|
-
Missing on Windows N/KN editions and some Windows Server installations.
|
|
115
|
-
"""
|
|
116
|
-
system_root = os.environ.get('SystemRoot', r'C:\Windows')
|
|
117
|
-
mf_dlls = ['MFPlat.dll', 'MF.dll', 'MFReadWrite.dll']
|
|
118
|
-
|
|
119
|
-
for dll in mf_dlls:
|
|
120
|
-
dll_path = Path(system_root) / 'System32' / dll
|
|
121
|
-
if not dll_path.exists():
|
|
122
|
-
return False
|
|
123
|
-
return True
|
|
124
|
-
|
|
125
|
-
def install_media_foundation(self, log_callback=None) -> Tuple[bool, Optional[str]]:
|
|
126
|
-
"""
|
|
127
|
-
Install Media Foundation via DISM.
|
|
128
|
-
|
|
129
|
-
Requires administrator privileges. Will trigger UAC prompt if needed.
|
|
130
|
-
|
|
131
|
-
Returns:
|
|
132
|
-
Tuple of (success, error_message)
|
|
133
|
-
"""
|
|
134
|
-
log = log_callback or print
|
|
135
|
-
|
|
136
|
-
if self.check_media_foundation():
|
|
137
|
-
return (True, None)
|
|
138
|
-
|
|
139
|
-
log("Media Foundation not found. Installing via DISM...")
|
|
140
|
-
log("(This requires administrator privileges - a UAC prompt may appear)")
|
|
141
|
-
|
|
142
|
-
# DISM command to install Media Feature Pack
|
|
143
|
-
dism_cmd = [
|
|
144
|
-
"DISM.exe", "/Online", "/Add-Capability",
|
|
145
|
-
"/CapabilityName:Media.MediaFeaturePack~~~~0.0.1.0"
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
try:
|
|
149
|
-
# First try without elevation (in case already running as admin)
|
|
150
|
-
result = subprocess.run(
|
|
151
|
-
dism_cmd,
|
|
152
|
-
capture_output=True,
|
|
153
|
-
text=True,
|
|
154
|
-
timeout=300 # 5 minute timeout for installation
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
if result.returncode == 0:
|
|
158
|
-
log("Media Foundation installed successfully!")
|
|
159
|
-
return (True, None)
|
|
160
|
-
|
|
161
|
-
# Check if we need elevation
|
|
162
|
-
if "administrator" in result.stderr.lower() or result.returncode == 740:
|
|
163
|
-
log("Requesting administrator privileges...")
|
|
164
|
-
return self._install_media_foundation_elevated(log)
|
|
165
|
-
|
|
166
|
-
# Other error
|
|
167
|
-
return (False,
|
|
168
|
-
f"DISM failed with code {result.returncode}:\n{result.stderr}\n\n"
|
|
169
|
-
f"Please install Media Foundation manually:\n"
|
|
170
|
-
f" 1. Open Settings > Apps > Optional Features\n"
|
|
171
|
-
f" 2. Click 'Add a feature'\n"
|
|
172
|
-
f" 3. Search for 'Media Feature Pack' and install it\n"
|
|
173
|
-
f" 4. Restart your computer")
|
|
174
|
-
|
|
175
|
-
except subprocess.TimeoutExpired:
|
|
176
|
-
return (False, "DISM timed out. Please try installing manually via Settings.")
|
|
177
|
-
except FileNotFoundError:
|
|
178
|
-
return (False, "DISM.exe not found. Please install Media Feature Pack manually via Settings.")
|
|
179
|
-
except Exception as e:
|
|
180
|
-
return (False, f"Error running DISM: {e}")
|
|
181
|
-
|
|
182
|
-
def _install_media_foundation_elevated(self, log_callback=None) -> Tuple[bool, Optional[str]]:
|
|
183
|
-
"""
|
|
184
|
-
Install Media Foundation with UAC elevation.
|
|
185
|
-
|
|
186
|
-
Uses PowerShell Start-Process -Verb RunAs to trigger UAC prompt.
|
|
187
|
-
"""
|
|
188
|
-
log = log_callback or print
|
|
189
|
-
|
|
190
|
-
# Create a PowerShell script that runs DISM elevated
|
|
191
|
-
ps_script = '''
|
|
192
|
-
$result = Start-Process -FilePath "DISM.exe" -ArgumentList "/Online", "/Add-Capability", "/CapabilityName:Media.MediaFeaturePack~~~~0.0.1.0" -Verb RunAs -Wait -PassThru
|
|
193
|
-
exit $result.ExitCode
|
|
194
|
-
'''
|
|
195
|
-
|
|
196
|
-
try:
|
|
197
|
-
result = subprocess.run(
|
|
198
|
-
["powershell", "-ExecutionPolicy", "Bypass", "-Command", ps_script],
|
|
199
|
-
capture_output=True,
|
|
200
|
-
text=True,
|
|
201
|
-
timeout=300
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
if result.returncode == 0:
|
|
205
|
-
# Verify installation
|
|
206
|
-
if self.check_media_foundation():
|
|
207
|
-
log("Media Foundation installed successfully!")
|
|
208
|
-
return (True, None)
|
|
209
|
-
else:
|
|
210
|
-
return (False,
|
|
211
|
-
"Installation completed but Media Foundation DLLs not found.\n"
|
|
212
|
-
"A system restart may be required.")
|
|
213
|
-
else:
|
|
214
|
-
return (False,
|
|
215
|
-
f"Installation failed or was cancelled.\n"
|
|
216
|
-
f"Please install Media Feature Pack manually:\n"
|
|
217
|
-
f" Settings > Apps > Optional Features > Add a feature > Media Feature Pack")
|
|
218
|
-
|
|
219
|
-
except subprocess.TimeoutExpired:
|
|
220
|
-
return (False, "Installation timed out. Please try installing manually via Settings.")
|
|
221
|
-
except Exception as e:
|
|
222
|
-
return (False, f"Error during elevated installation: {e}")
|
|
223
|
-
|
|
224
|
-
def ensure_media_foundation(self, log_callback=None) -> Tuple[bool, Optional[str]]:
|
|
225
|
-
"""
|
|
226
|
-
Ensure Media Foundation is installed, installing if necessary.
|
|
227
|
-
|
|
228
|
-
This is the main entry point for MF dependency checking.
|
|
229
|
-
"""
|
|
230
|
-
if self.check_media_foundation():
|
|
231
|
-
return (True, None)
|
|
232
|
-
|
|
233
|
-
return self.install_media_foundation(log_callback)
|
|
234
|
-
|
|
235
|
-
# =========================================================================
|
|
236
|
-
# OpenCV DLL Directory Setup
|
|
237
|
-
# =========================================================================
|
|
238
|
-
|
|
239
|
-
def setup_opencv_dll_paths(self, env_dir: Path) -> Tuple[bool, Optional[str]]:
|
|
240
|
-
"""
|
|
241
|
-
Set up the DLL directory structure that opencv-python expects.
|
|
242
|
-
|
|
243
|
-
OpenCV's config.py expects VC++ DLLs at:
|
|
244
|
-
{site-packages}/cv2/../../x64/vc17/bin
|
|
245
|
-
Which resolves to:
|
|
246
|
-
{env_dir}/Lib/x64/vc17/bin
|
|
247
|
-
|
|
248
|
-
This copies the VC++ DLLs to that location.
|
|
249
|
-
"""
|
|
250
|
-
# Target directory that opencv expects
|
|
251
|
-
target_dir = env_dir / "Lib" / "x64" / "vc17" / "bin"
|
|
252
|
-
|
|
253
|
-
# Source: DLLs in Scripts or base env dir (from msvc-runtime package)
|
|
254
|
-
scripts_dir = env_dir / "Scripts"
|
|
255
|
-
|
|
256
|
-
vc_dlls = [
|
|
257
|
-
'vcruntime140.dll', 'vcruntime140_1.dll', 'vcruntime140_threads.dll',
|
|
258
|
-
'msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll',
|
|
259
|
-
'msvcp140_atomic_wait.dll', 'msvcp140_codecvt_ids.dll',
|
|
260
|
-
'concrt140.dll', 'vcomp140.dll', 'vcamp140.dll', 'vccorlib140.dll'
|
|
261
|
-
]
|
|
262
|
-
|
|
263
|
-
try:
|
|
264
|
-
target_dir.mkdir(parents=True, exist_ok=True)
|
|
265
|
-
|
|
266
|
-
copied = 0
|
|
267
|
-
for dll_name in vc_dlls:
|
|
268
|
-
# Try Scripts first, then env root
|
|
269
|
-
for source_dir in [scripts_dir, env_dir]:
|
|
270
|
-
source = source_dir / dll_name
|
|
271
|
-
if source.exists():
|
|
272
|
-
target = target_dir / dll_name
|
|
273
|
-
if not target.exists():
|
|
274
|
-
shutil.copy2(source, target)
|
|
275
|
-
copied += 1
|
|
276
|
-
break
|
|
277
|
-
|
|
278
|
-
if copied > 0:
|
|
279
|
-
return (True, f"Copied {copied} VC++ DLLs to opencv path")
|
|
280
|
-
else:
|
|
281
|
-
return (True, "VC++ DLLs already in place or not found in venv")
|
|
282
|
-
|
|
283
|
-
except Exception as e:
|
|
284
|
-
return (False, f"Failed to set up opencv DLL paths: {e}")
|
comfy_env/pixi/resolver.py
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import platform
|
|
2
|
-
import sys
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import Dict, Optional, Tuple
|
|
5
|
-
|
|
6
|
-
from .cuda_detection import detect_cuda_version, detect_gpu_info
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@dataclass
|
|
10
|
-
class RuntimeEnv:
|
|
11
|
-
"""
|
|
12
|
-
Detected runtime environment for wheel resolution.
|
|
13
|
-
|
|
14
|
-
Contains all variables needed for wheel URL template expansion.
|
|
15
|
-
"""
|
|
16
|
-
# OS/Platform
|
|
17
|
-
os_name: str # linux, windows, darwin
|
|
18
|
-
platform_tag: str # linux_x86_64, win_amd64, macosx_...
|
|
19
|
-
|
|
20
|
-
# Python
|
|
21
|
-
python_version: str # 3.10, 3.11, 3.12
|
|
22
|
-
python_short: str # 310, 311, 312
|
|
23
|
-
|
|
24
|
-
# CUDA
|
|
25
|
-
cuda_version: Optional[str] # 12.8, 12.4, None
|
|
26
|
-
cuda_short: Optional[str] # 128, 124, None
|
|
27
|
-
|
|
28
|
-
# PyTorch (detected or configured)
|
|
29
|
-
torch_version: Optional[str] # 2.8.0, 2.5.1
|
|
30
|
-
torch_short: Optional[str] # 280, 251
|
|
31
|
-
torch_mm: Optional[str] # 28, 25 (major.minor without dot)
|
|
32
|
-
|
|
33
|
-
# GPU info
|
|
34
|
-
gpu_name: Optional[str] = None
|
|
35
|
-
gpu_compute: Optional[str] = None # sm_89, sm_100
|
|
36
|
-
|
|
37
|
-
@classmethod
|
|
38
|
-
def detect(cls, torch_version: Optional[str] = None) -> "RuntimeEnv":
|
|
39
|
-
"""
|
|
40
|
-
Detect runtime environment from current system.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
torch_version: Optional PyTorch version override. If not provided,
|
|
44
|
-
attempts to detect from installed torch.
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
RuntimeEnv with detected values.
|
|
48
|
-
"""
|
|
49
|
-
# OS detection
|
|
50
|
-
os_name = sys.platform
|
|
51
|
-
if os_name.startswith('linux'):
|
|
52
|
-
os_name = 'linux'
|
|
53
|
-
elif os_name == 'win32':
|
|
54
|
-
os_name = 'windows'
|
|
55
|
-
elif os_name == 'darwin':
|
|
56
|
-
os_name = 'darwin'
|
|
57
|
-
|
|
58
|
-
# Platform tag
|
|
59
|
-
platform_tag = _get_platform_tag()
|
|
60
|
-
|
|
61
|
-
# Python version
|
|
62
|
-
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
63
|
-
py_short = f"{sys.version_info.major}{sys.version_info.minor}"
|
|
64
|
-
|
|
65
|
-
# CUDA version
|
|
66
|
-
cuda_version = detect_cuda_version()
|
|
67
|
-
cuda_short = cuda_version.replace(".", "") if cuda_version else None
|
|
68
|
-
|
|
69
|
-
# PyTorch version
|
|
70
|
-
if torch_version is None:
|
|
71
|
-
torch_version = _detect_torch_version()
|
|
72
|
-
|
|
73
|
-
torch_short = None
|
|
74
|
-
torch_mm = None
|
|
75
|
-
if torch_version:
|
|
76
|
-
torch_short = torch_version.replace(".", "")
|
|
77
|
-
parts = torch_version.split(".")[:2]
|
|
78
|
-
torch_mm = "".join(parts)
|
|
79
|
-
|
|
80
|
-
# GPU info
|
|
81
|
-
gpu_name = None
|
|
82
|
-
gpu_compute = None
|
|
83
|
-
try:
|
|
84
|
-
gpu_info = detect_gpu_info()
|
|
85
|
-
if gpu_info:
|
|
86
|
-
gpu_name = gpu_info[0].get("name") if isinstance(gpu_info, list) else gpu_info.get("name")
|
|
87
|
-
gpu_compute = gpu_info[0].get("compute_capability") if isinstance(gpu_info, list) else gpu_info.get("compute_capability")
|
|
88
|
-
except Exception:
|
|
89
|
-
pass
|
|
90
|
-
|
|
91
|
-
return cls(
|
|
92
|
-
os_name=os_name,
|
|
93
|
-
platform_tag=platform_tag,
|
|
94
|
-
python_version=py_version,
|
|
95
|
-
python_short=py_short,
|
|
96
|
-
cuda_version=cuda_version,
|
|
97
|
-
cuda_short=cuda_short,
|
|
98
|
-
torch_version=torch_version,
|
|
99
|
-
torch_short=torch_short,
|
|
100
|
-
torch_mm=torch_mm,
|
|
101
|
-
gpu_name=gpu_name,
|
|
102
|
-
gpu_compute=gpu_compute,
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
def as_dict(self) -> Dict[str, str]:
|
|
106
|
-
"""Convert to dict for template substitution."""
|
|
107
|
-
py_minor = self.python_version.split(".")[-1] if self.python_version else ""
|
|
108
|
-
|
|
109
|
-
result = {
|
|
110
|
-
"os": self.os_name,
|
|
111
|
-
"platform": self.platform_tag,
|
|
112
|
-
"python_version": self.python_version,
|
|
113
|
-
"py_version": self.python_version,
|
|
114
|
-
"py_short": self.python_short,
|
|
115
|
-
"py_minor": py_minor,
|
|
116
|
-
"py_tag": f"cp{self.python_short}",
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if self.cuda_version:
|
|
120
|
-
result["cuda_version"] = self.cuda_version
|
|
121
|
-
result["cuda_short"] = self.cuda_short
|
|
122
|
-
result["cuda_major"] = self.cuda_version.split(".")[0]
|
|
123
|
-
|
|
124
|
-
if self.torch_version:
|
|
125
|
-
result["torch_version"] = self.torch_version
|
|
126
|
-
result["torch_short"] = self.torch_short
|
|
127
|
-
result["torch_mm"] = self.torch_mm
|
|
128
|
-
parts = self.torch_version.split(".")[:2]
|
|
129
|
-
result["torch_dotted_mm"] = ".".join(parts)
|
|
130
|
-
|
|
131
|
-
return result
|
|
132
|
-
|
|
133
|
-
def __str__(self) -> str:
|
|
134
|
-
parts = [
|
|
135
|
-
f"Python {self.python_version}",
|
|
136
|
-
f"CUDA {self.cuda_version}" if self.cuda_version else "CPU",
|
|
137
|
-
]
|
|
138
|
-
if self.torch_version:
|
|
139
|
-
parts.append(f"PyTorch {self.torch_version}")
|
|
140
|
-
if self.gpu_name:
|
|
141
|
-
parts.append(f"GPU: {self.gpu_name}")
|
|
142
|
-
return ", ".join(parts)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def _get_platform_tag() -> str:
|
|
146
|
-
"""Get wheel platform tag for current system."""
|
|
147
|
-
machine = platform.machine().lower()
|
|
148
|
-
|
|
149
|
-
if sys.platform.startswith('linux'):
|
|
150
|
-
if machine in ('x86_64', 'amd64'):
|
|
151
|
-
return 'linux_x86_64'
|
|
152
|
-
elif machine == 'aarch64':
|
|
153
|
-
return 'linux_aarch64'
|
|
154
|
-
return f'linux_{machine}'
|
|
155
|
-
|
|
156
|
-
elif sys.platform == 'win32':
|
|
157
|
-
if machine in ('amd64', 'x86_64'):
|
|
158
|
-
return 'win_amd64'
|
|
159
|
-
return 'win32'
|
|
160
|
-
|
|
161
|
-
elif sys.platform == 'darwin':
|
|
162
|
-
if machine == 'arm64':
|
|
163
|
-
return 'macosx_11_0_arm64'
|
|
164
|
-
return 'macosx_10_9_x86_64'
|
|
165
|
-
|
|
166
|
-
return f'{sys.platform}_{machine}'
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def _detect_torch_version() -> Optional[str]:
|
|
170
|
-
"""Detect installed PyTorch version."""
|
|
171
|
-
try:
|
|
172
|
-
import torch
|
|
173
|
-
version = torch.__version__
|
|
174
|
-
if '+' in version:
|
|
175
|
-
version = version.split('+')[0]
|
|
176
|
-
return version
|
|
177
|
-
except ImportError:
|
|
178
|
-
return None
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def parse_wheel_requirement(req: str) -> Tuple[str, Optional[str]]:
|
|
182
|
-
"""
|
|
183
|
-
Parse a wheel requirement string.
|
|
184
|
-
|
|
185
|
-
Examples:
|
|
186
|
-
"nvdiffrast==0.4.0" -> ("nvdiffrast", "0.4.0")
|
|
187
|
-
"pytorch3d>=0.7.8" -> ("pytorch3d", "0.7.8")
|
|
188
|
-
"torch-cluster" -> ("torch-cluster", None)
|
|
189
|
-
|
|
190
|
-
Returns:
|
|
191
|
-
Tuple of (package_name, version_or_None).
|
|
192
|
-
"""
|
|
193
|
-
for op in ['==', '>=', '<=', '~=', '!=', '>', '<']:
|
|
194
|
-
if op in req:
|
|
195
|
-
parts = req.split(op, 1)
|
|
196
|
-
return (parts[0].strip(), parts[1].strip())
|
|
197
|
-
|
|
198
|
-
return (req.strip(), None)
|
comfy_env/prestartup.py
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Prestartup helpers for ComfyUI custom nodes.
|
|
3
|
-
|
|
4
|
-
Call setup_env() in your prestartup_script.py before any native imports.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import glob
|
|
8
|
-
import os
|
|
9
|
-
import sys
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Optional, Dict
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def get_env_name(dir_name: str) -> str:
|
|
15
|
-
"""Convert directory name to env name: ComfyUI-UniRig -> _env_unirig"""
|
|
16
|
-
name = dir_name.lower().replace("-", "_").lstrip("comfyui_")
|
|
17
|
-
return f"_env_{name}"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _load_env_vars(config_path: str) -> Dict[str, str]:
|
|
21
|
-
"""
|
|
22
|
-
Load [env_vars] section from comfy-env.toml.
|
|
23
|
-
|
|
24
|
-
Uses tomllib (Python 3.11+) or tomli fallback.
|
|
25
|
-
Returns empty dict if file not found or parsing fails.
|
|
26
|
-
"""
|
|
27
|
-
if not os.path.exists(config_path):
|
|
28
|
-
return {}
|
|
29
|
-
|
|
30
|
-
try:
|
|
31
|
-
if sys.version_info >= (3, 11):
|
|
32
|
-
import tomllib
|
|
33
|
-
else:
|
|
34
|
-
try:
|
|
35
|
-
import tomli as tomllib
|
|
36
|
-
except ImportError:
|
|
37
|
-
return {}
|
|
38
|
-
|
|
39
|
-
with open(config_path, "rb") as f:
|
|
40
|
-
data = tomllib.load(f)
|
|
41
|
-
|
|
42
|
-
env_vars_data = data.get("env_vars", {})
|
|
43
|
-
return {str(k): str(v) for k, v in env_vars_data.items()}
|
|
44
|
-
except Exception:
|
|
45
|
-
return {}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _dedupe_libomp_macos():
|
|
49
|
-
"""
|
|
50
|
-
macOS: Dedupe libomp.dylib to prevent OpenMP runtime conflicts.
|
|
51
|
-
|
|
52
|
-
Torch and other packages (PyMeshLab, etc.) bundle their own libomp.
|
|
53
|
-
Two OpenMP runtimes in same process = crash.
|
|
54
|
-
Fix: symlink all libomp copies to torch's (canonical source).
|
|
55
|
-
"""
|
|
56
|
-
if sys.platform != "darwin":
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
# Find torch's libomp (canonical source) via introspection
|
|
60
|
-
try:
|
|
61
|
-
import torch
|
|
62
|
-
torch_libomp = os.path.join(os.path.dirname(torch.__file__), 'lib', 'libomp.dylib')
|
|
63
|
-
if not os.path.exists(torch_libomp):
|
|
64
|
-
return
|
|
65
|
-
except ImportError:
|
|
66
|
-
return # No torch, skip
|
|
67
|
-
|
|
68
|
-
# Find site-packages for scanning
|
|
69
|
-
site_packages = os.path.dirname(os.path.dirname(torch.__file__))
|
|
70
|
-
|
|
71
|
-
# Find other libomp files and symlink to torch's
|
|
72
|
-
patterns = [
|
|
73
|
-
os.path.join(site_packages, '*', 'Frameworks', 'libomp.dylib'),
|
|
74
|
-
os.path.join(site_packages, '*', '.dylibs', 'libomp.dylib'),
|
|
75
|
-
os.path.join(site_packages, '*', 'lib', 'libomp.dylib'),
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
for pattern in patterns:
|
|
79
|
-
for libomp in glob.glob(pattern):
|
|
80
|
-
# Skip torch's own copy
|
|
81
|
-
if 'torch' in libomp:
|
|
82
|
-
continue
|
|
83
|
-
# Skip if already a symlink pointing to torch
|
|
84
|
-
if os.path.islink(libomp):
|
|
85
|
-
if os.path.realpath(libomp) == os.path.realpath(torch_libomp):
|
|
86
|
-
continue
|
|
87
|
-
# Replace with symlink to torch's
|
|
88
|
-
try:
|
|
89
|
-
if os.path.islink(libomp):
|
|
90
|
-
os.unlink(libomp)
|
|
91
|
-
else:
|
|
92
|
-
os.rename(libomp, libomp + '.bak')
|
|
93
|
-
os.symlink(torch_libomp, libomp)
|
|
94
|
-
except OSError:
|
|
95
|
-
pass # Permission denied, etc.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def setup_env(node_dir: Optional[str] = None) -> None:
|
|
99
|
-
"""
|
|
100
|
-
Set up environment for pixi conda libraries.
|
|
101
|
-
|
|
102
|
-
Call this in prestartup_script.py before any native library imports.
|
|
103
|
-
- Applies [env_vars] from comfy-env.toml first (for OpenMP settings, etc.)
|
|
104
|
-
- Sets LD_LIBRARY_PATH (Linux/Mac) or PATH (Windows) for conda libs
|
|
105
|
-
- Adds pixi site-packages to sys.path
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
node_dir: Path to the custom node directory. Auto-detected if not provided.
|
|
109
|
-
|
|
110
|
-
Example:
|
|
111
|
-
# In prestartup_script.py:
|
|
112
|
-
from comfy_env import setup_env
|
|
113
|
-
setup_env()
|
|
114
|
-
"""
|
|
115
|
-
# macOS: Dedupe libomp to prevent OpenMP conflicts (torch vs pymeshlab, etc.)
|
|
116
|
-
_dedupe_libomp_macos()
|
|
117
|
-
|
|
118
|
-
# Auto-detect node_dir from caller
|
|
119
|
-
if node_dir is None:
|
|
120
|
-
import inspect
|
|
121
|
-
frame = inspect.stack()[1]
|
|
122
|
-
node_dir = str(Path(frame.filename).parent)
|
|
123
|
-
|
|
124
|
-
# Apply [env_vars] from comfy-env.toml FIRST (before any library loading)
|
|
125
|
-
config_path = os.path.join(node_dir, "comfy-env.toml")
|
|
126
|
-
env_vars = _load_env_vars(config_path)
|
|
127
|
-
for key, value in env_vars.items():
|
|
128
|
-
os.environ[key] = value
|
|
129
|
-
|
|
130
|
-
# Resolve environment path with fallback chain:
|
|
131
|
-
# 1. Marker file -> central cache
|
|
132
|
-
# 2. _env_<name> (current local)
|
|
133
|
-
# 3. .pixi/envs/default (old pixi)
|
|
134
|
-
pixi_env = None
|
|
135
|
-
|
|
136
|
-
# 1. Check marker file -> central cache
|
|
137
|
-
marker_path = os.path.join(node_dir, ".comfy-env-marker.toml")
|
|
138
|
-
if os.path.exists(marker_path):
|
|
139
|
-
try:
|
|
140
|
-
if sys.version_info >= (3, 11):
|
|
141
|
-
import tomllib
|
|
142
|
-
else:
|
|
143
|
-
import tomli as tomllib
|
|
144
|
-
with open(marker_path, "rb") as f:
|
|
145
|
-
marker = tomllib.load(f)
|
|
146
|
-
env_path = marker.get("env", {}).get("path")
|
|
147
|
-
if env_path and os.path.exists(env_path):
|
|
148
|
-
pixi_env = env_path
|
|
149
|
-
except Exception:
|
|
150
|
-
pass # Fall through to other options
|
|
151
|
-
|
|
152
|
-
# 2. Check _env_<name> (local)
|
|
153
|
-
if not pixi_env:
|
|
154
|
-
env_name = get_env_name(os.path.basename(node_dir))
|
|
155
|
-
local_env = os.path.join(node_dir, env_name)
|
|
156
|
-
if os.path.exists(local_env):
|
|
157
|
-
pixi_env = local_env
|
|
158
|
-
|
|
159
|
-
# 3. Fallback to old .pixi path
|
|
160
|
-
if not pixi_env:
|
|
161
|
-
old_pixi = os.path.join(node_dir, ".pixi", "envs", "default")
|
|
162
|
-
if os.path.exists(old_pixi):
|
|
163
|
-
pixi_env = old_pixi
|
|
164
|
-
|
|
165
|
-
if not pixi_env:
|
|
166
|
-
return # No environment found
|
|
167
|
-
|
|
168
|
-
if sys.platform == "win32":
|
|
169
|
-
# Windows: add to PATH for DLL loading
|
|
170
|
-
lib_dir = os.path.join(pixi_env, "Library", "bin")
|
|
171
|
-
if os.path.exists(lib_dir):
|
|
172
|
-
os.environ["PATH"] = lib_dir + ";" + os.environ.get("PATH", "")
|
|
173
|
-
elif sys.platform == "darwin":
|
|
174
|
-
# macOS: DYLD_LIBRARY_PATH
|
|
175
|
-
lib_dir = os.path.join(pixi_env, "lib")
|
|
176
|
-
if os.path.exists(lib_dir):
|
|
177
|
-
os.environ["DYLD_LIBRARY_PATH"] = lib_dir + ":" + os.environ.get("DYLD_LIBRARY_PATH", "")
|
|
178
|
-
else:
|
|
179
|
-
# Linux: LD_LIBRARY_PATH
|
|
180
|
-
lib_dir = os.path.join(pixi_env, "lib")
|
|
181
|
-
if os.path.exists(lib_dir):
|
|
182
|
-
os.environ["LD_LIBRARY_PATH"] = lib_dir + ":" + os.environ.get("LD_LIBRARY_PATH", "")
|
|
183
|
-
|
|
184
|
-
# Add site-packages to sys.path for pixi-installed Python packages
|
|
185
|
-
if sys.platform == "win32":
|
|
186
|
-
site_packages = os.path.join(pixi_env, "Lib", "site-packages")
|
|
187
|
-
else:
|
|
188
|
-
matches = glob.glob(os.path.join(pixi_env, "lib", "python*", "site-packages"))
|
|
189
|
-
site_packages = matches[0] if matches else None
|
|
190
|
-
|
|
191
|
-
if site_packages and os.path.exists(site_packages) and site_packages not in sys.path:
|
|
192
|
-
sys.path.insert(0, site_packages)
|