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
32 changes: 32 additions & 0 deletions .github/wheel_contents_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Fail when a wheel holds a file outside the install-layout allowlist.

Usage: python wheel_contents_check.py <wheel> [<wheel> ...]"""

import fnmatch
import sys
import zipfile

# fnmatch's * crosses path separators, so one pattern covers a subtree.
ALLOWED = [
"cppjit/*.py",
"cppjit/libcppjit.so",
"cppjit/interop/lib/libclangCppInterOp*",
"cppjit/interop/lib/clang/*",
"cppjit/interop/include/*",
"cppjit-*.dist-info/*",
]


def check(path):
# directory entries (trailing slash) carry no content
members = [m for m in zipfile.ZipFile(path).namelist() if not m.endswith("/")]
bad = [m for m in members if not any(fnmatch.fnmatch(m, p) for p in ALLOWED)]
for member in bad:
print(f"{path}: unexpected member {member}")
return not bad


if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit(__doc__)
sys.exit(0 if all([check(path) for path in sys.argv[1:]]) else 1)
20 changes: 20 additions & 0 deletions .github/wheel_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""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
183 changes: 183 additions & 0 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
name: Wheels

# Build the wheels (cibuildwheel; config in pyproject.toml) and the sdist
# as artifacts. 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'
- '.github/wheel_contents_check.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
with:
persist-credentials: false

# 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

- name: Assert the build left the checkout clean
run: git diff --exit-code

- name: Check the wheels against the content allowlist
run: python3 .github/wheel_contents_check.py wheelhouse/*.whl

- uses: actions/upload-artifact@v7
with:
name: wheels-${{ matrix.label }}
path: wheelhouse/*.whl
if-no-files-found: error

sdist:
name: sdist
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- run: pipx run build --sdist

- name: Check the sdist metadata
run: pipx run twine check dist/*.tar.gz

- uses: actions/upload-artifact@v7
with:
name: sdist
path: dist/*.tar.gz
if-no-files-found: error

# Run the full suite on a plain runner, outside the manylinux
# container the wheel was built in.
test-wheel:
name: test wheel (full suite)
needs: wheels
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- uses: actions/setup-python@v7
with:
python-version: '3.12'

- uses: actions/download-artifact@v8
with:
name: wheels-manylinux-x86_64
path: wheelhouse

- 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
run: python -X faulthandler .github/wheel_smoke.py

- name: Run the test suite against the installed wheel
env:
CPPINTEROP_EXTRA_INTERPRETER_ARGS: -std=c++20
run: |
cd test
make -j$(nproc) PYTHON=python
python -m pytest -ra

# Build from the sdist and run the full suite against the install.
test-sdist:
name: test sdist (build + full suite)
needs: sdist
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- uses: actions/setup-python@v7
with:
python-version: '3.12'

- uses: compiler-research/ci-workflows/actions/setup-recipe@main
id: llvm
with:
recipe: llvm-wheel
version: '21.1.8'
os: ubuntu-24.04
arch: x86_64
ref: b760e4c171961786b7b20e2cc514302df5373eef

- uses: actions/download-artifact@v8
with:
name: sdist
path: dist

- name: Install the test suite's native deps
run: sudo apt-get -q update && sudo apt-get -y install libeigen3-dev libboost-dev

- name: Build and install from the sdist with the test requirements
env:
RECIPE_PATH: ${{ steps.llvm.outputs.path }}
run: >
python -m pip install dist/cppjit-*.tar.gz -v
--config-settings=cmake.define.LLVM_DIR="$RECIPE_PATH/lib/cmake/llvm"
--config-settings=cmake.define.Clang_DIR="$RECIPE_PATH/lib/cmake/clang"
-r requirements.txt

- name: Smoke the install outside pytest
run: python -X faulthandler .github/wheel_smoke.py

- name: Run the test suite against the sdist install
env:
CPPINTEROP_EXTRA_INTERPRETER_ARGS: -std=c++20
run: |
cd test
make -j$(nproc) PYTHON=python
python -m pytest -ra
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ __pycache__/

# Built test dictionaries and extension modules
*.so
*.so.*.tmp
*Dict.lock

# Packaging
build/
Expand Down
44 changes: 21 additions & 23 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ include(GNUInstallDirs)
# This option won't make a lot of sense since we only ship the shared library in site-packages
# Perhaps this should permanently be OFF and users can build their own CppInterOp if they want to run the tests?
option(CPPJIT_ENABLE_CPPINTEROP_TESTS "enable CppInterOp tests" OFF)
set(CPPINTEROP_GIT_REPOSITORY "https://github.com/compiler-research/CppInterOp.git" CACHE STRING "")
set(CPPINTEROP_GIT_TAG "8d624c621a4b95e36ff73ac708c85a768287478f" CACHE STRING "")
set(CPPINTEROP_GIT_REPOSITORY "https://github.com/keremsahn/CppInterOp.git" CACHE STRING "")
set(CPPINTEROP_GIT_TAG "attr-design" CACHE STRING "")
set(CPPINTEROP_SOURCE_DIR "" CACHE PATH
"Override default CppInterOp built by ExternalProject_Add, with a path to local CppInterOp source")

Expand Down Expand Up @@ -101,7 +101,10 @@ if(_python_platlib)
else()
set(CPPINTEROP_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
endif()
set(CPPINTEROP_INSTALL_DIR "${CPPINTEROP_INSTALL_PREFIX}/cppjit_backend")

# CppInterOp installs here; cppjit's own rules ship a subset, so the wheel
# owns every installed file.
set(CPPINTEROP_STAGE_DIR "${CMAKE_BINARY_DIR}/cppinterop-stage")

# Include cmake for CppInterOp config and build using ExternalProject.
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/AddCppInterOp.cmake)
Expand All @@ -121,11 +124,11 @@ add_dependencies(cppjit CppInterOp)
# falling back to the install prefix (see cppinterop_paths()); the clang
# major names the versioned compiler probed for the runtime resource dir.
target_compile_definitions(cppjit PRIVATE
CPPINTEROP_INSTALL_PREFIX="${CPPINTEROP_INSTALL_PREFIX}"
CPPINTEROP_LIBRARY="cppjit_backend/lib/libclangCppInterOp${CMAKE_SHARED_LIBRARY_SUFFIX}"
CPPINTEROP_INCLUDE_DIR="cppjit_backend/include"
CPPINTEROP_INSTALL_PREFIX="${CPPINTEROP_INSTALL_PREFIX}/cppjit"
CPPINTEROP_LIBRARY="interop/lib/libclangCppInterOp${CMAKE_SHARED_LIBRARY_SUFFIX}"
CPPINTEROP_INCLUDE_DIR="interop/include"
CPPJIT_CLANG_MAJOR="${LLVM_VERSION_MAJOR}"
CPPJIT_CLANG_INCLUDE_DIR="cppjit_backend/lib/clang/${LLVM_VERSION_MAJOR}"
CPPJIT_CLANG_INCLUDE_DIR="interop/lib/clang/${LLVM_VERSION_MAJOR}"
)

target_include_directories(cppjit PRIVATE
Expand All @@ -134,7 +137,7 @@ target_include_directories(cppjit PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/cpyrt
${CMAKE_CURRENT_SOURCE_DIR}/src/interop
${CPPINTEROP_INSTALL_DIR}/include
${CPPINTEROP_STAGE_DIR}/include
${Python_INCLUDE_DIRS}
)

Expand All @@ -159,22 +162,17 @@ set_target_properties(cppjit PROPERTIES
PREFIX "lib"
)

# libcppjit.so is installed at the site-packages root (import libcppjit)
# the extension lives inside the package (import cppjit.libcppjit)
install(TARGETS cppjit
LIBRARY DESTINATION .
LIBRARY DESTINATION cppjit
)

# install CppInterOp libraries and headers
install(CODE "
file(GLOB _interop_libs \"${CPPINTEROP_INSTALL_DIR}/lib/libclangCppInterOp*\")
foreach(_lib \${_interop_libs})
file(INSTALL \${_lib} DESTINATION \${CMAKE_INSTALL_PREFIX}/cppjit_backend/lib)
endforeach()
")

install(CODE "
file(INSTALL \"${CPPINTEROP_INSTALL_DIR}/include/\" DESTINATION \${CMAKE_INSTALL_PREFIX}/cppjit_backend/include)
")
install(DIRECTORY "${CPPINTEROP_STAGE_DIR}/lib/"
DESTINATION cppjit/interop/lib
)
install(DIRECTORY "${CPPINTEROP_STAGE_DIR}/include/"
DESTINATION cppjit/interop/include
)

# ship the builtin headers of the build clang, laid out as a headers-only
# resource dir: only include/ ships
Expand All @@ -185,7 +183,7 @@ if(NOT EXISTS "${_clang_resource_dir}/include")
"${LLVM_DIR} carries no clang resource directory")
endif()
install(DIRECTORY "${_clang_resource_dir}/include/"
DESTINATION "cppjit_backend/lib/clang/${LLVM_VERSION_MAJOR}/include"
DESTINATION "cppjit/interop/lib/clang/${LLVM_VERSION_MAJOR}/include"
)

# the public cpyrt API headers keep their installed cpyrt/ prefix
Expand All @@ -195,5 +193,5 @@ install(FILES
src/cpyrt/DispatchPtr.h
src/cpyrt/PyException.h
src/cpyrt/Reflex.h
DESTINATION cppjit_backend/include/cpyrt
DESTINATION cppjit/interop/include/cpyrt
)
10 changes: 8 additions & 2 deletions cmake/AddCppInterOp.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ function(cppjit_add_cppinterop)
-DLLVM_DIR=${LLVM_DIR}
-DCPPINTEROP_ENABLE_TESTING=${CPPJIT_ENABLE_CPPINTEROP_TESTS}
-DBUILD_SHARED_LIBS=ON
-DCMAKE_INSTALL_PREFIX=${CPPINTEROP_INSTALL_DIR}
# The wheel ships a single unversioned library file.
-DCPPINTEROP_SHARED_LIBRARY_VERSIONING=OFF
-DCMAKE_INSTALL_PREFIX=${CPPINTEROP_STAGE_DIR}
-DCMAKE_INSTALL_LIBDIR=lib
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_CXX_STANDARD=17
Expand Down Expand Up @@ -85,12 +87,16 @@ function(cppjit_add_cppinterop)
set(_log_args "")
endif()

# Install only the library and headers, not CppInterOp's full install tree.
ExternalProject_Add(CppInterOp
${_source_args}
PREFIX "${CMAKE_BINARY_DIR}/CppInterOp"
CMAKE_ARGS ${_args}
# -stripped keeps .dynsym, so the dlsym-based dispatch still resolves.
INSTALL_COMMAND ${CMAKE_COMMAND} --build <BINARY_DIR>
--target install-clangCppInterOp-stripped install-cppinterop-headers
BUILD_BYPRODUCTS
"${CPPINTEROP_INSTALL_DIR}/lib/libclangCppInterOp${CMAKE_SHARED_LIBRARY_SUFFIX}"
"${CPPINTEROP_STAGE_DIR}/lib/libclangCppInterOp${CMAKE_SHARED_LIBRARY_SUFFIX}"
${_log_args}
)

Expand Down
Loading
Loading