Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
22f1821
feat: add riscv64 to Linux wheel build matrix
gounthar Mar 12, 2026
7b026c6
feat(riscv64): add riscv64 to build-native-wheels matrix
gounthar Mar 12, 2026
280fa92
Merge branch 'main' into feat/riscv64-wheels
gounthar Mar 12, 2026
c673eb8
Merge branch 'main' into feat/riscv64-wheels
gounthar Mar 13, 2026
8e8a161
Merge branch 'main' into feat/riscv64-wheels
gounthar Mar 27, 2026
9c93f7c
Merge branch 'main' into feat/riscv64-wheels
radarhere Mar 28, 2026
816fe45
ci: use manylinux_2_39 for riscv64 (2_28 has no riscv64 image)
gounthar Mar 28, 2026
7f293c5
Merge branch 'main' into feat/riscv64-wheels
gounthar Mar 28, 2026
bc007bc
Merge branch 'main' into feat/riscv64-wheels
gounthar Mar 29, 2026
094db1d
Merge branch 'main' into feat/riscv64-wheels
radarhere Apr 2, 2026
d5e42ac
Update docker/setup-qemu-action
radarhere Apr 3, 2026
3194d33
Merge branch 'main' into feat/riscv64-wheels
radarhere Apr 4, 2026
0ccd26a
fix: update EXPECTED_DISTS to 68 (66 base + 2 riscv64 wheels)
gounthar Apr 5, 2026
30b7714
Merge branch 'main' into feat/riscv64-wheels
gounthar Apr 5, 2026
b3d4a13
Merge branch 'main' into feat/riscv64-wheels
gounthar Apr 5, 2026
37f731d
fix: disable libjpeg-turbo SIMD on riscv64 to avoid 3.1.4.1 build error
gounthar Apr 5, 2026
ec1c53d
fix: bump cibuildwheel to 3.4.1 for correct riscv64 manylinux_2_39 im…
gounthar Apr 5, 2026
db188af
ci: disable libaom RVV optimizations for riscv64
gounthar Apr 6, 2026
e2d2677
ci: use cp313+cp314 instead of cp312+cp313 for riscv64
gounthar Apr 15, 2026
3a0f9e6
Merge branch 'main' into feat/riscv64-wheels
gounthar Apr 15, 2026
21c44fe
Merge branch 'python-pillow:main' into feat/riscv64-wheels
gounthar Apr 23, 2026
43ac97c
Update .github/workflows/wheels.yml
gounthar Apr 23, 2026
bf2de38
ci: skip tests for riscv64 QEMU build
gounthar Apr 23, 2026
85c36d1
Merge branch 'main' into feat/riscv64-wheels
gounthar Apr 29, 2026
fb52ed0
Instead of skipping all riscv64 tests, only skip timeout-based tests
radarhere Apr 24, 2026
930ec88
Merge branch 'main' into feat/riscv64-wheels
gounthar May 1, 2026
e4570e5
Merge branch 'main' into feat/riscv64-wheels
radarhere May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/wheels-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if libaom plan to address this in the future? Is there an issue tracking the progress?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there is an upstream issue tracking it: https://aomedia.issues.chromium.org/issues/492439207

There is also a Gerrit CL in review that addresses RVV handling for specific block sizes: https://aomedia-review.googlesource.com/c/aom/+/208401

Not sure yet how soon it will land, but once it does the -DAOM_TARGET_CPU=generic workaround could probably be dropped. For now keeping it in is the safe option.


local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz)

Expand Down Expand Up @@ -268,6 +274,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
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ concurrency:
cancel-in-progress: true

env:
EXPECTED_DISTS: 66
EXPECTED_DISTS: 68
FORCE_COLOR: 1

jobs:
Expand Down Expand Up @@ -90,6 +90,12 @@ jobs:
os: ubuntu-24.04-arm
cibw_arch: aarch64
build: "*musllinux*"
- name: "manylinux_2_39 riscv64"
platform: linux
os: ubuntu-latest
cibw_arch: riscv64
build: "cp3{13,14}-manylinux*"
manylinux: "manylinux_2_39"
- name: "iOS arm64 device"
platform: ios
os: macos-latest
Expand All @@ -112,6 +118,12 @@ jobs:
with:
python-version: "3.x"

- name: Set up QEMU
if: matrix.cibw_arch == 'riscv64'
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
with:
platforms: riscv64

- name: Install cibuildwheel
run: |
python3 -m pip install -r .ci/requirements-cibw.txt
Expand All @@ -124,6 +136,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_ENABLE: cpython-prerelease pypy
CIBW_MANYLINUX_RISCV64_IMAGE: ${{ matrix.manylinux }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
7 changes: 5 additions & 2 deletions Tests/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions Tests/test_file_eps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"],
Expand Down
4 changes: 2 additions & 2 deletions Tests/test_file_fli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
is_win32,
mark_if_feature_version,
skip_unless_feature,
timeout_unless_slower_valgrind,
timeout_unless_slower,
)

ElementTree: ModuleType | None
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Tests/test_file_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
hopper,
mark_if_feature_version,
skip_unless_feature,
timeout_unless_slower_valgrind,
timeout_unless_slower,
)


Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
hopper,
is_pypy,
is_win32,
timeout_unless_slower_valgrind,
timeout_unless_slower,
)

ElementTree: ModuleType | None
Expand Down Expand Up @@ -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:
Expand All @@ -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"):
Expand Down
4 changes: 2 additions & 2 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
is_win32,
mark_if_feature_version,
skip_unless_feature,
timeout_unless_slower_valgrind,
timeout_unless_slower,
)

ElementTree: ModuleType | None
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions Tests/test_imagefontpil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Tests/test_imagemorph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand Down
Loading