1313import subprocess
1414import ctypes
1515
16- def validate_library (lib_path : Path ) -> bool :
16+ def validate_library (lib_path ) :
1717 """
1818 Validate that library is a proper PyHelios library that can be loaded.
1919
@@ -53,7 +53,104 @@ def validate_library(lib_path: Path) -> bool:
5353 print (f"[ERROR] Error validating library { lib_path .name } : { e } " )
5454 return False
5555
56- def copy_assets_for_packaging (project_root : Path ) -> None :
56+ def find_windows_dll_dependencies (lib_path , project_root ):
57+ """
58+ Find and collect all required DLL dependencies for Windows wheels.
59+
60+ Args:
61+ lib_path: Path to the main library (libhelios.dll)
62+ project_root: Path to project root directory
63+
64+ Returns:
65+ List of Path objects for all required DLLs
66+ """
67+ if platform .system () != 'Windows' :
68+ return []
69+
70+ print (f"\n Scanning Windows DLL dependencies for { lib_path .name } ..." )
71+
72+ dependencies = []
73+
74+ # 1. OptiX DLL (if radiation plugin is built)
75+ # PyHelios CMake copies OptiX DLL to build/lib/ directory for wheel packaging
76+ build_lib_dir = project_root / 'pyhelios_build' / 'build' / 'lib'
77+ optix_dlls = ['optix.6.5.0.dll' , 'optix.51.dll' ] # Support both versions
78+
79+ for optix_dll in optix_dlls :
80+ optix_path = build_lib_dir / optix_dll
81+ if optix_path .exists ():
82+ dependencies .append (optix_path )
83+ print (f"[FOUND] OptiX dependency: { optix_dll } " )
84+ break
85+
86+ # 2. CUDA Runtime DLLs (from CI environment)
87+ # Check common CUDA installation paths
88+ cuda_paths = [
89+ 'C:\\ Program Files\\ NVIDIA GPU Computing Toolkit\\ CUDA' ,
90+ 'C:\\ Program Files (x86)\\ NVIDIA GPU Computing Toolkit\\ CUDA'
91+ ]
92+
93+ cuda_dlls = ['cudart64_12.dll' , 'cudart64_11.dll' , 'cudart64_10.dll' ] # Common versions
94+ for cuda_path in cuda_paths :
95+ cuda_root = Path (cuda_path )
96+ if cuda_root .exists ():
97+ for version_dir in cuda_root .glob ('v*' ):
98+ bin_dir = version_dir / 'bin'
99+ if bin_dir .exists ():
100+ for cuda_dll in cuda_dlls :
101+ cuda_dll_path = bin_dir / cuda_dll
102+ if cuda_dll_path .exists ():
103+ dependencies .append (cuda_dll_path )
104+ print (f"[FOUND] CUDA Runtime: { cuda_dll } " )
105+ break
106+ break
107+ break
108+
109+ # 3. Visual C++ Runtime (from Windows SDK/Visual Studio)
110+ vcruntime_dlls = ['vcruntime140.dll' , 'msvcp140.dll' , 'concrt140.dll' ]
111+
112+ # Check system directories and Visual Studio installations
113+ system_paths = [
114+ 'C:\\ Windows\\ System32' ,
115+ 'C:\\ Program Files (x86)\\ Microsoft Visual Studio\\ 2019\\ Enterprise\\ VC\\ Redist\\ MSVC' ,
116+ 'C:\\ Program Files (x86)\\ Microsoft Visual Studio\\ 2022\\ Enterprise\\ VC\\ Redist\\ MSVC' ,
117+ 'C:\\ Program Files\\ Microsoft Visual Studio\\ 2019\\ Enterprise\\ VC\\ Redist\\ MSVC' ,
118+ 'C:\\ Program Files\\ Microsoft Visual Studio\\ 2022\\ Enterprise\\ VC\\ Redist\\ MSVC'
119+ ]
120+
121+ for vc_dll in vcruntime_dlls :
122+ found = False
123+ for base_path in system_paths :
124+ base_path = Path (base_path )
125+ if base_path .exists ():
126+ # Check direct path and subdirectories
127+ for dll_path in [base_path / vc_dll ] + list (base_path .rglob (vc_dll )):
128+ if dll_path .exists () and dll_path .is_file ():
129+ dependencies .append (dll_path )
130+ print (f"[FOUND] VC++ Runtime: { vc_dll } " )
131+ found = True
132+ break
133+ if found :
134+ break
135+
136+ # 4. Additional OptiX dependencies if found in NVIDIA directories
137+ nvidia_paths = [
138+ 'C:\\ Program Files\\ NVIDIA Corporation\\ OptiX SDK 6.5.0\\ bin64' ,
139+ 'C:\\ ProgramData\\ NVIDIA Corporation\\ OptiX\\ cache'
140+ ]
141+
142+ for nvidia_path in nvidia_paths :
143+ nvidia_path = Path (nvidia_path )
144+ if nvidia_path .exists ():
145+ for optix_file in nvidia_path .glob ('*.dll' ):
146+ if 'optix' in optix_file .name .lower ():
147+ dependencies .append (optix_file )
148+ print (f"[FOUND] Additional OptiX: { optix_file .name } " )
149+
150+ print (f"Found { len (dependencies )} Windows DLL dependencies" )
151+ return dependencies
152+
153+ def copy_assets_for_packaging (project_root ):
57154 """
58155 Copy Helios assets to pyhelios/assets/build for packaging in wheels.
59156
@@ -115,9 +212,45 @@ def copy_assets_for_packaging(project_root: Path) -> None:
115212 plugin_asset_dirs = {
116213 'weberpenntree' : ['leaves' , 'wood' , 'xml' ],
117214 'visualizer' : ['textures' , 'shaders' ],
118- 'radiation' : ['spectral_data' ] if Path (plugins_src_dir / 'radiation' / 'spectral_data' ).exists () else []
119215 # NOTE: plantarchitecture and canopygenerator are not integrated with PyHelios - assets not needed
120216 }
217+
218+ # Add radiation assets only on platforms that build GPU plugins (Windows/Linux)
219+ if platform .system () != 'Darwin' : # Exclude radiation on macOS
220+ radiation_assets = []
221+
222+ # Add spectral data if it exists
223+ if Path (plugins_src_dir / 'radiation' / 'spectral_data' ).exists ():
224+ radiation_assets .append ('spectral_data' )
225+
226+ # Copy generated PTX files from build directory (critical for OptiX functionality)
227+ radiation_build_dir = build_dir / 'plugins' / 'radiation'
228+ if radiation_build_dir .exists ():
229+ # Copy generated PTX files from build directory
230+ plugin_dest = dest_assets_dir / 'plugins' / 'radiation'
231+ plugin_dest .mkdir (parents = True , exist_ok = True )
232+
233+ ptx_files = list (radiation_build_dir .glob ('*.ptx' ))
234+ if ptx_files :
235+ ptx_copied = 0
236+ for ptx_file in ptx_files :
237+ try :
238+ shutil .copy2 (ptx_file , plugin_dest / ptx_file .name )
239+ print (f"[OK] Copied PTX file: { ptx_file .name } " )
240+ ptx_copied += 1
241+ except Exception as e :
242+ print (f"[ERROR] Failed to copy PTX file { ptx_file .name } : { e } " )
243+
244+ if ptx_copied > 0 :
245+ print (f"[OK] Successfully copied { ptx_copied } PTX files for radiation plugin" )
246+ total_copied += ptx_copied
247+ else :
248+ print (f"[WARNING] No PTX files found in radiation build directory: { radiation_build_dir } " )
249+ else :
250+ print (f"[WARNING] Radiation build directory not found: { radiation_build_dir } " )
251+
252+ if radiation_assets :
253+ plugin_asset_dirs ['radiation' ] = radiation_assets
121254
122255 # Process each plugin directory
123256 for plugin_dir in plugins_src_dir .iterdir ():
@@ -161,7 +294,10 @@ def copy_assets_for_packaging(project_root: Path) -> None:
161294 print (f"[OK] Successfully copied { total_copied } total assets for packaging" )
162295 else :
163296 print ("Warning: No assets found to copy" )
164-
297+
298+ # Note: Asset directories should NOT have __init__.py files as they are data directories,
299+ # not Python packages. setuptools handles them correctly via package_data configuration.
300+
165301 print (f"[OK] Assets packaged in { dest_assets_dir } " )
166302
167303def build_and_prepare (build_args ):
@@ -317,7 +453,66 @@ def build_and_prepare(build_args):
317453 # Continue anyway - some failures might be acceptable
318454
319455 print (f"[OK] Successfully prepared { copied_count } libraries for packaging" )
320-
456+
457+ # Windows-specific: Bundle additional DLL dependencies
458+ if system == 'Windows' and copied_count > 0 :
459+ print (f"\n === Windows DLL Dependency Bundling ===" )
460+
461+ # Find the main library (usually libhelios.dll or helios.dll)
462+ main_library = None
463+ for lib_file in found_libraries :
464+ if 'helios' in lib_file .name .lower () and lib_file .suffix == '.dll' :
465+ main_library = lib_file
466+ break
467+
468+ if main_library :
469+ # Find all required DLL dependencies
470+ dependencies = find_windows_dll_dependencies (main_library , project_root )
471+
472+ dependency_copied = 0
473+ dependency_failed = 0
474+
475+ for dep_dll in dependencies :
476+ try :
477+ dest_dll = plugins_dir / dep_dll .name
478+
479+ # Skip if already exists (avoid overwriting main libraries)
480+ if dest_dll .exists ():
481+ print (f"[SKIP] Dependency already bundled: { dep_dll .name } " )
482+ continue
483+
484+ shutil .copy2 (dep_dll , dest_dll )
485+ print (f"[OK] Bundled dependency: { dep_dll .name } " )
486+ dependency_copied += 1
487+
488+ except (OSError , PermissionError ) as e :
489+ print (f"[ERROR] Failed to bundle critical dependency { dep_dll .name } : { e } " )
490+ print (f"This dependency is required for libhelios.dll to load properly on Windows systems" )
491+ print (f"without development tools installed. The wheel will not work correctly." )
492+ dependency_failed += 1
493+
494+ print (f"\n === Dependency Bundle Summary ===" )
495+ print (f"Found: { len (dependencies )} DLL dependencies" )
496+ print (f"Bundled: { dependency_copied } dependencies" )
497+ print (f"Failed: { dependency_failed } dependencies" )
498+
499+ # Fail-fast: If critical dependencies are missing, the wheel is broken
500+ if len (dependencies ) > 0 and dependency_copied == 0 :
501+ print (f"[ERROR] CRITICAL: No Windows DLL dependencies were bundled!" )
502+ print (f"This means the wheel will fail to load on systems without development tools." )
503+ print (f"Required dependencies: { [dep .name for dep in dependencies ]} " )
504+ print (f"The wheel build cannot continue with missing critical dependencies." )
505+ sys .exit (1 )
506+ elif dependency_failed > 0 :
507+ print (f"[ERROR] CRITICAL: { dependency_failed } critical dependencies could not be bundled!" )
508+ print (f"The wheel will not work properly on clean Windows systems." )
509+ print (f"All dependencies must be bundled for the wheel to function correctly." )
510+ sys .exit (1 )
511+ else :
512+ print (f"[OK] All { dependency_copied } Windows DLL dependencies bundled successfully" )
513+ else :
514+ print (f"[WARNING] Could not find main Helios library for dependency analysis" )
515+
321516 # Copy assets for packaging
322517 copy_assets_for_packaging (project_root )
323518
@@ -327,15 +522,33 @@ def main():
327522 print ("prepare_wheel.py - Build PyHelios native libraries and prepare for wheel packaging" )
328523 print ()
329524 print ("Usage: python prepare_wheel.py <build_args...>" )
330- print ("Example: python prepare_wheel.py --buildmode release --exclude radiation,aeriallidar --verbose" )
525+ print ("Examples:" )
526+ print (" python prepare_wheel.py --buildmode release --nogpu --verbose" )
527+ print (" python prepare_wheel.py --plugins weberpenntree,visualizer" )
528+ print (" python prepare_wheel.py --exclude radiation --buildmode debug" )
331529 print ()
332530 print ("This script:" )
333531 print (" 1. Calls build_scripts/build_helios.py with the provided arguments" )
334532 print (" 2. Copies built libraries to pyhelios/plugins/ for wheel packaging" )
335- print (" 3. Validates libraries can be loaded properly" )
533+ print (" 3. Copies required assets to pyhelios/assets/build/" )
534+ print (" 4. Validates libraries can be loaded properly" )
336535 print ()
337- print ("For build argument options, run:" )
536+ print ("Common build arguments:" )
537+ print (" --buildmode {debug,release,relwithdebinfo} CMake build type" )
538+ print (" --nogpu Exclude GPU plugins" )
539+ print (" --novis Exclude visualization plugins" )
540+ print (" --plugins <plugin1,plugin2,...> Specific plugins to build" )
541+ print (" --exclude <plugin1,plugin2,...> Plugins to exclude" )
542+ print (" --clean Clean build artifacts first" )
543+ print (" --verbose Verbose output" )
544+ print ()
545+ print ("For complete build argument options, run:" )
338546 print (" python build_scripts/build_helios.py --help" )
547+ print ()
548+ print ("For list of integrated plugins, run:" )
549+ print (" python build_scripts/build_helios.py --list-plugins" )
550+ print ("For list of all helios-core plugins, run:" )
551+ print (" python build_scripts/build_helios.py --list-all-plugins" )
339552 sys .exit (0 if '--help' in sys .argv or '-h' in sys .argv else 1 )
340553
341554 build_args = sys .argv [1 :]
0 commit comments