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.
Files changed (49) hide show
  1. {comfy_env-0.0.5 → comfy_env-0.0.8}/PKG-INFO +1 -1
  2. {comfy_env-0.0.5 → comfy_env-0.0.8}/pyproject.toml +1 -1
  3. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/__init__.py +1 -1
  4. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/manager.py +56 -5
  5. comfy_env-0.0.8/src/comfy_env/env/platform/windows.py +377 -0
  6. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/workers/venv.py +202 -132
  7. comfy_env-0.0.5/src/comfy_env/env/platform/windows.py +0 -195
  8. {comfy_env-0.0.5 → comfy_env-0.0.8}/.github/workflows/publish.yml +0 -0
  9. {comfy_env-0.0.5 → comfy_env-0.0.8}/.gitignore +0 -0
  10. {comfy_env-0.0.5 → comfy_env-0.0.8}/CLAUDE.md +0 -0
  11. {comfy_env-0.0.5 → comfy_env-0.0.8}/CRITICISM.md +0 -0
  12. {comfy_env-0.0.5 → comfy_env-0.0.8}/LICENSE +0 -0
  13. {comfy_env-0.0.5 → comfy_env-0.0.8}/README.md +0 -0
  14. {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/basic_node/__init__.py +0 -0
  15. {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/basic_node/comfy-env.toml +0 -0
  16. {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/basic_node/nodes.py +0 -0
  17. {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/basic_node/worker.py +0 -0
  18. {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/decorator_node/__init__.py +0 -0
  19. {comfy_env-0.0.5 → comfy_env-0.0.8}/examples/decorator_node/nodes.py +0 -0
  20. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/cli.py +0 -0
  21. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/decorator.py +0 -0
  22. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/__init__.py +0 -0
  23. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/config.py +0 -0
  24. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/config_file.py +0 -0
  25. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/detection.py +0 -0
  26. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/platform/__init__.py +0 -0
  27. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/platform/base.py +0 -0
  28. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/platform/darwin.py +0 -0
  29. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/platform/linux.py +0 -0
  30. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/env/security.py +0 -0
  31. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/errors.py +0 -0
  32. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/install.py +0 -0
  33. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/__init__.py +0 -0
  34. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/bridge.py +0 -0
  35. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/protocol.py +0 -0
  36. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/tensor.py +0 -0
  37. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/torch_bridge.py +0 -0
  38. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/transport.py +0 -0
  39. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/ipc/worker.py +0 -0
  40. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/registry.py +0 -0
  41. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/resolver.py +0 -0
  42. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/runner.py +0 -0
  43. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/stubs/__init__.py +0 -0
  44. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/stubs/folder_paths.py +0 -0
  45. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/workers/__init__.py +0 -0
  46. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/workers/base.py +0 -0
  47. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/workers/pool.py +0 -0
  48. {comfy_env-0.0.5 → comfy_env-0.0.8}/src/comfy_env/workers/tensor_utils.py +0 -0
  49. {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.5
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "comfy-env"
3
- version = "0.0.5"
3
+ version = "0.0.8"
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"}
@@ -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.5"
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: Bundle VC++ DLLs
600
+ # Windows: Install VC++ runtime and set up DLL paths
547
601
  if self.platform.name == 'windows':
548
- self.log("Bundling VC++ DLLs...")
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}")