Skip to content

Commit 872ae39

Browse files
FrancescAltedclaude
andcommitted
Build a single abi3 wheel per platform instead of one per version
Set wheel.py-api = "cp311" so scikit-build-core tags the wheel cp311-abi3, and turn the SKBUILD_SABI_* variables it exports into USE_SABI on each extension target. USE_SABI defines Py_LIMITED_API, which is what flips Cython into CYTHON_LIMITED_API mode, and together with WITH_SOABI names the modules <name>.abi3.so. A plain cmake invocation sets neither variable, so the version-specific behaviour is unchanged outside of a wheel build. No .pyx changes were needed. The cdef classes (SChunk, vlmeta) build as PyType_FromSpec heap types, the legacy `np.ndarray[dtype, ndim=N]` buffer syntax in indexing_ext still compiles, and utf8_ext's NpyString usage is limited-API clean, all with Cython 3.2.9 and NumPy 2.5. Measured on macOS arm64, 4 interleaved runs, min-of-7 per measurement: worst case 1.013x (compress2 of 8 KB buffers, i.e. call-overhead bound), everything else within noise. SChunk attribute access -- the most direct probe of the heap-type path -- came out at 0.987x. Full suite passes on the build interpreter (3.14) and, using that same wheel, on 3.11/3.12/3.13. Free-threaded builds keep their own version-specific wheels: scikit-build-core ignores py-api when Py_GIL_DISABLED is set, so cp314t/cp315t need no extra configuration. Collapsing those into one wheel needs abi3t (PEP 803), which starts at 3.15 and requires a PEP 793 module export hook that Cython cannot emit yet. So this is 7 wheels per platform down to 3, not 2. Since the abi3 wheel is built only on 3.11 and cibuildwheel tests a wheel just on the interpreter that produced it, add test_abi3_matrix: it installs the single wheel on 3.11-3.15 across all three OSes, asserts the .abi3. module is the one loaded, and runs a slice of the suite. That is also the job that would catch a future CPython breaking the stable ABI -- the risk abi3 takes on in exchange for not rebuilding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e5250de commit 872ae39

3 files changed

Lines changed: 127 additions & 17 deletions

File tree

.github/workflows/cibuildwheels.yml

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,38 +43,50 @@ jobs:
4343
strategy:
4444
fail-fast: false
4545
matrix:
46-
# The `*` after the version list is what lets the free-threaded builds
47-
# through: their identifiers are `cp314t-...`, so a bare `cp314-...`
48-
# pattern silently drops them. cibuildwheel enables free-threading by
49-
# default from 3.14 on, so cp314t/cp315t are selected while cp313t is
50-
# not -- deliberate, as 3.13's free-threaded build was still marked
51-
# experimental (PEP 703 phase I) and carried a large single-thread
52-
# penalty. Opting into it would need CIBW_ENABLE: cpython-freethreading.
46+
# Three builds per platform, not one per Python version:
47+
#
48+
# cp311 -> a single `cp311-abi3` wheel serving CPython 3.11+, via
49+
# `wheel.py-api` in pyproject.toml. Building it on any
50+
# other version would emit the *same* filename, so only
51+
# the floor version is built.
52+
# cp314t -> free-threaded 3.14. Cannot use abi3: the free-threaded
53+
# cp315t ABI is distinct, and its stable variant (abi3t, PEP 803)
54+
# starts at 3.15 and needs a PEP 793 module export hook
55+
# that Cython does not emit yet. These stay
56+
# version-specific until that lands upstream.
57+
#
58+
# cp313t is deliberately absent: 3.13's free-threaded build was still
59+
# experimental (PEP 703 phase I) with a large single-thread penalty.
60+
# cibuildwheel keeps it behind CIBW_ENABLE: cpython-freethreading.
61+
#
62+
# NOTE: cibuildwheel only tests a wheel on the interpreter that built
63+
# it, so the abi3 wheel is exercised on 3.11 alone here. The
64+
# test_abi3_matrix job below installs it on every supported version.
5365
include:
5466
# Linux x86_64 builds
5567
- os: ubuntu-latest
5668
arch: x86_64
57-
cibw_pattern: "cp3{11,12,13,14,15}*-manylinux*"
69+
cibw_pattern: "cp311-manylinux* cp314t-manylinux* cp315t-manylinux*"
5870
artifact_name: "linux-x86_64"
5971

