From d4445dc39ac28335c729c990cded1a6fd0717ac2 Mon Sep 17 00:00:00 2001 From: Aaron Jomy Date: Tue, 25 Aug 2026 15:51:13 +0200 Subject: [PATCH 1/2] [test] Find eigen and boost under the Homebrew and MacPorts prefixes --- test/test_boost.py | 30 ++++++++++++++++++++++++++---- test/test_eigen.py | 2 ++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/test/test_boost.py b/test/test_boost.py index 3680641..5d51cae 100644 --- a/test/test_boost.py +++ b/test/test_boost.py @@ -3,12 +3,30 @@ from pytest import mark, raises, skip from support import IS_MAC_ARM, IS_MAC_X86 -noboost = False -if not ( +# /usr/include and /usr/local/include are on the compiler's default search +# path; the Homebrew (arm64) and MacPorts prefixes are not, so a hit there +# is remembered and added explicitly before the first include. +boost_extra_inc = None +noboost = not ( os.path.exists(os.path.join(os.path.sep, "usr", "include", "boost")) or os.path.exists(os.path.join(os.path.sep, "usr", "local", "include", "boost")) -): - noboost = True +) +if noboost: + for p in ( + os.path.join(os.path.sep, "opt", "homebrew", "include"), + os.path.join(os.path.sep, "opt", "local", "include"), + ): + if os.path.exists(os.path.join(p, "boost")): + boost_extra_inc = p + noboost = False + break + + +def add_boost_include_path(): + if boost_extra_inc is not None: + import cppjit + + cppjit.add_include_path(boost_extra_inc) @mark.skipif(noboost == True, reason="boost not found") @@ -16,6 +34,7 @@ class TestBOOSTANY: def setup_class(cls): import cppjit + add_boost_include_path() cppjit.include("boost/any.hpp") @mark.skipif((IS_MAC_ARM or IS_MAC_X86), reason="Fails to include boost on OS X") @@ -76,6 +95,7 @@ class TestBOOSTOPERATORS: def setup_class(cls): import cppjit + add_boost_include_path() cppjit.include("boost/operators.hpp") def test01_ordered(self): @@ -101,6 +121,7 @@ class TestBOOSTVARIANT: def setup_class(cls): import cppjit + add_boost_include_path() cppjit.include("boost/variant/variant.hpp") cppjit.include("boost/variant/get.hpp") @@ -147,6 +168,7 @@ class TestBOOSTERASURE: def setup_class(cls): import cppjit + add_boost_include_path() cppjit.include("boost/type_erasure/any.hpp") cppjit.include("boost/type_erasure/member.hpp") cppjit.include("boost/mpl/vector.hpp") diff --git a/test/test_eigen.py b/test/test_eigen.py index ab33c34..88a0772 100644 --- a/test/test_eigen.py +++ b/test/test_eigen.py @@ -5,6 +5,8 @@ inc_paths = [ os.path.join(os.path.sep, "usr", "include"), os.path.join(os.path.sep, "usr", "local", "include"), + os.path.join(os.path.sep, "opt", "homebrew", "include"), # Homebrew on arm64 + os.path.join(os.path.sep, "opt", "local", "include"), # MacPorts ] eigen_path = None From 684e8393397da45947a48b4dceb0b104777fd90b Mon Sep 17 00:00:00 2001 From: Aaron Jomy Date: Tue, 25 Aug 2026 15:51:13 +0200 Subject: [PATCH 2/2] [ci] Build and test manylinux and macOS arm64 wheels --- .github/wheel_smoke.py | 22 ++++++ .github/workflows/wheels.yml | 136 +++++++++++++++++++++++++++++++++++ pyproject.toml | 29 +++++++- 3 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 .github/wheel_smoke.py create mode 100644 .github/workflows/wheels.yml diff --git a/.github/wheel_smoke.py b/.github/wheel_smoke.py new file mode 100644 index 0000000..49f668c --- /dev/null +++ b/.github/wheel_smoke.py @@ -0,0 +1,22 @@ +"""Wheel smoke test, run from a clean venv by cibuildwheel's test step: +libcppjit.so must locate libclangCppInterOp relative to its own path (the +build tree is gone by test time), and the template instantiation plus the +header check prove the shipped include tree.""" + +import os + +import cppjit + +cppjit.cppdef("int wheel_smoke(int x) { return x + 1; }") +assert cppjit.gbl.wheel_smoke(41) == 42 + +v = cppjit.gbl.std.vector["int"]() +v.push_back(7) +assert v[0] == 7 + +api = os.path.join( + os.path.dirname(cppjit.__file__), "interop", "include", "cpyrt", "API.h" +) +assert os.path.exists(api), api + +print("wheel smoke OK") diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 0000000..f6722d4 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,136 @@ +name: Wheels + +# Build the wheels (cibuildwheel; config in pyproject.toml) and the sdist +# as artifacts; no index publishing. setup-recipe stages the llvm-wheel +# toolchain at /opt/llvm; linux mounts it into the build container, the +# same manylinux_2_28 image the toolchain was built on. + +on: + workflow_dispatch: + pull_request: + paths: + - '.github/workflows/wheels.yml' + - '.github/wheel_smoke.py' + - 'pyproject.toml' + - 'CMakeLists.txt' + - 'cmake/**' + - 'src/interop/**' + - 'python/cppjit/_cpython_cppjit.py' + push: + tags: ['v*'] + schedule: + - cron: '30 4 * * 1' + +permissions: + contents: read + +concurrency: + group: wheels-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + wheels: + name: wheels ${{ matrix.label }} + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-24.04, label: manylinux-x86_64, arch: x86_64 } + - { os: macos-26, label: macosx-arm64, arch: arm64 } + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v7 + + # ref pins the recipe content the cache key is computed from. + - uses: compiler-research/ci-workflows/actions/setup-recipe@main + id: llvm + with: + recipe: llvm-wheel + version: '21.1.8' + os: ${{ matrix.os }} + arch: ${{ matrix.arch }} + ref: b760e4c171961786b7b20e2cc514302df5373eef + + - name: Stage the toolchain at /opt/llvm + env: + RECIPE_PATH: ${{ steps.llvm.outputs.path }} + run: sudo mv "$RECIPE_PATH" /opt/llvm + + - uses: pypa/cibuildwheel@v4.2.0 + + - uses: actions/upload-artifact@v7 + with: + name: wheels-${{ matrix.label }} + path: wheelhouse/*.whl + + sdist: + name: sdist + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + + - run: pipx run build --sdist + + - uses: actions/upload-artifact@v7 + with: + name: sdist + path: dist/*.tar.gz + + # Run the full suite on a plain runner, outside the manylinux + # container the wheel was built in -- the real relocation boundary. + test-wheel: + name: test wheel (full suite) + needs: wheels + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v7 + with: + python-version: '3.12' + + - uses: actions/download-artifact@v8 + with: + name: wheels-manylinux-x86_64 + path: wheelhouse + + - name: Assert the manylinux DT_NEEDED whitelist + # Static toolchain: anything beyond the whitelist is a grafted + # or leaked dependency. + run: | + set -euo pipefail + allowed='libdl|libpthread|librt|libz|libstdc\+\+|libm|libgcc_s|libc|ld-linux' + bad=0 + for whl in wheelhouse/*.whl; do + dir=$(mktemp -d) + unzip -q "$whl" -d "$dir" + while IFS= read -r so; do + echo "== $whl: ${so#"$dir"/}" + readelf -d "$so" | awk '/NEEDED/ {print $NF}' + readelf -d "$so" | awk '/NEEDED/ {print $NF}' \ + | grep -Ev "^\[($allowed)(\.so|-)" && bad=1 || true + done < <(find "$dir" -name '*.so*') + done + test "$bad" = 0 + + - name: Install the test suite's native deps + # test_eigen/test_boost need them; the CI cells install the same pair. + run: sudo apt-get -q update && sudo apt-get -y install libeigen3-dev libboost-dev + + - name: Install the wheel and the test requirements + run: python -m pip install wheelhouse/cppjit-*cp312*.whl -r requirements.txt + + - name: Smoke the wheel outside pytest + # pytest captures output at the fd level, so a native abort during + # collection dies silently; boot the interpreter outside it first. + run: python -X faulthandler .github/wheel_smoke.py + + - name: Run the test suite against the installed wheel + env: + # Match the CI cells, which run the interpreter under C++20. + CPPINTEROP_EXTRA_INTERPRETER_ARGS: -std=c++20 + run: | + cd test + make -j$(nproc) PYTHON=python + python -m pytest -ra diff --git a/pyproject.toml b/pyproject.toml index 5308b63..8485bd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "scikit_build_core.build" name = "cppjit" dynamic = ["version"] description = "CppJIT: fast and automatic Python-C++ interoperability" -license = {text = "LBNL BSD"} +license = "BSD-3-Clause-LBNL" requires-python = ">=3.12" authors = [ {name = "Aaron Jomy"}, @@ -21,6 +21,7 @@ maintainers = [ ] [tool.scikit-build] +minimum-version = "build-system.requires" wheel.install-dir = "." wheel.packages = ["python/cppjit"] cmake.build-type = "Release" @@ -30,6 +31,32 @@ provider = "scikit_build_core.metadata.regex" field = "version" input = "python/cppjit/_version.py" +[tool.cibuildwheel] +build = ["cp312-*", "cp313-*", "cp314-*"] +skip = ["*-musllinux*"] +build-verbosity = 1 +# The full suite needs a real runner: the image's gcc-8 libstdc++ +# headers crash the JIT on std::filesystem (test_regression test35). +# macOS runs it below, linux in the test-wheel job. +test-sources = ["test", "requirements.txt", ".github/wheel_smoke.py"] +test-command = "python .github/wheel_smoke.py" + +[tool.cibuildwheel.linux] +archs = ["x86_64"] +manylinux-x86_64-image = "manylinux_2_28" +# /opt/llvm is staged on the runner by wheels.yml. +container-engine = { name = "docker", create-args = ["--volume=/opt/llvm:/opt/llvm"] } +# CMAKE_ARGS reaches the CppInterOp ExternalProject sub-configure. +environment = { CMAKE_ARGS = "-DLLVM_DIR=/opt/llvm/lib/cmake/llvm -DClang_DIR=/opt/llvm/lib/cmake/clang" } + +[tool.cibuildwheel.macos] +archs = ["arm64"] +# Test-suite deps (test_eigen/test_boost). +before-all = "brew install eigen boost" +test-command = "python -m pip install -r requirements.txt && python .github/wheel_smoke.py && cd test && make -j$(sysctl -n hw.ncpu) PYTHON=python && python -m pytest -ra" +# 14.0 is the llvm-wheel toolchain's own deployment floor. +environment = { CMAKE_ARGS = "-DLLVM_DIR=/opt/llvm/lib/cmake/llvm -DClang_DIR=/opt/llvm/lib/cmake/clang", MACOSX_DEPLOYMENT_TARGET = "14.0" } + [tool.pytest.ini_options] testpaths = ["test"] pythonpath = ["test"]