Skip to content
Open
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
22 changes: 22 additions & 0 deletions .github/wheel_smoke.py
Original file line number Diff line number Diff line change
@@ -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")
136 changes: 136 additions & 0 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we reuse the recipe we created with manylinux

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
Comment on lines +60 to +71

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are standard PyPA cibuildwheel and sdist actions required to generate the Python wheel


- 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
29 changes: 28 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand All @@ -21,6 +21,7 @@ maintainers = [
]

[tool.scikit-build]
minimum-version = "build-system.requires"
wheel.install-dir = "."
wheel.packages = ["python/cppjit"]
cmake.build-type = "Release"
Expand All @@ -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]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vgvassilev You can see here that the vendored LLVM is reused exactly as build args on the machine that builds the wheel. Those machines are configured in the matrix in wheels.yml:

- { os: ubuntu-24.04, label: manylinux-x86_64, arch: x86_64 }
- { os: macos-26,     label: macosx-arm64,     arch: arm64 }

So for every new target platform we can add a new row and extend the wheels infra. I hope this addresses your concern of any redundancy here

archs = ["x86_64"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about Linux arm?

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" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same to MacOS quesion about the /opt/llvm/ build


[tool.cibuildwheel.macos]
archs = ["arm64"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know its being supported less and less, but what about MacOS intel. There is clearly appetite for it by people wanting to use cppjit (see #39 (comment))

# Test-suite deps (test_eigen/test_boost).
before-all = "brew install eigen boost"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do this assume you are using homebrew? Not everyone does use it on MacOS. I use Macports for example.

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" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused. Where does this /opt/llvm/ build come from?


[tool.pytest.ini_options]
testpaths = ["test"]
pythonpath = ["test"]
Expand Down
30 changes: 26 additions & 4 deletions test/test_boost.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,38 @@
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")
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")
Expand Down Expand Up @@ -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):
Expand All @@ -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")

Expand Down Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions test/test_eigen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading