|
| 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. |
0 commit comments