Skip to content

Commit 08b868a

Browse files
committed
Implemented static linking configuration to make the DLL self-contained.
Tests should be passing on Linux and MacOS. It's hopeful this will fix the Windows issues.
1 parent 0a0cded commit 08b868a

3 files changed

Lines changed: 150 additions & 19 deletions

File tree

.github/workflows/pytest-windows.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ jobs:
2929
python -m pip install --upgrade pip
3030
3131
- name: Build native libraries
32+
env:
33+
CMAKE_RC_COMPILER: ""
3234
run: |
3335
python build_scripts/build_helios.py --buildmode release --plugins visualizer,weberpenntree --verbose
3436

build_scripts/build_helios.py

Lines changed: 130 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -506,15 +506,30 @@ def run_cmake_configure(self, additional_args: Optional[List[str]] = None) -> No
506506
cmake_cmd.extend(additional_args)
507507

508508
# Add PyHelios-specific options
509-
cmake_cmd.extend([
510-
'-DBUILD_SHARED_LIBS=ON', # Build as shared library
511-
])
509+
if self.platform_name == 'Windows':
510+
# On Windows, disable shared libs for dependencies but enable for PyHelios
511+
cmake_cmd.extend([
512+
'-DBUILD_SHARED_LIBS=OFF', # Static dependencies to avoid DLL hell
513+
'-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded', # Static MSVC runtime
514+
'-DZLIB_ROOT=', # Use bundled zlib
515+
'-DPNG_BUILD_ZLIB=ON', # Build PNG with bundled zlib
516+
'-DPNG_SHARED=OFF', # Static libpng
517+
'-DJPEG_BUILD_SHARED=OFF', # Static libjpeg
518+
])
519+
else:
520+
cmake_cmd.extend([
521+
'-DBUILD_SHARED_LIBS=ON', # Build as shared library on Unix
522+
])
512523

513524
# Windows-specific workaround for zlib resource compilation issue
514525
if self.platform_name == 'Windows':
515526
# The RC compiler fails on win32/zlib1.rc due to C syntax in included headers
516527
# Patch zlib CMakeLists.txt to disable the problematic shared library target
517528
self._patch_zlib_cmake_for_windows()
529+
530+
# Disable resource compilation entirely if CMAKE_RC_COMPILER is not set
531+
if not os.environ.get('CMAKE_RC_COMPILER'):
532+
cmake_cmd.extend(['-DCMAKE_RC_COMPILER=']) # Empty RC compiler disables resource compilation
518533

519534
print(f"Running CMake configure: {' '.join(cmake_cmd)}")
520535

@@ -589,6 +604,67 @@ def find_built_library(self) -> Path:
589604

590605
raise HeliosBuildError(f"Built library {lib_name} not found in build directory")
591606

