Skip to content

feat: add riscv64 to Linux wheel build matrix#987

Open
gounthar wants to merge 10 commits intojcrist:mainfrom
gounthar:feat/riscv64-wheels
Open

feat: add riscv64 to Linux wheel build matrix#987
gounthar wants to merge 10 commits intojcrist:mainfrom
gounthar:feat/riscv64-wheels

Conversation

@gounthar
Copy link
Copy Markdown

@gounthar gounthar commented Mar 12, 2026

Summary

Add linux_riscv64 wheels to the build matrix.

Changes

  • Add riscv64 matrix entry (ubuntu-latest) to build-wheels.yml
  • Add QEMU setup step for riscv64 emulation on x86_64 runners

Evidence

Context

  • manylinux_2_28_riscv64 is available in pypa/manylinux
  • cibuildwheel 3.x supports riscv64 via QEMU
  • Several packages already ship riscv64 wheels: aiohttp, yarl, multidict, propcache
  • RISC-V hardware is shipping (SiFive, SpacemiT, Sophgo SG2044)

Ref: #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.

Add QEMU emulation for riscv64 and include riscv64 in the build matrix
so that linux_riscv64 wheels are built alongside existing architectures.
@gounthar
Copy link
Copy Markdown
Author

Native build verified on riscv64 hardware (BananaPi F3, SpacemiT K1, rv64gc, GCC 14.2.0): SUCCESS.

Signed-off-by: Bruno Verachten <gounthar@gmail.com>
@gounthar
Copy link
Copy Markdown
Author

gounthar commented Apr 5, 2026

Updated docker/setup-qemu-action from v3 to v4 (pinned SHA ce360397).

Comment on lines +39 to +43
- name: Set up QEMU
if: matrix.archs == 'riscv64'
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
with:
platforms: riscv64
Copy link
Copy Markdown
Collaborator

@Siyet Siyet Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: riscv64

to the matrix above.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks. Fixed in the latest push.

Two changes beyond just adding the matrix entry:

  1. Replaced QEMU emulation with a native ubuntu-24.04-riscv runner (RISE Project free runners). Native is faster and more representative than emulation.

  2. The pypa/cibuildwheel action calls actions/setup-python internally, which has no riscv64 binaries. Added a dedicated riscv64 step that installs cibuildwheel via pip and runs it directly — this is the established workaround for native riscv64 CI.

@Siyet
Copy link
Copy Markdown
Collaborator

Siyet commented Apr 11, 2026

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 enable flags are needed - once the matrix is fixed it should "just work".

A few things on my mind, mostly around CI cost:

  1. Build time. .cibuildwheel.toml builds 7 interpreters (cp39 through cp314t). Under QEMU on x86_64 runners this can add a meaningful chunk of wall time per release, and I'd like to understand what we're signing up for. If you happen to have a rough number for build time per wheel under emulation, I'd love to see it - it'd help me make the call.

  2. Tests under emulation. Our cibuildwheel config runs the unit tests as part of the build (test-command = "pytest {project}/tests/unit"). Running pytest under QEMU roughly doubles the per-wheel time. I'm leaning toward setting CIBW_TEST_SKIP: "*-linux_riscv64" for this arch and not running the tests under emulation - QEMU is heavy enough as it is, and I'm not sure the extra confidence is worth the cost. Longer term, the RISE RISC-V runners app you mentioned with native runners sounds more interesting to me than emulation, but that's a separate conversation.

  3. Smoke test on current main. Your tested wheel was built against 0.19.0 / 0.20.1.dev3. We've had a few releases since (we're at 0.21.0 now) - before merging I'd feel better seeing a quick confirmation that current main also builds and a basic decode works. If you have a chance to rebuild, I'd appreciate it.

Happy to help with any of the above if it's easier, just let me know.

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 <gounthar@gmail.com>
@gounthar
Copy link
Copy Markdown
Author

gounthar commented Apr 11, 2026

Thanks for the review, @Siyet.

On the QEMU / build time question: the latest push drops QEMU and uses native ubuntu-24.04-riscv runners from the RISE Project instead. I don't have a msgspec-specific build time yet; the fork CI triggered on the last push should give a real number. My expectation is it'll be slower than amd64 but not as brutal as QEMU, though I could be wrong on that.

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 CIBW_TEST_SKIP is easy to do.

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.

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 <gounthar@gmail.com>
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 <gounthar@gmail.com>
@gounthar
Copy link
Copy Markdown
Author

Fork CI numbers are in: native build on ubuntu-24.04-riscv took 27 min 27 s for all 7 interpreters (cp39 through cp314t), tests included. Run: https://github.com/gounthar/msgspec/actions/runs/24290434391

The fork is 0 commits behind 0.21.0, so those are current-main numbers.

On tests: with native completing in under 30 minutes with tests on, I'd leave them enabled for now. Easy to add CIBW_TEST_SKIP later if release wall time becomes a concern.

Happy to adjust if the direction isn't right.

@Siyet
Copy link
Copy Markdown
Collaborator

Siyet commented Apr 11, 2026

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: .cibuildwheel.toml is not being loaded

In the Build options dump from https://github.com/gounthar/msgspec/actions/runs/24290434391, I'm seeing values that don't match what's in .cibuildwheel.toml:

test_command:
test_groups:
build_frontend: build      # expected "build[uv]"
build_config: *            # expected "cp39-* cp310-* ..."

These are all cibuildwheel defaults, not the values from the config. Confirmation by negation: there's not a single pytest or test session starts line in the logs - which means tests are not running on riscv64 in this build at all. Also, only 12 wheels are produced (cp310-cp314t × manylinux + musllinux), not 14 - cp39 is correctly filtered out by requires-python = ">=3.10" from pyproject.toml, which is the only piece of config that did get picked up.

The most likely cause is how CIBW_CONFIG_FILE: .cibuildwheel.toml is resolved when calling python3 -m cibuildwheel directly - the standard pypa/cibuildwheel@v3.2.1 action handles config-file differently and works correctly for the other architectures in the same matrix.

This is a serious issue: without the config loaded properly we'd be publishing riscv64 wheels we haven't even verified pass pytest tests/unit. That's a blocker for me - I really don't want to ship untested wheels on a new architecture.

Possible fixes:

  1. Pass an absolute path: CIBW_CONFIG_FILE: ${{ github.workspace }}/.cibuildwheel.toml
  2. Pass via CLI: python3 -m cibuildwheel --config-file "$GITHUB_WORKSPACE/.cibuildwheel.toml" ...
  3. Include .cibuildwheel.toml in the sdist (via MANIFEST.in) so it's available in package_dir

I'd try option 1 first - least invasive. After the fix it would be great to re-run the benchmark to get real numbers with tests included.

CI time is a real concern

This connects to the previous point - 27 minutes is the build without tests; with tests included the actual time will be noticeably longer.

For context, here are the wheel build timings from the last 0.21.0 release (run https://github.com/jcrist/msgspec/actions/runs/24155171921, full build including musllinux):

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

  1. cibuildwheel version is duplicated - currently pypa/cibuildwheel@v3.2.1 (the action) and pip 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.

  2. /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 up python3 via command -v python3 or similar.

  3. .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:

  1. Technical (on you): fix the .cibuildwheel.toml loading 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.

  2. Coordination (on me and the senior maintainers):

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).

- 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 <gounthar@gmail.com>
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 <gounthar@gmail.com>
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 <gounthar@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants