Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions .github/workflows/build-scikit-survival.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions docs/packages/scikit-survival.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
From 17ec8da55f97261781ede418ee606ed56d6e6b21 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
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 <git@ludovic.dev>
---
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

Loading