diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 625e54c050b..cf71d79983c 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -1,38 +1,54 @@ #!/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 # up the cross environment. PROJECTDIR=$(pwd) PATCH_DIR=$(pwd)/patches -if [[ "$CIBW_PLATFORM" == "ios" ]]; then - check_cibw_archs - # On iOS, CIBW_ARCHS is actually a multi-arch - arm64_iphoneos, + +# 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 + +if [ "$CIBW_OS" = "android" ]; then + IS_ANDROID=1 + case "$CIBW_ARCH" in + arm64_v8a) PLAT=aarch64 ;; + x86_64) PLAT=x86_64 ;; + *) + echo "ERROR: Unknown Android architecture '$CIBW_ARCH'" + exit 1 + esac + HOST_CONFIGURE_FLAGS="--host=$PLAT-linux-android" + 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 + # 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/") + BUILD_PREFIX=$WORKDIR/prefix # GNU tooling insists on using aarch64 rather than arm64 if [[ $PLAT == "arm64" ]]; then @@ -66,27 +82,41 @@ if [[ "$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 +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 - PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" - WORKDIR=$(pwd)/build + 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 # Define custom utilities source wheels/multibuild/common_utils.sh + +# 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 + source wheels/multibuild/library_builders.sh -if [[ -z "$IS_MACOS" ]]; then +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 @@ -108,34 +138,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. - # 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 @@ -143,6 +145,9 @@ 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 build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --installnamedir=$BUILD_PREFIX/lib --zlib-compat @@ -161,7 +166,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 \ @@ -174,10 +178,13 @@ 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 + # 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 + local have_nasm=1 fi local build_shared=ON @@ -196,11 +203,24 @@ 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 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") 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 @@ -243,14 +263,18 @@ 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_args+=" UNAME_TARGET_SYSTEM=Linux" + fi (cd $out_dir \ - && make -j4 install) + && make -j4 install $zstd_args) 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 @@ -259,22 +283,25 @@ 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 - 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=( @@ -325,8 +352,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 [[ -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 @@ -339,7 +366,7 @@ function build { function create_meson_cross_config { cat << EOF > $WORKDIR/meson-cross.txt [binaries] -pkg-config = '$BUILD_PREFIX/bin/pkg-config' +pkg-config = '$PKG_CONFIG' cmake = '$(which cmake)' c = '$CC' cpp = '$CXX' @@ -377,19 +404,41 @@ 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. +# +# 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_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) + export PKG_CONFIG_LIBDIR="" # https://github.com/pypackaging-native/pkgconf-pypi/issues/110#issuecomment-6402850060 +fi +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" 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/.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/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 228a5052bff..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: @@ -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/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)), 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"} diff --git a/docs/releasenotes/13.1.0.rst b/docs/releasenotes/13.1.0.rst index 5edc0d28693..69093672249 100644 --- a/docs/releasenotes/13.1.0.rst +++ b/docs/releasenotes/13.1.0.rst @@ -49,6 +49,11 @@ TODO Other changes ============= +Android +^^^^^^^ + +Android ARM64 and x86_64 wheels are now built for Python 3.13 and later. + Resizing I;16B and I;16N images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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 d978f394324..d74e352363f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,7 +101,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", @@ -110,36 +110,39 @@ 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\ - """ -# Disable platform guessing on iOS, and disable raqm (since there won't be a +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]] +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 = [ - "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 = [ +config-settings = "raqm=disable imagequant=disable platform-guessing=disable" +# Mobile platforms only support Python test-commands, not shell scripts. +test-command = "python -m wheels_test" +test-sources = [ "checks", "Tests", "selftest.py", + "wheels_test.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 = [] + +[[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" diff --git a/setup.py b/setup.py index 2d02cf129ca..e793d37cdad 100644 --- a/setup.py +++ b/setup.py @@ -280,6 +280,12 @@ 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.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: command_libs = [command, "--libs-only-L", name] @@ -294,13 +300,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 +941,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"]) 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")