From 2364fbab669775fa93e66584ebae4a40caf79a48 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 17 Oct 2026 16:11:59 +0100 Subject: [PATCH 01/21] Add Android to wheels.yml, wheels-dependencies.sh and pyproject.toml --- .github/workflows/wheels-dependencies.sh | 50 +++++++++++++++------ .github/workflows/wheels.yml | 8 ++++ docs/releasenotes/13.1.0.rst | 55 ++++++++++++++++++++++++ pyproject.toml | 47 ++++++++++++-------- 4 files changed, 131 insertions(+), 29 deletions(-) create mode 100644 docs/releasenotes/13.1.0.rst diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 625e54c050b..93e2586a30b 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -20,7 +20,30 @@ function check_cibw_archs { # up the cross environment. PROJECTDIR=$(pwd) PATCH_DIR=$(pwd)/patches -if [[ "$CIBW_PLATFORM" == "ios" ]]; then + +if [[ "$CIBW_PLATFORM" == "android" ]]; then + check_cibw_archs + case "$CIBW_ARCHS" in + arm64_v8a): + HOST_TRIPLE=aarch64-linux-android + PLAT=arm64 + ;; + x86_64): + HOST_TRIPLE=x86_64-linux-android + PLAT=x86_64 + ;; + *) + echo "ERROR: Unknown CIBW_ARCHS value '$CIBW_ARCHS'" + exit 1 + ;; + esac + HOST_CONFIGURE_FLAGS="--disable-shared --enable-static --host=$HOST_TRIPLE" + HOST_CMAKE_FLAGS="-DBUILD_SHARED_LIBS=NO -DENABLE_SHARED=NO" + + WORKDIR=$(pwd)/build/android_$CIBW_ARCHS + BUILD_PREFIX=$(pwd)/build/deps/android_$CIBW_ARCHS + +elif [[ "$CIBW_PLATFORM" == "ios" ]]; then check_cibw_archs # On iOS, CIBW_ARCHS is actually a multi-arch - arm64_iphoneos, # arm64_iphonesimulator or x86_64_iphonesimulator. Split into the CPU @@ -111,11 +134,11 @@ LIBAVIF_VERSION=$(_get_ver libavif) function build_pkg_config { if [ -e pkg-config-stamp ]; then return; fi # This essentially duplicates the Homebrew recipe. - # On iOS, we need a binary that can be executed on the build machine; but we - # can create a host-specific pc-path to store iOS .pc files. To ensure a + # When cross-compiling, we need a binary that can be executed on the build machine; + # but we can create a host-specific pc-path to store .pc files. To ensure a # macOS-compatible build, we temporarily clear environment flags that set - # iOS-specific values. - if [[ -n "$IOS_SDK" ]]; then + # host-specific values. + if [[ "$CIBW_PLATFORM" =~ ^(android|ios)$ ]]; then ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET unset HOST_CONFIGURE_FLAGS @@ -127,7 +150,7 @@ function build_pkg_config { --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ --with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include - if [[ -n "$IOS_SDK" ]]; then + if [[ "$CIBW_PLATFORM" =~ ^(android|ios)$ ]]; then HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS IPHONEOS_DEPLOYMENT_TARGET=$ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET fi; @@ -197,6 +220,9 @@ function build_libavif { fi else libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now") + if [ "$CIBW_PLATFORM" = "android"]; then + build_shared=OFF + fi fi if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic) @@ -325,8 +351,8 @@ function build { # Break the cycle by building FreeType twice, so that the FreeType we ship is linked against the HarfBuzz we ship. build_freetype - if [[ -z "$IOS_SDK" ]]; then - # On iOS, there's no vendor-provided raqm, and we can't ship it due to + if ! [[ "$CIBW_PLATFORM" =~ ^(android|ios)$ ]]; then + # On some platforms there's no vendor-provided raqm, and we can't ship it due to # licensing, so there's no point building harfbuzz. build_harfbuzz @@ -416,13 +442,13 @@ fi wrap_wheel_builder build -# A safety catch for iOS. iOS can't use dynamic libraries, but clang will prefer -# to link dynamic libraries to static libraries. The only way to reliably +# A safety catch for mobile platforms. They can't use dynamic libraries, but the +# linker will prefer dynamic libraries to static libraries. The only way to reliably # prevent this is to not have dynamic libraries available in the first place. # The build process *shouldn't* generate any dylibs... but just in case, purge # any dylibs that *have* been installed into the build prefix directory. -if [[ -n "$IOS_SDK" ]]; then - find "$BUILD_PREFIX" -name "*.dylib" -exec rm -rf {} \; +if [[ "$CIBW_PLATFORM" =~ ^(android|ios)$ ]]; then + find "$BUILD_PREFIX" -name "*.dylib" -or -name "*.so" -exec rm -rf {} \; fi # Return to the project root to finish the build diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2a9e26a41a6..71c9ece56e6 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -120,6 +120,14 @@ jobs: os: ubuntu-24.04-arm cibw_arch: aarch64 build: "*musllinux*" + - name: "Android arm64" + platform: android + os: ubuntu-latest + cibw_arch: arm64_v8a + - name: "Android x86_64" + platform: android + os: ubuntu-latest + cibw_arch: x86_64 - name: "iOS arm64 device" platform: ios os: macos-latest diff --git a/docs/releasenotes/13.1.0.rst b/docs/releasenotes/13.1.0.rst new file mode 100644 index 00000000000..7ed21d998e6 --- /dev/null +++ b/docs/releasenotes/13.1.0.rst @@ -0,0 +1,55 @@ +13.1.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards incompatible changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API changes +=========== + +TODO +^^^^ + +TODO + +API additions +============= + +TODO +^^^^ + +TODO + +Other changes +============= + +Android +^^^^^^^ + +Android ARM64 and x86_64 wheels are now built for Python 3.13 and later. diff --git a/pyproject.toml b/pyproject.toml index d978f394324..385d5075865 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,33 +113,46 @@ macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disab macos.environment.PATH = """\ $(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin\ """ -# Disable platform guessing on iOS, and disable raqm (since there won't be a + +# Settings for mobile platforms +[[tool.cibuildwheel.overrides]] +select = [ "*-android_*", "*-ios_*" ] +# Disable platform guessing, and disable raqm (since there won't be a # vendor version, and we can't distribute it due to licensing) -ios.config-settings = "raqm=disable imagequant=disable platform-guessing=disable" -# iOS needs to be given a specific pytest invocation and list of test sources. -ios.test-command = [ +config-settings = "raqm=disable imagequant=disable platform-guessing=disable" +# Mobile platforms only support Python test-commands, not shell scripts. +test-command = [ "python -m selftest", "python -m pytest -vv -x -W always checks/check_wheel.py Tests", ] -# There's no numpy wheel for iOS (yet...) -ios.test-requires = [] -ios.test-sources = [ +test-sources = [ "checks", "Tests", "selftest.py", ] -overrides = [ - # iOS environment is isolated by cibuildwheel, but needs the dependencies - { select = "*_iphoneos", environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" }, - # iOS simulator environment is isolated by cibuildwheel, but needs the dependencies - { select = "*_iphonesimulator", environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" }, - { select = "*-win32", test-requires = [] }, - # NumPy does not yet have PyPy 3.12 wheels - { select = "pp312*", test-requires = [] }, +xbuild-tools = [] + +# iOS environment is isolated by cibuildwheel, but needs the dependencies +[[tool.cibuildwheel.overrides]] +select = "*_iphoneos" +environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" + +# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies +[[tool.cibuildwheel.overrides]] +select = "*_iphonesimulator" +environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" + +[[tool.cibuildwheel.overrides]] +select = [ + # NumPy does not have wheels for these platforms or Python versions. + "*-android_*", + "*-ios_*", + "*-win32", + "pp312*", # NumPy's PyPy 3.11 wheels fail to import on PyPy 3.11 v8.0.0 for Windows - { select = "pp311-win_amd64", test-requires = [] }, + "pp311-win_amd64", ] -xbuild-tools = [] +test-requires = [] [tool.black] exclude = "wheels/multibuild" From 3cafaecc0a460136a7974a613d16a62a688ebdd8 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 17 Oct 2026 19:47:55 +0100 Subject: [PATCH 02/21] Temporarily disable some workflows --- .github/workflows/benchmark.yml | 96 ------------ .github/workflows/cifuzz.yml | 62 -------- .github/workflows/test-docker.yml | 106 ------------- .github/workflows/test-msys2.yml | 89 ----------- .github/workflows/test-windows.yml | 240 ----------------------------- .github/workflows/test.yml | 168 -------------------- 6 files changed, 761 deletions(-) delete mode 100644 .github/workflows/benchmark.yml delete mode 100644 .github/workflows/cifuzz.yml delete mode 100644 .github/workflows/test-docker.yml delete mode 100644 .github/workflows/test-msys2.yml delete mode 100644 .github/workflows/test-windows.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml deleted file mode 100644 index f0234cc103a..00000000000 --- a/.github/workflows/benchmark.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Benchmark - -on: - push: - branches: - - "**" - paths-ignore: &paths-ignore - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: *paths-ignore - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - FORCE_COLOR: 1 - PIP_DISABLE_PIP_VERSION_CHECK: 1 - -jobs: - benchmark: - - strategy: - fail-fast: false - matrix: - python-version: [ - # NB: Codspeed doesn't seem to support multiple runs of the same benchmark: - # > At least one benchmark was run multiple times in your benchmarking workflow. - # > This could be because you used a matrix that runs your benchmarks multiple times. - # Hence, this list should only ever have exactly one version. - "3.15", - ] - - runs-on: ubuntu-latest - name: Benchmark Python ${{ matrix.python-version }} - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - cache: pip - cache-dependency-path: | - ".ci/*.sh" - "pyproject.toml" - - - name: Cache libavif - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - id: cache-libavif - with: - path: ~/cache-libavif - key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh') }} - - - name: Cache libimagequant - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - id: cache-libimagequant - with: - path: ~/cache-libimagequant - key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} - - - name: Cache libwebp - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - id: cache-libwebp - with: - path: ~/cache-libwebp - key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} - - - name: Install Linux dependencies - run: | - .ci/install.sh - env: - GHA_PYTHON_VERSION: ${{ matrix.python-version }} - GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} - GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} - - - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1 - with: - mode: simulation - run: | - python3 -m pip install -e .[tests] pytest-codspeed - python3 -m pytest -vv --codspeed Tests/benchmarks.py diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml deleted file mode 100644 index aa86ef85d6b..00000000000 --- a/.github/workflows/cifuzz.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: CIFuzz - -on: - push: - branches: - - "**" - paths: &paths - - ".github/dependencies.json" - - ".github/workflows/cifuzz.yml" - - ".github/workflows/wheels-dependencies.sh" - - "**.c" - - "**.h" - pull_request: - paths: *paths - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - FORCE_COLOR: 1 - -jobs: - Fuzzing: - runs-on: ubuntu-latest - steps: - - name: Build Fuzzers - id: build - uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@187888a8d1e04b93ebc0de70b87f624c0c136d49 # master - with: - oss-fuzz-project-name: 'pillow' - language: python - dry-run: false - - name: Run Fuzzers - id: run - uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@187888a8d1e04b93ebc0de70b87f624c0c136d49 # master - with: - oss-fuzz-project-name: 'pillow' - fuzz-seconds: 600 - language: python - dry-run: false - - name: Upload New Crash - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: failure() && steps.build.outcome == 'success' - with: - name: artifacts - path: ./out/artifacts - - name: Upload Legacy Crash - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: steps.run.outcome == 'success' - with: - name: crash - path: ./out/crash* - - name: Fail on legacy crash - if: success() - run: | - [ ! -e out/crash-* ] - echo No legacy crash detected diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml deleted file mode 100644 index 48c3de62141..00000000000 --- a/.github/workflows/test-docker.yml +++ /dev/null @@ -1,106 +0,0 @@ -name: Test Docker - -on: - push: - branches: - - "**" - paths-ignore: &paths-ignore - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: *paths-ignore - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - FORCE_COLOR: 1 - -jobs: - build: - - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: ["ubuntu-latest"] - docker: [ - # Run slower jobs first to give them a headstart and reduce waiting time - ubuntu-26.04-resolute-ppc64le, - ubuntu-26.04-resolute-s390x, - # Then run the remainder - alpine, - amazon-2023-amd64, - arch, - centos-stream-9-amd64, - centos-stream-10-amd64, - debian-13-trixie-x86, - debian-13-trixie-amd64, - fedora-44-amd64, - gentoo, - ubuntu-24.04-noble-amd64, - ubuntu-26.04-resolute-amd64, - ] - dockerTag: [main] - include: - - docker: "ubuntu-26.04-resolute-ppc64le" - qemu-arch: "ppc64le" - - docker: "ubuntu-26.04-resolute-s390x" - qemu-arch: "s390x" - - docker: "ubuntu-26.04-resolute-arm64v8" - os: "ubuntu-24.04-arm" - dockerTag: main - - name: ${{ matrix.docker }} - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Build system information - run: python3 .github/workflows/system-info.py - - - name: Set up QEMU - if: "matrix.qemu-arch" - uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 - with: - platforms: ${{ matrix.qemu-arch }} - - - name: Docker pull - run: | - docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} - - - name: Docker build - run: | - # The Pillow user in the docker container is UID 1001 - sudo chown -R 1001 $GITHUB_WORKSPACE - docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} - sudo chown -R runner $GITHUB_WORKSPACE - - - name: Upload coverage - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 - with: - flags: GHA_Docker - name: ${{ matrix.docker }} - - success: - permissions: - contents: none - if: always() - needs: build - runs-on: ubuntu-latest - name: Docker Test Successful - steps: - - name: Success - env: - BUILD_RESULT: ${{ needs.build.result }} - run: test "$BUILD_RESULT" = "success" diff --git a/.github/workflows/test-msys2.yml b/.github/workflows/test-msys2.yml deleted file mode 100644 index 568ee646f6f..00000000000 --- a/.github/workflows/test-msys2.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Test MSYS2 - -on: - push: - branches: - - "**" - paths-ignore: &paths-ignore - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: *paths-ignore - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - FORCE_COLOR: 1 - -jobs: - build: - runs-on: windows-latest - - defaults: - run: - shell: bash.exe --login -eo pipefail "{0}" - env: - MSYSTEM: UCRT64 - CHERE_INVOKING: 1 - - timeout-minutes: 30 - name: "MSYS2" - - steps: - - name: Checkout Pillow - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up shell - run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH - shell: pwsh - - - name: Install dependencies - run: | - pacman -S --noconfirm \ - mingw-w64-ucrt-x86_64-freetype \ - mingw-w64-ucrt-x86_64-gcc \ - mingw-w64-ucrt-x86_64-ghostscript \ - mingw-w64-ucrt-x86_64-lcms2 \ - mingw-w64-ucrt-x86_64-libavif \ - mingw-w64-ucrt-x86_64-libimagequant \ - mingw-w64-ucrt-x86_64-libjpeg-turbo \ - mingw-w64-ucrt-x86_64-libraqm \ - mingw-w64-ucrt-x86_64-libtiff \ - mingw-w64-ucrt-x86_64-libwebp \ - mingw-w64-ucrt-x86_64-openjpeg2 \ - mingw-w64-ucrt-x86_64-python-numpy \ - mingw-w64-ucrt-x86_64-python-olefile \ - mingw-w64-ucrt-x86_64-python-pip \ - mingw-w64-ucrt-x86_64-python-psutil \ - mingw-w64-ucrt-x86_64-python-pytest \ - mingw-w64-ucrt-x86_64-python-pytest-cov \ - mingw-w64-ucrt-x86_64-python-pytest-timeout \ - mingw-w64-ucrt-x86_64-python-pyqt6 - - pushd depends && ./install_extra_test_images.sh && popd - - - name: Build Pillow - run: CFLAGS="-coverage" python3 -m pip install .[tests] - - - name: Test Pillow - run: | - python3 selftest.py --installed - .ci/test.sh - - - name: Upload coverage - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 - with: - files: ./coverage.xml - flags: GHA_Windows - name: "MSYS2" diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml deleted file mode 100644 index 0716d79d82d..00000000000 --- a/.github/workflows/test-windows.yml +++ /dev/null @@ -1,240 +0,0 @@ -name: Test Windows - -on: - push: - branches: - - "**" - paths-ignore: &paths-ignore - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: *paths-ignore - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - FORCE_COLOR: 1 - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - python-version: ["pypy3.12", "pypy3.11", "3.15", "3.14", "3.13", "3.12"] - architecture: ["x64"] - os: ["windows-latest"] - include: - # Test the oldest Python on 32-bit - - { python-version: "3.11", architecture: "x86", os: "windows-2022" } - - timeout-minutes: 45 - - name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) - - steps: - - name: Checkout Pillow - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Checkout cached dependencies - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - repository: python-pillow/pillow-depends - path: winbuild\depends - - - name: Checkout extra test images - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - repository: python-pillow/test-images - path: Tests\test-images - - # sets env: pythonLocation - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - architecture: ${{ matrix.architecture }} - cache: pip - cache-dependency-path: ".github/workflows/test-windows.yml" - - - name: Print build system information - run: python3 .github/workflows/system-info.py - - - name: Upgrade pip - run: | - python3 -m pip install --upgrade pip - - - name: Install CPython dependencies - if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'" - run: | - python3 -m pip install PyQt6 - - - name: Install PyArrow dependency - run: | - python3 -m pip install --only-binary=:all: pyarrow || true - - - name: Install dependencies - id: install - run: | - choco install nasm --no-progress - echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - - choco install ghostscript --no-progress - foreach ($dir in (Get-ChildItem -Path "C:\Program Files\gs\gs*\bin")) { - echo $dir.FullName >> $env:GITHUB_PATH - } - - # Install extra test images - xcopy /S /Y Tests\test-images\* Tests\images - - # make cache key depend on VS version - & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` - | find """catalog_buildVersion""" ` - | ForEach-Object { $a = $_.split(" ")[1]; echo "vs=$a" >> $env:GITHUB_OUTPUT } - shell: pwsh - - - name: Cache build - id: build-cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: winbuild\build - key: - ${{ hashFiles('.github\dependencies.json') }}-${{ hashFiles('winbuild\build_prepare.py') }}-${{ hashFiles('.github\workflows\test-windows.yml') }}-${{ env.pythonLocation }}-${{ steps.install.outputs.vs }} - - - name: Prepare build - if: steps.build-cache.outputs.cache-hit != 'true' - run: | - & python.exe winbuild\build_prepare.py -v - shell: pwsh - - - name: Build dependencies / libjpeg-turbo - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libjpeg.cmd" - - - name: Build dependencies / zlib - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_zlib.cmd" - - - name: Build dependencies / xz - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_xz.cmd" - - - name: Build dependencies / WebP - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libwebp.cmd" - - - name: Build dependencies / LibTiff - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libtiff.cmd" - - # for FreeType CBDT/SBIX font support - - name: Build dependencies / libpng - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libpng.cmd" - - - name: Build dependencies / libavif - if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64' - run: "& winbuild\\build\\build_dep_libavif.cmd" - - # for FreeType WOFF2 font support - - name: Build dependencies / brotli - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_brotli.cmd" - - - name: Build dependencies / FreeType - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_freetype.cmd" - - - name: Build dependencies / LCMS2 - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_lcms2.cmd" - - - name: Build dependencies / OpenJPEG - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_openjpeg.cmd" - - # GPL licensed - - name: Build dependencies / libimagequant - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libimagequant.cmd" - - # Raqm dependencies - - name: Build dependencies / HarfBuzz - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_harfbuzz.cmd" - - # Raqm dependencies - - name: Build dependencies / FriBidi - if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_fribidi.cmd" - - # trim ~150MB for each job - - name: Optimize build cache - if: steps.build-cache.outputs.cache-hit != 'true' - run: | - rm -rf winbuild\build\src - shell: bash - - - name: Build Pillow - run: | - $FLAGS="-C raqm=vendor -C fribidi=vendor" - cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS .[tests]" - & $env:pythonLocation\python.exe selftest.py --installed - shell: pwsh - - # skip PyPy for speed - - name: Enable heap verification - if: "!contains(matrix.python-version, 'pypy')" - run: | - & reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f - - - name: Test Pillow - run: | - .ci\test.cmd - - - name: Prepare to upload errors - if: failure() - run: | - mkdir -p Tests/errors - shell: bash - - - name: Upload errors - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: failure() - with: - name: errors - path: Tests/errors - - - name: Upload coverage - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 - with: - files: ./coverage.xml - flags: GHA_Windows - name: ${{ runner.os }} Python ${{ matrix.python-version }} - - success: - permissions: - contents: none - if: always() - needs: build - runs-on: ubuntu-latest - name: Windows Test Successful - steps: - - name: Success - env: - BUILD_RESULT: ${{ needs.build.result }} - run: test "$BUILD_RESULT" = "success" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index e089addc614..00000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,168 +0,0 @@ -name: Test - -on: - push: - branches: - - "**" - paths-ignore: &paths-ignore - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: *paths-ignore - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - FORCE_COLOR: 1 - PIP_DISABLE_PIP_VERSION_CHECK: 1 - -jobs: - build: - - strategy: - fail-fast: false - matrix: - os: [ - "macos-latest", - "ubuntu-latest", - ] - python-version: [ - "pypy3.12", - "pypy3.11", - "3.15t", - "3.15", - "3.14t", - "3.14", - "3.13", - "3.12", - "3.11", - ] - include: - - { python-version: "3.13", PYTHONOPTIMIZE: 1 } - - { python-version: "3.12", PYTHONOPTIMIZE: 2 } - # Intel - - { os: "macos-26-intel", python-version: "3.11" } - exclude: - - { os: "macos-latest", python-version: "3.11" } - - runs-on: ${{ matrix.os }} - name: ${{ matrix.os }} Python ${{ matrix.python-version }} - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - cache: pip - cache-dependency-path: | - ".ci/*.sh" - "pyproject.toml" - - - name: Build system information - run: python3 .github/workflows/system-info.py - - - name: Cache libavif - if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - id: cache-libavif - with: - path: ~/cache-libavif - key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh') }} - - - name: Cache libimagequant - if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - id: cache-libimagequant - with: - path: ~/cache-libimagequant - key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} - - - name: Cache libwebp - if: startsWith(matrix.os, 'ubuntu') - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - id: cache-libwebp - with: - path: ~/cache-libwebp - key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} - - - name: Install Linux dependencies - if: startsWith(matrix.os, 'ubuntu') - run: | - .ci/install.sh - env: - GHA_PYTHON_VERSION: ${{ matrix.python-version }} - GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} - GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} - - - name: Install macOS dependencies - if: startsWith(matrix.os, 'macOS') - run: | - .github/workflows/macos-install.sh - env: - GHA_PYTHON_VERSION: ${{ matrix.python-version }} - - - name: Register gcc problem matcher - if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'" - run: echo "::add-matcher::.github/problem-matchers/gcc.json" - - - name: Build - run: | - .ci/build.sh - - - name: Test - run: | - if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - xvfb-run -s '-screen 0 1024x768x24' sway& - export WAYLAND_DISPLAY=wayland-1 - .ci/test.sh - else - .ci/test.sh - fi - env: - PYTHONOPTIMIZE: ${{ matrix.PYTHONOPTIMIZE }} - - - name: Prepare to upload errors - if: failure() - run: | - mkdir -p Tests/errors - - - name: Upload errors - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: failure() - with: - name: errors - path: Tests/errors - - - name: Upload coverage - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 - with: - flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} - name: ${{ matrix.os }} Python ${{ matrix.python-version }} - - success: - permissions: - contents: none - if: always() - needs: build - runs-on: ubuntu-latest - name: Test Successful - steps: - - name: Success - env: - BUILD_RESULT: ${{ needs.build.result }} - run: test "$BUILD_RESULT" = "success" From 6d720b97a5fe9db699b59ede8bb9c38476a85bae Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 17 Oct 2026 20:30:13 +0100 Subject: [PATCH 03/21] Switch to shared libraries --- .github/workflows/wheels-dependencies.sh | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 93e2586a30b..054101c767b 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -37,8 +37,7 @@ if [[ "$CIBW_PLATFORM" == "android" ]]; then exit 1 ;; esac - HOST_CONFIGURE_FLAGS="--disable-shared --enable-static --host=$HOST_TRIPLE" - HOST_CMAKE_FLAGS="-DBUILD_SHARED_LIBS=NO -DENABLE_SHARED=NO" + HOST_CONFIGURE_FLAGS="--host=$HOST_TRIPLE" WORKDIR=$(pwd)/build/android_$CIBW_ARCHS BUILD_PREFIX=$(pwd)/build/deps/android_$CIBW_ARCHS @@ -220,9 +219,6 @@ function build_libavif { fi else libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now") - if [ "$CIBW_PLATFORM" = "android"]; then - build_shared=OFF - fi fi if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic) @@ -442,13 +438,13 @@ fi wrap_wheel_builder build -# A safety catch for mobile platforms. They can't use dynamic libraries, but the -# linker will prefer dynamic libraries to static libraries. The only way to reliably +# A safety catch for iOS. iOS can't use dynamic libraries, but clang will prefer +# to link dynamic libraries to static libraries. The only way to reliably # prevent this is to not have dynamic libraries available in the first place. # The build process *shouldn't* generate any dylibs... but just in case, purge # any dylibs that *have* been installed into the build prefix directory. -if [[ "$CIBW_PLATFORM" =~ ^(android|ios)$ ]]; then - find "$BUILD_PREFIX" -name "*.dylib" -or -name "*.so" -exec rm -rf {} \; +if [[ -n "$IOS_SDK" ]]; then + find "$BUILD_PREFIX" -name "*.dylib" -exec rm -rf {} \; fi # Return to the project root to finish the build From f77be138d93fda287b3f0ded1a0b53689ab563b1 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 17 Oct 2026 20:36:19 +0100 Subject: [PATCH 04/21] Temporarily remove path and tag filters from wheels.yml --- .github/workflows/wheels.yml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 97cbad5710e..266a760718b 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -10,23 +10,6 @@ on: # │ │ │ │ │ - cron: "42 1 * * 0,3" push: - paths: &paths - - ".ci/requirements-cibw.txt" - - ".ci/requirements-sbom.txt" - - ".github/compare-dist-sizes.py" - - ".github/dependencies.json" - - ".github/embed-sbom.py" - - ".github/generate-sbom.py" - - ".github/workflows/wheels*" - - "pyproject.toml" - - "setup.py" - - "wheels/*" - - "winbuild/build_prepare.py" - - "winbuild/fribidi.cmake" - tags: - - "*" - pull_request: - paths: *paths workflow_dispatch: permissions: From e7619c434f47c7595fb6fbeef1ef8b3d8396d791 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 17 Oct 2026 21:35:59 +0100 Subject: [PATCH 05/21] Install pkg-config using pip, and move all pip dependencies to build-system.requires --- .github/workflows/wheels-dependencies.sh | 40 +----------------------- pyproject.toml | 16 +++------- 2 files changed, 6 insertions(+), 50 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 054101c767b..4e6649792b3 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -130,34 +130,6 @@ LIBXCB_VERSION=$(_get_ver libxcb) BROTLI_VERSION=$(_get_ver brotli) LIBAVIF_VERSION=$(_get_ver libavif) -function build_pkg_config { - if [ -e pkg-config-stamp ]; then return; fi - # This essentially duplicates the Homebrew recipe. - # When cross-compiling, we need a binary that can be executed on the build machine; - # but we can create a host-specific pc-path to store .pc files. To ensure a - # macOS-compatible build, we temporarily clear environment flags that set - # host-specific values. - if [[ "$CIBW_PLATFORM" =~ ^(android|ios)$ ]]; then - ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS - ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET - unset HOST_CONFIGURE_FLAGS - unset IPHONEOS_DEPLOYMENT_TARGET - fi - - CFLAGS="$CFLAGS -Wno-int-conversion" CPPFLAGS="" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ - --disable-debug --disable-host-tool --with-internal-glib \ - --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ - --with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include - - if [[ "$CIBW_PLATFORM" =~ ^(android|ios)$ ]]; then - HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS - IPHONEOS_DEPLOYMENT_TARGET=$ORIGINAL_IPHONEOS_DEPLOYMENT_TARGET - fi; - - export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config - touch pkg-config-stamp -} - function build_zlib_ng { if [ -e zlib-stamp ]; then return; fi # zlib-ng uses a "configure" script, but it's not a GNU autotools script, so @@ -183,7 +155,6 @@ function build_brotli { function build_harfbuzz { if [ -e harfbuzz-stamp ]; then return; fi - python3 -m pip install meson ninja local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ @@ -196,8 +167,6 @@ function build_harfbuzz { function build_libavif { if [ -e libavif-stamp ]; then return; fi - python3 -m pip install meson ninja - if ([[ "$PLAT" == "x86_64" ]] && [[ -z "$IOS_SDK" ]]) || [ -n "$SANITIZER" ]; then build_simple nasm 3.02 https://www.nasm.us/pub/nasm/releasebuilds/3.02 fi @@ -404,14 +373,7 @@ if [[ -n "$IS_MACOS" ]]; then mkdir -p "$BUILD_PREFIX/bin" mkdir -p "$BUILD_PREFIX/lib" - # Ensure pkg-config is available. This is done *before* setting CC, CFLAGS - # etc. to ensure that the build is *always* a macOS build, even when building - # for iOS. - build_pkg_config - - # Ensure cmake is available, and that the default prefix used by CMake is - # the build prefix - python3 -m pip install cmake + # Ensure that the default prefix used by CMake is the build prefix export CMAKE_PREFIX_PATH=$BUILD_PREFIX if [[ -n "$IOS_SDK" ]]; then diff --git a/pyproject.toml b/pyproject.toml index 385d5075865..ecf81809ca4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,10 @@ [build-system] build-backend = "backend" requires = [ + "cmake", + "meson", + "ninja", + "pkgconf", "pybind11", "setuptools>=77", ] @@ -101,7 +105,7 @@ dynamic.version = { attr = "PIL.__version__" } [tool.cibuildwheel] build-verbosity = 1 config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" -before-all = ".github/workflows/wheels-dependencies.sh" +before-build = ".github/workflows/wheels-dependencies.sh" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-requires = [ "numpy", @@ -132,16 +136,6 @@ test-sources = [ ] xbuild-tools = [] -# iOS environment is isolated by cibuildwheel, but needs the dependencies -[[tool.cibuildwheel.overrides]] -select = "*_iphoneos" -environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" - -# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies -[[tool.cibuildwheel.overrides]] -select = "*_iphonesimulator" -environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" - [[tool.cibuildwheel.overrides]] select = [ # NumPy does not have wheels for these platforms or Python versions. From 3cd64029fa1e81c0e5ff87fbc9f54eac71eb9784 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Mon, 19 Oct 2026 22:43:55 +0100 Subject: [PATCH 06/21] All dependencies compiling --- .github/workflows/wheels-dependencies.sh | 152 +++++++++++++---------- patches/bzip2-1.0.8.tar.gz.patch | 37 ++++++ pyproject.toml | 4 +- 3 files changed, 121 insertions(+), 72 deletions(-) create mode 100644 patches/bzip2-1.0.8.tar.gz.patch diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4e6649792b3..6fe39939db0 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,19 +1,11 @@ #!/bin/bash -# Safety check - Pillow builds require that CIBW_ARCHS is set, and that it only -# contains a single value (even though cibuildwheel allows multiple values in -# CIBW_ARCHS). This check doesn't work on Linux because of how the CIBW_ARCHS -# variable is exposed. -function check_cibw_archs { - if [[ -z "$CIBW_ARCHS" ]]; then - echo "ERROR: Pillow builds require CIBW_ARCHS be defined." - exit 1 - fi - if [[ "$CIBW_ARCHS" == *" "* ]]; then - echo "ERROR: Pillow builds only support a single architecture in CIBW_ARCHS." - exit 1 - fi -} +set -e + +# CIBUILDWHEEL_BUILD_IDENTIFIER has the format {python_version}-{os}_{arch} +CIBW_OS_ARCH=${CIBUILDWHEEL_BUILD_IDENTIFIER#*-} +CIBW_OS=${CIBW_OS_ARCH%%_*} +CIBW_ARCH=${CIBW_OS_ARCH#*_} # Setup that needs to be done before multibuild utils are invoked. Process # potential cross-build platforms before native platforms to ensure that we pick @@ -21,40 +13,33 @@ function check_cibw_archs { PROJECTDIR=$(pwd) PATCH_DIR=$(pwd)/patches -if [[ "$CIBW_PLATFORM" == "android" ]]; then - check_cibw_archs - case "$CIBW_ARCHS" in - arm64_v8a): - HOST_TRIPLE=aarch64-linux-android - PLAT=arm64 - ;; - x86_64): - HOST_TRIPLE=x86_64-linux-android - PLAT=x86_64 - ;; +if [ "$CIBW_OS" = "android" ]; then + IS_ANDROID=1 + case "$CIBW_ARCH" in + arm64_v8a) PLAT=aarch64 ;; + x86_64) PLAT=x86_64 ;; *) - echo "ERROR: Unknown CIBW_ARCHS value '$CIBW_ARCHS'" + echo "ERROR: Unknown Android architecture '$CIBW_ARCH'" exit 1 - ;; esac + HOST_TRIPLE="$PLAT-linux-android" HOST_CONFIGURE_FLAGS="--host=$HOST_TRIPLE" - WORKDIR=$(pwd)/build/android_$CIBW_ARCHS - BUILD_PREFIX=$(pwd)/build/deps/android_$CIBW_ARCHS - -elif [[ "$CIBW_PLATFORM" == "ios" ]]; then - check_cibw_archs - # On iOS, CIBW_ARCHS is actually a multi-arch - arm64_iphoneos, + # When building for Android, cibuildwheel sets the CMAKE_TOOLCHAIN_FILE environment + # variable, which includes most of the flags we need. Some additional flags are + # required by libavif. + android_toolchain_root=$(cd "$(dirname "$CC")/.." && pwd) + android_ndk=$(cd "$android_toolchain_root/../../../.." && pwd) + HOST_CMAKE_FLAGS+=" -DANDROID_TOOLCHAIN_ROOT=$android_toolchain_root" + HOST_CMAKE_FLAGS+=" -DANDROID_NDK=$android_ndk" + +elif [ "$CIBW_OS" == "ios" ]; then + # IS_MACOS will be set by common_utils.sh. + # On iOS, CIBW_ARCH is actually a multi-arch - arm64_iphoneos, # arm64_iphonesimulator or x86_64_iphonesimulator. Split into the CPU # platform, and the iOS SDK. - PLAT=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\1/") - IOS_SDK=$(echo $CIBW_ARCHS | sed "s/\(.*\)_\(.*\)/\2/") - - # Build iOS builds in `build/iphoneos` or `build/iphonesimulator` - # (depending on the build target). Install them into `build/deps/iphoneos` - # or `build/deps/iphonesimulator` - WORKDIR=$(pwd)/build/$IOS_SDK - BUILD_PREFIX=$(pwd)/build/deps/$IOS_SDK + PLAT=$(echo $CIBW_ARCH | sed "s/\(.*\)_\(.*\)/\1/") + IOS_SDK=$(echo $CIBW_ARCH | sed "s/\(.*\)_\(.*\)/\2/") # GNU tooling insists on using aarch64 rather than arm64 if [[ $PLAT == "arm64" ]]; then @@ -65,9 +50,9 @@ elif [[ "$CIBW_PLATFORM" == "ios" ]]; then IOS_SDK_PATH=$(xcrun --sdk $IOS_SDK --show-sdk-path) CMAKE_SYSTEM_NAME=iOS - IOS_HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET + HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET if [[ "$IOS_SDK" == "iphonesimulator" ]]; then - IOS_HOST_TRIPLE=$IOS_HOST_TRIPLE-simulator + HOST_TRIPLE=$HOST_TRIPLE-simulator fi # GNU Autotools doesn't recognize the existence of arm64-apple-ios-simulator @@ -88,25 +73,34 @@ elif [[ "$CIBW_PLATFORM" == "ios" ]]; then # This will be generated once CC etc. have been evaluated. HOST_MESON_FLAGS="--cross-file $WORKDIR/meson-cross.txt -Dprefer_static=true -Ddefault_library=static" -elif [[ "$(uname -s)" == "Darwin" ]]; then - check_cibw_archs - # Build macOS dependencies in `build/darwin` - # Install them into `build/deps/darwin` - PLAT=$CIBW_ARCHS - WORKDIR=$(pwd)/build/darwin - BUILD_PREFIX=$(pwd)/build/deps/darwin -else - # Build prefix will default to /usr/local - PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" - WORKDIR=$(pwd)/build +elif [ "$CIBW_OS" = "macosx" ]; then + # IS_MACOS will be set by common_utils.sh. + PLAT=$CIBW_ARCH + +elif [ "$CIBW_OS" = *linux ]; then + IS_LINUX=1 + PLAT=$CIBW_ARCH MB_ML_LIBC=${AUDITWHEEL_POLICY::9} MB_ML_VER=${AUDITWHEEL_POLICY:9} + +else + echo "ERROR: Unknown operating system '$CIBW_OS'" + exit 1 fi +WORKDIR=$(pwd)/build/$CIBW_OS_ARCH +BUILD_PREFIX=$WORKDIR/prefix + # Define custom utilities source wheels/multibuild/common_utils.sh + +# Adjust variables for cross-compilation. +if [ -n "$IS_ANDROID" ]; then + unset IS_MACOS IS_OSX MB_ML_VER +fi + source wheels/multibuild/library_builders.sh -if [[ -z "$IS_MACOS" ]]; then +if [ -n "$IS_LINUX" ]; then source wheels/multibuild/manylinux_utils.sh fi @@ -138,7 +132,10 @@ function build_zlib_ng { ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS unset HOST_CONFIGURE_FLAGS - build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat + # CHOST is the equivalent of the autotools --host option. If it's empty, the library + # will be built for the build machine's platform. + CHOST=$HOST_TRIPLE build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION \ + --installnamedir=$BUILD_PREFIX/lib --zlib-compat HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS touch zlib-stamp @@ -187,6 +184,14 @@ function build_libavif { build_shared=OFF fi else + if [ -n "$IS_ANDROID" ]; then + # CMake's internal Clang.cmake passes -fuse-ld=gold when building with LTO + # on Android, but current versions of the NDK no longer include gold. It + # looks like it should be possible to prevent this by setting + # CMAKE_ANDROID_NDK_VERSION to a high enough number, but doing that using -D + # has no effect, so maybe it needs to be in the toolchain file. + lto=OFF + fi libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now") fi if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then @@ -234,14 +239,20 @@ function build_libavif { function build_zstd { if [ -e zstd-stamp ]; then return; fi local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz) + + if [ -n "$IS_ANDROID" ]; then + zstd_uname="Linux" + else + zstd_uname=$(uname) + fi (cd $out_dir \ - && make -j4 install) + && make -j4 install UNAME_TARGET_SYSTEM=$zstd_uname PREFIX="$BUILD_PREFIX") touch zstd-stamp } function build { build_xz - if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then + if [ -n "$IS_LINUX" ] && [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ]; then yum remove -y zlib-devel fi if [[ -n "$IS_MACOS" ]]; then @@ -250,15 +261,18 @@ function build { build_zlib_ng fi - build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto - if [[ -n "$IS_MACOS" ]]; then - build_simple xorgproto 2025.1 https://www.x.org/pub/individual/proto - build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib - build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist - else - sed "s/\${pc_sysrootdir\}//" $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc + # There's no X server on mobile devices. + if [ -z "$IS_ANDROID" ] && [ -z "$IOS_SDK" ]; then + build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto + if [[ -n "$IS_MACOS" ]]; then + build_simple xorgproto 2025.1 https://www.x.org/pub/individual/proto + build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib + build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist + else + sed "s/\${pc_sysrootdir\}//" $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc + fi + build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib fi - build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib # -DCMAKE_POSITION_INDEPENDENT_CODE=1 is a workaround from https://github.com/libjpeg-turbo/libjpeg-turbo/issues/898 HOST_CMAKE_FLAGS="-DCMAKE_POSITION_INDEPENDENT_CODE=1 $HOST_CMAKE_FLAGS" build_libjpeg_turbo @@ -316,7 +330,7 @@ function build { # Break the cycle by building FreeType twice, so that the FreeType we ship is linked against the HarfBuzz we ship. build_freetype - if ! [[ "$CIBW_PLATFORM" =~ ^(android|ios)$ ]]; then + if [[ -z "$IS_ANDROID" ]] && [[ -z "$IOS_SDK" ]]; then # On some platforms there's no vendor-provided raqm, and we can't ship it due to # licensing, so there's no point building harfbuzz. build_harfbuzz @@ -385,8 +399,8 @@ if [[ -n "$IS_MACOS" ]]; then export STRIP=$(xcrun --find --sdk $IOS_SDK strip) CPPFLAGS="$CPPFLAGS --sysroot=$IOS_SDK_PATH" - CFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" - CXXFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" + CFLAGS="-target $HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" + CXXFLAGS="-target $HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" # Having IPHONEOS_DEPLOYMENT_TARGET in the environment causes problems # with some cross-building toolchains, because it introduces implicit diff --git a/patches/bzip2-1.0.8.tar.gz.patch b/patches/bzip2-1.0.8.tar.gz.patch new file mode 100644 index 00000000000..8d8737ad4e1 --- /dev/null +++ b/patches/bzip2-1.0.8.tar.gz.patch @@ -0,0 +1,37 @@ +--- Makefile 2019-07-13 18:50:05 ++++ Makefile 2026-10-19 19:45:39 +@@ -15,13 +15,14 @@ + SHELL=/bin/sh + + # To assist in cross-compiling +-CC=gcc +-AR=ar +-RANLIB=ranlib +-LDFLAGS= ++# Pillow: disabled overrides of commands and flags to allow cross compilation ++# CC=gcc ++# AR=ar ++# RANLIB=ranlib ++# LDFLAGS= + + BIGFILES=-D_FILE_OFFSET_BITS=64 +-CFLAGS=-Wall -Winline -O2 -g $(BIGFILES) ++CFLAGS+=-Wall -Winline -O2 -g $(BIGFILES) + + # Where you want it installed when you do 'make install' + PREFIX=/usr/local +--- Makefile-libbz2_so 2019-07-13 18:50:05 ++++ Makefile-libbz2_so 2026-10-19 19:45:31 +@@ -22,9 +22,10 @@ + + + SHELL=/bin/sh +-CC=gcc ++# Pillow: disabled overrides of commands and flags to allow cross compilation ++# CC=gcc + BIGFILES=-D_FILE_OFFSET_BITS=64 +-CFLAGS=-fpic -fPIC -Wall -Winline -O2 -g $(BIGFILES) ++CFLAGS+=-fpic -fPIC -Wall -Winline -O2 -g $(BIGFILES) + + OBJS= blocksort.o \ + huffman.o \ diff --git a/pyproject.toml b/pyproject.toml index ecf81809ca4..bea9c53c1d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,9 +114,7 @@ test-extras = "tests" # Disable platform guessing on macOS to avoid picking up Homebrew etc. macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" # Isolate macOS build environment from Homebrew etc. -macos.environment.PATH = """\ - $(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin\ - """ +macos.environment.PATH = "$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" # Settings for mobile platforms [[tool.cibuildwheel.overrides]] From ae5282ce192d5ba623d881c8da8d005c65c48fbd Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Tue, 20 Oct 2026 16:41:31 +0100 Subject: [PATCH 07/21] Wheel build and repair working --- .github/workflows/wheels-dependencies.sh | 15 +++++++++++---- pyproject.toml | 4 ++++ setup.py | 12 +++++++----- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 6fe39939db0..3022911c50d 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -13,6 +13,15 @@ CIBW_ARCH=${CIBW_OS_ARCH#*_} PROJECTDIR=$(pwd) PATCH_DIR=$(pwd)/patches +# Use a subdirectory to allow building for multiple platforms on the same machine, but +# create a "current" symlink to simplify setting PKG_CONFIG_PATH in setup.py, and +# AUDITWHEEL_LD_LIBRARY_PATH in pyproject.toml. +deps_dir=$(pwd)/build/deps +mkdir -p $deps_dir +WORKDIR=$deps_dir/$CIBW_OS_ARCH +ln -sfn $CIBW_OS_ARCH $deps_dir/current +BUILD_PREFIX=$WORKDIR/prefix + if [ "$CIBW_OS" = "android" ]; then IS_ANDROID=1 case "$CIBW_ARCH" in @@ -88,13 +97,11 @@ else exit 1 fi -WORKDIR=$(pwd)/build/$CIBW_OS_ARCH -BUILD_PREFIX=$WORKDIR/prefix - # Define custom utilities source wheels/multibuild/common_utils.sh -# Adjust variables for cross-compilation. +# Android builds can be done on Linux or macOS, but we don't want to enable any behavior +# specific to those platforms. if [ -n "$IS_ANDROID" ]; then unset IS_MACOS IS_OSX MB_ML_VER fi diff --git a/pyproject.toml b/pyproject.toml index bea9c53c1d8..a20bbfe2bbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,6 +134,10 @@ test-sources = [ ] xbuild-tools = [] +[[tool.cibuildwheel.overrides]] +select = [ "*-android_*", "*-*linux_*" ] +environment.AUDITWHEEL_LD_LIBRARY_PATH = "$(pwd)/build/deps/current/prefix/lib" + [[tool.cibuildwheel.overrides]] select = [ # NumPy does not have wheels for these platforms or Python versions. diff --git a/setup.py b/setup.py index 2d02cf129ca..6d6a08e6cdd 100644 --- a/setup.py +++ b/setup.py @@ -280,6 +280,11 @@ def _cmd_exists(cmd: str) -> bool: def _pkg_config(name: str) -> tuple[list[str], list[str]] | None: command = os.environ.get("PKG_CONFIG", "pkg-config") + env = { + **os.environ, + "PKG_CONFIG_PATH": os.path.abspath("build/deps/current/prefix/lib/pkgconfig"), + } + for keep_system in (True, False): try: command_libs = [command, "--libs-only-L", name] @@ -294,13 +299,13 @@ def _pkg_config(name: str) -> tuple[list[str], list[str]] | None: command_cflags.append("--silence-errors") libs = re.split( r"(^|\s+)-L", - subprocess.check_output(command_libs, stderr=stderr) + subprocess.check_output(command_libs, env=env, stderr=stderr) .decode("utf8") .strip(), )[::2][1:] cflags = re.split( r"(^|\s+)-I", - subprocess.check_output(command_cflags).decode("utf8").strip(), + subprocess.check_output(command_cflags, env=env).decode("utf8").strip(), )[::2][1:] return libs, cflags except Exception: @@ -935,9 +940,6 @@ def build_extensions(self) -> None: defs.append(("HAVE_LIBIMAGEQUANT", None)) if feature.get("xcb"): libs.append(feature.get("xcb")) - if sys.platform == "ios": - # Ensure transitive dependencies are linked. - libs.append("Xau") defs.append(("HAVE_XCB", None)) if sys.platform == "win32": libs.extend(["kernel32", "user32", "gdi32"]) From efc79f04bf1949e0599654beb3a524d8c3579064 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Fri, 23 Oct 2026 00:01:20 +0100 Subject: [PATCH 08/21] Move build tools installation back from build-system.requires to wheels-dependencies.sh --- .github/workflows/wheels-dependencies.sh | 6 ++++++ pyproject.toml | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3022911c50d..56905a1cee2 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -389,6 +389,12 @@ if [[ ! -d $WORKDIR/pillow-depends-main ]]; then untar $PROJECTDIR/pillow-depends-main.zip fi +# Install necessary build tools from PyPI. We can't use the build-system.requires +# setting in pyproject.toml, because on all platforms except Android, cibuildwheel +# runs the before-build script before processing that setting. +unset CIBW_HOST_TRIPLET # Install for the build system, not the host system. +python3 -m pip install cmake meson ninja pkgconf + if [[ -n "$IS_MACOS" ]]; then # Ensure the basic structure of the build prefix directory exists. mkdir -p "$BUILD_PREFIX/bin" diff --git a/pyproject.toml b/pyproject.toml index a20bbfe2bbd..02023f4bb73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,6 @@ [build-system] build-backend = "backend" requires = [ - "cmake", - "meson", - "ninja", - "pkgconf", "pybind11", "setuptools>=77", ] From 96ac6cc9a80601e608a82720f658aecc6627cf7d Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Fri, 23 Oct 2026 00:02:38 +0100 Subject: [PATCH 09/21] Miscellaneous fixes --- .github/workflows/wheels-dependencies.sh | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 56905a1cee2..9bd2b754c12 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -42,7 +42,7 @@ if [ "$CIBW_OS" = "android" ]; then HOST_CMAKE_FLAGS+=" -DANDROID_TOOLCHAIN_ROOT=$android_toolchain_root" HOST_CMAKE_FLAGS+=" -DANDROID_NDK=$android_ndk" -elif [ "$CIBW_OS" == "ios" ]; then +elif [ "$CIBW_OS" = "ios" ]; then # IS_MACOS will be set by common_utils.sh. # On iOS, CIBW_ARCH is actually a multi-arch - arm64_iphoneos, # arm64_iphonesimulator or x86_64_iphonesimulator. Split into the CPU @@ -86,7 +86,7 @@ elif [ "$CIBW_OS" = "macosx" ]; then # IS_MACOS will be set by common_utils.sh. PLAT=$CIBW_ARCH -elif [ "$CIBW_OS" = *linux ]; then +elif [[ "$CIBW_OS" = *linux ]]; then IS_LINUX=1 PLAT=$CIBW_ARCH MB_ML_LIBC=${AUDITWHEEL_POLICY::9} @@ -110,6 +110,11 @@ source wheels/multibuild/library_builders.sh if [ -n "$IS_LINUX" ]; then source wheels/multibuild/manylinux_utils.sh fi +if [ -n "$IS_ANDROID" ] || [ -n "$IOS_SDK" ]; then + # LIBRARY_PATH may cause pkg-config to omit -L options, which isn't correct when + # cross-compiling. + unset LIBRARY_PATH +fi ARCHIVE_SDIR=pillow-depends-main @@ -171,7 +176,11 @@ function build_harfbuzz { function build_libavif { if [ -e libavif-stamp ]; then return; fi - if ([[ "$PLAT" == "x86_64" ]] && [[ -z "$IOS_SDK" ]]) || [ -n "$SANITIZER" ]; then + # nasm has to be compiled for the build machine, but Android and iOS will have + # environment variables set for cross-compilation. + if ([[ "$PLAT" == "x86_64" ]] && [[ -z "$IS_ANDROID" ]] && [[ -z "$IOS_SDK" ]]) \ + || [ -n "$SANITIZER" ] + then build_simple nasm 3.02 https://www.nasm.us/pub/nasm/releasebuilds/3.02 fi @@ -196,7 +205,7 @@ function build_libavif { # on Android, but current versions of the NDK no longer include gold. It # looks like it should be possible to prevent this by setting # CMAKE_ANDROID_NDK_VERSION to a high enough number, but doing that using -D - # has no effect, so maybe it needs to be in the toolchain file. + # has no effect, so maybe cibuildwheel should put it in the toolchain file. lto=OFF fi libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now") From eb9a43e56050c84ead72db2a206dafaf745139bf Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 24 Oct 2026 14:40:46 +0100 Subject: [PATCH 10/21] Use same AOM / dav1d configuration as iOS --- .github/workflows/wheels-dependencies.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 9bd2b754c12..864bb808c5f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -182,6 +182,7 @@ function build_libavif { || [ -n "$SANITIZER" ] then build_simple nasm 3.02 https://www.nasm.us/pub/nasm/releasebuilds/3.02 + local have_nasm=1 fi local build_shared=ON @@ -210,9 +211,14 @@ function build_libavif { fi libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now") fi - if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then + + # AOM and dav1d both require nasm by default on x86_64. AOM has a CMake option to + # disable it, but dav1d doesn't. + if [[ -z "$have_nasm" ]] && [[ "$PLAT" == "x86_64" ]]; then + # Use AOM for both encoding and decoding, and don't enable dav1d. libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic) else + # Use AOM for encoding and dav1d for decoding. libavif_cmake_flags+=( -DAVIF_CODEC_AOM_DECODE=OFF \ -DAVIF_CODEC_DAV1D=LOCAL From 1bbc4d3824fa3e08c5f198d3e1139ac7ae298289 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 24 Oct 2026 15:56:36 +0100 Subject: [PATCH 11/21] Revert Linux to using BUILD_PREFIX=/usr/local --- .github/workflows/wheels-dependencies.sh | 5 ++++- pyproject.toml | 2 +- setup.py | 9 +++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 864bb808c5f..b4c4da2fe43 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -20,7 +20,6 @@ deps_dir=$(pwd)/build/deps mkdir -p $deps_dir WORKDIR=$deps_dir/$CIBW_OS_ARCH ln -sfn $CIBW_OS_ARCH $deps_dir/current -BUILD_PREFIX=$WORKDIR/prefix if [ "$CIBW_OS" = "android" ]; then IS_ANDROID=1 @@ -33,6 +32,7 @@ if [ "$CIBW_OS" = "android" ]; then esac HOST_TRIPLE="$PLAT-linux-android" HOST_CONFIGURE_FLAGS="--host=$HOST_TRIPLE" + BUILD_PREFIX=$WORKDIR/prefix # When building for Android, cibuildwheel sets the CMAKE_TOOLCHAIN_FILE environment # variable, which includes most of the flags we need. Some additional flags are @@ -49,6 +49,7 @@ elif [ "$CIBW_OS" = "ios" ]; then # platform, and the iOS SDK. PLAT=$(echo $CIBW_ARCH | sed "s/\(.*\)_\(.*\)/\1/") IOS_SDK=$(echo $CIBW_ARCH | sed "s/\(.*\)_\(.*\)/\2/") + BUILD_PREFIX=$WORKDIR/prefix # GNU tooling insists on using aarch64 rather than arm64 if [[ $PLAT == "arm64" ]]; then @@ -85,8 +86,10 @@ elif [ "$CIBW_OS" = "ios" ]; then elif [ "$CIBW_OS" = "macosx" ]; then # IS_MACOS will be set by common_utils.sh. PLAT=$CIBW_ARCH + BUILD_PREFIX=$WORKDIR/prefix elif [[ "$CIBW_OS" = *linux ]]; then + # Build prefix will default to /usr/local IS_LINUX=1 PLAT=$CIBW_ARCH MB_ML_LIBC=${AUDITWHEEL_POLICY::9} diff --git a/pyproject.toml b/pyproject.toml index 02023f4bb73..2098455ec9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,7 +131,7 @@ test-sources = [ xbuild-tools = [] [[tool.cibuildwheel.overrides]] -select = [ "*-android_*", "*-*linux_*" ] +select = [ "*-android_*" ] environment.AUDITWHEEL_LD_LIBRARY_PATH = "$(pwd)/build/deps/current/prefix/lib" [[tool.cibuildwheel.overrides]] diff --git a/setup.py b/setup.py index 6d6a08e6cdd..e793d37cdad 100644 --- a/setup.py +++ b/setup.py @@ -280,10 +280,11 @@ def _cmd_exists(cmd: str) -> bool: def _pkg_config(name: str) -> tuple[list[str], list[str]] | None: command = os.environ.get("PKG_CONFIG", "pkg-config") - env = { - **os.environ, - "PKG_CONFIG_PATH": os.path.abspath("build/deps/current/prefix/lib/pkgconfig"), - } + env = os.environ.copy() + if sys.platform in ["android", "darwin", "ios"]: + env["PKG_CONFIG_PATH"] = os.path.abspath( + "build/deps/current/prefix/lib/pkgconfig" + ) for keep_system in (True, False): try: From 03d6d2193e59f43930b8e9dd5d3c510da38bcaa3 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 24 Oct 2026 16:33:28 +0100 Subject: [PATCH 12/21] Use the same cross venv workaround on both Android and iOS --- .github/workflows/wheels-dependencies.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index b4c4da2fe43..82f933bdff6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -410,8 +410,20 @@ fi # Install necessary build tools from PyPI. We can't use the build-system.requires # setting in pyproject.toml, because on all platforms except Android, cibuildwheel # runs the before-build script before processing that setting. -unset CIBW_HOST_TRIPLET # Install for the build system, not the host system. +# +# When cross-compiling, install for the build system, not the host system. +site_packages=$(python3 -c ' +import sys +print(next(entry for entry in sys.path if entry.endswith("site-packages"))) +') +cross_pth="$site_packages/_cross_venv.pth" +if [ -e "$cross_pth" ]; then + mv "$cross_pth" "$cross_pth.disabled" +fi python3 -m pip install cmake meson ninja pkgconf +if [ -e "$cross_pth.disabled" ]; then + mv "$cross_pth.disabled" "$cross_pth" +fi if [[ -n "$IS_MACOS" ]]; then # Ensure the basic structure of the build prefix directory exists. From 6bd96f41c15821b0ffbc87409dda0a8906d8fddd Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 24 Oct 2026 17:04:27 +0100 Subject: [PATCH 13/21] Don't run wheels-dependencies.sh on Windows --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2098455ec9e..de5178ea17d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,10 @@ test-extras = "tests" macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" # Isolate macOS build environment from Homebrew etc. macos.environment.PATH = "$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +# Windows uses build_dep_all.cmd instead of wheels-dependencies.sh. +windows.before-build = "" +# Tell auditwheel where to find dependencies. +android.environment.AUDITWHEEL_LD_LIBRARY_PATH = "$(pwd)/build/deps/current/prefix/lib" # Settings for mobile platforms [[tool.cibuildwheel.overrides]] @@ -130,10 +134,6 @@ test-sources = [ ] xbuild-tools = [] -[[tool.cibuildwheel.overrides]] -select = [ "*-android_*" ] -environment.AUDITWHEEL_LD_LIBRARY_PATH = "$(pwd)/build/deps/current/prefix/lib" - [[tool.cibuildwheel.overrides]] select = [ # NumPy does not have wheels for these platforms or Python versions. From 374beb5e1f9d79e355b3a910971c375cb1c4f87f Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 24 Oct 2026 17:06:36 +0100 Subject: [PATCH 14/21] Add $BUILD_PREFIX/share/pkgconfig to PKG_CONFIG_PATH --- .github/workflows/wheels-dependencies.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 82f933bdff6..8693a431b34 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -369,7 +369,7 @@ function build { function create_meson_cross_config { cat << EOF > $WORKDIR/meson-cross.txt [binaries] -pkg-config = '$BUILD_PREFIX/bin/pkg-config' +pkg-config = '$(which pkg-config)' cmake = '$(which cmake)' c = '$CC' cpp = '$CXX' @@ -425,6 +425,9 @@ if [ -e "$cross_pth.disabled" ]; then mv "$cross_pth.disabled" "$cross_pth" fi +export PKG_CONFIG=$(which pkg-config) +export PKG_CONFIG_PATH="$BUILD_PREFIX/lib/pkgconfig:$BUILD_PREFIX/share/pkgconfig" + if [[ -n "$IS_MACOS" ]]; then # Ensure the basic structure of the build prefix directory exists. mkdir -p "$BUILD_PREFIX/bin" From e04ee3796f5ded8a4ee0449059f12c2a1614050a Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 24 Oct 2026 19:30:56 +0100 Subject: [PATCH 15/21] Partly unify test scripts --- .github/workflows/wheels-test.ps1 | 6 +----- .github/workflows/wheels-test.sh | 4 +--- pyproject.toml | 6 ++---- wheels_test.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 12 deletions(-) create mode 100755 wheels_test.py diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 index d82b007b112..5a9d2a55df6 100644 --- a/.github/workflows/wheels-test.ps1 +++ b/.github/workflows/wheels-test.ps1 @@ -18,9 +18,5 @@ if (Test-Path $venv\Scripts\pypy.exe) { cd $pillow & $venv\Scripts\$python -VV if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\$python selftest.py -if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\$python -m pytest -vv -x checks\check_wheel.py -if (!$?) { exit $LASTEXITCODE } -& $venv\Scripts\$python -m pytest -vv -x --numprocesses=logical --dist=worksteal Tests +& $venv\Scripts\$python -m wheels_test if (!$?) { exit $LASTEXITCODE } diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh index 14906039bea..71be64add1f 100755 --- a/.github/workflows/wheels-test.sh +++ b/.github/workflows/wheels-test.sh @@ -32,6 +32,4 @@ if [ ! -d "test-images-main" ]; then fi # Runs tests -python3 selftest.py -python3 -m pytest -vv -x checks/check_wheel.py -python3 -m pytest -vv -x --numprocesses=logical --dist=worksteal +python3 -m wheels_test diff --git a/pyproject.toml b/pyproject.toml index de5178ea17d..d74e352363f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,14 +123,12 @@ select = [ "*-android_*", "*-ios_*" ] # vendor version, and we can't distribute it due to licensing) config-settings = "raqm=disable imagequant=disable platform-guessing=disable" # Mobile platforms only support Python test-commands, not shell scripts. -test-command = [ - "python -m selftest", - "python -m pytest -vv -x -W always checks/check_wheel.py Tests", -] +test-command = "python -m wheels_test" test-sources = [ "checks", "Tests", "selftest.py", + "wheels_test.py", ] xbuild-tools = [] diff --git a/wheels_test.py b/wheels_test.py new file mode 100755 index 00000000000..c4042a1b187 --- /dev/null +++ b/wheels_test.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import runpy +import sys + + +def run_module(mod_name, *args): + old_args = sys.argv[1:] + sys.argv[1:] = args + try: + runpy.run_module(mod_name, run_name="__main__", alter_sys=True) + except SystemExit as e: + if e.code not in [0, None]: + raise + finally: + sys.argv[1:] = old_args + + +if __name__ == "__main__": + run_module("selftest") + + pytest_args = ["pytest", "-vv", "-x"] + run_module(*pytest_args, "checks/check_wheel.py") + + # Mobile platforms don't support multiple processes. + if sys.platform not in ["android", "ios"]: + pytest_args += ["--numprocesses=logical", "--dist=worksteal"] + run_module(*pytest_args, "Tests") From 25d534c388095436ea0d909c0c345ae321b16437 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 24 Oct 2026 20:10:25 +0100 Subject: [PATCH 16/21] More pkg-config fixes --- .github/workflows/wheels-dependencies.sh | 14 ++++++++++++-- checks/check_wheel.py | 10 +++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 8693a431b34..029f3398498 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -296,7 +296,10 @@ function build { else sed "s/\${pc_sysrootdir\}//" $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc fi + # FIXME + set build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib + cat libxcb*/config.log fi # -DCMAKE_POSITION_INDEPENDENT_CODE=1 is a workaround from https://github.com/libjpeg-turbo/libjpeg-turbo/issues/898 @@ -369,7 +372,7 @@ function build { function create_meson_cross_config { cat << EOF > $WORKDIR/meson-cross.txt [binaries] -pkg-config = '$(which pkg-config)' +pkg-config = '$PKG_CONFIG' cmake = '$(which cmake)' c = '$CC' cpp = '$CXX' @@ -425,7 +428,14 @@ if [ -e "$cross_pth.disabled" ]; then mv "$cross_pth.disabled" "$cross_pth" fi -export PKG_CONFIG=$(which pkg-config) +if [ -n "$IS_LINUX" ]; then + # This command will fall back to the system pkg-config executable if something isn't + # found on PKG_CONFIG_PATH. This is necessary for libraries such as libXau. + export PKG_CONFIG=$(which pkgconf-pypi) +else + # This command only searches PKG_CONFIG_PATH. + export PKG_CONFIG=$(which pkgconf) +fi export PKG_CONFIG_PATH="$BUILD_PREFIX/lib/pkgconfig:$BUILD_PREFIX/share/pkgconfig" if [[ -n "$IS_MACOS" ]]; then diff --git a/checks/check_wheel.py b/checks/check_wheel.py index f716c8498bb..61a87bc0368 100644 --- a/checks/check_wheel.py +++ b/checks/check_wheel.py @@ -22,8 +22,8 @@ def test_wheel_modules() -> None: if platform.machine() == "ARM64": expected_modules.remove("avif") - elif sys.platform == "ios": - # tkinter is not available on iOS + elif sys.platform in ["android", "ios"]: + # tkinter is not available on mobile devices expected_modules.remove("tkinter") assert set(features.get_supported_modules()) == expected_modules @@ -47,7 +47,11 @@ def test_wheel_features() -> None: if sys.platform == "win32": expected_features.remove("xcb") - elif sys.platform == "ios": + + elif sys.platform in ["android", "ios"]: + # There's no X server on mobile devices. + expected_features.remove("xcb") + # Can't distribute raqm due to licensing, and there's no system version; # fribidi and harfbuzz won't be available if raqm isn't available. expected_features -= {"raqm", "fribidi", "harfbuzz"} From cabe03cc4df8a0801afc311d1914ee5cd8d2bc44 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sat, 24 Oct 2026 21:42:40 +0100 Subject: [PATCH 17/21] zlib-ng and tiff fixes; all Android tests passing locally --- .github/workflows/wheels-dependencies.sh | 12 ++++++------ Tests/test_main.py | 5 ++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 029f3398498..4d40792959b 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -146,11 +146,11 @@ function build_zlib_ng { # cross-compilation flags. ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS unset HOST_CONFIGURE_FLAGS + if [ -n "$IS_ANDROID" ]; then + HOST_CONFIGURE_FLAGS="--uname=Linux" + fi - # CHOST is the equivalent of the autotools --host option. If it's empty, the library - # will be built for the build machine's platform. - CHOST=$HOST_TRIPLE build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION \ - --installnamedir=$BUILD_PREFIX/lib --zlib-compat + build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS touch zlib-stamp @@ -305,9 +305,9 @@ function build { # -DCMAKE_POSITION_INDEPENDENT_CODE=1 is a workaround from https://github.com/libjpeg-turbo/libjpeg-turbo/issues/898 HOST_CMAKE_FLAGS="-DCMAKE_POSITION_INDEPENDENT_CODE=1 $HOST_CMAKE_FLAGS" build_libjpeg_turbo - if [[ -n "$IS_MACOS" ]]; then + if [[ -n "$IS_MACOS" ]] || [[ -n "$IS_ANDROID" ]]; then # Custom tiff build to include jpeg; by default, configure won't include - # headers/libs in the custom macOS/iOS prefix. Explicitly disable webp, + # headers/libs in the BUILD_PREFIX. Explicitly disable webp, # libdeflate and zstd, because on x86_64 macs, it will pick up the # Homebrew versions of those libraries from /usr/local. tiff_configure_args=( diff --git a/Tests/test_main.py b/Tests/test_main.py index 65e7a44d8d0..23c90fc783d 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -7,7 +7,10 @@ import pytest -@pytest.mark.skipif(sys.platform == "ios", reason="Processes not supported on iOS") +@pytest.mark.skipif( + sys.platform in ["android", "ios"], + reason="Subprocesses not supported on this platform", +) @pytest.mark.parametrize( "args, report", ((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)), From 9019fd1a1079b23fb4679aa73e85ce226a405415 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sun, 25 Oct 2026 19:49:09 +0000 Subject: [PATCH 18/21] macOS x86_64 debugging --- .github/workflows/wheels-dependencies.sh | 4 +- .github/workflows/wheels.yml | 115 ++++++++++++----------- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 4d40792959b..d5c866ca8e2 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -296,7 +296,8 @@ function build { else sed "s/\${pc_sysrootdir\}//" $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc fi - # FIXME + + echo FIXME set build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib cat libxcb*/config.log @@ -436,6 +437,7 @@ else # This command only searches PKG_CONFIG_PATH. export PKG_CONFIG=$(which pkgconf) fi +export PKG_CONFIG_LIBDIR="" export PKG_CONFIG_PATH="$BUILD_PREFIX/lib/pkgconfig:$BUILD_PREFIX/share/pkgconfig" if [[ -n "$IS_MACOS" ]]; then diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 266a760718b..72254f1d5cd 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -66,63 +66,64 @@ jobs: cibw_arch: x86_64 build: "cp311*" macosx_deployment_target: "10.10" - - name: "macOS 10.13 x86_64" - platform: macos - os: macos-26-intel - cibw_arch: x86_64 - build: "cp3{12,13}*" - macosx_deployment_target: "10.13" - - name: "macOS 10.15 x86_64" - platform: macos - os: macos-26-intel - cibw_arch: x86_64 - build: "{cp314,cp315,pp3}*" - macosx_deployment_target: "10.15" - - name: "macOS arm64" - platform: macos - os: macos-latest - cibw_arch: arm64 - macosx_deployment_target: "11.0" - - name: "manylinux_2_28 x86_64" - platform: linux - os: ubuntu-latest - cibw_arch: x86_64 - build: "*manylinux*" - - name: "musllinux x86_64" - platform: linux - os: ubuntu-latest - cibw_arch: x86_64 - build: "*musllinux*" - - name: "manylinux_2_28 aarch64" - platform: linux - os: ubuntu-24.04-arm - cibw_arch: aarch64 - build: "*manylinux*" - - name: "musllinux aarch64" - platform: linux - os: ubuntu-24.04-arm - cibw_arch: aarch64 - build: "*musllinux*" - - name: "Android arm64" - platform: android - os: ubuntu-latest - cibw_arch: arm64_v8a - - name: "Android x86_64" - platform: android - os: ubuntu-latest - cibw_arch: x86_64 - - name: "iOS arm64 device" - platform: ios - os: macos-latest - cibw_arch: arm64_iphoneos - - name: "iOS arm64 simulator" - platform: ios - os: macos-latest - cibw_arch: arm64_iphonesimulator - - name: "iOS x86_64 simulator" - platform: ios - os: macos-26-intel - cibw_arch: x86_64_iphonesimulator + # FIXME + # - name: "macOS 10.13 x86_64" + # platform: macos + # os: macos-26-intel + # cibw_arch: x86_64 + # build: "cp3{12,13}*" + # macosx_deployment_target: "10.13" + # - name: "macOS 10.15 x86_64" + # platform: macos + # os: macos-26-intel + # cibw_arch: x86_64 + # build: "{cp314,cp315,pp3}*" + # macosx_deployment_target: "10.15" + # - name: "macOS arm64" + # platform: macos + # os: macos-latest + # cibw_arch: arm64 + # macosx_deployment_target: "11.0" + # - name: "manylinux_2_28 x86_64" + # platform: linux + # os: ubuntu-latest + # cibw_arch: x86_64 + # build: "*manylinux*" + # - name: "musllinux x86_64" + # platform: linux + # os: ubuntu-latest + # cibw_arch: x86_64 + # build: "*musllinux*" + # - name: "manylinux_2_28 aarch64" + # platform: linux + # os: ubuntu-24.04-arm + # cibw_arch: aarch64 + # build: "*manylinux*" + # - name: "musllinux aarch64" + # platform: linux + # os: ubuntu-24.04-arm + # cibw_arch: aarch64 + # build: "*musllinux*" + # - name: "Android arm64" + # platform: android + # os: ubuntu-latest + # cibw_arch: arm64_v8a + # - name: "Android x86_64" + # platform: android + # os: ubuntu-latest + # cibw_arch: x86_64 + # - name: "iOS arm64 device" + # platform: ios + # os: macos-latest + # cibw_arch: arm64_iphoneos + # - name: "iOS arm64 simulator" + # platform: ios + # os: macos-latest + # cibw_arch: arm64_iphonesimulator + # - name: "iOS x86_64 simulator" + # platform: ios + # os: macos-26-intel + # cibw_arch: x86_64_iphonesimulator steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: From 2c11ab8f010a3f60aa645afecc9cd67cecc333b5 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sun, 25 Oct 2026 20:17:20 +0000 Subject: [PATCH 19/21] Revert temporary changes --- .github/workflows/benchmark.yml | 96 +++++++++ .github/workflows/cifuzz.yml | 62 ++++++ .github/workflows/test-docker.yml | 106 ++++++++++ .github/workflows/test-msys2.yml | 89 +++++++++ .github/workflows/test-windows.yml | 240 +++++++++++++++++++++++ .github/workflows/test.yml | 168 ++++++++++++++++ .github/workflows/wheels-dependencies.sh | 4 - .github/workflows/wheels.yml | 132 +++++++------ 8 files changed, 835 insertions(+), 62 deletions(-) create mode 100644 .github/workflows/benchmark.yml create mode 100644 .github/workflows/cifuzz.yml create mode 100644 .github/workflows/test-docker.yml create mode 100644 .github/workflows/test-msys2.yml create mode 100644 .github/workflows/test-windows.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000000..f0234cc103a --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,96 @@ +name: Benchmark + +on: + push: + branches: + - "**" + paths-ignore: &paths-ignore + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + pull_request: + paths-ignore: *paths-ignore + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 + +jobs: + benchmark: + + strategy: + fail-fast: false + matrix: + python-version: [ + # NB: Codspeed doesn't seem to support multiple runs of the same benchmark: + # > At least one benchmark was run multiple times in your benchmarking workflow. + # > This could be because you used a matrix that runs your benchmarks multiple times. + # Hence, this list should only ever have exactly one version. + "3.15", + ] + + runs-on: ubuntu-latest + name: Benchmark Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + cache: pip + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" + + - name: Cache libavif + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: cache-libavif + with: + path: ~/cache-libavif + key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh') }} + + - name: Cache libimagequant + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + + - name: Cache libwebp + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: cache-libwebp + with: + path: ~/cache-libwebp + key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} + + - name: Install Linux dependencies + run: | + .ci/install.sh + env: + GHA_PYTHON_VERSION: ${{ matrix.python-version }} + GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} + + - name: Run CodSpeed benchmarks + uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1 + with: + mode: simulation + run: | + python3 -m pip install -e .[tests] pytest-codspeed + python3 -m pytest -vv --codspeed Tests/benchmarks.py diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml new file mode 100644 index 00000000000..aa86ef85d6b --- /dev/null +++ b/.github/workflows/cifuzz.yml @@ -0,0 +1,62 @@ +name: CIFuzz + +on: + push: + branches: + - "**" + paths: &paths + - ".github/dependencies.json" + - ".github/workflows/cifuzz.yml" + - ".github/workflows/wheels-dependencies.sh" + - "**.c" + - "**.h" + pull_request: + paths: *paths + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +jobs: + Fuzzing: + runs-on: ubuntu-latest + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@187888a8d1e04b93ebc0de70b87f624c0c136d49 # master + with: + oss-fuzz-project-name: 'pillow' + language: python + dry-run: false + - name: Run Fuzzers + id: run + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@187888a8d1e04b93ebc0de70b87f624c0c136d49 # master + with: + oss-fuzz-project-name: 'pillow' + fuzz-seconds: 600 + language: python + dry-run: false + - name: Upload New Crash + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts + - name: Upload Legacy Crash + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: steps.run.outcome == 'success' + with: + name: crash + path: ./out/crash* + - name: Fail on legacy crash + if: success() + run: | + [ ! -e out/crash-* ] + echo No legacy crash detected diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml new file mode 100644 index 00000000000..48c3de62141 --- /dev/null +++ b/.github/workflows/test-docker.yml @@ -0,0 +1,106 @@ +name: Test Docker + +on: + push: + branches: + - "**" + paths-ignore: &paths-ignore + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + pull_request: + paths-ignore: *paths-ignore + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest"] + docker: [ + # Run slower jobs first to give them a headstart and reduce waiting time + ubuntu-26.04-resolute-ppc64le, + ubuntu-26.04-resolute-s390x, + # Then run the remainder + alpine, + amazon-2023-amd64, + arch, + centos-stream-9-amd64, + centos-stream-10-amd64, + debian-13-trixie-x86, + debian-13-trixie-amd64, + fedora-44-amd64, + gentoo, + ubuntu-24.04-noble-amd64, + ubuntu-26.04-resolute-amd64, + ] + dockerTag: [main] + include: + - docker: "ubuntu-26.04-resolute-ppc64le" + qemu-arch: "ppc64le" + - docker: "ubuntu-26.04-resolute-s390x" + qemu-arch: "s390x" + - docker: "ubuntu-26.04-resolute-arm64v8" + os: "ubuntu-24.04-arm" + dockerTag: main + + name: ${{ matrix.docker }} + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Set up QEMU + if: "matrix.qemu-arch" + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + with: + platforms: ${{ matrix.qemu-arch }} + + - name: Docker pull + run: | + docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + + - name: Docker build + run: | + # The Pillow user in the docker container is UID 1001 + sudo chown -R 1001 $GITHUB_WORKSPACE + docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + sudo chown -R runner $GITHUB_WORKSPACE + + - name: Upload coverage + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + flags: GHA_Docker + name: ${{ matrix.docker }} + + success: + permissions: + contents: none + if: always() + needs: build + runs-on: ubuntu-latest + name: Docker Test Successful + steps: + - name: Success + env: + BUILD_RESULT: ${{ needs.build.result }} + run: test "$BUILD_RESULT" = "success" diff --git a/.github/workflows/test-msys2.yml b/.github/workflows/test-msys2.yml new file mode 100644 index 00000000000..568ee646f6f --- /dev/null +++ b/.github/workflows/test-msys2.yml @@ -0,0 +1,89 @@ +name: Test MSYS2 + +on: + push: + branches: + - "**" + paths-ignore: &paths-ignore + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + pull_request: + paths-ignore: *paths-ignore + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +jobs: + build: + runs-on: windows-latest + + defaults: + run: + shell: bash.exe --login -eo pipefail "{0}" + env: + MSYSTEM: UCRT64 + CHERE_INVOKING: 1 + + timeout-minutes: 30 + name: "MSYS2" + + steps: + - name: Checkout Pillow + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up shell + run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH + shell: pwsh + + - name: Install dependencies + run: | + pacman -S --noconfirm \ + mingw-w64-ucrt-x86_64-freetype \ + mingw-w64-ucrt-x86_64-gcc \ + mingw-w64-ucrt-x86_64-ghostscript \ + mingw-w64-ucrt-x86_64-lcms2 \ + mingw-w64-ucrt-x86_64-libavif \ + mingw-w64-ucrt-x86_64-libimagequant \ + mingw-w64-ucrt-x86_64-libjpeg-turbo \ + mingw-w64-ucrt-x86_64-libraqm \ + mingw-w64-ucrt-x86_64-libtiff \ + mingw-w64-ucrt-x86_64-libwebp \ + mingw-w64-ucrt-x86_64-openjpeg2 \ + mingw-w64-ucrt-x86_64-python-numpy \ + mingw-w64-ucrt-x86_64-python-olefile \ + mingw-w64-ucrt-x86_64-python-pip \ + mingw-w64-ucrt-x86_64-python-psutil \ + mingw-w64-ucrt-x86_64-python-pytest \ + mingw-w64-ucrt-x86_64-python-pytest-cov \ + mingw-w64-ucrt-x86_64-python-pytest-timeout \ + mingw-w64-ucrt-x86_64-python-pyqt6 + + pushd depends && ./install_extra_test_images.sh && popd + + - name: Build Pillow + run: CFLAGS="-coverage" python3 -m pip install .[tests] + + - name: Test Pillow + run: | + python3 selftest.py --installed + .ci/test.sh + + - name: Upload coverage + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + files: ./coverage.xml + flags: GHA_Windows + name: "MSYS2" diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml new file mode 100644 index 00000000000..0716d79d82d --- /dev/null +++ b/.github/workflows/test-windows.yml @@ -0,0 +1,240 @@ +name: Test Windows + +on: + push: + branches: + - "**" + paths-ignore: &paths-ignore + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + pull_request: + paths-ignore: *paths-ignore + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["pypy3.12", "pypy3.11", "3.15", "3.14", "3.13", "3.12"] + architecture: ["x64"] + os: ["windows-latest"] + include: + # Test the oldest Python on 32-bit + - { python-version: "3.11", architecture: "x86", os: "windows-2022" } + + timeout-minutes: 45 + + name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) + + steps: + - name: Checkout Pillow + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Checkout cached dependencies + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + repository: python-pillow/pillow-depends + path: winbuild\depends + + - name: Checkout extra test images + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + repository: python-pillow/test-images + path: Tests\test-images + + # sets env: pythonLocation + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + architecture: ${{ matrix.architecture }} + cache: pip + cache-dependency-path: ".github/workflows/test-windows.yml" + + - name: Print build system information + run: python3 .github/workflows/system-info.py + + - name: Upgrade pip + run: | + python3 -m pip install --upgrade pip + + - name: Install CPython dependencies + if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'" + run: | + python3 -m pip install PyQt6 + + - name: Install PyArrow dependency + run: | + python3 -m pip install --only-binary=:all: pyarrow || true + + - name: Install dependencies + id: install + run: | + choco install nasm --no-progress + echo "C:\Program Files\NASM" >> $env:GITHUB_PATH + + choco install ghostscript --no-progress + foreach ($dir in (Get-ChildItem -Path "C:\Program Files\gs\gs*\bin")) { + echo $dir.FullName >> $env:GITHUB_PATH + } + + # Install extra test images + xcopy /S /Y Tests\test-images\* Tests\images + + # make cache key depend on VS version + & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` + | find """catalog_buildVersion""" ` + | ForEach-Object { $a = $_.split(" ")[1]; echo "vs=$a" >> $env:GITHUB_OUTPUT } + shell: pwsh + + - name: Cache build + id: build-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: winbuild\build + key: + ${{ hashFiles('.github\dependencies.json') }}-${{ hashFiles('winbuild\build_prepare.py') }}-${{ hashFiles('.github\workflows\test-windows.yml') }}-${{ env.pythonLocation }}-${{ steps.install.outputs.vs }} + + - name: Prepare build + if: steps.build-cache.outputs.cache-hit != 'true' + run: | + & python.exe winbuild\build_prepare.py -v + shell: pwsh + + - name: Build dependencies / libjpeg-turbo + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libjpeg.cmd" + + - name: Build dependencies / zlib + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_zlib.cmd" + + - name: Build dependencies / xz + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_xz.cmd" + + - name: Build dependencies / WebP + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libwebp.cmd" + + - name: Build dependencies / LibTiff + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libtiff.cmd" + + # for FreeType CBDT/SBIX font support + - name: Build dependencies / libpng + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libpng.cmd" + + - name: Build dependencies / libavif + if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64' + run: "& winbuild\\build\\build_dep_libavif.cmd" + + # for FreeType WOFF2 font support + - name: Build dependencies / brotli + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_brotli.cmd" + + - name: Build dependencies / FreeType + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_freetype.cmd" + + - name: Build dependencies / LCMS2 + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_lcms2.cmd" + + - name: Build dependencies / OpenJPEG + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_openjpeg.cmd" + + # GPL licensed + - name: Build dependencies / libimagequant + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libimagequant.cmd" + + # Raqm dependencies + - name: Build dependencies / HarfBuzz + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_harfbuzz.cmd" + + # Raqm dependencies + - name: Build dependencies / FriBidi + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_fribidi.cmd" + + # trim ~150MB for each job + - name: Optimize build cache + if: steps.build-cache.outputs.cache-hit != 'true' + run: | + rm -rf winbuild\build\src + shell: bash + + - name: Build Pillow + run: | + $FLAGS="-C raqm=vendor -C fribidi=vendor" + cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS .[tests]" + & $env:pythonLocation\python.exe selftest.py --installed + shell: pwsh + + # skip PyPy for speed + - name: Enable heap verification + if: "!contains(matrix.python-version, 'pypy')" + run: | + & reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f + + - name: Test Pillow + run: | + .ci\test.cmd + + - name: Prepare to upload errors + if: failure() + run: | + mkdir -p Tests/errors + shell: bash + + - name: Upload errors + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() + with: + name: errors + path: Tests/errors + + - name: Upload coverage + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + files: ./coverage.xml + flags: GHA_Windows + name: ${{ runner.os }} Python ${{ matrix.python-version }} + + success: + permissions: + contents: none + if: always() + needs: build + runs-on: ubuntu-latest + name: Windows Test Successful + steps: + - name: Success + env: + BUILD_RESULT: ${{ needs.build.result }} + run: test "$BUILD_RESULT" = "success" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000000..e089addc614 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,168 @@ +name: Test + +on: + push: + branches: + - "**" + paths-ignore: &paths-ignore + - ".github/workflows/docs.yml" + - ".github/workflows/wheels*" + - ".gitmodules" + - "docs/**" + - "wheels/**" + pull_request: + paths-ignore: *paths-ignore + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 + +jobs: + build: + + strategy: + fail-fast: false + matrix: + os: [ + "macos-latest", + "ubuntu-latest", + ] + python-version: [ + "pypy3.12", + "pypy3.11", + "3.15t", + "3.15", + "3.14t", + "3.14", + "3.13", + "3.12", + "3.11", + ] + include: + - { python-version: "3.13", PYTHONOPTIMIZE: 1 } + - { python-version: "3.12", PYTHONOPTIMIZE: 2 } + # Intel + - { os: "macos-26-intel", python-version: "3.11" } + exclude: + - { os: "macos-latest", python-version: "3.11" } + + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + cache: pip + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Cache libavif + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: cache-libavif + with: + path: ~/cache-libavif + key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh') }} + + - name: Cache libimagequant + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + + - name: Cache libwebp + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + id: cache-libwebp + with: + path: ~/cache-libwebp + key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }} + + - name: Install Linux dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + .ci/install.sh + env: + GHA_PYTHON_VERSION: ${{ matrix.python-version }} + GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }} + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} + GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }} + + - name: Install macOS dependencies + if: startsWith(matrix.os, 'macOS') + run: | + .github/workflows/macos-install.sh + env: + GHA_PYTHON_VERSION: ${{ matrix.python-version }} + + - name: Register gcc problem matcher + if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'" + run: echo "::add-matcher::.github/problem-matchers/gcc.json" + + - name: Build + run: | + .ci/build.sh + + - name: Test + run: | + if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + xvfb-run -s '-screen 0 1024x768x24' sway& + export WAYLAND_DISPLAY=wayland-1 + .ci/test.sh + else + .ci/test.sh + fi + env: + PYTHONOPTIMIZE: ${{ matrix.PYTHONOPTIMIZE }} + + - name: Prepare to upload errors + if: failure() + run: | + mkdir -p Tests/errors + + - name: Upload errors + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() + with: + name: errors + path: Tests/errors + + - name: Upload coverage + uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 + with: + flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} + name: ${{ matrix.os }} Python ${{ matrix.python-version }} + + success: + permissions: + contents: none + if: always() + needs: build + runs-on: ubuntu-latest + name: Test Successful + steps: + - name: Success + env: + BUILD_RESULT: ${{ needs.build.result }} + run: test "$BUILD_RESULT" = "success" diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index d5c866ca8e2..18006403bf0 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -296,11 +296,7 @@ function build { else sed "s/\${pc_sysrootdir\}//" $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc fi - - echo FIXME - set build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib - cat libxcb*/config.log fi # -DCMAKE_POSITION_INDEPENDENT_CODE=1 is a workaround from https://github.com/libjpeg-turbo/libjpeg-turbo/issues/898 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 72254f1d5cd..97cbad5710e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -10,6 +10,23 @@ on: # │ │ │ │ │ - cron: "42 1 * * 0,3" push: + paths: &paths + - ".ci/requirements-cibw.txt" + - ".ci/requirements-sbom.txt" + - ".github/compare-dist-sizes.py" + - ".github/dependencies.json" + - ".github/embed-sbom.py" + - ".github/generate-sbom.py" + - ".github/workflows/wheels*" + - "pyproject.toml" + - "setup.py" + - "wheels/*" + - "winbuild/build_prepare.py" + - "winbuild/fribidi.cmake" + tags: + - "*" + pull_request: + paths: *paths workflow_dispatch: permissions: @@ -66,64 +83,63 @@ jobs: cibw_arch: x86_64 build: "cp311*" macosx_deployment_target: "10.10" - # FIXME - # - name: "macOS 10.13 x86_64" - # platform: macos - # os: macos-26-intel - # cibw_arch: x86_64 - # build: "cp3{12,13}*" - # macosx_deployment_target: "10.13" - # - name: "macOS 10.15 x86_64" - # platform: macos - # os: macos-26-intel - # cibw_arch: x86_64 - # build: "{cp314,cp315,pp3}*" - # macosx_deployment_target: "10.15" - # - name: "macOS arm64" - # platform: macos - # os: macos-latest - # cibw_arch: arm64 - # macosx_deployment_target: "11.0" - # - name: "manylinux_2_28 x86_64" - # platform: linux - # os: ubuntu-latest - # cibw_arch: x86_64 - # build: "*manylinux*" - # - name: "musllinux x86_64" - # platform: linux - # os: ubuntu-latest - # cibw_arch: x86_64 - # build: "*musllinux*" - # - name: "manylinux_2_28 aarch64" - # platform: linux - # os: ubuntu-24.04-arm - # cibw_arch: aarch64 - # build: "*manylinux*" - # - name: "musllinux aarch64" - # platform: linux - # os: ubuntu-24.04-arm - # cibw_arch: aarch64 - # build: "*musllinux*" - # - name: "Android arm64" - # platform: android - # os: ubuntu-latest - # cibw_arch: arm64_v8a - # - name: "Android x86_64" - # platform: android - # os: ubuntu-latest - # cibw_arch: x86_64 - # - name: "iOS arm64 device" - # platform: ios - # os: macos-latest - # cibw_arch: arm64_iphoneos - # - name: "iOS arm64 simulator" - # platform: ios - # os: macos-latest - # cibw_arch: arm64_iphonesimulator - # - name: "iOS x86_64 simulator" - # platform: ios - # os: macos-26-intel - # cibw_arch: x86_64_iphonesimulator + - name: "macOS 10.13 x86_64" + platform: macos + os: macos-26-intel + cibw_arch: x86_64 + build: "cp3{12,13}*" + macosx_deployment_target: "10.13" + - name: "macOS 10.15 x86_64" + platform: macos + os: macos-26-intel + cibw_arch: x86_64 + build: "{cp314,cp315,pp3}*" + macosx_deployment_target: "10.15" + - name: "macOS arm64" + platform: macos + os: macos-latest + cibw_arch: arm64 + macosx_deployment_target: "11.0" + - name: "manylinux_2_28 x86_64" + platform: linux + os: ubuntu-latest + cibw_arch: x86_64 + build: "*manylinux*" + - name: "musllinux x86_64" + platform: linux + os: ubuntu-latest + cibw_arch: x86_64 + build: "*musllinux*" + - name: "manylinux_2_28 aarch64" + platform: linux + os: ubuntu-24.04-arm + cibw_arch: aarch64 + build: "*manylinux*" + - name: "musllinux aarch64" + platform: linux + os: ubuntu-24.04-arm + cibw_arch: aarch64 + build: "*musllinux*" + - name: "Android arm64" + platform: android + os: ubuntu-latest + cibw_arch: arm64_v8a + - name: "Android x86_64" + platform: android + os: ubuntu-latest + cibw_arch: x86_64 + - name: "iOS arm64 device" + platform: ios + os: macos-latest + cibw_arch: arm64_iphoneos + - name: "iOS arm64 simulator" + platform: ios + os: macos-latest + cibw_arch: arm64_iphonesimulator + - name: "iOS x86_64 simulator" + platform: ios + os: macos-26-intel + cibw_arch: x86_64_iphonesimulator steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: From a69654137efe921f1179bcd04cd2e92e3b150bc7 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sun, 25 Oct 2026 20:43:57 +0000 Subject: [PATCH 20/21] Cleanups --- .github/workflows/wheels-dependencies.sh | 23 ++++++++++------------- .github/workflows/wheels.yml | 2 +- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 18006403bf0..6e03bf096c1 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -30,8 +30,7 @@ if [ "$CIBW_OS" = "android" ]; then echo "ERROR: Unknown Android architecture '$CIBW_ARCH'" exit 1 esac - HOST_TRIPLE="$PLAT-linux-android" - HOST_CONFIGURE_FLAGS="--host=$HOST_TRIPLE" + HOST_CONFIGURE_FLAGS="--host=$PLAT-linux-android" BUILD_PREFIX=$WORKDIR/prefix # When building for Android, cibuildwheel sets the CMAKE_TOOLCHAIN_FILE environment @@ -60,9 +59,9 @@ elif [ "$CIBW_OS" = "ios" ]; then IOS_SDK_PATH=$(xcrun --sdk $IOS_SDK --show-sdk-path) CMAKE_SYSTEM_NAME=iOS - HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET + IOS_HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET if [[ "$IOS_SDK" == "iphonesimulator" ]]; then - HOST_TRIPLE=$HOST_TRIPLE-simulator + IOS_HOST_TRIPLE=$IOS_HOST_TRIPLE-simulator fi # GNU Autotools doesn't recognize the existence of arm64-apple-ios-simulator @@ -221,7 +220,7 @@ function build_libavif { # Use AOM for both encoding and decoding, and don't enable dav1d. libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic) else - # Use AOM for encoding and dav1d for decoding. + # Use AOM for encoding, and dav1d for decoding. libavif_cmake_flags+=( -DAVIF_CODEC_AOM_DECODE=OFF \ -DAVIF_CODEC_DAV1D=LOCAL @@ -264,14 +263,12 @@ function build_libavif { function build_zstd { if [ -e zstd-stamp ]; then return; fi local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz) - + local zstd_args="PREFIX=$BUILD_PREFIX" if [ -n "$IS_ANDROID" ]; then - zstd_uname="Linux" - else - zstd_uname=$(uname) + zstd_args+=" UNAME_TARGET_SYSTEM=Linux" fi (cd $out_dir \ - && make -j4 install UNAME_TARGET_SYSTEM=$zstd_uname PREFIX="$BUILD_PREFIX") + && make -j4 install $zstd_args) touch zstd-stamp } @@ -433,7 +430,7 @@ else # This command only searches PKG_CONFIG_PATH. export PKG_CONFIG=$(which pkgconf) fi -export PKG_CONFIG_LIBDIR="" +export PKG_CONFIG_LIBDIR="" # https://github.com/pypackaging-native/pkgconf-pypi/issues/110#issuecomment-6402850060 export PKG_CONFIG_PATH="$BUILD_PREFIX/lib/pkgconfig:$BUILD_PREFIX/share/pkgconfig" if [[ -n "$IS_MACOS" ]]; then @@ -453,8 +450,8 @@ if [[ -n "$IS_MACOS" ]]; then export STRIP=$(xcrun --find --sdk $IOS_SDK strip) CPPFLAGS="$CPPFLAGS --sysroot=$IOS_SDK_PATH" - CFLAGS="-target $HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" - CXXFLAGS="-target $HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" + CFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" + CXXFLAGS="-target $IOS_HOST_TRIPLE --sysroot=$IOS_SDK_PATH -mios-version-min=$IPHONEOS_DEPLOYMENT_TARGET" # Having IPHONEOS_DEPLOYMENT_TARGET in the environment causes problems # with some cross-building toolchains, because it introduces implicit diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 97cbad5710e..38bcb8f444a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -37,7 +37,7 @@ concurrency: cancel-in-progress: true env: - EXPECTED_DISTS: 83 + EXPECTED_DISTS: 89 FORCE_COLOR: 1 jobs: From 22c1120992d2230603088602069a54cc5e295ce1 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Sun, 25 Oct 2026 20:58:52 +0000 Subject: [PATCH 21/21] Don't set PKG_CONFIG_LIBDIR on Linux --- .github/workflows/wheels-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 6e03bf096c1..cf71d79983c 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -429,8 +429,8 @@ if [ -n "$IS_LINUX" ]; then else # This command only searches PKG_CONFIG_PATH. export PKG_CONFIG=$(which pkgconf) + export PKG_CONFIG_LIBDIR="" # https://github.com/pypackaging-native/pkgconf-pypi/issues/110#issuecomment-6402850060 fi -export PKG_CONFIG_LIBDIR="" # https://github.com/pypackaging-native/pkgconf-pypi/issues/110#issuecomment-6402850060 export PKG_CONFIG_PATH="$BUILD_PREFIX/lib/pkgconfig:$BUILD_PREFIX/share/pkgconfig" if [[ -n "$IS_MACOS" ]]; then