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
|
@@ -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
|