comfy-env 0.1.2__tar.gz → 0.1.4__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 (34) hide show
  1. {comfy_env-0.1.2 → comfy_env-0.1.4}/PKG-INFO +1 -1
  2. {comfy_env-0.1.2 → comfy_env-0.1.4}/pyproject.toml +1 -1
  3. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/isolation/wrap.py +23 -2
  4. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/pixi/core.py +86 -19
  5. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/workers/mp.py +10 -3
  6. {comfy_env-0.1.2 → comfy_env-0.1.4}/.github/workflows/ci.yml +0 -0
  7. {comfy_env-0.1.2 → comfy_env-0.1.4}/.github/workflows/publish.yml +0 -0
  8. {comfy_env-0.1.2 → comfy_env-0.1.4}/.gitignore +0 -0
  9. {comfy_env-0.1.2 → comfy_env-0.1.4}/LICENSE +0 -0
  10. {comfy_env-0.1.2 → comfy_env-0.1.4}/README.md +0 -0
  11. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/__init__.py +0 -0
  12. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/cli.py +0 -0
  13. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/config/__init__.py +0 -0
  14. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/config/parser.py +0 -0
  15. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/config/types.py +0 -0
  16. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/errors.py +0 -0
  17. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/install.py +0 -0
  18. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/isolation/__init__.py +0 -0
  19. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/nodes.py +0 -0
  20. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/pixi/__init__.py +0 -0
  21. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/pixi/cuda_detection.py +0 -0
  22. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/pixi/platform/__init__.py +0 -0
  23. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/pixi/platform/base.py +0 -0
  24. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/pixi/platform/darwin.py +0 -0
  25. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/pixi/platform/linux.py +0 -0
  26. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/pixi/platform/windows.py +0 -0
  27. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/pixi/resolver.py +0 -0
  28. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/prestartup.py +0 -0
  29. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/templates/comfy-env-instructions.txt +0 -0
  30. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/templates/comfy-env.toml +0 -0
  31. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/workers/__init__.py +0 -0
  32. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/workers/base.py +0 -0
  33. {comfy_env-0.1.2 → comfy_env-0.1.4}/src/comfy_env/workers/subprocess.py +0 -0
  34. {comfy_env-0.1.2 → comfy_env-0.1.4}/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.1.2
3
+ Version: 0.1.4
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.1.2"
3
+ version = "0.1.4"
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"}
@@ -67,6 +67,7 @@ def _get_worker(
67
67
  working_dir: Path,
68
68
  sys_path: list[str],
69
69
  lib_path: Optional[str] = None,
70
+ env_vars: Optional[dict] = None,
70
71
  ):
71
72
  """Get or create a persistent worker for the isolated environment."""
72
73
  cache_key = str(env_dir)
