Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1d74636
feat(build-system): add foundation ExecPlan for Rust-backed performan…
leynos Jan 18, 2026
edf5a14
docs(build-system): clarify uv_build as primary Python build backend
leynos Jan 18, 2026
573373e
feat(rust-extension): add optional Rust backend with build and CI int…
leynos Jan 19, 2026
4a4257b
feat(ci): replace cibuildwheel with maturin for native wheels
leynos Jan 19, 2026
d9ab6ac
docs(build): update manylinux compatibility flag in Linux wheel build…
leynos Jan 19, 2026
95d97e2
docs(build): update maturin build commands for manylinux wheels
leynos Jan 19, 2026
0d8f25a
refactor(ci): optimize Python interpreter resolution in manylinux build
leynos Jan 19, 2026
f0a3636
feat(rust-extension): add availability probe and improve Rust wheel b…
leynos Jan 20, 2026
70c3e1b
fix(ci): specify stable Rust toolchain and fix native wheel path
leynos Jan 20, 2026
6b83374
feat(rust-extension): upgrade Rust edition to 2024 and add Cargo.lock
leynos Jan 21, 2026
853ba8d
docs(documentation): correct numbered list format and update Rust ver…
leynos Jan 21, 2026
1681893
feat(rust-extension): add public API to check Rust extension availabi…
leynos Jan 21, 2026
5ddaea9
docs(build): update Rust toolchain versions and macOS build target
leynos Jan 21, 2026
8e92434
fix(ci): use explicit manylinux Python interpreter path for 3.13+
leynos Jan 21, 2026
1c8db54
fix(ci): use version-only interpreter for aarch64 cross-compilation
leynos Jan 22, 2026
1efb742
fix(ci): handle patch versions in pytag computation for manylinux
leynos Jan 23, 2026
4351145
fix(ci): unquote glob pattern variable in verify-wheel-install
leynos Jan 23, 2026
0381949
fix(ci): add pyproject.toml for maturin in rust crate directory
leynos Jan 25, 2026
1e2e490
debug(ci): add wheel inspection to verify-wheel-install
leynos Jan 25, 2026
c621728
chore(test-wheelhouse): add built wheel binary file
leynos Jan 25, 2026
9e7e9a8
refactor(build): address PR review comments
leynos Jan 25, 2026
f684a3e
fix(build): remove rust/cuprum-rust/pyproject.toml causing incomplete…
leynos Jan 26, 2026
29f1e91
fix(ci): run wheel verification imports from temp dir to avoid local …
leynos Jan 26, 2026
ae97a57
fix(ci): add count guard for pure wheel glob expansion
leynos Jan 26, 2026
61735e5
fix(ci): use pipefail for robust shell error handling
leynos Jan 26, 2026
8954ca4
refactor(ci): remove redundant nullglob shell option
leynos Jan 26, 2026
f7cab4b
fix(ci): fail fast when native module import succeeds but reports una…
leynos Jan 27, 2026
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
43 changes: 31 additions & 12 deletions .github/actions/build-wheels/action.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
name: Build Wheels
description: Builds Python wheels across specified architectures using cibuildwheel and uploads them.
description: Builds native Python wheels using maturin and uploads them.
inputs:
python-version:
description: Python version to set up
required: true
cibw-arch:
description: Architecture to pass to cibuildwheel
required: true
target:
description: Rust target triple for cross builds (optional)
required: false
artifact-name:
description: Name of the uploaded artifact
required: true
wheelhouse:
description: Output directory for built wheels
required: false
default: wheelhouse
maturin-version:
description: Maturin version to install
required: false
default: "1.6.0"
runs:
using: composite
steps:
Expand All @@ -20,17 +28,28 @@ runs:
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ inputs.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
- name: Build wheels
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561
with:
toolchain: stable
- name: Install maturin
shell: bash
run: python -m pip install --upgrade "maturin==${{ inputs.maturin-version }}"
- name: Build wheels with maturin
shell: bash
run: |
uvx --with 'cibuildwheel>=2.16.0,<4.0.0' cibuildwheel --output-dir wheelhouse
env:
CIBW_ARCHS: ${{ inputs.cibw-arch }}
CIBW_SKIP: 'pp*'
set -eu
args=(
--release
--out "${{ inputs.wheelhouse }}"
--manifest-path rust/cuprum-rust/Cargo.toml
)
if [ -n "${{ inputs.target }}" ]; then
args+=(--target "${{ inputs.target }}")
fi
maturin build "${args[@]}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Upload wheel artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ inputs.artifact-name }}
path: wheelhouse/*.whl
path: ${{ inputs.wheelhouse }}/*.whl
221 changes: 206 additions & 15 deletions .github/workflows/build-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,226 @@ on:
type: string
required: true

env:
MATURIN_VERSION: "1.6.0"

Comment thread
leynos marked this conversation as resolved.
jobs:
build:
build-pure-wheel:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: ./.github/actions/pure-python-wheel
with:
python-version: ${{ inputs['python-version'] }}
artifact-name: wheels-pure

build-native-wheels:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
arch: x86_64
cibw_arch: x86_64
target: x86_64-unknown-linux-gnu
manylinux: 2_28
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# aarch64 wheels are built via QEMU emulation under maturin-action.
- os: ubuntu-latest
arch: aarch64
cibw_arch: aarch64
- os: windows-latest
target: aarch64-unknown-linux-gnu
manylinux: 2_28
- os: macos-15-intel
arch: x86_64
cibw_arch: AMD64
- os: windows-latest
arch: aarch64
cibw_arch: ARM64
target: x86_64-apple-darwin
- os: macos-latest
arch: arm64
target: aarch64-apple-darwin
- os: windows-2022
arch: x86_64
cibw_arch: x86_64
- os: macos-latest
arch: aarch64
cibw_arch: arm64
target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- name: Compute Python tag for manylinux interpreter path
if: startsWith(matrix.os, 'ubuntu')
id: pytag
shell: bash
run: |
# Convert "3.13" or "3.13.1" to "cp313" format for manylinux /opt/python paths
# Extract major.minor only, then remove the dot
version='${{ inputs.python-version }}'
major_minor="$(echo "${version}" | cut -d. -f1,2)"
pytag="cp$(echo "${major_minor}" | tr -d '.')"
echo "tag=${pytag}" >> "$GITHUB_OUTPUT"
echo "interpreter=/opt/python/${pytag}-${pytag}/bin/python" >> "$GITHUB_OUTPUT"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Build manylinux wheels (Linux x86_64)
# Maturin action builds inside a manylinux container for PyPI tags.
if: startsWith(matrix.os, 'ubuntu') && matrix.arch == 'x86_64'
uses: messense/maturin-action@47fbb7acd3db01f7830330b50689bd15135c12b9
with:
maturin-version: ${{ env.MATURIN_VERSION }}
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux }}
command: build
args: >-
--release --out wheelhouse --manifest-path rust/cuprum-rust/Cargo.toml
-i ${{ steps.pytag.outputs.interpreter }}
- name: Set up QEMU (Linux aarch64)
if: startsWith(matrix.os, 'ubuntu') && matrix.arch == 'aarch64'
uses: docker/setup-qemu-action@45136fd328ae20ed788d00d3bada68ab496c214c
with:
platforms: arm64
- name: Build manylinux wheels (Linux aarch64)
# aarch64 wheels are cross-compiled; use version-only interpreter spec
# since the container's Python binaries are aarch64 and cannot execute.
if: startsWith(matrix.os, 'ubuntu') && matrix.arch == 'aarch64'
uses: messense/maturin-action@47fbb7acd3db01f7830330b50689bd15135c12b9
with:
maturin-version: ${{ env.MATURIN_VERSION }}
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux }}
container: ghcr.io/rust-cross/manylinux_2_28-cross:aarch64
Comment thread
leynos marked this conversation as resolved.
command: build
args: >-
--release --out wheelhouse --manifest-path rust/cuprum-rust/Cargo.toml
-i ${{ inputs.python-version }}
- name: Build native wheels (macOS/Windows)
if: "!startsWith(matrix.os, 'ubuntu')"
uses: ./.github/actions/build-wheels
with:
python-version: ${{ inputs['python-version'] }}
target: ${{ matrix.target }}
artifact-name: wheels-native-${{ matrix.os }}-${{ matrix.arch }}
wheelhouse: wheelhouse
maturin-version: ${{ env.MATURIN_VERSION }}
- name: Upload wheel artifact (Linux)
if: startsWith(matrix.os, 'ubuntu')
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: wheels-native-${{ matrix.os }}-${{ matrix.arch }}
path: wheelhouse/*.whl

verify-wheel-install:
runs-on: ubuntu-latest
needs:
- build-pure-wheel
- build-native-wheels
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
- uses: ./.github/actions/build-wheels
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ inputs['python-version'] }}
cibw-arch: ${{ matrix.cibw_arch }}
artifact-name: wheels-${{ matrix.os }}-${{ matrix.arch }}
- name: Download wheel artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: dist
- name: Install pure Python wheel, then native wheel
shell: bash
env:
NATIVE_WHEEL_DIR_PATTERN: dist/wheels-native-*
run: |
set -euo pipefail
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip

# Expand pure wheel glob and verify exactly one match
shopt -s nullglob
PURE_WHEEL_GLOB=(dist/wheels-pure/*.whl)
if [ "${#PURE_WHEEL_GLOB[@]}" -ne 1 ]; then
echo "Expected exactly one pure wheel." >&2
echo "Matched: ${PURE_WHEEL_GLOB[*]:-<none>}" >&2
ls -al dist/wheels-pure >&2 || true
exit 1
fi
PURE_WHEEL=$(realpath "${PURE_WHEEL_GLOB[0]}")
python -m pip install "${PURE_WHEEL}"

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Create temp dir and run Python checks there to avoid importing local repo
VERIFY_DIR=$(mktemp -d)
(
cd "${VERIFY_DIR}"
python - <<'PY'
import cuprum as c
assert c.is_rust_available() is False
PY
python - <<'PY'
import importlib.metadata as im
import json

meta = im.metadata("cuprum")
snapshot = {
"name": meta.get("Name"),
"version": meta.get("Version"),
"requires_python": meta.get("Requires-Python"),
"requires_dist": sorted(meta.get_all("Requires-Dist") or []),
"classifiers": sorted(meta.get_all("Classifier") or []),
}
with open("/tmp/cuprum-metadata.json", "w", encoding="utf-8") as handle:
json.dump(snapshot, handle, indent=2, sort_keys=True)
PY
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

PYTAG=$(python - <<'PY'
import sys
print(f"cp{sys.version_info.major}{sys.version_info.minor}")
PY
)
ARCH=$(uname -m)
# Note: NATIVE_WHEEL_DIR_PATTERN must NOT be quoted to allow glob expansion
NATIVE_WHEEL_GLOB=(
${NATIVE_WHEEL_DIR_PATTERN}/*${PYTAG}*manylinux*${ARCH}*.whl
)
if [ "${#NATIVE_WHEEL_GLOB[@]}" -ne 1 ]; then
echo "Expected exactly one native wheel for ${ARCH}." >&2
echo "Matched: ${NATIVE_WHEEL_GLOB[*]:-<none>}" >&2
ls -al dist >&2
exit 1
fi

NATIVE_WHEEL=$(realpath "${NATIVE_WHEEL_GLOB[0]}")
echo "=== Inspecting native wheel contents ==="
unzip -l "${NATIVE_WHEEL}" | grep -E "_rust_backend|\.so"
python -m pip install --force-reinstall "${NATIVE_WHEEL}"

# Run native wheel checks in temp dir to avoid importing local repo
(
cd "${VERIFY_DIR}"
echo "=== Listing installed cuprum package ==="
python -c "import cuprum; import os; pkg_dir = os.path.dirname(cuprum.__file__); print(f'Package at: {pkg_dir}'); import subprocess; subprocess.run(['ls', '-la', pkg_dir])"
echo "=== Testing Rust extension import ==="
python - <<'PY'
import cuprum as c
import cuprum._rust_backend_native as native

print(f"Native module loaded: {native}")
print(f"native.is_available() = {native.is_available()}")
print(f"c.is_rust_available() = {c.is_rust_available()}")

assert native.is_available() is True, f"native.is_available() returned {native.is_available()}"
assert c.is_rust_available() is True, f"c.is_rust_available() returned {c.is_rust_available()}"
PY
Comment thread
coderabbitai[bot] marked this conversation as resolved.
python - <<'PY'
import importlib.metadata as im
import json

with open("/tmp/cuprum-metadata.json", "r", encoding="utf-8") as handle:
baseline = json.load(handle)

meta = im.metadata("cuprum")
current = {
"name": meta.get("Name"),
"version": meta.get("Version"),
"requires_python": meta.get("Requires-Python"),
"requires_dist": sorted(meta.get_all("Requires-Dist") or []),
"classifiers": sorted(meta.get_all("Classifier") or []),
}

if baseline != current:
raise SystemExit(
"Metadata mismatch between pure and native wheels:\n"
f"pure={baseline}\n"
f"native={current}"
)
PY
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,8 @@ jobs:
--format "cobertura" \
--metric "line-coverage" \
coverage.xml

build-wheels:
uses: ./.github/workflows/build-wheels.yml
with:
python-version: '3.13'
43 changes: 20 additions & 23 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,39 @@ concurrency:

permissions:
contents: write
id-token: write


jobs:
build-wheels:
uses: ./.github/workflows/build-wheels.yml
with:
python-version: '3.13'

pure-wheel:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
fetch-depth: 0
- uses: ./.github/actions/pure-python-wheel
with:
python-version: '3.13'
artifact-name: wheels-pure

release:
# This project has no C or Rust extensions, so cross-platform
# builds are unnecessary. Only the pure Python wheel is published.
publish:
needs:
- pure-wheel
- build-wheels
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
with:
fetch-depth: 0
- uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
generate_release_notes: true
python-version: '3.13'
- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: dist
- name: Upload wheels to release
- name: Collect wheel artifacts
run: |
set -eu
mkdir -p dist/publish
find dist -type f -name "*.whl" -print0 | \
xargs -0 -r -I {} cp {} dist/publish/
- name: Publish to PyPI with uv
run: |
set -eu
find dist/wheels-* -type f -name "*.whl" -print0 | \
xargs -0 -r gh release upload "${{ github.ref_name }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uv publish dist/publish/*.whl
2 changes: 2 additions & 0 deletions cuprum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from cuprum.events import ExecEvent
from cuprum.logging_hooks import LoggingHookRegistration, logging_hook
from cuprum.program import Program
from cuprum.rust import is_rust_available
from cuprum.sh import (
CommandResult,
ExecutionContext,
Expand Down Expand Up @@ -103,6 +104,7 @@
"before",
"current_context",
"get_context",
"is_rust_available",
"logging_hook",
"observe",
"run_concurrent",
Expand Down
Loading