comfy-env 0.0.72__tar.gz → 0.0.74__tar.gz

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 (35) hide show
  1. {comfy_env-0.0.72 → comfy_env-0.0.74}/PKG-INFO +1 -1
  2. {comfy_env-0.0.72 → comfy_env-0.0.74}/pyproject.toml +1 -1
  3. comfy_env-0.0.74/src/comfy_env/install.py +335 -0
  4. comfy_env-0.0.72/src/comfy_env/install.py +0 -171
  5. {comfy_env-0.0.72 → comfy_env-0.0.74}/.github/workflows/ci.yml +0 -0
  6. {comfy_env-0.0.72 → comfy_env-0.0.74}/.github/workflows/publish.yml +0 -0
  7. {comfy_env-0.0.72 → comfy_env-0.0.74}/.gitignore +0 -0
  8. {comfy_env-0.0.72 → comfy_env-0.0.74}/LICENSE +0 -0
  9. {comfy_env-0.0.72 → comfy_env-0.0.74}/README.md +0 -0
  10. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/__init__.py +0 -0
  11. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/cli.py +0 -0
  12. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/config/__init__.py +0 -0
  13. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/config/parser.py +0 -0
  14. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/config/types.py +0 -0
  15. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/errors.py +0 -0
  16. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/isolation/__init__.py +0 -0
  17. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/isolation/wrap.py +0 -0
  18. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/nodes.py +0 -0
  19. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/pixi/__init__.py +0 -0
  20. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/pixi/core.py +0 -0
  21. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/pixi/cuda_detection.py +0 -0
  22. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/pixi/platform/__init__.py +0 -0
  23. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/pixi/platform/base.py +0 -0
  24. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/pixi/platform/darwin.py +0 -0
  25. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/pixi/platform/linux.py +0 -0
  26. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/pixi/platform/windows.py +0 -0
  27. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/pixi/resolver.py +0 -0
  28. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/prestartup.py +0 -0
  29. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/templates/comfy-env-instructions.txt +0 -0
  30. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/templates/comfy-env.toml +0 -0
  31. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/workers/__init__.py +0 -0
  32. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/workers/base.py +0 -0
  33. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/workers/mp.py +0 -0
  34. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/workers/subprocess.py +0 -0
  35. {comfy_env-0.0.72 → comfy_env-0.0.74}/src/comfy_env/workers/tensor_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfy-env
3
- Version: 0.0.72
3
+ Version: 0.0.74
4
4
  Summary: Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation
5
5
  Project-URL: Homepage, https://github.com/PozzettiAndrea/comfy-env
6
6
  Project-URL: Repository, https://github.com/PozzettiAndrea/comfy-env
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "comfy-env"
3
- version = "0.0.72"
3
+ version = "0.0.74"
4
4
  description = "Environment management for ComfyUI custom nodes - CUDA wheel resolution and process isolation"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -0,0 +1,335 @@
