Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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 delocate

- 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
15 changes: 4 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -187,8 +186,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 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:

Expand All @@ -209,19 +207,14 @@ 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: |
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:
Expand Down
40 changes: 32 additions & 8 deletions change-wheel-tag-macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}")
11 changes: 11 additions & 0 deletions python-wheel-globals.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 24 additions & 5 deletions python-wheel.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -181,14 +193,21 @@ 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)

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)
Expand Down
35 changes: 0 additions & 35 deletions repair-wheel-macos.bash

This file was deleted.

6 changes: 5 additions & 1 deletion test-wheel.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
;;
Expand Down
98 changes: 98 additions & 0 deletions tests/macos-wheel-test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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: $<TARGET_FILE:simple_lib>")
message(STATUS "simple_lib import lib: $<TARGET_LINKER_FILE:simple_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: $<TARGET_FILE:py_simple>")
message(STATUS "py_simple will link: $<TARGET_LINKER_FILE:simple_lib>")
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"
)
1 change: 1 addition & 0 deletions tests/macos-wheel-test/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MIT License - Test Project
13 changes: 13 additions & 0 deletions tests/macos-wheel-test/py_simple.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include <pybind11/pybind11.h>
#include "simple_lib.h"

namespace py = pybind11;

PYBIND11_MODULE(py_simple, m) {
m.doc() = "Simple test library for python-cmake-wheel";

py::class_<simple::Calculator>(m, "Calculator")
.def(py::init<>())
.def("add", &simple::Calculator::add, "Add two numbers")
.def("multiply", &simple::Calculator::multiply, "Multiply two numbers");
}
13 changes: 13 additions & 0 deletions tests/macos-wheel-test/simple_lib.cpp
Original file line number Diff line number Diff line change
@@ -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
Loading