@@ -106,11 +107,14 @@ def _get_worker(
106
107
 
107
108
  print(f"[comfy-env] Starting isolated worker (MPWorker)")
108
109
  print(f"[comfy-env] Env: {env_dir}")
110
+ if env_vars:
111
+ print(f"[comfy-env] env_vars: {', '.join(f'{k}={v}' for k, v in env_vars.items())}")
109
112
 
110
113
  worker = MPWorker(
111
114
  name=working_dir.name,
112
115
  sys_path=sys_path,
113
116
  lib_path=lib_path,
117
+ env_vars=env_vars,
114
118
  )
115
119
 
116
120
  _workers[cache_key] = worker
@@ -187,6 +191,7 @@ def _wrap_node_class(
187
191
  working_dir: Path,
188
192
  sys_path: list[str],
189
193
  lib_path: Optional[str] = None,
194
+ env_vars: Optional[dict] = None,
190
195
  ) -> type:
191
196
  """
192
197
  Wrap a node class so its FUNCTION method runs in the isolated environment.
@@ -232,7 +237,7 @@ def _wrap_node_class(
232
237
  print(f"[comfy-env] PROXY CALLED: {cls.__name__}.{func_name}", flush=True)
233
238
  print(f"[comfy-env] kwargs keys: {list(kwargs.keys())}", flush=True)
234
239
 
235
- worker = _get_worker(env_dir, working_dir, sys_path, lib_path)
240
+ worker = _get_worker(env_dir, working_dir, sys_path, lib_path, env_vars)
236
241
  if _DEBUG:
237
242
  print(f"[comfy-env] worker alive: {worker.is_alive()}", flush=True)
238
243
 
@@ -322,6 +327,20 @@ def wrap_isolated_nodes(
322
327
  print(f"[comfy-env] Warning: No comfy-env.toml in {nodes_dir}")
323
328
  return node_class_mappings
324
329
 
330
+ # Read env_vars from comfy-env.toml
331
+ env_vars = {}
332
+ try:
333
+ if sys.version_info >= (3, 11):
334
+ import tomllib
335
+ else:
336
+ import tomli as tomllib
337
+ with open(config_file, "rb") as f:
338
+ config = tomllib.load(f)
339
+ env_vars_data = config.get("env_vars", {})
340
+ env_vars = {str(k): str(v) for k, v in env_vars_data.items()}
341
+ except Exception:
342
+ pass # Ignore errors reading config
343
+
325
344
  # Find environment directory and paths
326
345
  env_dir = _find_env_dir(nodes_dir)
327
346
  site_packages, lib_dir = _find_env_paths(nodes_dir)
@@ -342,10 +361,12 @@ def wrap_isolated_nodes(
342
361
  print(f"[comfy-env] site-packages: {site_packages}")
343
362
  if lib_path:
344
363
  print(f"[comfy-env] lib: {lib_path}")
364
+ if env_vars:
365
+ print(f"[comfy-env] env_vars: {', '.join(f'{k}={v}' for k, v in env_vars.items())}")
345
366
 
346
367
  # Wrap all node classes
347
368
  for node_name, node_cls in node_class_mappings.items():
348
369
  if hasattr(node_cls, "FUNCTION"):
349
- _wrap_node_class(node_cls, env_dir, nodes_dir, sys_path, lib_path)
370
+ _wrap_node_class(node_cls, env_dir, nodes_dir, sys_path, lib_path, env_vars)
350
371
 
351
372
  return node_class_mappings
@@ -9,10 +9,12 @@ See: https://pixi.sh/
9
9
 
10
10
  import copy
11
11
  import platform
12
+ import re
12
13
  import shutil
13
14
  import stat
14
15
  import subprocess
15
16
  import sys
17
+ import urllib.request
16
18
  from pathlib import Path
17
19
  from typing import Any, Callable, Dict, List, Optional
18
20
 
@@ -28,15 +30,9 @@ PIXI_URLS = {
28
30
  ("Windows", "AMD64"): "https://github.com/prefix-dev/pixi/releases/latest/download/pixi-x86_64-pc-windows-msvc.exe",
29
31
  }
30
32
 
31
- # CUDA wheels index
33
+ # CUDA wheels index (includes flash-attn, PyG packages, and custom wheels)
32
34
  CUDA_WHEELS_INDEX = "https://pozzettiandrea.github.io/cuda-wheels/"
33
35
 
34
- # Flash attention pre-built wheels (mjun0812)
35
- FLASH_ATTN_INDEX = "https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/expanded_assets/v0.7.16"
36
-
37
- # PyTorch Geometric wheels index
38
- PYG_WHEELS_INDEX = "https://data.pyg.org/whl/"
39
-
40
36
  # CUDA version -> PyTorch version mapping
41
37
  CUDA_TORCH_MAP = {
42
38
  "12.8": "2.8",
@@ -44,19 +40,87 @@ CUDA_TORCH_MAP = {
44
40
  "12.1": "2.4",
45
41
  }
46
42
 
43
+ def find_matching_wheel(package: str, torch_version: str, cuda_version: str) -> Optional[str]:
44
+ """
45
+ Query cuda-wheels index to find a wheel matching the CUDA/torch version.
46
+ Returns the full version spec (e.g., "flash-attn===2.8.3+cu128torch2.8") or None.
47
+ """
48
+ cuda_short = cuda_version.replace(".", "")[:3] # "12.8" -> "128"
49
+ torch_short = torch_version.replace(".", "")[:2] # "2.8" -> "28"
50
+
51
+ # Try different directory name variants
52
+ pkg_variants = [package, package.replace("-", "_"), package.replace("_", "-")]
53
+
54
+ for pkg_dir in pkg_variants:
55
+ url = f"{CUDA_WHEELS_INDEX}{pkg_dir}/"
56
+ try:
57
+ with urllib.request.urlopen(url, timeout=10) as resp:
58
+ html = resp.read().decode("utf-8")
59
+ except Exception:
60
+ continue
61
+
62
+ # Parse wheel filenames from href attributes
63
+ # Pattern: package_name-version+localversion-cpXX-cpXX-platform.whl
64
+ wheel_pattern = re.compile(
65
+ r'href="[^"]*?([^"/]+\.whl)"',
66
+ re.IGNORECASE
67
+ )
68
+
69
+ # Local version patterns to match:
70
+ # flash-attn style: +cu128torch2.8
71
+ # PyG style: +pt28cu128
72
+ local_patterns = [
73
+ f"+cu{cuda_short}torch{torch_version}", # flash-attn style
74
+ f"+pt{torch_short}cu{cuda_short}", # PyG style
75
+ ]
76
+
77
+ best_match = None
78
+ best_version = None
79
+
80
+ for match in wheel_pattern.finditer(html):
81
+ wheel_name = match.group(1)
82
+ # URL decode
83
+ wheel_name = wheel_name.replace("%2B", "+")
84
+
85
+ # Check if wheel matches our CUDA/torch version
86
+ for local_pattern in local_patterns:
87
+ if local_pattern in wheel_name:
88
+ # Extract version from wheel name
89
+ # Format: name-version+local-cpXX-cpXX-platform.whl
90
+ parts = wheel_name.split("-")
91
+ if len(parts) >= 2:
92
+ version_part = parts[1] # e.g., "2.8.3+cu128torch2.8"
93
+ if best_version is None or version_part > best_version:
94
+ best_version = version_part
95
+ best_match = f"{package}==={version_part}"
96
+ break
97
+
98
+ if best_match:
99
+ return best_match
100
+
101
+ return None
102
+
103
+
104
+ def get_package_spec(package: str, torch_version: str, cuda_version: str) -> str:
105
+ """
106
+ Get package spec with local version for CUDA/torch compatibility.
107
+ Queries the index to find matching wheels dynamically.
108
+ """
109
+ spec = find_matching_wheel(package, torch_version, cuda_version)
110
+ return spec if spec else package
111
+
47
112
 
48
113
  def get_all_find_links(package: str, torch_version: str, cuda_version: str) -> list:
49
114
  """Get all find-links URLs for a CUDA package."""
50
- cuda_short = cuda_version.replace(".", "")[:3]
51
-
52
- return [
53
- # cuda-wheels index (package-specific page)
54
- f"{CUDA_WHEELS_INDEX}{package}/",
55
- # flash-attn GitHub releases
56
- FLASH_ATTN_INDEX,
57
- # PyG index (torch version specific)
58
- f"{PYG_WHEELS_INDEX}torch-{torch_version}.0+cu{cuda_short}.html",
59
- ]
115
+ # Try both underscore and hyphen variants since directory naming is inconsistent
116
+ pkg_underscore = package.replace("-", "_")
117
+ pkg_hyphen = package.replace("_", "-")
118
+ urls = [f"{CUDA_WHEELS_INDEX}{package}/"]
119
+ if pkg_underscore != package:
120
+ urls.append(f"{CUDA_WHEELS_INDEX}{pkg_underscore}/")
121
+ if pkg_hyphen != package:
122
+ urls.append(f"{CUDA_WHEELS_INDEX}{pkg_hyphen}/")
123
+ return urls
60
124
 
61
125
 
62
126
  def get_current_platform() -> str:
@@ -340,6 +404,7 @@ def pixi_install(
340
404
  for package in cfg.cuda_packages:
341
405
  pip_cmd = [
342
406
  str(python_path), "-m", "pip", "install",
407
+ "--no-index", # Only use --find-links, don't query PyPI
343
408
  "--no-deps",
344
409
  "--no-cache-dir",
345
410
  ]
@@ -348,8 +413,10 @@ def pixi_install(
348
413
  for url in get_all_find_links(package, torch_version, cuda_version):
349
414
  pip_cmd.extend(["--find-links", url])
350
415
 
351
- log(f" Installing {package}")
352
- pip_cmd.append(package)
416
+ # Get package spec with local version for CUDA/torch compatibility
417
+ pkg_spec = get_package_spec(package, torch_version, cuda_version)
418
+ log(f" Installing {pkg_spec}")
419
+ pip_cmd.append(pkg_spec)
353
420
  result = subprocess.run(pip_cmd, capture_output=True, text=True)
354
421
  if result.returncode != 0:
355
422
  log(f"CUDA package install failed for {package}:\n{result.stderr}")
@@ -40,7 +40,7 @@ _SHUTDOWN = object()
40
40
  _CALL_METHOD = "call_method"
41
41
 
42
42
 
43
- def _worker_loop(queue_in, queue_out, sys_path_additions=None, lib_path=None):
43
+ def _worker_loop(queue_in, queue_out, sys_path_additions=None, lib_path=None, env_vars=None):
44
44
  """
45
45
  Worker process main loop.
46
46
 
@@ -56,11 +56,16 @@ def _worker_loop(queue_in, queue_out, sys_path_additions=None, lib_path=None):
56
56
  queue_out: Output queue for sending results
57
57
  sys_path_additions: Paths to add to sys.path
58
58
  lib_path: Path to add to LD_LIBRARY_PATH (for conda libraries)
59
+ env_vars: Environment variables to set (from comfy-env.toml)
59
60
  """
60
61
  import os
61
62
  import sys
62
63
  from pathlib import Path
63
64
 
65
+ # Apply env_vars FIRST (before any library imports that might check them)
66
+ if env_vars:
67
+ os.environ.update(env_vars)
68
+
64
69
  # Set worker mode env var
65
70
  os.environ["COMFYUI_ISOLATION_WORKER"] = "1"
66
71
 
@@ -424,7 +429,7 @@ class MPWorker(Worker):
424
429
  interpreter without inherited state from the parent.
425
430
  """
426
431
 
427
- def __init__(self, name: Optional[str] = None, sys_path: Optional[list] = None, lib_path: Optional[str] = None):
432
+ def __init__(self, name: Optional[str] = None, sys_path: Optional[list] = None, lib_path: Optional[str] = None, env_vars: Optional[dict] = None):
428
433
  """
429
434
  Initialize the worker.
430
435
 
@@ -432,10 +437,12 @@ class MPWorker(Worker):
432
437
  name: Optional name for logging/debugging.
433
438
  sys_path: Optional list of paths to add to sys.path in worker process.
434
439
  lib_path: Optional path to add to LD_LIBRARY_PATH (for conda libraries).
440
+ env_vars: Optional environment variables to set in worker process.
435
441
  """
436
442
  self.name = name or "MPWorker"
437
443
  self._sys_path = sys_path or []
438
444
  self._lib_path = lib_path
445
+ self._env_vars = env_vars or {}
439
446
  self._process = None
440
447
  self._queue_in = None
441
448
  self._queue_out = None
@@ -504,7 +511,7 @@ class MPWorker(Worker):
504
511
  self._queue_out = ctx.Queue()
505
512
  self._process = ctx.Process(
506
513
  target=_worker_loop,
507
- args=(self._queue_in, self._queue_out, self._sys_path, self._lib_path),
514
+ args=(self._queue_in, self._queue_out, self._sys_path, self._lib_path, self._env_vars),
508
515
  daemon=True,
509
516
  )
510
517
  self._process.start()
File without changes
File without changes
File without changes