diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index b3a5782091b..3cf4bafafe9 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -206,6 +206,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) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 17023104bf9..92f0f831e9e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -37,7 +37,7 @@ concurrency: cancel-in-progress: true env: - EXPECTED_DISTS: 78 + EXPECTED_DISTS: 80 FORCE_COLOR: 1 jobs: @@ -120,6 +120,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 @@ -142,6 +148,12 @@ jobs: with: python-version: "3.x" + - name: Set up QEMU + if: matrix.cibw_arch == 'riscv64' + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + with: + platforms: riscv64 + - name: Install cibuildwheel run: | python3 -m pip install -r .ci/requirements-cibw.txt @@ -155,6 +167,7 @@ jobs: CIBW_BUILD: ${{ matrix.build }} CIBW_ENABLE: cpython-prerelease pypy CIBW_ENVIRONMENT_PASS_LINUX: FORCE_COLOR + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ matrix.manylinux }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - name: Download SBOM diff --git a/Tests/helper.py b/Tests/helper.py index 7571bc7113b..f550c8d660b 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -161,8 +161,11 @@ def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) != len(items), msg -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 e3b7f0981bb..6b7ea657915 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -15,7 +15,7 @@ is_win32, mark_if_feature_version, skip_unless_feature, - timeout_unless_slower_valgrind, + timeout_unless_slower, ) TYPE_CHECKING = False @@ -418,7 +418,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 9000c0b01ba..a38ddcc8ddc 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 @@ -192,7 +192,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 749e2882b9f..a7777ae1708 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -30,7 +30,7 @@ is_win32, mark_if_feature_version, skip_unless_feature, - timeout_unless_slower_valgrind, + timeout_unless_slower, ) TYPE_CHECKING = False @@ -1072,7 +1072,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 7f821d110a4..2d9159a72c5 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -15,7 +15,7 @@ hopper, mark_if_feature_version, skip_unless_feature, - timeout_unless_slower_valgrind, + timeout_unless_slower, ) TYPE_CHECKING = False @@ -348,7 +348,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 523db1d0a29..e63b3553310 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -25,7 +25,7 @@ hopper, is_pypy, is_win32, - timeout_unless_slower_valgrind, + timeout_unless_slower, ) TYPE_CHECKING = False @@ -1022,7 +1022,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: @@ -1035,7 +1035,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 d0516b849f2..ff1642ea03a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -31,7 +31,7 @@ is_win32, mark_if_feature_version, skip_unless_feature, - timeout_unless_slower_valgrind, + timeout_unless_slower, ) TYPE_CHECKING = False @@ -571,7 +571,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, 10_000_000), (10_000_000, 0)), ids=("tall", "wide") ) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index f94bf2cd074..1ba3857e787 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"): @@ -97,7 +97,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 74bdbd9e154..e69bfa07a9a 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -5,7 +5,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 TYPE_CHECKING = False if TYPE_CHECKING: @@ -268,7 +268,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")