diff --git a/.github/workflows/build-polars-ols.yml b/.github/workflows/build-polars-ols.yml new file mode 100644 index 000000000000..2d5dc786f547 --- /dev/null +++ b/.github/workflows/build-polars-ols.yml @@ -0,0 +1,147 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `linux` and `linux_tests` jobs of +# https://github.com/azmyrajab/polars_ols/blob/v0.3.5/.github/workflows/CI.yml +name: Build polars-ols wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/polars-ols.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-polars-ols.yml' + - 'docs/packages/polars-ols.yaml' + - 'patches/polars-ols/**' + push: + branches: [main] + paths: + - '.github/workflows/build-polars-ols.yml' + - 'docs/packages/polars-ols.yaml' + - 'patches/polars-ols/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + # rust-toolchain.toml floats on "nightly"; this is the nightly upstream's 0.3.5 + # wheels were built with (2024-08-25), which its pinned crates still compile on. + RUST_TOOLCHAIN: nightly-2024-08-25 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: polars-ols + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build polars-ols ${{ matrix.version }} cp38-abi3-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + + env: + POLARS_OLS_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout polars_ols v${{ env.POLARS_OLS_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: azmyrajab/polars_ols + ref: v${{ env.POLARS_OLS_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + sparse-checkout: patches/polars-ols + persist-credentials: false + + - name: Apply patches + run: git apply -v python-wheels/patches/polars-ols/${{ env.POLARS_OLS_VERSION }}/*.patch + + - name: Build and test wheel + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + env: + # abi3-py38 gives one wheel, built on cp312 and re-tested on cp313. cp314: + # statsmodels has no riscv64 cp314 wheel; cp314t: pyo3 0.21 has no free-threading. + # musllinux is dropped: rustup.rs ships no riscv64 musl toolchain. + CIBW_BUILD: cp312-manylinux_riscv64 cp313-manylinux_riscv64 + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + CIBW_BEFORE_ALL_LINUX: >- + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | + sh -s -- -y --profile minimal --default-toolchain ${{ env.RUST_TOOLCHAIN }} + # gotcha 76: pyproject.toml's build-system.requires pins an unused "polars" + # (maturin builds the Rust polars crate from crates.io, never imports the + # Python package), which resolves to polars-runtime-32 and falls back to a + # from-source build needing a newer Rust than RUST_TOOLCHAIN. Skip resolving + # build-system.requires entirely instead of patching the pin. + CIBW_BEFORE_BUILD_LINUX: pip install "maturin>=1.0,<2.0" + CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation" + CIBW_ENVIRONMENT_LINUX: >- + PATH="$PATH:$HOME/.cargo/bin" + RUSTUP_TOOLCHAIN=${{ env.RUST_TOOLCHAIN }} + RUSTFLAGS="-Dwarnings" + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + # gotcha 67: PyPI's newer numpy/pandas sdists outrun the registry; pandas 3.1.0 fails to compile on riscv64. + CIBW_TEST_ENVIRONMENT: PIP_ONLY_BINARY=numpy,pandas,pyarrow,scipy,statsmodels,scikit-learn,polars-runtime-32 + # tests/requirements-test.txt, minus maturin/pyperf; the 0.3.5 suite calls + # Expr.flatten, removed in polars 2.0. + CIBW_TEST_REQUIRES: polars[pandas,pyarrow]<2 numpy pytest statsmodels scikit-learn + CIBW_TEST_SOURCES: tests + CIBW_TEST_COMMAND: >- + python -c "import polars_ols._polars_ols as m; assert m.__file__.endswith('.so'), m.__file__" && + python -m pytest -v tests + + - name: Check the licence made it into the wheel + run: | + python3 - wheelhouse/*.whl <<'EOF' + import sys, zipfile + for whl in sys.argv[1:]: + assert "-cp38-abi3-manylinux_" in whl and whl.endswith("_riscv64.whl"), whl + names = zipfile.ZipFile(whl).namelist() + assert any(n.endswith("dist-info/licenses/LICENSE") for n in names), names + print(whl, "ok") + EOF + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: polars-ols-${{ env.POLARS_OLS_VERSION }}-cp38-abi3-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish polars-ols ${{ 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: polars-ols-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/polars-ols.yaml b/docs/packages/polars-ols.yaml new file mode 100644 index 000000000000..297c7cf59b66 --- /dev/null +++ b/docs/packages/polars-ols.yaml @@ -0,0 +1,6 @@ +package-name: polars-ols +source-code: https://github.com/azmyrajab/polars_ols +license: MIT +versions: +- version: 0.3.5 + patched: true diff --git a/patches/polars-ols/0.3.5/0001-Cargo-build-faer-and-pulp-without-nightly-on-riscv64.patch b/patches/polars-ols/0.3.5/0001-Cargo-build-faer-and-pulp-without-nightly-on-riscv64.patch new file mode 100644 index 000000000000..3591defa8d8e --- /dev/null +++ b/patches/polars-ols/0.3.5/0001-Cargo-build-faer-and-pulp-without-nightly-on-riscv64.patch @@ -0,0 +1,51 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Thu, 22 Oct 2026 00:00:00 +0000 +Subject: [PATCH] Cargo: build faer and pulp without "nightly" on riscv64 + +Every non-ARM target enables pulp's and faer's "nightly" features. +faer's "nightly" turns on faer-entity/gemm/gemm-common "nightly", which +all end up enabling pulp/nightly. On riscv64, pulp 0.18.22 then does + + #[cfg(all(feature = "nightly", target_arch = "riscv64"))] + pub use core::arch::riscv64::*; + +without adding #![feature(riscv_ext_intrinsics)], so the crate fails +to compile on the nightly toolchain: + + error[E0658]: use of unstable library feature 'riscv_ext_intrinsics' + --> pulp-0.18.22/src/core_arch/mod.rs:24:13 + +pulp has no riscv64 SIMD paths for "nightly" to switch on (its +re-export is never used on riscv64), so on riscv64 build faer and pulp +without "nightly", as Cargo.toml already does for ARM. faer keeps the +same "std", "rayon", "serde", "rand", "npy" features. The dependency +graph is unchanged, so Cargo.lock still resolves with --locked; on +x86_64 the feature set is identical to before. + +Upstream-Status: To upstream [not submitted from this automated port run; needs a pull request against azmyrajab/polars_ols] + +Signed-off-by: Ludovic Henry +--- + Cargo.toml | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/Cargo.toml b/Cargo.toml +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -40,7 +40,12 @@ ndarray-linalg = {version = "*"} + faer = { version = "*" } # don't allow nightly for ARM due to issues with NEON support + faer-ext = { version = "*", features = ["ndarray"] } + +-[target.'cfg(not(target_arch = "arm"))'.dependencies] ++[target.'cfg(target_arch = "riscv64")'.dependencies] ++pulp = { version = "0.18.22" } # pulp's "nightly" re-exports core::arch::riscv64 without enabling riscv_ext_intrinsics ++faer = { version = "*", features = ["std", "rayon", "serde", "rand", "npy"] } ++faer-ext = { version = "*", features = ["ndarray"] } ++ ++[target.'cfg(not(any(target_arch = "arm", target_arch = "riscv64")))'.dependencies] + pulp = { version = "0.18.22", features = ["nightly"] } + faer = { version = "*", features = ["std", "rayon", "serde", "rand", "npy", "nightly"] } + faer-ext = { version = "*", features = ["ndarray"] } +-- +2.43.0 diff --git a/patches/polars-ols/0.3.5/0002-tests-cast-insert_nulls-values-to-float.patch b/patches/polars-ols/0.3.5/0002-tests-cast-insert_nulls-values-to-float.patch new file mode 100644 index 000000000000..2a158af319d9 --- /dev/null +++ b/patches/polars-ols/0.3.5/0002-tests-cast-insert_nulls-values-to-float.patch @@ -0,0 +1,38 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Thu, 22 Oct 2026 00:00:00 +0000 +Subject: [PATCH] tests: cast insert_nulls values to float + +_make_data(add_missing=True) runs every column, including the Int64 +"group" column, through map_elements(insert_nulls, +return_dtype=pl.Float64), and insert_nulls hands back the original int. +polars up to 1.35 silently cast those ints to Float64; polars 1.36 and +later reject them while building the Series: + + polars.exceptions.SchemaError: unexpected value while building + Series of type Float64; found value of type Int64: 0 + +so test_non_contiguous fails before reaching polars-ols. The oldest +polars available for riscv64 is 1.39, so no installable version still +accepts it. Return float(val), which is the cast older polars did +implicitly; the generated data is unchanged. + +Upstream-Status: To upstream [not submitted from this automated port run; needs a pull request against azmyrajab/polars_ols] + +Signed-off-by: Ludovic Henry +--- + tests/test_ols.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tests/test_ols.py b/tests/test_ols.py +--- a/tests/test_ols.py ++++ b/tests/test_ols.py +@@ -42,7 +42,7 @@ + if add_missing: + + def insert_nulls(val): +- return None if rng.random() < 0.1 else val ++ return None if rng.random() < 0.1 else float(val) + + columns = missing_columns or df.columns + df = df.with_columns(