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
@@ -0,0 +1,279 @@
1
+ Metadata-Version: 2.4
2
+ Name: comfy-env
3
+ Version: 0.1.16
4
+ Summary: Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation
5
+ Project-URL: Homepage, https://github.com/PozzettiAndrea/comfy-env
6
+ Project-URL: Repository, https://github.com/PozzettiAndrea/comfy-env
7
+ Project-URL: Issues, https://github.com/PozzettiAndrea/comfy-env/issues
8
+ Author: Andrea Pozzetti
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: comfyui,cuda,environment,isolation,process,venv,wheels
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: pip>=21.0
21
+ Requires-Dist: tomli-w>=1.0.0
22
+ Requires-Dist: tomli>=2.0.0
23
+ Requires-Dist: uv>=0.4.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy; extra == 'dev'
26
+ Requires-Dist: pytest; extra == 'dev'
27
+ Requires-Dist: ruff; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # comfy-env
31
+
32
+ Environment management for ComfyUI custom nodes.
33
+
34
+ ## Quick Start
35
+
36
+ ```bash
37
+ pip install comfy-env
38
+ ```
39
+
40
+ **1. Create `comfy-env.toml` in your node directory:**
41
+
42
+ ```toml
43
+ [cuda]
44
+ packages = ["nvdiffrast", "pytorch3d"]
45
+
46
+ [pypi-dependencies]
47
+ trimesh = { version = "*", extras = ["easy"] }
48
+ ```
49
+
50
+ **2. In `install.py`:**
51
+
52
+ ```python
53
+ from comfy_env import install
54
+ install()
55
+ ```
56
+
57
+ **3. In `prestartup_script.py`:**
58
+
59
+ ```python
60
+ from comfy_env import setup_env
61
+ setup_env()
62
+ ```
63
+
64
+ That's it. CUDA wheels install without compilation, and the environment is ready.
65
+
66
+ ---
67
+
68
+ ## Configuration
69
+
70
+ Create `comfy-env.toml` in your node directory:
71
+
72
+ ```toml
73
+ # Python version for isolated environment (optional)
74
+ python = "3.11"
75
+
76
+ # CUDA packages from cuda-wheels index (no compilation needed)
77
+ [cuda]
78
+ packages = ["nvdiffrast", "pytorch3d", "flash-attn"]
79
+
80
+ # System packages (Linux only)
81
+ [apt]
82
+ packages = ["libgl1-mesa-glx", "libglu1-mesa"]
83
+
84
+ # Environment variables
85
+ [env_vars]
86
+ KMP_DUPLICATE_LIB_OK = "TRUE"
87
+ OMP_NUM_THREADS = "1"
88
+
89
+ # Dependent custom nodes to auto-install
90
+ [node_reqs]
91
+ ComfyUI_essentials = "cubiq/ComfyUI_essentials"
92
+
93
+ # Conda packages (via pixi)
94
+ [dependencies]
95
+ cgal = "*"
96
+
97
+ # PyPI packages
98
+ [pypi-dependencies]
99
+ trimesh = { version = "*", extras = ["easy"] }
100
+ numpy = "*"
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Process Isolation
106
+
107
+ For nodes with conflicting dependencies, use isolated execution:
108
+
109
+ ```python
110
+ # In nodes/__init__.py
111
+ from pathlib import Path
112
+ from comfy_env import wrap_isolated_nodes
113
+
114
+ # Import your isolated nodes
115
+ from .cgal import NODE_CLASS_MAPPINGS as cgal_mappings
116
+
117
+ # Wrap them for isolated execution
118
+ NODE_CLASS_MAPPINGS = wrap_isolated_nodes(
119
+ cgal_mappings,
120
+ Path(__file__).parent / "cgal" # Directory with comfy-env.toml
121
+ )
122
+ ```
123
+
124
+ Each wrapped node runs in a subprocess with its own Python environment.
125
+
126
+ ---
127
+
128
+ ## CLI Commands
129
+
130
+ ```bash
131
+ # Show detected environment
132
+ comfy-env info
133
+
134
+ # Install dependencies
135
+ comfy-env install
136
+
137
+ # Preview without installing
138
+ comfy-env install --dry-run
139
+
140
+ # Verify packages
141
+ comfy-env doctor
142
+
143
+ # Install system packages
144
+ comfy-env apt-install
145
+ ```
146
+
147
+ ---
148
+
149
+ ## API Reference
150
+
151
+ ### install()
152
+
153
+ Install dependencies from comfy-env.toml:
154
+
155
+ ```python
156
+ from comfy_env import install
157
+
158
+ install() # Auto-detect config
159
+ install(dry_run=True) # Preview only
160
+ install(config="path.toml") # Explicit config
161
+ ```
162
+
163
+ ### setup_env()
164
+
165
+ Set up environment at ComfyUI startup:
166
+
167
+ ```python
168
+ from comfy_env import setup_env
169
+
170
+ setup_env() # Auto-detects node directory from caller
171
+ ```
172
+
173
+ Sets library paths, environment variables, and injects site-packages.
174
+
175
+ ### wrap_isolated_nodes()
176
+
177
+ Wrap nodes for subprocess isolation:
178
+
179
+ ```python
180
+ from comfy_env import wrap_isolated_nodes
181
+
182
+ wrapped = wrap_isolated_nodes(NODE_CLASS_MAPPINGS, node_dir)
183
+ ```
184
+
185
+ ### Detection
186
+
187
+ ```python
188
+ from comfy_env import (
189
+ detect_cuda_version, # Returns "12.8", "12.4", or None
190
+ detect_gpu, # Returns GPUInfo or None
191
+ get_gpu_summary, # Human-readable string
192
+ RuntimeEnv, # Combined runtime info
193
+ )
194
+
195
+ env = RuntimeEnv.detect()
196
+ print(env) # Python 3.11, CUDA 12.8, PyTorch 2.8.0, GPU: RTX 4090
197
+ ```
198
+
199
+ ### Workers
200
+
201
+ Low-level process isolation:
202
+
203
+ ```python
204
+ from comfy_env import MPWorker, SubprocessWorker
205
+
206
+ # Same Python version (multiprocessing)
207
+ worker = MPWorker()
208
+ result = worker.call(my_function, arg1, arg2)
209
+
210
+ # Different Python version (subprocess)
211
+ worker = SubprocessWorker(python="/path/to/python")
212
+ result = worker.call(my_function, arg1, arg2)
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Real Example
218
+
219
+ See [ComfyUI-GeometryPack](https://github.com/PozzettiAndrea/ComfyUI-GeometryPack) for a production example with:
220
+
221
+ - Multiple isolated environments (CGAL, Blender, GPU)
222
+ - Per-subdirectory comfy-env.toml
223
+ - Prestartup asset copying
224
+ - Different Python versions (3.11 for Blender API)
225
+
226
+ ---
227
+
228
+ ## Architecture
229
+
230
+ ### Layers
231
+
232
+ ```
233
+ comfy_env/
234
+ ├── detection/ # Pure functions - CUDA, GPU, platform detection
235
+ ├── config/ # Pure parsing - comfy-env.toml → typed config
236
+ ├── environment/ # Side effects - cache, paths, setup
237
+ ├── packages/ # Side effects - pixi, cuda-wheels, apt
238
+ ├── isolation/ # Side effects - subprocess workers, node wrapping
239
+ └── install.py # Orchestration
240
+ ```
241
+
242
+ ### Why Isolation?
243
+
244
+ ComfyUI nodes share a single Python environment. This breaks when:
245
+
246
+ 1. **Dependency conflicts**: Node A needs `torch==2.4`, Node B needs `torch==2.8`
247
+ 2. **Native library conflicts**: Two packages bundle incompatible libomp
248
+ 3. **Python version requirements**: Blender API requires Python 3.11
249
+
250
+ Solution: Run each node group in its own subprocess with isolated dependencies.
251
+
252
+ ### Why CUDA Wheels?
253
+
254
+ Installing packages like `nvdiffrast` normally requires:
255
+ - CUDA toolkit
256
+ - C++ compiler
257
+ - 30+ minutes of compilation
258
+
259
+ CUDA wheels from [cuda-wheels](https://pozzettiandrea.github.io/cuda-wheels/) are pre-built for common configurations:
260
+
261
+ | GPU | CUDA | PyTorch |
262
+ |-----|------|---------|
263
+ | Blackwell (sm_100+) | 12.8 | 2.8 |
264
+ | Ada/Hopper/Ampere | 12.8 | 2.8 |
265
+ | Turing | 12.8 | 2.8 |
266
+ | Pascal | 12.4 | 2.4 |
267
+
268
+ ### How Environments Work
269
+
270
+ 1. **Central cache**: Environments stored at `~/.comfy-env/envs/`
271
+ 2. **Marker files**: `.comfy-env-marker.toml` links node → env
272
+ 3. **Orphan cleanup**: Envs deleted when their node is removed
273
+ 4. **Hash-based naming**: Config changes create new envs
274
+
275
+ ---
276
+
277
+ ## License
278
+
279
+ MIT
@@ -0,0 +1,36 @@
1
+ comfy_env/__init__.py,sha256=uoHq-m1274tOeinWsIpW81MkRQilHx9z8_coJSqH9qc,4641
2
+ comfy_env/cli.py,sha256=abdUBItk8OkzxKBu7cKRPizO5_i5DFflvx7pYpPc7OM,6696
3
+ comfy_env/install.py,sha256=BV8OfY3Rt-BiKFzpDNHfs4mfLYalbsiIMNU-y9CmLDg,10467
4
+ comfy_env/config/__init__.py,sha256=noqhycE0UF4ZntV5NnWcFApppQludQDHvJOtLsQgUIo,542
5
+ comfy_env/config/parser.py,sha256=79C8LIwCO4920MU7CJ08hfMWh-5Ehzr-i5XQna5K81A,1951
6
+ comfy_env/config/types.py,sha256=vFgWeEl_p26OmcoUv0wAOlHe9GBio2isjIWl7kACKFY,1096
7
+ comfy_env/detection/__init__.py,sha256=dH84PlSRJfs8MRJp2gp9_NX8ZzGIDHR8iZXy7_B8Ez4,1671
8
+ comfy_env/detection/cuda.py,sha256=BOaRQOGP2yoaPCO9eqPvWBB5Us_MNo-sSbadQsIjHqM,1708
9
+ comfy_env/detection/gpu.py,sha256=Rf7pgtZXzUbJqcXzZXQi-yK5naeuSP1FiL6SdreeADM,8393
10
+ comfy_env/detection/platform.py,sha256=Xe01dIZm7JT19kIH-j11h7KIBVRaKTLh8u4TzI3uZ6E,2127
11
+ comfy_env/detection/runtime.py,sha256=gDplni7ZPGW7WjNJuqWbtgSwkWz27kBWSFvYbhXun6o,3756
12
+ comfy_env/environment/__init__.py,sha256=WfZnyOvbI0MrDQPYTtOG2kHn0XCSCrqKcOlJcmB29nU,1009
13
+ comfy_env/environment/cache.py,sha256=RGfVW2caMO0Dd1nX2otUQP0xW3pVS7iSOP4vIUAMdEA,4568
14
+ comfy_env/environment/libomp.py,sha256=nzr3kDnRLgcf9CZ_WF4ItWskqEDS2S0geqZS43XoKig,1319
15
+ comfy_env/environment/paths.py,sha256=5TFFAkOZXa8R3cHfVHDEFnwy6_JcHilVBOHJuy-yqR0,1129
16
+ comfy_env/environment/setup.py,sha256=34To-cJX85sZ5W33dxcNosedrrICNUzVzqBcJkq4FLI,3013
17
+ comfy_env/isolation/__init__.py,sha256=XfMLEiBIcEzHG_k2vk9fT9GvFfmOsfbpM26czuxbdRI,800
18
+ comfy_env/isolation/tensor_utils.py,sha256=2_f4jjylqCPaPldD1Jw-es5CyOtuF5I1ROdyEIxsg-U,2951
19
+ comfy_env/isolation/wrap.py,sha256=K7GAkqU_Uxe717eUtPsFv5kcr_Jfbh3x79A-8vbY1nY,8592
20
+ comfy_env/isolation/workers/__init__.py,sha256=Zp6sZSRBcb5Negqgzqs3jPjfO9T1u3nNrQhp6WqTAuc,325
21
+ comfy_env/isolation/workers/base.py,sha256=4ZYTaQ4J0kBHCoO_OfZnsowm4rJCoqinZUaOtgkOPbw,2307
22
+ comfy_env/isolation/workers/mp.py,sha256=ygOgx2iyLN7l5fWkKI4lqzQsDyfAAd9Gb4gTYLp7o1A,34061
23
+ comfy_env/isolation/workers/subprocess.py,sha256=ML6I9IenReagP8iT0Cd2ipet6JPK1gnDbOianOuFwOw,57164
24
+ comfy_env/packages/__init__.py,sha256=6PTwUfUdJDTbIw46dCiA42qk4zUe_gw29xOaklBiMMc,1193
25
+ comfy_env/packages/apt.py,sha256=pxy3A5ZHv3X8ExCVyohODY8Fcy9ji4izIVPfYoxhqT4,1027
26
+ comfy_env/packages/cuda_wheels.py,sha256=G_CnlwNcfeWlEU24aCVBpeqQQ05y8_02dDLBwBFNwII,3980
27
+ comfy_env/packages/node_dependencies.py,sha256=AX_CY6j43tTY5KhyPfU7Wz6zgLAfWF0o0JkTrcNSecg,2966
28
+ comfy_env/packages/pixi.py,sha256=RPu8x5sSOLE1CYAhWMMjoQrbFGGt00fdsbqtRcTz7LQ,3871
29
+ comfy_env/packages/toml_generator.py,sha256=Vhc8F9euHhMTwH1TV6t96-D9Pjrn9jIN4e9WXrCIFE8,3414
30
+ comfy_env/templates/comfy-env-instructions.txt,sha256=ve1RAthW7ouumU9h6DM7mIRX1MS8_Tyonq2U4tcrFu8,1031
31
+ comfy_env/templates/comfy-env.toml,sha256=ROIqi4BlPL1MEdL1VgebfTHpdwPNYGHwWeigI9Kw-1I,4831
32
+ comfy_env-0.1.16.dist-info/METADATA,sha256=SLbnLOFz5AbqS8YT9-TWslRzo4QghO2EwUtQwn2B0OE,6468
33
+ comfy_env-0.1.16.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
+ comfy_env-0.1.16.dist-info/entry_points.txt,sha256=J4fXeqgxU_YenuW_Zxn_pEL7J-3R0--b6MS5t0QmAr0,49
35
+ comfy_env-0.1.16.dist-info/licenses/LICENSE,sha256=E68QZMMpW4P2YKstTZ3QU54HRQO8ecew09XZ4_Vn870,1093
36
+ comfy_env-0.1.16.dist-info/RECORD,,
comfy_env/cache.py DELETED
@@ -1,331 +0,0 @@
1
- """
2
- Central environment cache management for comfy-env.
3
-
4
- Stores environments in ~/.comfy-env/envs/<nodename>_<subfolder>_<hash>/
5
- to avoid Windows MAX_PATH (260 char) issues.
6
-
7
- Marker files in node folders link to central envs and enable orphan cleanup.
8
- """
9
-
10
- import hashlib
11
- import os
12
- import shutil
13
- import sys
14
- from datetime import datetime
15
- from pathlib import Path
16
- from typing import Optional, Tuple, Callable
17
-
18
- # Import version
19
- try:
20
- from . import __version__
21
- except ImportError:
22
- __version__ = "0.0.0-dev"
23
-
24
- # Lazy import tomli/tomllib
25
- def _get_tomli():
26
- if sys.version_info >= (3, 11):
27
- import tomllib
28
- return tomllib
29
- else:
30
- try:
31
- import tomli
32
- return tomli
33
- except ImportError:
34
- return None
35
-
36
- def _get_tomli_w():
37
- try:
38
- import tomli_w
39
- return tomli_w
40
- except ImportError:
41
- return None
42
-
43
-
44
- # Constants
45
- CACHE_DIR = Path.home() / ".comfy-env" / "envs"
46
- MARKER_FILE = ".comfy-env-marker.toml"
47
- METADATA_FILE = ".comfy-env-metadata.toml"
48
-
49
-
50
- def get_cache_dir() -> Path:
51
- """Get central cache directory, create if needed."""
52
- CACHE_DIR.mkdir(parents=True, exist_ok=True)
53
- return CACHE_DIR
54
-
55
-
56
- def compute_config_hash(config_path: Path) -> str:
57
- """Compute hash of comfy-env.toml content (first 8 chars of SHA256)."""
58
- content = config_path.read_bytes()
59
- return hashlib.sha256(content).hexdigest()[:8]
60
-
61
-
62
- def sanitize_name(name: str) -> str:
63
- """Sanitize a name for use in filesystem paths."""
64
- # Lowercase and replace problematic chars
65
- name = name.lower()
66
- for prefix in ("comfyui-", "comfyui_"):
67
- if name.startswith(prefix):
68
- name = name[len(prefix):]
69
- return name.replace("-", "_").replace(" ", "_")
70
-
71
-
72
- def get_env_name(node_dir: Path, config_path: Path) -> str:
73
- """
74
- Generate env name: <nodename>_<subfolder>_<hash>
75
-
76
- Args:
77
- node_dir: The custom node directory (e.g., custom_nodes/ComfyUI-UniRig)
78
- config_path: Path to comfy-env.toml
79
-
80
- Examples:
81
- - ComfyUI-UniRig/nodes/comfy-env.toml -> unirig_nodes_a1b2c3d4
82
- - ComfyUI-Pack/comfy-env.toml -> pack__f5e6d7c8 (double underscore = no subfolder)
83
- """
84
- # Get node name
85
- node_name = sanitize_name(node_dir.name)
86
-
87
- # Get subfolder (relative path from node_dir to config parent)
88
- config_parent = config_path.parent
89
- if config_parent == node_dir:
90
- subfolder = ""
91
- else:
92
- try:
93
- rel_path = config_parent.relative_to(node_dir)
94
- subfolder = rel_path.as_posix().replace("/", "_")
95
- except ValueError:
96
- # config_path not under node_dir - use parent folder name
97
- subfolder = sanitize_name(config_parent.name)
98
-
99
- # Compute hash
100
- config_hash = compute_config_hash(config_path)
101
-
102
- return f"{node_name}_{subfolder}_{config_hash}"
103
-
104
-
105
- def get_central_env_path(node_dir: Path, config_path: Path) -> Path:
106
- """Get path to central environment for this config."""
107
- env_name = get_env_name(node_dir, config_path)
108
- return get_cache_dir() / env_name
109
-
110
-
111
- def write_marker(config_path: Path, env_path: Path) -> None:
112
- """
113
- Write marker file linking node to central env.
114
-
115
- Args:
116
- config_path: Path to comfy-env.toml
117
- env_path: Path to central environment
118
- """
119
- tomli_w = _get_tomli_w()
120
- if not tomli_w:
121
- # Fallback to manual TOML writing
122
- marker_path = config_path.parent / MARKER_FILE
123
- content = f'''[env]
124
- name = "{env_path.name}"
125
- path = "{env_path}"
126
- config_hash = "{compute_config_hash(config_path)}"
127
- created = "{datetime.now().isoformat()}"
128
- comfy_env_version = "{__version__}"
129
- '''
130
- marker_path.write_text(content)
131
- return
132
-
133
- marker_path = config_path.parent / MARKER_FILE
134
- marker_data = {
135
- "env": {
136
- "name": env_path.name,
137
- "path": str(env_path),
138
- "config_hash": compute_config_hash(config_path),
139
- "created": datetime.now().isoformat(),
140
- "comfy_env_version": __version__,
141
- }
142
- }
143
- marker_path.write_text(tomli_w.dumps(marker_data))
144
-
145
-
146
- def write_env_metadata(env_path: Path, marker_path: Path) -> None:
147
- """
148
- Write metadata file in env for orphan detection.
149
-
150
- Args:
151
- env_path: Path to central environment
152
- marker_path: Path to marker file in node folder
153
- """
154
- tomli_w = _get_tomli_w()
155
- metadata_path = env_path / METADATA_FILE
156
-
157
- if not tomli_w:
158
- # Fallback to manual TOML writing
159
- content = f'''marker_path = "{marker_path}"
160
- created = "{datetime.now().isoformat()}"
161
- '''
162
- metadata_path.write_text(content)
163
- return
164
-
165
- metadata = {
166
- "marker_path": str(marker_path),
167
- "created": datetime.now().isoformat(),
168
- }
169
- metadata_path.write_text(tomli_w.dumps(metadata))
170
-
171
-
172
- def read_marker(marker_path: Path) -> Optional[dict]:
173
- """
174
- Read marker file, return None if invalid/missing.
175
-
176
- Args:
177
- marker_path: Path to .comfy-env-marker.toml
178
-
179
- Returns:
180
- Parsed marker data or None
181
- """
182
- if not marker_path.exists():
183
- return None
184
-
185
- tomli = _get_tomli()
186
- if not tomli:
187
- return None
188
-
189
- try:
190
- with open(marker_path, "rb") as f:
191
- return tomli.load(f)
192
- except Exception:
193
- return None
194
-
195
-
196
- def read_env_metadata(env_path: Path) -> Optional[dict]:
197
- """
198
- Read metadata file from env, return None if invalid/missing.
199
-
200
- Args:
201
- env_path: Path to central environment
202
-
203
- Returns:
204
- Parsed metadata or None
205
- """
206
- metadata_path = env_path / METADATA_FILE
207
- if not metadata_path.exists():
208
- return None
209
-
210
- tomli = _get_tomli()
211
- if not tomli:
212
- return None
213
-
214
- try:
215
- with open(metadata_path, "rb") as f:
216
- return tomli.load(f)
217
- except Exception:
218
- return None
219
-
220
-
221
- def resolve_env_path(node_dir: Path) -> Tuple[Optional[Path], Optional[Path], Optional[Path]]:
222
- """
223
- Resolve environment path with fallback chain.
224
-
225
- Args:
226
- node_dir: Directory containing comfy-env.toml or marker file
227
-
228
- Returns:
229
- (env_path, site_packages, lib_dir) or (None, None, None)
230
-
231
- Fallback order:
232
- 1. Marker file -> central cache
233
- 2. _env_<name> (current location)
234
- 3. .pixi/envs/default (old pixi)
235
- 4. .venv (venv support)
236
- """
237
- # 1. Check marker file -> central cache
238
- marker_path = node_dir / MARKER_FILE
239
- marker = read_marker(marker_path)
240
- if marker and "env" in marker:
241
- env_path = Path(marker["env"]["path"])
242
- if env_path.exists():
243
- return _get_env_paths(env_path)
244
-
245
- # 2. Check _env_<name>
246
- node_name = sanitize_name(node_dir.name)
247
- env_name = f"_env_{node_name}"
248
- local_env = node_dir / env_name
249
- if local_env.exists():
250
- return _get_env_paths(local_env)
251
-
252
- # 3. Check .pixi/envs/default
253
- pixi_env = node_dir / ".pixi" / "envs" / "default"
254
- if pixi_env.exists():
255
- return _get_env_paths(pixi_env)
256
-
257
- # 4. Check .venv
258
- venv_dir = node_dir / ".venv"
259
- if venv_dir.exists():
260
- return _get_env_paths(venv_dir)
261
-
262
- return None, None, None
263
-
264
-
265
- def _get_env_paths(env_path: Path) -> Tuple[Path, Optional[Path], Optional[Path]]:
266
- """
267
- Get site-packages and lib paths from an environment.
268
-
269
- Args:
270
- env_path: Path to environment root
271
-
272
- Returns:
273
- (env_path, site_packages, lib_dir)
274
- """
275
- import glob
276
-
277
- if sys.platform == "win32":
278
- site_packages = env_path / "Lib" / "site-packages"
279
- lib_dir = env_path / "Library" / "bin"
280
- else:
281
- # Linux/Mac: lib/python*/site-packages
282
- matches = glob.glob(str(env_path / "lib" / "python*" / "site-packages"))
283
- site_packages = Path(matches[0]) if matches else None
284
- lib_dir = env_path / "lib"
285
-
286
- return env_path, site_packages, lib_dir
287
-
288
-
289
- def cleanup_orphaned_envs(log: Callable[[str], None] = print) -> int:
290
- """
291
- Scan central cache and remove orphaned environments.
292
-
293
- An env is orphaned if its marker file no longer exists
294
- (meaning the node was deleted).
295
-
296
- Args:
297
- log: Logging function
298
-
299
- Returns:
300
- Number of envs cleaned up
301
- """
302
- cache_dir = get_cache_dir()
303
- if not cache_dir.exists():
304
- return 0
305
-
306
- cleaned = 0
307
- for env_dir in cache_dir.iterdir():
308
- if not env_dir.is_dir():
309
- continue
310
-
311
- # Skip if no metadata (might be manually created or old format)
312
- metadata = read_env_metadata(env_dir)
313
- if not metadata:
314
- continue
315
-
316
- # Check if marker file still exists
317
- marker_path_str = metadata.get("marker_path", "")
318
- if not marker_path_str:
319
- continue
320
-
321
- marker_path = Path(marker_path_str)
322
- if not marker_path.exists():
323
- # Marker gone = node was deleted = orphan
324
- log(f"[comfy-env] Cleaning orphaned env: {env_dir.name}")
325
- try:
326
- shutil.rmtree(env_dir)
327
- cleaned += 1
328
- except Exception as e:
329
- log(f"[comfy-env] Failed to cleanup {env_dir.name}: {e}")
330
-
331
- return cleaned