diff --git a/.github/workflows/build-praat-parselmouth.yml b/.github/workflows/build-praat-parselmouth.yml new file mode 100644 index 000000000..207b29791 --- /dev/null +++ b/.github/workflows/build-praat-parselmouth.yml @@ -0,0 +1,107 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `wheels` job of upstream's own cibuildwheel config: +# https://github.com/YannickJadoul/Parselmouth/blob/v0.4.7/.github/workflows/wheels.yml +name: Build praat-parselmouth wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'praat-parselmouth version to build (git tag without leading v, e.g. 0.4.7)' + required: true + default: '0.4.7' + pull_request: + paths: + - '.github/workflows/build-praat-parselmouth.yml' + - 'patches/praat-parselmouth/**' + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || '0.4.7' }}-${{ 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.4.7 there. + PARSELMOUTH_VERSION: ${{ inputs.version || '0.4.7' }} + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + + build_wheels: + needs: [setup] + name: Build praat-parselmouth ${{ inputs.version || '0.4.7' }} cp312/cp313/cp314-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 480 # ~1M lines of vendored Praat C/C++ (clapack, gsl, glpk, flac, portaudio, espeak, vorbis, opusfile) + + steps: + - name: Checkout Parselmouth v${{ env.PARSELMOUTH_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: YannickJadoul/Parselmouth + ref: v${{ env.PARSELMOUTH_VERSION }} + # praat/ and pybind11/ are vendored directly; only extern/fmt is a real submodule. + submodules: recursive + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Patch praat-parselmouth source + run: git apply python-wheels/patches/praat-parselmouth/${{ env.PARSELMOUTH_VERSION }}/*.patch + + - name: Pin cmake build dependency + # Mirrors upstream's own CI (which rewrites pyproject.toml the same way at + # build time): cmake 4 dropped compatibility with cmake_minimum_required + # versions below 3.5, which the vendored pybind11 (3.5) and fmt (3.1...3.18) + # CMakeLists.txt rely on. + run: sed -i 's/"cmake>=3.18"/"cmake>=3.18,<4"/' pyproject.toml + + - name: Build and test wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + env: + CIBW_ARCHS: riscv64 + CIBW_BUILD: cp312-manylinux_riscv64 cp313-manylinux_riscv64 cp314-manylinux_riscv64 + # No musllinux riscv64 numpy wheel on the registry yet (workflow-anatomy.md). + CIBW_SKIP: "*musllinux*" + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # Builds the heavy vendored `praat` static lib once for the whole job + # instead of once per interpreter (gotcha 15), exactly like upstream's + # own CIBW_BEFORE_ALL. + CIBW_BEFORE_ALL_LINUX: | + cmake -S . -B build_dependencies + cmake --build build_dependencies --target praat -j "$(nproc)" + CIBW_BEFORE_BUILD: rm -rf _skbuild + CIBW_ENVIRONMENT: >- + PARSELMOUTH_EXTRA_CMAKE_ARGS="-DPREBUILT_DEPENDENCIES=$(pwd)/build_dependencies" + CMAKE_BUILD_PARALLEL_LEVEL=4 + PIP_ONLY_BINARY=numpy,cmake + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + CIBW_TEST_REQUIRES: "pytest<8 pytest-lazy-fixture tgt future" + CIBW_TEST_COMMAND: pytest {project}/tests + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: praat-parselmouth-${{ env.PARSELMOUTH_VERSION }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish praat-parselmouth ${{ inputs.version || '0.4.7' }} + needs: [setup, build_wheels] + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + with: + artifact-pattern: praat-parselmouth-${{ inputs.version || '0.4.7' }}-manylinux_riscv64 diff --git a/patches/praat-parselmouth/0.4.7/0001-test-tolerate-FP-rounding-differences-in-test_xs-s-g.patch b/patches/praat-parselmouth/0.4.7/0001-test-tolerate-FP-rounding-differences-in-test_xs-s-g.patch new file mode 100644 index 000000000..bb1656823 --- /dev/null +++ b/patches/praat-parselmouth/0.4.7/0001-test-tolerate-FP-rounding-differences-in-test_xs-s-g.patch @@ -0,0 +1,44 @@ +From fe441a9a966150645b0ec88f0bc62b7c22f4b797 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 11 Sep 2026 12:13:56 +0200 +Subject: [PATCH] test: tolerate FP rounding differences in test_xs's + grid-formula recomputation + +Upstream-Status: To upstream [not submitted; this port's automation may not open issues or PRs on repos other than riseproject-dev/python-wheels] + +test_xs re-derives each Sampled's x grid in Python (x1 + dx * arange(nx)) +and compares it to the C++-computed xs()/x_grid()/x_bins() with strict +floating-point equality. On riscv64 the two independently-computed +expressions differ in their last bit or two (compiler FMA fusion +differences between GCC's riscv64 codegen and numpy's separate +multiply-then-add), so the exact-equality assert fails even though the +values agree to machine precision. Reproduces on riscv64 only; not seen +on the x86_64/aarch64/arm64/windows runners upstream's own CI covers. + +Switch the three assertions to np.allclose with a tolerance far above +double-precision rounding error but far below any real functional +divergence. +--- + tests/test_sampled.py | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/tests/test_sampled.py b/tests/test_sampled.py +index fc4c141..7b7cc93 100644 +--- a/tests/test_sampled.py ++++ b/tests/test_sampled.py +@@ -21,9 +21,9 @@ import numpy as np + + + def test_xs(sampled): +- assert np.all(sampled.xs() == sampled.x1 + sampled.dx * np.arange(sampled.nx)) +- assert np.all(sampled.x_grid() == sampled.x1 + sampled.dx * (np.arange(sampled.nx + 1) - 0.5)) +- assert np.all(sampled.x_bins() == np.vstack((sampled.x_grid()[:-1], sampled.x_grid()[1:])).T) ++ assert np.allclose(sampled.xs(), sampled.x1 + sampled.dx * np.arange(sampled.nx), rtol=1e-9, atol=1e-12) ++ assert np.allclose(sampled.x_grid(), sampled.x1 + sampled.dx * (np.arange(sampled.nx + 1) - 0.5), rtol=1e-9, atol=1e-12) ++ assert np.allclose(sampled.x_bins(), np.vstack((sampled.x_grid()[:-1], sampled.x_grid()[1:])).T, rtol=1e-9, atol=1e-12) + + + def test_len(sampled): +-- +2.50.1 (Apple Git-155) +