From 22f1821a67a21b01c4c6f4bd9ef1810cf16318c2 Mon Sep 17 00:00:00 2001 From: Bruno Verachten Date: Thu, 12 Mar 2026 10:00:20 +0100 Subject: [PATCH 01/12] feat: add riscv64 to Linux wheel build matrix Add QEMU emulation for riscv64 and include manylinux_2_28 riscv64 entry in the build-native-wheels matrix. --- .github/workflows/wheels.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ed49f15c0b8..ec0803de42e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -116,6 +116,12 @@ jobs: with: python-version: "3.x" + - name: Set up QEMU + if: matrix.cibw_arch == 'riscv64' + uses: docker/setup-qemu-action@v3 + with: + platforms: riscv64 + - name: Install cibuildwheel run: | python3 -m pip install -r .ci/requirements-cibw.txt From 7b026c67801c00b8a28b667aff912f6d9b2fe747 Mon Sep 17 00:00:00 2001 From: Bruno Verachten Date: Thu, 12 Mar 2026 22:37:33 +0100 Subject: [PATCH 02/12] feat(riscv64): add riscv64 to build-native-wheels matrix --- .github/workflows/wheels.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ec0803de42e..809e4dd32d1 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -94,6 +94,11 @@ jobs: os: ubuntu-24.04-arm cibw_arch: aarch64 build: "*manylinux*" + - name: "manylinux_2_28 riscv64" + platform: linux + os: ubuntu-latest + cibw_arch: riscv64 + build: "cp3{12,13}-manylinux*" - name: "iOS arm64 device" platform: ios os: macos-latest From 816fe45896ae3f9778862496f817c1b13b320a33 Mon Sep 17 00:00:00 2001 From: Bruno Verachten Date: Sat, 28 Mar 2026 09:32:09 +0100 Subject: [PATCH 03/12] ci: use manylinux_2_39 for riscv64 (2_28 has no riscv64 image) --- .github/workflows/wheels.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 74070208c28..1543436dd4d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -94,11 +94,12 @@ jobs: os: ubuntu-24.04-arm cibw_arch: aarch64 build: "*manylinux*" - - name: "manylinux_2_28 riscv64" + - name: "manylinux_2_39 riscv64" platform: linux os: ubuntu-latest cibw_arch: riscv64 build: "cp3{12,13}-manylinux*" + manylinux: "manylinux_2_39" - name: "iOS arm64 device" platform: ios os: macos-latest @@ -142,6 +143,7 @@ jobs: CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} From d5e42ac963e582b8a6d5967a8c2969a9a3c31969 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:22:20 +1100 Subject: [PATCH 04/12] Update docker/setup-qemu-action --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 608d218cd30..40ed4814f44 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -124,7 +124,7 @@ jobs: - name: Set up QEMU if: matrix.cibw_arch == 'riscv64' - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 with: platforms: riscv64 From 0ccd26a38ba9a8f44271a592a4c5b24815813441 Mon Sep 17 00:00:00 2001 From: Bruno Verachten Date: Sun, 5 Apr 2026 11:57:21 +0200 Subject: [PATCH 05/12] fix: update EXPECTED_DISTS to 68 (66 base + 2 riscv64 wheels) After upstream dropped free-threaded wheels, EXPECTED_DISTS went from 75 to 66. Adding cp312 and cp313 manylinux_2_39_riscv64 brings it to 68. Signed-off-by: Bruno Verachten --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 596ef95633f..7ce72286553 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -39,7 +39,7 @@ concurrency: cancel-in-progress: true env: - EXPECTED_DISTS: 66 + EXPECTED_DISTS: 68 FORCE_COLOR: 1 jobs: From 37f731dda5219df934593e88cd94f2a39f573fd1 Mon Sep 17 00:00:00 2001 From: Bruno Verachten Date: Sun, 5 Apr 2026 14:49:40 +0200 Subject: [PATCH 06/12] fix: disable libjpeg-turbo SIMD on riscv64 to avoid 3.1.4.1 build error libjpeg-turbo 3.1.4.1 simdcoverage.c references jsimd_can_encode_mcu_AC_refine_prepare which is only declared in the RVV SIMD extensions added to upstream main (commit 9817c40) but not included in any stable release yet. Building with -DWITH_SIMD=FALSE avoids the error; riscv64 has no production SIMD support in 3.1.4.1 in any case. Signed-off-by: Bruno Verachten --- .github/workflows/wheels-dependencies.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 7750a2e07b5..0d93db20792 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -267,6 +267,13 @@ function build { fi build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib + # libjpeg-turbo 3.1.4.1 simdcoverage.c references riscv64 RVV SIMD + # functions that are only in upstream main and not yet released. Disable + # SIMD for riscv64 to avoid the build error; there is no production + # riscv64 SIMD support in this version anyway. + if [[ "$(uname -m)" == "riscv64" ]]; then + HOST_CMAKE_FLAGS="${HOST_CMAKE_FLAGS} -DWITH_SIMD=FALSE" + fi build_libjpeg_turbo if [[ -n "$IS_MACOS" ]]; then # Custom tiff build to include jpeg; by default, configure won't include From ec1c53d6d7aa07d4389f17f9e2c735bb92d016c1 Mon Sep 17 00:00:00 2001 From: Bruno Verachten Date: Sun, 5 Apr 2026 17:55:58 +0200 Subject: [PATCH 07/12] fix: bump cibuildwheel to 3.4.1 for correct riscv64 manylinux_2_39 image pin cibuildwheel 3.4.0 pins manylinux_2_39_riscv64 to 2026.03.01-1 which does not exist on quay.io (earliest available tag is 2026.03.06-3). 3.4.1 corrects the pin to 2026.03.20-1. Signed-off-by: Bruno Verachten --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index fd4183aff35..c824c10bce7 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.4.0 +cibuildwheel==3.4.1 From db188af557353ba0b3176ad261d30e3e47214775 Mon Sep 17 00:00:00 2001 From: Bruno Verachten Date: Mon, 6 Apr 2026 12:37:18 +0200 Subject: [PATCH 08/12] ci: disable libaom RVV optimizations for riscv64 libaom's riscv64 RVV code (highbd_convolve_rvv.c) calls functions without declarations; GCC 14 in manylinux_2_39 treats this as an error. Pass -DAOM_TARGET_CPU=generic for riscv64 to skip the arch-specific code. QEMU-based builds don't benefit from RVV optimizations anyway. Signed-off-by: Bruno Verachten --- .github/workflows/wheels-dependencies.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 0d93db20792..771671e82c0 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -204,6 +204,12 @@ function build_libavif { -DAVIF_CODEC_DAV1D=LOCAL ) fi + # libaom's riscv64 RVV code calls functions without declarations, which + # GCC 14 (manylinux_2_39) treats as errors. Disable arch-specific AOM + # optimizations for riscv64; QEMU-based builds don't benefit from them. + if [[ "$(uname -m)" == "riscv64" ]]; then + libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic) + fi local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) From e2d2677904cac9e4e57195fdb7e41522b2ed4789 Mon Sep 17 00:00:00 2001 From: Bruno Verachten Date: Wed, 15 Apr 2026 08:33:37 +0200 Subject: [PATCH 09/12] ci: use cp313+cp314 instead of cp312+cp313 for riscv64 Signed-off-by: Bruno Verachten --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 7ce72286553..ed538f0d400 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -98,7 +98,7 @@ jobs: platform: linux os: ubuntu-latest cibw_arch: riscv64 - build: "cp3{12,13}-manylinux*" + build: "cp3{13,14}-manylinux*" manylinux: "manylinux_2_39" - name: "iOS arm64 device" platform: ios From 43ac97cfc3680b8172ab40724f5616e2c5b92846 Mon Sep 17 00:00:00 2001 From: Bruno Verachten Date: Thu, 23 Apr 2026 11:03:15 +0200 Subject: [PATCH 10/12] Update .github/workflows/wheels.yml Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 263bebc610e..2b3e7620993 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -126,7 +126,7 @@ jobs: - name: Set up QEMU if: matrix.cibw_arch == 'riscv64' - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: platforms: riscv64 From bf2de3803fb91413373f97a448b46e79588caa66 Mon Sep 17 00:00:00 2001 From: Bruno Verachten Date: Thu, 23 Apr 2026 14:23:24 +0200 Subject: [PATCH 11/12] ci: skip tests for riscv64 QEMU build The test_redos test has a 1-second timeout that fails under QEMU emulation where riscv64 regex operations are significantly slower than on native hardware. Signed-off-by: Bruno Verachten --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2b3e7620993..a3f1baec8eb 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -143,6 +143,7 @@ jobs: CIBW_BUILD: ${{ matrix.build }} CIBW_ENABLE: cpython-prerelease pypy CIBW_MANYLINUX_RISCV64_IMAGE: ${{ matrix.manylinux }} + CIBW_TEST_SKIP: ${{ matrix.cibw_arch == 'riscv64' && '*-linux_riscv64' || '' }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 From fb52ed018b933d6234b6210189c41257d6b116a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Apr 2026 12:01:08 +1000 Subject: [PATCH 12/12] Instead of skipping all riscv64 tests, only skip timeout-based tests Signed-off-by: Bruno Verachten --- .github/workflows/wheels.yml | 1 - Tests/helper.py | 7 +++++-- Tests/test_file_eps.py | 4 ++-- Tests/test_file_fli.py | 4 ++-- Tests/test_file_jpeg.py | 4 ++-- Tests/test_file_pdf.py | 4 ++-- Tests/test_file_tiff.py | 6 +++--- Tests/test_image.py | 4 ++-- Tests/test_imagefontpil.py | 4 ++-- Tests/test_imagemorph.py | 4 ++-- 10 files changed, 22 insertions(+), 20 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 123b3421107..2ac952ede5d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -137,7 +137,6 @@ jobs: CIBW_BUILD: ${{ matrix.build }} CIBW_ENABLE: cpython-prerelease pypy CIBW_MANYLINUX_RISCV64_IMAGE: ${{ matrix.manylinux }} - CIBW_TEST_SKIP: ${{ matrix.cibw_arch == 'riscv64' && '*-linux_riscv64' || '' }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/Tests/helper.py b/Tests/helper.py index d77b4b807ec..0ce778a5295 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -163,8 +163,11 @@ def assert_tuple_approx_equal( pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) -def timeout_unless_slower_valgrind(timeout: float) -> pytest.MarkDecorator: - if "PILLOW_VALGRIND_TEST" in os.environ: +def timeout_unless_slower(timeout: float) -> pytest.MarkDecorator: + if ( + "PILLOW_VALGRIND_TEST" in os.environ + or os.environ.get("AUDITWHEEL_ARCH") == "riscv64" + ): return pytest.mark.pil_noop_mark() return pytest.mark.timeout(timeout) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index d41bab30741..26860f6bd3e 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -16,7 +16,7 @@ is_win32, mark_if_feature_version, skip_unless_feature, - timeout_unless_slower_valgrind, + timeout_unless_slower, ) HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript() @@ -411,7 +411,7 @@ def test_emptyline() -> None: assert image.format == "EPS" -@timeout_unless_slower_valgrind(5) +@timeout_unless_slower(5) @pytest.mark.parametrize( "test_file", ["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 13c6a43239b..3a34f2d02ed 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -11,7 +11,7 @@ assert_image_equal, assert_image_equal_tofile, is_pypy, - timeout_unless_slower_valgrind, + timeout_unless_slower, ) # created as an export of a palette image from Gimp2.6 @@ -196,7 +196,7 @@ def test_seek() -> None: "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", ], ) -@timeout_unless_slower_valgrind(3) +@timeout_unless_slower(3) def test_timeouts(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 5103a767278..23996b555e9 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -31,7 +31,7 @@ is_win32, mark_if_feature_version, skip_unless_feature, - timeout_unless_slower_valgrind, + timeout_unless_slower, ) ElementTree: ModuleType | None @@ -1048,7 +1048,7 @@ def test_save_xmp(self, tmp_path: Path) -> None: with pytest.raises(ValueError): im.save(f, xmp=b"1" * 65505) - @timeout_unless_slower_valgrind(1) + @timeout_unless_slower(1) def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # Even though this decoder never says that it is finished # the image should still end when there is no new data diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index a2218673b44..967e0a2e3ff 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -17,7 +17,7 @@ hopper, mark_if_feature_version, skip_unless_feature, - timeout_unless_slower_valgrind, + timeout_unless_slower, ) @@ -344,7 +344,7 @@ def test_pdf_append_to_bytesio() -> None: assert len(f.getvalue()) > initial_size -@timeout_unless_slower_valgrind(1) +@timeout_unless_slower(1) @pytest.mark.parametrize("newline", (b"\r", b"\n")) def test_redos(newline: bytes) -> None: malicious = b" trailer<<>>" + newline * 3456 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index e442471d1ca..0a220c4862e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -28,7 +28,7 @@ hopper, is_pypy, is_win32, - timeout_unless_slower_valgrind, + timeout_unless_slower, ) ElementTree: ModuleType | None @@ -1015,7 +1015,7 @@ def test_string_dimension(self) -> None: with pytest.raises(OSError): im.load() - @timeout_unless_slower_valgrind(6) + @timeout_unless_slower(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: @@ -1028,7 +1028,7 @@ def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif", ], ) - @timeout_unless_slower_valgrind(2) + @timeout_unless_slower(2) def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning, match="Corrupt EXIF data"): diff --git a/Tests/test_image.py b/Tests/test_image.py index 32c79919595..73dd2d00fbd 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -34,7 +34,7 @@ is_win32, mark_if_feature_version, skip_unless_feature, - timeout_unless_slower_valgrind, + timeout_unless_slower, ) ElementTree: ModuleType | None @@ -577,7 +577,7 @@ def test_check_size(self) -> None: i = Image.new("RGB", [1, 1]) assert isinstance(i.size, tuple) - @timeout_unless_slower_valgrind(0.75) + @timeout_unless_slower(0.75) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) def test_empty_image(self, size: tuple[int, int]) -> None: Image.new("RGB", size) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 883df051d1e..18b7196058a 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -7,7 +7,7 @@ from PIL import Image, ImageDraw, ImageFont, _util, features -from .helper import assert_image_equal_tofile, timeout_unless_slower_valgrind +from .helper import assert_image_equal_tofile, timeout_unless_slower fonts = [ImageFont.load_default_imagefont()] if not features.check_module("freetype2"): @@ -78,7 +78,7 @@ def test_decompression_bomb() -> None: font.getmask("A" * 1_000_000) -@timeout_unless_slower_valgrind(4) +@timeout_unless_slower(4) def test_oom() -> None: glyph = struct.pack( ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 1d2fae1a6fa..c6702fdd30d 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -7,7 +7,7 @@ from PIL import Image, ImageMorph, _imagingmorph -from .helper import assert_image_equal_tofile, hopper, timeout_unless_slower_valgrind +from .helper import assert_image_equal_tofile, hopper, timeout_unless_slower def string_to_img(image_string: str) -> Image.Image: @@ -266,7 +266,7 @@ def test_unknown_pattern() -> None: @pytest.mark.parametrize( "pattern", ("a pattern with a syntax error", "4:(" + "X" * 30000) ) -@timeout_unless_slower_valgrind(1) +@timeout_unless_slower(1) def test_pattern_syntax_error(pattern: str) -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner")