diff --git a/.github/workflows/build-scikit-bio.yml b/.github/workflows/build-scikit-bio.yml new file mode 100644 index 000000000000..acaa5df3809e --- /dev/null +++ b/.github/workflows/build-scikit-bio.yml @@ -0,0 +1,163 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `build_wheels` job of +# https://github.com/scikit-bio/scikit-bio/blob/0.7.3/.github/workflows/wheels.yml +name: Build scikit-bio wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/scikit-bio.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-scikit-bio.yml' + - 'docs/packages/scikit-bio.yaml' + - 'patches/scikit-bio/**' + push: + branches: [main] + paths: + - '.github/workflows/build-scikit-bio.yml' + - 'docs/packages/scikit-bio.yaml' + - 'patches/scikit-bio/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: scikit-bio + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build scikit-bio ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + # Matches upstream's own CIBW_BUILD: no free-threaded wheels are published. + # No cp310/cp311: the h5py and biom-format runtime deps have riscv64 wheels for cp312+ only. + python: + - "cp312" + - "cp313" + - "cp314" + + env: + SCIKIT_BIO_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout scikit-bio ${{ env.SCIKIT_BIO_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: scikit-bio/scikit-bio + ref: ${{ env.SCIKIT_BIO_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Patch scikit-bio source + run: git apply python-wheels/patches/scikit-bio/${{ env.SCIKIT_BIO_VERSION }}/*.patch + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + CIBW_ENVIRONMENT: PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + # Without this, pip resolves the runtime dep pandas>=1.5.0 to PyPI's newest + # release, which our registry hasn't built yet and whose sdist fails on + # riscv64 (xsimd has no generic/scalar load_unaligned/reduce_add/broadcast + # there). PIP_PREFER_BINARY makes it take our registry's older wheel-backed + # pandas instead (gotcha 354; test-phase only, per gotcha 12). + CIBW_TEST_ENVIRONMENT: PIP_PREFER_BINARY=1 + CIBW_TEST_COMMAND: python -m skbio.test + + - name: Check the C extensions made it into the wheel + run: | + python3 - wheelhouse/*.whl <<'EOF' + import sys, zipfile + for whl in sys.argv[1:]: + names = [n for n in zipfile.ZipFile(whl).namelist() if n.endswith(".so")] + assert len(names) == 7, (whl, names) + print(whl, "ok") + EOF + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: scikit-bio-${{ env.SCIKIT_BIO_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + gpl_sources: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + name: Collect GPL sources (gcc) for scikit-bio ${{ matrix.version }} + runs-on: ubuntu-24.04-riscv + + env: + SCIKIT_BIO_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # The extensions build with -fopenmp, so auditwheel vendors the image's libgomp. + - uses: ./actions/collect-gpl-sources + with: + image: ${{ env.MANYLINUX_RISCV64_IMAGE }} + packages: gcc + output: gpl-sources.tar + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: scikit-bio-${{ env.SCIKIT_BIO_VERSION }}-gpl-sources + path: gpl-sources.tar + if-no-files-found: error + + publish: + name: Publish scikit-bio ${{ matrix.version }} + needs: [setup, build_wheels, gpl_sources] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: scikit-bio-${{ matrix.version }}-*-manylinux_riscv64 + gpl-sources-artifact: scikit-bio-${{ matrix.version }}-gpl-sources + gpl-sources-description: gcc diff --git a/docs/packages/scikit-bio.yaml b/docs/packages/scikit-bio.yaml new file mode 100644 index 000000000000..1d96dde46658 --- /dev/null +++ b/docs/packages/scikit-bio.yaml @@ -0,0 +1,5 @@ +package-name: scikit-bio +source-code: https://github.com/scikit-bio/scikit-bio +license: BSD-3-Clause +versions: +- version: 0.7.3 diff --git a/patches/scikit-bio/0.7.3/0001-use-schedule-static-for-the-dynamic-OpenMP-loops-in-.patch b/patches/scikit-bio/0.7.3/0001-use-schedule-static-for-the-dynamic-OpenMP-loops-in-.patch new file mode 100644 index 000000000000..9339018b6cd8 --- /dev/null +++ b/patches/scikit-bio/0.7.3/0001-use-schedule-static-for-the-dynamic-OpenMP-loops-in-.patch @@ -0,0 +1,55 @@ +From 7641767e67e2affd82ba59131e59e02be8435c74 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 23 Oct 2026 17:48:26 +0000 +Subject: [PATCH] use schedule(static) for the dynamic OpenMP loops in + _c_me.pyx + +Upstream-Status: Inappropriate [works around a libgomp defect on the riscv64 runners, riseproject-dev/python-wheels#617] + +libgomp's dynamic and guided work-share schedules fault on the riscv64 +machines these wheels are built and tested on, while schedule(static) +is clean (see the LightGBM and healpy patches in this repository). + +The cp312 job segfaulted in tree/tests/test_me.py::test_bme_real, with +the Python stack ending at skbio/tree/_me.py's _bme calling +_bal_avgdist_nest. That function's only parallel loop is a +prange(..., schedule="dynamic"), which compiles to +GOMP_loop_nonmonotonic_dynamic_start/next. The cp313 and cp314 jobs ran +the same test green, as expected of this intermittent fault. + +_c_me.pyx has exactly two dynamic pranges, in _bal_avgdist_nest and +_bal_min_branch_p; every other prange in scikit-bio leaves the schedule +unset, which GCC compiles as static. Both loops do independent work per +chunk of clades and write disjoint outputs, so the schedule only changes +which thread takes which chunk, not the result. Moving both to static +leaves the extension with no calls into libgomp's dynamic or guided +iterators. Revert once the runners' toolchain is fixed. +--- + skbio/tree/_c_me.pyx | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/skbio/tree/_c_me.pyx b/skbio/tree/_c_me.pyx +index 1a7b239..4b6096b 100644 +--- a/skbio/tree/_c_me.pyx ++++ b/skbio/tree/_c_me.pyx +@@ -1031,7 +1031,7 @@ def _bal_min_branch_p( + + # Parallel phase: Do calculations within chunks of clades. + +- for ichu in prange(nchu, nogil=True, schedule="dynamic"): ++ for ichu in prange(nchu, nogil=True, schedule="static"): + for icla in range(chunks[ichu], chunks[ichu + 1]): + i = clades[icla] + min_i = roots[i] +@@ -2256,7 +2256,7 @@ def _bal_avgdist_nest( + # number of branches from insertion point to any node + cdef Py_ssize_t deg + +- for ichu in prange(nchu, nogil=True, schedule="dynamic"): ++ for ichu in prange(nchu, nogil=True, schedule="static"): + iseg = chusegs[ichu] # next segment index + seg = segs[iseg] + lvl = lvls[iseg - 1] if iseg else -1 # current segment's level +-- +2.43.0 +