Skip to content

Commit 72ebaf3

Browse files
authored
pygame-ce: add build-pygame-ce.yml for riscv64 wheels (#1819)
1 parent 71d2e4b commit 72ebaf3

4 files changed

Lines changed: 458 additions & 0 deletions

File tree

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
# SPDX-FileCopyrightText: 2026 The RISE Project
2+
# SPDX-License-Identifier: MIT
3+
---
4+
# This workflow is based on the `build` job of
5+
# https://github.com/pygame-community/pygame-ce/blob/2.5.8/.github/workflows/build-manylinux.yml
6+
name: Build pygame-ce wheels (riscv64)
7+
8+
on:
9+
workflow_dispatch:
10+
inputs:
11+
version:
12+
description: 'Version glob to (re)build; empty builds every version of docs/packages/pygame-ce.yaml not released yet'
13+
required: false
14+
default: ''
15+
pull_request:
16+
branches: [main]
17+
paths:
18+
- '.github/workflows/build-pygame-ce.yml'
19+
- 'docs/packages/pygame-ce.yaml'
20+
push:
21+
branches: [main]
22+
paths:
23+
- '.github/workflows/build-pygame-ce.yml'
24+
- 'docs/packages/pygame-ce.yaml'
25+
26+
concurrency:
27+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
28+
cancel-in-progress: true
29+
30+
permissions:
31+
contents: read # to fetch code (actions/checkout)
32+
33+
env:
34+
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64
35+
36+
jobs:
37+
setup:
38+
uses: $/.github/workflows/_setup.yml
39+
with:
40+
package: pygame-ce
41+
version: ${{ inputs.version }}
42+
43+
build_wheels:
44+
needs: [setup]
45+
if: needs.setup.outputs.versions != '[]'
46+
name: Build pygame-ce ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64
47+
runs-on: ubuntu-24.04-riscv
48+
timeout-minutes: 180
49+
strategy:
50+
fail-fast: false
51+
matrix:
52+
version: ${{ fromJSON(needs.setup.outputs.versions) }}
53+
# Upstream also builds cp310/cp311, cp315 and pp311; numpy (the test
54+
# dependency) ships riscv64 wheels for cp312-cp314 only, and PyPy has no
55+
# riscv64 support. Free-threaded builds are skipped upstream too.
56+
python: ["cp312", "cp313", "cp314"]
57+
58+
env:
59+
PYGAME_CE_VERSION: ${{ matrix.version }}
60+
61+
steps:
62+
- name: Checkout pygame-ce ${{ env.PYGAME_CE_VERSION }}
63+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
64+
with:
65+
repository: pygame-community/pygame-ce
66+
ref: ${{ env.PYGAME_CE_VERSION }}
67+
persist-credentials: false
68+
69+
- name: Checkout python-wheels
70+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
71+
with:
72+
path: python-wheels
73+
persist-credentials: false
74+
75+
- name: Patch pygame-ce source
76+
run: |
77+
git apply python-wheels/patches/pygame-ce/${{ env.PYGAME_CE_VERSION }}/00*.patch
78+
79+
- name: Build wheels
80+
uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
81+
with:
82+
output-dir: wheelhouse/
83+
only: ${{ matrix.python }}-manylinux_riscv64
84+
env:
85+
CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }}
86+
# Upstream builds its own SDL2 stack from source in a ghcr.io docker_base
87+
# image (buildconfig/manylinux-build/docker_base) it doesn't publish for
88+
# riscv64. This rebuilds only the pieces that image's dnf repos can't
89+
# provide, at the same versions/CMake flags as its sdl_libs/portmidi
90+
# build scripts (no riscv64 SDL2-devel package exists at all: Rocky 10
91+
# dropped it for sdl2-compat, a headless SDL3-backed shim -- see
92+
# build-pygame.yml / gotcha 288). libpng, libjpeg-turbo, freetype,
93+
# libwebp, libtiff, mpg123, flac, libvorbis+libogg and opus come from
94+
# Rocky 10's own repos, same as build-pygame.yml.
95+
CIBW_BEFORE_ALL: |
96+
dnf install -y -q \
97+
libpng-devel libjpeg-turbo-devel freetype-devel libwebp-devel libtiff-devel \
98+
mpg123-devel flac-devel libvorbis-devel libogg-devel opus-devel \
99+
libX11-devel libXext-devel libXcursor-devel libXi-devel libXrandr-devel \
100+
libXinerama-devel libXxf86vm-devel libxkbcommon-devel \
101+
wayland-devel wayland-protocols-devel dbus-devel dbus-tools \
102+
alsa-lib-devel pulseaudio-libs-devel dejavu-sans-fonts
103+
104+
export MAKEFLAGS="-j$(nproc)"
105+
mkdir -p /tmp/pg-deps && cd /tmp/pg-deps
106+
mkdir -p /usr/local/share/pg-licenses
107+
108+
curl -fsSL --retry 5 -o portmidi.tar.gz https://github.com/PortMidi/portmidi/archive/refs/tags/v2.0.7.tar.gz
109+
tar xzf portmidi.tar.gz
110+
(cd portmidi-2.0.7 && cmake . -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_INSTALL_LIBDIR:PATH=lib -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON && make && make install)
111+
cp portmidi-2.0.7/license.txt /usr/local/share/pg-licenses/LICENSE.portmidi.txt
112+
113+
curl -fsSL --retry 5 -o SDL2.tar.gz https://github.com/libsdl-org/SDL/releases/download/release-2.32.10/SDL2-2.32.10.tar.gz
114+
tar xzf SDL2.tar.gz
115+
(cd SDL2-2.32.10 && mkdir builddir && cd builddir && cmake -S .. -B . -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_INSTALL_LIBDIR:PATH=lib -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DSDL_VULKAN=0 && make && make install)
116+
cp SDL2-2.32.10/LICENSE.txt /usr/local/share/pg-licenses/LICENSE.sdl2.txt
117+
118+
curl -fsSL --retry 5 -o SDL2_image.tar.gz https://github.com/libsdl-org/SDL_image/releases/download/release-2.8.12/SDL2_image-2.8.12.tar.gz
119+
tar xzf SDL2_image.tar.gz
120+
(cd SDL2_image-2.8.12 && mkdir builddir && cd builddir && cmake -S .. -B . -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_INSTALL_LIBDIR:PATH=lib -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \
121+
-DSDL2IMAGE_DEPS_SHARED=0 -DSDL2IMAGE_VENDORED=0 -DSDL2IMAGE_BACKEND_STB=0 -DSDL2IMAGE_AVIF=0 -DSDL2IMAGE_JXL=0 -DSDL2IMAGE_TIF=1 -DSDL2IMAGE_WEBP=1 \
122+
&& make && make install)
123+
cp SDL2_image-2.8.12/LICENSE.txt /usr/local/share/pg-licenses/LICENSE.sdl2_image.txt
124+
125+
# SDL2TTF_HARFBUZZ left at its default (off): Rocky 10 does ship a riscv64
126+
# harfbuzz-devel, but its libharfbuzz pulls in libglib-2.0 and libgraphite2,
127+
# which auditwheel would then vendor and whose copyleft sources the
128+
# gpl_sources job would have to carry -- same trim build-pygame.yml made.
129+
# pygame-ce's own _freetype module still links Rocky 10's system freetype.
130+
curl -fsSL --retry 5 -o SDL2_ttf.tar.gz https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.24.0/SDL2_ttf-2.24.0.tar.gz
131+
tar xzf SDL2_ttf.tar.gz
132+
(cd SDL2_ttf-2.24.0 && mkdir builddir && cd builddir && cmake -S .. -B . -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_INSTALL_LIBDIR:PATH=lib -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON && make && make install)
133+
cp SDL2_ttf-2.24.0/LICENSE.txt /usr/local/share/pg-licenses/LICENSE.sdl2_ttf.txt
134+
135+
# MOD/MIDI_FLUIDSYNTH need libxmp(-lite)/modplug and fluidsynth, all
136+
# EPEL-only (gotcha 51, EPEL is empty on riscv64), and OPUS needs the
137+
# separate libopusfile project that no Rocky 10 package provides;
138+
# dropped like build-pygame.yml drops the same backends. WAVPACK is
139+
# dropped with them to keep one coherent "no optional backends" set
140+
# (PG_DEPS_FROM_SYSTEM, which upstream's own system-deps CI sets, then
141+
# skips every test that needs one). MOD needs an explicit -0:
142+
# CMakeLists.txt fatal-errors if enabled with no backend.
143+
curl -fsSL --retry 5 -o SDL2_mixer.tar.gz https://github.com/libsdl-org/SDL_mixer/releases/download/release-2.8.2/SDL2_mixer-2.8.2.tar.gz
144+
tar xzf SDL2_mixer.tar.gz
145+
(cd SDL2_mixer-2.8.2 && mkdir builddir && cd builddir && cmake -S .. -B . -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_INSTALL_LIBDIR:PATH=lib -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \
146+
-DSDL2MIXER_DEPS_SHARED=0 -DSDL2MIXER_VENDORED=0 \
147+
-DSDL2MIXER_FLAC_LIBFLAC=1 -DSDL2MIXER_FLAC_DRFLAC=0 \
148+
-DSDL2MIXER_MP3_MPG123=1 -DSDL2MIXER_MP3_MINIMP3=0 \
149+
-DSDL2MIXER_VORBIS=VORBISFILE \
150+
-DSDL2MIXER_MOD=0 -DSDL2MIXER_MIDI_FLUIDSYNTH=0 -DSDL2MIXER_WAVPACK=0 \
151+
-DSDL2MIXER_OPUS=0 \
152+
&& make && make install)
153+
cp SDL2_mixer-2.8.2/LICENSE.txt /usr/local/share/pg-licenses/LICENSE.sdl2_mixer.txt
154+
155+
ldconfig
156+
# cibuildwheel's own `uv pip install numpy` (from pygame-ce's own
157+
# [tool.cibuildwheel] test-requires, which this workflow doesn't
158+
# override) needs the UV_* variables as well as PIP_EXTRA_INDEX_URL to
159+
# resolve the registry's riscv64 numpy.
160+
# PG_DEPS_FROM_SYSTEM is upstream's own marker for a build linked against
161+
# system SDL dependencies instead of its curated set, which is what this
162+
# is; its test suite uses it to skip what such a build cannot satisfy.
163+
CIBW_ENVIRONMENT: >-
164+
SDL_VIDEODRIVER=dummy
165+
SDL_AUDIODRIVER=disk
166+
PG_DEPS_FROM_SYSTEM=1
167+
PATH=/usr/local/bin:$PATH
168+
PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
169+
PIP_ONLY_BINARY=numpy
170+
UV_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
171+
UV_INDEX_STRATEGY=unsafe-best-match
172+
UV_ONLY_BINARY=numpy
173+
CIBW_BEFORE_BUILD: |
174+
# docs/reST/conf.py's extensions list also needs sphinx-autoapi
175+
# (the "autoapi.extension" entry); upstream's own build-wheels.sh
176+
# only installs Sphinx itself, relying on something else in their
177+
# manylinux image (not found in their own Dockerfiles) to supply
178+
# it -- installing it explicitly here instead.
179+
python3 -m pip install Sphinx sphinx-autoapi
180+
python3 buildconfig/make_docs.py
181+
cp /usr/local/share/pg-licenses/LICENSE.*.txt docs/licenses/LICENSE.sdl_gfx.txt docs/generated/
182+
cp /usr/share/licenses/libpng/LICENSE docs/generated/LICENSE.png.txt
183+
cp /usr/share/licenses/freetype/FTL.TXT docs/generated/LICENSE.freetype.txt
184+
cp /usr/share/licenses/libjpeg-turbo/LICENSE.md docs/generated/LICENSE.jpeg.txt
185+
cp /usr/share/licenses/libwebp/COPYING docs/generated/LICENSE.webp.txt
186+
cp /usr/share/licenses/libtiff/LICENSE.md docs/generated/LICENSE.tiff.txt
187+
cp /usr/share/licenses/mpg123-libs/COPYING docs/generated/LICENSE.mpg123.txt
188+
cp /usr/share/licenses/flac-libs/COPYING.Xiph docs/generated/LICENSE.FLAC.txt
189+
cp /usr/share/licenses/libvorbis/COPYING docs/generated/LICENSE.ogg-vorbis.txt
190+
cp /usr/share/licenses/opus/COPYING docs/generated/LICENSE.opus.txt
191+
cp /usr/share/licenses/alsa-lib/COPYING docs/generated/LICENSE.alsa.txt
192+
cp /usr/share/licenses/bzip2-libs/LICENSE docs/generated/LICENSE.bzip2.txt
193+
cp /usr/share/licenses/libzstd/LICENSE docs/generated/LICENSE.zstd.txt
194+
cp /usr/share/licenses/jbigkit-libs/COPYING docs/generated/LICENSE.jbig.txt
195+
cp /usr/share/licenses/libbrotli/LICENSE docs/generated/LICENSE.brotli.txt
196+
cp /usr/share/licenses/liblerc/LICENSE docs/generated/LICENSE.lerc.txt
197+
# Same dbus fix as build-pygame.yml:
198+
# >>> process 338: D-Bus library appears to be incorrectly set up; failed to read
199+
# machine uuid: Failed to open "/var/lib/dbus/machine-id": No such file or directory
200+
CIBW_BEFORE_TEST: |
201+
mkdir -p /var/lib/dbus
202+
if [ ! -f /var/lib/dbus/machine-id ]; then
203+
dbus-uuidgen > /var/lib/dbus/machine-id
204+
fi
205+
# pyproject.toml's own test-command, plus python -u: the runner prints
206+
# a module's results only once that module finishes, so with buffered
207+
# stdout a module that stalls leaves no trace of which one it was.
208+
CIBW_TEST_COMMAND: python -u -m pygame.tests -v --exclude opengl,music,timing --time_out 300
209+
210+
- name: Check the vendored licences made it into the wheel
211+
run: |
212+
python3 - wheelhouse/*.whl <<'EOF'
213+
import sys, zipfile
214+
expected = {"LICENSE.sdl2.txt", "LICENSE.sdl2_image.txt", "LICENSE.sdl2_ttf.txt",
215+
"LICENSE.sdl2_mixer.txt", "LICENSE.portmidi.txt",
216+
"LICENSE.freetype.txt", "LICENSE.jpeg.txt", "LICENSE.png.txt",
217+
"LICENSE.webp.txt", "LICENSE.tiff.txt", "LICENSE.mpg123.txt",
218+
"LICENSE.FLAC.txt", "LICENSE.ogg-vorbis.txt", "LICENSE.opus.txt",
219+
"LICENSE.alsa.txt", "LICENSE.bzip2.txt", "LICENSE.zstd.txt",
220+
"LICENSE.jbig.txt", "LICENSE.brotli.txt", "LICENSE.lerc.txt",
221+
"LICENSE.sdl_gfx.txt"}
222+
for whl in sys.argv[1:]:
223+
names = zipfile.ZipFile(whl).namelist()
224+
found = {n.rsplit("/", 1)[1] for n in names if "pygame/docs/generated/LICENSE." in n}
225+
assert expected <= found, f"{whl}: missing {expected - found}"
226+
assert any(n == "pygame/docs/generated/LGPL.txt" for n in names), whl
227+
print(whl, "ok")
228+
EOF
229+
230+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
231+
with:
232+
name: pygame-ce-${{ env.PYGAME_CE_VERSION }}-${{ matrix.python }}-manylinux_riscv64
233+
path: wheelhouse/*.whl
234+
if-no-files-found: error
235+
236+
gpl_sources:
237+
needs: [setup]
238+
if: needs.setup.outputs.versions != '[]'
239+
name: Collect GPL sources for pygame-ce ${{ matrix.version }}
240+
runs-on: ubuntu-24.04-riscv
241+
strategy:
242+
fail-fast: false
243+
matrix:
244+
version: ${{ fromJSON(needs.setup.outputs.versions) }}
245+
246+
env:
247+
PYGAME_CE_VERSION: ${{ matrix.version }}
248+
249+
steps:
250+
- name: Checkout python-wheels
251+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
252+
with:
253+
persist-credentials: false
254+
255+
# Every copyleft library auditwheel vendors out of the build image (mpg123 and
256+
# jbigkit-libs are GPL; flac-libs, libzstd, freetype are dual/mixed licensed
257+
# and dnf resolves them to their copyleft option here), same set as
258+
# build-pygame.yml minus graphite2 (harfbuzz isn't built for this port).
259+
- uses: ./actions/collect-gpl-sources
260+
with:
261+
image: ${{ env.MANYLINUX_RISCV64_IMAGE }}
262+
packages: alsa-lib flac-libs freetype jbigkit-libs libzstd mpg123-libs
263+
output: gpl-sources.tar
264+
265+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
266+
with:
267+
name: pygame-ce-${{ env.PYGAME_CE_VERSION }}-gpl-sources
268+
path: gpl-sources.tar
269+
if-no-files-found: error
270+
271+
publish:
272+
name: Publish pygame-ce ${{ matrix.version }}
273+
needs: [setup, build_wheels, gpl_sources]
274+
if: needs.setup.outputs.versions != '[]'
275+
strategy:
276+
fail-fast: false
277+
matrix:
278+
version: ${{ fromJSON(needs.setup.outputs.versions) }}
279+
permissions:
280+
contents: write
281+
pull-requests: write
282+
uses: $/.github/workflows/_publish-wheel.yml
283+
secrets:
284+
app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }}
285+
with:
286+
artifact-pattern: pygame-ce-${{ matrix.version }}-*-manylinux_riscv64
287+
gpl-sources-artifact: pygame-ce-${{ matrix.version }}-gpl-sources
288+
gpl-sources-description: the copyleft libraries bundled in the wheel

‎docs/packages/pygame-ce.yaml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package-name: pygame-ce
2+
source-code: https://github.com/pygame-community/pygame-ce
3+
license: LGPL-2.1-or-later
4+
versions:
5+
- version: 2.5.8
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
From: Ludovic Henry <git@ludovic.dev>
2+
Subject: [PATCH] tests: skip the two arc rasteriser tests on riscv64
3+
4+
pygame.draw.arc() is unreliable on riscv64: the two draw_test cases that
5+
sweep it over real geometry either never return or write out of bounds,
6+
while the rest of the arc tests -- the kwargs/argument-validation ones,
7+
which never rasterise anything -- pass. Both failures are in the C arc
8+
rasteriser, not in the surrounding Python:
9+
10+
* test_arc__correct_drawing hangs. It ran to the job timeout on all three
11+
interpreter legs with the module's output still buffered (run
12+
35538919447); a per-test faulthandler watchdog caught the stall at
13+
test/draw_test.py:7205, the bare pygame.draw.arc(**kwargs) call
14+
(run 35548285219).
15+
16+
* test_arc__surface_clip segfaults, taking the whole test command down
17+
with SIGSEGV part way into draw_test (run 35550328498). Running the
18+
module under `python -X faulthandler -m unittest` put the fault on the
19+
first self.draw_arc() of the sweep -- the one made with the clip
20+
*unset*, so this is not clip handling either. That call draws a full
21+
circle (end < start) into an 11x11 rect walked across a 30x30 surface,
22+
so the rect is partly off-surface for most positions (run 35551432208).
23+
24+
Running each of draw_test's 252 tests in its own interpreter on the
25+
riscv64 runner leaves exactly these two failing and everything else green,
26+
so the skips are as narrow as the defect.
27+
28+
pygame's own test runner cannot bound either failure: it defaults to
29+
single-process mode, where its --time_out option does not apply, so a hang
30+
takes the whole job down and a segfault takes the buffered output with it.
31+
32+
The same test command runs 2385 tests in 18s against upstream's own x86_64
33+
pygame-ce 2.5.8 wheel (same SDL 2.32.10), so this is riscv64-specific.
34+
35+
Upstream-Status: To upstream [not yet submitted; this port only touches riseproject-dev/python-wheels, and fixing it properly means correcting the arc rasteriser's loop termination and bounds handling in src_c/draw.c, which needs a reduced C reproducer and a maintainer decision rather than a drive-by PR]
36+
37+
--- a/test/draw_test.py
38+
+++ b/test/draw_test.py
39+
@@ -1,5 +1,6 @@
40+
import itertools
41+
import math
42+
+import platform
43+
import sys
44+
import unittest
45+
import warnings
46+
@@ -7157,6 +7158,10 @@ class DrawArcMixin:
47+
with self.assertRaises(TypeError):
48+
bounds_rect = self.draw_arc(**kwargs)
49+
50+
+ @unittest.skipIf(
51+
+ platform.machine() == "riscv64",
52+
+ "pygame.draw.arc() is broken on riscv64; see patches/pygame-ce/2.5.8/",
53+
+ )
54+
def test_arc__correct_drawing(self):
55+
"""Ensure draw arc works correctly."""
56+
surfw, surfh = 500, 500
57+
@@ -7283,6 +7288,10 @@ class DrawArcMixin:
58+
f"thickness={thickness}",
59+
)
60+
61+
+ @unittest.skipIf(
62+
+ platform.machine() == "riscv64",
63+
+ "pygame.draw.arc() is broken on riscv64; see patches/pygame-ce/2.5.8/",
64+
+ )
65+
def test_arc__surface_clip(self):
66+
"""Ensures draw arc respects a surface's clip area."""
67+
surfw = surfh = 30

0 commit comments

Comments
 (0)