comfy-env 0.0.8__py3-none-any.whl → 0.0.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 CHANGED
@@ -1,46 +1,4 @@
1
- """
2
- comfy-env: Environment management for ComfyUI custom nodes.
3
-
4
- This package provides:
5
- - CUDA wheel resolution and in-place installation (Type 2 nodes)
6
- - Process isolation with separate venvs (Type 1 nodes)
7
-
8
- ## Quick Start - In-Place Installation
9
-
10
- from comfy_env import install
11
-
12
- # Auto-discover config and install CUDA wheels
13
- install()
14
-
15
- # Or with explicit config
16
- install(config="comfyui_env.toml")
17
-
18
- ## Quick Start - Process Isolation
19
-
20
- from comfy_env.workers import get_worker, TorchMPWorker
21
-
22
- # Same-venv isolation (zero-copy tensors)
23
- worker = TorchMPWorker()
24
- result = worker.call(my_gpu_function, image=tensor)
25
-
26
- # Cross-venv isolation
27
- from comfy_env.workers import PersistentVenvWorker
28
- worker = PersistentVenvWorker(python="/path/to/venv/bin/python")
29
- result = worker.call_module("my_module", "my_func", image=tensor)
30
-
31
- ## CLI
32
-
33
- comfy-env install # Install from config
34
- comfy-env info # Show environment info
35
- comfy-env resolve pkg==1.0 # Show resolved wheel URL
36
- comfy-env doctor # Verify installation
37
-
38
- ## Legacy APIs (still supported)
39
-
40
- The @isolated decorator and WorkerBridge are still available.
41
- """
42
-
43
- __version__ = "0.0.8"
1
+ __version__ = "0.0.14"
44
2
 
45
3
  from .env.config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq
