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
comfy_env/errors.py DELETED
@@ -1,293 +0,0 @@
1
- """
2
- Rich error messages for comfy-env.
3
-
4
- This module provides error classes with actionable, user-friendly messages.
5
- Instead of cryptic pip errors, users see exactly what went wrong and what
6
- they can do to fix it.
7
-
8
- Example output:
9
-
10
- +------------------------------------------------------------------+
11
- | CUDA Wheel Not Found |
12
- +------------------------------------------------------------------+
13
- | Package: nvdiffrast==0.4.0 |
14
- | Requested: cu130-torch291-cp312-linux_x86_64 |
15
- | |
16
- | Suggestions: |
17
- | 1. Use Python 3.10 instead of 3.12 |
18
- | 2. Use CUDA 12.8 (set cuda = "12.8" in config) |
19
- | 3. Build wheel locally: comfy-env build nvdiffrast |
20
- +------------------------------------------------------------------+
21
- """
22
-
23
- from dataclasses import dataclass, field
24
- from typing import List, Optional, TYPE_CHECKING
25
-
26
- if TYPE_CHECKING:
27
- from .resolver import RuntimeEnv
28
-
29
-
30
- class EnvManagerError(Exception):
31
- """Base exception for comfy-env errors."""
32
-
33
- def __init__(self, message: str, details: Optional[str] = None):
34
- self.message = message
35
- self.details = details
36
- super().__init__(self.format())
37
-
38
- def format(self) -> str:
39
- """Format error message for display."""
40
- if self.details:
41
- return f"{self.message}\n\n{self.details}"
42
- return self.message
43
-
44
-
45
- class ConfigError(EnvManagerError):
46
- """Error in configuration file."""
47
-
48
- def __init__(self, message: str, file_path: Optional[str] = None, line: Optional[int] = None):
49
- self.file_path = file_path
50
- self.line = line
51
-
52
- details = None
53
- if file_path:
54
- location = f"in {file_path}"
55
- if line:
56
- location += f" at line {line}"
57
- details = location
58
-
59
- super().__init__(message, details)
60
-
61
-
62
- @dataclass
63
- class WheelNotFoundError(EnvManagerError):
64
- """
65
- Raised when a CUDA wheel cannot be found or resolved.
66
-
67
- Provides actionable suggestions based on the environment and what
68
- wheels are typically available.
69
- """
70
- package: str
71
- version: Optional[str] = None
72
- env: Optional["RuntimeEnv"] = None
73
- tried_urls: List[str] = field(default_factory=list)
74
- reason: Optional[str] = None
75
- available_combos: List[str] = field(default_factory=list)
76
-
77
- def __post_init__(self):
78
- # Build the formatted message
79
- self.message = self._build_message()
80
- self.details = self._build_details()
81
- Exception.__init__(self, self.format())
82
-
83
- def _build_message(self) -> str:
84
- """Build the main error message."""
85
- pkg = self.package
86
- if self.version:
87
- pkg = f"{self.package}=={self.version}"
88
- return f"CUDA wheel not found: {pkg}"
89
-
90
- def _build_details(self) -> str:
91
- """Build detailed error with suggestions."""
92
- lines = []
93
-
94
- # Box header
95
- lines.append("+" + "-" * 66 + "+")
96
- lines.append("| CUDA Wheel Not Found" + " " * 44 + "|")
97
- lines.append("+" + "-" * 66 + "+")
98
-
99
- # Package info
100
- pkg_line = f" Package: {self.package}"
101
- if self.version:
102
- pkg_line += f"=={self.version}"
103
- lines.append(f"|{pkg_line:<66}|")
104
-
105
- # Requested configuration
106
- if self.env:
107
- requested = self._format_requested()
108
- lines.append(f"| Requested: {requested:<54}|")
109
-
110
- lines.append("|" + " " * 66 + "|")
111
-
112
- # Reason if provided
113
- if self.reason:
114
- lines.append(f"| Reason: {self.reason:<57}|")
115
- lines.append("|" + " " * 66 + "|")
116
-
117
- # Tried URLs
118
- if self.tried_urls:
119
- lines.append("| Tried URLs:" + " " * 53 + "|")
120
- for url in self.tried_urls[:3]: # Limit to first 3
121
- # Truncate long URLs
122
- if len(url) > 60:
123
- url = "..." + url[-57:]
124
- lines.append(f"| {url:<62}|")
125
- lines.append("|" + " " * 66 + "|")
126
-
127
- # Suggestions
128
- suggestions = self._generate_suggestions()
129
- if suggestions:
130
- lines.append("| Suggestions:" + " " * 52 + "|")
131
- for i, suggestion in enumerate(suggestions, 1):
132
- lines.append(f"| {i}. {suggestion:<60}|")
133
- lines.append("|" + " " * 66 + "|")
134
-
135
- # Footer
136
- lines.append("+" + "-" * 66 + "+")
137
-
138
- return "\n".join(lines)
139
-
140
- def _format_requested(self) -> str:
141
- """Format the requested configuration."""
142
- if not self.env:
143
- return "unknown"
144
-
145
- parts = []
146
- if self.env.cuda_short:
147
- parts.append(f"cu{self.env.cuda_short}")
148
- else:
149
- parts.append("cpu")
150
-
151
- if self.env.torch_mm:
152
- parts.append(f"torch{self.env.torch_mm}")
153
-
154
- parts.append(f"cp{self.env.python_short}")
155
- parts.append(self.env.platform_tag)
156
-
157
- return "-".join(parts)
158
-
159
- def _generate_suggestions(self) -> List[str]:
160
- """Generate actionable suggestions based on the error context."""
161
- suggestions = []
162
-
163
- if not self.env:
164
- suggestions.append("Run 'comfy-env info' to see your environment")
165
- return suggestions
166
-
167
- # Python version suggestion
168
- if self.env.python_short not in ("310", "311"):
169
- suggestions.append(
170
- f"Use Python 3.10 or 3.11 (you have {self.env.python_version})"
171
- )
172
-
173
- # CUDA version suggestion
174
- if self.env.cuda_version and self.env.cuda_version not in ("12.4", "12.8"):
175
- suggestions.append(
176
- f"Use CUDA 12.4 or 12.8 (you have {self.env.cuda_version})"
177
- )
178
-
179
- # PyTorch version suggestion
180
- if self.env.torch_version:
181
- torch_major_minor = ".".join(self.env.torch_version.split(".")[:2])
182
- if torch_major_minor not in ("2.5", "2.8"):
183
- suggestions.append(
184
- f"Use PyTorch 2.5 or 2.8 (you have {self.env.torch_version})"
185
- )
186
-
187
- # General suggestions
188
- suggestions.append(
189
- f"Check if wheel exists: comfy-env resolve {self.package}"
190
- )
191
- suggestions.append(
192
- f"Build wheel locally: comfy-env build {self.package}"
193
- )
194
-
195
- return suggestions[:4] # Limit to 4 suggestions
196
-
197
- def format(self) -> str:
198
- """Format the complete error message."""
199
- return f"{self.message}\n\n{self.details}"
200
-
201
-
202
- class DependencyError(EnvManagerError):
203
- """Error resolving or installing dependencies."""
204
-
205
- def __init__(
206
- self,
207
- message: str,
208
- package: Optional[str] = None,
209
- pip_error: Optional[str] = None,
210
- ):
211
- self.package = package
212
- self.pip_error = pip_error
213
-
214
- details = None
215
- if pip_error:
216
- # Extract relevant lines from pip error
217
- relevant_lines = self._extract_pip_error(pip_error)
218
- if relevant_lines:
219
- details = "pip error:\n" + "\n".join(relevant_lines)
220
-
221
- super().__init__(message, details)
222
-
223
- def _extract_pip_error(self, pip_error: str) -> List[str]:
224
- """Extract the most relevant lines from pip error output."""
225
- lines = pip_error.strip().split("\n")
226
- relevant = []
227
-
228
- for line in lines:
229
- # Skip empty lines and progress bars
230
- if not line.strip():
231
- continue
232
- if line.startswith(" ") and "%" in line:
233
- continue
234
-
235
- # Keep error lines and important info
236
- lower = line.lower()
237
- if any(keyword in lower for keyword in [
238
- "error", "failed", "could not", "no matching",
239
- "requirement", "conflict", "incompatible"
240
- ]):
241
- relevant.append(line)
242
-
243
- return relevant[:10] # Limit to 10 lines
244
-
245
-
246
- class CUDANotFoundError(EnvManagerError):
247
- """Raised when CUDA is required but not available."""
248
-
249
- def __init__(self, package: Optional[str] = None):
250
- message = "CUDA is required but not detected"
251
- details_lines = [
252
- "This package requires a CUDA-capable GPU.",
253
- "",
254
- "To fix this:",
255
- " 1. Ensure you have an NVIDIA GPU",
256
- " 2. Install NVIDIA drivers",
257
- " 3. Install CUDA Toolkit",
258
- "",
259
- "Or if you want to run on CPU:",
260
- " Set 'fallback_to_cpu = true' in your config",
261
- ]
262
-
263
- if package:
264
- message = f"CUDA is required for {package} but not detected"
265
-
266
- super().__init__(message, "\n".join(details_lines))
267
-
268
-
269
- class InstallError(EnvManagerError):
270
- """Error during package installation."""
271
-
272
- def __init__(
273
- self,
274
- message: str,
275
- package: Optional[str] = None,
276
- exit_code: Optional[int] = None,
277
- stderr: Optional[str] = None,
278
- ):
279
- self.package = package
280
- self.exit_code = exit_code
281
- self.stderr = stderr
282
-
283
- details_parts = []
284
- if exit_code is not None:
285
- details_parts.append(f"Exit code: {exit_code}")
286
- if stderr:
287
- # Truncate long stderr
288
- if len(stderr) > 500:
289
- stderr = stderr[:500] + "\n... (truncated)"
290
- details_parts.append(f"Output:\n{stderr}")
291
-
292
- details = "\n".join(details_parts) if details_parts else None
293
- super().__init__(message, details)
comfy_env/nodes.py DELETED
@@ -1,187 +0,0 @@
1
- """
2
- Node dependency installation for comfy-env.
3
-
4
- This module handles installation of dependent ComfyUI custom nodes
5
- specified in the [node_reqs] section of comfy-env.toml.
6
-
7
- Example configuration:
8
- [node_reqs]
9
- ComfyUI_essentials = "cubiq/ComfyUI_essentials"
10
- ComfyUI-DepthAnythingV2 = "kijai/ComfyUI-DepthAnythingV2"
11
- """
12
-
13
- import shutil
14
- import subprocess
15
- import sys
16
- from pathlib import Path
17
- from typing import TYPE_CHECKING, Callable, List, Set
18
-
19
- if TYPE_CHECKING:
20
- from .config.types import NodeReq
21
-
22
-
23
- def normalize_repo_url(repo: str) -> str:
24
- """
25
- Convert GitHub shorthand to full URL.
26
-
27
- Args:
28
- repo: Either 'owner/repo' or full URL like 'https://github.com/owner/repo'
29
-
30
- Returns:
31
- Full GitHub URL
32
- """
33
- if repo.startswith("http://") or repo.startswith("https://"):
34
- return repo
35
- return f"https://github.com/{repo}"
36
-
37
-
38
- def clone_node(
39
- repo: str,
40
- name: str,
41
- target_dir: Path,
42
- log: Callable[[str], None],
43
- ) -> Path:
44
- """
45
- Clone a node repository to target_dir/name.
46
-
47
- Args:
48
- repo: GitHub repo path (e.g., 'owner/repo') or full URL
49
- name: Directory name for the cloned repo
50
- target_dir: Parent directory (usually custom_nodes/)
51
- log: Logging callback
52
-
53
- Returns:
54
- Path to the cloned node directory
55
-
56
- Raises:
57
- RuntimeError: If git clone fails
58
- """
59
- node_path = target_dir / name
60
- url = normalize_repo_url(repo)
61
-
62
- log(f" Cloning {name} from {url}...")
63
- result = subprocess.run(
64
- ["git", "clone", "--depth", "1", url, str(node_path)],
65
- capture_output=True,
66
- text=True,
67
- )
68
-
69
- if result.returncode != 0:
70
- raise RuntimeError(f"Failed to clone {url}: {result.stderr.strip()}")
71
-
72
- return node_path
73
-
74
-
75
- def install_requirements(
76
- node_dir: Path,
77
- log: Callable[[str], None],
78
- ) -> None:
79
- """
80
- Install requirements.txt in a node directory if it exists.
81
-
82
- Args:
83
- node_dir: Path to the node directory
84
- log: Logging callback
85
- """
86
- requirements_file = node_dir / "requirements.txt"
87
-
88
- if not requirements_file.exists():
89
- return
90
-
91
- log(f" Installing requirements for {node_dir.name}...")
92
-
93
- # Try uv first, fall back to pip if uv not in PATH
94
- if shutil.which("uv"):
95
- cmd = ["uv", "pip", "install", "-r", str(requirements_file), "--python", sys.executable]
96
- else:
97
- cmd = [sys.executable, "-m", "pip", "install", "-r", str(requirements_file)]
98
-
99
- result = subprocess.run(cmd, cwd=node_dir, capture_output=True, text=True)
100
- if result.returncode != 0:
101
- log(f" Warning: requirements.txt install failed for {node_dir.name}: {result.stderr.strip()[:200]}")
102
-
103
-
104
- def run_install_script(
105
- node_dir: Path,
106
- log: Callable[[str], None],
107
- ) -> None:
108
- """
109
- Run install.py in a node directory if it exists.
110
-
111
- Args:
112
- node_dir: Path to the node directory
113
- log: Logging callback
114
- """
115
- install_script = node_dir / "install.py"
116
-
117
- if install_script.exists():
118
- log(f" Running install.py for {node_dir.name}...")
119
- result = subprocess.run(
120
- [sys.executable, str(install_script)],
121
- cwd=node_dir,
122
- capture_output=True,
123
- text=True,
124
- )
125
- if result.returncode != 0:
126
- log(f" Warning: install.py failed for {node_dir.name}: {result.stderr.strip()[:200]}")
127
-
128
-
129
- def install_node_deps(
130
- node_reqs: "List[NodeReq]",
131
- custom_nodes_dir: Path,
132
- log: Callable[[str], None],
133
- visited: Set[str],
134
- ) -> None:
135
- """
136
- Install node dependencies recursively.
137
-
138
- Args:
139
- node_reqs: List of NodeReq objects to install
140
- custom_nodes_dir: Path to custom_nodes directory
141
- log: Logging callback
142
- visited: Set of already-processed node names (for cycle detection)
143
- """
144
- from .config.parser import discover_config
145
-
146
- for req in node_reqs:
147
- # Skip if already visited (cycle detection)
148
- if req.name in visited:
149
- log(f" {req.name}: already in dependency chain, skipping")
150
- continue
151
-
152
- visited.add(req.name)
153
-
154
- node_path = custom_nodes_dir / req.name
155
-
156
- # Skip if already installed (directory exists)
157
- if node_path.exists():
158
- log(f" {req.name}: already installed, skipping")
159
- continue
160
-
161
- try:
162
- # Clone the repository
163
- clone_node(req.repo, req.name, custom_nodes_dir, log)
164
-
165
- # Install requirements.txt if present
166
- install_requirements(node_path, log)
167
-
168
- # Run install.py if present
169
- run_install_script(node_path, log)
170
-
171
- # Recursively process nested node_reqs
172
- try:
173
- nested_config = discover_config(node_path)
174
- if nested_config and nested_config.node_reqs:
175
- log(f" {req.name}: found {len(nested_config.node_reqs)} nested dependencies")
176
- install_node_deps(
177
- nested_config.node_reqs,
178
- custom_nodes_dir,
179
- log,
180
- visited,
181
- )
182
- except Exception as e:
183
- # Don't fail if we can't parse nested config
184
- log(f" {req.name}: could not check for nested deps: {e}")
185
-
186
- except Exception as e:
187
- log(f" Warning: Failed to install {req.name}: {e}")
@@ -1,48 +0,0 @@
1
- """
2
- Pixi integration for comfy-env.
3
-
4
- All dependencies go through pixi for unified management.
5
- """
6
-
7
- from .core import (
8
- ensure_pixi,
9
- get_pixi_path,
10
- get_pixi_python,
11
- pixi_run,
12
- pixi_install,
13
- clean_pixi_artifacts,
14
- CUDA_WHEELS_INDEX,
15
- )
16
- from .cuda_detection import (
17
- detect_cuda_version,
18
- detect_cuda_environment,
19
- detect_gpu_info,
20
- detect_gpus,
21
- get_gpu_summary,
22
- get_recommended_cuda_version,
23
- GPUInfo,
24
- CUDAEnvironment,
25
- )
26
- from .resolver import RuntimeEnv
27
-
28
- __all__ = [
29
- # Core pixi functions
30
- "ensure_pixi",
31
- "get_pixi_path",
32
- "get_pixi_python",
33
- "pixi_run",
34
- "pixi_install",
35
- "clean_pixi_artifacts",
36
- "CUDA_WHEELS_INDEX",
37
- # CUDA detection
38
- "detect_cuda_version",
39
- "detect_cuda_environment",
40
- "detect_gpu_info",
41
- "detect_gpus",
42
- "get_gpu_summary",
43
- "get_recommended_cuda_version",
44
- "GPUInfo",
45
- "CUDAEnvironment",
46
- # Resolver
47
- "RuntimeEnv",
48
- ]