6072
# Linux ARM64 builds (native runners)
6173
- os: ubuntu-24.04-arm
6274
arch: aarch64
63-
cibw_pattern: "cp3{11,12,13,14,15}*-manylinux*"
75+
cibw_pattern: "cp311-manylinux* cp314t-manylinux* cp315t-manylinux*"
6476
artifact_name: "linux-aarch64"
6577
# Don't use native runners for now (looks like wait times are too long)
6678
#runs-on: ["ubuntu-latest", "arm64"]
6779

6880
# Windows builds
6981
- os: windows-latest
7082
arch: x86_64
71-
cibw_pattern: "cp3{11,12,13,14,15}*-win_amd64"
83+
cibw_pattern: "cp311-win_amd64 cp314t-win_amd64 cp315t-win_amd64"
7284
artifact_name: "windows-x86_64"
7385

7486
# macOS builds (universal2)
7587
- os: macos-latest
7688
arch: x86_64
77-
cibw_pattern: "cp3{11,12,13,14,15}*-macosx*"
89+
cibw_pattern: "cp311-macosx* cp314t-macosx* cp315t-macosx*"
7890
artifact_name: "macos-universal2"
7991
steps:
8092
- name: Checkout repo
@@ -195,8 +207,75 @@ jobs:
195207
path: ./wheelhouse/*.whl
196208

197209

210+
# The abi3 wheel is built once, on 3.11, and cibuildwheel only tests a wheel
211+
# on the interpreter that produced it. Every other supported version would
212+
# therefore ship untested, so install the one wheel on each of them and run a
213+
# slice of the suite. This is also the job that would catch a CPython release
214+
# breaking the stable ABI -- the failure mode abi3 trades away rebuilds for.
215+
test_abi3_matrix:
216+
name: Test abi3 wheel on ${{ matrix.python-version }} (${{ matrix.os }})
217+
needs: [ build_wheels ]
218+
runs-on: ${{ matrix.os }}
219+
strategy:
220+
fail-fast: false
221+
matrix:
222+
# Both are real matrix dimensions so they cross-product (15 short jobs).
223+
# The `include` entries key on `os`, which is an existing dimension, so
224+
# they attach artifact_name to the matching combinations rather than
225+
# being appended as extra jobs.
226+
os: [ubuntu-latest, windows-latest, macos-latest]
227+
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
228+
include:
229+
- os: ubuntu-latest
230+
artifact_name: "linux-x86_64"
231+
- os: windows-latest
232+
artifact_name: "windows-x86_64"
233+
- os: macos-latest
234+
artifact_name: "macos-universal2"
235+
steps:
236+
- uses: actions/checkout@v7
237+
238+
- uses: actions/setup-python@v7
239+
with:
240+
python-version: ${{ matrix.python-version }}
241+
# 3.15 is still a pre-release until October
242+
allow-prereleases: true
243+
244+
- uses: actions/download-artifact@v8
245+
with:
246+
name: ${{ matrix.artifact_name }}
247+
path: ./wheelhouse
248+
249+
# Pin the exact version so pip resolves blosc2 from ./wheelhouse (picking
250+
# the arch-compatible file) while still pulling dependencies from PyPI.
251+
- name: Install the abi3 wheel
252+
shell: bash
253+
run: |
254+
WHL=$(ls wheelhouse/blosc2-*abi3*.whl | head -1)
255+
echo "Installing $WHL on $(python -V)"
256+
python -m pip install --upgrade pip
257+
python -m pip install --find-links ./wheelhouse "blosc2==$(basename "$WHL" | cut -d- -f2)"
258+
259+
- name: Confirm the abi3 module was the one loaded
260+
shell: bash
261+
run: |
262+
python -c "
263+
import blosc2, blosc2.blosc2_ext as ext
264+
assert '.abi3.' in ext.__file__, f'not an abi3 module: {ext.__file__}'
265+
print('OK:', ext.__file__)
266+
blosc2.print_versions()
267+
"
268+
269+
- name: Run a slice of the test suite
270+
shell: bash
271+
run: |
272+
python -m pip install pytest
273+
python -m pytest tests/test_open.py tests/test_vlmeta.py \
274+
tests/ndarray/test_evaluate.py -q -p no:cacheprovider
275+
276+
198277
upload_pypi:
199-
needs: [ build_wheels, build_wheels_wasm ]
278+
needs: [ build_wheels, build_wheels_wasm, test_abi3_matrix ]
200279
runs-on: ubuntu-latest
201280
# Only upload wheels when tagging (typically a release)
202281
if: startsWith(github.event.ref, 'refs/tags')

CMakeLists.txt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,27 @@ endif()
2727
# does not include a python symbolic link to python3.
2828
# find_package(Python 3.12 COMPONENTS Interpreter NumPy Development.Module REQUIRED)
2929
# IMO, this would need to be solved in Fedora, so we can just use the following line:
30-
find_package(Python COMPONENTS Interpreter NumPy Development.Module REQUIRED)
30+
find_package(Python COMPONENTS Interpreter NumPy Development.Module
31+
${SKBUILD_SABI_COMPONENT} REQUIRED)
32+
33+
# abi3 (Stable ABI) support. scikit-build-core sets SKBUILD_SABI_COMPONENT to
34+
# "Development.SABIModule" and SKBUILD_SABI_VERSION to e.g. "3.11" when
35+
# `wheel.py-api` requests a limited-API build; both are empty otherwise, so a
36+
# plain CMake invocation keeps the version-specific behaviour. Passing
37+
# USE_SABI to Python_add_library defines Py_LIMITED_API for the target (which
38+
# is what flips Cython into CYTHON_LIMITED_API mode) and, together with
39+
# WITH_SOABI, names the module `<name>.abi3.so` instead of
40+
# `<name>.cpython-3XY-<plat>.so`.
41+
#
42+
# Note scikit-build-core silently ignores py-api on free-threaded
43+
# interpreters, so cp3XXt builds fall back to version-specific wheels on their
44+
# own -- abi3t (PEP 803) is a separate ABI that Cython cannot emit yet.
45+
if(SKBUILD_SABI_VERSION)
46+
set(B2_SABI USE_SABI ${SKBUILD_SABI_VERSION})
47+
message(STATUS "Building limited-API (abi3) modules for Python ${SKBUILD_SABI_VERSION}+")
48+
else()
49+
set(B2_SABI "")
50+
endif()
3151

3252
# Add custom command to generate the version file
3353
add_custom_command(
@@ -68,15 +88,15 @@ add_custom_command(
6888
VERBATIM)
6989

7090
# ...and add it to the target
71-
Python_add_library(blosc2_ext MODULE blosc2_ext.c WITH_SOABI)
91+
Python_add_library(blosc2_ext MODULE ${B2_SABI} WITH_SOABI blosc2_ext.c)
7292
target_sources(blosc2_ext PRIVATE src/blosc2/matmul_kernels.c)
7393
target_include_directories(blosc2_ext PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/blosc2)
7494
if(UNIX)
7595
target_link_libraries(blosc2_ext PRIVATE ${CMAKE_DL_LIBS})
7696
endif()
77-
Python_add_library(indexing_ext MODULE indexing_ext.c WITH_SOABI)
78-
Python_add_library(groupby_ext MODULE groupby_ext.c WITH_SOABI)
79-
Python_add_library(utf8_ext MODULE utf8_ext.c WITH_SOABI)
97+
Python_add_library(indexing_ext MODULE ${B2_SABI} WITH_SOABI indexing_ext.c)
98+
Python_add_library(groupby_ext MODULE ${B2_SABI} WITH_SOABI groupby_ext.c)
99+
Python_add_library(utf8_ext MODULE ${B2_SABI} WITH_SOABI utf8_ext.c)
80100
# NpyString_pack() and friends are part of NumPy's 2.0 C API; opt in
81101
# explicitly since numpy/*.h otherwise targets an older API version by
82102
# default for source compatibility.

pyproject.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@ test-command = "python -c \"import blosc2; blosc2.print_versions()\""
119119
manylinux-x86_64-image = "manylinux_2_28"
120120
manylinux-aarch64-image = "manylinux_2_28"
121121

122+
[tool.scikit-build]
123+
# Build a single Stable ABI (abi3) wheel covering CPython 3.11+ instead of one
124+
# wheel per minor version. CMakeLists.txt turns the SKBUILD_SABI_* variables
125+
# this produces into USE_SABI on each extension target.
126+
#
127+
# On free-threaded interpreters scikit-build-core ignores this and falls back to
128+
# a version-specific wheel, because abi3t (PEP 803) is a distinct ABI that
129+
# starts at 3.15 and that Cython cannot emit yet. So cp314t/cp315t keep
130+
# building as normal cp3XXt wheels with no extra configuration.
131+
wheel.py-api = "cp311"
132+
122133
[tool.scikit-build.sdist]
123134
exclude = ["bench*", ".github*"]
124135

0 commit comments

Comments
 (0)