diff --git a/.github/workflows/build-nutpie.yml b/.github/workflows/build-nutpie.yml new file mode 100644 index 00000000000..d3e4d4662b4 --- /dev/null +++ b/.github/workflows/build-nutpie.yml @@ -0,0 +1,175 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `build_linux` + `test_linux` jobs of +# https://github.com/pymc-devs/nutpie/blob/v0.16.11/.github/workflows/ci.yml +name: Build nutpie wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/nutpie.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-nutpie.yml' + - 'docs/packages/nutpie.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-nutpie.yml' + - 'docs/packages/nutpie.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: nutpie + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build nutpie ${{ matrix.version }} manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 480 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + + env: + NUTPIE_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout nutpie v${{ env.NUTPIE_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: pymc-devs/nutpie + ref: v${{ env.NUTPIE_VERSION }} + persist-credentials: false + + # nuts-rs/faer/zarrs are all pure Rust (no BLAS/LAPACK), so the riscv64gc + # target resolves the same dependency tree as x86_64/aarch64. bridgestan's + # build.rs only runs bindgen over a header (no Stan compilation at build + # time), which needs libclang. + # + # No `--locked` (matching upstream's own ci.yml, which never passes it): + # the v0.16.11 tag's committed Cargo.lock still lists the "nutpie" crate + # itself as version 0.16.10 (a stale self-entry left by their release + # tooling), so cargo needs to update that one line and --locked forbids + # it. + - name: Build wheels + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 + with: + target: riscv64gc-unknown-linux-gnu + args: --release --out dist --interpreter 3.12 3.13 3.14 + manylinux: '2_39' + before-script-linux: dnf install -y clang-libs clang || apt install -y llvm-dev libclang-dev clang + + - name: Verify the wheels ship the compiled extension + run: | + set -euo pipefail + for whl in dist/*.whl; do + python3 - "$whl" <<'EOF' + import sys, zipfile + names = zipfile.ZipFile(sys.argv[1]).namelist() + sos = [n for n in names if n.endswith(".so")] + assert any("_lib" in n for n in sos), sos + assert any(n.endswith(".dist-info/licenses/LICENSE") for n in names), names + print(sys.argv[1], "->", sos) + EOF + done + + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: false + + # Upstream gates its release on the "stan" pytest suite, but that needs + # stanc3 (github.com/stan-dev/stanc3 releases), which ships no riscv64 + # build; "pymc"/"flow" need numba (-> llvmlite, parked, gotcha + # feasibility-and-triage.md) or jax. None of upstream's own suites can + # run here, so this drives the compiled sampler directly through + # nutpie's dependency-free `compiled_pyfunc` API instead (gotcha 187). + - name: Sample a toy model with each wheel + env: + UV_EXTRA_INDEX_URL: https://pypi.riseproject.dev/simple/ + UV_INDEX_STRATEGY: unsafe-best-match + UV_ONLY_BINARY: numpy,scipy,pandas,pyarrow + run: | + set -euo pipefail + for py in 3.12 3.13 3.14; do + venv="$RUNNER_TEMP/venv-$py" + uv venv --python "$py" "$venv" + tag="cp$(echo "$py" | tr -d '.')" + uv pip install --python "$venv/bin/python" dist/nutpie-*-"$tag"-*.whl + "$venv/bin/python" - <<'EOF' + import numpy as np + import nutpie + from nutpie.compiled_pyfunc import from_pyfunc + + def make_logp_fn(): + def logp(x): + return float(-0.5 * x[0] ** 2), np.array([-x[0]], dtype="float64") + + return logp + + def make_expand_fn(seed1, seed2, chain): + def expand(x): + return {"x": np.asarray(x, dtype="float64", order="C")} + + return expand + + model = from_pyfunc( + ndim=1, + make_logp_fn=make_logp_fn, + make_expand_fn=make_expand_fn, + expanded_dtypes=[np.dtype("float64")], + expanded_shapes=[(1,)], + expanded_names=["x"], + ) + trace = nutpie.sample( + model, chains=2, tune=300, draws=300, cores=1, seed=42, progress_bar=False + ) + draws = trace.posterior["x"].to_numpy() + mean = draws.mean() + std = draws.std() + assert abs(mean) < 0.3, mean + assert 0.6 < std < 1.4, std + print("sampled standard normal via compiled_pyfunc: mean", mean, "std", std) + EOF + done + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: nutpie-${{ env.NUTPIE_VERSION }}-manylinux_riscv64 + path: dist/*.whl + if-no-files-found: error + + publish: + name: Publish nutpie ${{ 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: nutpie-${{ matrix.version }}-manylinux_riscv64 diff --git a/docs/packages/nutpie.yaml b/docs/packages/nutpie.yaml new file mode 100644 index 00000000000..69c29cc78cf --- /dev/null +++ b/docs/packages/nutpie.yaml @@ -0,0 +1,5 @@ +package-name: nutpie +source-code: https://github.com/pymc-devs/nutpie +license: MIT +versions: +- version: 0.16.11 diff --git a/skills/python-project-porting/references/gotchas-index.md b/skills/python-project-porting/references/gotchas-index.md index d45480fca53..d57cea8a94a 100644 --- a/skills/python-project-porting/references/gotchas-index.md +++ b/skills/python-project-porting/references/gotchas-index.md @@ -705,6 +705,10 @@ The porting gotchas (550 of them) live in [`references/gotchas/`](gotchas/), spl sysroot or container: host `clang --target=riscv64-linux-gnu` with the x86_64 glibc headers, plus stubs for `gnu/stubs-32.h` and the `regparm` in `pthreadtypes-arch.h` (the foxglove-sdk case). +- **608** — A tagged release's own committed `Cargo.lock` can be stale by one version bump in + the crate's own self-entry (release tooling bumps `Cargo.toml` before regenerating the + lock), which only `--locked` turns into a build failure; drop `--locked` to match upstream's + own CI rather than patching or regenerating `Cargo.lock` (the nutpie 0.16.11 case). ### Bazel & driving the build container — [`gotchas/native-build-bazel-and-drivers.md`](gotchas/native-build-bazel-and-drivers.md) diff --git a/skills/python-project-porting/references/gotchas/rust-maturin-and-pyo3.md b/skills/python-project-porting/references/gotchas/rust-maturin-and-pyo3.md index 50d29097a53..9a4ab260054 100644 --- a/skills/python-project-porting/references/gotchas/rust-maturin-and-pyo3.md +++ b/skills/python-project-porting/references/gotchas/rust-maturin-and-pyo3.md @@ -28,6 +28,8 @@ To pull up one entry: `grep -n '^N\. ' references/gotchas/rust-maturin-and-pyo3. different path in the git checkout than in the PyPI sdist. - **259** — A maturin `bindings = "bin"` project can declare two `[[bin]]` targets where - **260** — `puccinialin` (and similar rust-bootstrap-on-demand helpers) has no riscv64 entry +- **608** — A tagged release's own committed `Cargo.lock` can be stale by one version bump in + the crate's own self-entry, which only `--locked` turns into a build failure. - **371** — pyo3 0.22's version ceiling (gotcha 306) is a hard ceiling for a non-abi3, per-interpreter build too, one minor above its own release-time latest — the `PYO3_USE_ABI3_FORWARD_COMPATIBILITY` escape hatch works without turning abi3 on. @@ -91,6 +93,9 @@ To pull up one entry: `grep -n '^N\. ' references/gotchas/rust-maturin-and-pyo3. reverse-dependency closure in `Cargo.lock`, not by "one extension per interpreter". - **597** — A riscv64 `cargo check` of a tree whose `-sys` crates compile C needs no riscv64 sysroot: host clang plus x86_64 glibc headers and two stub headers. +- **608** — A tagged release's committed `Cargo.lock` can have a stale self-version entry + (the crate's own `[[package]] version` a release behind its `Cargo.toml`), which only + `maturin-action`'s `--locked` turns into a build failure. --- @@ -1481,3 +1486,30 @@ To pull up one entry: `grep -n '^N\. ' references/gotchas/rust-maturin-and-pyo3. so they say nothing about riscv64 C codegen. That matters little for the well-trodden `lz4-sys`/`zstd-sys`, but it is no substitute for gotcha 412's generic-path check when the C is the unknown. + +608. **A tagged release's own committed `Cargo.lock` can be stale by one version bump, and + `maturin-action`'s `args: --locked` turns that into a hard failure even when upstream's + own CI never passes `--locked` at all** (nutpie 0.16.11: `cargo metadata`/`cargo build` + error `cannot update the lock file ... because --locked was passed`, `💥 maturin failed`). + The mismatch isn't a real dependency drift — it's the crate's *own* self-entry: the + package's release tooling bumps `Cargo.toml`'s `version` but tags before regenerating + `Cargo.lock`, so the lock's `[[package]] name = "" version = "..."` line still + names the previous release. `cargo metadata --locked` (or a plain build) refuses to fix + even that one line without `--locked` being dropped or `--offline` with a pre-populated + registry being used instead. Diagnose by cloning the upstream tag and comparing: + ```bash + git clone --depth 1 --branch /tmp/x && cd /tmp/x + cargo metadata --locked --format-version 1 # reproduces the exact CI error + git diff Cargo.lock # after a metadata run *without* --locked + ``` + If the diff is a single `version = "..."` line inside the crate's own `[[package]]` + block, this is that trap, not a real re-resolution risk (gotcha 10's "floating deps" + case is different: there the *whole* tree re-resolves because no lock ships at all). + Fix: check whether upstream's own release workflow passes `--locked` to + `maturin-action`/`cargo build` — it usually doesn't, because upstream never builds from + a lock file this stale — and drop `--locked` from our own `args:` to match (see + `build-tombi.yml` for the same pattern from a version-patch step leaving workspace-local + `Cargo.lock` entries stale). Regenerating and committing a patched `Cargo.lock` under + `patches///` is unnecessary extra surface for a one-line, upstream-caused + mismatch that plain `cargo build` (no `--locked`) fixes on every run without touching any + other dependency.