comfy-env 0.1.25__tar.gz → 0.1.27__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 (37) hide show
  1. {comfy_env-0.1.25 → comfy_env-0.1.27}/PKG-INFO +2 -1
  2. {comfy_env-0.1.25 → comfy_env-0.1.27}/pyproject.toml +2 -1
  3. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/config/parser.py +10 -1
  4. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/config/types.py +7 -0
  5. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/install.py +2 -2
  6. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/isolation/workers/subprocess.py +11 -4
  7. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/isolation/wrap.py +27 -11
  8. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/packages/pixi.py +24 -5
  9. {comfy_env-0.1.25 → comfy_env-0.1.27}/.github/workflows/ci.yml +0 -0
  10. {comfy_env-0.1.25 → comfy_env-0.1.27}/.github/workflows/publish.yml +0 -0
  11. {comfy_env-0.1.25 → comfy_env-0.1.27}/.gitignore +0 -0
  12. {comfy_env-0.1.25 → comfy_env-0.1.27}/LICENSE +0 -0
  13. {comfy_env-0.1.25 → comfy_env-0.1.27}/README.md +0 -0
  14. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/__init__.py +0 -0
  15. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/cli.py +0 -0
  16. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/config/__init__.py +0 -0
  17. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/detection/__init__.py +0 -0
  18. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/detection/cuda.py +0 -0
  19. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/detection/gpu.py +0 -0
  20. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/detection/platform.py +0 -0
  21. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/detection/runtime.py +0 -0
  22. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/environment/__init__.py +0 -0
  23. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/environment/cache.py +0 -0
  24. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/environment/libomp.py +0 -0
  25. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/environment/paths.py +0 -0
  26. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/environment/setup.py +0 -0
  27. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/isolation/__init__.py +0 -0
  28. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/isolation/tensor_utils.py +0 -0
  29. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/isolation/workers/__init__.py +0 -0
  30. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/isolation/workers/base.py +0 -0
  31. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/packages/__init__.py +0 -0
  32. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/packages/apt.py +0 -0
  33. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/packages/cuda_wheels.py +0 -0
  34. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/packages/node_dependencies.py +0 -0
  35. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/packages/toml_generator.py +0 -0
  36. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/templates/comfy-env-instructions.txt +0 -0
  37. {comfy_env-0.1.25 → comfy_env-0.1.27}/src/comfy_env/templates/comfy-env.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfy-env
3
- Version: 0.1.25
3
+ Version: 0.1.27
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
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Requires-Python: >=3.10
20
20
  Requires-Dist: numpy
21
21
  Requires-Dist: pip>=21.0
22
+ Requires-Dist: pixi>=0.40.0
22
23
  Requires-Dist: tomli-w>=1.0.0
23
24
  Requires-Dist: tomli>=2.0.0
24
25
  Requires-Dist: uv>=0.4.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "comfy-env"
3
- version = "0.1.25"
3
+ version = "0.1.27"
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"}
@@ -22,6 +22,7 @@ dependencies = [
22
22
  "tomli>=2.0.0",
23
23
  "tomli-w>=1.0.0",
24
24
  "uv>=0.4.0",
25
+ "pixi>=0.40.0",
25
26
  "pip>=21.0",
26
27
  "numpy",
27
28
  ]
@@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional
6
6
 
7
7
  import tomli
8
8
 
9
- from .types import ComfyEnvConfig, NodeDependency
9
+ from .types import ComfyEnvConfig, ComfyEnvOptions, NodeDependency
10
10
 
11
11
  ROOT_CONFIG_FILE_NAME = "comfy-env-root.toml" # Main node config
12
12
  CONFIG_FILE_NAME = "comfy-env.toml" # Isolated folder config
@@ -43,6 +43,7 @@ def parse_config(data: Dict[str, Any]) -> ComfyEnvConfig:
43
43
  apt_packages = _ensure_list(data.pop("apt", {}).get("packages", []))
44
44
  env_vars = {str(k): str(v) for k, v in data.pop("env_vars", {}).items()}
45
45
  node_reqs = _parse_node_reqs(data.pop("node_reqs", {}))
46
+ options = _parse_options(data.pop("options", {}))
46
47
 
47
48
  return ComfyEnvConfig(
48
49
  python=python_version,
@@ -50,10 +51,18 @@ def parse_config(data: Dict[str, Any]) -> ComfyEnvConfig:
50
51
  apt_packages=apt_packages,
51
52
  env_vars=env_vars,
52
53
  node_reqs=node_reqs,
54
+ options=options,
53
55
  pixi_passthrough=data,
54
56
  )
55
57
 
56
58
 
