comfy-env 0.0.5__tar.gz → 0.0.8__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.
- {comfy_env-0.0.5 → comfy_env-0.0.8}/PKG-INFO +1 -1
- {comfy_env-0.0.5 → comfy_env-0.0.8}/pyproject.toml +1 -1
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/__init__.py +1 -1
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/manager.py +56 -5
- comfy_env-0.0.8/src/comfy_env/env/platform/windows.py +377 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/workers/venv.py +202 -132
- comfy_env-0.0.5/src/comfy_env/env/platform/windows.py +0 -195
- {comfy_env-0.0.5 → comfy_env-0.0.8}/.github/workflows/publish.yml +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/.gitignore +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/CLAUDE.md +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/CRITICISM.md +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/LICENSE +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/README.md +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/basic_node/__init__.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/basic_node/comfy-env.toml +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/basic_node/nodes.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/basic_node/worker.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/decorator_node/__init__.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/decorator_node/nodes.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/cli.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/decorator.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/__init__.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/config.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/config_file.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/detection.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/platform/__init__.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/platform/base.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/platform/darwin.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/platform/linux.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/security.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/errors.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/install.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/__init__.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/bridge.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/protocol.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/tensor.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/torch_bridge.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/transport.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/worker.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/registry.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/resolver.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/runner.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/stubs/__init__.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/stubs/folder_paths.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/workers/__init__.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/workers/base.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/workers/pool.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/workers/tensor_utils.py +0 -0
- {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/workers/torch_mp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comfy-env
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8
|
|
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
|
|
@@ -40,7 +40,7 @@ This package provides:
|
|
|
40
40
|
The @isolated decorator and WorkerBridge are still available.
|
|
41
41
|
"""
|
|
42
42
|
|
|
43
|
-
__version__ = "0.0.
|
|
43
|
+
__version__ = "0.0.8"
|
|
44
44
|
|
|
45
45
|
from .env.config import IsolatedEnv, EnvManagerConfig, LocalConfig, NodeReq
|
|
46
46
|
from .env.config_file import (
|
|
@@ -502,6 +502,57 @@ class IsolatedEnvManager:
|
|
|
502
502
|
else:
|
|
503
503
|
self.log("comfy-env installed")
|
|
504
504
|
|
|
505
|
+
def _setup_windows_deps(self, env: IsolatedEnv, env_dir: Path) -> None:
|
|
506
|
+
"""
|
|
507
|
+
Set up Windows-specific dependencies (VC++ runtime, DLL paths).
|
|
508
|
+
|
|
509
|
+
This:
|
|
510
|
+
1. Installs msvc-runtime package (provides VC++ DLLs)
|
|
511
|
+
2. Sets up the Lib/x64/vc17/bin directory structure for opencv
|
|
512
|
+
"""
|
|
513
|
+
python_exe = self.get_python(env)
|
|
514
|
+
uv = self._find_uv()
|
|
515
|
+
|
|
516
|
+
# Install msvc-runtime package to get VC++ DLLs
|
|
517
|
+
self.log("Installing VC++ runtime (msvc-runtime)...")
|
|
518
|
+
result = subprocess.run(
|
|
519
|
+
[str(uv), "pip", "install", "--python", str(python_exe), "msvc-runtime"],
|
|
520
|
+
capture_output=True,
|
|
521
|
+
text=True,
|
|
522
|
+
)
|
|
523
|
+
if result.returncode != 0:
|
|
524
|
+
self.log(f"Warning: Failed to install msvc-runtime: {result.stderr}")
|
|
525
|
+
else:
|
|
526
|
+
self.log("msvc-runtime installed")
|
|
527
|
+
|
|
528
|
+
# Set up opencv DLL paths (copies DLLs to Lib/x64/vc17/bin)
|
|
529
|
+
self.log("Setting up opencv DLL paths...")
|
|
530
|
+
success, msg = self.platform.setup_opencv_dll_paths(env_dir)
|
|
531
|
+
if success:
|
|
532
|
+
if msg:
|
|
533
|
+
self.log(f" {msg}")
|
|
534
|
+
else:
|
|
535
|
+
self.log(f"Warning: {msg}")
|
|
536
|
+
|
|
537
|
+
def ensure_system_deps(self) -> bool:
|
|
538
|
+
"""
|
|
539
|
+
Ensure system-level dependencies are installed (Windows only).
|
|
540
|
+
|
|
541
|
+
On Windows, this checks for Media Foundation and installs if missing.
|
|
542
|
+
Returns True if all deps are available, False otherwise.
|
|
543
|
+
"""
|
|
544
|
+
if self.platform.name != 'windows':
|
|
545
|
+
return True
|
|
546
|
+
|
|
547
|
+
# Check and install Media Foundation if needed
|
|
548
|
+
success, error = self.platform.ensure_media_foundation(self.log)
|
|
549
|
+
if not success:
|
|
550
|
+
self.log(f"WARNING: {error}")
|
|
551
|
+
self.log("Some packages (like opencv) may not work correctly.")
|
|
552
|
+
return False
|
|
553
|
+
|
|
554
|
+
return True
|
|
555
|
+
|
|
505
556
|
def setup(
|
|
506
557
|
self,
|
|
507
558
|
env: IsolatedEnv,
|
|
@@ -525,6 +576,9 @@ class IsolatedEnvManager:
|
|
|
525
576
|
self.log(f"Setting up isolated environment: {env.name}")
|
|
526
577
|
self.log("=" * 50)
|
|
527
578
|
|
|
579
|
+
# Ensure system-level deps (Media Foundation on Windows)
|
|
580
|
+
self.ensure_system_deps()
|
|
581
|
+
|
|
528
582
|
# Check if already ready
|
|
529
583
|
if self.is_ready(env, verify_packages):
|
|
530
584
|
self.log("Environment already ready, skipping setup")
|
|
@@ -543,12 +597,9 @@ class IsolatedEnvManager:
|
|
|
543
597
|
# Install other requirements
|
|
544
598
|
self.install_requirements(env)
|
|
545
599
|
|
|
546
|
-
# Windows:
|
|
600
|
+
# Windows: Install VC++ runtime and set up DLL paths
|
|
547
601
|
if self.platform.name == 'windows':
|
|
548
|
-
self.
|
|
549
|
-
success, error = self.platform.bundle_vc_dlls_to_env(env_dir)
|
|
550
|
-
if not success:
|
|
551
|
-
self.log(f"Warning: {error}")
|
|
602
|
+
self._setup_windows_deps(env, env_dir)
|
|
552
603
|
|
|
553
604
|
# Verify installation
|
|
554
605
|
if verify_packages:
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Windows platform provider implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
from .base import PlatformProvider, PlatformPaths
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WindowsPlatformProvider(PlatformProvider):
|
|
18
|
+
"""Platform provider for Windows systems."""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self) -> str:
|
|
22
|
+
return 'windows'
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def executable_suffix(self) -> str:
|
|
26
|
+
return '.exe'
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def shared_lib_extension(self) -> str:
|
|
30
|
+
return '.dll'
|
|
31
|
+
|
|
32
|
+
def get_env_paths(self, env_dir: Path, python_version: str = "3.10") -> PlatformPaths:
|
|
33
|
+
return PlatformPaths(
|
|
34
|
+
python=env_dir / "Scripts" / "python.exe",
|
|
35
|
+
pip=env_dir / "Scripts" / "pip.exe",
|
|
36
|
+
site_packages=env_dir / "Lib" / "site-packages",
|
|
37
|
+
bin_dir=env_dir / "Scripts"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def check_prerequisites(self) -> Tuple[bool, Optional[str]]:
|
|
41
|
+
# Check for MSYS2/Cygwin/Git Bash
|
|
42
|
+
shell_env = self._detect_shell_environment()
|
|
43
|
+
if shell_env in ('msys2', 'cygwin', 'git-bash'):
|
|
44
|
+
return (False,
|
|
45
|
+
f"Running in {shell_env.upper()} environment.\n"
|
|
46
|
+
f"This package requires native Windows Python.\n"
|
|
47
|
+
f"Please use PowerShell, Command Prompt, or native Windows terminal.")
|
|
48
|
+
|
|
49
|
+
# Check Visual C++ Redistributable
|
|
50
|
+
vc_ok, vc_error = self._check_vc_redistributable()
|
|
51
|
+
if not vc_ok:
|
|
52
|
+
return (False, vc_error)
|
|
53
|
+
|
|
54
|
+
return (True, None)
|
|
55
|
+
|
|
56
|
+
def _detect_shell_environment(self) -> str:
|
|
57
|
+
"""Detect if running in MSYS2, Cygwin, Git Bash, or native Windows."""
|
|
58
|
+
msystem = os.environ.get('MSYSTEM', '')
|
|
59
|
+
if msystem:
|
|
60
|
+
if 'MINGW' in msystem:
|
|
61
|
+
return 'git-bash'
|
|
62
|
+
return 'msys2'
|
|
63
|
+
|
|
64
|
+
term = os.environ.get('TERM', '')
|
|
65
|
+
if term and 'cygwin' in term:
|
|
66
|
+
return 'cygwin'
|
|
67
|
+
|
|
68
|
+
return 'native-windows'
|
|
69
|
+
|
|
70
|
+
def _find_vc_dlls(self) -> Dict[str, Optional[Path]]:
|
|
71
|
+
"""Find VC++ runtime DLLs in common locations."""
|
|
72
|
+
required_dlls = ['vcruntime140.dll', 'msvcp140.dll']
|
|
73
|
+
found = {}
|
|
74
|
+
|
|
75
|
+
# Search locations in order of preference
|
|
76
|
+
search_paths = []
|
|
77
|
+
|
|
78
|
+
# 1. Current Python environment (conda/venv)
|
|
79
|
+
if hasattr(sys, 'base_prefix'):
|
|
80
|
+
search_paths.append(Path(sys.base_prefix) / 'Library' / 'bin')
|
|
81
|
+
search_paths.append(Path(sys.base_prefix) / 'DLLs')
|
|
82
|
+
if hasattr(sys, 'prefix'):
|
|
83
|
+
search_paths.append(Path(sys.prefix) / 'Library' / 'bin')
|
|
84
|
+
search_paths.append(Path(sys.prefix) / 'DLLs')
|
|
85
|
+
|
|
86
|
+
# 2. System directories
|
|
87
|
+
system_root = os.environ.get('SystemRoot', r'C:\Windows')
|
|
88
|
+
search_paths.append(Path(system_root) / 'System32')
|
|
89
|
+
|
|
90
|
+
# 3. Visual Studio redistributable directories
|
|
91
|
+
program_files = os.environ.get('ProgramFiles', r'C:\Program Files')
|
|
92
|
+
vc_redist = Path(program_files) / 'Microsoft Visual Studio' / '2022' / 'Community' / 'VC' / 'Redist' / 'MSVC'
|
|
93
|
+
if vc_redist.exists():
|
|
94
|
+
for version_dir in vc_redist.iterdir():
|
|
95
|
+
search_paths.append(version_dir / 'x64' / 'Microsoft.VC143.CRT')
|
|
96
|
+
|
|
97
|
+
for dll_name in required_dlls:
|
|
98
|
+
found[dll_name] = None
|
|
99
|
+
for search_path in search_paths:
|
|
100
|
+
dll_path = search_path / dll_name
|
|
101
|
+
if dll_path.exists():
|
|
102
|
+
found[dll_name] = dll_path
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
return found
|
|
106
|
+
|
|
107
|
+
def bundle_vc_dlls_to_env(self, env_dir: Path) -> Tuple[bool, Optional[str]]:
|
|
108
|
+
"""Bundle VC++ runtime DLLs into the isolated environment."""
|
|
109
|
+
required_dlls = ['vcruntime140.dll', 'msvcp140.dll']
|
|
110
|
+
found_dlls = self._find_vc_dlls()
|
|
111
|
+
|
|
112
|
+
# Check which DLLs are missing
|
|
113
|
+
missing = [dll for dll, path in found_dlls.items() if path is None]
|
|
114
|
+
|
|
115
|
+
if missing:
|
|
116
|
+
return (False,
|
|
117
|
+
f"Could not find VC++ DLLs to bundle: {', '.join(missing)}\n\n"
|
|
118
|
+
f"Please install Visual C++ Redistributable:\n"
|
|
119
|
+
f" Download: https://aka.ms/vs/17/release/vc_redist.x64.exe\n"
|
|
120
|
+
f"\nAfter installation, delete the environment and try again.")
|
|
121
|
+
|
|
122
|
+
# Copy DLLs to the environment's Scripts directory
|
|
123
|
+
scripts_dir = env_dir / 'Scripts'
|
|
124
|
+
|
|
125
|
+
copied = []
|
|
126
|
+
for dll_name, source_path in found_dlls.items():
|
|
127
|
+
if source_path:
|
|
128
|
+
try:
|
|
129
|
+
if scripts_dir.exists():
|
|
130
|
+
scripts_target = scripts_dir / dll_name
|
|
131
|
+
if not scripts_target.exists():
|
|
132
|
+
shutil.copy2(source_path, scripts_target)
|
|
133
|
+
copied.append(f"{dll_name} -> Scripts/")
|
|
134
|
+
except (OSError, IOError) as e:
|
|
135
|
+
return (False, f"Failed to copy {dll_name}: {e}")
|
|
136
|
+
|
|
137
|
+
return (True, None)
|
|
138
|
+
|
|
139
|
+
def _check_vc_redistributable(self) -> Tuple[bool, Optional[str]]:
|
|
140
|
+
"""Check if Visual C++ Redistributable DLLs are available."""
|
|
141
|
+
required_dlls = ['vcruntime140.dll', 'msvcp140.dll']
|
|
142
|
+
found_dlls = self._find_vc_dlls()
|
|
143
|
+
|
|
144
|
+
missing = [dll for dll, path in found_dlls.items() if path is None]
|
|
145
|
+
|
|
146
|
+
if missing:
|
|
147
|
+
error_msg = (
|
|
148
|
+
f"Visual C++ Redistributable DLLs not found!\n"
|
|
149
|
+
f"\nMissing: {', '.join(missing)}\n"
|
|
150
|
+
f"\nPlease install Visual C++ Redistributable for Visual Studio 2015-2022:\n"
|
|
151
|
+
f"\n Download (64-bit): https://aka.ms/vs/17/release/vc_redist.x64.exe\n"
|
|
152
|
+
f"\nAfter installation, restart your terminal and try again."
|
|
153
|
+
)
|
|
154
|
+
return (False, error_msg)
|
|
155
|
+
|
|
156
|
+
return (True, None)
|
|
157
|
+
|
|
158
|
+
def make_executable(self, path: Path) -> None:
|
|
159
|
+
# No-op on Windows - executables are determined by extension
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
def rmtree_robust(self, path: Path, max_retries: int = 5, delay: float = 0.5) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
Windows-specific rmtree with retry logic for file locking issues.
|
|
165
|
+
|
|
166
|
+
Handles Windows file locking, read-only files, and antivirus interference.
|
|
167
|
+
"""
|
|
168
|
+
def handle_remove_readonly(func, fpath, exc):
|
|
169
|
+
"""Error handler for removing read-only files."""
|
|
170
|
+
if isinstance(exc[1], PermissionError):
|
|
171
|
+
try:
|
|
172
|
+
os.chmod(fpath, stat.S_IWRITE)
|
|
173
|
+
func(fpath)
|
|
174
|
+
except Exception:
|
|
175
|
+
raise exc[1]
|
|
176
|
+
else:
|
|
177
|
+
raise exc[1]
|
|
178
|
+
|
|
179
|
+
for attempt in range(max_retries):
|
|
180
|
+
try:
|
|
181
|
+
shutil.rmtree(path, onerror=handle_remove_readonly)
|
|
182
|
+
return True
|
|
183
|
+
except PermissionError:
|
|
184
|
+
if attempt < max_retries - 1:
|
|
185
|
+
wait_time = delay * (2 ** attempt)
|
|
186
|
+
time.sleep(wait_time)
|
|
187
|
+
else:
|
|
188
|
+
raise
|
|
189
|
+
except OSError:
|
|
190
|
+
if attempt < max_retries - 1:
|
|
191
|
+
wait_time = delay * (2 ** attempt)
|
|
192
|
+
time.sleep(wait_time)
|
|
193
|
+
else:
|
|
194
|
+
raise
|
|
195
|
+
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
# =========================================================================
|
|
199
|
+
# Media Foundation Detection and Installation
|
|
200
|
+
# =========================================================================
|
|
201
|
+
|
|
202
|
+
def check_media_foundation(self) -> bool:
|
|
203
|
+
"""
|
|
204
|
+
Check if Media Foundation DLLs exist on the system.
|
|
205
|
+
|
|
206
|
+
These are required by packages like opencv-python for video/media support.
|
|
207
|
+
Missing on Windows N/KN editions and some Windows Server installations.
|
|
208
|
+
"""
|
|
209
|
+
system_root = os.environ.get('SystemRoot', r'C:\Windows')
|
|
210
|
+
mf_dlls = ['MFPlat.dll', 'MF.dll', 'MFReadWrite.dll']
|
|
211
|
+
|
|
212
|
+
for dll in mf_dlls:
|
|
213
|
+
dll_path = Path(system_root) / 'System32' / dll
|
|
214
|
+
if not dll_path.exists():
|
|
215
|
+
return False
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
def install_media_foundation(self, log_callback=None) -> Tuple[bool, Optional[str]]:
|
|
219
|
+
"""
|
|
220
|
+
Install Media Foundation via DISM.
|
|
221
|
+
|
|
222
|
+
Requires administrator privileges. Will trigger UAC prompt if needed.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Tuple of (success, error_message)
|
|
226
|
+
"""
|
|
227
|
+
log = log_callback or print
|
|
228
|
+
|
|
229
|
+
if self.check_media_foundation():
|
|
230
|
+
return (True, None)
|
|
231
|
+
|
|
232
|
+
log("Media Foundation not found. Installing via DISM...")
|
|
233
|
+
log("(This requires administrator privileges - a UAC prompt may appear)")
|
|
234
|
+
|
|
235
|
+
# DISM command to install Media Feature Pack
|
|
236
|
+
dism_cmd = [
|
|
237
|
+
"DISM.exe", "/Online", "/Add-Capability",
|
|
238
|
+
"/CapabilityName:Media.MediaFeaturePack~~~~0.0.1.0"
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
# First try without elevation (in case already running as admin)
|
|
243
|
+
result = subprocess.run(
|
|
244
|
+
dism_cmd,
|
|
245
|
+
capture_output=True,
|
|
246
|
+
text=True,
|
|
247
|
+
timeout=300 # 5 minute timeout for installation
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if result.returncode == 0:
|
|
251
|
+
log("Media Foundation installed successfully!")
|
|
252
|
+
return (True, None)
|
|
253
|
+
|
|
254
|
+
# Check if we need elevation
|
|
255
|
+
if "administrator" in result.stderr.lower() or result.returncode == 740:
|
|
256
|
+
log("Requesting administrator privileges...")
|
|
257
|
+
return self._install_media_foundation_elevated(log)
|
|
258
|
+
|
|
259
|
+
# Other error
|
|
260
|
+
return (False,
|
|
261
|
+
f"DISM failed with code {result.returncode}:\n{result.stderr}\n\n"
|
|
262
|
+
f"Please install Media Foundation manually:\n"
|
|
263
|
+
f" 1. Open Settings > Apps > Optional Features\n"
|
|
264
|
+
f" 2. Click 'Add a feature'\n"
|
|
265
|
+
f" 3. Search for 'Media Feature Pack' and install it\n"
|
|
266
|
+
f" 4. Restart your computer")
|
|
267
|
+
|
|
268
|
+
except subprocess.TimeoutExpired:
|
|
269
|
+
return (False, "DISM timed out. Please try installing manually via Settings.")
|
|
270
|
+
except FileNotFoundError:
|
|
271
|
+
return (False, "DISM.exe not found. Please install Media Feature Pack manually via Settings.")
|
|
272
|
+
except Exception as e:
|
|
273
|
+
return (False, f"Error running DISM: {e}")
|
|
274
|
+
|
|
275
|
+
def _install_media_foundation_elevated(self, log_callback=None) -> Tuple[bool, Optional[str]]:
|
|
276
|
+
"""
|
|
277
|
+
Install Media Foundation with UAC elevation.
|
|
278
|
+
|
|
279
|
+
Uses PowerShell Start-Process -Verb RunAs to trigger UAC prompt.
|
|
280
|
+
"""
|
|
281
|
+
log = log_callback or print
|
|
282
|
+
|
|
283
|
+
# Create a PowerShell script that runs DISM elevated
|
|
284
|
+
ps_script = '''
|
|
285
|
+
$result = Start-Process -FilePath "DISM.exe" -ArgumentList "/Online", "/Add-Capability", "/CapabilityName:Media.MediaFeaturePack~~~~0.0.1.0" -Verb RunAs -Wait -PassThru
|
|
286
|
+
exit $result.ExitCode
|
|
287
|
+
'''
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
result = subprocess.run(
|
|
291
|
+
["powershell", "-ExecutionPolicy", "Bypass", "-Command", ps_script],
|
|
292
|
+
capture_output=True,
|
|
293
|
+
text=True,
|
|
294
|
+
timeout=300
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if result.returncode == 0:
|
|
298
|
+
# Verify installation
|
|
299
|
+
if self.check_media_foundation():
|
|
300
|
+
log("Media Foundation installed successfully!")
|
|
301
|
+
return (True, None)
|
|
302
|
+
else:
|
|
303
|
+
return (False,
|
|
304
|
+
"Installation completed but Media Foundation DLLs not found.\n"
|
|
305
|
+
"A system restart may be required.")
|
|
306
|
+
else:
|
|
307
|
+
return (False,
|
|
308
|
+
f"Installation failed or was cancelled.\n"
|
|
309
|
+
f"Please install Media Feature Pack manually:\n"
|
|
310
|
+
f" Settings > Apps > Optional Features > Add a feature > Media Feature Pack")
|
|
311
|
+
|
|
312
|
+
except subprocess.TimeoutExpired:
|
|
313
|
+
return (False, "Installation timed out. Please try installing manually via Settings.")
|
|
314
|
+
except Exception as e:
|
|
315
|
+
return (False, f"Error during elevated installation: {e}")
|
|
316
|
+
|
|
317
|
+
def ensure_media_foundation(self, log_callback=None) -> Tuple[bool, Optional[str]]:
|
|
318
|
+
"""
|
|
319
|
+
Ensure Media Foundation is installed, installing if necessary.
|
|
320
|
+
|
|
321
|
+
This is the main entry point for MF dependency checking.
|
|
322
|
+
"""
|
|
323
|
+
if self.check_media_foundation():
|
|
324
|
+
return (True, None)
|
|
325
|
+
|
|
326
|
+
return self.install_media_foundation(log_callback)
|
|
327
|
+
|
|
328
|
+
# =========================================================================
|
|
329
|
+
# OpenCV DLL Directory Setup
|
|
330
|
+
# =========================================================================
|
|
331
|
+
|
|
332
|
+
def setup_opencv_dll_paths(self, env_dir: Path) -> Tuple[bool, Optional[str]]:
|
|
333
|
+
"""
|
|
334
|
+
Set up the DLL directory structure that opencv-python expects.
|
|
335
|
+
|
|
336
|
+
OpenCV's config.py expects VC++ DLLs at:
|
|
337
|
+
{site-packages}/cv2/../../x64/vc17/bin
|
|
338
|
+
Which resolves to:
|
|
339
|
+
{env_dir}/Lib/x64/vc17/bin
|
|
340
|
+
|
|
341
|
+
This copies the VC++ DLLs to that location.
|
|
342
|
+
"""
|
|
343
|
+
# Target directory that opencv expects
|
|
344
|
+
target_dir = env_dir / "Lib" / "x64" / "vc17" / "bin"
|
|
345
|
+
|
|
346
|
+
# Source: DLLs in Scripts or base env dir (from msvc-runtime package)
|
|
347
|
+
scripts_dir = env_dir / "Scripts"
|
|
348
|
+
|
|
349
|
+
vc_dlls = [
|
|
350
|
+
'vcruntime140.dll', 'vcruntime140_1.dll', 'vcruntime140_threads.dll',
|
|
351
|
+
'msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll',
|
|
352
|
+
'msvcp140_atomic_wait.dll', 'msvcp140_codecvt_ids.dll',
|
|
353
|
+
'concrt140.dll', 'vcomp140.dll', 'vcamp140.dll', 'vccorlib140.dll'
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
358
|
+
|
|
359
|
+
copied = 0
|
|
360
|
+
for dll_name in vc_dlls:
|
|
361
|
+
# Try Scripts first, then env root
|
|
362
|
+
for source_dir in [scripts_dir, env_dir]:
|
|
363
|
+
source = source_dir / dll_name
|
|
364
|
+
if source.exists():
|
|
365
|
+
target = target_dir / dll_name
|
|
366
|
+
if not target.exists():
|
|
367
|
+
shutil.copy2(source, target)
|
|
368
|
+
copied += 1
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
if copied > 0:
|
|
372
|
+
return (True, f"Copied {copied} VC++ DLLs to opencv path")
|
|
373
|
+
else:
|
|
374
|
+
return (True, "VC++ DLLs already in place or not found in venv")
|
|
375
|
+
|
|
376
|
+
except Exception as e:
|
|
377
|
+
return (False, f"Failed to set up opencv DLL paths: {e}")
|