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
11 changes: 11 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ build --incompatible_default_to_explicit_init_py
build --cxxopt=-std=c++20
build --host_cxxopt=-std=c++20

# =============================================================================
# Python Stable ABI (nanobind)
# =============================================================================
# Build the nanobind extension against CPython's stable ABI (abi3), floored at
# 3.12. A single `cp312-abi3` extension then runs on every CPython >= 3.12, so
# dev/test builds here exercise the exact artifact shipped in the main wheel.
# Each wheel target re-pins this via its Starlark transition, so the artifact is
# correct even in a host workspace whose .bazelrc lacks this flag, and so
# //python:fastslide_wheel_cp311 can opt back out for the 3.11 build.
build --@nanobind_bazel//:py-limited-api=cp312

# =============================================================================
# Release Config (opt + strip)
# =============================================================================
Expand Down
71 changes: 46 additions & 25 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -351,25 +351,45 @@ jobs:
# setup args from pyproject.toml, so every dependency (aifocore/simpletiff
# via git wraps, codecs via WrapDB) is built from source and linked
# statically into the self-contained _fastslide.pyd. No DLLs to bundle.
- name: Build wheels (${{ matrix.platform }})
# Two cibuildwheel invocations, because the stable-ABI and version-specific
# wheels need different Meson setup args and cibuildwheel applies
# config-settings uniformly across one run. Both write to the same
# output-dir. `--vsenv` forces MSVC: the windows-2022/windows-11-arm images
# ship MinGW GCC (C:\mingw64\bin) on PATH and Meson's auto-detection picks
# that `cc` over MSVC, producing a GCC build (which also hits the
# MinGW-only jpeglib.h/<cstdio> FILE error). It makes Meson activate the
# Visual Studio environment even when another compiler is already on PATH,
# matching the MSVC toolchain used by the Java native build.
- name: Build cp311 wheel (${{ matrix.platform }})
if: steps.gate.outputs.run == 'true' && matrix.method == 'meson'
uses: pypa/cibuildwheel@v3.2.0
with:
package-dir: .
output-dir: artifacts/wheels
env:
# cp310 has no official Windows ARM64 CPython; cibuildwheel skips it
# there automatically, so the same build set is safe for both arches.
CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-* cp314-*"
# 3.11 predates nanobind's stable-ABI floor, so this is a conventional
# version-specific build. `python.allow_limited_api` defaults to false
# in meson.build, which is what keeps meson-python from trying to tag
# the result abi3 (it errors out rather than mislabel the wheel).
CIBW_BUILD: "cp311-*"
CIBW_ARCHS_WINDOWS: ${{ matrix.cibw_arch }}
CIBW_BUILD_VERBOSITY: "1"
# Force MSVC. The windows-2022/windows-11-arm images ship MinGW GCC
# (C:\mingw64\bin) on PATH, and Meson's auto-detection picks that `cc`
# over MSVC, producing a GCC build (which also hits the MinGW-only
# jpeglib.h/<cstdio> FILE error). `--vsenv` makes Meson activate the
# Visual Studio environment even when another compiler is already on
# PATH, matching the MSVC toolchain used by the Java native build.
CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--vsenv"
- name: Build cp312-abi3 wheel (${{ matrix.platform }})
if: steps.gate.outputs.run == 'true' && matrix.method == 'meson'
uses: pypa/cibuildwheel@v3.2.0
with:
package-dir: .
output-dir: artifacts/wheels
env:
# Opting in to the limited API here is what makes meson-python stamp
# the wheel `cp312-abi3` instead of `cp312-cp312`; the pyproject.toml
# `limited-api = true` key alone is inert while the Meson option is
# off. The result installs on every CPython >= 3.12.
CIBW_BUILD: "cp312-*"
CIBW_ARCHS_WINDOWS: ${{ matrix.cibw_arch }}
CIBW_BUILD_VERBOSITY: "1"
CIBW_CONFIG_SETTINGS_WINDOWS: "setup-args=--vsenv setup-args=-Dpython.allow_limited_api=true"
- name: Upload wheels
if: steps.gate.outputs.run == 'true'
uses: actions/upload-artifact@v6
Expand Down Expand Up @@ -586,8 +606,8 @@ jobs:
# shell: bash
# run: |
# set -euo pipefail
# uv python install 3.11
# uv venv --python 3.11 .venv
# uv python install 3.12
# uv venv --python 3.12 .venv
# uv pip install --python .venv/bin/python --no-binary fastslide dist/*.tar.gz
# .venv/bin/python tools/smoke_test_python.py
smoke-wheels:
Expand All @@ -597,7 +617,10 @@ jobs:
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
# Two wheels per platform: a cp311 one, and a cp312-abi3 one imported on
# every CPython >= 3.12 to prove the stable-ABI tag really does load
# across versions. uv resolves each interpreter to its matching wheel.
python: ["3.11", "3.12", "3.13", "3.14"]
target:
- platform: linux_x86_64
runner: ubuntu-24.04
Expand All @@ -614,12 +637,6 @@ jobs:
runner: windows-2022
- platform: windows_arm64
runner: windows-11-arm
# No official CPython 3.10 for Windows ARM64, so no cp310 win_arm64 wheel
# is built; drop that smoke combination.
exclude:
- python: "3.10"
target:
platform: windows_arm64
steps:
- name: Checkout
uses: actions/checkout@v5
Expand Down Expand Up @@ -650,11 +667,14 @@ jobs:
# Build a venv on the right interpreter (x86_64 CPython for the Rosetta
# leg), install the local wheel under test (uv picks the dist wheel whose
# tags match the venv's Python/arch), and run the import + open-sample
# smoke. --find-links dist makes the local wheel the source for fastslide,
# while the index supplies its runtime dep (numpy>=1.20), which is NOT
# vendored in dist. Do NOT add --no-index here: that made the install fail
# to resolve numpy on a cold uv cache (it only "worked" elsewhere by
# accident, off a warm cache that already had numpy).
# smoke.
#
# numpy (the only runtime dep, and NOT vendored in dist) is installed from
# the index in a separate step so that fastslide itself can be installed
# with --no-index. That pin matters: pip/uv prefer an exact cpXY tag over
# an abi3 one, so with the index enabled a same-version wheel already on
# PyPI would outrank the local cp312-abi3 wheel on 3.13/3.14 and the job
# would silently smoke-test the published artifact instead of this build.
- name: Import + open-sample smoke (${{ matrix.target.platform }} py${{ matrix.python }})
if: steps.gate.outputs.run == 'true'
shell: bash
Expand All @@ -671,7 +691,8 @@ jobs:
else
py=".venv/bin/python"
fi
uv pip install --python "$py" --find-links dist fastslide
uv pip install --python "$py" numpy
uv pip install --python "$py" --no-index --find-links dist fastslide
"$py" tools/smoke_test_python.py
publish-pypi:
name: Publish wheels + sdist to PyPI
Expand Down
18 changes: 9 additions & 9 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ local_path_override.

