Repository navigation
feat: add riscv64 to Linux wheel build matrix - #987
Conversation
|
Native build verified on riscv64 hardware (BananaPi F3, SpacemiT K1, rv64gc, GCC 14.2.0): SUCCESS. |
|
Updated |
| - name: Set up QEMU | ||
| if: matrix.archs == 'riscv64' | ||
| uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 | ||
| with: | ||
| platforms: riscv64 |
There was a problem hiding this comment.
Small thing here: the if: matrix.archs == 'riscv64' condition will never match, because there's no riscv64 entry in strategy.matrix.include above (only aarch64/x86_64/arm64/AMD64). The matrix entry mentioned in the PR description seems to have been lost from the patch. Without it this step is always skipped and no riscv64 wheels get built. I think you also need to add:
- os: ubuntu-latest
archs: riscv64to the matrix above.
There was a problem hiding this comment.
Good catch, thanks. Fixed in the latest push.
Two changes beyond just adding the matrix entry:
-
Replaced QEMU emulation with a native
ubuntu-24.04-riscvrunner (RISE Project free runners). Native is faster and more representative than emulation. -
The
pypa/cibuildwheelaction callsactions/setup-pythoninternally, which has no riscv64 binaries. Added a dedicated riscv64 step that installs cibuildwheel viapipand runs it directly — this is the established workaround for native riscv64 CI.
|
Thanks for putting this together @gounthar, and for the detailed evidence and ecosystem context in the description - it really helps to see the bigger picture here. RISC-V wheels would be a nice addition for msgspec. I left an inline note on the QEMU step - looks like the matrix entry from the PR description didn't make it into the patch, and without it the step never runs. That's a blocker for the functional part. Good news on the compatibility side: cibuildwheel has had riscv64 enabled by default since v3.1.2 for anyone targeting it (changelog #2509), and we're already pinned to v3.2.1, so no extra A few things on my mind, mostly around CI cost:
Happy to help with any of the above if it's easier, just let me know. |
|
Thanks for the review, @Siyet. On the QEMU / build time question: the latest push drops QEMU and uses native On tests: with a native runner I'd lean toward leaving them enabled rather than skipping, but if the per-wheel time turns out to be too long in practice, adding On current main: I believe the branch is already based on 0.21.0 (0 commits behind upstream), the 5 commits ahead are CI-only. I should have a CI run to point at shortly. I'll follow up once that's done rather than asking you to take my word for it. Happy to rework any of this if the direction isn't right. |
|
Fork CI numbers are in: native build on The fork is 0 commits behind On tests: with native completing in under 30 minutes with tests on, I'd leave them enabled for now. Easy to add Happy to adjust if the direction isn't right. |
|
Thanks @gounthar for the fast turnaround, and especially for putting together the benchmark workflow for timing - that's exactly what I asked for. Switching from QEMU to native RISE Project runners is the right call, and a better one than I had suggested myself. I also saw your follow-up note saying the benchmark covers all 7 interpreters with tests included - my close read of the run logs actually disagrees with both points (details in the first section below). I think there's a config-loading issue at play that's quietly hiding it. I went through the benchmark run and found a few things we need to talk about before this PR can land. Critical:
|
| Job | Time |
|---|---|
| macos arm64 | 2:47 |
| windows AMD64 | 4:04 |
| macos x86_64 | 4:06 |
| windows ARM64 | 4:52 |
| ubuntu x86_64 | 5:56 |
| ubuntu aarch64 (longest) | 6:15 |
| riscv64 (no tests, from benchmark) | ~27:00 |
| riscv64 (with tests, estimate) | likely 35-45 min |
That means release CI grows from ~6 minutes to ~30-45 minutes on the longest path. And this isn't just release CI - build-wheels also runs on every PR that touches src/msgspec/**, pyproject.toml, or .cibuildwheel.toml (see the paths filter in ci.yml). On PRs build-all=false, musllinux is skipped, leaving 6 wheels - so PR CI grows from ~3-4 minutes to ~13-20 minutes on the longest path, on every PR that touches C code or build config.
I'm a new collaborator on this project and a CI duration increase of this size isn't a decision I'm comfortable making on my own. It needs explicit sign-off from @jcrist or @ofek. I'd like to hear from one of you whether the project is OK with this kind of feedback-loop slowdown for riscv64 wheels, or whether we should look for ways to reduce it - for example, building only the latest stable Python for riscv64 on PRs (with the full set only on release)? Skipping tests on riscv64 in PRs? Open to ideas, but the final "yes" on this isn't mine to give.
Blocker: RISE GitHub App and timing with the org migration
The ubuntu-24.04-riscv runner label is only available after installing the RISE GitHub App on a repo or org. Installation requires admin permissions - as a collaborator I can't do this myself.
There's also a timing wrinkle: in #1015 we're planning to migrate msgspec into a dedicated organization. If we install RISE on jcrist/msgspec now, we'll have to reinstall it on the new org after the transfer. Cleaner: finish the org migration first, install RISE on the new org, then merge this PR. That's probably one to two weeks out.
None of this reflects on the quality of your work - it's purely coordination. I'll ping you as soon as the org is ready and RISE is installed.
Smaller things
-
cibuildwheel version is duplicated - currently
pypa/[email protected](the action) andpip install cibuildwheel==3.2.1(the riscv64 step). When we bump the version, both places need to be remembered. Could be lifted into an env var / matrix value, or stick to just one of the two strategies rather than both. -
/opt/python-3.12/bin/hardcoded path - this assumes a specific RISE image layout. If RISE updates the image to 3.13, this breaks silently. At minimum, a comment with the date / image version it was tested against. Better, look uppython3viacommand -v python3or similar. -
.github/workflows/benchmark-riscv64.yml- is this benchmark workflow meant to land in main, or be removed before merge? From the description ("for upstream discussion") it looks one-off, but it would be good to confirm explicitly.
Summing up
Two independent blockers:
-
Technical (on you): fix the
.cibuildwheel.tomlloading and re-run the benchmark to get real numbers with tests. Without that we'd be publishing untested wheels, and the timings we're discussing here are incomplete. -
Coordination (on me and the senior maintainers):
- Sign-off on this CI wall-time increase from @jcrist (see the section above) - this isn't a call I can make on my own.
- Installing the RISE GitHub App after the org migration (Migrate msgspec to a dedicated GitHub organization #1015) - also not my button to push.
Thanks again for the fast iteration and the right direction. I don't want this to read as a "no" - the PR is going the right way; it's just that decisions of this scale need time and input from people who've been around the project longer than I have.
cc @jcrist - I'd really like your read on growing wall-time CI from ~6 minutes to ~30-45 minutes on the longest path in exchange for riscv64 wheels. If you're OK with it, we move forward after the config fix and the org migration. If you'd like to trim it, let's discuss strategies (e.g. building only a limited set of Python versions for riscv64 on PRs).
|
Addressing the points from your review. Config loading Fixed by passing the path via CLI flag instead of the env var: Re-run: https://github.com/gounthar/msgspec/actions/runs/24306327826 (2026-04-12). Options dump now shows the right values: 6000+ unit tests pass per interpreter, zero failures. Timing with tests Real number: 67 min 28 sec for all 12 wheels (cp310-cp314t x manylinux + musllinux). Worse than your 35-45 min estimate. Per-interpreter it breaks down to roughly 5-6 min build + ~1 min 15 sec tests.
The musllinux image does not have uv pre-installed, so Smaller things
|
|
I'm quite skeptical that it's worth pursuing this if we can't get the build time down. One option could be to split the build specifically for risk into a matrix on the py-versions, so we'd have one independent job per risc build |
|
Agreed, I think if we want to target that platform then it should only run on releases. If it's possible to express with GitHub Actions syntax then I'm also fine with running it when a commit matches a certain pattern so that it could be tested willingly on PRs. |
|
Yes, both are doable. I think the cleanest fit given the existing if: matrix.archs != 'riscv64' || inputs.build-riscv64The caller in build-riscv64: ${{ github.event_name == 'release' || contains(github.event.head_commit.message, '[riscv64]') }}Releases always include it; pushes opt in via commit message. Open to a different prefix if the project already has a convention for that. Will push that change once the org migration is done and the RISE runner label is available on the repo. No point wiring it up before then. |
f271022 to
e133e77
Compare
|
Addressed the build-time concern with a if: matrix.archs != 'riscv64' || inputs.build-riscv64In build-riscv64: ${{ github.event_name == 'release' || contains(github.event.pull_request.labels.*.name, 'build-riscv64') }}I went with a label rather than a commit-message pattern since it felt slightly cleaner, but if you'd prefer the commit-message approach (e.g. PR CI is unaffected. Release CI gains the ~67 min riscv64 job, which runs in parallel with the other release jobs. The longest existing job is ~6 min, so the critical-path wall time for a release roughly triples. Worth being clear-eyed about that cost before deciding to merge. Also removed Branch is current with 0.21.1. |
Add QEMU emulation for riscv64 and include riscv64 in the build matrix so that linux_riscv64 wheels are built alongside existing architectures. Signed-off-by: Bruno Verachten <[email protected]>
Signed-off-by: Bruno Verachten <[email protected]>
The QEMU setup step was present but the riscv64 matrix entry was missing, so it never triggered. Replace the QEMU approach with a native ubuntu-24.04-riscv runner (RISE Project). The pypa/cibuildwheel action calls actions/setup-python internally, which has no riscv64 binaries. Add a dedicated riscv64 step that installs cibuildwheel via pip and runs it directly, consistent with how other projects handle native riscv64 CI with RISE runners. Signed-off-by: Bruno Verachten <[email protected]>
workflow_dispatch trigger that builds the sdist then runs cibuildwheel on the native ubuntu-24.04-riscv runner. Bypasses the full CI pipeline which requires a fork release to exist. Produces real build times for upstream discussion. Signed-off-by: Bruno Verachten <[email protected]>
Use /opt/python-3.12/bin/python3.12 -m pip directly. No python3/pip symlink in PATH on ubuntu-24.04-riscv by default. Also pass sdist name via env var instead of inline expression. Signed-off-by: Bruno Verachten <[email protected]>
- Pass CIBW_CONFIG_FILE as an absolute path (${{ github.workspace }})
so the config is loaded correctly when invoking cibuildwheel via uvx
(relative path resolution differs from the pypa/cibuildwheel action)
- Replace /opt/python-3.12 hardcoded path with uvx cibuildwheel==3.2.1;
add astral-sh/setup-uv@v7 step for riscv64 (already used on non-Linux)
- Extend the uv install condition to include riscv64 Linux runners
Signed-off-by: Bruno Verachten <[email protected]>
The CIBW_CONFIG_FILE env var was not being picked up when cibuildwheel runs against an sdist (it finds pyproject.toml in the extracted source tree and uses that instead). Pass the config path explicitly via the --config-file CLI argument so it takes precedence regardless of what files are present in the build working directory. Drop CIBW_CONFIG_FILE from env since --config-file supersedes it. Signed-off-by: Bruno Verachten <[email protected]>
build[uv] from .cibuildwheel.toml requires uv to be pre-installed inside the container. The musllinux_1_2_riscv64 image does not ship uv, causing 'which uv' to fail during environment setup. Override via CIBW_BUILD_FRONTEND=pip which works without uv in the container and is sufficient for the benchmark timing goal. Signed-off-by: Bruno Verachten <[email protected]>
Add a build-riscv64 boolean input (default false) to build-wheels.yml. Gate the riscv64 matrix entry at the job level: if: matrix.archs != 'riscv64' || inputs.build-riscv64 Wire the input in ci.yml: enabled on releases and on PRs labelled 'build-riscv64'. All other CI runs skip the riscv64 job entirely, keeping PR feedback loops at current speed. Remove benchmark-riscv64.yml (temporary workflow used for timing data, not intended to land in main). Signed-off-by: Bruno Verachten <[email protected]>
A job-level if referencing both matrix and inputs contexts fails validation in workflow_call reusable workflows. Split riscv64 out of the matrix into a dedicated build-wheels-riscv64 job gated behind inputs.build-riscv64 (no matrix context needed). Non-riscv64 matrix unchanged. Signed-off-by: Bruno Verachten <[email protected]>
Signed-off-by: Bruno Verachten <[email protected]>
86bbfa8 to
9f3d1a1
Compare
|
@msgspec/committers I accepted @Siyet's request to install the GitHub app that was recommended by @gounthar. |
|
Now that the RISE runners app is installed on the org (thanks @ofek, @Siyet), the build-wheels-riscv64 job can finally run for real. It's gated behind a build-riscv64 PR label, so it stays off by default and only fires when we opt in. Could a maintainer drop that label on this PR? That'd give us a native ubuntu-24.04-riscv run as proof it goes green. One thing I noticed while looking: the riscv64 job still pins its actions to tags (@v5, @v7), but main has moved to full-SHA pins since this branch was opened, and the CI capability policy (zizmor) enforces it. So those four uses lines will need SHAs to pass the check. I'm happy to push that change if it helps, unless @Siyet is already on it. |
|
@gounthar I added the label so the wheels should get built on your next commits updating the branch and pinning the actions. |
Match the repo convention of pinning third-party actions to a commit SHA with a version comment, reusing the SHAs already in use elsewhere in the workflow (checkout v5, download-artifact v6, setup-uv v7, upload-artifact v5). Signed-off-by: Bruno Verachten <[email protected]>
Mirror the existing wheel job: set persist-credentials: false on checkout, enable-cache: false on setup-uv, and pass the sdist path via a PACKAGE_DIR env var instead of expanding inputs.sdist-name directly in the run script (zizmor template-injection finding). Signed-off-by: Bruno Verachten <[email protected]>
|
Pushed: pinned the four actions in the riscv64 job to the same SHAs already used elsewhere in the workflow. I also hit two zizmor findings in that job that were failing the capability policy check (the |
The riscv64 job failed at 'which uv' inside the build container: setup-uv installs uv on the runner, but cibuildwheel 3.2.1 does not provision uv inside the riscv64 container, so the default build[uv] frontend cannot find it. Use the plain build (pip) frontend for riscv64. Also mirror the main job's CIBW_SKIP so musllinux is only built on release, matching the other architectures. Signed-off-by: Bruno Verachten <[email protected]>
|
Pushed the fix and validated it on a real riscv64 board (SpaceMiT K1) before bothering you with a CI run: building |
Replace the separate riscv64 wheel job with a matrix member, now that cibuildwheel 4.x treats riscv64 as a first-class target (manylinux_2_39 and musllinux_1_2 riscv64 images are pinned upstream). The build[uv] frontend and musllinux skips no longer need riscv64-specific overrides, so the entry mirrors the other native Linux runners. Builds run natively on the ubuntu-24.04-riscv runner. Signed-off-by: Bruno Verachten <[email protected]>
|
Done, thanks. Extended the matrix instead of the separate job: - target: Linux (riscv64)
os: ubuntu-24.04-riscv
archs: riscv64With cibuildwheel 4.x it needed no riscv64-specific overrides, so the diff against main is just that row. I ran it on a native riscv64 runner on my fork to check it actually builds and tests: 6 wheels (cp310-cp314 plus cp314t free-threaded), test suite green per wheel. Run: https://github.com/gounthar/msgspec/actions/runs/27937959213 One thing I'm unsure about: as a matrix member it now runs on every PR via the RISE ubuntu-24.04-riscv pool, same as the aarch64 runner. You have more context on how much CI budget you want to spend there. Happy to keep it on the default path, or gate it behind a label or release-only if you'd prefer. |
|
It now fails the release CI :) |
) The 0.22.0 release run died on `cp310-musllinux_riscv64` with `Command ['which', 'uv'] failed with code 1`, from my riscv64 matrix entry. All six manylinux riscv64 wheels in that same job built, repaired and tested fine, so it is specific to the musllinux image. pypa/manylinux used to skip installing uv in the musllinux riscv64 image. At the commit the pinned image was built from (`8dbfa8e`, 4 June), `docker/build_scripts/finalize.sh` had: ``` musllinux*_riscv64-uv) continue;; # uv doesn't provide musl riscv64 wheels due to Rust issues ``` That was removed by pypa/manylinux#1969 on 11 July, and their image test suite now runs `uv venv` / `uv pip install` inside musllinux riscv64. The comment was already out of date when it was written: uv has published a `manylinux_2_31_riscv64.musllinux_1_1_riscv64` wheel since 0.11.18 (1 June). The pins differ across cibuildwheel releases: | cibuildwheel | `musllinux_1_2_riscv64` image | uv present | |---|---|---| | 4.1.0 | `2026.06.04-1` | no | | 4.2.0 | `2026.08.04-1` | yes | So `build-frontend = "build[uv]"` has nothing to find on 4.1.0. This bumps both `uvx --from` pins to 4.2.0. 4.2.0 builds CPython 3.15 by default, but `.cibuildwheel.toml` sets an explicit `build = "cp310-* cp311-* cp312-* cp313-* cp314-* cp314t-*"` list, so no new target appears. The rest of that release is Pyodide 314.0.4, a GraalPy pip hold, and download retries. I have not run this. The reasoning is from the failing job log and the manylinux source, not from a green build. CI here will not test it either unless someone adds the `build-riscv64` label, since the riscv64 job is excluded on unlabelled PRs, and even then `CIBW_SKIP` drops musllinux outside a release build. So the honest state is that only a release run proves it. If you would rather not move cibuildwheel right before a release, `musllinux-riscv64-image` in `.cibuildwheel.toml` pins just that one image and leaves everything else alone. Two things worth knowing either way: The riscv64 job is gated behind the `build-riscv64` label on PRs, and `CIBW_SKIP` drops musllinux unless `build-all` is set. So nothing had ever exercised musllinux riscv64 until the release turned `build-all` on. Whatever you decide here, that combination will keep hiding riscv64 musllinux breakage until release day. Dependabot covers `github-actions` and `uv`, but the cibuildwheel version is an inline `uvx --from` argument rather than a tracked dependency, which is why it sat at 4.1.0. Moving it into the uv ecosystem somehow would stop the next one, though I do not have a clean suggestion for how. Refs msgspec#1134, msgspec#987 Signed-off-by: Bruno Verachten <[email protected]> Co-authored-by: sobolevn <[email protected]>
Summary
Add
linux_riscv64wheels to the build matrix.Changes
ubuntu-latest) tobuild-wheels.ymlEvidence
Context
manylinux_2_28_riscv64is available in pypa/manylinuxRef: #986
Note: this work is part of the RISE Project effort to improve Python ecosystem support on riscv64 platforms. Native riscv64 CI runners are available for free via RISE RISC-V runners.