Skip to content

Commit b23b017

Browse files
authored
python-wheels: add a development guide (#159)
* docs: infrastructure.md: increase nav_order We want the Development Guide to appear before the Infrastructure page, so increment the nav_order value. Signed-off-by: Trevor Gamblin <tgamblin@baylibre.com> * docs: development.md: add Provide a development guide to the documentation covering best practices for: - Workflow creation and maintenance processes - Usage of RISE's RISC-V Runners for GHA - The Python version matrix used in builds - Use of the `uv` tool - Using `python-wheels` actions and other customizations in workflows - Testing new workflows - Handling problematic musl builds - When and how to include custom patches for a package's workflow - Building and publishing wheels to the RISE package registry - Miscellaneous workflow tips (e.g. handling license fields, runner toolchains, Rust-based modules, and including Upstream-Status tags in patches) Signed-off-by: Trevor Gamblin <tgamblin@baylibre.com>
1 parent 74b17f7 commit b23b017

2 files changed

Lines changed: 347 additions & 1 deletion

File tree

docs/development.md

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
---
2+
title: Development Guide
3+
layout: default
4+
nav_order: 6
5+
---
6+
7+
# Development Guide
8+
9+
Structurally, the [python-wheels](https://github.com/riseproject-dev/python-wheels) repository has four goals:
10+
11+
1. Provide a simple interface for users to install Python wheels from.
12+
2. Create GitHub Actions workflows for building binary Python wheels that
13+
closely match upstream projects' existing CI/CD, but which build and test
14+
only for riscv64.
15+
3. Add supplemental workflows and tooling to track upstream releases, automate
16+
version upgrades, and simplify deprecation once upstream projects incorporate
17+
riscv64 builds, allowing developers to focus on broader package support.
18+
4. Serve as a reference to upstream maintainers, indicating that their projects
19+
can easily add support for riscv64.
20+
21+
## Workflow Creation Process
22+
23+
`python-wheels` workflows should closely match those for the upstream project to
24+
ensure that our build process for riscv64 wheels is consistent and can be
25+
submitted to the upstream maintainers as evidence of feasibility. Unless
26+
otherwise noted, this guide will reference the existing `build-numpy.yml`
27+
workflow for example code.
28+
29+
The general process:
30+
31+
1. Review the upstream project's build and test workflows (they may be called
32+
`python.yml`, `wheel.yml`, `build.yml`, `release.yml`, or something else
33+
entirely), identifying the section(s) which build for Linux with glibc and
34+
musl.
35+
2. Create a copy of the upstream build workflow in the `python-wheels` repo at
36+
`.github/workflows/build-<package>.yml`, where `<package>` matches the
37+
project name (e.g. `build-numpy.yml` for NumPy`).
38+
3. Remove any workflow logic not related to the Linux glibc and musl (if
39+
present) build processes, support for other architectures and operating
40+
systems (e.g. Windows, Mac OS). This includes the sdist build (unless it is
41+
consumed by a build or test step).
42+
4. Repeat steps #2 and #3 for the corresponding test workflow, if it is separate
43+
from the upstream build file.
44+
45+
From this point, some customizations are required to enable builds targeting
46+
riscv64.
47+
48+
## Workflow Customizations for riscv64
49+
50+
### riscv64 Runners
51+
52+
We make use of the official [RISE RISC-V
53+
Runners](https://riscv-runners.riseproject.dev/) for any jobs which should run
54+
on a riscv64 platform, particularly build and test jobs. The `python-wheels`
55+
repository is already configured to access them. The `runs-on` directives in any
56+
new workflows should be changed like so:
57+
58+
```
59+
jobs:
60+
build:
61+
runs-on: ubuntu-24.04-riscv
62+
```
63+
64+
### Target Python Versions
65+
66+
Previously RISE has used a Python version matrix covering the four latest
67+
releases (`major.minor`, e.g. `3.14`), plus any freethreaded variants available
68+
(e.g. `3.14t`). As of July 14th, 2026, this includes Pythons 3.11, 3.12, 3.13,
69+
and 3.14 (along with 3.14t, the freethreaded equivalent). However, the NumPy
70+
project (as of version 2.5.0) supports Python 3.12 as the minimum. Since this
71+
package is fundamental to many others which we are supporting, we will follow
72+
its precedent when defining our version matrix. Some wheels have previously been
73+
built for 3.13t, but since this was an experimental version with limited support
74+
we avoid it now.
75+
76+
With these factors, our default version matrix becomes:
77+
78+
`['3.12', '3.13', '3.14', '3.14t']`
79+
80+
Exceptions may be necessary for some packages, which should be carefully
81+
considered to balance achieving similarity to upstream with feasibility of
82+
maintenance.
83+
84+
### uv
85+
86+
The official `actions/setup-python` Action does not yet support riscv64 builds,
87+
so workflows using it will fall back to using the host version (if one
88+
is present matching the `major.minor` numbering used by the workflow, e.g.
89+
`3.12`). A simple alternative is to replace any usage of `actions/setup-python`
90+
in the upstream workflow with `astral-sh/setup-uv` like so:
91+
92+
```
93+
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
94+
name: Install Python
95+
with:
96+
python-version: '3.12'
97+
activate-environment: true
98+
enable-cache: false
99+
```
100+
101+
Note the `python-version` 'activate-environment', and 'enable-cache' options.
102+
The first two allow us to select the environment Python and have it pre-enabled
103+
(matching `actions/setup-python` behaviour for our purposes). The `enable-cache`
104+
option is disabled for now, as it has caused failures in previous build
105+
attempts.
106+
107+
### Upstream Project Checkouts
108+
109+
We use the `actions/checkout` action to checkout the upstream repository at the
110+
desired tag:
111+
112+
```
113+
- name: Checkout numpy v${{ env.NUMPY_VERSION }}
114+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
115+
with:
116+
repository: numpy/numpy
117+
ref: v${{ env.NUMPY_VERSION }}
118+
submodules: true
119+
persist-credentials: false
120+
```
121+
122+
This effectively overwrites the default project layout for the workflow, which
123+
would otherwise be a copy of `python-wheels`. It allows our workflows to operate
124+
as if they are part of the upstream project without having to include them in a
125+
fork. More importantly, it is critical for uncomplicated usage of tools like
126+
cibuildwheel, which assumes that the root directory is the project to be built
127+
when invoked.
128+
129+
### Using the python-wheels Repository in Workflows
130+
131+
The `python-wheels` repository contains some custom Actions we require, and
132+
patch files to apply for certain projects. The most critical example is the
133+
`publish-to-gitlab` Action. With it in place, the `build-numpy.yml` script's
134+
`publish` job looks like this:
135+
136+
```
137+
publish:
138+
name: Publish numpy ${{ inputs.version || '2.5.0' }} to GitLab
139+
needs: build_wheels
140+
# Only publish when the workflow was triggered from main with a specific
141+
# version. Manual trigger is the only entry point, so checking the ref is
142+
# enough to gate uploads.
143+
if: github.ref == 'refs/heads/main'
144+
runs-on: ubuntu-latest
145+
permissions:
146+
contents: read
147+
148+
steps:
149+
- name: Download wheels
150+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
151+
with:
152+
pattern: numpy-${{ env.NUMPY_VERSION }}-*-manylinux_riscv64
153+
path: dist
154+
merge-multiple: true
155+
156+
- name: Publish to GitLab PyPI registry
157+
uses: riseproject-dev/python-wheels/actions/publish-to-gitlab@main
158+
with:
159+
gitlab-username: ${{ vars.GITLAB_DEPLOY_USER }}
160+
gitlab-token: ${{ secrets.GITLAB_DEPLOY_TOKEN }}
161+
gitlab-project-id: ${{ vars.GITLAB_PROJECT_ID }}
162+
files: |
163+
dist/*.whl
164+
```
165+
166+
Other workflows need to follow a similar process - checkout the `python-wheels`
167+
repo, and run the `publish-to-gitlab` action to upload built wheels to the RISE
168+
Python registry.
169+
170+
## Testing a New Workflow
171+
172+
Open a new draft PR with the workflow(s) included, and include a `Trigger:` line
173+
in the PR description with a version for each package version you want to build,
174+
like so:
175+
176+
`Trigger: numpy:v2.5.0`
177+
`Trigger: numpy:v2.5.1`
178+
179+
The repository's automation logic will pick up on and trigger the appropriate
180+
build workflows for each version. Achieving a passing (green) build may require
181+
several attempts including rework and possible patches, depending on the nature
182+
of the failure.
183+
184+
### Skipping musl Builds
185+
186+
While building for both glibc and musl (in cibuildwheel terms, `manylinux` and
187+
`musllinux`) is desirable, some of the projects we target do not build for
188+
musllinux (or they do, but run into various issues on riscv64 specifically), and
189+
so dependent packages cannot rely on musl versions of the packages either. If
190+
the musl builds fail without an obvious solution, strip those jobs from the
191+
workflow and retry, while opening an issue to track the musl incompatibility.
192+
193+
### Patching a Project
194+
195+
Some workflows may fail consistently when building or testing a module, despite
196+
following the guidelines above. When this occurs, consider whether the failure
197+
meets one of the following criteria:
198+
199+
1. The failure exercises a narrow part of the module's functionality, or relies
200+
on external resources (e.g. large downloads over the network)
201+
2. The failure is due to reliance on some other software unavailable on riscv64
202+
3. The failure is a consequence of an artificial test limitation, e.g. a maximum
203+
timeout
204+
4. The project's build scripts use host tooling which isn't available on the
205+
runners or in the riscv64 manylinux images (e.g. `apt` vs `dnf`)
206+
207+
Additionally, some packages may not fully incorporate all of the LICENSE files
208+
found in their repositories or as required by their dependencies. See the
209+
[Licensing](#licensing) section for more info on this topic. In these cases, it
210+
may be justified to add one or more patch files to remove these cases from the
211+
workflow. In this scenario, follow these steps:
212+
213+
1. Any such patches should be placed in a `patches/<package_name>/<version_tag>`
214+
path inside `python-wheels`.
215+
2. Each patch should include an `Upstream-Status` tag. See the [Upstream Status
216+
Tags](#upstream-status-tags) section for details on valid types.
217+
3. An extra step should be added to the build/test workflows before execution to
218+
use `git apply` to make necessary modifications to the project source.
219+
4. The change should be documented for the package, so that users are aware of
220+
modifications made.
221+
222+
**Note: Patching should be performed and reviewed on a case-by-case basis - as
223+
much functionality as possible should be tested by our system to ensure a smooth
224+
user experience when consuming wheels from RISE's package registry.**
225+
226+
## Releasing a Wheel
227+
228+
The `publish-to-gitlab` action does not run unless the workflow is triggered
229+
from main. This is intentional, and is meant to ensure that only those workflows
230+
which have been fully tested, reviewed, and merged are used to build and push
231+
packages. Following the merge of a PR, the workflow(s) must be re-triggered from
232+
the `main` branch in order to release the wheels to the package registry.
233+
234+
## Other Workflow Tips and Tricks
235+
236+
### Licensing
237+
238+
The wheels built by the `python-wheels` project use a variety of open-source
239+
licenses. Since RISE is the distributor of riscv64 wheels in the corresponding
240+
package registry, we must ensure that the wheels adhere to each project's
241+
licensing requirements. More specifically, check:
242+
243+
1. The built wheel contains one or more `LICENSE` files corresponding to those
244+
contained in the upstream project source.
245+
2. If the wheel ships any statically- or dynamically-linked libraries from other
246+
projects, the licensing requirements for those projects are also correctly
247+
addressed.
248+
249+
If either point is not met, we should follow the [Patching a
250+
Project](#patching-a-project) process for patching our build, and submit an
251+
issue and/or PR upstream to help them comply with license requirements as well.
252+
253+
### Adding Builds for Rust Packages
254+
255+
Modules which are cross-compiled from Rust to Python typically use
256+
[maturin](https://www.maturin.rs/). This greatly simplifies building binary
257+
wheels for riscv64, but there is a pitfall here to watch out for - many projects
258+
use a matrix definition looking like:
259+
260+
```
261+
matrix:
262+
platform:
263+
- runner: ubuntu-22.04
264+
target: x86_64
265+
- runner: ubuntu-22.04
266+
target: x86
267+
- runner: ubuntu-22.04
268+
target: aarch64
269+
- runner: ubuntu-22.04
270+
target: armv7
271+
- runner: ubuntu-22.04
272+
target: ppc64le
273+
```
274+
275+
For riscv64 and some other architectures, the `rustc` toolchain target name does
276+
not follow this simple pattern (i.e. the `arch` part of the triple is not exact):
277+
278+
```
279+
tgamblin@alchemist ~/workspace/baylibre/rise/python-wheels (tgamblin/dev-guide)$ rustup target list | grep riscv64
280+
riscv64a23-unknown-linux-gnu
281+
riscv64gc-unknown-linux-gnu
282+
riscv64gc-unknown-linux-musl
283+
riscv64gc-unknown-none-elf
284+
riscv64imac-unknown-none-elf
285+
```
286+
287+
Simply adding a new line with `target: riscv64` will lead to build failures. The
288+
recommended approach here is to make the matrix more explicit, then add riscv64,
289+
so that each entry looks like:
290+
291+
```
292+
- runner: ubuntu-24.04-riscv
293+
target: riscv64gc-unknown-linux-gnu
294+
arch: riscv64
295+
```
296+
297+
Note that doing so typically requires a tweak to an `Upload wheels` step or
298+
similar, so that it uses the `arch` field:
299+
300+
```
301+
- name: Upload wheels
302+
uses: actions/upload-artifact@v4
303+
with:
304+
name: wheels-linux-${{ matrix.platform.arch }}
305+
path: dist
306+
```
307+
308+
### GCC Version Mismatches
309+
310+
Some packages may require GCC 14 or later to compile for riscv64. If your build
311+
requires GCC 14, ensure that you are either using a cibuildwheel container
312+
approach, or (if the project doesn't use cibuildwheel) have an appropriate
313+
workaround in place, since the RISC-V runners currently ship GCC 13 by default.
314+
315+
### Upstream Status Tags
316+
317+
We use a pattern established by the [Yocto
318+
Project](https://docs.yoctoproject.org/dev/contributor-guide/recipe-style-guide.html#patch-upstream-status)
319+
for indicating the purpose and status of custom patches which we carry for
320+
various projects. Each tag should be specified in the patch file's commit
321+
message like so:
322+
323+
```
324+
Upstream-Status: <type> [reason and/or link]
325+
```
326+
327+
There are five valid `Upstream-Status` types for the `python-wheels` repository:
328+
329+
1. `Issue`: An issue has been opened on the upstream project to indicate a bug
330+
was found during build/test. The issue link should be included.
331+
2. `Submitted`: A change was submitted to fix an issue upstream, but we are
332+
carrying the patch ourselves until it's merged and released in a future
333+
version. Include a link to the upstream PR and/or merged commit.
334+
3. `To upstream`: The patch needs to be submitted upstream, but submission is
335+
blocked. Include an explanation of why it can't be submitted upstream yet.
336+
4. `Inappropriate`: The patch includes changes which are necessary for riscv64
337+
builds and/or our infrastructure, but are not relevant upstream. Include a
338+
short description of why.
339+
5. `Backport`: The patch includes a fix which is already merged in an upstream
340+
release version, but not in the version we're trying to build. Include a link
341+
and short description of the problem.
342+
343+
Including an `Upstream-Status` tag is a baseline requirement for custom patches,
344+
and it is automatically searched for by the `ci_scripts/check_patch.py` script
345+
when submitting a PR. However, any additional detail which can be provided in
346+
each patch's commit message is helpful for maintenance.

docs/infrastructure.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Infrastructure
33
layout: default
4-
nav_order: 6
4+
nav_order: 7
55
---
66

77
# Infrastructure

0 commit comments

Comments
 (0)