607+
def _check_windows_dll_dependencies(self, dll_path: Path) -> Dict[str, bool]:
608+
"""
609+
Check Windows DLL dependencies using dumpbin if available.
610+
611+
Args:
612+
dll_path: Path to the DLL to check
613+
614+
Returns:
615+
Dictionary of dependency name -> found status
616+
"""
617+
dependencies = {}
618+
619+
try:
620+
# Try using dumpbin to check dependencies
621+
result = subprocess.run(
622+
['dumpbin', '/dependents', str(dll_path)],
623+
capture_output=True, text=True, timeout=30
624+
)
625+
626+
if result.returncode == 0:
627+
lines = result.stdout.split('\n')
628+
in_dependencies = False
629+
630+
for line in lines:
631+
line = line.strip()
632+
if 'Image has the following dependencies:' in line:
633+
in_dependencies = True
634+
continue
635+
elif in_dependencies and line:
636+
if line.endswith('.dll'):
637+
dep_name = line.lower()
638+
# Check if this is a system DLL that should be available
639+
system_dlls = {
640+
'kernel32.dll', 'user32.dll', 'gdi32.dll', 'advapi32.dll',
641+
'msvcrt.dll', 'vcruntime140.dll', 'msvcp140.dll', 'ucrtbase.dll'
642+
}
643+
644+
if dep_name in system_dlls:
645+
dependencies[dep_name] = True # Assume system DLLs are available
646+
else:
647+
# Check if the DLL exists in system paths
648+
try:
649+
result_check = subprocess.run(
650+
['where', dep_name], capture_output=True
651+
)
652+
dependencies[dep_name] = result_check.returncode == 0
653+
except:
654+
dependencies[dep_name] = False
655+
elif line.startswith('Summary'):
656+
break
657+
658+
print(f"DLL dependencies for {dll_path.name}:")
659+
for dep, found in dependencies.items():
660+
status = "✓" if found else "✗"
661+
print(f" {status} {dep}")
662+
663+
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.CalledProcessError):
664+
print(f"Could not check DLL dependencies (dumpbin not available)")
665+
666+
return dependencies
667+
592668
def _validate_library_loadable(self, library_path: Path) -> None:
593669
"""
594670
Validate that the library can be loaded by ctypes.
@@ -606,7 +682,16 @@ def _validate_library_loadable(self, library_path: Path) -> None:
606682
try:
607683
import ctypes
608684

685+
# Check dependencies before attempting to load (Windows only)
609686
if self.platform_name == 'Windows':
687+
print(f"Checking Windows DLL dependencies for: {library_path.name}")
688+
dependencies = self._check_windows_dll_dependencies(library_path)
689+
missing_deps = [dep for dep, found in dependencies.items() if not found]
690+
691+
if missing_deps:
692+
print(f"WARNING: Missing dependencies detected: {missing_deps}")
693+
print("This may cause ctypes loading to fail")
694+
610695
# Test Windows DLL loading
611696
test_lib = ctypes.WinDLL(str(library_path))
612697
else:
@@ -618,22 +703,42 @@ def _validate_library_loadable(self, library_path: Path) -> None:
618703

619704
except Exception as e:
620705
# FAIL-FAST: Library cannot be loaded by ctypes
621-
error_msg = (
622-
f"CRITICAL: Built library cannot be loaded by ctypes: {library_path}\n"
623-
f"Load error: {e}\n\n"
624-
f"This is a fatal build error. PyHelios requires libraries that can be loaded by ctypes.\n\n"
625-
f"Possible causes:\n"
626-
f"1. Static library (.a) was created instead of shared library (.dylib/.so/.dll)\n"
627-
f"2. Missing dependencies or incorrect linking\n"
628-
f"3. Architecture mismatch (x86 vs ARM64)\n"
629-
f"4. Corrupted library file\n\n"
630-
f"Solutions:\n"
631-
f"1. Ensure CMake builds shared libraries: -DBUILD_SHARED_LIBS=ON\n"
632-
f"2. Check that all dependencies are available\n"
633-
f"3. Rebuild with correct architecture flags\n"
634-
f"4. Verify library dependencies with 'otool -L' (macOS) or 'ldd' (Linux)\n\n"
635-
f"This build cannot proceed with an unusable library."
636-
)
706+
if self.platform_name == 'Windows':
707+
# Windows-specific error handling and dependency checking
708+
error_msg = (
709+
f"CRITICAL: Built Windows DLL cannot be loaded by ctypes: {library_path}\n"
710+
f"Load error: {e}\n\n"
711+
f"This indicates missing DLL dependencies. Common causes:\n\n"
712+
f"1. Visual C++ Runtime Libraries missing\n"
713+
f"2. Static linking not configured properly\n"
714+
f"3. Third-party dependencies (zlib, libpng, etc.) not embedded\n"
715+
f"4. Architecture mismatch (x86 vs x64)\n\n"
716+
f"Windows-specific solutions:\n"
717+
f"1. Install Visual C++ Redistributable (vcredist)\n"
718+
f"2. Use static linking: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded\n"
719+
f"3. Ensure -DBUILD_SHARED_LIBS=OFF for dependencies\n"
720+
f"4. Check DLL dependencies with 'dumpbin /dependents {library_path.name}'\n"
721+
f"5. Verify architecture matches Python (x86 vs x64)\n\n"
722+
f"The built DLL has missing dependencies and cannot be used."
723+
)
724+
else:
725+
# Unix error handling
726+
error_msg = (
727+
f"CRITICAL: Built library cannot be loaded by ctypes: {library_path}\n"
728+
f"Load error: {e}\n\n"
729+
f"This is a fatal build error. PyHelios requires libraries that can be loaded by ctypes.\n\n"
730+
f"Possible causes:\n"
731+
f"1. Static library (.a) was created instead of shared library (.dylib/.so/.dll)\n"
732+
f"2. Missing dependencies or incorrect linking\n"
733+
f"3. Architecture mismatch (x86 vs ARM64)\n"
734+
f"4. Corrupted library file\n\n"
735+
f"Solutions:\n"
736+
f"1. Ensure CMake builds shared libraries: -DBUILD_SHARED_LIBS=ON\n"
737+
f"2. Check that all dependencies are available\n"
738+
f"3. Rebuild with correct architecture flags\n"
739+
f"4. Verify library dependencies with 'otool -L' (macOS) or 'ldd' (Linux)\n\n"
740+
f"This build cannot proceed with an unusable library."
741+
)
637742
raise HeliosBuildError(error_msg)
638743

639744
def copy_to_output(self, library_path: Path) -> Path:
@@ -1269,6 +1374,12 @@ def _patch_zlib_cmake_for_windows(self) -> None:
12691374
(" set_target_properties(zlib PROPERTIES SUFFIX \"1.dll\")", " # set_target_properties(zlib PROPERTIES SUFFIX \"1.dll\")"),
12701375
# Update install targets to exclude zlib shared library
12711376
(" install(TARGETS zlib zlibstatic", " install(TARGETS zlibstatic"),
1377+
# Add static runtime linking for zlib
1378+
("add_library(zlibstatic STATIC ${ZLIB_SRCS} ${ZLIB_PUBLIC_HDRS} ${ZLIB_PRIVATE_HDRS})",
1379+
"add_library(zlibstatic STATIC ${ZLIB_SRCS} ${ZLIB_PUBLIC_HDRS} ${ZLIB_PRIVATE_HDRS})\n# PYHELIOS PATCH: Use static MSVC runtime\nif(MSVC)\n set_target_properties(zlibstatic PROPERTIES MSVC_RUNTIME_LIBRARY \"MultiThreaded$<$<CONFIG:Debug>:Debug>\")\nendif()"),
1380+
# Disable problematic resource compilation
1381+
("if(NOT MINGW)\n set(ZLIB_DLL_SRCS\n win32/zlib1.rc # If present will override custom build rule below.\n )\nendif()",
1382+
"# PYHELIOS PATCH: Disable resource compilation to avoid RC errors\n# if(NOT MINGW)\n# set(ZLIB_DLL_SRCS\n# win32/zlib1.rc # If present will override custom build rule below.\n# )\n# endif()"),
12721383
]
12731384

12741385
for old, new in patches:

pyhelios_build/CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,28 @@ endif()
7777
if(WIN32)
7878
# Define DLL export macro for Windows
7979
target_compile_definitions(pyhelios_shared PRIVATE BUILDING_PYHELIOS_DLL)
80+
81+
# Ensure static linking of Visual Studio runtime and system libraries
8082
set_target_properties(pyhelios_shared PROPERTIES
8183
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
8284
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
8385
OUTPUT_NAME "libhelios"
86+
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
87+
)
88+
89+
# Static link system libraries to avoid missing DLL dependencies
90+
target_link_libraries(pyhelios_shared PRIVATE
91+
kernel32 user32 gdi32 winspool shell32 ole32 oleaut32 uuid comdlg32 advapi32
92+
)
93+
94+
# Force static linking for all targets on Windows to avoid DLL dependencies
95+
set_property(TARGET pyhelios_shared PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
96+
97+
# Ensure all dependencies are found and linked statically
98+
# This prevents "module not found" errors with missing DLLs
99+
target_link_options(pyhelios_shared PRIVATE
100+
"/NODEFAULTLIB:msvcrt.lib"
101+
"/NODEFAULTLIB:msvcrtd.lib"
84102
)
85103
elseif(APPLE)
86104
set_target_properties(pyhelios_shared PROPERTIES

0 commit comments

Comments
 (0)