46
4
  from .env.config_file import (
@@ -51,7 +9,16 @@ from .env.config_file import (
51
9
  CONFIG_FILE_NAMES,
52
10
  )
53
11
  from .env.manager import IsolatedEnvManager
54
- from .env.detection import detect_cuda_version, detect_gpu_info, get_gpu_summary
12
+ from .env.cuda_gpu_detection import (
13
+ GPUInfo,
14
+ CUDAEnvironment,
15
+ detect_cuda_environment,
16
+ detect_cuda_version,
17
+ detect_gpu_info,
18
+ detect_gpus,
19
+ get_gpu_summary,
20
+ get_recommended_cuda_version,
21
+ )
55
22
  from .env.security import (
56
23
  normalize_env_name,
57
24
  validate_dependency,
@@ -134,9 +101,14 @@ __all__ = [
134
101
  "discover_config",
135
102
  "CONFIG_FILE_NAMES",
136
103
  # Detection
104
+ "GPUInfo",
105
+ "CUDAEnvironment",
106
+ "detect_cuda_environment",
137
107
  "detect_cuda_version",
138
108
  "detect_gpu_info",
109
+ "detect_gpus",
139
110
  "get_gpu_summary",
111
+ "get_recommended_cuda_version",
140
112
  # Security validation
141
113
  "normalize_env_name",
142
114
  "validate_dependency",
comfy_env/cli.py CHANGED
@@ -234,6 +234,7 @@ def cmd_info(args) -> int:
234
234
  def cmd_resolve(args) -> int:
235
235
  """Handle resolve command."""
236
236
  from .resolver import RuntimeEnv, WheelResolver, parse_wheel_requirement
237
+ from .registry import PACKAGE_REGISTRY
237
238
  from .env.config_file import discover_env_config, load_env_from_file
238
239
 
239
240
  env = RuntimeEnv.detect()
@@ -270,19 +271,116 @@ def cmd_resolve(args) -> int:
270
271
  print(f" {package}: No version specified, skipping")
271
272
  continue
272
273
 
274
+ pkg_lower = package.lower()
273
275
  try:
274
- url = resolver.resolve(package, version, env, verify=args.verify)
275
- status = "OK" if args.verify else "resolved"
276
- print(f" {package}=={version}: {status}")
277
- print(f" {url}")
276
+ # Check if package is in registry with github_release method
277
+ if pkg_lower in PACKAGE_REGISTRY:
278
+ registry_config = PACKAGE_REGISTRY[pkg_lower]
279
+ method = registry_config.get("method")
280
+
281
+ if method == "github_release":
282
+ # Resolve URL from registry sources
283
+ url = _resolve_github_release_url(package, version, env, registry_config)
284
+ status = "OK" if args.verify else "resolved"
285
+ print(f" {package}=={version}: {status}")
286
+ print(f" {url}")
287
+ else:
288
+ # For other methods, just show what method will be used
289
+ print(f" {package}=={version}: uses {method} method")
290
+ if "index_url" in registry_config:
291
+ index_url = _substitute_template(registry_config["index_url"], env)
292
+ print(f" index: {index_url}")
293
+ elif "package_template" in registry_config:
294
+ pkg_name = _substitute_template(registry_config["package_template"], env)
295
+ print(f" installs as: {pkg_name}")
296
+ else:
297
+ # Fall back to WheelResolver
298
+ url = resolver.resolve(package, version, env, verify=args.verify)
299
+ status = "OK" if args.verify else "resolved"
300
+ print(f" {package}=={version}: {status}")
301
+ print(f" {url}")
278
302
  except Exception as e:
279
303
  print(f" {package}=={version}: FAILED")
280
- print(f" {e}")
304
+ _print_wheel_not_found_error(package, version, env, e)
281
305
  all_ok = False
282
306
 
283
307
  return 0 if all_ok else 1
284
308
 
285
309
 
310
+ def _substitute_template(template: str, env) -> str:
311
+ """Substitute environment variables into a URL template."""
312
+ vars_dict = env.as_dict()
313
+ result = template
314
+ for key, value in vars_dict.items():
315
+ if value is not None:
316
+ result = result.replace(f"{{{key}}}", str(value))
317
+ return result
318
+
319
+
320
+ def _resolve_github_release_url(package: str, version: str, env, config: dict) -> str:
321
+ """Resolve URL for github_release method packages."""
322
+ sources = config.get("sources", [])
323
+ if not sources:
324
+ raise ValueError(f"No sources configured for {package}")
325
+
326
+ # Build template variables
327
+ vars_dict = env.as_dict()
328
+ vars_dict["version"] = version
329
+ vars_dict["py_tag"] = f"cp{env.python_short}"
330
+ if env.cuda_version:
331
+ vars_dict["cuda_major"] = env.cuda_version.split(".")[0]
332
+
333
+ # Filter sources by platform
334
+ current_platform = env.platform_tag
335
+ compatible_sources = [
336
+ s for s in sources
337
+ if current_platform in s.get("platforms", [])
338
+ ]
339
+
340
+ if not compatible_sources:
341
+ available = set()
342
+ for s in sources:
343
+ available.update(s.get("platforms", []))
344
+ raise ValueError(
345
+ f"No {package} wheels for platform {current_platform}. "
346
+ f"Available: {', '.join(sorted(available))}"
347
+ )
348
+
349
+ # Return URL from first compatible source
350
+ source = compatible_sources[0]
351
+ url_template = source.get("url_template", "")
352
+ url = url_template
353
+ for key, value in vars_dict.items():
354
+ if value is not None:
355
+ url = url.replace(f"{{{key}}}", str(value))
356
+
357
+ return url
358
+
359
+
360
+ def _print_wheel_not_found_error(package: str, version: str, env, error: Exception) -> None:
361
+ """Print a formatted error message for wheel not found."""
362
+ from .errors import WheelNotFoundError
363
+
364
+ if isinstance(error, WheelNotFoundError):
365
+ print(f" CUDA wheel not found: {package}=={version}")
366
+ print()
367
+ print("+------------------------------------------------------------------+")
368
+ print("| CUDA Wheel Not Found |")
369
+ print("+------------------------------------------------------------------+")
370
+ print(f"| Package: {package}=={version:<46} |")
371
+ print(f"| Requested: cu{env.cuda_short}-torch{env.torch_mm}-{env.python_short}-{env.platform_tag:<17} |")
372
+ print("| |")
373
+ print(f"| Reason: {error.reason:<54} |")
374
+ print("| |")
375
+ print("| Suggestions: |")
376
+ print(f"| 1. Check if wheel exists: comfy-env resolve {package:<15} |")
377
+ print(f"| 2. Build wheel locally: comfy-env build {package:<18} |")
378
+ print("| |")
379
+ print("+------------------------------------------------------------------+")
380
+ else:
381
+ print(f" {error}")
382
+
383
+
286
384
  def cmd_doctor(args) -> int:
287
385
  """Handle doctor command."""
288
386
  from .install import verify_installation
comfy_env/env/__init__.py CHANGED
@@ -1,8 +1,17 @@
1
1
  """Environment management for comfyui-isolation."""
2
2
 
3
- from .config import IsolatedEnv
3
+ from .config import IsolatedEnv, ToolConfig
4
4
  from .manager import IsolatedEnvManager
5
- from .detection import detect_cuda_version, detect_gpu_info, get_gpu_summary
5
+ from .cuda_gpu_detection import (
6
+ GPUInfo,
7
+ CUDAEnvironment,
8
+ detect_cuda_environment,
9
+ detect_cuda_version,
10
+ detect_gpu_info,
11
+ detect_gpus,
12
+ get_gpu_summary,
13
+ get_recommended_cuda_version,
14
+ )
6
15
  from .platform import get_platform, PlatformProvider, PlatformPaths
7
16
  from .security import (
8
17
  normalize_env_name,
@@ -15,9 +24,17 @@ from .security import (
15
24
  __all__ = [
16
25
  "IsolatedEnv",
17
26
  "IsolatedEnvManager",
27
+ "ToolConfig",
28
+ # GPU Detection
29
+ "GPUInfo",
30
+ "CUDAEnvironment",
31
+ "detect_cuda_environment",
18
32
  "detect_cuda_version",
19
33
  "detect_gpu_info",
34
+ "detect_gpus",
20
35
  "get_gpu_summary",
36
+ "get_recommended_cuda_version",
37
+ # Platform
21
38
  "get_platform",
22
39
  "PlatformProvider",
23
40
  "PlatformPaths",
comfy_env/env/config.py CHANGED
@@ -23,6 +23,14 @@ class NodeReq:
23
23
  repo: str # GitHub repo path, e.g., "Kosinkadink/ComfyUI-VideoHelperSuite"
24
24
 
25
25
 
26
+ @dataclass
27
+ class ToolConfig:
28
+ """Configuration for an external tool like Blender."""
29
+ name: str
30
+ version: str = "latest"
31
+ install_dir: Optional[Path] = None
32
+
33
+
26
34
  @dataclass
27
35
  class EnvManagerConfig:
28
36
  """
@@ -35,10 +43,12 @@ class EnvManagerConfig:
35
43
  [envname.cuda] - CUDA packages for isolated env
36
44
  [envname.packages] - Regular packages for isolated env
37
45
  [node_reqs] - Node dependencies
46
+ [tools] - External tools (e.g., blender = "4.2")
38
47
  """
39
48
  local: LocalConfig = field(default_factory=LocalConfig)
40
49
  envs: Dict[str, "IsolatedEnv"] = field(default_factory=dict)
41
50
  node_reqs: List[NodeReq] = field(default_factory=list)
51
+ tools: Dict[str, ToolConfig] = field(default_factory=dict)
42
52
 
43
53
  @property
44
54
  def has_local(self) -> bool:
@@ -98,6 +108,10 @@ class IsolatedEnv:
98
108
  cuda: Optional[str] = None
99
109
  requirements: list[str] = field(default_factory=list)
100
110
  no_deps_requirements: list[str] = field(default_factory=list) # Install with --no-deps
111
+ # Platform-specific requirements (merged at install time)
112
+ windows_requirements: list[str] = field(default_factory=list)
113
+ linux_requirements: list[str] = field(default_factory=list)
114
+ darwin_requirements: list[str] = field(default_factory=list)
101
115
  requirements_file: Optional[Path] = None
102
116
  wheel_sources: list[str] = field(default_factory=list)
103
117
  index_urls: list[str] = field(default_factory=list)
@@ -63,8 +63,8 @@ else:
63
63
  except ImportError:
64
64
  tomllib = None # type: ignore
65
65
 
66
- from .config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq
67
- from .detection import detect_cuda_version
66
+ from .config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq, ToolConfig
67
+ from .cuda_gpu_detection import detect_cuda_version
68
68
 
69
69
 
70
70
  # Config file name
@@ -168,7 +168,7 @@ def _get_default_pytorch_version(cuda_version: Optional[str]) -> str:
168
168
  - CUDA 12.8 (Turing+): PyTorch 2.8.0
169
169
  """
170
170
  if cuda_version == "12.4":
171
- return "2.5.1" # Legacy: Pascal GPUs
171
+ return "2.5.1" # Full: Pascal GPUs
172
172
  return "2.8.0" # Modern: Turing through Blackwell
173
173
 
174
174
 
@@ -176,7 +176,7 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
176
176
  """
177
177
  Parse TOML data into IsolatedEnv.
178
178
 
179
- Supports both simplified and legacy config formats:
179
+ Supports both simplified and full config formats:
180
180
 
181
181
  Simplified (CUDA packages only):
182
182
  [packages]
@@ -186,7 +186,7 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
186
186
  Or as list:
187
187
  packages = ["torch-scatter==2.1.2", "torch-cluster==1.6.3"]
188
188
 
189
- Legacy:
189
+ Full:
190
190
  [env]
191
191
  name = "my-node"
192
192
  [packages]
@@ -233,7 +233,7 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
233
233
  variables.setdefault("pytorch_mm", pytorch_mm)
234
234
 
235
235
  # Parse CUDA packages - support multiple formats
236
- # Priority: [cuda] section > cuda = [...] > legacy [packages] section
236
+ # Priority: [cuda] section > cuda = [...] > [packages] section
237
237
  no_deps_requirements = []
238
238
  requirements = []
239
239
 
@@ -258,12 +258,12 @@ def _parse_config(data: Dict[str, Any], base_dir: Path) -> IsolatedEnv:
258
258
 
259
259
  elif isinstance(packages_section, dict):
260
260
  # Check for simplified format: [packages] with key=value pairs
261
- # vs legacy format: [packages] with requirements/no_deps lists
261
+ # vs old format: [packages] with requirements/no_deps lists
262
262
 
263
- has_legacy_keys = any(k in packages_section for k in ["requirements", "no_deps", "requirements_file"])
263
+ has_old_keys = any(k in packages_section for k in ["requirements", "no_deps", "requirements_file"])
264
264
 
265
- if has_legacy_keys:
266
- # Legacy format
265
+ if has_old_keys:
266
+ # Old format
267
267
  raw_requirements = packages_section.get("requirements", [])
268
268
  requirements = [_substitute_vars(req, variables) for req in raw_requirements]
269
269
 
@@ -332,7 +332,7 @@ def _substitute_vars(s: str, variables: Dict[str, str]) -> str:
332
332
  # =============================================================================
333
333
 
334
334
  # Reserved table names that are NOT isolated environments
335
- RESERVED_TABLES = {"local", "node_reqs", "env", "packages", "sources", "cuda", "variables", "worker"}
335
+ RESERVED_TABLES = {"local", "node_reqs", "env", "packages", "sources", "cuda", "variables", "worker", "tools"}
336
336
 
337
337
 
338
338
  def load_config(
@@ -340,9 +340,9 @@ def load_config(
340
340
  base_dir: Optional[Path] = None,
341
341
  ) -> EnvManagerConfig:
342
342
  """
343
- Load full EnvManagerConfig from a TOML file (v2 schema).
343
+ Load full EnvManagerConfig from a TOML file.
344
344
 
345
- Supports both v2 schema and legacy format (auto-detected).
345
+ Supports both full schema (named envs) and simple format (auto-detected).
346
346
 
347
347
  Args:
348
348
  path: Path to the TOML config file
@@ -373,7 +373,7 @@ def load_config(
373
373
  with open(path, "rb") as f:
374
374
  data = tomllib.load(f)
375
375
 
376
- return _parse_config_v2(data, base_dir)
376
+ return _parse_full_config(data, base_dir)
377
377
 
378
378
 
379
379
  def discover_config(
@@ -404,9 +404,9 @@ def discover_config(
404
404
  return None
405
405
 
406
406
 
407
- def _parse_config_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
407
+ def _parse_full_config(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
408
408
  """
409
- Parse TOML data into EnvManagerConfig (v2 schema).
409
+ Parse TOML data into EnvManagerConfig.
410
410
 
411
411
  Schema:
412
412
  [local.cuda] - CUDA packages for host
@@ -416,24 +416,26 @@ def _parse_config_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
416
416
  [envname.packages] - Packages for env
417
417
  [node_reqs] - Node dependencies
418
418
 
419
- Also supports legacy format for backward compatibility.
419
+ Also supports simple format ([env] + [packages]) for backward compatibility.
420
420
  """
421
- # Detect if this is v2 schema or legacy
422
- is_v2 = "local" in data or _has_named_env(data)
421
+ # Detect if this is full schema or simple format
422
+ is_full = "local" in data or _has_named_env(data)
423
423
 
424
- if not is_v2:
425
- # Legacy format - convert to v2 structure
426
- return _convert_legacy_to_v2(data, base_dir)
424
+ if not is_full:
425
+ # Simple format - convert to full structure
426
+ return _convert_simple_to_full(data, base_dir)
427
427
 
428
- # Parse v2 schema
428
+ # Parse full schema
429
429
  local = _parse_local_section(data.get("local", {}))
430
430
  envs = _parse_env_sections(data, base_dir)
431
431
  node_reqs = _parse_node_reqs(data.get("node_reqs", {}))
432
+ tools = _parse_tools_section(data.get("tools", {}))
432
433
 
433
434
  return EnvManagerConfig(
434
435
  local=local,
435
436
  envs=envs,
436
437
  node_reqs=node_reqs,
438
+ tools=tools,
437
439
  )
438
440
 
439
441
 
@@ -529,6 +531,30 @@ def _parse_single_env(name: str, env_data: Dict[str, Any], base_dir: Path) -> Is
529
531
  elif isinstance(packages_section, list):
530
532
  requirements = packages_section
531
533
 
534
+ # Parse platform-specific packages [envname.packages.windows], etc.
535
+ windows_reqs = []
536
+ linux_reqs = []
537
+ darwin_reqs = []
538
+
539
+ if isinstance(packages_section, dict):
540
+ win_section = packages_section.get("windows", {})
541
+ if isinstance(win_section, dict):
542
+ windows_reqs = win_section.get("requirements", [])
543
+ elif isinstance(win_section, list):
544
+ windows_reqs = win_section
545
+
546
+ linux_section = packages_section.get("linux", {})
547
+ if isinstance(linux_section, dict):
548
+ linux_reqs = linux_section.get("requirements", [])
549
+ elif isinstance(linux_section, list):
550
+ linux_reqs = linux_section
551
+
552
+ darwin_section = packages_section.get("darwin", {})
553
+ if isinstance(darwin_section, dict):
554
+ darwin_reqs = darwin_section.get("requirements", [])
555
+ elif isinstance(darwin_section, list):
556
+ darwin_reqs = darwin_section
557
+
532
558
  return IsolatedEnv(
533
559
  name=name,
534
560
  python=python,
@@ -536,6 +562,9 @@ def _parse_single_env(name: str, env_data: Dict[str, Any], base_dir: Path) -> Is
536
562
  pytorch_version=pytorch,
537
563
  requirements=requirements,
538
564
  no_deps_requirements=no_deps_requirements,
565
+ windows_requirements=windows_reqs,
566
+ linux_requirements=linux_reqs,
567
+ darwin_requirements=darwin_reqs,
539
568
  )
540
569
 
541
570
 
@@ -555,27 +584,63 @@ def _parse_node_reqs(node_reqs_data: Dict[str, Any]) -> List[NodeReq]:
555
584
  return reqs
556
585
 
557
586
 
558
- def _convert_legacy_to_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
559
- """Convert legacy config format to v2 EnvManagerConfig."""
560
- # Parse using legacy parser to get IsolatedEnv
561
- legacy_env = _parse_config(data, base_dir)
587
+ def _parse_tools_section(tools_data: Dict[str, Any]) -> Dict[str, ToolConfig]:
588
+ """Parse [tools] section.
589
+
590
+ Supports:
591
+ [tools]
592
+ blender = "4.2"
593
+
594
+ Or extended:
595
+ [tools.blender]
596
+ version = "4.2"
597
+ install_dir = "/custom/path"
598
+ """
599
+ tools = {}
600
+
601
+ for name, value in tools_data.items():
602
+ if isinstance(value, str):
603
+ # Simple format: blender = "4.2"
604
+ tools[name] = ToolConfig(name=name, version=value)
605
+ elif isinstance(value, dict):
606
+ # Extended format: [tools.blender] with version, install_dir
607
+ version = value.get("version", "latest")
608
+ install_dir = value.get("install_dir")
609
+ if install_dir:
610
+ install_dir = Path(install_dir)
611
+ tools[name] = ToolConfig(name=name, version=version, install_dir=install_dir)
612
+
613
+ return tools
614
+
615
+
616
+ def _convert_simple_to_full(data: Dict[str, Any], base_dir: Path) -> EnvManagerConfig:
617
+ """Convert simple config format to full EnvManagerConfig.
562
618
 
563
- # Check if this is really just a Type 2 config (local CUDA only, no venv)
564
- # Type 2 indicators: no [env] section with name, or name matches directory
619
+ Simple configs have [env] and [packages] sections but no named environments.
620
+ This converts them to the full format with a single named environment.
621
+ """
622
+ # Parse using simple parser to get IsolatedEnv
623
+ simple_env = _parse_config(data, base_dir)
624
+
625
+ # Parse tools section (shared between simple and full format)
626
+ tools = _parse_tools_section(data.get("tools", {}))
627
+
628
+ # Check if this has explicit env settings (isolated venv) vs just CUDA packages (local install)
565
629
  env_section = data.get("env", {})
566
630
  has_explicit_env = bool(env_section.get("name") or env_section.get("python"))
567
631
 
568
632
  if has_explicit_env:
569
- # This is a Type 1 config (isolated venv)
633
+ # Isolated venv config
570
634
  return EnvManagerConfig(
571
635
  local=LocalConfig(),
572
- envs={legacy_env.name: legacy_env},
636
+ envs={simple_env.name: simple_env},
573
637
  node_reqs=[],
638
+ tools=tools,
574
639
  )
575
640
  else:
576
- # This is a Type 2 config (local CUDA only)
641
+ # Local CUDA packages only (no isolated venv)
577
642
  cuda_packages = {}
578
- for req in legacy_env.no_deps_requirements:
643
+ for req in simple_env.no_deps_requirements:
579
644
  if "==" in req:
580
645
  pkg, ver = req.split("==", 1)
581
646
  cuda_packages[pkg] = ver
@@ -585,8 +650,9 @@ def _convert_legacy_to_v2(data: Dict[str, Any], base_dir: Path) -> EnvManagerCon
585
650
  return EnvManagerConfig(
586
651
  local=LocalConfig(
587
652
  cuda_packages=cuda_packages,
588
- requirements=legacy_env.requirements,
653
+ requirements=simple_env.requirements,
589
654
  ),
590
655
  envs={},
591
656
  node_reqs=[],
592
- )
657
+ tools=tools,
658
+ )