From f3fb37aae7b7e8c6968b3134fdf439469a5fe7d7 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Thu, 10 Sep 2026 07:44:22 +0200 Subject: [PATCH] pact-python-ffi: add build-pact-python-ffi.yml for riscv64 wheels hatch_build.py's build hook downloads a prebuilt libpact_ffi from pact-foundation/pact-reference's GitHub releases and compiles a CFFI extension against it. pact-reference publishes no riscv64 asset, so CIBW_BEFORE_ALL_LINUX builds libpact_ffi from the pact-reference source at the matching libpact_ffi-v tag and stages it at the path the build hook already caches downloads to. A small patch teaches the hook's platform detection to recognize riscv64. Builds only cp312-abi3 (our registry's floor), matching upstream's own release job which builds a single interpreter and relies on the abi3 tag for forward compatibility; cp313/cp314 reuse that wheel via cibuildwheel's find_compatible_wheel. musllinux is dropped: rustup.rs ships no riscv64 musl toolchain. The first attempt at the platform-detection patch only widened the `manylinux`-prefixed branch of `_lib_url`, which turned out to be unreachable: `packaging` 26.3 (an unpinned build dependency, resolved fresh in the isolated build env) reordered `sys_tags()` to yield the generic `linux_` tag before any `manylinux_*`/`musllinux_*` tag, so `_sys_tag_platform()` returns `linux_riscv64` and none of `_lib_url`'s `startswith("manylinux")` branches ever match - confirmed from CI job 102765101921's log (Rust build finishes in 39m24s, then `python -m build` still raises `UnsupportedPlatformError: Unsupported platform linux_riscv64`) and from packaging's own 26.3 changelog/source (`_linux_platforms` in tags.py yields `linux_{arch}` before `_manylinux.platform_tags(archs)`). The patch now also accepts the generic `linux` prefix in that branch. The CIBW_TEST_COMMAND smoke-check also needed a fix: `pact_ffi/__init__.py` does `from pact_ffi.ffi import ffi, lib`, which - because the submodule and the symbol it imports are both named `ffi` - shadows the `pact_ffi.ffi` package attribute with the imported `ffi` object (a cffi.FFI instance with no `__file__`) once `pact_ffi` finishes initializing. `import pact_ffi.ffi as m` therefore binds `m` to that FFI instance, not the compiled extension module, so `m.__file__` raised AttributeError (job 102778776915, run 34448554573). Go through `sys.modules['pact_ffi.ffi']` instead, which holds the actual compiled module regardless of the attribute shadowing. That fix then surfaced a second, independent test-step failure: upstream's own pyproject.toml unconditionally enables coverage in pytest's addopts (--cov-config, --cov-report, --cov=pact_ffi), which needs the pytest-cov plugin - not installed by CIBW_TEST_REQUIRES: pytest alone (job 102793239322, run 34453121784). Add pytest-cov to CIBW_TEST_REQUIRES. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Az13NcXsVZzzxXmaxxUEy7 --- .github/workflows/build-pact-python-ffi.yml | 140 ++++++++++++++++++ ...nize-riscv64-in-the-hatch-build-hook.patch | 72 +++++++++ 2 files changed, 212 insertions(+) create mode 100644 .github/workflows/build-pact-python-ffi.yml create mode 100644 patches/pact-python-ffi/0.5.4.1/0001-recognize-riscv64-in-the-hatch-build-hook.patch diff --git a/.github/workflows/build-pact-python-ffi.yml b/.github/workflows/build-pact-python-ffi.yml new file mode 100644 index 000000000..28e7e97b6 --- /dev/null +++ b/.github/workflows/build-pact-python-ffi.yml @@ -0,0 +1,140 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `build-wheels` job of +# https://github.com/pact-foundation/pact-python/blob/pact-python-ffi/0.5.4.1/.github/workflows/release-ffi.yml +name: Build pact-python-ffi wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'pact-python-ffi version to build (git tag suffix, e.g. 0.5.4.1)' + required: true + default: '0.5.4.1' + pull_request: + paths: + - '.github/workflows/build-pact-python-ffi.yml' + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || '0.5.4.1' }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + # `inputs.version` is empty on pull_request events; default to 0.5.4.1 there. + PACT_PYTHON_FFI_VERSION: ${{ inputs.version || '0.5.4.1' }} + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + + build_wheels: + needs: [setup] + name: Build pact-python-ffi ${{ inputs.version || '0.5.4.1' }} ${{ matrix.tag }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + include: + # hatch_build.py tags the wheel cp-abi3, same as + # upstream's own release job (CIBW_BUILD: cp310-*); building on our + # registry's floor (cp312) covers cp312/cp313/cp314 through abi3. + # Upstream ships no free-threaded wheel for this package. + - tag: cp312-abi3 + build: >- + cp312-manylinux_riscv64 cp313-manylinux_riscv64 + cp314-manylinux_riscv64 + + steps: + - name: Checkout pact-python-ffi ${{ env.PACT_PYTHON_FFI_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: pact-foundation/pact-python + ref: pact-python-ffi/${{ env.PACT_PYTHON_FFI_VERSION }} + persist-credentials: false + + - name: Compute the matching pact-reference libpact_ffi tag + # hatch_build.py derives this from the first 3 components of its own + # version (pact-python-ffi versions itself {pact-reference version}.{N}). + id: pact-reference + run: echo "tag=libpact_ffi-v$(cut -d. -f1-3 <<< '${{ env.PACT_PYTHON_FFI_VERSION }}')" >> "$GITHUB_OUTPUT" + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Patch pact-python-ffi source + run: git apply python-wheels/patches/pact-python-ffi/${{ env.PACT_PYTHON_FFI_VERSION }}/*.patch + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + package-dir: pact-python-ffi + output-dir: wheelhouse/ + env: + CIBW_BUILD: ${{ matrix.build }} + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # musllinux is dropped: rustup.rs ships no riscv64 musl toolchain. + # + # hatch_build.py's build hook downloads a prebuilt libpact_ffi from + # pact-reference's GitHub releases, which has no riscv64 asset; build + # it from source instead (gotcha 77) and stage the gzipped .so + + # header at the exact path _download() already caches to, so the + # patched platform check is satisfied with no network request. + # aws-lc-sys (pulled in via pact-plugin-driver/pact_verifier's TLS + # stack) ships prebuilt riscv64gc bindings, and cmake is already in + # the manylinux image. + CIBW_BEFORE_ALL_LINUX: | + set -eux + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + git clone --depth 1 --branch ${{ steps.pact-reference.outputs.tag }} \ + https://github.com/pact-foundation/pact-reference /tmp/pact-reference + cd /tmp/pact-reference/rust + "$HOME/.cargo/bin/cargo" build --release --locked -p pact_ffi + mkdir -p {package}/src/pact_ffi/data + gzip -c target/release/libpact_ffi.so \ + > {package}/src/pact_ffi/data/libpact_ffi-linux-riscv64.so.gz + curl --proto '=https' --tlsv1.2 -sSfL \ + -o {package}/src/pact_ffi/data/pact.h \ + https://github.com/pact-foundation/pact-reference/releases/download/${{ steps.pact-reference.outputs.tag }}/pact.h + CIBW_BUILD_FRONTEND: build + # pyproject.toml's [tool.pytest] addopts always enables coverage + # (--cov=pact_ffi et al), which requires pytest-cov to be installed. + CIBW_TEST_REQUIRES: pytest pytest-cov + CIBW_TEST_SOURCES: pact-python-ffi/tests/test_init.py pact-python-ffi/pyproject.toml + # pact_ffi/__init__.py does `from pact_ffi.ffi import ffi, lib`, which + # shadows the `pact_ffi.ffi` package attribute with the imported `ffi` + # object (a cffi.FFI instance with no __file__) - go through + # sys.modules instead to check the compiled extension module itself. + CIBW_TEST_COMMAND: | + cd pact-python-ffi && + python3 - <<'PYEOF' && + import sys + import pact_ffi + m = sys.modules['pact_ffi.ffi'] + assert m.__file__.endswith('.so'), m.__file__ + PYEOF + python -m pytest -v tests/test_init.py + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pact-python-ffi-${{ env.PACT_PYTHON_FFI_VERSION }}-${{ matrix.tag }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish pact-python-ffi ${{ inputs.version || '0.5.4.1' }} + needs: [setup, build_wheels] + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + with: + artifact-pattern: pact-python-ffi-${{ inputs.version || '0.5.4.1' }}-*-manylinux_riscv64 diff --git a/patches/pact-python-ffi/0.5.4.1/0001-recognize-riscv64-in-the-hatch-build-hook.patch b/patches/pact-python-ffi/0.5.4.1/0001-recognize-riscv64-in-the-hatch-build-hook.patch new file mode 100644 index 000000000..d66b878ca --- /dev/null +++ b/patches/pact-python-ffi/0.5.4.1/0001-recognize-riscv64-in-the-hatch-build-hook.patch @@ -0,0 +1,72 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Thu, 10 Sep 2026 00:00:00 +0000 +Subject: [PATCH] recognize riscv64 in the hatch build hook + +hatch_build.py downloads a prebuilt libpact_ffi from +pact-foundation/pact-reference's GitHub releases and compiles the CFFI +extension against it. `_lib_url` derives the target platform from +`next(t.platform for t in packaging.tags.sys_tags())` and only +recognizes `_aarch64`/`_x86_64` wheel-tag suffixes on a tag that starts +with `"manylinux"`. + +Two things are wrong for riscv64, confirmed from the CI log (job +102765101921: the from-source build finishes in 39m24s, then `python -m +build` still raises `UnsupportedPlatformError: Unsupported platform +linux_riscv64`): + +1. `packaging` 26.3 (an unpinned build dependency here, and the version + actually resolved in that job) reordered `sys_tags()` so the generic + `linux_` tag now comes before any `manylinux_*`/`musllinux_*` + tag (packaging changelog: "Native linux_* platform tags are now + ordered before manylinux and musllinux tags", packaging#160). So + `_sys_tag_platform()` returns `linux_riscv64`, not a + `manylinux_2_39_riscv64`-shaped tag - `startswith("manylinux")` + never matches, on any architecture, once a fresh isolated build + environment resolves `packaging>=26.3`. +2. Even when the `manylinux` branch is reached, it has no riscv64 case. + +pact-reference publishes no riscv64 libpact_ffi asset (there is no +prebuilt binary to download), so recognizing the platform alone does +not make the download succeed. Paired with this patch, the riscv64 CI +job builds libpact_ffi from the pact-reference source at the matching +tag and stages the gzipped .so at the exact +`src/pact_ffi/data/libpact_ffi-linux-riscv64.so.gz` path `_download()` +already caches to, so it is picked up without a network request. + +Upstream-Status: To upstream [blocked: pact-reference publishes no riscv64 libpact_ffi release asset yet] + +Signed-off-by: Ludovic Henry +--- +diff --git a/pact-python-ffi/hatch_build.py b/pact-python-ffi/hatch_build.py +index 0000000..0000000 100644 +--- a/pact-python-ffi/hatch_build.py ++++ b/pact-python-ffi/hatch_build.py +@@ -209,6 +209,7 @@ + + aarch64 = ("_arm64", "_aarch64") + x86_64 = ("_x86_64", "_amd64") ++ riscv64 = ("_riscv64",) + + # Simplified platform and architecture detection + if wheel_platform.startswith("macosx"): +@@ -233,7 +234,7 @@ + else: + raise UnsupportedPlatformError(wheel_platform) + +- elif wheel_platform.startswith("manylinux"): ++ elif wheel_platform.startswith(("manylinux", "linux")): + os, ext = "linux", "so.gz" + prefix = "lib" + suffix = "" +@@ -241,6 +242,8 @@ + platform = "aarch64" + elif wheel_platform.endswith(x86_64): + platform = "x86_64" ++ elif wheel_platform.endswith(riscv64): ++ platform = "riscv64" + else: + raise UnsupportedPlatformError(wheel_platform) + +-- +2.51.0