diff --git a/.github/workflows/QA.yaml b/.github/workflows/QA.yaml index f0d3afb8..9269e548 100644 --- a/.github/workflows/QA.yaml +++ b/.github/workflows/QA.yaml @@ -40,3 +40,6 @@ jobs: - name: Check stubs annotations run: inv check-stub-annotations + + - name: Check build deps are in sync + run: inv check-build-deps-sync diff --git a/.github/workflows/Tests.yaml b/.github/workflows/Tests.yaml index fef16bf8..a25cc3a6 100644 --- a/.github/workflows/Tests.yaml +++ b/.github/workflows/Tests.yaml @@ -73,6 +73,28 @@ jobs: - name: Run the tests run: inv coverage --args "-vvv" + build-floor-versions: # validate that the floor of our build deps ranges still works + runs-on: ubuntu-26.04 + + steps: + - uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.11" + + - name: Install floor versions of build dependencies + run: | + pip install -U pip packaging + pip install $(python3 scripts/floor_versions.py) + + - name: Install project against floor versions (no build isolation) + run: pip install -e .[test,scripts] --no-build-isolation + + - name: Run the tests + run: inv coverage --args "-vvv" + build-docs: # validate that doc can be built runs-on: ubuntu-26.04 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf9de29..330fa057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Relax `setuptools`/`wheel`/`cython` build dependency pins to `>=floor,` ranges instead of exact versions, easing downstream packaging; `delocate` stays exactly pinned (#288) + ## [4.0.0] - 2026-10-29 ### Added diff --git a/pyproject.toml b/pyproject.toml index a6b0256b..2c7413a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,17 @@ [build-system] requires = [ - "setuptools == 84.2.1", - "wheel == 0.48.0", - "cython == 3.3.0", + # this list must stay byte-for-byte identical to [project.optional-dependencies].build + # below. PEP 518 requires build-system.requires to be static TOML, parsed before any + # project/build-backend code runs, so it cannot reference the `build` extra (or vice + # versa): the duplication is unavoidable and is checked by + # scripts/check_build_deps_sync.py (run in CI). + # + # floor versions below are read by scripts/floor_versions.py and + # validated by the "build-floor-versions" CI job; when bumping, only move + # the floor if actually required, not on every bump + "setuptools >=84.2.1,<85", + "wheel >=0.48.0,<0.49", + "cython >=3.3.0,<3.4", # https://github.com/pypa/cibuildwheel/blob/v2.22/cibuildwheel/resources/constraints.txt "delocate == 0.13.0 ; platform_system=='Windows'", ] @@ -69,9 +78,11 @@ test = [ "libzim[build]", ] build = [ - "setuptools == 84.2.1", - "wheel == 0.48.0", - "cython == 3.3.0", + # must stay byte-for-byte identical to [build-system].requires above; see the + # comment there + "setuptools >=84.2.1,<85", + "wheel >=0.48.0,<0.49", + "cython >=3.3.0,<3.4", "delocate == 0.13.0 ; platform_system=='Windows'", ] docs = [ diff --git a/scripts/check_build_deps_sync.py b/scripts/check_build_deps_sync.py new file mode 100644 index 00000000..9c1b694b --- /dev/null +++ b/scripts/check_build_deps_sync.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Check that build-system.requires and the `build` extra stay in sync. + +PEP 518 requires build-system.requires to be static TOML, parsed before any +project or build-backend code runs, so it cannot reference +project.optional-dependencies.build (or vice-versa): the two lists must be +duplicated by hand. This script is the drift check for that duplication. +""" + +import sys +import tomllib +from pathlib import Path + + +def main() -> int: + pyproject = tomllib.loads(Path("pyproject.toml").read_text()) + build_system_requires = pyproject["build-system"]["requires"] + build_extra = pyproject["project"]["optional-dependencies"]["build"] + + if build_system_requires == build_extra: + print("build-system.requires and the `build` extra are in sync") # noqa: T201 + return 0 + + print( # noqa: T201 + "error: build-system.requires and the `build` extra have drifted apart" + ) + print(f" build-system.requires = {build_system_requires}") # noqa: T201 + print(f" optional-dependencies.build = {build_extra}") # noqa: T201 + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/floor_versions.py b/scripts/floor_versions.py new file mode 100644 index 00000000..b47fa30f --- /dev/null +++ b/scripts/floor_versions.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Print `name==version` pins for the floor of build-system.requires in pyproject.toml. + +Used by CI to install and test against the lowest versions we claim to +support, without hardcoding those versions a second time in the workflow. +""" + +import tomllib +from pathlib import Path + +from packaging.requirements import Requirement + +pyproject = tomllib.loads(Path("pyproject.toml").read_text()) + +pins = [] +for req in pyproject["build-system"]["requires"]: + requirement = Requirement(req) + if requirement.marker and not requirement.marker.evaluate(): + continue + # exact pins (e.g. delocate) have nothing to float, skip them + floor = next( + (spec.version for spec in requirement.specifier if spec.operator == ">="), + None, + ) + if floor is not None: + pins.append(f"{requirement.name}=={floor}") + +print(" ".join(pins)) # noqa: T201 diff --git a/tasks.py b/tasks.py index e0474fb0..4037dc6a 100644 --- a/tasks.py +++ b/tasks.py @@ -113,12 +113,19 @@ def check_stub_annotations(ctx: Context): ctx.run("python -m scripts.check_stub_annotations", pty=use_pty) +@task +def check_build_deps_sync(ctx: Context): + """check that build-system.requires and the build extra are in sync""" + ctx.run("python -m scripts.check_build_deps_sync", pty=use_pty) + + @task(optional=["args"], help={"args": "check tools (pyright) additional arguments"}) def checkall(ctx: Context, args: str = ""): """check static types and type stubs""" check_pyright(ctx, args) check_stubtest(ctx) check_stub_annotations(ctx) + check_build_deps_sync(ctx) @task(optional=["args"], help={"args": "black additional arguments"})