1
+ """
2
+ Installation API for comfy-env.
3
+
4
+ Example:
5
+ from comfy_env import install
6
+ install() # Auto-discovers comfy-env.toml and installs
7
+ """
8
+
9
+ import inspect
10
+ from pathlib import Path
11
+ from typing import Callable, List, Optional, Set, Union
12
+
13
+ from .config.types import ComfyEnvConfig, NodeReq
14
+ from .config.parser import load_config, discover_config
15
+
16
+
17
+ def install(
18
+ config: Optional[Union[str, Path]] = None,
19
+ node_dir: Optional[Path] = None,
20
+ log_callback: Optional[Callable[[str], None]] = None,
21
+ dry_run: bool = False,
22
+ ) -> bool:
23
+ """
24
+ Install dependencies from comfy-env.toml.
25
+
26
+ Args:
27
+ config: Optional path to comfy-env.toml. Auto-discovered if not provided.
28
+ node_dir: Optional node directory. Auto-discovered from caller if not provided.
29
+ log_callback: Optional callback for logging. Defaults to print.
30
+ dry_run: If True, show what would be installed without installing.
31
+
32
+ Returns:
33
+ True if installation succeeded.
34
+ """
35
+ # Auto-discover caller's directory if not provided
36
+ if node_dir is None:
37
+ frame = inspect.stack()[1]
38
+ caller_file = frame.filename
39
+ node_dir = Path(caller_file).parent.resolve()
40
+
41
+ log = log_callback or print
42
+
43
+ # Load config
44
+ if config is not None:
45
+ config_path = Path(config)
46
+ if not config_path.is_absolute():
47
+ config_path = node_dir / config_path
48
+ cfg = load_config(config_path)
49
+ else:
50
+ cfg = discover_config(node_dir)
51
+
52
+ if cfg is None:
53
+ raise FileNotFoundError(
54
+ f"No comfy-env.toml found in {node_dir}. "
55
+ "Create comfy-env.toml to define dependencies."
56
+ )
57
+
58
+ # Install apt packages first (Linux only)
59
+ if cfg.apt_packages:
60
+ _install_apt_packages(cfg.apt_packages, log, dry_run)
61
+
62
+ # Set persistent env vars (for OpenMP settings, etc.)
63
+ if cfg.env_vars:
64
+ _set_persistent_env_vars(cfg.env_vars, log, dry_run)
65
+
66
+ # Install node dependencies
67
+ if cfg.node_reqs:
68
+ _install_node_dependencies(cfg.node_reqs, node_dir, log, dry_run)
69
+
70
+ # Install everything via pixi
71
+ _install_via_pixi(cfg, node_dir, log, dry_run)
72
+
73
+ # Auto-discover and install isolated subdirectory environments
74
+ _install_isolated_subdirs(node_dir, log, dry_run)
75
+
76
+ log("\nInstallation complete!")
77
+ return True
78
+
79
+
80
+ def _install_apt_packages(
81
+ packages: List[str],
82
+ log: Callable[[str], None],
83
+ dry_run: bool,
84
+ ) -> None:
85
+ """Install apt packages (Linux only)."""
86
+ import os
87
+ import platform
88
+ import shutil
89
+ import subprocess
90
+
91
+ if platform.system() != "Linux":
92
+ log(f"[apt] Skipping apt packages (not Linux)")
93
+ return
94
+
95
+ log(f"\n[apt] Installing {len(packages)} system package(s):")
96
+ for pkg in packages:
97
+ log(f" - {pkg}")
98
+
99
+ if dry_run:
100
+ log(" (dry run - no changes made)")
101
+ return
102
+
103
+ # Determine if we need sudo
104
+ is_root = os.geteuid() == 0
105
+ has_sudo = shutil.which("sudo") is not None
106
+ use_sudo = not is_root and has_sudo
107
+ prefix = ["sudo"] if use_sudo else []
108
+
109
+ if not is_root and not has_sudo:
110
+ log(f"[apt] Warning: No root access. Install manually:")
111
+ log(f" sudo apt-get update && sudo apt-get install -y {' '.join(packages)}")
112
+ return
113
+
114
+ # Run apt-get update (suppress output, just show errors)
115
+ log("[apt] Updating package lists...")
116
+ result = subprocess.run(
117
+ prefix + ["apt-get", "update"],
118
+ capture_output=True,
119
+ text=True,
120
+ )
121
+ if result.returncode != 0:
122
+ log(f"[apt] Warning: apt-get update failed: {result.stderr.strip()}")
123
+
124
+ # Install each package individually (some may not exist on all distros)
125
+ log("[apt] Installing packages...")
126
+ installed = []
127
+ skipped = []
128
+ for pkg in packages:
129
+ result = subprocess.run(
130
+ prefix + ["apt-get", "install", "-y", pkg],
131
+ capture_output=True,
132
+ text=True,
133
+ )
134
+ if result.returncode == 0:
135
+ installed.append(pkg)
136
+ log(f" [apt] Installed {pkg}")
137
+ else:
138
+ skipped.append(pkg)
139
+ log(f" [apt] Skipped {pkg} (not available)")
140
+
141
+ if installed:
142
+ log(f"[apt] Installed {len(installed)} package(s)")
143
+ if skipped:
144
+ log(f"[apt] Skipped {len(skipped)} unavailable package(s)")
145
+
146
+
147
+ def _set_persistent_env_vars(
148
+ env_vars: dict,
149
+ log: Callable[[str], None],
150
+ dry_run: bool,
151
+ ) -> None:
152
+ """Set env vars permanently (survives restarts)."""
153
+ import os
154
+ import platform
155
+ import subprocess
156
+ from pathlib import Path
157
+
158
+ if not env_vars:
159
+ return
160
+
161
+ system = platform.system()
162
+ log(f"\n[env] Setting {len(env_vars)} persistent environment variable(s)...")
163
+
164
+ for key, value in env_vars.items():
165
+ log(f" - {key}={value}")
166
+
167
+ if dry_run:
168
+ log(" (dry run - no changes made)")
169
+ return
170
+
171
+ if system == "Windows":
172
+ # Windows: use setx (writes to registry)
173
+ for key, value in env_vars.items():
174
+ result = subprocess.run(
175
+ ["setx", key, value],
176
+ capture_output=True, text=True
177
+ )
178
+ if result.returncode == 0:
179
+ log(f" [env] Set {key} (Windows registry)")
180
+ else:
181
+ log(f" [env] Warning: Failed to set {key}: {result.stderr.strip()}")
182
+ log("[env] Restart terminal/ComfyUI for changes to take effect")
183
+
184
+ elif system == "Darwin": # macOS
185
+ # macOS: launchctl for GUI apps + zshrc for terminal
186
+ for key, value in env_vars.items():
187
+ subprocess.run(["launchctl", "setenv", key, value], capture_output=True)
188
+ log(f" [env] Set {key} (launchctl)")
189
+
190
+ # Also add to zshrc for terminal (zsh is default on macOS)
191
+ _add_to_shell_profile(env_vars, log)
192
+
193
+ else: # Linux
194
+ _add_to_shell_profile(env_vars, log)
195
+
196
+
197
+ def _add_to_shell_profile(
198
+ env_vars: dict,
199
+ log: Callable[[str], None],
200
+ ) -> None:
201
+ """Add env vars to shell profile (Linux/macOS)."""
202
+ import os
203
+ from pathlib import Path
204
+
205
+ # Determine shell profile
206
+ shell = os.environ.get("SHELL", "/bin/bash")
207
+ if "zsh" in shell:
208
+ rc_file = Path.home() / ".zshrc"
209
+ else:
210
+ rc_file = Path.home() / ".bashrc"
211
+
212
+ profile_file = Path.home() / ".comfy-env-profile"
213
+
214
+ # Write env vars to our dedicated file
215
+ with open(profile_file, "w") as f:
216
+ f.write("# Generated by comfy-env - do not edit manually\n")
217
+ for key, value in env_vars.items():
218
+ f.write(f'export {key}="{value}"\n')
219
+ log(f" [env] Wrote {profile_file}")
220
+
221
+ # Add source line to shell rc (only once)
222
+ source_line = f'source "{profile_file}"'
223
+ existing = rc_file.read_text() if rc_file.exists() else ""
224
+
225
+ if source_line not in existing and str(profile_file) not in existing:
226
+ with open(rc_file, "a") as f:
227
+ f.write(f'\n# comfy-env environment variables\n')
228
+ f.write(f'{source_line}\n')
229
+ log(f" [env] Added source line to {rc_file}")
230
+ else:
231
+ log(f" [env] Already configured in {rc_file}")
232
+
233
+ log("[env] Restart terminal/ComfyUI for changes to take effect")
234
+
235
+
236
+ def _install_node_dependencies(
237
+ node_reqs: List[NodeReq],
238
+ node_dir: Path,
239
+ log: Callable[[str], None],
240
+ dry_run: bool,
241
+ ) -> None:
242
+ """Install node dependencies (other ComfyUI custom nodes)."""
243
+ from .nodes import install_node_deps
244
+
245
+ custom_nodes_dir = node_dir.parent
246
+ log(f"\nInstalling {len(node_reqs)} node dependencies...")
247
+
248
+ if dry_run:
249
+ for req in node_reqs:
250
+ node_path = custom_nodes_dir / req.name
251
+ status = "exists" if node_path.exists() else "would clone"
252
+ log(f" {req.name}: {status}")
253
+ return
254
+
255
+ visited: Set[str] = {node_dir.name}
256
+ install_node_deps(node_reqs, custom_nodes_dir, log, visited)
257
+
258
+
259
+ def _install_via_pixi(
260
+ cfg: ComfyEnvConfig,
261
+ node_dir: Path,
262
+ log: Callable[[str], None],
263
+ dry_run: bool,
264
+ ) -> None:
265
+ """Install all packages via pixi."""
266
+ from .pixi import pixi_install
267
+
268
+ # Count what we're installing
269
+ cuda_count = len(cfg.cuda_packages)
270
+
271
+ # Count from passthrough (pixi-native format)
272
+ deps = cfg.pixi_passthrough.get("dependencies", {})
273
+ pypi_deps = cfg.pixi_passthrough.get("pypi-dependencies", {})
274
+
275
+ if cuda_count == 0 and not deps and not pypi_deps:
276
+ log("No packages to install")
277
+ return
278
+
279
+ log(f"\nInstalling via pixi:")
280
+ if cuda_count:
281
+ log(f" CUDA packages: {', '.join(cfg.cuda_packages)}")
282
+ if deps:
283
+ log(f" Conda packages: {len(deps)}")
284
+ if pypi_deps:
285
+ log(f" PyPI packages: {len(pypi_deps)}")
286
+
287
+ if dry_run:
288
+ log("\n(dry run - no changes made)")
289
+ return
290
+
291
+ pixi_install(cfg, node_dir, log)
292
+
293
+
294
+ def _install_isolated_subdirs(
295
+ node_dir: Path,
296
+ log: Callable[[str], None],
297
+ dry_run: bool,
298
+ ) -> None:
299
+ """Find and install comfy-env.toml in subdirectories."""
300
+ from .pixi import pixi_install
301
+ from .config.parser import CONFIG_FILE_NAME
302
+
303
+ # Find all comfy-env.toml files in subdirectories (not root)
304
+ for config_file in node_dir.rglob(CONFIG_FILE_NAME):
305
+ if config_file.parent == node_dir:
306
+ continue # Skip root (already installed)
307
+
308
+ sub_dir = config_file.parent
309
+ relative = sub_dir.relative_to(node_dir)
310
+
311
+ log(f"\n[isolated] Installing: {relative}")
312
+ sub_cfg = load_config(config_file)
313
+
314
+ if dry_run:
315
+ log(f" (dry run)")
316
+ continue
317
+
318
+ pixi_install(sub_cfg, sub_dir, log)
319
+
320
+
321
+ def verify_installation(
322
+ packages: List[str],
323
+ log: Callable[[str], None] = print,
324
+ ) -> bool:
325
+ """Verify that packages are importable."""
326
+ all_ok = True
327
+ for package in packages:
328
+ import_name = package.replace("-", "_").split("[")[0]
329
+ try:
330
+ __import__(import_name)
331
+ log(f" {package}: OK")
332
+ except ImportError as e:
333
+ log(f" {package}: FAILED ({e})")
334
+ all_ok = False
335
+ return all_ok
@@ -1,171 +0,0 @@
1
- """
2
- Installation API for comfy-env.
3
-
4
- Example:
5
- from comfy_env import install
6
- install() # Auto-discovers comfy-env.toml and installs
7
- """
8
-
9
- import inspect
10
- from pathlib import Path
11
- from typing import Callable, List, Optional, Set, Union
12
-
13
- from .config.types import ComfyEnvConfig, NodeReq
14
- from .config.parser import load_config, discover_config
15
-
16
-
17
- def install(
18
- config: Optional[Union[str, Path]] = None,
19
- node_dir: Optional[Path] = None,
20
- log_callback: Optional[Callable[[str], None]] = None,
21
- dry_run: bool = False,
22
- ) -> bool:
23
- """
24
- Install dependencies from comfy-env.toml.
25
-
26
- Args:
27
- config: Optional path to comfy-env.toml. Auto-discovered if not provided.
28
- node_dir: Optional node directory. Auto-discovered from caller if not provided.
29
- log_callback: Optional callback for logging. Defaults to print.
30
- dry_run: If True, show what would be installed without installing.
31
-
32
- Returns:
33
- True if installation succeeded.
34
- """
35
- # Auto-discover caller's directory if not provided
36
- if node_dir is None:
37
- frame = inspect.stack()[1]
38
- caller_file = frame.filename
39
- node_dir = Path(caller_file).parent.resolve()
40
-
41
- log = log_callback or print
42
-
43
- # Load config
44
- if config is not None:
45
- config_path = Path(config)
46
- if not config_path.is_absolute():
47
- config_path = node_dir / config_path
48
- cfg = load_config(config_path)
49
- else:
50
- cfg = discover_config(node_dir)
51
-
52
- if cfg is None:
53
- raise FileNotFoundError(
54
- f"No comfy-env.toml found in {node_dir}. "
55
- "Create comfy-env.toml to define dependencies."
56
- )
57
-
58
- # Install node dependencies first
59
- if cfg.node_reqs:
60
- _install_node_dependencies(cfg.node_reqs, node_dir, log, dry_run)
61
-
62
- # Install everything via pixi
63
- _install_via_pixi(cfg, node_dir, log, dry_run)
64
-
65
- # Auto-discover and install isolated subdirectory environments
66
- _install_isolated_subdirs(node_dir, log, dry_run)
67
-
68
- log("\nInstallation complete!")
69
- return True
70
-
71
-
72
- def _install_node_dependencies(
73
- node_reqs: List[NodeReq],
74
- node_dir: Path,
75
- log: Callable[[str], None],
76
- dry_run: bool,
77
- ) -> None:
78
- """Install node dependencies (other ComfyUI custom nodes)."""
79
- from .nodes import install_node_deps
80
-
81
- custom_nodes_dir = node_dir.parent
82
- log(f"\nInstalling {len(node_reqs)} node dependencies...")
83
-
84
- if dry_run:
85
- for req in node_reqs:
86
- node_path = custom_nodes_dir / req.name
87
- status = "exists" if node_path.exists() else "would clone"
88
- log(f" {req.name}: {status}")
89
- return
90
-
91
- visited: Set[str] = {node_dir.name}
92
- install_node_deps(node_reqs, custom_nodes_dir, log, visited)
93
-
94
-
95
- def _install_via_pixi(
96
- cfg: ComfyEnvConfig,
97
- node_dir: Path,
98
- log: Callable[[str], None],
99
- dry_run: bool,
100
- ) -> None:
101
- """Install all packages via pixi."""
102
- from .pixi import pixi_install
103
-
104
- # Count what we're installing
105
- cuda_count = len(cfg.cuda_packages)
106
-
107
- # Count from passthrough (pixi-native format)
108
- deps = cfg.pixi_passthrough.get("dependencies", {})
109
- pypi_deps = cfg.pixi_passthrough.get("pypi-dependencies", {})
110
-
111
- if cuda_count == 0 and not deps and not pypi_deps:
112
- log("No packages to install")
113
- return
114
-
115
- log(f"\nInstalling via pixi:")
116
- if cuda_count:
117
- log(f" CUDA packages: {', '.join(cfg.cuda_packages)}")
118
- if deps:
119
- log(f" Conda packages: {len(deps)}")
120
- if pypi_deps:
121
- log(f" PyPI packages: {len(pypi_deps)}")
122
-
123
- if dry_run:
124
- log("\n(dry run - no changes made)")
125
- return
126
-
127
- pixi_install(cfg, node_dir, log)
128
-
129
-
130
- def _install_isolated_subdirs(
131
- node_dir: Path,
132
- log: Callable[[str], None],
133
- dry_run: bool,
134
- ) -> None:
135
- """Find and install comfy-env.toml in subdirectories."""
136
- from .pixi import pixi_install
137
- from .config.parser import CONFIG_FILE_NAME
138
-
139
- # Find all comfy-env.toml files in subdirectories (not root)
140
- for config_file in node_dir.rglob(CONFIG_FILE_NAME):
141
- if config_file.parent == node_dir:
142
- continue # Skip root (already installed)
143
-
144
- sub_dir = config_file.parent
145
- relative = sub_dir.relative_to(node_dir)
146
-
147
- log(f"\n[isolated] Installing: {relative}")
148
- sub_cfg = load_config(config_file)
149
-
150
- if dry_run:
151
- log(f" (dry run)")
152
- continue
153
-
154
- pixi_install(sub_cfg, sub_dir, log)
155
-
156
-
157
- def verify_installation(
158
- packages: List[str],
159
- log: Callable[[str], None] = print,
160
- ) -> bool:
161
- """Verify that packages are importable."""
162
- all_ok = True
163
- for package in packages:
164
- import_name = package.replace("-", "_").split("[")[0]
165
- try:
166
- __import__(import_name)
167
- log(f" {package}: OK")
168
- except ImportError as e:
169
- log(f" {package}: FAILED ({e})")
170
- all_ok = False
171
- return all_ok
File without changes
File without changes
File without changes