Skip to content
Draft
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
23 changes: 23 additions & 0 deletions .github/wheel_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""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
import cppjit_backend

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_backend.__file__), "include", "cpyrt", "API.h"
)
assert os.path.exists(api), api

print("wheel smoke OK")
118 changes: 118 additions & 0 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Wheels

# Build the distributable artifacts -- manylinux and macOS arm64 wheels via
# cibuildwheel (config in pyproject.toml) plus the sdist; no index publishing.
# The weekly schedule is a drift canary for the conda-forge/homebrew LLVM.

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.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 }
- { os: macos-26, label: macosx-arm64 }
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

- uses: pypa/cibuildwheel@v4.1.1

- uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.label }}
path: wheelhouse/*.whl

sdist:
name: sdist
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4

- run: pipx run build --sdist

- uses: actions/upload-artifact@v4
with:
name: sdist
path: dist/*.tar.gz

# Install the linux wheel on a plain runner, outside the manylinux
# container it was built in, and run the full suite against it.
test-wheel:
name: test wheel (full suite)
needs: wheels
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4

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

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

- name: Install the LLVM the wheel requires at runtime
# The JIT needs the builtin headers of the wheel's LLVM major;
# the loader detects them from `clang` or `clang-21` on PATH.
run: wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- 21

- name: Install the wheel and the test requirements
run: |
python -m venv wheel-venv
wheel-venv/bin/pip install wheelhouse/cppjit-*cp312*.whl
wheel-venv/bin/pip install -r requirements.txt

- name: Smoke the wheel outside pytest
# pytest captures output at the fd level, so a native abort during
# collection dies silently; this surfaces interpreter-boot errors.
run: |
clang --version
clang-21 -print-resource-dir
wheel-venv/bin/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 -j4 PYTHON=$GITHUB_WORKSPACE/wheel-venv/bin/python
rc=0
$GITHUB_WORKSPACE/wheel-venv/bin/python -X faulthandler -m pytest -ra \
> pytest.log 2>&1 || rc=$?
tail -80 pytest.log
exit $rc

- name: Fail on dependency-driven skips
# "0 failed" proves nothing about tests that silently stopped
# running; a skip naming a missing module means the wheel venv
# lost dependency coverage.
run: "! grep -iE '^SKIPPED.*(no module named|could not import|module .* not installed)' test/pytest.log"
25 changes: 16 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ set(CPPINTEROP_GIT_TAG "8d624c621a4b95e36ff73ac708c85a768287478f" CACHE STRING "
set(CPPINTEROP_SOURCE_DIR "" CACHE PATH
"Override default CppInterOp built by ExternalProject_Add, with a path to local CppInterOp source")

# Development.Module, not Development: the full component additionally
# requires libpython (Development.Embed), which manylinux images do not
# ship — extension modules only need headers and module-link rules.
set(Python_FIND_VIRTUALENV ONLY)
find_package(Python COMPONENTS Interpreter Development)
find_package(Python COMPONENTS Interpreter Development.Module)
if(NOT Python_FOUND)
set(Python_FIND_VIRTUALENV STANDARD)
find_package(Python COMPONENTS Interpreter Development)
find_package(Python COMPONENTS Interpreter Development.Module)
endif()
if(NOT Python_Development_FOUND)
if(NOT Python_Development.Module_FOUND)
message(FATAL_ERROR "Python development headers not found")
endif()

Expand Down Expand Up @@ -95,10 +98,11 @@ execute_process(
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(_python_platlib)
set(CPPINTEROP_INSTALL_DIR "${_python_platlib}/cppjit_backend")
set(CPPINTEROP_INSTALL_PREFIX "${_python_platlib}")
else()
set(CPPINTEROP_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/cppjit_backend")
set(CPPINTEROP_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
endif()
set(CPPINTEROP_INSTALL_DIR "${CPPINTEROP_INSTALL_PREFIX}/cppjit_backend")

# Include cmake for CppInterOp config and build using ExternalProject.
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/AddCppInterOp.cmake)
Expand All @@ -114,11 +118,14 @@ set(INTEROP_SOURCES
add_library(cppjit SHARED ${CPYRT_SOURCES} ${INTEROP_SOURCES})
add_dependencies(cppjit CppInterOp)

# The exact library file the wrapper dlopens and the include dir the
# interpreter boot requires.
# The wrapper anchors these relative spellings at its own load location,
# 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_LIBRARY="${CPPINTEROP_INSTALL_DIR}/lib/libclangCppInterOp${CMAKE_SHARED_LIBRARY_SUFFIX}"
CPPINTEROP_INCLUDE_DIR="${CPPINTEROP_INSTALL_DIR}/include"
CPPINTEROP_INSTALL_PREFIX="${CPPINTEROP_INSTALL_PREFIX}"
CPPINTEROP_LIBRARY="cppjit_backend/lib/libclangCppInterOp${CMAKE_SHARED_LIBRARY_SUFFIX}"
CPPINTEROP_INCLUDE_DIR="cppjit_backend/include"
CPPJIT_CLANG_MAJOR="${LLVM_VERSION_MAJOR}"
)

target_include_directories(cppjit PRIVATE
Expand Down
31 changes: 31 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,37 @@ cmake.build-type = "Release"
metadata.version.provider = "scikit_build_core.metadata.regex"
metadata.version.input = "python/cppjit/_version.py"

[tool.cibuildwheel]
build = ["cp312-*", "cp313-*", "cp314-*"]
skip = ["*-musllinux*"]
build-verbosity = 1
test-command = "python {project}/.github/wheel_smoke.py"

[tool.cibuildwheel.linux]
archs = ["x86_64"]
manylinux-x86_64-image = "manylinux_2_28"
# LLVM/Clang 21 from conda-forge: built against a glibc within the
# manylinux_2_28 policy, unlike apt.llvm.org or release-tarball builds.
# lld is mandatory: BFD ld mis-relaxes R_X86_64_GOTPCRELX relocations in
# the conda static archives, and the result crashes at the first JIT use.
before-all = """
curl -fsSL --retry 5 -o /tmp/micromamba.tar.bz2 https://micro.mamba.pm/api/micromamba/linux-64/latest
tar -xjf /tmp/micromamba.tar.bz2 -C /usr/local bin/micromamba
export MAMBA_ROOT_PREFIX=/opt/mamba
micromamba create -y -p /opt/llvm -c conda-forge 'llvmdev=21.*' 'clangdev=21.*' lld zstd zlib libxml2
"""
# Environment variables reach the CppInterOp ExternalProject sub-configure
# (find_package(zstd) needs the conda env) and auditwheel (LD_LIBRARY_PATH
# resolves the conda DT_NEEDEDs it grafts); ld.lld comes from PATH.
environment = { CMAKE_ARGS = "-DLLVM_DIR=/opt/llvm/lib/cmake/llvm -DClang_DIR=/opt/llvm/lib/cmake/clang", CMAKE_PREFIX_PATH = "/opt/llvm", LDFLAGS = "-fuse-ld=lld", PATH = "/opt/llvm/bin:$PATH", LD_LIBRARY_PATH = "/opt/llvm/lib" }

[tool.cibuildwheel.macos]
archs = ["arm64"]
before-all = "brew install llvm@21"
# The deployment target must match the Homebrew bottles the wheel grafts
# (bottles target the runner's OS); delocate rejects the default 11.0 label.
environment = { CMAKE_ARGS = "-DLLVM_DIR=/opt/homebrew/opt/llvm@21/lib/cmake/llvm -DClang_DIR=/opt/homebrew/opt/llvm@21/lib/cmake/clang", MACOSX_DEPLOYMENT_TARGET = "26.0" }

[tool.pytest.ini_options]
testpaths = ["test"]
pythonpath = ["test"]
Expand Down
49 changes: 12 additions & 37 deletions python/cppjit/_cpython_cppjit.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""CPython-specific touch-ups"""

import ctypes
import sys
import importlib.util

from . import _stdcpp_fix # noqa: F401

Expand All @@ -17,42 +17,17 @@
"_end_capture_stderr",
]

# the merged libcppjit extension is the backend: importing it loads the
# C++ runtime (no separate loader.load_cpp_backend() step, which would
# initialize the interpreter twice)
import libcppjit as _backend

# explicitly expose APIs from libcppjit
_w = ctypes.CDLL(_backend.__file__, ctypes.RTLD_GLOBAL)


# some beautification for inspect (only on p2)
if sys.hexversion < 0x3000000:
# TODO: this reliese on CPPOverload cooking up a func_code object, which atm
# is simply not implemented for p3 :/

# convince inspect that cppjit method proxies are possible drop-ins for python
# methods and classes for pydoc
import inspect

inspect._old_isfunction = inspect.isfunction

def isfunction(object):
if isinstance(object, _backend.CPPOverload) and not object.im_class:
return True
return inspect._old_isfunction(object)

inspect.isfunction = isfunction

inspect._old_ismethod = inspect.ismethod

def ismethod(object):
if isinstance(object, _backend.CPPOverload):
return True
return inspect._old_ismethod(object)

inspect.ismethod = ismethod
del isfunction, ismethod
# preload the merged extension with ctypes and run LoadCppInterOp() first,
# so the interpreter is ready before the extension module initializes
_spec = importlib.util.find_spec("libcppjit")
if _spec is None or not _spec.origin:
raise ImportError("cannot locate the libcppjit extension module")
_w = ctypes.CDLL(_spec.origin, ctypes.RTLD_GLOBAL)
if not _w.LoadCppInterOp():
raise RuntimeError("failed to load CppInterOp (LoadCppInterOp returned 0)")
del _spec

import libcppjit as _backend # noqa: E402


### template support ---------------------------------------------------------
Expand Down
Loading
Loading