Skip to content
Open
149 changes: 149 additions & 0 deletions .github/workflows/build-skia-python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# This workflow is based on the `build_wheels` job of:
# https://github.com/kyamagu/skia-python/blob/v144.0.post2/.github/workflows/ci.yml
name: Build skia-python wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'skia-python version to build (git tag without leading v, e.g. 144.0.post2)'
required: true
default: '144.0.post2'
pull_request:
paths:
- '.github/workflows/build-skia-python.yml'
- 'patches/skia-python/**'

concurrency:
group: ${{ github.workflow }}-${{ inputs.version || '144.0.post2' }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read # to fetch code (actions/checkout)

env:
# `inputs.version` is empty on pull_request events; default to 144.0.post2 there.
SKIA_PYTHON_VERSION: ${{ inputs.version || '144.0.post2' }}
GN_REVISION: 0625da07ece7b1a374ec0719b6f40ebe3d0950c1
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64

jobs:
setup:
uses: $/.github/workflows/_setup.yml

build_wheels:
needs: [setup]
name: Build skia-python ${{ inputs.version || '144.0.post2' }} cp312/cp313/cp314/cp314t-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 1440 # full Skia (Chromium's 2D graphics engine) compile from source

steps:
- name: Checkout skia-python v${{ env.SKIA_PYTHON_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: kyamagu/skia-python
ref: v${{ env.SKIA_PYTHON_VERSION }}
submodules: true
persist-credentials: false

- name: Checkout python-wheels
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: python-wheels
persist-credentials: false

- name: Patch skia-python source
run: git apply python-wheels/patches/skia-python/${{ env.SKIA_PYTHON_VERSION }}/*.patch

- name: Build wheels
uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
with:
output-dir: wheelhouse/
env:
CIBW_ARCHS: riscv64
CIBW_BUILD: cp312-manylinux_riscv64 cp313-manylinux_riscv64 cp314-manylinux_riscv64 cp314t-manylinux_riscv64
CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }}
CIBW_SKIP: "*musllinux*"
# Reimplements scripts/build_Linux.sh: its `yum install ... ninja-build gn`
# comes from EPEL, which the riscv64 manylinux image doesn't carry (gotcha
# 51), so ninja comes from its own riscv64 PyPI wheel and gn is bootstrapped
# from source (gn.googlesource.com/gn's build/gen.py has no arch-specific
# logic). Everything after that mirrors the upstream script unchanged.
CIBW_BEFORE_ALL_LINUX: |
set -e
dnf install -y --enablerepo=crb \
fontconfig-devel mesa-libGL-devel mesa-libEGL-devel libglvnd-devel \
mesa-libGLES-devel libglvnd mesa-libGLES mesa-libEGL libglvnd-egl \
mesa-dri-drivers expat-devel libxml2-devel libstdc++-static
python3 -m pip install ninja
# Full clone, not --depth 1: build/gen.py's GenerateLastCommitPosition
# runs `git describe --match initial-commit` unconditionally whenever
# .git exists, which needs the full history back to that root tag.
git clone https://gn.googlesource.com/gn /tmp/gn
git -C /tmp/gn checkout ${{ env.GN_REVISION }}
# build/gen.py defaults CXX to clang++ on any non-MSVC/AIX/Windows
# host when CXX isn't set; this image has no clang, only GCC.
# --allow-warnings: gn's own source only gets clang -Werror
# coverage upstream; GCC's -Wcomment fires on an ASCII-art
# diagram in a doc-comment (src/gn/header_checker.h).
(cd /tmp/gn && CXX=g++ python3 build/gen.py --allow-warnings && ninja -C out gn)
install -m 0755 /tmp/gn/out/gn /usr/local/bin/gn
# BUILD.gn's `skia.h` action hardcodes rebase_path("//bin/gn") --
# the path our patched bin/fetch-gn would have populated -- so PATH
# alone isn't enough; put the same binary there too.
install -D -m 0755 /tmp/gn/out/gn skia/bin/gn
cd skia
patch -R -p1 < ../patch/0001-Make-SkPath-immutable-on-GN-build.patch
patch -R -p1 < ../patch/0001-Disable-legacy-PNG-encoding-decoding-in-SkPicture.patch
patch -p1 < ../patch/skia-m144-minimize-download.patch
patch -p1 < ../patch/skia-m132-colrv1-freetype.diff
patch -p1 < ../patch/skia-m132-egl-runtime.diff
python3 tools/git-sync-deps
gn gen out/Release --args="
is_official_build=true
skia_enable_svg=true
skia_use_vulkan=true
skia_use_system_libjpeg_turbo=false
skia_use_system_libwebp=false
skia_use_system_libpng=false
skia_use_system_icu=false
skia_use_system_harfbuzz=false
skia_use_system_freetype2=false
extra_cflags_cc=[\"-frtti\"]
extra_ldflags=[\"-lrt\"]
"
ninja -C out/Release
cd ..
CIBW_BEFORE_BUILD: pip install pybind11 numpy
CIBW_ENVIRONMENT: PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
CIBW_TEST_REQUIRES: pytest pillow glfw
# moderngl (used only by test_GrDirectContext_MakeGL_EGL) has no riscv64 wheel;
# test_GrDirectContext_MakeGL_GLX has no display-unavailable skip at all, unlike
# the fixture-based 'gpu'-parametrized tests 0001-*.patch covers. No xvfb-run:
# Rocky 10 ships no Xvfb (nor Xvnc/weston) at all, so there's no display to run
# one under. -k, not --deselect {project}/...: gotcha 14 -- a path-based
# --deselect silently no-ops since pytest reports nodeids relative to its
# rootdir, not the absolute {project} path.
CIBW_TEST_COMMAND: >-
python -m pytest {project}/tests
-k "not test_GrDirectContext_MakeGL_EGL and not test_GrDirectContext_MakeGL_GLX"
CIBW_REPAIR_WHEEL_COMMAND_LINUX: "auditwheel repair --exclude libEGL.so.1 -w {dest_dir} {wheel}"

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: skia-python-${{ env.SKIA_PYTHON_VERSION }}-manylinux_riscv64
path: wheelhouse/*.whl
if-no-files-found: error

publish:
name: Publish skia-python ${{ inputs.version || '144.0.post2' }}
needs: [setup, build_wheels]
permissions:
contents: write
pull-requests: write
uses: $/.github/workflows/_publish-wheel.yml
with:
artifact-pattern: skia-python-${{ inputs.version || '144.0.post2' }}-*-manylinux_riscv64
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
From ba4f377e460a8611d1ca7ff6015306913bcfd714 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Thu, 10 Sep 2026 13:07:37 +0200
Subject: [PATCH] Skip GPU-context tests when glfw.init() fails, not just when
glfw is missing

opengl_context's try/except only catches ImportError (glfw not
installed) and UserWarning; glfw_context() raises a plain RuntimeError
when glfw.init() returns False, which happens whenever no display
server (X11/Wayland) is reachable, not only when glfw itself is
absent. That RuntimeError propagates uncaught, erroring every test
that depends on the 'gpu'-parametrized surface/context fixtures
instead of the graceful pytest.skip('OpenGL is not available') the
rest of this fixture already falls back to.

Rocky Linux 10 (the riscv64 manylinux image's base) ships no virtual
X server at all: xorg-x11-server-Xvfb, tigervnc-server and weston are
all absent from BaseOS/AppStream/CRB/Extras on every architecture, not
just riscv64 (RHEL 10 dropped Xvfb outright, same shape as its SDL2
removal) -- so no DISPLAY is ever available to build a real GL context
in that container, and every test using 'gpu' hits this exact path.

Upstream-Status: Inappropriate [Rocky 10, this repo's riscv64 CI base image, ships no Xvfb/Xvnc/weston at all -- specific to our infra, not a defect other environments with a display server would hit]
---
tests/conftest.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/conftest.py b/tests/conftest.py
index 0658d2a..af22987 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -46,7 +46,7 @@ def opengl_context(request):
return
except ImportError:
logger.warning('glfw not found')
- except UserWarning as e:
+ except (RuntimeError, UserWarning) as e:
logger.exception(e)
pytest.skip('GLFW error')

--
2.50.1 (Apple Git-155)

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
From 7a8dff07c5fd64343665d6b7d17994d6ee3a5b87 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Thu, 10 Sep 2026 17:30:42 +0200
Subject: [PATCH] Skip the CIPD gn download when a gn is already on PATH

bin/fetch-gn's CPU map (platform.machine().lower() -> {aarch64,
amd64, arm64, x86_64}) has no riscv64 entry, and there's no prebuilt
gn published for riscv64 on Google's CIPD to add one for -- so this
KeyErrors outright on any riscv64 host, unconditionally, with no way
to opt out (tools/git-sync-deps calls it with no skip flag, unlike
its --skip-emsdk sibling). When a gn is already resolvable via PATH,
skip the download entirely instead of trying to fetch one: nothing
downstream of git-sync-deps reads skia/bin/gn or third_party/gn/gn
directly, they're only ever invoked through PATH.

Upstream-Status: Inappropriate [riscv64 has no prebuilt gn published on Google's CIPD to add a CPU-map entry for; this only ever fires when PATH already resolves one, which is specific to how this repo builds skia-python for that architecture]
---
bin/fetch-gn | 3 +++
1 file changed, 3 insertions(+)

diff --git a/skia/bin/fetch-gn b/skia/bin/fetch-gn
index ebdc31c4..c425d70e 100755
--- a/skia/bin/fetch-gn
+++ b/skia/bin/fetch-gn
@@ -17,6 +17,9 @@ from urllib.request import urlopen

os.chdir(os.path.join(os.path.dirname(__file__), os.pardir))

+if shutil.which('gn'):
+ sys.exit(0)
+
gnzip = os.path.join(tempfile.mkdtemp(), 'gn.zip')
with open(gnzip, 'wb') as f:
OS = {'darwin': 'mac', 'linux': 'linux', 'linux2': 'linux', 'win32': 'windows'}[sys.platform]
--
2.50.1 (Apple Git-155)