diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 12593b3f564..7e9d9f14609 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -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 local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz) @@ -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 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e0edb3ac0b7..a19bb2f3e61 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -35,7 +35,7 @@ concurrency: cancel-in-progress: true env: - EXPECTED_DISTS: 66 + EXPECTED_DISTS: 68 FORCE_COLOR: 1 jobs: @@ -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 @@ -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 @@ -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 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 81bd4729926..7b6f691402d 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")