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
42 changes: 27 additions & 15 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
@@ -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/**' ]
Expand Down Expand Up @@ -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

Expand All @@ -88,18 +92,26 @@ 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: |
mkdir tables
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
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion docs/guide/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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."""
Expand Down
Loading