Skip to content
Merged
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
159 changes: 159 additions & 0 deletions .github/workflows/build-wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
name: Build wheels

# Reusable wheel-build matrix shared by:
# - publish.yml (on release: builds the full matrix, then uploads to PyPI)
# - release-build-check.yml (on PR: build-only smoke of the release path)
# Keeping the build jobs in ONE place means the pre-merge guard can never
# drift from what actually publishes.
on:
workflow_call:
inputs:
linux_only:
description: "Build only the manylinux Linux wheels (PR-guard mode); skip macOS/Windows/sdist."
type: boolean
default: false

permissions:
contents: read

jobs:
# Build wheels on Linux using manylinux containers
build-linux:
name: Build Linux ${{ matrix.arch }} wheels
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container }}
strategy:
matrix:
include:
- arch: x86_64
runner: ubuntu-latest
container: quay.io/pypa/manylinux_2_28_x86_64
artifact: wheels-linux-x86_64
- arch: aarch64
runner: ubuntu-24.04-arm
container: quay.io/pypa/manylinux_2_28_aarch64
artifact: wheels-linux-aarch64
steps:
- uses: actions/checkout@v7

- name: Install system dependencies
run: dnf install -y openssl-devel perl-IPC-Cmd openblas-devel

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install maturin
run: /opt/python/cp312-cp312/bin/pip install maturin

- name: Build wheels
run: |
expected=0
for pyver in 39 310 311 312 313 314; do
pybin="/opt/python/cp${pyver}-cp${pyver}/bin/python"
if [ ! -f "$pybin" ]; then
echo "ERROR: Expected Python interpreter not found: $pybin"
exit 1
fi
/opt/python/cp312-cp312/bin/maturin build --release --out dist -i "$pybin" --features extension-module,openblas
expected=$((expected + 1))
done
actual=$(find dist -maxdepth 1 -name '*.whl' | wc -l)
echo "Built $actual wheels (expected $expected)"
if [ "$actual" -ne "$expected" ]; then
echo "ERROR: Expected $expected wheels but found $actual"
exit 1
fi

- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact }}
path: dist/*.whl

# Build wheels on macOS ARM64 (native build)
# Note: macOS x86_64 skipped - Intel runners retired, users can install from sdist
build-macos-arm:
name: Build macOS ARM64 wheels
if: ${{ !inputs.linux_only }}
runs-on: macos-14
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install maturin
run: pip install maturin

- name: Build wheel
run: maturin build --release --out dist --features extension-module,accelerate

- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-macos-arm64-py${{ matrix.python-version }}
path: dist/*.whl

# Build wheels on Windows
build-windows:
name: Build Windows wheels
if: ${{ !inputs.linux_only }}
runs-on: windows-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install maturin
run: pip install maturin

- name: Build wheel
run: maturin build --release --out dist --features extension-module

- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-windows-py${{ matrix.python-version }}
path: dist/*.whl

# Build source distribution
build-sdist:
name: Build source distribution
if: ${{ !inputs.linux_only }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'

- name: Install maturin
run: pip install maturin

- name: Build sdist
run: maturin sdist --out dist

- name: Upload sdist
uses: actions/upload-artifact@v7
with:
name: sdist
path: dist/*.tar.gz
142 changes: 4 additions & 138 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,148 +8,14 @@ permissions:
contents: read

jobs:
# Build wheels on Linux using manylinux containers
build-linux:
name: Build Linux ${{ matrix.arch }} wheels
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container }}
strategy:
matrix:
include:
- arch: x86_64
runner: ubuntu-latest
container: quay.io/pypa/manylinux_2_28_x86_64
artifact: wheels-linux-x86_64
- arch: aarch64
runner: ubuntu-24.04-arm
container: quay.io/pypa/manylinux_2_28_aarch64
artifact: wheels-linux-aarch64
steps:
- uses: actions/checkout@v7

- name: Install system dependencies
run: dnf install -y openssl-devel perl-IPC-Cmd openblas-devel

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install maturin
run: /opt/python/cp312-cp312/bin/pip install maturin

- name: Build wheels
run: |
expected=0
for pyver in 39 310 311 312 313 314; do
pybin="/opt/python/cp${pyver}-cp${pyver}/bin/python"
if [ ! -f "$pybin" ]; then
echo "ERROR: Expected Python interpreter not found: $pybin"
exit 1
fi
/opt/python/cp312-cp312/bin/maturin build --release --out dist -i "$pybin" --features extension-module,openblas
expected=$((expected + 1))
done
actual=$(ls dist/*.whl 2>/dev/null | wc -l)
echo "Built $actual wheels (expected $expected)"
if [ "$actual" -ne "$expected" ]; then
echo "ERROR: Expected $expected wheels but found $actual"
exit 1
fi

- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact }}
path: dist/*.whl

# Build wheels on macOS ARM64 (native build)
# Note: macOS x86_64 skipped - Intel runners retired, users can install from sdist
build-macos-arm:
name: Build macOS ARM64 wheels
runs-on: macos-14
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install maturin
run: pip install maturin

- name: Build wheel
run: maturin build --release --out dist --features extension-module,accelerate

- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-macos-arm64-py${{ matrix.python-version }}
path: dist/*.whl

# Build wheels on Windows
build-windows:
name: Build Windows wheels
runs-on: windows-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install maturin
run: pip install maturin

- name: Build wheel
run: maturin build --release --out dist --features extension-module

- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-windows-py${{ matrix.python-version }}
path: dist/*.whl

# Build source distribution
build-sdist:
name: Build source distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'

- name: Install maturin
run: pip install maturin

- name: Build sdist
run: maturin sdist --out dist

- name: Upload sdist
uses: actions/upload-artifact@v7
with:
name: sdist
path: dist/*.tar.gz
# Build the full wheel matrix via the shared reusable workflow.
build:
uses: ./.github/workflows/build-wheels.yml

# Publish to PyPI
publish:
name: Publish to PyPI
needs: [build-linux, build-macos-arm, build-windows, build-sdist]
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/release-build-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Release build check

# Pre-merge guard for the PyPI release build path. publish.yml only runs on
# `release: published`, so its manylinux container build (checkout@v7 inside
# glibc-2.28, openblas, py3.9-3.14) is never exercised by PR CI. This calls the
# SAME reusable workflow build-only (no PyPI upload) so a PR that would break our
# ability to mint a release fails here instead of at release time.
#
# On PRs it builds only the manylinux leg (the gap rust-test.yml doesn't cover);
# `workflow_dispatch` runs the full matrix as a manual pre-release rehearsal.
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled, unlabeled]
paths:
- 'rust/**'
- 'pyproject.toml'
- '.github/workflows/build-wheels.yml'
- '.github/workflows/publish.yml'
- '.github/workflows/release-build-check.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
release-build:
# Skip unrelated label churn: a non-ready-for-ci label add/remove won't run this job.
if: >-
github.event_name != 'pull_request'
|| (contains(github.event.pull_request.labels.*.name, 'ready-for-ci')
&& (github.event.action != 'labeled' && github.event.action != 'unlabeled'
|| github.event.label.name == 'ready-for-ci'))
uses: ./.github/workflows/build-wheels.yml
with:
linux_only: ${{ github.event_name == 'pull_request' }}
2 changes: 2 additions & 0 deletions .github/workflows/rust-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ on:
- '.github/workflows/docs-tests.yml'
- '.github/workflows/notebooks.yml'
- '.github/workflows/ci-gate.yml'
- '.github/workflows/release-build-check.yml'
# The AI-review surfaces below are tested by
# tests/test_openai_review.py (TestWorkflowPromptHardening,
# TestAdaptReviewCriteria, etc.). Without these path filters, a
Expand Down Expand Up @@ -47,6 +48,7 @@ on:
- '.github/workflows/docs-tests.yml'
- '.github/workflows/notebooks.yml'
- '.github/workflows/ci-gate.yml'
- '.github/workflows/release-build-check.yml'
- '.github/workflows/ai_pr_review.yml'
- '.github/codex/prompts/pr_review.md'
- '.claude/scripts/openai_review.py'
Expand Down
20 changes: 10 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`maturin develop --features accelerate` against the pinned `ndarray 0.17`, the Rust unit
tests, and the full Python⇄Rust equivalence suite (`tests/test_rust_backend.py`).

### Security
- **Bumped the Rust backend's `pyo3` and `numpy` crates 0.28 → 0.29.** Resolves two RustSec
advisories in `pyo3 < 0.29` — RUSTSEC-2026-0176 (out-of-bounds read in `PyList`/`PyTuple`
`nth`/`nth_back`, High) and RUSTSEC-2026-0177 (missing `Sync` bound on
`PyCFunction::new_closure`, Medium). Neither vulnerable path was reachable in this crate
(no `PyList`/`PyTuple` iteration, no `new_closure`, no free-threaded wheels); `numpy` 0.29 is
bumped in lockstep because it requires `pyo3` ^0.29. No API or numerical change — both crates
are FFI/binding layers, and the math/RNG crates (`ndarray`, `faer`, `rand`, `rand_xoshiro`)
are unchanged.

## [3.5.3] - 2026-06-25

### Added
Expand All @@ -43,16 +53,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`treatment_fraction` remains inert (balanced 2×2×2); pass `group_frac`/`partition_frac`
via `data_generator_kwargs`. See `docs/methodology/REGISTRY.md` §PowerAnalysis.

### Security
- **Bumped the Rust backend's `pyo3` and `numpy` crates 0.28 → 0.29.** Resolves two RustSec
advisories in `pyo3 < 0.29` — RUSTSEC-2026-0176 (out-of-bounds read in `PyList`/`PyTuple`
`nth`/`nth_back`, High) and RUSTSEC-2026-0177 (missing `Sync` bound on
`PyCFunction::new_closure`, Medium). Neither vulnerable path was reachable in this crate
(no `PyList`/`PyTuple` iteration, no `new_closure`, no free-threaded wheels); `numpy` 0.29 is
bumped in lockstep because it requires `pyo3` ^0.29. No API or numerical change — both crates
are FFI/binding layers, and the math/RNG crates (`ndarray`, `faer`, `rand`, `rand_xoshiro`)
are unchanged.

## [3.5.2] - 2026-06-08

### Added
Expand Down
Loading
Loading