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.
Files changed (51) hide show
  1. comfy_env/__init__.py +115 -62
  2. comfy_env/cli.py +89 -319
  3. comfy_env/config/__init__.py +18 -8
  4. comfy_env/config/parser.py +21 -122
  5. comfy_env/config/types.py +37 -70
  6. comfy_env/detection/__init__.py +77 -0
  7. comfy_env/detection/cuda.py +61 -0
  8. comfy_env/detection/gpu.py +230 -0
  9. comfy_env/detection/platform.py +70 -0
  10. comfy_env/detection/runtime.py +103 -0
  11. comfy_env/environment/__init__.py +53 -0
  12. comfy_env/environment/cache.py +141 -0
  13. comfy_env/environment/libomp.py +41 -0
  14. comfy_env/environment/paths.py +38 -0
  15. comfy_env/environment/setup.py +88 -0
  16. comfy_env/install.py +163 -249
  17. comfy_env/isolation/__init__.py +33 -2
  18. comfy_env/isolation/tensor_utils.py +83 -0
  19. comfy_env/isolation/workers/__init__.py +16 -0
  20. comfy_env/{workers → isolation/workers}/mp.py +1 -1
  21. comfy_env/{workers → isolation/workers}/subprocess.py +2 -2
  22. comfy_env/isolation/wrap.py +149 -409
  23. comfy_env/packages/__init__.py +60 -0
  24. comfy_env/packages/apt.py +36 -0
  25. comfy_env/packages/cuda_wheels.py +97 -0
  26. comfy_env/packages/node_dependencies.py +77 -0
  27. comfy_env/packages/pixi.py +85 -0
  28. comfy_env/packages/toml_generator.py +88 -0
  29. comfy_env-0.1.16.dist-info/METADATA +279 -0
  30. comfy_env-0.1.16.dist-info/RECORD +36 -0
  31. comfy_env/cache.py +0 -331
  32. comfy_env/errors.py +0 -293
  33. comfy_env/nodes.py +0 -187
  34. comfy_env/pixi/__init__.py +0 -48
  35. comfy_env/pixi/core.py +0 -588
  36. comfy_env/pixi/cuda_detection.py +0 -303
  37. comfy_env/pixi/platform/__init__.py +0 -21
  38. comfy_env/pixi/platform/base.py +0 -96
  39. comfy_env/pixi/platform/darwin.py +0 -53
  40. comfy_env/pixi/platform/linux.py +0 -68
  41. comfy_env/pixi/platform/windows.py +0 -284
  42. comfy_env/pixi/resolver.py +0 -198
  43. comfy_env/prestartup.py +0 -192
  44. comfy_env/workers/__init__.py +0 -38
  45. comfy_env/workers/tensor_utils.py +0 -188
  46. comfy_env-0.1.14.dist-info/METADATA +0 -291
  47. comfy_env-0.1.14.dist-info/RECORD +0 -33
  48. /comfy_env/{workers → isolation/workers}/base.py +0 -0
  49. {comfy_env-0.1.14.dist-info → comfy_env-0.1.16.dist-info}/WHEEL +0 -0
  50. {comfy_env-0.1.14.dist-info → comfy_env-0.1.16.dist-info}/entry_points.txt +0 -0
  51. {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}")
@@ -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)