module(
name = "fastslide",
version = "0.8.0",
version = "0.8.1",
)

bazel_dep(name = "rules_cc", version = "0.2.17")
Expand Down Expand Up @@ -140,29 +140,29 @@ bazel_dep(name = "aspect_rules_js", version = "2.9.2", dev_dependency = True)
bazel_dep(name = "buildifier_prebuilt", version = "8.0.3", dev_dependency = True)
bazel_dep(name = "toolchains_llvm", version = "1.7.0", dev_dependency = True)

# The Python bindings ship two wheels: a stable-ABI (abi3) wheel built against
# 3.12 that runs on every CPython >= 3.12, plus a version-specific cp311 wheel
# for the one release still predating the stable-ABI floor. 3.12 is the default
# toolchain; 3.11 is registered only so the cp311 wheel's transition can resolve.
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
python.toolchain(
is_default = True,
python_version = "3.11",
python_version = "3.12",
)
python.toolchain(python_version = "3.9")
python.toolchain(python_version = "3.10")
python.toolchain(python_version = "3.12")
python.toolchain(python_version = "3.13")
python.toolchain(python_version = "3.14")
python.toolchain(python_version = "3.11")

pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
pip.parse(
enable_implicit_namespace_pkgs = True,
hub_name = "fastslide_pip",
python_version = "3.11",
python_version = "3.12",
requirements_darwin = "//:requirements_darwin.txt",
requirements_lock = "//:requirements_linux.txt",
)
pip.parse(
enable_implicit_namespace_pkgs = True,
hub_name = "fastslide_docs_pip",
python_version = "3.11",
python_version = "3.12",
requirements_darwin = "//docs:requirements_darwin.txt",
requirements_lock = "//docs:requirements_linux.txt",
)
Expand Down
43 changes: 30 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ uv build
self-contained: all codecs are statically linked into the `_fastslide`
extension, so the resulting wheel has no native runtime dependencies.

