Add tooling to support iOS build.

This commit is contained in:
Russell Keith-Magee 2025-02-17 08:54:20 +08:00
parent ef0bab0c65
commit 3c4b6191eb
No known key found for this signature in database
GPG Key ID: 3D2DAB6A37BB5BC3
30 changed files with 414 additions and 65 deletions

View File

@ -1,31 +1,82 @@
#!/bin/bash
# Setup that needs to be done before multibuild utils are invoked
# 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).
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
# 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 [[ "$(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).
if [[ -z "$CIBW_ARCHS" ]]; then
echo "ERROR: Pillow macOS 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."
exit 1
if [[ "$CIBW_PLATFORM" == "ios" ]]; then
# 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)
if [[ "$IOS_SDK" == "iphonesimulator" ]]; then
CMAKE_SYSTEM_NAME=iOS
IOS_HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET-simulator
else
CMAKE_SYSTEM_NAME=iOS
IOS_HOST_TRIPLE=$PLAT-apple-ios$IPHONEOS_DEPLOYMENT_TARGET
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
# Build macOS dependencies in `build/darwin`
# Install them into `build/deps/darwin`
PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}"
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
@ -36,7 +87,9 @@ 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,31 +100,57 @@ 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
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0
BROTLI_VERSION=1.1.0 # Patched
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
@ -79,20 +158,25 @@ function build_zlib_ng {
function build_brotli {
if [ -e brotli-stamp ]; then return; fi
local name=brotli
local version=$BROTLI_VERSION
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
}
function build_harfbuzz {
if [ -e harfbuzz-stamp ]; then return; fi
python3 -m pip install meson ninja
local name=harfbuzz
local version=$HARFBUZZ_VERSION
python3 -m pip install --disable-pip-version-check 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 \
&& 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
@ -110,19 +194,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 \
@ -146,14 +230,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.
@ -172,24 +286,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
python3 -m pip install cmake
# Ensure cmake is available, and that the default prefix used by CMake is
# the build prefix
python3 -m pip install --disable-pip-version-check 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

@ -23,7 +23,7 @@ cd $pillow
if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python selftest.py
if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python -m pytest -vx Tests\check_wheel.py
& $venv\Scripts\$python -m pytest -vx checks\check_wheel.py
if (!$?) { exit $LASTEXITCODE }
& $venv\Scripts\$python -m pytest -vx 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 Tests/check_wheel.py
python3 -m pytest checks/check_wheel.py
python3 -m pytest

View File

@ -51,40 +51,66 @@ 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}*"
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-13
cibw_arch: arm64
build: "*iphoneos"
iphoneos_deployment_target: "13.0"
- name: "iOS arm64 simulator"
platform: ios
os: macos-13
cibw_arch: arm64
build: "*iphonesimulator"
iphoneos_deployment_target: "13.0"
- name: "iOS x86_64 simulator"
platform: ios
os: macos-latest
cibw_arch: x86_64
build: "*iphonesimulator"
iphoneos_deployment_target: "13.0"
steps:
- uses: actions/checkout@v4
with:
@ -103,6 +129,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
@ -111,10 +138,11 @@ jobs:
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
IPHONEOS_DEPLOYMENT_TARGET: ${{ matrix.iphoneos_deployment_target }}
- 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:

4
.gitignore vendored
View File

@ -80,6 +80,10 @@ docs/_build/
# JetBrains
.idea
# Files downloaded as part of the build process
pillow-depends-main.zip
pillow-test-images.zip
# Extra test images installed from python-pillow/test-images
Tests/images/README.md
Tests/images/crash_1.tif

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.5
@ -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.0

View File

@ -10,8 +10,12 @@ 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 in {"ios", "android"}:
pytest.skip(
"Fuzzer doesn't run on Windows or mobile",
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,10 @@ import sys
import pytest
@pytest.mark.skipif(
sys.platform in {"ios", "android"},
reason="Not required on mobile",
)
@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

@ -5,7 +5,7 @@ import sys
from PIL import features
from .helper import is_pypy
from Tests.helper import is_pypy
def test_wheel_modules() -> None:
@ -19,6 +19,9 @@ def test_wheel_modules() -> None:
assert tkinter
except ImportError:
expected_modules.remove("tkinter")
elif sys.platform == "ios":
# tkinter is not available on iOS
expected_modules.remove("tkinter")
assert set(features.get_supported_modules()) == expected_modules
@ -46,5 +49,11 @@ 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;
# fribid and harfbuzz won't be available if raqm isn't available.
expected_features.remove("fribidi")
expected_features.remove("raqm")
expected_features.remove("harfbuzz")
assert set(features.get_supported_features()) == expected_features

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,43 @@
# Brotli doesn't have explicit support for iOS as a CMAKE_SYSTEM_NAME.
#
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,39 @@
# 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.
#
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,49 @@ 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",
"pyproject.toml",
"selftest.py",
]
test-command = "python -m pytest -vv selftest.py 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.black]
exclude = "wheels/multibuild"
@ -168,7 +202,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

@ -474,6 +474,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] = []
@ -615,14 +628,6 @@ class pil_build_ext(build_ext):
_add_directory(library_dirs, "/usr/X11/lib")
_add_directory(include_dirs, "/usr/X11/include")
# Add the macOS SDK path.
sdk_path = self.get_macos_sdk_path()
if 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)
@ -677,6 +682,27 @@ class pil_build_ext(build_ext):
_add_directory(library_dirs, os.path.join(best_path, "lib"))
_add_directory(include_dirs, os.path.join(best_path, "include"))
elif sys.platform == "darwin":
# Alwasy include the macOS SDK path.
sdk_path = self.get_macos_sdk_path()
if 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 == "ios":
# Always include 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"]
#
# insert new dirs *before* default libs, to avoid conflicts
# between Python PYD stub libs and real libraries
@ -878,6 +904,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))
@ -894,6 +923,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"])
@ -925,6 +957,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:
@ -941,6 +978,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")