From 62b1b66f546f6ebbb23d2af6f56cd7ced36a2511 Mon Sep 17 00:00:00 2001 From: gudoshnikovn Date: Wed, 14 Oct 2026 08:29:07 +1000 Subject: [PATCH] build: one abi3 wheel per platform, a wheel per free-threaded CPython cffi compiles the module against the limited API, and abi3audit finds nothing beyond 3.10's; the wheel was nevertheless tagged for one CPython, so a platform needed a wheel per version. setup.py now tags a wheel built under the GIL cp310-abi3; cibuildwheel builds it on 3.10 and tests it on each later CPython, and the hash tables of wheels.yml run it under every one of them. Free-threaded CPython has no stable ABI and keeps a wheel per version. --- .github/workflows/wheels.yml | 42 +++++++++++++++++++++++------------- CHANGELOG.md | 3 ++- CLAUDE.md | 2 +- docs/guide/install.md | 4 +++- setup.py | 14 +++++++++++- 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 26ce5ac..569b6b6 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,17 +1,20 @@ name: Wheels # Every wheel a release has, built and tested by cibuildwheel (configuration in -# pyproject.toml, which also says which platforms get wheels). The code itself is -# tested on every platform by ci.yml; what only a wheel build exercises is the build: -# the manylinux images, the free-threaded builds, the repair of each wheel and the -# cibuildwheel configuration. Beyond each wheel's test suite, every wheel writes the -# hashes of every fixture, and the tables of all platforms must be the same byte for -# byte; and libphash configured without libjpeg-turbo, decoding JPEG with stb_image, -# passes the suite with its own golden hashes. Every wheel and the sdist pass -# tools/audit.py (exports, dependencies, licenses, SBOM, contents) and twine's checks; -# the sdist is built twice and must be the same bytes, and a wheel is built from it -# without an index. So the workflow runs when a file the build reads changes, by hand, -# and from release.yml, which publishes the artifacts it uploads. +# pyproject.toml, which also says which platforms get wheels): per platform one abi3 +# wheel, built on the oldest CPython and tested on each later one, and a wheel per +# free-threaded CPython. The code itself is tested on every platform by ci.yml; what +# only a wheel build exercises is the build: the manylinux images, the free-threaded +# builds, the repair of each wheel and the cibuildwheel configuration. Beyond each +# wheel's test suite, every wheel writes the hashes of every fixture under each Python +# it is for, and the tables of all platforms must be the same byte for byte; and +# libphash configured without libjpeg-turbo, decoding JPEG with stb_image, passes the +# suite with its own golden hashes. Every wheel and the sdist pass tools/audit.py +# (exports, dependencies, licenses, SBOM, contents) and twine's checks, and abi3audit +# holds the abi3 wheel to the limited API of 3.10; the sdist is built twice and must be +# the same bytes, and a wheel is built from it without an index. So the workflow runs +# when a file the build reads changes, by hand, and from release.yml, which publishes +# the artifacts it uploads. on: push: branches: [ main, 'release/**' ] @@ -79,6 +82,7 @@ jobs: shell: bash run: | uv run --no-project --with lief --with 'setuptools>=77' python tools/audit.py wheelhouse/*.whl + uvx abi3audit --strict --verbose wheelhouse/*-abi3-*.whl uvx twine check --strict wheelhouse/*.whl uvx check-wheel-contents wheelhouse/*.whl @@ -88,9 +92,10 @@ jobs: path: wheelhouse/*.whl if-no-files-found: error - # Each wheel with the Python it is for, installed by uv on the runner itself: every + # Each wheel with each Python it is for, installed by uv on the runner itself: every # wheel is for the runner's own architecture, and a manylinux wheel runs on Ubuntu. - # The ABI tag names the Python (cp312 -> 3.12, cp314t -> 3.14t). + # The abi3 wheel serves every CPython with the GIL from 3.10 on; a free-threaded + # wheel's ABI tag names its Python (cp314t -> 3.14t). - name: Hash tables of the wheels shell: bash run: | @@ -98,8 +103,15 @@ jobs: for wheel in wheelhouse/*.whl; do name=$(basename "$wheel" .whl) abi=$(echo "$name" | cut -d- -f4) - uv run --no-project --python "${abi:2:1}.${abi:3}" --with "$wheel" \ - python tools/hash_table.py native/libphash/tests/data > "tables/$name.txt" + if [ "$abi" = abi3 ]; then + pythons="3.10 3.11 3.12 3.13 3.14 3.15" + else + pythons="${abi:2:1}.${abi:3}" + fi + for python in $pythons; do + uv run --no-project --python "$python" --with "$wheel" \ + python tools/hash_table.py native/libphash/tests/data > "tables/$name-$python.txt" + done done - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5184006..bcdca35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,7 +112,8 @@ read [`MIGRATION.md`](https://github.com/gudoshnikovn/python-libphash/blob/main/ - Releases are published with PEP 740 attestations on PyPI and build provenance on the GitHub Release (`SECURITY.md`, "Verifying a release"); the sdist is reproducible. - Wheels for Linux arm64 and Windows x86-64, and for the free-threaded builds of - CPython 3.14 and later. Paths with any characters, Cyrillic, CJK and emoji included, load on Windows. + CPython 3.14 and later. A wheel per platform for CPython with the GIL, tagged `abi3`: + it installs on every CPython from 3.10 on, a later one included. Paths with any characters, Cyrillic, CJK and emoji included, load on Windows. ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index fda4eb8..980ffb2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,7 @@ How the extension is built: - **`MANIFEST.in`** — the sdist carries only what libphash's CMake build reads (`CMakeLists.txt`, `cmake/`, `include/`, `src/`, `vendor/`, licenses), enough to build offline; not the tests, which need libphash's fixtures. - **Licenses** — `license` in `pyproject.toml` is the SPDX expression of everything the wheel links, and `license-files` carries each library's license into the wheel. -Wheels are built for the platforms libphash's own CI supports: Linux x86-64 and arm64 (manylinux), macOS arm64, Windows x86-64 (`[tool.cibuildwheel]` in `pyproject.toml`). Elsewhere pip builds the sdist; `tools/native.py` still honours `ARCHFLAGS` on macOS and the Python's architecture on Windows. Platforms are verified in CI on native runners, never under emulation. +Wheels are built for the platforms libphash's own CI supports: Linux x86-64 and arm64 (manylinux), macOS arm64, Windows x86-64 (`[tool.cibuildwheel]` in `pyproject.toml`). The module uses only the limited API (cffi), so `setup.py` tags a wheel built under the GIL `cp310-abi3`: cibuildwheel builds it on 3.10 and tests it on each later CPython; each free-threaded CPython, which has no stable ABI, gets its own wheel. Elsewhere pip builds the sdist; `tools/native.py` still honours `ARCHFLAGS` on macOS and the Python's architecture on Windows. Platforms are verified in CI on native runners, never under emulation. ## Architecture diff --git a/docs/guide/install.md b/docs/guide/install.md index 7d8ec4e..8335da6 100644 --- a/docs/guide/install.md +++ b/docs/guide/install.md @@ -15,7 +15,9 @@ Wheels exist for the platforms libphash's own CI builds and tests: | macOS arm64 (11 or later) | `macosx_11_0_arm64` | | Windows x86-64 | `win_amd64` | -for every CPython from 3.10 on, and for the free-threaded builds from 3.14t on. Each +for every CPython from 3.10 on, and for the free-threaded builds from 3.14t on: one +`abi3` wheel per platform serves every CPython with the GIL, a later release of Python +included, and each free-threaded version has a wheel of its own. Each wheel links libphash and its decoders statically (libjpeg-turbo, libpng, libwebp, zlib-ng, stb_image), and needs only the system's C library: no system libpng or libjpeg is used, and the decoders inside cannot clash with another copy of the same libraries in diff --git a/setup.py b/setup.py index 0d33937..189a28c 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ """Build the CFFI extension, with libphash built by its own CMake project first.""" import sys +import sysconfig from pathlib import Path from setuptools import setup @@ -35,7 +36,18 @@ def run(self) -> None: class BdistWheel(bdist_wheel): - """Add the SBOM of the statically linked libraries to the wheel (PEP 770).""" + """Tag the wheel abi3 and add the SBOM of the linked libraries to it (PEP 770). + + cffi compiles the module against the limited API wherever the interpreter has one, + so one wheel built on 3.10 serves every later CPython with the GIL. A free-threaded + interpreter has no stable ABI: its wheel keeps the tag of its version (cp314t). + """ + + def finalize_options(self) -> None: + """Ask for the abi3 tag, from the oldest supported Python, under the GIL.""" + if not sysconfig.get_config_var("Py_GIL_DISABLED"): + self.py_limited_api = "cp310" + super().finalize_options() def egg2dist(self, egginfo_path: str, distinfo_path: str) -> None: """Write the .dist-info directory, then its sboms/ entry."""