A source build targets the interpreter running it, producing a `cp3XX` wheel.
That is the right default locally, where a stable-ABI build buys nothing. To
reproduce the redistributable `cp312-abi3` wheel instead, opt in explicitly (on
CPython >= 3.12; 3.11 cannot use the limited API):

```bash
uv build --wheel -Csetup-args=-Dpython.allow_limited_api=true
```

#### Option 3: Build from source with Bazel

FastSlide is a [Bazel module](https://bazel.build/external/module). Builds are
Expand All @@ -125,42 +134,50 @@ bazelisk test //...

##### Building Python wheels with Bazel

Wheels are platform-specific because they bundle the native C++ extension. Each
Python version has its own Bazel target: `//python:fastslide_wheel_cp310` through
`//python:fastslide_wheel_cp314` (Python 3.10–3.14). Unlike the Meson path,
Bazel can also cross-compile wheels for other platforms.
Wheels are platform-specific because they bundle the native C++ extension. Two
are built per platform:

| Target | Tag | Installable on |
| -------------------------------- | ------------ | --------------- |
| `//python:fastslide_wheel` | `cp312-abi3` | CPython >= 3.12 |
| `//python:fastslide_wheel_cp311` | `cp311` | CPython 3.11 |

The first targets CPython's stable ABI, so one build serves every CPython from
3.12 onwards. 3.11 predates that floor and therefore needs a conventional
version-specific build. `//python:all_wheels` builds both. Unlike the Meson
path, Bazel can also cross-compile wheels for other platforms.

**Current platform** — build on the host OS/arch without cross-compilation:

```bash
# Example: Python 3.11 wheel for the machine you are on.
bazelisk build //python:fastslide_wheel_cp311
# Both wheels for the machine you are on.
bazelisk build //python:all_wheels
```

The `.whl` file appears under `bazel-bin/python/`.
The `.whl` files appear under `bazel-bin/python/`.

**Cross-compilation** — build wheels for other platforms using the Zig-backed
hermetic toolchains (`--config=hermetic` in `.bazelrc`):

```bash
# Example: Linux x86_64 wheel for Python 3.11, e.g. from macOS.
# Example: Linux x86_64 wheels, e.g. from macOS.
bazelisk build --config=hermetic --platforms=//platforms:linux_x86_64 \
//python:fastslide_wheel_cp311
//python:all_wheels
```

Supported platform keys: `linux_x86_64`, `linux_arm64`, `darwin_x86_64`,
`darwin_aarch64`, `windows_x86_64`. When building for the host macOS
architecture from macOS, the native toolchain is used instead of hermetic Zig.

**Batch builds** — `tools/build_wheels.py` drives Bazel for multiple platforms
and Python versions and copies wheels into `artifacts/wheels/`:
and copies wheels into `artifacts/wheels/`:

```bash
# All supported platforms and Python versions.
# All supported platforms.
python tools/build_wheels.py

# Subset, e.g. one platform and one Python tag.
python tools/build_wheels.py --platform linux_x86_64 --python cp311
# Subset, e.g. one platform.
python tools/build_wheels.py --platform linux_x86_64

# Continue after individual failures.
python tools/build_wheels.py --keep-going
Expand Down
2 changes: 1 addition & 1 deletion docs/Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ PROJECT_NAME = FastSlide
# could be handy for archiving the generated documentation or if some version
# control system is used.

PROJECT_NUMBER = 0.8.0
PROJECT_NUMBER = 0.8.1
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewers a
# quick idea about the purpose of the project. Keep the description short.
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
html_title = "FastSlide Documentation"
copyright = "2025, AI for Oncology"
author = "AI for Oncology"
release = "0.8.0"
release = "0.8.1"
version = "0.2"

# -- General configuration ---------------------------------------------------
Expand Down
41 changes: 33 additions & 8 deletions docs/source/guides/packages_and_releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,12 @@ so no native path configuration is needed.
Python wheels
-------------

The Python package is distributed as platform wheels (one per
platform/CPython version, cp310--cp314) built with Bazel and **published to
PyPI**, so consumers just::
The Python package is distributed as two wheels per platform. The first is
tagged ``cp312-abi3`` and built against CPython's stable ABI, so it runs
unchanged on every CPython >= 3.12; the second is tagged ``cp311``, a
conventional version-specific build for 3.11, which predates the stable-ABI
floor. Wheels are built with Bazel and **published to PyPI**, so consumers
just::

pip install fastslide

Expand All @@ -87,6 +90,25 @@ built for ``linux``/``darwin`` (x86_64 + aarch64) and ``windows`` (x86_64);
matching the Java side). Build them locally with ``tools/build_wheels.py``
(see below).

How the stable ABI is turned on differs per build system, and on the Meson side
it takes **two** cooperating switches:

- **Bazel** (``linux``/``darwin``): ``//python:fastslide_wheel`` pins
``@nanobind_bazel//:py-limited-api=cp312`` through its Starlark transition,
while ``//python:fastslide_wheel_cp311`` pins it to ``unset``.
- **Meson** (``windows``): ``meson.build``'s ``limited_api`` kwarg controls how
the *extension* is compiled, but meson-python reads the *wheel tag* from the
separate ``[tool.meson-python] limited-api`` key in ``pyproject.toml``. Set
one without the other and you get a limited-API binary shipped under a
``cp3XX-cp3XX`` tag, installable only on the exact version that built it.
Because that key is static, the Meson build option
``python.allow_limited_api`` (which meson-python honours as a one-way
disable) defaults to ``false``, so ordinary source builds stay
version-specific and work on any supported interpreter. Only the release
job's 3.12 leg passes ``-Dpython.allow_limited_api=true``, which is what
actually yields the ``cp312-abi3`` wheel. 3.11 never opts in: nanobind
supports the limited API from 3.12 onwards only.

.. _java-local-release:

Building and testing locally (no GitHub)
Expand Down Expand Up @@ -130,7 +152,7 @@ a faithful preview of the real release.

To build wheels locally (optionally narrowing the platform/Python matrix)::

python3 tools/build_wheels.py --platform darwin_aarch64 --python cp311
python3 tools/build_wheels.py --platform darwin_aarch64
# -> artifacts/wheels/*.whl

``publish_java_artifacts.py`` also attaches any wheels in ``artifacts/wheels``
Expand Down Expand Up @@ -180,12 +202,12 @@ single aggregate **GitHub Release**:
smx["darwin x86_64 (Rosetta) / aarch64"]
swx["windows_x86_64 / windows-2022"]
end
subgraph bw [build-wheels: cp310-cp314 per platform]
subgraph bw [build-wheels: cp312-abi3 + cp311 wheels per platform]
wl["linux x86_64/arm64"]
wm["darwin x86_64/aarch64"]
ww["windows_x86_64"]
end
subgraph sw [smoke-wheels: import + open sample, every platform x cp310-cp314]
subgraph sw [smoke-wheels: import + open sample, every platform x cp311-cp314]
swl["linux x86_64/arm64"]
swm["darwin x86_64 (Rosetta) / aarch64"]
sww["windows_x86_64 / windows-2022"]
Expand Down Expand Up @@ -217,8 +239,11 @@ Why the split:
- **Smoke tests always run on the real target runner** (including native
Windows). The Java smoke needs only a JDK + the JARs; the wheel smoke
(``smoke-wheels``) installs the built wheel into a fresh ``uv`` venv on each
platform x CPython (3.10--3.14), then ``import fastslide`` and opens the
bundled sample (``tools/smoke_test_python.py``). No Bazel -- so both validate
platform x CPython (3.11--3.14), then ``import fastslide`` and opens the
bundled sample (``tools/smoke_test_python.py``). ``uv`` resolves 3.11 to the
cp311 wheel and everything above it to the abi3 one, so importing on every
version proves the stable-ABI tag actually loads. No Bazel -- so both
validate
the exact artifact a consumer would load. PyPI/TestPyPI publishing
``needs:`` the wheel smoke, so broken wheels never reach an index.

Expand Down
Loading
Loading