From 670fda122f1f43c60d8668f110d1e86f10e74b62 Mon Sep 17 00:00:00 2001 From: Serein Pfeiffer Date: Thu, 9 Oct 2025 14:17:01 +0200 Subject: [PATCH 1/7] macos: Replace repair script by directly usage of correct paths directly in python-wheel.cmake. --- python-wheel.cmake | 15 +++++++++++---- repair-wheel-macos.bash | 35 ----------------------------------- 2 files changed, 11 insertions(+), 39 deletions(-) delete mode 100755 repair-wheel-macos.bash diff --git a/python-wheel.cmake b/python-wheel.cmake index d5c4708..dab2ca9 100644 --- a/python-wheel.cmake +++ b/python-wheel.cmake @@ -185,10 +185,17 @@ function (add_wheel WHEEL_TARGET) add_dependencies(wheel ${WHEEL_TARGET}-setup-py) - set_target_properties(${WHEEL_TARGET} PROPERTIES - BUILD_RPATH "\$ORIGIN;" # Override hardcoded RPATH - INSTALL_RPATH "\$ORIGIN;" - ) + if(APPLE) + set_target_properties(${WHEEL_TARGET} PROPERTIES + BUILD_RPATH "@loader_path" # macOS uses @loader_path + INSTALL_RPATH "@loader_path" + ) + else() + set_target_properties(${WHEEL_TARGET} PROPERTIES + BUILD_RPATH "\$ORIGIN;" # Linux uses $ORIGIN + INSTALL_RPATH "\$ORIGIN;" + ) + endif() endfunction() function (add_wheel_test TEST_NAME) diff --git a/repair-wheel-macos.bash b/repair-wheel-macos.bash deleted file mode 100755 index d1fc3b9..0000000 --- a/repair-wheel-macos.bash +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash - -in_whl=$1 -out_dir=$2 -dylib_dirname=$3 - -set -ex -cd "$(mktemp -d)" -unzip "$in_whl" - -# Print the contents of the current directory after unzipping -echo "\nContents of directory after unzipping wheel ..." -ls -l - -# Set the DYLD_FALLBACK_LIBRARY_PATH to include /usr/local/lib -export DYLD_FALLBACK_LIBRARY_PATH="/usr/local/lib" - -# Add all subdirectories in the current directory to DYLD_FALLBACK_LIBRARY_PATH -for dir in $(find . -type d); do - export DYLD_FALLBACK_LIBRARY_PATH="${DYLD_FALLBACK_LIBRARY_PATH}:$(pwd)/$dir" -done - -echo "\nDelocating ..." -delocate-path -L "$dylib_dirname.dylibs" . - -echo "\nRepackaging ..." -wheel=$(basename "$in_whl") -zip -r "$wheel" ./* -mkdir -p "$out_dir" -mv "$wheel" "$out_dir" -tempdir=$(pwd) -cd - -rm -rf "$tempdir" - -echo "\nDone." From 112ae22fd147f9b189ba9830407744fc9dcacf51 Mon Sep 17 00:00:00 2001 From: Serein Pfeiffer Date: Thu, 9 Oct 2025 14:21:23 +0200 Subject: [PATCH 2/7] macos: Try to extract macos version directly from wheel name, if not possible fallback to macosx_deployment_target usage. --- change-wheel-tag-macos.py | 40 +++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/change-wheel-tag-macos.py b/change-wheel-tag-macos.py index fa6304f..a849abf 100644 --- a/change-wheel-tag-macos.py +++ b/change-wheel-tag-macos.py @@ -4,18 +4,42 @@ import shutil import re -# Read the deployment target -deployment_target = os.getenv('MACOSX_DEPLOYMENT_TARGET') -if not deployment_target: - sys.exit("MACOSX_DEPLOYMENT_TARGET is not set") - # Find the latest wheel file wheel_files = glob.glob(f"{sys.argv[1]}/{sys.argv[2]}*.whl") +if not wheel_files: + sys.exit(f"No wheel files found matching {sys.argv[1]}/{sys.argv[2]}*.whl") + latest_file = max(wheel_files, key=os.path.getctime) +# Extract current macOS version from wheel filename +match = re.search(r'macosx_(\d+)_(\d+)', latest_file) +if match: + current_version = f"{match.group(1)}_{match.group(2)}" +else: + # Fallback if we can't extract version + current_version = None + +# Determine target deployment version +deployment_target = os.getenv('MACOSX_DEPLOYMENT_TARGET') +if deployment_target: + # Use environment variable if set + deployment_target = deployment_target.replace(".", "_") + print(f"Using MACOSX_DEPLOYMENT_TARGET from environment: {deployment_target}") +elif current_version: + # Keep current version from wheel filename + deployment_target = current_version + print(f"Auto-detected macOS version from wheel: {deployment_target}") +else: + # Use sensible default (macOS 11.0 - Big Sur, widely compatible) + deployment_target = "11_0" + print(f"Using default macOS version: {deployment_target}") + # Define the new filename -deployment_target = deployment_target.replace(".", "_") new_filename = re.sub(r'macosx_\d+_\d+', f'macosx_{deployment_target}', latest_file) -# Rename the wheel file -shutil.move(latest_file, new_filename) +# Only rename if filename actually changed +if latest_file != new_filename: + print(f"Renaming: {os.path.basename(latest_file)} -> {os.path.basename(new_filename)}") + shutil.move(latest_file, new_filename) +else: + print(f"Wheel already has correct tag: {os.path.basename(latest_file)}") From 268888e3e20db2d6080607d0f4021c08b8826949 Mon Sep 17 00:00:00 2001 From: Serein Pfeiffer Date: Thu, 9 Oct 2025 14:21:59 +0200 Subject: [PATCH 3/7] doc: Update docs to reflect changes reg. macos support. --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index d356a2f..590b83e 100644 --- a/README.md +++ b/README.md @@ -187,8 +187,7 @@ jobs: ### macOS -For macOS, this repo provides the `repair-wheel-macos.bash` script, which controls -invocations of the `delocate-path` tool which bundles dependencies into your wheel. +macOS wheels are now built correctly without any post-processing. (Note: Versions prior to 1.1.0 required manual wheel repair with `repair-wheel-macos.bash`, which is no longer needed.) Use it in your Github action like this: @@ -212,16 +211,9 @@ jobs: - name: Build (macOS) if: matrix.os == 'macos-13' run: | - python -m pip install delocate - export MACOSX_DEPLOYMENT_TARGET=10.15 mkdir build && cd build cmake .. cmake --build . - # Important step: Audit the wheels! - mv bin/wheel bin/wheel-auditme # Same as on Linux - ./_deps/python-cmake-wheel-src/repair-wheel-macos.bash \ - "$(pwd)"/bin/wheel-auditme/mapget*.whl \ - "$(pwd)"/bin/wheel mapget - name: Deploy uses: actions/upload-artifact@v3 with: From b1d399e70ce0b18641bf3efd4822632a48b75506 Mon Sep 17 00:00:00 2001 From: Serein Pfeiffer Date: Thu, 9 Oct 2025 14:23:04 +0200 Subject: [PATCH 4/7] ci: Intro minimal ci configuration which tests python-wheel.cmake with a sample project. --- .github/workflows/test.yml | 54 +++++++++++++++ test-wheel.bash | 6 +- tests/macos-wheel-test/CMakeLists.txt | 98 +++++++++++++++++++++++++++ tests/macos-wheel-test/LICENSE | 1 + tests/macos-wheel-test/py_simple.cpp | 13 ++++ tests/macos-wheel-test/simple_lib.cpp | 13 ++++ tests/macos-wheel-test/simple_lib.h | 11 +++ tests/macos-wheel-test/test_wheel.py | 63 +++++++++++++++++ 8 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml create mode 100644 tests/macos-wheel-test/CMakeLists.txt create mode 100644 tests/macos-wheel-test/LICENSE create mode 100644 tests/macos-wheel-test/py_simple.cpp create mode 100644 tests/macos-wheel-test/simple_lib.cpp create mode 100644 tests/macos-wheel-test/simple_lib.h create mode 100755 tests/macos-wheel-test/test_wheel.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d2d1979 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,54 @@ +name: Test + +on: + push: + pull_request: + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-13, macos-14, windows-latest] + python-version: ["3.10", "3.11", "3.12", "3.13"] + exclude: + # Python 3.10 doesn't have ARM64 builds for macOS + - os: macos-14 + python-version: "3.10" + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + + - name: Install Python dependencies + run: python -m pip install setuptools wheel + + - name: Configure + working-directory: tests/macos-wheel-test + run: | + mkdir build + cd build + cmake .. + + - name: Build (Linux/macOS) + if: runner.os != 'Windows' + working-directory: tests/macos-wheel-test/build + run: cmake --build . + + - name: Build (Windows) + if: runner.os == 'Windows' + working-directory: tests/macos-wheel-test/build + run: cmake --build . --config Release + + - name: Test (Linux/macOS) + if: runner.os != 'Windows' + working-directory: tests/macos-wheel-test/build + run: ctest --verbose --no-tests=error + + - name: Test (Windows) + if: runner.os == 'Windows' + working-directory: tests/macos-wheel-test/build + run: ctest -C Release --verbose --no-tests=error diff --git a/test-wheel.bash b/test-wheel.bash index cba84a0..35ccee2 100755 --- a/test-wheel.bash +++ b/test-wheel.bash @@ -59,7 +59,11 @@ while [[ $# -gt 0 ]]; do ;; -f|--foreground) echo "→ Starting foreground task: $2" - $2 + if [[ "$2" == *.py ]] && [[ "$2" != *[[:space:]]* ]]; then + python "$2" + else + $2 + fi shift shift ;; diff --git a/tests/macos-wheel-test/CMakeLists.txt b/tests/macos-wheel-test/CMakeLists.txt new file mode 100644 index 0000000..606aed0 --- /dev/null +++ b/tests/macos-wheel-test/CMakeLists.txt @@ -0,0 +1,98 @@ +cmake_minimum_required(VERSION 3.20) +project(test-macos-wheel VERSION 1.0.0) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Export all symbols on Windows (like zswag does) +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) + +# Enable testing +enable_testing() + +# Include python-wheel.cmake from repository root +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +include(python-wheel) + +# Set wheel output directory +set(WHEEL_DEPLOY_DIRECTORY "${CMAKE_BINARY_DIR}/wheel") + +# Find Python +find_package(Python3 COMPONENTS Interpreter Development.Module REQUIRED) + +# Fetch CPM.cmake +set(CPM_DOWNLOAD_VERSION 0.40.2) +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() +if(NOT (EXISTS ${CPM_DOWNLOAD_LOCATION})) + message(STATUS "Downloading CPM.cmake to ${CPM_DOWNLOAD_LOCATION}") + file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} + ) +endif() +include(${CPM_DOWNLOAD_LOCATION}) + +# Fetch pybind11 via CPM +CPMAddPackage("gh:pybind/pybind11@2.13.6") + +# Create simple C++ library +add_library(simple_lib SHARED + simple_lib.cpp + simple_lib.h +) + +target_include_directories(simple_lib PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# Diagnostic logging for Windows +if(WIN32) + message(STATUS "=== Windows Build Diagnostics ===") + message(STATUS "CMAKE_GENERATOR: ${CMAKE_GENERATOR}") + message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}") + message(STATUS "CMAKE_CONFIGURATION_TYPES: ${CMAKE_CONFIGURATION_TYPES}") + get_target_property(simple_lib_output simple_lib RUNTIME_OUTPUT_DIRECTORY) + get_target_property(simple_lib_archive simple_lib ARCHIVE_OUTPUT_DIRECTORY) + message(STATUS "simple_lib RUNTIME_OUTPUT_DIRECTORY: ${simple_lib_output}") + message(STATUS "simple_lib ARCHIVE_OUTPUT_DIRECTORY: ${simple_lib_archive}") + message(STATUS "simple_lib will output to: $") + message(STATUS "simple_lib import lib: $") +endif() + +# Create Python module using pybind11 +pybind11_add_module(py_simple + py_simple.cpp +) + +target_link_libraries(py_simple PRIVATE simple_lib) + +# More diagnostics after linking +if(WIN32) + get_target_property(py_simple_output py_simple RUNTIME_OUTPUT_DIRECTORY) + message(STATUS "py_simple RUNTIME_OUTPUT_DIRECTORY: ${py_simple_output}") + message(STATUS "py_simple will output to: $") + message(STATUS "py_simple will link: $") +endif() + +# Package as wheel using add_wheel() +add_wheel(py_simple + NAME simple_test + VERSION "1.0.0" + AUTHOR "Test Author" + EMAIL "test@example.com" + DESCRIPTION "Minimal test wheel for python-cmake-wheel" + PYTHON_REQUIRES ">=3.9" + TARGET_DEPENDENCIES simple_lib +) + +# Add test to validate wheel functionality +add_wheel_test(test-simple-wheel + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMANDS -f "${CMAKE_CURRENT_SOURCE_DIR}/test_wheel.py" +) diff --git a/tests/macos-wheel-test/LICENSE b/tests/macos-wheel-test/LICENSE new file mode 100644 index 0000000..f6d7b04 --- /dev/null +++ b/tests/macos-wheel-test/LICENSE @@ -0,0 +1 @@ +MIT License - Test Project diff --git a/tests/macos-wheel-test/py_simple.cpp b/tests/macos-wheel-test/py_simple.cpp new file mode 100644 index 0000000..2819c2e --- /dev/null +++ b/tests/macos-wheel-test/py_simple.cpp @@ -0,0 +1,13 @@ +#include +#include "simple_lib.h" + +namespace py = pybind11; + +PYBIND11_MODULE(py_simple, m) { + m.doc() = "Simple test library for python-cmake-wheel"; + + py::class_(m, "Calculator") + .def(py::init<>()) + .def("add", &simple::Calculator::add, "Add two numbers") + .def("multiply", &simple::Calculator::multiply, "Multiply two numbers"); +} diff --git a/tests/macos-wheel-test/simple_lib.cpp b/tests/macos-wheel-test/simple_lib.cpp new file mode 100644 index 0000000..2801fd8 --- /dev/null +++ b/tests/macos-wheel-test/simple_lib.cpp @@ -0,0 +1,13 @@ +#include "simple_lib.h" + +namespace simple { + +int Calculator::add(int a, int b) { + return a + b; +} + +int Calculator::multiply(int a, int b) { + return a * b; +} + +} // namespace simple diff --git a/tests/macos-wheel-test/simple_lib.h b/tests/macos-wheel-test/simple_lib.h new file mode 100644 index 0000000..09aefbe --- /dev/null +++ b/tests/macos-wheel-test/simple_lib.h @@ -0,0 +1,11 @@ +#pragma once + +namespace simple { + +class Calculator { +public: + int add(int a, int b); + int multiply(int a, int b); +}; + +} // namespace simple diff --git a/tests/macos-wheel-test/test_wheel.py b/tests/macos-wheel-test/test_wheel.py new file mode 100755 index 0000000..03ba52d --- /dev/null +++ b/tests/macos-wheel-test/test_wheel.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +Test script to validate that the simple_test wheel works correctly. +Tests import, instantiation, and basic functionality. +""" +import sys + +def test_wheel(): + """Test that the wheel can be imported and used.""" + print("Testing simple_test wheel import and functionality...") + + # Debug: Show what's installed + import site + import os + print(f"Site packages: {site.getsitepackages()}") + for sp in site.getsitepackages(): + simple_test_dir = os.path.join(sp, "simple_test") + if os.path.exists(simple_test_dir): + print(f"Found simple_test in: {simple_test_dir}") + print(f"Contents: {os.listdir(simple_test_dir)}") + else: + print(f"simple_test NOT in: {sp}") + + # Test import + try: + import simple_test + print("[OK] Import successful") + except ImportError as e: + print(f"[FAIL] Import failed: {e}") + import traceback + traceback.print_exc() + return False + + # Test instantiation + try: + calc = simple_test.Calculator() + print("[OK] Calculator instantiation successful") + except Exception as e: + print(f"[FAIL] Calculator instantiation failed: {e}") + return False + + # Test add method + result = calc.add(10, 20) + if result == 30: + print(f"[OK] Calculator.add(10, 20) = {result}") + else: + print(f"[FAIL] Calculator.add(10, 20) returned {result}, expected 30") + return False + + # Test multiply method + result = calc.multiply(5, 6) + if result == 30: + print(f"[OK] Calculator.multiply(5, 6) = {result}") + else: + print(f"[FAIL] Calculator.multiply(5, 6) returned {result}, expected 30") + return False + + print("\n[OK] All tests passed!") + return True + +if __name__ == "__main__": + success = test_wheel() + sys.exit(0 if success else 1) From 247a3ea89f61251442830dfc2a27f1a4e79b066b Mon Sep 17 00:00:00 2001 From: Serein Pfeiffer Date: Wed, 29 Oct 2025 15:45:15 +0100 Subject: [PATCH 5/7] Use 'delocate' again for transitive dep support, but without need for manual fixup script invocation. --- README.md | 7 ++++--- python-wheel-globals.cmake | 11 +++++++++++ python-wheel.cmake | 14 +++++++++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 590b83e..a254cad 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,7 @@ add_wheel(mylib-python-bindings The `add_wheel` command will create a temporary `setup.py` for your project in the build folder, which bundles the necessary files. The execution of this `setup.py` is attached to the custom target `wheelname-setup-py`. It will be executed when you run `cmake --build .` in your build directory. -**Note: On macOS, when the `MACOSX_DEPLOYMENT_TARGET` env is set, the wheel will be -tagged with the indicated deployment target version.** +**Note: On macOS, `delocate` must be installed (`pip install delocate`) for proper handling of library dependencies. The build process automatically invokes `delocate-path` to fix rpaths in bundled libraries, ensuring transitive dependencies work correctly. When `MACOSX_DEPLOYMENT_TARGET` is set, the wheel will be tagged with the indicated deployment target version.** ## Adding tests @@ -187,7 +186,7 @@ jobs: ### macOS -macOS wheels are now built correctly without any post-processing. (Note: Versions prior to 1.1.0 required manual wheel repair with `repair-wheel-macos.bash`, which is no longer needed.) +macOS wheels require `delocate` for proper handling of library dependencies. The build process automatically invokes `delocate-path` to fix rpaths in all bundled libraries, ensuring transitive dependencies (e.g., OpenSSL → libcrypto) work correctly at runtime. Use it in your Github action like this: @@ -208,6 +207,8 @@ jobs: with: python-version: ${{ matrix.python-version }} architecture: x64 + - name: Install delocate + run: pip install delocate - name: Build (macOS) if: matrix.os == 'macos-13' run: | diff --git a/python-wheel-globals.cmake b/python-wheel-globals.cmake index 6479257..0a04edb 100644 --- a/python-wheel-globals.cmake +++ b/python-wheel-globals.cmake @@ -5,6 +5,17 @@ if (APPLE) set(CMAKE_MACOSX_RPATH ON) set(CMAKE_BUILD_WITH_INSTALL_RPATH ON) set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_RPATH};@loader_path") + + # Check for delocate (required for proper library dependency handling) + execute_process( + COMMAND "${Python3_EXECUTABLE}" -c "import delocate" + RESULT_VARIABLE DELOCATE_CHECK + OUTPUT_QUIET + ERROR_QUIET + ) + if(NOT DELOCATE_CHECK EQUAL 0) + message(FATAL_ERROR "delocate is required for macOS wheel building to handle library dependencies correctly. Install with: pip install delocate") + endif() endif() # Guess python wheel filename infixes (abi + platform) to be used diff --git a/python-wheel.cmake b/python-wheel.cmake index dab2ca9..8b2be2a 100644 --- a/python-wheel.cmake +++ b/python-wheel.cmake @@ -168,9 +168,21 @@ function (add_wheel WHEEL_TARGET) configure_file("${PY_WHEEL_SETUP_FILE}" "${SETUP_FILE}") if(APPLE) + # Run delocate-path to fix library dependencies and rpaths + add_custom_target(${WHEEL_TARGET}-delocate + COMMAND + "${Python3_EXECUTABLE}" "-m" "delocate.cmd.delocate_path" + "-L" "${WHEEL_NAME}.dylibs" + "${WHEEL_PACKAGE_DIR}" + COMMENT "Fixing macOS library dependencies with delocate-path" + ) + add_dependencies(${WHEEL_TARGET}-delocate ${WHEEL_TARGET}-copy-files ${WHEEL_TARGET}) + set(EXTRA_ARGS COMMAND "${Python3_EXECUTABLE}" "${PY_CHANGE_TAG_FILE}" "${WHEEL_DEPLOY_DIRECTORY}" "${WHEEL_NAME}") + set(DELOCATE_DEPENDENCY ${WHEEL_TARGET}-delocate) else() set(EXTRA_ARGS "") + set(DELOCATE_DEPENDENCY ${WHEEL_TARGET}-copy-files) endif() add_custom_target(${WHEEL_TARGET}-setup-py @@ -181,7 +193,7 @@ function (add_wheel WHEEL_TARGET) "-w" "${WHEEL_DEPLOY_DIRECTORY}" ${EXTRA_ARGS}) - add_dependencies(${WHEEL_TARGET}-setup-py ${WHEEL_TARGET}-copy-files ${WHEEL_TARGET}) + add_dependencies(${WHEEL_TARGET}-setup-py ${DELOCATE_DEPENDENCY} ${WHEEL_TARGET}) add_dependencies(wheel ${WHEEL_TARGET}-setup-py) From 222b1ba2784eabb0f297d7f1bb4e6cf0326dcb8c Mon Sep 17 00:00:00 2001 From: Serein Pfeiffer Date: Wed, 29 Oct 2025 15:46:27 +0100 Subject: [PATCH 6/7] test: Add test for wheels with bundled transitive dependencies. --- tests/transitive-deps-test/CMakeLists.txt | 94 ++++++++++++++++++++ tests/transitive-deps-test/LICENSE | 21 +++++ tests/transitive-deps-test/README.md | 40 +++++++++ tests/transitive-deps-test/lib_a.cpp | 7 ++ tests/transitive-deps-test/lib_a.h | 13 +++ tests/transitive-deps-test/lib_b.cpp | 5 ++ tests/transitive-deps-test/lib_b.h | 13 +++ tests/transitive-deps-test/py_transitive.cpp | 16 ++++ tests/transitive-deps-test/test_wheel.py | 54 +++++++++++ 9 files changed, 263 insertions(+) create mode 100644 tests/transitive-deps-test/CMakeLists.txt create mode 100644 tests/transitive-deps-test/LICENSE create mode 100644 tests/transitive-deps-test/README.md create mode 100644 tests/transitive-deps-test/lib_a.cpp create mode 100644 tests/transitive-deps-test/lib_a.h create mode 100644 tests/transitive-deps-test/lib_b.cpp create mode 100644 tests/transitive-deps-test/lib_b.h create mode 100644 tests/transitive-deps-test/py_transitive.cpp create mode 100644 tests/transitive-deps-test/test_wheel.py diff --git a/tests/transitive-deps-test/CMakeLists.txt b/tests/transitive-deps-test/CMakeLists.txt new file mode 100644 index 0000000..7b2f78c --- /dev/null +++ b/tests/transitive-deps-test/CMakeLists.txt @@ -0,0 +1,94 @@ +cmake_minimum_required(VERSION 3.20) +project(test-transitive-deps VERSION 1.0.0) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Export all symbols on Windows +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) + +# Enable testing +enable_testing() + +# Include python-wheel.cmake from repository root +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../..") +include(python-wheel) + +# Set wheel output directory +set(WHEEL_DEPLOY_DIRECTORY "${CMAKE_BINARY_DIR}/wheel") + +# Find Python +find_package(Python3 COMPONENTS Interpreter Development.Module REQUIRED) + +# Fetch CPM.cmake +set(CPM_DOWNLOAD_VERSION 0.40.2) +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() +if(NOT (EXISTS ${CPM_DOWNLOAD_LOCATION})) + message(STATUS "Downloading CPM.cmake to ${CPM_DOWNLOAD_LOCATION}") + file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} + ) +endif() +include(${CPM_DOWNLOAD_LOCATION}) + +# Fetch pybind11 via CPM +CPMAddPackage("gh:pybind/pybind11@2.13.6") + +# Create lib_b (lowest level dependency) +add_library(lib_b SHARED + lib_b.cpp + lib_b.h +) + +target_compile_definitions(lib_b PRIVATE LIB_B_EXPORTS) + +target_include_directories(lib_b PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# Create lib_a (depends on lib_b) +add_library(lib_a SHARED + lib_a.cpp + lib_a.h +) + +target_compile_definitions(lib_a PRIVATE LIB_A_EXPORTS) + +target_include_directories(lib_a PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(lib_a PUBLIC lib_b) + +# Create Python module using pybind11 (depends on lib_a) +pybind11_add_module(py_transitive + py_transitive.cpp +) + +target_link_libraries(py_transitive PRIVATE lib_a) + +# Package as wheel using add_wheel() +# Note: We include both lib_a and lib_b in TARGET_DEPENDENCIES +# This tests that delocate properly handles the transitive dependency chain +add_wheel(py_transitive + NAME transitive_test + VERSION "1.0.0" + AUTHOR "Test Author" + EMAIL "test@example.com" + DESCRIPTION "Test wheel with transitive library dependencies (lib_a → lib_b)" + PYTHON_REQUIRES ">=3.9" + TARGET_DEPENDENCIES lib_a lib_b +) + +# Add test to validate wheel functionality +add_wheel_test(test-transitive-wheel + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMANDS -f "${CMAKE_CURRENT_SOURCE_DIR}/test_wheel.py" +) diff --git a/tests/transitive-deps-test/LICENSE b/tests/transitive-deps-test/LICENSE new file mode 100644 index 0000000..57a52e2 --- /dev/null +++ b/tests/transitive-deps-test/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Test + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/transitive-deps-test/README.md b/tests/transitive-deps-test/README.md new file mode 100644 index 0000000..ad24fe8 --- /dev/null +++ b/tests/transitive-deps-test/README.md @@ -0,0 +1,40 @@ +# Transitive Dependencies Test + +This test validates that `python-cmake-wheel` correctly handles transitive library dependencies on macOS using `delocate`. + +## Dependency Chain + +``` +py_transitive (Python module) + ↓ links to +lib_a (shared library) + ↓ links to +lib_b (shared library) +``` + +## What This Tests + +1. **Build**: All three libraries are built and bundled into a wheel +2. **Delocate**: The `delocate-path` tool should: + - Fix rpaths in `py_transitive.so` to use `@loader_path` + - Fix rpaths in `lib_a` to use `@loader_path` + - Fix rpaths in `lib_b` to use `@loader_path` + - Bundle all dependencies into the `.dylibs` directory +3. **Runtime**: When the wheel is installed and imported: + - `py_transitive` can find `lib_a` + - `lib_a` can find `lib_b` + - Function calls work correctly across all three levels + +## Why This Matters + +Without proper delocate handling, `lib_a` would still have absolute paths to `lib_b` (e.g., `/usr/local/lib/lib_b.dylib`), causing runtime failures when the wheel is installed in a different environment. + +## Running the Test + +```bash +cd tests/transitive-deps-test +mkdir build && cd build +cmake .. +cmake --build . +ctest +``` diff --git a/tests/transitive-deps-test/lib_a.cpp b/tests/transitive-deps-test/lib_a.cpp new file mode 100644 index 0000000..092160f --- /dev/null +++ b/tests/transitive-deps-test/lib_a.cpp @@ -0,0 +1,7 @@ +#include "lib_a.h" +#include "lib_b.h" + +int process_data(int x) { + // lib_a uses lib_b internally + return compute_value(x) + 5; +} diff --git a/tests/transitive-deps-test/lib_a.h b/tests/transitive-deps-test/lib_a.h new file mode 100644 index 0000000..6851509 --- /dev/null +++ b/tests/transitive-deps-test/lib_a.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef _WIN32 + #ifdef LIB_A_EXPORTS + #define LIB_A_API __declspec(dllexport) + #else + #define LIB_A_API __declspec(dllimport) + #endif +#else + #define LIB_A_API __attribute__((visibility("default"))) +#endif + +LIB_A_API int process_data(int x); diff --git a/tests/transitive-deps-test/lib_b.cpp b/tests/transitive-deps-test/lib_b.cpp new file mode 100644 index 0000000..4e9d289 --- /dev/null +++ b/tests/transitive-deps-test/lib_b.cpp @@ -0,0 +1,5 @@ +#include "lib_b.h" + +int compute_value(int x) { + return x * 2 + 10; +} diff --git a/tests/transitive-deps-test/lib_b.h b/tests/transitive-deps-test/lib_b.h new file mode 100644 index 0000000..27c7645 --- /dev/null +++ b/tests/transitive-deps-test/lib_b.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef _WIN32 + #ifdef LIB_B_EXPORTS + #define LIB_B_API __declspec(dllexport) + #else + #define LIB_B_API __declspec(dllimport) + #endif +#else + #define LIB_B_API __attribute__((visibility("default"))) +#endif + +LIB_B_API int compute_value(int x); diff --git a/tests/transitive-deps-test/py_transitive.cpp b/tests/transitive-deps-test/py_transitive.cpp new file mode 100644 index 0000000..5a2b152 --- /dev/null +++ b/tests/transitive-deps-test/py_transitive.cpp @@ -0,0 +1,16 @@ +#include +#include "lib_a.h" + +namespace py = pybind11; + +PYBIND11_MODULE(py_transitive, m) { + m.doc() = "Test module with transitive library dependencies"; + + m.def("process", &process_data, "Process data using lib_a (which uses lib_b)"); + + m.def("test_transitive", []() { + // This tests that lib_a can successfully call lib_b + int result = process_data(5); + return result == 25; // (5 * 2 + 10) + 5 = 25 + }, "Test that transitive dependencies work correctly"); +} diff --git a/tests/transitive-deps-test/test_wheel.py b/tests/transitive-deps-test/test_wheel.py new file mode 100644 index 0000000..5ff7eb3 --- /dev/null +++ b/tests/transitive-deps-test/test_wheel.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Test script for transitive dependency wheel.""" + +import sys + +def test_import(): + """Test that the module can be imported.""" + try: + import transitive_test + print("✓ Module imported successfully") + return True + except ImportError as e: + print(f"✗ Failed to import module: {e}") + return False + +def test_transitive_deps(): + """Test that transitive dependencies work correctly.""" + import transitive_test + + # Test the transitive dependency chain: py_transitive → lib_a → lib_b + result = transitive_test.process(5) + expected = 25 # (5 * 2 + 10) + 5 = 25 + + if result == expected: + print(f"✓ Transitive dependency test passed: process(5) = {result}") + return True + else: + print(f"✗ Transitive dependency test failed: expected {expected}, got {result}") + return False + +def test_built_in_validation(): + """Test the built-in validation function.""" + import transitive_test + + if transitive_test.test_transitive(): + print("✓ Built-in transitive test passed") + return True + else: + print("✗ Built-in transitive test failed") + return False + +if __name__ == "__main__": + all_passed = True + + all_passed &= test_import() + all_passed &= test_transitive_deps() + all_passed &= test_built_in_validation() + + if all_passed: + print("\n✓ All tests passed!") + sys.exit(0) + else: + print("\n✗ Some tests failed") + sys.exit(1) From 62c17617f5a2050435b10e9bc60d1f12a969f19b Mon Sep 17 00:00:00 2001 From: Serein Pfeiffer Date: Wed, 29 Oct 2025 15:53:01 +0100 Subject: [PATCH 7/7] ci: Ensure we install delocate, which is a requirement. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d2d1979..6712be4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: architecture: x64 - name: Install Python dependencies - run: python -m pip install setuptools wheel + run: python -m pip install setuptools wheel delocate - name: Configure working-directory: tests/macos-wheel-test