Add support for iOS (#9030)

Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
This commit is contained in:
Russell Keith-Magee 2025-06-30 21:46:07 +10:00 committed by GitHub
parent be2b4e7864
commit da10ed1cf3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 406 additions and 58 deletions

View File

@ -1,42 +1,98 @@
#!/bin/bash
# Setup that needs to be done before multibuild utils are invoked
PROJECTDIR=$(pwd)
if [[ "$(uname -s)" == "Darwin" ]]; then
# Safety check - macOS builds require that CIBW_ARCHS is set, and that it
# only contains a single value (even though cibuildwheel allows multiple
# values in CIBW_ARCHS).
# 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 macOS builds require CIBW_ARCHS be defined."
echo "ERROR: Pillow builds require CIBW_ARCHS be defined."
exit 1
fi
if [[ "$CIBW_ARCHS" == *" "* ]]; then
echo "ERROR: Pillow macOS builds only support a single architecture in CIBW_ARCHS."
echo "ERROR: Pillow builds only support a single architecture in CIBW_ARCHS."
exit 1
fi
}
# Setup that needs to be done before multibuild utils are invoked. Process
# potential cross-build platforms before native platforms to ensure that we pick
# up the cross environment.
PROJECTDIR=$(pwd)
if [[ "$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
# 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
PATCH_DIR=$(pwd)/patches/iOS
# GNU tooling insists on using aarch64 rather than arm64
if [[ $PLAT == "arm64" ]]; then
GNU_ARCH=aarch64
else
GNU_ARCH=x86_64
fi
IOS_SDK_PATH=$(xcrun --sdk $IOS_SDK --show-sdk-path)
CMAKE_SYSTEM_NAME=iOS
IOS_HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET
if [[ "$IOS_SDK" == "iphonesimulator" ]]; then
IOS_HOST_TRIPLE=$IOS_HOST_TRIPLE-simulator
fi
# GNU Autotools doesn't recognize the existence of arm64-apple-ios-simulator
# as a valid host. However, the only difference between arm64-apple-ios and
# arm64-apple-ios-simulator is the choice of sysroot, and that is
# coordinated by CC, CFLAGS etc. From the perspective of configure, the two
# platforms are identical, so we can use arm64-apple-ios consistently.
# This (mostly) avoids us needing to patch config.sub in dependency sources.
HOST_CONFIGURE_FLAGS="--disable-shared --enable-static --host=$GNU_ARCH-apple-ios --build=$GNU_ARCH-apple-darwin"
# CMake has native support for iOS. However, most of that support is based
# on using the Xcode builder, which isn't very helpful for most of Pillow's
# dependencies. Therefore, we lean on the OSX configurations, plus CC, CFLAGS
# etc. to ensure the right sysroot is selected.
HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO"
# Meson needs to be pointed at a cross-platform configuration file
# 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
MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
MB_ML_VER=${AUDITWHEEL_POLICY:9}
fi
PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}"
# Define custom utilities
source wheels/multibuild/common_utils.sh
source wheels/multibuild/library_builders.sh
if [ -z "$IS_MACOS" ]; then
if [[ -z "$IS_MACOS" ]]; then
source wheels/multibuild/manylinux_utils.sh
fi
ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
# Package versions for fresh source builds. Version numbers with "Patched"
# annotations have a source code patch that is required for some platforms. If
# you change those versions, ensure the patch is also updated.
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.2.1
LIBPNG_VERSION=1.6.49
@ -47,32 +103,58 @@ TIFF_VERSION=4.7.0
LCMS2_VERSION=2.17
ZLIB_VERSION=1.3.1
ZLIB_NG_VERSION=2.2.4
LIBWEBP_VERSION=1.5.0
LIBWEBP_VERSION=1.5.0 # Patched; next release won't need patching. See patch file.
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0
BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file.
LIBAVIF_VERSION=1.3.0
function build_pkg_config {
if [ -e pkg-config-stamp ]; then return; fi
# This essentially duplicates the Homebrew recipe
CFLAGS="$CFLAGS -Wno-int-conversion" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \
# 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
# macOS-compatible build, we temporarily clear environment flags that set
# iOS-specific values.
if [[ -n "$IOS_SDK" ]]; 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 [[ -n "$IOS_SDK" ]]; 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
# it doesn't honor the usual flags. Temporarily disable any
# cross-compilation flags.
ORIGINAL_HOST_CONFIGURE_FLAGS=$HOST_CONFIGURE_FLAGS
unset HOST_CONFIGURE_FLAGS
build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat
if [ -n "$IS_MACOS" ]; then
HOST_CONFIGURE_FLAGS=$ORIGINAL_HOST_CONFIGURE_FLAGS
if [[ -n "$IS_MACOS" ]] && [[ -z "$IOS_SDK" ]]; then
# Ensure that on macOS, the library name is an absolute path, not an
# @rpath, so that delocate picks up the right library (and doesn't need
# DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an
# option to control the install_name.
# option to control the install_name. This isn't needed on iOS, as iOS
# only builds the static library.
install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib
fi
touch zlib-stamp
@ -82,7 +164,7 @@ function build_brotli {
if [ -e brotli-stamp ]; then return; fi
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
(cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \
&& make install)
touch brotli-stamp
}
@ -93,7 +175,7 @@ function build_harfbuzz {
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 \
&& meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled)
&& meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled $HOST_MESON_FLAGS)
(cd $out_dir/build \
&& meson install)
touch harfbuzz-stamp
@ -164,19 +246,19 @@ function build {
fi
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then
if [[ -n "$IS_MACOS" ]]; then
build_simple xorgproto 2024.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
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
build_libjpeg_turbo
if [ -n "$IS_MACOS" ]; then
if [[ -n "$IS_MACOS" ]]; then
# Custom tiff build to include jpeg; by default, configure won't include
# headers/libs in the custom macOS prefix. Explicitly disable webp,
# headers/libs in the custom macOS/iOS 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.
build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \
@ -186,7 +268,10 @@ function build {
build_tiff
fi
build_libavif
if [[ -z "$IOS_SDK" ]]; then
# Short term workaround; don't build libavif on iOS
build_libavif
fi
build_libpng
build_lcms2
build_openjpeg
@ -201,14 +286,44 @@ function build {
build_brotli
if [ -n "$IS_MACOS" ]; then
if [[ -n "$IS_MACOS" ]]; then
# Custom freetype build
build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
else
build_freetype
fi
build_harfbuzz
if [[ -z "$IOS_SDK" ]]; then
# On iOS, there's no vendor-provided raqm, and we can't ship it due to
# licensing, so there's no point building harfbuzz.
build_harfbuzz
fi
}
function create_meson_cross_config {
cat << EOF > $WORKDIR/meson-cross.txt
[binaries]
pkg-config = '$BUILD_PREFIX/bin/pkg-config'
cmake = '$(which cmake)'
c = '$CC'
cpp = '$CXX'
strip = '$STRIP'
[built-in options]
c_args = '$CFLAGS -I$BUILD_PREFIX/include'
cpp_args = '$CXXFLAGS -I$BUILD_PREFIX/include'
c_link_args = '$CFLAGS -L$BUILD_PREFIX/lib'
cpp_link_args = '$CFLAGS -L$BUILD_PREFIX/lib'
[host_machine]
system = 'darwin'
subsystem = 'ios'
kernel = 'xnu'
cpu_family = '$(uname -m)'
cpu = '$(uname -m)'
endian = 'little'
EOF
}
# Perform all dependency builds in the build subfolder.
@ -227,24 +342,40 @@ if [[ ! -d $WORKDIR/pillow-depends-main ]]; then
fi
if [[ -n "$IS_MACOS" ]]; then
# Homebrew (or similar packaging environments) install can contain some of
# the libraries that we're going to build. However, they may be compiled
# with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use,
# and they may bring in other dependencies that we don't want. The same will
# be true of any other locations on the path. To avoid conflicts, strip the
# path down to the bare minimum (which, on macOS, won't include any
# development dependencies).
export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
export CMAKE_PREFIX_PATH=$BUILD_PREFIX
# Ensure the basic structure of the build prefix directory exists.
mkdir -p "$BUILD_PREFIX/bin"
mkdir -p "$BUILD_PREFIX/lib"
# Ensure pkg-config is available
# 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
# Ensure cmake is available, and that the default prefix used by CMake is
# the build prefix
python3 -m pip install cmake
export CMAKE_PREFIX_PATH=$BUILD_PREFIX
if [[ -n "$IOS_SDK" ]]; then
export AR="$(xcrun --find --sdk $IOS_SDK ar)"
export CPP="$(xcrun --find --sdk $IOS_SDK clang) -E"
export CC=$(xcrun --find --sdk $IOS_SDK clang)
export CXX=$(xcrun --find --sdk $IOS_SDK clang++)
export LD=$(xcrun --find --sdk $IOS_SDK ld)
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"
# Having IPHONEOS_DEPLOYMENT_TARGET in the environment causes problems
# with some cross-building toolchains, because it introduces implicit
# behavior into clang.
unset IPHONEOS_DEPLOYMENT_TARGET
# Now that we know CC etc., we can create a meson cross-configuration file
create_meson_cross_config
fi
fi
wrap_wheel_builder build

View File

@ -15,15 +15,12 @@ if (Test-Path $venv\Scripts\pypy.exe) {
$python = "python.exe"
}
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
& $venv\Scripts\$python -m pip install numpy
}
cd $pillow
& $venv\Scripts\$python -VV
if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python selftest.py
if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python -m pytest -vv -x Tests\check_wheel.py
& $venv\Scripts\$python -m pytest -vv -x checks\check_wheel.py
if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python -m pytest -vv -x Tests
if (!$?) { exit $LASTEXITCODE }

View File

@ -25,8 +25,6 @@ else
yum install -y fribidi
fi
python3 -m pip install numpy
if [ ! -d "test-images-main" ]; then
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
unzip pillow-test-images.zip
@ -35,5 +33,5 @@ fi
# Runs tests
python3 selftest.py
python3 -m pytest -vv -x Tests/check_wheel.py
python3 -m pytest -vv -x checks/check_wheel.py
python3 -m pytest -vv -x

View File

@ -51,40 +51,60 @@ jobs:
matrix:
include:
- name: "macOS 10.10 x86_64"
platform: macos
os: macos-13
cibw_arch: x86_64
build: "cp3{9,10,11}*"
macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64"
platform: macos
os: macos-13
cibw_arch: x86_64
build: "cp3{12,13,14}*"
macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64"
platform: macos
os: macos-13
cibw_arch: x86_64
build: "pp3*"
macosx_deployment_target: "10.15"
- name: "macOS arm64"
platform: macos
os: macos-latest
cibw_arch: arm64
macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64"
platform: linux
os: ubuntu-latest
cibw_arch: x86_64
- name: "manylinux_2_28 x86_64"
platform: linux
os: ubuntu-latest
cibw_arch: x86_64
build: "*manylinux*"
manylinux: "manylinux_2_28"
- name: "manylinux2014 and musllinux aarch64"
platform: linux
os: ubuntu-24.04-arm
cibw_arch: aarch64
- name: "manylinux_2_28 aarch64"
platform: linux
os: ubuntu-24.04-arm
cibw_arch: aarch64
build: "*manylinux*"
manylinux: "manylinux_2_28"
- 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-13
cibw_arch: x86_64_iphonesimulator
steps:
- uses: actions/checkout@v4
with:
@ -103,6 +123,7 @@ jobs:
run: |
python3 -m cibuildwheel --output-dir wheelhouse
env:
CIBW_PLATFORM: ${{ matrix.platform }}
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
@ -114,7 +135,7 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.os }}${{ matrix.macosx_deployment_target && format('-{0}', matrix.macosx_deployment_target) }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
name: dist-${{ matrix.name }}
path: ./wheelhouse/*.whl
windows:

View File

@ -21,7 +21,7 @@ repos:
rev: v1.5.5
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v20.1.6
@ -46,9 +46,9 @@ repos:
- id: check-yaml
args: [--allow-multiple-documents]
- id: end-of-file-fixer
exclude: ^Tests/images/
exclude: ^Tests/images/|\.patch$
- id: trailing-whitespace
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.1

View File

@ -13,6 +13,8 @@ include LICENSE
include Makefile
include tox.ini
graft Tests
graft checks
graft patches
graft src
graft depends
graft winbuild

View File

@ -10,8 +10,9 @@ import pytest
from PIL import Image, features
from Tests.helper import skip_unless_feature
if sys.platform.startswith("win32"):
pytest.skip("Fuzzer is linux only", allow_module_level=True)
if sys.platform.startswith("win32") or sys.platform == "ios":
pytest.skip("Fuzzer doesn't run on Windows or iOS", allow_module_level=True)
libjpeg_turbo_version = features.version("libjpeg_turbo")
if libjpeg_turbo_version is not None:
version = packaging.version.parse(libjpeg_turbo_version)

View File

@ -7,6 +7,7 @@ import sys
import pytest
@pytest.mark.skipif(sys.platform == "ios", reason="Processes not supported on iOS")
@pytest.mark.parametrize(
"args, report",
((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)),

View File

@ -1,5 +1,7 @@
from __future__ import annotations
from importlib.metadata import metadata
import pytest
from PIL import __version__
@ -9,7 +11,7 @@ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
def test_pyroma() -> None:
# Arrange
data = pyroma.projectdata.get_data(".")
data = pyroma.projectdata.map_metadata_keys(metadata("Pillow"))
# Act
rating = pyroma.ratings.rate(data)

View File

@ -4,8 +4,7 @@ import platform
import sys
from PIL import features
from .helper import is_pypy
from Tests.helper import is_pypy
def test_wheel_modules() -> None:
@ -24,6 +23,11 @@ def test_wheel_modules() -> None:
if platform.machine() == "ARM64":
expected_modules.remove("avif")
elif sys.platform == "ios":
# tkinter is not available on iOS
# libavif is not available on iOS (for now)
expected_modules -= {"tkinter", "avif"}
assert set(features.get_supported_modules()) == expected_modules
@ -50,5 +54,9 @@ def test_wheel_features() -> None:
expected_features.remove("xcb")
elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm":
expected_features.remove("zlib_ng")
elif sys.platform == "ios":
# 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"}
assert set(features.get_supported_features()) == expected_features

View File

@ -68,7 +68,13 @@ AVIF support in wheels
^^^^^^^^^^^^^^^^^^^^^^
Support for reading and writing AVIF images is now included in Pillow's wheels, except
for Windows ARM64. libaom is available as an encoder and dav1d as a decoder.
for Windows ARM64 and iOS. libaom is available as an encoder and dav1d as a decoder.
iOS
^^^
Pillow now provides wheels that can be used on iOS ARM64 devices, and on the iOS
simulator on ARM64 and x86_64. Currently, only Python 3.13 wheels are available.
Python 3.14 beta
^^^^^^^^^^^^^^^^

14
patches/README.md Normal file
View File

@ -0,0 +1,14 @@
Although we try to use official sources for dependencies, sometimes the official
sources don't support a platform (especially mobile platforms), or there's a bug
fix/feature that is required to support Pillow's usage.
This folder contains patches that must be applied to official sources, organized
by the platforms that need those patches.
Each patch is against the root of the unpacked official tarball, and is named by
appending `.patch` to the end of the tarball that is to be patched. This
includes the full version number; so if the version is bumped, the patch will
at a minimum require a filename change.
Wherever possible, these patches should be contributed upstream, in the hope that
future Pillow versions won't need to maintain these patches.

View File

@ -0,0 +1,46 @@
# Brotli 1.1.0 doesn't have explicit support for iOS as a CMAKE_SYSTEM_NAME.
# That release was from 2023; there have been subsequent changes that allow
# Brotli to build on iOS without any patches, as long as -DBROTLI_BUILD_TOOLS=NO
# is specified on the command line.
#
diff -ru brotli-1.1.0-orig/CMakeLists.txt brotli-1.1.0/CMakeLists.txt
--- brotli-1.1.0-orig/CMakeLists.txt 2023-08-29 19:00:29
+++ brotli-1.1.0/CMakeLists.txt 2024-11-07 10:46:26
@@ -114,6 +114,8 @@
add_definitions(-DOS_MACOSX)
set(CMAKE_MACOS_RPATH TRUE)
set(CMAKE_INSTALL_NAME_DIR "${CMAKE_INSTALL_PREFIX}/lib")
+elseif(${CMAKE_SYSTEM_NAME} MATCHES "iOS")
+ add_definitions(-DOS_IOS)
endif()
if(BROTLI_EMSCRIPTEN)
@@ -174,10 +176,12 @@
# Installation
if(NOT BROTLI_BUNDLED_MODE)
- install(
- TARGETS brotli
- RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
- )
+ if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "iOS")
+ install(
+ TARGETS brotli
+ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
+ )
+ endif()
install(
TARGETS ${BROTLI_LIBRARIES_CORE}
diff -ru brotli-1.1.0-orig/c/common/platform.h brotli-1.1.0/c/common/platform.h
--- brotli-1.1.0-orig/c/common/platform.h 2023-08-29 19:00:29
+++ brotli-1.1.0/c/common/platform.h 2024-11-07 10:47:28
@@ -33,7 +33,7 @@
#include <endian.h>
#elif defined(OS_FREEBSD)
#include <machine/endian.h>
-#elif defined(OS_MACOSX)
+#elif defined(OS_MACOSX) || defined(OS_IOS)
#include <machine/endian.h>
/* Let's try and follow the Linux convention */
#define BROTLI_X_BYTE_ORDER BYTE_ORDER

View File

@ -0,0 +1,42 @@
# libwebp example binaries require dependencies that aren't available for iOS builds.
# There's also no easy way to invoke the build to *exclude* the example builds.
# Since we don't need the examples anyway, remove them from the Makefile.
#
# As a point of reference, libwebp provides an XCFramework build script that involves
# 7 separate invocations of make to avoid building the examples. Patching the Makefile
# to remove the examples is a simpler approach, and one that is more compatible with
# the existing multibuild infrastructure.
#
# In the next release, it should be possible to pass --disable-libwebpexamples
# instead of applying this patch.
#
diff -ur libwebp-1.5.0-orig/Makefile.am libwebp-1.5.0/Makefile.am
--- libwebp-1.5.0-orig/Makefile.am 2024-12-20 09:17:50
+++ libwebp-1.5.0/Makefile.am 2025-01-09 11:24:17
@@ -5,5 +5,3 @@
if BUILD_EXTRAS
SUBDIRS += extras
endif
-
-SUBDIRS += examples
diff -ur libwebp-1.5.0-orig/Makefile.in libwebp-1.5.0/Makefile.in
--- libwebp-1.5.0-orig/Makefile.in 2024-12-20 09:52:53
+++ libwebp-1.5.0/Makefile.in 2025-01-09 11:24:17
@@ -156,7 +156,7 @@
unique=`for i in $$list; do \
if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
done | $(am__uniquify_input)`
-DIST_SUBDIRS = sharpyuv src imageio man extras examples
+DIST_SUBDIRS = sharpyuv src imageio man extras
am__DIST_COMMON = $(srcdir)/Makefile.in \
$(top_srcdir)/src/webp/config.h.in AUTHORS COPYING ChangeLog \
NEWS README.md ar-lib compile config.guess config.sub \
@@ -351,7 +351,7 @@
top_srcdir = @top_srcdir@
webp_libname_prefix = @webp_libname_prefix@
ACLOCAL_AMFLAGS = -I m4
-SUBDIRS = sharpyuv src imageio man $(am__append_1) examples
+SUBDIRS = sharpyuv src imageio man $(am__append_1)
EXTRA_DIST = COPYING autogen.sh
all: all-recursive

View File

@ -103,15 +103,55 @@ before-all = ".github/workflows/wheels-dependencies.sh"
build-verbosity = 1
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
# Disable platform guessing on macOS
macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests"
test-requires = [
"numpy",
]
xbuild-tools = [ ]
[tool.cibuildwheel.macos]
# Disable platform guessing on macOS to avoid picking up Homebrew etc.
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable"
[tool.cibuildwheel.macos.environment]
# Isolate macOS build environment from Homebrew etc.
PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
[tool.cibuildwheel.ios]
# Disable platform guessing on iOS, and disable raqm (since there won't be a
# vendor version, and we can't distribute it due to licensing)
config-settings = "raqm=disable imagequant=disable platform-guessing=disable"
# iOS needs to be given a specific pytest invocation and list of test sources.
test-sources = [
"checks",
"Tests",
"selftest.py",
]
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...)
test-requires = [ ]
[[tool.cibuildwheel.overrides]]
# iOS environment is isolated by cibuildwheel, but needs the dependencies
select = "*_iphoneos"
environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH"
[[tool.cibuildwheel.overrides]]
# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies
select = "*_iphonesimulator"
environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH"
[[tool.cibuildwheel.overrides]]
select = "*-win32"
test-requires = [ ]
[tool.black]
exclude = "wheels/multibuild"
@ -168,7 +208,7 @@ lint.isort.required-imports = [
max_supported_python = "3.13"
[tool.pytest.ini_options]
addopts = "-ra --color=yes"
addopts = "-ra --color=auto"
testpaths = [
"Tests",
]

View File

@ -473,6 +473,19 @@ class pil_build_ext(build_ext):
sdk_path = commandlinetools_sdk_path
return sdk_path
def get_ios_sdk_path(self) -> str:
try:
sdk = sys.implementation._multiarch.split("-")[-1]
_dbg("Using %s SDK", sdk)
return (
subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", sdk])
.strip()
.decode("latin1")
)
except Exception:
msg = "Unable to identify location of iOS SDK."
raise ValueError(msg)
def build_extensions(self) -> None:
library_dirs: list[str] = []
include_dirs: list[str] = []
@ -622,6 +635,18 @@ class pil_build_ext(build_ext):
for extension in self.extensions:
extension.extra_compile_args = ["-Wno-nullability-completeness"]
elif sys.platform == "ios":
# Add the iOS SDK path.
sdk_path = self.get_ios_sdk_path()
# Add the iOS SDK path.
_add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib"))
_add_directory(include_dirs, os.path.join(sdk_path, "usr", "include"))
for extension in self.extensions:
extension.extra_compile_args = ["-Wno-nullability-completeness"]
elif sys.platform.startswith(("linux", "gnu", "freebsd")):
for dirname in _find_library_dirs_ldconfig():
_add_directory(library_dirs, dirname)
@ -877,6 +902,9 @@ class pil_build_ext(build_ext):
# so we have to guess; by default it is defined in all Windows builds.
# See #4237, #5243, #5359 for more information.
defs.append(("USE_WIN32_FILEIO", None))
elif sys.platform == "ios":
# Ensure transitive dependencies are linked.
libs.append("lzma")
if feature.get("jpeg"):
libs.append(feature.get("jpeg"))
defs.append(("HAVE_LIBJPEG", None))
@ -893,6 +921,9 @@ class pil_build_ext(build_ext):
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"])
@ -924,6 +955,11 @@ class pil_build_ext(build_ext):
libs.append(feature.get("fribidi"))
else: # building FriBiDi shim from src/thirdparty
srcs.append("src/thirdparty/fribidi-shim/fribidi.c")
if sys.platform == "ios":
# Ensure transitive dependencies are linked.
libs.extend(["z", "bz2", "brotlicommon", "brotlidec", "png"])
self._update_extension("PIL._imagingft", libs, defs, srcs)
else:
@ -940,6 +976,9 @@ class pil_build_ext(build_ext):
webp = feature.get("webp")
if isinstance(webp, str):
libs = [webp, webp + "mux", webp + "demux"]
if sys.platform == "ios":
# Ensure transitive dependencies are linked.
libs.append("sharpyuv")
self._update_extension("PIL._webp", libs)
else:
self._remove_extension("PIL._webp")