59
+ def _parse_options(data: Dict[str, Any]) -> ComfyEnvOptions:
60
+ """Parse [options] section into ComfyEnvOptions."""
61
+ return ComfyEnvOptions(
62
+ health_check_timeout=float(data.get("health_check_timeout", 2.0)),
63
+ )
64
+
65
+
57
66
  def _parse_node_reqs(data: Dict[str, Any]) -> List[NodeDependency]:
58
67
  return [
59
68
  NodeDependency(name=name, repo=value if isinstance(value, str) else value.get("repo", ""))
@@ -14,6 +14,12 @@ class NodeDependency:
14
14
  NodeReq = NodeDependency # Backwards compat
15
15
 
16
16
 
17
+ @dataclass
18
+ class ComfyEnvOptions:
19
+ """Runtime options for comfy-env."""
20
+ health_check_timeout: float = 2.0 # Timeout for worker health checks (seconds)
21
+
22
+
17
23
  @dataclass
18
24
  class ComfyEnvConfig:
19
25
  """Parsed comfy-env.toml configuration."""
@@ -22,6 +28,7 @@ class ComfyEnvConfig:
22
28
  apt_packages: List[str] = field(default_factory=list)
23
29
  env_vars: Dict[str, str] = field(default_factory=dict)
24
30
  node_reqs: List[NodeDependency] = field(default_factory=list)
31
+ options: ComfyEnvOptions = field(default_factory=ComfyEnvOptions)
25
32
  pixi_passthrough: Dict[str, Any] = field(default_factory=dict)
26
33
 
27
34
  @property
@@ -151,7 +151,7 @@ def _install_via_pixi(cfg: ComfyEnvConfig, node_dir: Path, log: Callable[[str],
151
151
  log("Running pixi install...")
152
152
  result = subprocess.run([str(pixi_path), "install"], cwd=node_dir, capture_output=True, text=True)
153
153
  if result.returncode != 0:
154
- raise RuntimeError(f"pixi install failed: {result.stderr}")
154
+ raise RuntimeError(f"pixi install failed:\nstderr: {result.stderr}\nstdout: {result.stdout}")
155
155
 
156
156
  if cfg.cuda_packages and cuda_version:
157
157
  log(f"Installing CUDA packages...")
@@ -171,7 +171,7 @@ def _install_via_pixi(cfg: ComfyEnvConfig, node_dir: Path, log: Callable[[str],
171
171
  result = subprocess.run([str(python_path), "-m", "pip", "install", "--no-deps", "--no-cache-dir", wheel_url],
172
172
  capture_output=True, text=True)
173
173
  if result.returncode != 0:
174
- raise RuntimeError(f"Failed: {result.stderr}")
174
+ raise RuntimeError(f"Failed to install {package}:\nstderr: {result.stderr}\nstdout: {result.stdout}")
175
175
 
176
176
  # Find config file for marker
177
177
  config_path = node_dir / CONFIG_FILE_NAME
@@ -1200,6 +1200,7 @@ class SubprocessWorker(Worker):
1200
1200
  env: Optional[Dict[str, str]] = None,
1201
1201
  name: Optional[str] = None,
1202
1202
  share_torch: bool = True, # Kept for API compatibility
1203
+ health_check_timeout: float = 2.0,
1203
1204
  ):
1204
1205
  """
1205
1206
  Initialize persistent worker.
@@ -1211,12 +1212,14 @@ class SubprocessWorker(Worker):
1211
1212
  env: Additional environment variables.
1212
1213
  name: Optional name for logging.
1213
1214
  share_torch: Ignored (kept for API compatibility).
1215
+ health_check_timeout: Timeout in seconds for worker health checks.
1214
1216
  """
1215
1217
  self.python = Path(python)
1216
1218
  self.working_dir = Path(working_dir) if working_dir else Path.cwd()
1217
1219
  self.sys_path = sys_path or []
1218
1220
  self.extra_env = env or {}
1219
1221
  self.name = name or f"SubprocessWorker({self.python.parent.parent.name})"
1222
+ self.health_check_timeout = health_check_timeout
1220
1223
 
1221
1224
  if not self.python.exists():
1222
1225
  raise FileNotFoundError(f"Python not found: {self.python}")
@@ -1267,14 +1270,18 @@ class SubprocessWorker(Worker):
1267
1270
  def _check_socket_health(self) -> bool:
1268
1271
  """Check if socket connection is healthy using a quick ping."""
1269
1272
  if not self._transport:
1273
+ print(f"[{self.name}] Health check: no transport", file=sys.stderr, flush=True)
1270
1274
  return False
1271
1275
  try:
1272
- # Send a ping request with short timeout
1276
+ # Send a ping request with configurable timeout
1277
+ print(f"[{self.name}] Health check: ping (timeout={self.health_check_timeout}s)...", file=sys.stderr, flush=True)
1273
1278
  self._transport.send({"method": "ping"})
1274
- response = self._transport.recv(timeout=2.0)
1275
- return response is not None and response.get("status") == "pong"
1279
+ response = self._transport.recv(timeout=self.health_check_timeout)
1280
+ ok = response is not None and response.get("status") == "pong"
1281
+ print(f"[{self.name}] Health check: {'ok' if ok else 'failed'}", file=sys.stderr, flush=True)
1282
+ return ok
1276
1283
  except Exception as e:
1277
- print(f"[{self.name}] Socket health check failed: {e}", file=sys.stderr, flush=True)
1284
+ print(f"[{self.name}] Socket health check exception: {e}", file=sys.stderr, flush=True)
1278
1285
  return False
1279
1286
 
1280
1287
  def _kill_worker(self) -> None:
@@ -66,7 +66,8 @@ def _get_python_version(env_dir: Path) -> Optional[str]:
66
66
 
67
67
 
68
68
  def _get_worker(env_dir: Path, working_dir: Path, sys_path: list[str],
69
- lib_path: Optional[str] = None, env_vars: Optional[dict] = None):
69
+ lib_path: Optional[str] = None, env_vars: Optional[dict] = None,
70
+ health_check_timeout: float = 2.0):
70
71
  cache_key = str(env_dir)
71
72
  with _workers_lock:
72
73
  if cache_key in _workers and _workers[cache_key].is_alive():
@@ -79,7 +80,11 @@ def _get_worker(env_dir: Path, working_dir: Path, sys_path: list[str],
79
80
  # SubprocessWorker uses a clean entry script that avoids this issue.
80
81
  from .workers.subprocess import SubprocessWorker
81
82
  print(f"[comfy-env] SubprocessWorker: {python}")
82
- worker = SubprocessWorker(python=str(python), working_dir=working_dir, sys_path=sys_path, name=working_dir.name)
83
+ if env_vars:
84
+ print(f"[comfy-env] env_vars: {env_vars}")
85
+ if health_check_timeout != 2.0:
86
+ print(f"[comfy-env] health_check_timeout: {health_check_timeout}s")
87
+ worker = SubprocessWorker(python=str(python), working_dir=working_dir, sys_path=sys_path, name=working_dir.name, env=env_vars, health_check_timeout=health_check_timeout)
83
88
 
84
89
  _workers[cache_key] = worker
85
90
  return worker
@@ -95,7 +100,8 @@ def _shutdown_workers():
95
100
 
96
101
 
97
102
  def _wrap_node_class(cls: type, env_dir: Path, working_dir: Path, sys_path: list[str],
98
- lib_path: Optional[str] = None, env_vars: Optional[dict] = None) -> type:
103
+ lib_path: Optional[str] = None, env_vars: Optional[dict] = None,
104
+ health_check_timeout: float = 2.0) -> type:
99
105
  func_name = getattr(cls, "FUNCTION", None)
100
106
  if not func_name: return cls
101
107
  original = getattr(cls, func_name, None)
@@ -109,7 +115,7 @@ def _wrap_node_class(cls: type, env_dir: Path, working_dir: Path, sys_path: list
109
115
 
110
116
  @wraps(original)
111
117
  def proxy(self, **kwargs):
112
- worker = _get_worker(env_dir, working_dir, sys_path, lib_path, env_vars)
118
+ worker = _get_worker(env_dir, working_dir, sys_path, lib_path, env_vars, health_check_timeout)
113
119
  try:
114
120
  from .tensor_utils import prepare_for_ipc_recursive
115
121
  kwargs = {k: prepare_for_ipc_recursive(v) for k, v in kwargs.items()}
@@ -161,14 +167,19 @@ def wrap_nodes() -> None:
161
167
  if not env_dir or not sp: continue
162
168
 
163
169
  env_vars = {}
170
+ health_check_timeout = 2.0
164
171
  try:
165
172
  import tomli
166
173
  with open(cf, "rb") as f:
167
- env_vars = {str(k): str(v) for k, v in tomli.load(f).get("env_vars", {}).items()}
168
- except Exception: pass
174
+ toml_data = tomli.load(f)
175
+ env_vars = {str(k): str(v) for k, v in toml_data.get("env_vars", {}).items()}
176
+ health_check_timeout = float(toml_data.get("options", {}).get("health_check_timeout", 2.0))
177
+ print(f"[comfy-env] Parsed {cf}: health_check_timeout={health_check_timeout}")
178
+ except Exception as e:
179
+ print(f"[comfy-env] Failed to parse {cf}: {e}")
169
180
  if comfyui_base: env_vars["COMFYUI_BASE"] = str(comfyui_base)
170
181
 
171
- envs.append({"dir": cf.parent, "env_dir": env_dir, "sp": sp, "lib": lib, "env_vars": env_vars})
182
+ envs.append({"dir": cf.parent, "env_dir": env_dir, "sp": sp, "lib": lib, "env_vars": env_vars, "health_check_timeout": health_check_timeout})
172
183
 
173
184
  wrapped = 0
174
185
  for name, cls in mappings.items():
@@ -181,7 +192,7 @@ def wrap_nodes() -> None:
181
192
  try:
182
193
  src.relative_to(e["dir"])
183
194
  _wrap_node_class(cls, e["env_dir"], e["dir"], [str(e["sp"]), str(e["dir"])],
184
- str(e["lib"]) if e["lib"] else None, e["env_vars"])
195
+ str(e["lib"]) if e["lib"] else None, e["env_vars"], e.get("health_check_timeout", 2.0))
185
196
  wrapped += 1
186
197
  break
187
198
  except ValueError: continue
@@ -207,11 +218,16 @@ def wrap_isolated_nodes(node_class_mappings: Dict[str, type], nodes_dir: Path) -
207
218
  return node_class_mappings
208
219
 
209
220
  env_vars = {}
221
+ health_check_timeout = 2.0
210
222
  try:
211
223
  import tomli
212
224
  with open(config, "rb") as f:
213
- env_vars = {str(k): str(v) for k, v in tomli.load(f).get("env_vars", {}).items()}
214
- except Exception: pass
225
+ toml_data = tomli.load(f)
226
+ env_vars = {str(k): str(v) for k, v in toml_data.get("env_vars", {}).items()}
227
+ health_check_timeout = float(toml_data.get("options", {}).get("health_check_timeout", 2.0))
228
+ print(f"[comfy-env] Parsed {config}: health_check_timeout={health_check_timeout}")
229
+ except Exception as e:
230
+ print(f"[comfy-env] Failed to parse {config}: {e}")
215
231
  if comfyui_base: env_vars["COMFYUI_BASE"] = str(comfyui_base)
216
232
 
217
233
  env_dir = _find_env_dir(nodes_dir)
@@ -226,6 +242,6 @@ def wrap_isolated_nodes(node_class_mappings: Dict[str, type], nodes_dir: Path) -
226
242
  print(f"[comfy-env] Wrapping {len(node_class_mappings)} nodes from {nodes_dir.name}")
227
243
  for cls in node_class_mappings.values():
228
244
  if hasattr(cls, "FUNCTION"):
229
- _wrap_node_class(cls, env_dir, nodes_dir, sys_path, lib_path, env_vars)
245
+ _wrap_node_class(cls, env_dir, nodes_dir, sys_path, lib_path, env_vars, health_check_timeout)
230
246
 
231
247
  return node_class_mappings
@@ -19,15 +19,34 @@ PIXI_URLS = {
19
19
 
20
20
 
21
21
  def get_pixi_path() -> Optional[Path]:
22
- """Find pixi in PATH or common locations."""
23
- if cmd := shutil.which("pixi"): return Path(cmd)
22
+ """Find pixi in PATH, venv, or common user locations."""
23
+ # 1. PATH
24
+ if cmd := shutil.which("pixi"):
25
+ return Path(cmd)
26
+
27
+ # 2. Active venv (pip-installed pixi)
28
+ prefix = Path(sys.prefix)
29
+ candidates = []
30
+ if sys.platform == "win32":
31
+ candidates.append(prefix / "Scripts" / "pixi.exe")
32
+ else:
33
+ candidates.append(prefix / "bin" / "pixi")
34
+
35
+ # 3. User locations
24
36
  home = Path.home()
25
- for p in [home / ".pixi/bin/pixi", home / ".local/bin/pixi"]:
26
- candidate = p.with_suffix(".exe") if sys.platform == "win32" else p
27
- if candidate.exists(): return candidate
37
+ candidates.extend([
38
+ home / ".pixi" / "bin" / ("pixi.exe" if sys.platform == "win32" else "pixi"),
39
+ home / ".local" / "bin" / ("pixi.exe" if sys.platform == "win32" else "pixi"),
40
+ ])
41
+
42
+ for c in candidates:
43
+ if c.exists():
44
+ return c
45
+
28
46
  return None
29
47
 
30
48
 
49
+
31
50
  def ensure_pixi(install_dir: Optional[Path] = None, log: Callable[[str], None] = print) -> Path:
32
51
  """Ensure pixi is installed, downloading if necessary."""
33
52
  if existing := get_pixi_path(): return existing
File without changes
File without changes
File without changes