diff --git a/.github/workflows/build-scikit-survival.yml b/.github/workflows/build-scikit-survival.yml new file mode 100644 index 00000000000..5dd9c0a087f --- /dev/null +++ b/.github/workflows/build-scikit-survival.yml @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# Based on: https://github.com/sebp/scikit-survival/blob/v0.28.0/.github/workflows/wheels-workflow.yaml +name: Build scikit-survival wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/scikit-survival.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-scikit-survival.yml' + - 'docs/packages/scikit-survival.yaml' + - 'patches/scikit-survival/**' + push: + branches: [main] + paths: + - '.github/workflows/build-scikit-survival.yml' + - 'docs/packages/scikit-survival.yaml' + - 'patches/scikit-survival/**' + +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-survival + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build scikit-survival ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + # No cp314t: upstream ships none, and pandas (a hard dependency) has no free-threaded wheel. + python: ["cp312", "cp313", "cp314"] + + env: + SCIKIT_SURVIVAL_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout scikit-survival v${{ env.SCIKIT_SURVIVAL_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: sebp/scikit-survival + ref: v${{ env.SCIKIT_SURVIVAL_VERSION }} + submodules: true + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Apply patches + run: git apply python-wheels/patches/scikit-survival/${{ env.SCIKIT_SURVIVAL_VERSION }}/*.patch + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # Upstream's build[uv] crashes the audit step on the riscv runner (gotcha 13). + CIBW_BUILD_FRONTEND: build + # The patched tree would otherwise get a dirty setuptools_scm version (gotcha 31). + CIBW_ENVIRONMENT: >- + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + PIP_ONLY_BINARY=numpy,scipy,scikit-learn,pandas,ecos,osqp,numexpr + SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SCIKIT_SURVIVAL=${{ env.SCIKIT_SURVIVAL_VERSION }} + CIBW_TEST_GROUPS: test + # polars 1.43 (released after 0.28.0) deprecates cat.get_categories(), which the suite's filterwarnings=error turns into failures. + CIBW_TEST_REQUIRES: polars<1.43 + # riscv64 rounding moves a ~2.7e7 cumulative hazard by 6e-13 relative, past this test's 5-decimal absolute check. + CIBW_TEST_COMMAND: >- + pytest --strict-markers -m 'not slow' -k 'not test_fit_and_predict_linear_regression' + --deselect 'tests/test_coxnet.py::TestCoxnetBaselineModel::test_baseline_predict[predict_cumulative_hazard_function-False]' + {project}/tests + + - name: Check wheel contents + run: | + python3 - wheelhouse/*.whl <<'EOF' + import sys, zipfile + names = zipfile.ZipFile(sys.argv[1]).namelist() + exts = {n.split("/")[-1].split(".")[0] for n in names if n.endswith(".so")} + assert exts == {"_binarytrees", "_clinical_kernel", "_coxph_loss", "_prsvm", "_minlip", "_criterion", "_coxnet"}, exts + licences = {n.rsplit("/", 1)[-1] for n in names if ".dist-info/licenses/" in n} - {""} + assert licences == {"COPYING", "COPYING.MPL2", "COPYING.APACHE"}, licences + EOF + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: scikit-survival-${{ env.SCIKIT_SURVIVAL_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: ./wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish scikit-survival ${{ matrix.version }} + needs: [setup, build_wheels] + 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-survival-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/scikit-survival.yaml b/docs/packages/scikit-survival.yaml new file mode 100644 index 00000000000..ac5ccbb242d --- /dev/null +++ b/docs/packages/scikit-survival.yaml @@ -0,0 +1,6 @@ +package-name: scikit-survival +source-code: https://github.com/sebp/scikit-survival +license: GPL-3.0-or-later +versions: +- version: 0.28.0 + patched: true diff --git a/patches/scikit-survival/0.28.0/0001-ship-the-vendored-Eigen-licences-in-the-wheel.patch b/patches/scikit-survival/0.28.0/0001-ship-the-vendored-Eigen-licences-in-the-wheel.patch new file mode 100644 index 00000000000..f7dd2bd2f55 --- /dev/null +++ b/patches/scikit-survival/0.28.0/0001-ship-the-vendored-Eigen-licences-in-the-wheel.patch @@ -0,0 +1,47 @@ +From 17ec8da55f97261781ede418ee606ed56d6e6b21 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 2 Oct 2026 15:52:22 +0000 +Subject: [PATCH] ship the vendored Eigen licences in the wheel + +Upstream-Status: To upstream [not yet submitted; every scikit-survival wheel on PyPI has the same gap, so it needs raising with the maintainer rather than a drive-by PR] + +sksurv/linear_model/_coxnet.pyx compiles the sksurv/linear_model/src/eigen +submodule's Eigen/Core headers (header-only) straight into the +sksurv.linear_model._coxnet extension. Eigen/Core is MPL-2.0, except +Eigen/src/Core/arch/Default/BFloat16.h, which Eigen/Core includes +unconditionally and which carries the TensorFlow Authors' Apache-2.0 +notice. The built wheel ships only scikit-survival's own COPYING: +`license-files = ["COPYING"]` is an explicit PEP 639 list with no default +glob behind it, so Eigen's COPYING.* files never reach dist-info/licenses/. + +List COPYING.MPL2 and COPYING.APACHE in place from the submodule. The +other Eigen notices are not reachable from Eigen/Core: Half.h's Fabian +Giesen notice grants redistribution unconditionally, and the *_BLAS.h / +*_MKL.h files are only compiled in with EIGEN_USE_BLAS / EIGEN_USE_MKL, +which scikit-survival never defines. + +Signed-off-by: Ludovic Henry +--- + pyproject.toml | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/pyproject.toml b/pyproject.toml +index 5f6fb0b..b16056b 100644 +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -22,7 +22,11 @@ authors = [ + {name = "Sebastian Pölsterl", email = "sebp@k-d-w.org"} + ] + license = "GPL-3.0-or-later" +-license-files = ["COPYING"] ++license-files = [ ++ "COPYING", ++ "sksurv/linear_model/src/eigen/COPYING.MPL2", ++ "sksurv/linear_model/src/eigen/COPYING.APACHE", ++] + requires-python = ">=3.11" + classifiers = [ + "Development Status :: 4 - Beta", +-- +2.43.0 +