From acf4eb28265d1ec6e957d2cf46ff2ba30dc10e69 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 17 Feb 2025 11:29:27 +0800 Subject: [PATCH 01/37] Add support for building iOS wheels. --- README.md | 94 ++- bin/generate_schema.py | 1 + cibuildwheel/__main__.py | 31 +- cibuildwheel/architecture.py | 40 +- cibuildwheel/ios.py | 635 ++++++++++++++++++ cibuildwheel/logger.py | 3 + cibuildwheel/resources/build-platforms.toml | 11 + .../resources/cibuildwheel.schema.json | 90 +++ cibuildwheel/typing.py | 20 +- cibuildwheel/util/file.py | 25 +- cibuildwheel/util/packaging.py | 5 +- docs/options.md | 74 +- docs/setup.md | 98 +-- examples/github-deploy.yml | 37 +- examples/github-minimal.yml | 36 +- examples/github-pipx.yml | 46 ++ test/test_ios.py | 99 +++ test/test_projects/c.py | 2 +- 18 files changed, 1190 insertions(+), 157 deletions(-) create mode 100644 cibuildwheel/ios.py create mode 100644 examples/github-pipx.yml create mode 100644 test/test_ios.py diff --git a/README.md b/README.md index 96d7cad34..7a8a09d0f 100644 --- a/README.md +++ b/README.md @@ -24,21 +24,21 @@ What does it do? While cibuildwheel itself requires a recent Python version to run (we support the last three releases), it can target the following versions to build wheels: -| | macOS Intel | macOS Apple Silicon | Windows 64bit | Windows 32bit | Windows Arm64 | manylinux
musllinux x86_64 | manylinux
musllinux i686 | manylinux
musllinux aarch64 | manylinux
musllinux ppc64le | manylinux
musllinux s390x | manylinux
musllinux armv7l | Pyodide | -|----------------|----|-----|-----|-----|-----|----|-----|----|-----|-----|---|-----| -| CPython 3.6 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | -| CPython 3.7 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | -| CPython 3.8 | ✅ | ✅ | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | -| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | -| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | -| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | -| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | ✅⁴ | -| CPython 3.13³ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | -| PyPy 3.7 v7.3 | ✅ | N/A | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | -| PyPy 3.8 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | -| PyPy 3.9 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | -| PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | -| PyPy 3.11 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | +| | macOS Intel | macOS Apple Silicon | Windows 64bit | Windows 32bit | Windows Arm64 | manylinux
musllinux x86_64 | manylinux
musllinux i686 | manylinux
musllinux aarch64 | manylinux
musllinux ppc64le | manylinux
musllinux s390x | manylinux
musllinux armv7l | iOS | Pyodide | +|----------------|----|-----|-----|-----|-----|----|-----|----|-----|-----|---|-----|-----| +| CPython 3.6 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | +| CPython 3.7 | ✅ | N/A | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | +| CPython 3.8 | ✅ | ✅ | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | +| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | +| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | +| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | N/A | +| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | N/A | ✅⁴ | +| CPython 3.13³ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅ | ✅ | ✅ | ✅ | ✅ | ✅⁵ | ✅ | N/A | +| PyPy 3.7 v7.3 | ✅ | N/A | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | +| PyPy 3.8 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | +| PyPy 3.9 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | +| PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | +| PyPy 3.11 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅¹ | ✅¹ | ✅¹ | N/A | N/A | N/A | N/A | N/A | ¹ PyPy is only supported for manylinux wheels.
² Windows arm64 support is experimental.
@@ -58,26 +58,28 @@ Usage `cibuildwheel` runs inside a CI service. Supported platforms depend on which service you're using: -| | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM | -|-----------------|-------|-------|---------|-----------|-----------|-------------| -| GitHub Actions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² | -| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² | -| Travis CI | ✅ | | ✅ | ✅ | | | -| AppVeyor | ✅ | ✅ | ✅ | | ✅ | ✅² | -| CircleCI | ✅ | ✅ | | ✅ | ✅ | | -| Gitlab CI | ✅ | ✅ | ✅ | ✅¹ | ✅ | | -| Cirrus CI | ✅ | ✅ | ✅ | ✅ | ✅ | | +| | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM | iOS | +|-----------------|-------|-------|---------|-----------|-----------|-------------|-----| +| GitHub Actions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅³ | +| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² | | +| Travis CI | ✅ | | ✅ | ✅ | | | | +| AppVeyor | ✅ | ✅ | ✅ | | ✅ | ✅² | | +| CircleCI | ✅ | ✅ | | ✅ | ✅ | | | +| Gitlab CI | ✅ | ✅ | ✅ | ✅¹ | ✅ | | | +| Cirrus CI | ✅ | ✅ | ✅ | ✅ | ✅ | | | ¹ [Requires emulation](https://cibuildwheel.pypa.io/en/stable/faq/#emulation), distributed separately. Other services may also support Linux ARM through emulation or third-party build hosts, but these are not tested in our CI.
-² [Uses cross-compilation](https://cibuildwheel.pypa.io/en/stable/faq/#windows-arm64). It is not possible to test `arm64` on this CI platform. +² [Uses cross-compilation](https://cibuildwheel.pypa.io/en/stable/faq/#windows-arm64). It is not possible to test `arm64` on this CI platform.
+³ Requires a macOS runner; runs tests on the simulator for the runner's architecture. Example setup ------------- -To build manylinux, musllinux, macOS, and Windows wheels on GitHub Actions, you could use this `.github/workflows/wheels.yml`: +To build manylinux, musllinux, macOS (producing x86_64, ARM64 and universal2 wheels), Windows, iOS and pyodide wheels on GitHub Actions, you could use this `.github/workflows/wheels.yml`: + ```yaml name: Build @@ -85,11 +87,33 @@ on: [push, pull_request] jobs: build_wheels: - name: Build wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} + name: Build wheels for ${{ matrix.os }} + runs-on: ${{ matrix.runs-on }} strategy: matrix: - os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-latest] + os: [ linux-intel, linux-arm, windows, macOS-intel, macOS-arm, iOS, pyodide ] + include: + - archs: auto + platform: auto + - os: linux-intel + runs-on: ubuntu-latest + - os: linux-arm + runs-on: ubuntu-24.04-arm + - os: windows + runs-on: windows-latest + - os: macOS-intel + # macos-13 was the last x86_64 runner + runs-on: macos-13 + - os: macOS-arm + # macos-14+ (including latest) are ARM64 runners + runs-on: macos-latest + archs: auto,universal2 + - os: iOS + runs-on: macos-latest + platform: ios + - os: pyodide + runs-on: ubuntu-latest + platform: pyodide steps: - uses: actions/checkout@v4 @@ -102,15 +126,21 @@ jobs: - name: Build wheels run: python -m cibuildwheel --output-dir wheelhouse - # to supply options, put them in 'env', like: - # env: - # CIBW_SOME_OPTION: value + env: + CIBW_PLATFORM: ${{ matrix.platform }} + CIBW_ARCHS: ${{ matrix.archs }} + # Can also be configured directly, using `with:` + # with: + # package-dir: . + # output-dir: wheelhouse + # config-file: "{package}/pyproject.toml" - uses: actions/upload-artifact@v4 with: name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} path: ./wheelhouse/*.whl ``` + For more information, including PyPI deployment, and the use of other CI services or the dedicated GitHub Action, check out the [documentation](https://cibuildwheel.pypa.io) and the [examples](https://github.com/pypa/cibuildwheel/tree/main/examples). diff --git a/bin/generate_schema.py b/bin/generate_schema.py index 21e94b9a9..1d426858b 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -305,6 +305,7 @@ def as_object(d: dict[str, Any]) -> dict[str, Any]: "windows": as_object(not_linux), "macos": as_object(not_linux), "pyodide": as_object(not_linux), + "ios": as_object(not_linux), } oses["linux"]["properties"]["repair-wheel-command"] = { diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index d14f9f89f..1f2da4dd3 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -17,6 +17,7 @@ from typing import Any, Protocol, TextIO, assert_never import cibuildwheel +import cibuildwheel.ios import cibuildwheel.linux import cibuildwheel.macos import cibuildwheel.pyodide @@ -95,13 +96,24 @@ def main_inner(global_options: GlobalOptions) -> None: parser.add_argument( "--platform", - choices=["auto", "linux", "macos", "windows", "pyodide"], + choices=[ + "auto", + "linux", + "macos", + "windows", + "pyodide", + "ios", + "iphoneos", + "iphonesimulator", + ], default=None, help=""" - Platform to build for. Use this option to override the - auto-detected platform. Specifying "macos" or "windows" only works - on that operating system, but "linux" works on all three, as long - as Docker/Podman is installed. Default: auto. + Platform to build for. Use this option to override the auto-detected + platform. Specifying "macos" or "windows" only works on that + operating system. "linux" works on any desktop OS, as long as + Docker/Podman is installed. "pyodide" only works on linux and macOS. + "ios", "iphoneos" and "iphonesimulator" only work on macOS. Default: + auto. """, ) @@ -242,6 +254,11 @@ def _compute_platform_only(only: str) -> PlatformName: return "windows" if "pyodide_" in only: return "pyodide" + if "ios_" in only: + if "_iphonesimulator" in only: + return "iphonesimulator" + else: + return "iphoneos" msg = f"Invalid --only='{only}', must be a build selector with a known platform" raise errors.ConfigurationError(msg) @@ -303,6 +320,10 @@ def get_platform_module(platform: PlatformName) -> PlatformModule: return cibuildwheel.macos if platform == "pyodide": return cibuildwheel.pyodide + if platform == "ios": + return cibuildwheel.ios + if platform == "iphoneos" or platform == "iphonesimulator": + return cibuildwheel.ios.PlatformSDKModule(platform) assert_never(platform) diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py index 43fa4bdb5..037a71663 100644 --- a/cibuildwheel/architecture.py +++ b/cibuildwheel/architecture.py @@ -17,6 +17,9 @@ "macos": "macOS", "windows": "Windows", "pyodide": "Pyodide", + "ios": "iOS", + "iphoneos": "iOS (Device)", + "iphonesimulator": "iOS (Simulator)", } ARCH_SYNONYMS: Final[list[dict[PlatformName, str | None]]] = [ @@ -94,8 +97,8 @@ def parse_config(config: str, platform: PlatformName) -> set[Architecture]: @staticmethod def native_arch(platform: PlatformName) -> Architecture | None: - if platform == "pyodide": - return Architecture.wasm32 + native_machine = platform_module.machine() + native_architecture = Architecture(native_machine) # Cross-platform support. Used for --print-build-identifiers or docker builds. host_platform: PlatformName = ( @@ -104,8 +107,22 @@ def native_arch(platform: PlatformName) -> Architecture | None: else ("macos" if sys.platform.startswith("darwin") else "linux") ) - native_machine = platform_module.machine() - native_architecture = Architecture(native_machine) + if platform == "pyodide": + return Architecture.wasm32 + elif platform in {"ios", "iphonesimulator"}: + # Can only build for iOS on macOS + if host_platform == "macos": + # iOS Simulators matches the platform architecture. + return native_architecture + else: + return None + elif platform == "iphoneos": + # Can only build for iOS devices on macOS + if host_platform == "macos": + # iOS devices only support arm64 + return Architecture.arm64 + else: + return None # we might need to rename the native arch to the machine we're running # on, as the same arch can have different names on different platforms @@ -129,14 +146,22 @@ def auto_archs(platform: PlatformName) -> set[Architecture]: return set() # can't build anything on this platform result = {native_arch} - if platform == "linux": + if platform == "iphoneos": + # iOS devices only support 1 architecture + result = {Architecture.arm64} + + elif platform in {"ios", "iphonesimulator"}: + # iOS defaults to building all architectures + result = {Architecture.x86_64, Architecture.arm64} + + elif platform == "linux": if Architecture.x86_64 in result: # x86_64 machines can run i686 containers result.add(Architecture.i686) elif Architecture.aarch64 in result and _check_aarch32_el0(): result.add(Architecture.armv7l) - if platform == "windows" and Architecture.AMD64 in result: + elif platform == "windows" and Architecture.AMD64 in result: result.add(Architecture.x86) return result @@ -155,7 +180,10 @@ def all_archs(platform: PlatformName) -> set[Architecture]: "macos": {Architecture.x86_64, Architecture.arm64, Architecture.universal2}, "windows": {Architecture.x86, Architecture.AMD64, Architecture.ARM64}, "pyodide": {Architecture.wasm32}, + "iphoneos": {Architecture.arm64}, + "iphonesimulator": {Architecture.arm64, Architecture.x86_64}, } + all_archs_map["ios"] = all_archs_map["iphoneos"] | all_archs_map["iphonesimulator"] return all_archs_map[platform] @staticmethod diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py new file mode 100644 index 000000000..1885ef603 --- /dev/null +++ b/cibuildwheel/ios.py @@ -0,0 +1,635 @@ +from __future__ import annotations + +import os +import shlex +import shutil +import subprocess +import sys +from collections.abc import Sequence, Set +from dataclasses import dataclass +from pathlib import Path +from typing import assert_never + +from filelock import FileLock + +from . import errors +from .architecture import Architecture +from .environment import ParsedEnvironment +from .frontend import ( + BuildFrontendConfig, + BuildFrontendName, + get_build_frontend_extra_flags, +) +from .logger import log +from .macos import install_cpython as install_build_cpython +from .options import Options +from .selector import BuildSelector +from .typing import GenericPythonConfiguration, PathOrStr, PlatformName +from .util import resources +from .util.cmd import call, shell +from .util.file import ( + CIBW_CACHE_PATH, + copy_test_sources, + download, + extract_tar, + move_file, +) +from .util.helpers import prepare_command +from .util.packaging import ( + combine_constraints, + find_compatible_wheel, + get_pip_version, +) +from .venv import virtualenv + + +@dataclass(frozen=True) +class PythonConfiguration: + version: str + identifier: str + url: str + build_url: str + + @property + def sdk(self) -> str: + return self.identifier.split("-")[1].rsplit("_", 1)[1] + + @property + def arch(self) -> str: + return self.identifier.split("-")[1].split("_", 1)[1].rsplit("_", 1)[0] + + @property + def multiarch(self) -> str: + return f"{self.arch}-{self.sdk}" + + @property + def is_simulator(self) -> bool: + return self.sdk.endswith("simulator") + + @property + def slice(self) -> str: + return { + "iphoneos": "ios-arm64", + "iphonesimulator": "ios-arm64_x86_64-simulator", + }[self.sdk] + + +def get_python_configurations( + build_selector: BuildSelector, + architectures: Set[Architecture], + sdk: PlatformName | None = None, +) -> list[PythonConfiguration]: + # iOS builds are always cross builds; we need to install a macOS Python as + # well. Rather than duplicate the location of the URL of macOS installers, + # load the macos configurations, determine the macOS configuration that + # matches the platform we're building, and embed that URL in the parsed iOS + # configuration. + macos_python_configs = resources.read_python_configs("macos") + + def build_url(item: dict[str, str]) -> str: + # The iOS item will be something like cp313-ios_arm64_iphoneos. Drop + # the iphoneos suffix, then replace ios with macosx to yield + # cp313-macosx_arm64, which will be a macOS configuration item. + macos_identifier = item["identifier"].rsplit("_", 1)[0] + macos_identifier = macos_identifier.replace("ios", "macosx") + matching = [ + config for config in macos_python_configs if config["identifier"] == macos_identifier + ] + return matching[0]["url"] + + # Load the platform configuration + if sdk: + full_python_configs = resources.read_python_configs(sdk) + else: + full_python_configs = resources.read_python_configs("iphoneos") + full_python_configs += resources.read_python_configs("iphonesimulator") + + # Build the configurations, annotating with macOS URL details. + python_configurations = [ + PythonConfiguration( + **item, + build_url=build_url(item), + ) + for item in full_python_configs + ] + + # Filter out configs that don't match any of the selected architectures + python_configurations = [ + c + for c in python_configurations + if any(c.identifier.rsplit("_", 1)[0].endswith(a.value) for a in architectures) + ] + + # Skip builds as required by BUILD/SKIP + python_configurations = [c for c in python_configurations if build_selector(c.identifier)] + + return python_configurations + + +def install_host_cpython(tmp: Path, config: PythonConfiguration, free_threading: bool) -> Path: + if free_threading: + msg = "Free threading builds aren't available for iOS (yet)" + raise ValueError(msg) + + # Install an iOS build of CPython + ios_python_tar_gz = config.url.rsplit("/", 1)[-1] + extension = ".tar.gz" + assert ios_python_tar_gz.endswith(extension) + installation_path = CIBW_CACHE_PATH / ios_python_tar_gz[: -len(extension)] + with FileLock(str(installation_path) + ".lock"): + if not installation_path.exists(): + downloaded_tar_gz = tmp / ios_python_tar_gz + download(config.url, downloaded_tar_gz) + installation_path.mkdir(parents=True, exist_ok=True) + call("tar", "-C", installation_path, "-xf", downloaded_tar_gz) + downloaded_tar_gz.unlink() + + return installation_path + + +def cross_virtualenv( + py_version: str, + host_python: Path, + multiarch: str, + build_python: Path, + venv_path: Path, + dependency_constraint_flags: Sequence[PathOrStr], +) -> dict[str, str]: + """Create a cross-compilation virtual environment. + + In a cross-compilation environment, the *host* is the platform you're + targeting the *build* is the platform where you're running the compilation. + When building iOS wheels, iOS is the host machine and macOS is the build + machine. + + A cross-compilation virtualenv is an environment that is based on the + *build* python (so that binaries can execute); but it modifies the + environment at startup so that any request for platform details (such as + `sys.platform` or `sysconfig.get_platform()`) return details of the host + platform. It also applies a loader patch so that any virtualenv created by + the cross-compilation environment will also be a cross-compilation + environment. + + :param py_version: The Python version (major.minor) in use + :param host_python: The path to the python binary for the host platform + :param multiarch: The multiarch tag for the host platform (i.e., the value + of `sys.implementation._multiarch`) + :param build_python: The path to the python binary for the build platform + :param venv_path: The path where the cross virtual environment should be + created. + :param dependency_constraint_flags: Any flags that should be used when + constraining dependencies in the environment. + """ + # Create an initial macOS virtual environment + env = virtualenv( + py_version, + build_python, + venv_path, + dependency_constraint_flags, + use_uv=False, + ) + + # Convert the macOS virtual environment into an iOS virtual environment + # using the cross-platform conversion script in the iOS distribution. + + # host_python is the path to the Python binary; + # determine the root of the XCframework slice that is being used. + slice_path = host_python.parent.parent + call( + "python", + str(slice_path / f"platform-config/{multiarch}/make_cross_venv.py"), + str(venv_path), + env=env, + cwd=venv_path, + ) + + # When running on macOS, it's easy for the build environment to leak into + # the host environment, especially when building for ARM64 (because the + # architecture is the same as the host architecture). The primary culprit + # for this is Homebrew libraries leaking in as dependencies for iOS + # libraries. + # + # To prevent problems, set the PATH to isolate the build environment from + # sources that could introduce incompatible binaries. + if sys.platform == "darwin": + env["PATH"] = os.pathsep.join( + [ + # The host python's binary directory + str(host_python.parent), + # The cross-platform environments binary directory + str(venv_path / "bin"), + # Cargo's binary directory (to allow for Rust compilation) + str(Path.home() / ".cargo" / "bin"), + # The bare minimum Apple system paths. + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + "/Library/Apple/usr/bin", + ] + ) + + return env + + +def setup_python( + tmp: Path, + python_configuration: PythonConfiguration, + dependency_constraint_flags: Sequence[PathOrStr], + environment: ParsedEnvironment, + build_frontend: BuildFrontendName, +) -> tuple[Path, dict[str, str]]: + if build_frontend == "build[uv]": + msg = "uv doesn't support iOS" + raise ValueError(msg) + + # An iOS environment requires 2 python installs - one for the build machine + # (macOS), and one for the host (iOS). We'll only ever interact with the + # *host* python, but the build Python needs to exist to act as the base + # for a cross venv. + tmp.mkdir() + implementation_id = python_configuration.identifier.split("-")[0] + log.step(f"Installing Build Python {implementation_id}...") + if implementation_id.startswith("cp"): + free_threading = "t-iphone" in python_configuration.identifier + build_python = install_build_cpython( + tmp, + python_configuration.version, + python_configuration.build_url, + free_threading, + ) + else: + msg = "Unknown Python implementation" + raise ValueError(msg) + + assert build_python.exists(), ( + f"{build_python.name} not found, has {list(build_python.parent.iterdir())}" + ) + + log.step(f"Installing Host Python {implementation_id}...") + if implementation_id.startswith("cp"): + host_install_path = install_host_cpython(tmp, python_configuration, free_threading) + host_python = ( + host_install_path + / "Python.xcframework" + / python_configuration.slice + / "bin" + / f"python{python_configuration.version}" + ) + else: + msg = "Unknown Python implementation" + raise ValueError(msg) + + assert host_python.exists(), ( + f"{host_python.name} not found, has {list(host_install_path.iterdir())}" + ) + + log.step("Creating cross build environment...") + + venv_path = tmp / "venv" + env = cross_virtualenv( + py_version=python_configuration.version, + host_python=host_python, + multiarch=python_configuration.multiarch, + build_python=build_python, + venv_path=venv_path, + dependency_constraint_flags=dependency_constraint_flags, + ) + venv_bin_path = venv_path / "bin" + assert venv_bin_path.exists() + + # We version pip ourselves, so we don't care about pip version checking + env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" + + # upgrade pip to the version matching our constraints + # if necessary, reinstall it to ensure that it's available on PATH as 'pip' + pip = ["python", "-m", "pip"] + call( + *pip, + "install", + "--upgrade", + "pip", + *dependency_constraint_flags, + env=env, + cwd=venv_path, + ) + + # Apply our environment after pip is ready + env = environment.as_dictionary(prev_environment=env) + + # Check what Python version we're on + which_python = call("which", "python", env=env, capture_stdout=True).strip() + print(which_python) + if which_python != str(venv_bin_path / "python"): + msg = ( + "cibuildwheel: python available on PATH doesn't match our installed instance. " + "If you have modified PATH, ensure that you don't overwrite cibuildwheel's " + "entry or insert python above it." + ) + raise errors.FatalError(msg) + call("python", "--version", env=env) + + # Check what pip version we're on + assert (venv_bin_path / "pip").exists() + which_pip = call("which", "pip", env=env, capture_stdout=True).strip() + print(which_pip) + if which_pip != str(venv_bin_path / "pip"): + msg = ( + "cibuildwheel: pip available on PATH doesn't match our installed instance. " + "If you have modified PATH, ensure that you don't overwrite cibuildwheel's " + "entry or insert pip above it." + ) + raise errors.FatalError(msg) + call("pip", "--version", env=env) + + # Ensure that IPHONEOS_DEPLOYMENT_TARGET is set in the environment + ios_deployment_target = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0") + env["IPHONEOS_DEPLOYMENT_TARGET"] = ios_deployment_target + + log.step("Installing build tools...") + if build_frontend == "pip": + # No additional build tools required + pass + elif build_frontend == "build": + call( + "pip", + "install", + "--upgrade", + "build[virtualenv]", + *dependency_constraint_flags, + env=env, + ) + else: + assert_never(build_frontend) + + return host_install_path, env + + +def build(options: Options, tmp_path: Path, sdk: PlatformName | None = None) -> None: + python_configurations = get_python_configurations( + build_selector=options.globals.build_selector, + architectures=options.globals.architectures, + sdk=sdk, + ) + + if not python_configurations: + return + + try: + before_all_options_identifier = python_configurations[0].identifier + before_all_options = options.build_options(before_all_options_identifier) + + if before_all_options.before_all: + log.step("Running before_all...") + env = before_all_options.environment.as_dictionary(prev_environment=os.environ) + env.setdefault("IPHONEOS_DEPLOYMENT_TARGET", "13.0") + before_all_prepared = prepare_command( + before_all_options.before_all, + project=".", + package=before_all_options.package_dir, + ) + shell(before_all_prepared, env=env) + + built_wheels: list[Path] = [] + + for config in python_configurations: + build_options = options.build_options(config.identifier) + build_frontend = build_options.build_frontend or BuildFrontendConfig("pip") + # uv doesn't support iOS + if build_frontend.name == "build[uv]": + msg = "uv doesn't support iOS" + raise ValueError(msg) + + log.build_start(config.identifier) + + identifier_tmp_dir = tmp_path / config.identifier + identifier_tmp_dir.mkdir() + built_wheel_dir = identifier_tmp_dir / "built_wheel" + + dependency_constraint_flags: Sequence[PathOrStr] = [] + if build_options.dependency_constraints: + dependency_constraint_flags = [ + "-c", + build_options.dependency_constraints.get_for_python_version(config.version), + ] + + host_install_path, env = setup_python( + identifier_tmp_dir / "build", + config, + dependency_constraint_flags, + build_options.environment, + build_frontend.name, + ) + pip_version = get_pip_version(env) + + compatible_wheel = find_compatible_wheel(built_wheels, config.identifier) + if compatible_wheel: + log.step_end() + print( + f"\nFound previously built wheel {compatible_wheel.name} " + f"that is compatible with {config.identifier}. " + "Skipping build step..." + ) + test_wheel = compatible_wheel + else: + if build_options.before_build: + log.step("Running before_build...") + before_build_prepared = prepare_command( + build_options.before_build, + project=".", + package=build_options.package_dir, + ) + shell(before_build_prepared, env=env) + + log.step("Building wheel...") + built_wheel_dir.mkdir() + + extra_flags = get_build_frontend_extra_flags( + build_frontend, build_options.build_verbosity, build_options.config_settings + ) + + build_env = env.copy() + build_env["VIRTUALENV_PIP"] = pip_version + if build_options.dependency_constraints: + constraint_path = build_options.dependency_constraints.get_for_python_version( + config.version + ) + combine_constraints(build_env, constraint_path, None) + + if build_frontend.name == "pip": + # Path.resolve() is needed. Without it pip wheel may try to + # fetch package from pypi.org. See + # https://github.com/pypa/cibuildwheel/pull/369 + call( + "python", + "-m", + "pip", + "wheel", + build_options.package_dir.resolve(), + f"--wheel-dir={built_wheel_dir}", + "--no-deps", + *extra_flags, + env=build_env, + ) + elif build_frontend.name == "build": + call( + "python", + "-m", + "build", + build_options.package_dir, + "--wheel", + f"--outdir={built_wheel_dir}", + *extra_flags, + env=build_env, + ) + else: + assert_never(build_frontend) + + test_wheel = built_wheel = next(built_wheel_dir.glob("*.whl")) + + if built_wheel.name.endswith("none-any.whl"): + raise errors.NonPlatformWheelError() + + log.step_end() + + if build_options.test_command and build_options.test_selector(config.identifier): + if not config.is_simulator: + log.step("Skipping tests on non-simulator SDK") + elif config.arch != os.uname().machine: + log.step("Skipping tests on non-native simulator architecture") + else: + if build_options.before_test: + before_test_prepared = prepare_command( + build_options.before_test, + project=".", + package=build_options.package_dir, + ) + shell(before_test_prepared, env=env) + + log.step("Setting up test harness...") + # Clone the testbed project into the build directory + testbed_path = identifier_tmp_dir / "testbed" + call( + "python", + host_install_path / "testbed", + "clone", + testbed_path, + env=build_env, + ) + + if build_options.test_sources: + copy_test_sources( + build_options.test_sources, + build_options.package_dir, + testbed_path / "iOSTestbed" / "app", + ) + else: + # There is no explicit list of test sources. Copy *all* + # the test sources; however use the sdist to do this so + # that we avoid copying any .git or venv folders. + + # Build a sdist of the project + call( + "python", + "-m", + "build", + build_options.package_dir, + "--sdist", + f"--outdir={identifier_tmp_dir}", + capture_stdout=True, + ) + src_tarball = next(identifier_tmp_dir.glob("*.tar.gz")) + + # Unpack the source tarball into the stub testbed + extract_tar( + src_tarball, + testbed_path / "iOSTestbed" / "app", + strip=1, + ) + + log.step("Installing test requirements...") + # Install the compiled wheel (with any test extras), plus + # the test requirements. Use the --platform tag to force + # the installation of iOS wheels; this requires the use of + # --only-binary=:all: + ios_version = build_env["IPHONEOS_DEPLOYMENT_TARGET"] + platform_tag = f"ios_{ios_version.replace('.', '_')}_{config.arch}_{config.sdk}" + + call( + "python", + "-m", + "pip", + "install", + "--only-binary=:all:", + "--platform", + platform_tag, + "--target", + testbed_path / "iOSTestbed" / "app_packages", + f"{test_wheel}{build_options.test_extras}", + *build_options.test_requires, + env=build_env, + ) + + log.step("Running test suite...") + try: + call( + "python", + testbed_path, + "run", + *(["--verbose"] if build_options.build_verbosity > 0 else []), + "--", + *(shlex.split(build_options.test_command)), + env=build_env, + ) + failed = False + except subprocess.CalledProcessError: + failed = True + + log.step_end(success=not failed) + + if failed: + log.error(f"Test suite failed on {config.identifier}") + sys.exit(1) + + # We're all done here; move it to output (overwrite existing) + if compatible_wheel is None: + output_wheel = build_options.output_dir.joinpath(built_wheel.name) + moved_wheel = move_file(built_wheel, output_wheel) + if moved_wheel != output_wheel.resolve(): + log.warning( + f"{built_wheel} was moved to {moved_wheel} instead of {output_wheel}" + ) + built_wheels.append(output_wheel) + + # Clean up + shutil.rmtree(identifier_tmp_dir) + + log.build_end() + except subprocess.CalledProcessError as error: + msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}" + raise errors.FatalError(msg) from error + + +# The default iOS platform will build for all SDKs on the platform (both +# iphoneos and iphonesimulator). PlatformSDKModule allows the construction of an +# interface that behaves just like the iOS platform, but only exposes a single +# SDK. +class PlatformSDKModule: + def __init__(self, sdk: PlatformName): + self.sdk = sdk + + def get_python_configurations( + self, build_selector: BuildSelector, architectures: Set[Architecture] + ) -> Sequence[GenericPythonConfiguration]: + return get_python_configurations( + build_selector=build_selector, + architectures=architectures, + sdk=self.sdk, + ) + + def build(self, options: Options, tmp_path: Path) -> None: + build( + options=options, + tmp_path=tmp_path, + sdk=self.sdk, + ) diff --git a/cibuildwheel/logger.py b/cibuildwheel/logger.py index 4b7df5c60..234a45d15 100644 --- a/cibuildwheel/logger.py +++ b/cibuildwheel/logger.py @@ -37,6 +37,9 @@ "macosx_universal2": "macOS Universal 2 - x86_64 and arm64", "macosx_arm64": "macOS arm64 - Apple Silicon", "pyodide_wasm32": "Pyodide", + "ios_arm64_iphoneos": "iOS Device (ARM64)", + "ios_arm64_iphonesimulator": "iOS Simulator (ARM64)", + "ios_x86_64_iphonesimulator": "iOS Simulator (x86_64)", } diff --git a/cibuildwheel/resources/build-platforms.toml b/cibuildwheel/resources/build-platforms.toml index b2a83445c..aacb72387 100644 --- a/cibuildwheel/resources/build-platforms.toml +++ b/cibuildwheel/resources/build-platforms.toml @@ -198,3 +198,14 @@ python_configurations = [ python_configurations = [ { identifier = "cp312-pyodide_wasm32", version = "3.12", pyodide_version = "0.27.0", pyodide_build_version = "0.29.2", emscripten_version = "3.1.58", node_version = "v20" }, ] + +[iphoneos] +python_configurations = [ + { identifier = "cp313-ios_arm64_iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b4/Python-3.13-iOS-support.b4.tar.gz" }, +] + +[iphonesimulator] +python_configurations = [ + { identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b4/Python-3.13-iOS-support.b4.tar.gz" }, + { identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b4/Python-3.13-iOS-support.b4.tar.gz" }, +] diff --git a/cibuildwheel/resources/cibuildwheel.schema.json b/cibuildwheel/resources/cibuildwheel.schema.json index ddbadf3d1..dd3eeb005 100644 --- a/cibuildwheel/resources/cibuildwheel.schema.json +++ b/cibuildwheel/resources/cibuildwheel.schema.json @@ -918,6 +918,96 @@ "$ref": "#/properties/test-requires" } } + }, + "iphoneos": { + "type": "object", + "additionalProperties": false, + "properties": { + "archs": { + "$ref": "#/properties/archs" + }, + "before-all": { + "$ref": "#/properties/before-all" + }, + "before-build": { + "$ref": "#/properties/before-build" + }, + "before-test": { + "$ref": "#/properties/before-test" + }, + "build-frontend": { + "$ref": "#/properties/build-frontend" + }, + "build-verbosity": { + "$ref": "#/properties/build-verbosity" + }, + "config-settings": { + "$ref": "#/properties/config-settings" + }, + "dependency-versions": { + "$ref": "#/properties/dependency-versions" + }, + "environment": { + "$ref": "#/properties/environment" + }, + "repair-wheel-command": { + "$ref": "#/properties/repair-wheel-command" + }, + "test-command": { + "$ref": "#/properties/test-command" + }, + "test-extras": { + "$ref": "#/properties/test-extras" + }, + "test-requires": { + "$ref": "#/properties/test-requires" + } + } + }, + "iphonesimulator": { + "type": "object", + "additionalProperties": false, + "properties": { + "archs": { + "$ref": "#/properties/archs" + }, + "before-all": { + "$ref": "#/properties/before-all" + }, + "before-build": { + "$ref": "#/properties/before-build" + }, + "before-test": { + "$ref": "#/properties/before-test" + }, + "build-frontend": { + "$ref": "#/properties/build-frontend" + }, + "build-verbosity": { + "$ref": "#/properties/build-verbosity" + }, + "config-settings": { + "$ref": "#/properties/config-settings" + }, + "dependency-versions": { + "$ref": "#/properties/dependency-versions" + }, + "environment": { + "$ref": "#/properties/environment" + }, + "repair-wheel-command": { + "$ref": "#/properties/repair-wheel-command" + }, + "test-command": { + "$ref": "#/properties/test-command" + }, + "test-extras": { + "$ref": "#/properties/test-extras" + }, + "test-requires": { + "$ref": "#/properties/test-requires" + } + } } } } diff --git a/cibuildwheel/typing.py b/cibuildwheel/typing.py index 34ae273c8..6ba807284 100644 --- a/cibuildwheel/typing.py +++ b/cibuildwheel/typing.py @@ -21,8 +21,24 @@ PathOrStr = Union[str, "os.PathLike[str]"] -PlatformName = Literal["linux", "macos", "windows", "pyodide"] -PLATFORMS: Final[set[PlatformName]] = {"linux", "macos", "windows", "pyodide"} +PlatformName = Literal[ + "linux", + "macos", + "windows", + "pyodide", + "ios", + "iphoneos", + "iphonesimulator", +] +PLATFORMS: Final[set[PlatformName]] = { + "linux", + "macos", + "windows", + "pyodide", + "ios", + "iphoneos", + "iphonesimulator", +} class GenericPythonConfiguration(Protocol): diff --git a/cibuildwheel/util/file.py b/cibuildwheel/util/file.py index c961651e9..44e4fb426 100644 --- a/cibuildwheel/util/file.py +++ b/cibuildwheel/util/file.py @@ -63,13 +63,28 @@ def extract_zip(zip_src: Path, dest: Path) -> None: dest.joinpath(zinfo.filename).chmod(permissions) -def extract_tar(tar_src: Path, dest: Path) -> None: - """Extracts a tar file using the stdlib 'tar' filter. +def strip_filter(size: int = 1) -> Callable[[tarfile.TarInfo, str], tarfile.TarInfo | None]: + """Create a tarfile filter that implements the equivalent of --strip/-C""" - See: https://docs.python.org/3/library/tarfile.html#tarfile.tar_filter for filter details - """ + def _filter(member: tarfile.TarInfo, _: str) -> tarfile.TarInfo | None: + parts = member.path.split("/", size) + try: + member.path = parts[size] + return member + except IndexError: + return None + + return _filter + + +def extract_tar(tar_src: Path, dest: Path, strip: int = 0) -> None: with tarfile.open(tar_src) as tar_: - tar_.extraction_filter = getattr(tarfile, "tar_filter", (lambda member, _: member)) + if strip: + extraction_filter = strip_filter(strip) + else: + extraction_filter = getattr(tarfile, "tar_filter", (lambda member, _: member)) + + tar_.extraction_filter = extraction_filter tar_.extractall(dest) diff --git a/cibuildwheel/util/packaging.py b/cibuildwheel/util/packaging.py index c4e14f211..da673bd1e 100644 --- a/cibuildwheel/util/packaging.py +++ b/cibuildwheel/util/packaging.py @@ -95,8 +95,9 @@ def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None: # If a minor version number is given, it has to be lower than the current one. continue - if platform.startswith(("manylinux", "musllinux", "macosx")): - # Linux, macOS require the beginning and ending match (macos/manylinux version doesn't need to) + if platform.startswith(("manylinux", "musllinux", "macosx", "ios")): + # Linux, macOS, and iOS require the beginning and ending match + # (macos/manylinux/iOS version number doesn't need to match) os_, arch = platform.split("_", 1) if not tag.platform.startswith(os_): continue diff --git a/docs/options.md b/docs/options.md index 13b87e08c..bc52ee2ec 100644 --- a/docs/options.md +++ b/docs/options.md @@ -247,7 +247,7 @@ environment variables will completely override any TOML configuration. > Override the auto-detected target platform -Options: `auto` `linux` `macos` `windows` `pyodide` +Options: `auto` `linux` `macos` `windows` `ios` `iphoneos` `iphonesimulator` `pyodide` Default: `auto` @@ -255,10 +255,15 @@ Default: `auto` - For `linux`, you need [Docker or Podman](#container-engine) running, on Linux, macOS, or Windows. - For `macos` and `windows`, you need to be running on the respective system, with a working compiler toolchain installed - Xcode Command Line tools for macOS, and MSVC for Windows. +- For `ios`, `iphoneos` and `iphonesimulator`, you need to be running on macOS, with Xcode and the iOS simulator installed. + `ios` is an alias for the combination of both `iphoneos` and `iphonesimulator`. - For `pyodide` you need to be on an x86-64 linux runner and `python3.12` must be available in `PATH`. This option can also be set using the [command-line option](#command-line) `--platform`. This option is not available in the `pyproject.toml` config. +!!! note + The separation between `iphoneos` and `iphonesimulator` is required because although the API for all iOS builds is identical, builds for physical devices and simulators must use different SDKs. You can use the `ios` platform to target *both* devices and simulators in a single build; however, configurations must be specified independently for each iOS SDK. + !!! tip You can use this option to locally debug your cibuildwheel config on Linux, instead of pushing to CI to test every change. For example: @@ -288,21 +293,21 @@ When setting the options, you can use shell-style globbing syntax, as per [fnmat
-| | macOS | Windows | Linux Intel | Linux Other | -|---------------|------------------------------------------------------------------------|-----------------------------------------------------|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Python 3.6 | cp36-macosx_x86_64 | cp36-win_amd64
cp36-win32 | cp36-manylinux_x86_64
cp36-manylinux_i686
cp36-musllinux_x86_64
cp36-musllinux_i686 | cp36-manylinux_aarch64
cp36-manylinux_ppc64le
cp36-manylinux_s390x
cp36-manylinux_armv7l
cp36-musllinux_aarch64
cp36-musllinux_ppc64le
cp36-musllinux_s390x
cp36-musllinux_armv7l | -| Python 3.7 | cp37-macosx_x86_64 | cp37-win_amd64
cp37-win32 | cp37-manylinux_x86_64
cp37-manylinux_i686
cp37-musllinux_x86_64
cp37-musllinux_i686 | cp37-manylinux_aarch64
cp37-manylinux_ppc64le
cp37-manylinux_s390x
cp37-manylinux_armv7l
cp37-musllinux_aarch64
cp37-musllinux_ppc64le
cp37-musllinux_s390x
cp37-musllinux_armv7l | -| Python 3.8 | cp38-macosx_x86_64
cp38-macosx_universal2
cp38-macosx_arm64 | cp38-win_amd64
cp38-win32 | cp38-manylinux_x86_64
cp38-manylinux_i686
cp38-musllinux_x86_64
cp38-musllinux_i686 | cp38-manylinux_aarch64
cp38-manylinux_ppc64le
cp38-manylinux_s390x
cp38-manylinux_armv7l
cp38-musllinux_aarch64
cp38-musllinux_ppc64le
cp38-musllinux_s390x
cp38-musllinux_armv7l | -| Python 3.9 | cp39-macosx_x86_64
cp39-macosx_universal2
cp39-macosx_arm64 | cp39-win_amd64
cp39-win32
cp39-win_arm64 | cp39-manylinux_x86_64
cp39-manylinux_i686
cp39-musllinux_x86_64
cp39-musllinux_i686 | cp39-manylinux_aarch64
cp39-manylinux_ppc64le
cp39-manylinux_s390x
cp39-manylinux_armv7l
cp39-musllinux_aarch64
cp39-musllinux_ppc64le
cp39-musllinux_s390x
cp39-musllinux_armv7l | -| Python 3.10 | cp310-macosx_x86_64
cp310-macosx_universal2
cp310-macosx_arm64 | cp310-win_amd64
cp310-win32
cp310-win_arm64 | cp310-manylinux_x86_64
cp310-manylinux_i686
cp310-musllinux_x86_64
cp310-musllinux_i686 | cp310-manylinux_aarch64
cp310-manylinux_ppc64le
cp310-manylinux_s390x
cp310-manylinux_armv7l
cp310-musllinux_aarch64
cp310-musllinux_ppc64le
cp310-musllinux_s390x
cp310-musllinux_armv7l | -| Python 3.11 | cp311-macosx_x86_64
cp311-macosx_universal2
cp311-macosx_arm64 | cp311-win_amd64
cp311-win32
cp311-win_arm64 | cp311-manylinux_x86_64
cp311-manylinux_i686
cp311-musllinux_x86_64
cp311-musllinux_i686 | cp311-manylinux_aarch64
cp311-manylinux_ppc64le
cp311-manylinux_s390x
cp311-manylinux_armv7l
cp311-musllinux_aarch64
cp311-musllinux_ppc64le
cp311-musllinux_s390x
cp311-musllinux_armv7l | -| Python 3.12 | cp312-macosx_x86_64
cp312-macosx_universal2
cp312-macosx_arm64 | cp312-win_amd64
cp312-win32
cp312-win_arm64 | cp312-manylinux_x86_64
cp312-manylinux_i686
cp312-musllinux_x86_64
cp312-musllinux_i686 | cp312-manylinux_aarch64
cp312-manylinux_ppc64le
cp312-manylinux_s390x
cp312-musllinux_armv7l
cp312-musllinux_ppc64le
cp312-musllinux_s390x
cp312-musllinux_armv7l | -| Python 3.13 | cp313-macosx_x86_64
cp313-macosx_universal2
cp313-macosx_arm64 | cp313-win_amd64
cp313-win32
cp313-win_arm64 | cp313-manylinux_x86_64
cp313-manylinux_i686
cp313-musllinux_x86_64
cp313-musllinux_i686 | cp313-manylinux_aarch64
cp313-manylinux_ppc64le
cp313-manylinux_s390x
cp313-manylinux_armv7l
cp313-musllinux_aarch64
cp313-musllinux_ppc64le
cp313-musllinux_s390x
cp313-musllinux_armv7l | -| PyPy3.7 v7.3 | pp37-macosx_x86_64 | pp37-win_amd64 | pp37-manylinux_x86_64
pp37-manylinux_i686 | pp37-manylinux_aarch64 | -| PyPy3.8 v7.3 | pp38-macosx_x86_64
pp38-macosx_arm64 | pp38-win_amd64 | pp38-manylinux_x86_64
pp38-manylinux_i686 | pp38-manylinux_aarch64 | -| PyPy3.9 v7.3 | pp39-macosx_x86_64
pp39-macosx_arm64 | pp39-win_amd64 | pp39-manylinux_x86_64
pp39-manylinux_i686 | pp39-manylinux_aarch64 | -| PyPy3.10 v7.3 | pp310-macosx_x86_64
pp310-macosx_arm64 | pp310-win_amd64 | pp310-manylinux_x86_64
pp310-manylinux_i686 | pp310-manylinux_aarch64 | -| PyPy3.11 v7.3 | pp311-macosx_x86_64
pp311-macosx_arm64 | pp311-win_amd64 | pp311-manylinux_x86_64
pp311-manylinux_i686 | pp311-manylinux_aarch64 | +| | macOS | Windows | Linux Intel | Linux Other | iOS | +|---------------|------------------------------------------------------------------------|-----------------------------------------------------|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| +| Python 3.6 | cp36-macosx_x86_64 | cp36-win_amd64
cp36-win32 | cp36-manylinux_x86_64
cp36-manylinux_i686
cp36-musllinux_x86_64
cp36-musllinux_i686 | cp36-manylinux_aarch64
cp36-manylinux_ppc64le
cp36-manylinux_s390x
cp36-manylinux_armv7l
cp36-musllinux_aarch64
cp36-musllinux_ppc64le
cp36-musllinux_s390x
cp36-musllinux_armv7l | | +| Python 3.7 | cp37-macosx_x86_64 | cp37-win_amd64
cp37-win32 | cp37-manylinux_x86_64
cp37-manylinux_i686
cp37-musllinux_x86_64
cp37-musllinux_i686 | cp37-manylinux_aarch64
cp37-manylinux_ppc64le
cp37-manylinux_s390x
cp37-manylinux_armv7l
cp37-musllinux_aarch64
cp37-musllinux_ppc64le
cp37-musllinux_s390x
cp37-musllinux_armv7l | | +| Python 3.8 | cp38-macosx_x86_64
cp38-macosx_universal2
cp38-macosx_arm64 | cp38-win_amd64
cp38-win32 | cp38-manylinux_x86_64
cp38-manylinux_i686
cp38-musllinux_x86_64
cp38-musllinux_i686 | cp38-manylinux_aarch64
cp38-manylinux_ppc64le
cp38-manylinux_s390x
cp38-manylinux_armv7l
cp38-musllinux_aarch64
cp38-musllinux_ppc64le
cp38-musllinux_s390x
cp38-musllinux_armv7l | | +| Python 3.9 | cp39-macosx_x86_64
cp39-macosx_universal2
cp39-macosx_arm64 | cp39-win_amd64
cp39-win32
cp39-win_arm64 | cp39-manylinux_x86_64
cp39-manylinux_i686
cp39-musllinux_x86_64
cp39-musllinux_i686 | cp39-manylinux_aarch64
cp39-manylinux_ppc64le
cp39-manylinux_s390x
cp39-manylinux_armv7l
cp39-musllinux_aarch64
cp39-musllinux_ppc64le
cp39-musllinux_s390x
cp39-musllinux_armv7l | | +| Python 3.10 | cp310-macosx_x86_64
cp310-macosx_universal2
cp310-macosx_arm64 | cp310-win_amd64
cp310-win32
cp310-win_arm64 | cp310-manylinux_x86_64
cp310-manylinux_i686
cp310-musllinux_x86_64
cp310-musllinux_i686 | cp310-manylinux_aarch64
cp310-manylinux_ppc64le
cp310-manylinux_s390x
cp310-manylinux_armv7l
cp310-musllinux_aarch64
cp310-musllinux_ppc64le
cp310-musllinux_s390x
cp310-musllinux_armv7l | | +| Python 3.11 | cp311-macosx_x86_64
cp311-macosx_universal2
cp311-macosx_arm64 | cp311-win_amd64
cp311-win32
cp311-win_arm64 | cp311-manylinux_x86_64
cp311-manylinux_i686
cp311-musllinux_x86_64
cp311-musllinux_i686 | cp311-manylinux_aarch64
cp311-manylinux_ppc64le
cp311-manylinux_s390x
cp311-manylinux_armv7l
cp311-musllinux_aarch64
cp311-musllinux_ppc64le
cp311-musllinux_s390x
cp311-musllinux_armv7l | | +| Python 3.12 | cp312-macosx_x86_64
cp312-macosx_universal2
cp312-macosx_arm64 | cp312-win_amd64
cp312-win32
cp312-win_arm64 | cp312-manylinux_x86_64
cp312-manylinux_i686
cp312-musllinux_x86_64
cp312-musllinux_i686 | cp312-manylinux_aarch64
cp312-manylinux_ppc64le
cp312-manylinux_s390x
cp312-musllinux_armv7l
cp312-musllinux_ppc64le
cp312-musllinux_s390x
cp312-musllinux_armv7l | | +| Python 3.13 | cp313-macosx_x86_64
cp313-macosx_universal2
cp313-macosx_arm64 | cp313-win_amd64
cp313-win32
cp313-win_arm64 | cp313-manylinux_x86_64
cp313-manylinux_i686
cp313-musllinux_x86_64
cp313-musllinux_i686 | cp313-manylinux_aarch64
cp313-manylinux_ppc64le
cp313-manylinux_s390x
cp313-manylinux_armv7l
cp313-musllinux_aarch64
cp313-musllinux_ppc64le
cp313-musllinux_s390x
cp313-musllinux_armv7l | cp313-ios_arm64_iphoneos
cp313-ios_arm64_iphonesimulator
cp313-ios_x86_64_iphoneos | +| PyPy3.7 v7.3 | pp37-macosx_x86_64 | pp37-win_amd64 | pp37-manylinux_x86_64
pp37-manylinux_i686 | pp37-manylinux_aarch64 | | +| PyPy3.8 v7.3 | pp38-macosx_x86_64
pp38-macosx_arm64 | pp38-win_amd64 | pp38-manylinux_x86_64
pp38-manylinux_i686 | pp38-manylinux_aarch64 | | +| PyPy3.9 v7.3 | pp39-macosx_x86_64
pp39-macosx_arm64 | pp39-win_amd64 | pp39-manylinux_x86_64
pp39-manylinux_i686 | pp39-manylinux_aarch64 | | +| PyPy3.10 v7.3 | pp310-macosx_x86_64
pp310-macosx_arm64 | pp310-win_amd64 | pp310-manylinux_x86_64
pp310-manylinux_i686 | pp310-manylinux_aarch64 | | +| PyPy3.11 v7.3 | pp311-macosx_x86_64
pp311-macosx_arm64 | pp311-win_amd64 | pp311-manylinux_x86_64
pp311-manylinux_i686 | pp311-manylinux_aarch64 | | The list of supported and currently selected build identifiers can also be retrieved by passing the `--print-build-identifiers` flag to cibuildwheel. The format is `python_tag-platform_tag`, with tags similar to those in [PEP 425](https://www.python.org/dev/peps/pep-0425/#details). @@ -461,6 +466,8 @@ Default: `auto` | Windows / ARM64 | `ARM64` | `ARM64` | `ARM64` | | | macOS / Intel | `x86_64` | `x86_64` | `x86_64` | | | macOS / Apple Silicon | `arm64` | `arm64` | `arm64` | | +| iOS on macOS / Intel | `x86_64` | `arm64` `x86_64` | `arm64` `x86_64` | | +| iOS on macOS / Apple Silicon | `arm64` | `arm64` `x86_64` | `arm64` `x86_64` | | If not listed above, `auto` is the same as `native`. @@ -468,7 +475,7 @@ If not listed above, `auto` is the same as `native`. [binfmt]: https://hub.docker.com/r/tonistiigi/binfmt Platform-specific environment variables are also available:
- `CIBW_ARCHS_MACOS` | `CIBW_ARCHS_WINDOWS` | `CIBW_ARCHS_LINUX` + `CIBW_ARCHS_MACOS` | `CIBW_ARCHS_WINDOWS` | `CIBW_ARCHS_LINUX` | `CIBW_ARCHS_IPHONEOS` | `CIBW_ARCHS_IPHONESIMULATOR` This option can also be set using the [command-line option](#command-line) `--archs`. This option cannot be set in an `overrides` section in `pyproject.toml`. @@ -684,7 +691,7 @@ all build and test environments. This will generally speed up cibuildwheel. Make sure you have an external uv on Windows and macOS, either by pre-installing it, or installing cibuildwheel with the uv extra, `cibuildwheel[uv]`. `uv` will not be used for Python 3.6 or Python 3.7. You -cannot use uv currently on Windows for ARM or for musllinux on s390x as +cannot use uv currently on Windows for ARM, for musllinux on s390x, or for iOS, as binaries are not provided by uv. Legacy dependencies like setuptools on Python < 3.12 and pip are not installed if using uv. @@ -762,7 +769,7 @@ a table of items, including arrays. single values. Platform-specific environment variables also available:
-`CIBW_CONFIG_SETTINGS_MACOS` | `CIBW_CONFIG_SETTINGS_WINDOWS` | `CIBW_CONFIG_SETTINGS_LINUX` | `CIBW_CONFIG_SETTINGS_PYODIDE` +`CIBW_CONFIG_SETTINGS_MACOS` | `CIBW_CONFIG_SETTINGS_WINDOWS` | `CIBW_CONFIG_SETTINGS_LINUX` | `CIBW_CONFIG_SETTINGS_IPHONEOS` | `CIBW_CONFIG_SETTINGS_IPHONESIMULATOR` | `CIBW_CONFIG_SETTINGS_PYODIDE` #### Examples @@ -793,7 +800,7 @@ You can use `$PATH` syntax to insert other variables, or the `$(pwd)` syntax to To specify more than one environment variable, separate the assignments by spaces. Platform-specific environment variables are also available:
-`CIBW_ENVIRONMENT_MACOS` | `CIBW_ENVIRONMENT_WINDOWS` | `CIBW_ENVIRONMENT_LINUX` | `CIBW_ENVIRONMENT_PYODIDE` +`CIBW_ENVIRONMENT_MACOS` | `CIBW_ENVIRONMENT_WINDOWS` | `CIBW_ENVIRONMENT_LINUX` | `CIBW_ENVIRONMENT_IPHONEOS` | `CIBW_ENVIRONMENT_IPHONESIMULATOR` | `CIBW_ENVIRONMENT_PYODIDE` #### Examples @@ -925,7 +932,7 @@ On linux, overriding it triggers a new container launch. It cannot be overridden on macOS and Windows. Platform-specific environment variables also available:
-`CIBW_BEFORE_ALL_MACOS` | `CIBW_BEFORE_ALL_WINDOWS` | `CIBW_BEFORE_ALL_LINUX` | `CIBW_BEFORE_ALL_PYODIDE` +`CIBW_BEFORE_ALL_MACOS` | `CIBW_BEFORE_ALL_WINDOWS` | `CIBW_BEFORE_ALL_LINUX` | `CIBW_BEFORE_ALL_IPHONEOS` | `CIBW_BEFORE_ALL_IPHONESIMULATOR` | `CIBW_BEFORE_ALL_PYODIDE` !!! note @@ -990,7 +997,7 @@ The active Python binary can be accessed using `python`, and pip with `pip`; cib The command is run in a shell, so you can write things like `cmd1 && cmd2`. Platform-specific environment variables are also available:
- `CIBW_BEFORE_BUILD_MACOS` | `CIBW_BEFORE_BUILD_WINDOWS` | `CIBW_BEFORE_BUILD_LINUX` | `CIBW_BEFORE_BUILD_PYODIDE` + `CIBW_BEFORE_BUILD_MACOS` | `CIBW_BEFORE_BUILD_WINDOWS` | `CIBW_BEFORE_BUILD_LINUX` | `CIBW_BEFORE_BUILD_IPHONEOS` | `CIBW_BEFORE_BUILD_IPHONESIMULATOR` | `CIBW_BEFORE_BUILD_PYODIDE` #### Examples @@ -1071,6 +1078,7 @@ Default: - on Linux: `'auditwheel repair -w {dest_dir} {wheel}'` - on macOS: `'delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}'` - on Windows: `''` +- on iOS: `''` - on Pyodide: `''` A shell command to repair a built wheel by copying external library dependencies into the wheel tree and relinking them. @@ -1085,7 +1093,7 @@ The following placeholders must be used inside the command and will be replaced The command is run in a shell, so you can run multiple commands like `cmd1 && cmd2`. Platform-specific environment variables are also available:
-`CIBW_REPAIR_WHEEL_COMMAND_MACOS` | `CIBW_REPAIR_WHEEL_COMMAND_WINDOWS` | `CIBW_REPAIR_WHEEL_COMMAND_LINUX` | `CIBW_REPAIR_WHEEL_COMMAND_PYODIDE` +`CIBW_REPAIR_WHEEL_COMMAND_MACOS` | `CIBW_REPAIR_WHEEL_COMMAND_WINDOWS` | `CIBW_REPAIR_WHEEL_COMMAND_LINUX` | `CIBW_REPAIR_WHEEL_COMMAND_IPHONEOS` | `CIBW_REPAIR_WHEEL_COMMAND_IPHONESIMULATOR` | `CIBW_REPAIR_WHEEL_COMMAND_PYODIDE` !!! tip cibuildwheel doesn't yet ship a default repair command for Windows. @@ -1393,7 +1401,7 @@ here and it will be used instead. `./constraints.txt` if that's not found. Platform-specific environment variables are also available:
-`CIBW_DEPENDENCY_VERSIONS_MACOS` | `CIBW_DEPENDENCY_VERSIONS_WINDOWS` | `CIBW_DEPENDENCY_VERSIONS_PYODIDE` +`CIBW_DEPENDENCY_VERSIONS_MACOS` | `CIBW_DEPENDENCY_VERSIONS_WINDOWS` | `CIBW_DEPENDENCY_VERSIONS_IPHONEOS` | `CIBW_DEPENDENCY_VERSIONS_IPHONESIMULATOR` | `CIBW_DEPENDENCY_VERSIONS_PYODIDE` !!! note This option does not affect the tools used on the Linux build - those versions @@ -1434,7 +1442,7 @@ Platform-specific environment variables are also available:
## Testing ### `CIBW_TEST_COMMAND` {: #test-command} -> Execute a shell command to test each built wheel +> The command to test each built wheel Shell command to run tests after the build. The wheel will be installed automatically and available for import from the tests. If this variable is not @@ -1453,10 +1461,14 @@ Alternatively, you can use the [`CIBW_TEST_SOURCES`](#test-sources) setting to create a temporary folder populated with a specific subset of project files to run your test suite. -The command is run in a shell, so you can write things like `cmd1 && cmd2`. +On all platforms other than iOS, the command is run in a shell, so you can write +things like `cmd1 && cmd2`. + +On iOS, the value of the `CIBW_TEST_COMMAND` setting is interpreted as the arguments +to pass `python -m`. Shell commands cannot be used. Platform-specific environment variables are also available:
-`CIBW_TEST_COMMAND_MACOS` | `CIBW_TEST_COMMAND_WINDOWS` | `CIBW_TEST_COMMAND_LINUX` | `CIBW_TEST_COMMAND_PYODIDE` +`CIBW_TEST_COMMAND_MACOS` | `CIBW_TEST_COMMAND_WINDOWS` | `CIBW_TEST_COMMAND_LINUX` | `CIBW_TEST_COMMAND_IPHONEOS` | `CIBW_TEST_COMMAND_IPHONESIMULATOR` | `CIBW_TEST_COMMAND_PYODIDE` #### Examples @@ -1513,7 +1525,7 @@ The active Python binary can be accessed using `python`, and pip with `pip`; cib The command is run in a shell, so you can write things like `cmd1 && cmd2`. Platform-specific environment variables are also available:
- `CIBW_BEFORE_TEST_MACOS` | `CIBW_BEFORE_TEST_WINDOWS` | `CIBW_BEFORE_TEST_LINUX` | `CIBW_BEFORE_TEST_PYODIDE` + `CIBW_BEFORE_TEST_MACOS` | `CIBW_BEFORE_TEST_WINDOWS` | `CIBW_BEFORE_TEST_LINUX` | `CIBW_BEFORE_TEST_IPHONEOS` | `CIBW_BEFORE_TEST_IPHONESIMULATOR` | `CIBW_BEFORE_TEST_PYODIDE` #### Examples @@ -1604,7 +1616,7 @@ Platform-specific environment variables are also available:
Space-separated list of dependencies required for running the tests. Platform-specific environment variables are also available:
-`CIBW_TEST_REQUIRES_MACOS` | `CIBW_TEST_REQUIRES_WINDOWS` | `CIBW_TEST_REQUIRES_LINUX` | `CIBW_TEST_REQUIRES_PYODIDE` +`CIBW_TEST_REQUIRES_MACOS` | `CIBW_TEST_REQUIRES_WINDOWS` | `CIBW_TEST_REQUIRES_LINUX` | `CIBW_TEST_REQUIRES_IPHONEOS` | `CIBW_TEST_REQUIRES_IPHONESIMULATOR` | `CIBW_TEST_REQUIRES_PYODIDE` #### Examples @@ -1644,7 +1656,7 @@ tests. This can be used to avoid having to redefine test dependencies in `setup.cfg` or `setup.py`. Platform-specific environment variables are also available:
-`CIBW_TEST_EXTRAS_MACOS` | `CIBW_TEST_EXTRAS_WINDOWS` | `CIBW_TEST_EXTRAS_LINUX` | `CIBW_TEST_EXTRAS_PYODIDE` +`CIBW_TEST_EXTRAS_MACOS` | `CIBW_TEST_EXTRAS_WINDOWS` | `CIBW_TEST_EXTRAS_LINUX` | `CIBW_TEST_EXTRAS_IPHONEOS` | `CIBW_TEST_EXTRAS_IPHONESIMULATOR` | `CIBW_TEST_EXTRAS_PYODIDE` #### Examples @@ -1771,7 +1783,7 @@ export CIBW_DEBUG_TRACEBACK=TRUE A number from 1 to 3 to increase the level of verbosity (corresponding to invoking pip with `-v`, `-vv`, and `-vvv`), between -1 and -3 (`-q`, `-qq`, and `-qqq`), or just 0 (default verbosity). These flags are useful while debugging a build when the output of the actual build invoked by `pip wheel` is required. Has no effect on the `build` backend, which produces verbose output by default. Platform-specific environment variables are also available:
-`CIBW_BUILD_VERBOSITY_MACOS` | `CIBW_BUILD_VERBOSITY_WINDOWS` | `CIBW_BUILD_VERBOSITY_LINUX` | `CIBW_BUILD_VERBOSITY_PYODIDE` +`CIBW_BUILD_VERBOSITY_MACOS` | `CIBW_BUILD_VERBOSITY_WINDOWS` | `CIBW_BUILD_VERBOSITY_LINUX` | `CIBW_BUILD_VERBOSITY_IPHONEOS` | `CIBW_BUILD_VERBOSITY_IPHONESIMULATOR` | `CIBW_BUILD_VERBOSITY_PYODIDE` #### Examples diff --git a/docs/setup.md b/docs/setup.md index e118bb94d..89481fe31 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -76,7 +76,7 @@ they're perfectly reproducible. The only side effect to your system will be docker images being pulled. -### macOS / Windows builds +### macOS / Windows builds {: #macos-windows} Pre-requisite: you need to have native build tools installed. @@ -107,6 +107,34 @@ You can override the cache folder using the ``CIBW_CACHE_PATH`` environment vari Download link: https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg ``` +### iOS builds + +Pre-requisite: You must be building on a macOS machine, with Xcode installed. +The Xcode installation must have an iOS SDK available. To check if an iOS SDK +is available, open the Xcode settings panel, and check the Platforms tab. + +To build iOS wheels, pass in `--platform ios` when invoking `cibuildwheel`. This will +build three wheels: + +* An ARM64 wheel for iOS devices; +* An ARM64 wheel for the iOS simulator; and +* An x86_64 wheel for the iOS simulator. + +Alternatively, you can build only wheels for iOS devices by using +`--platform iphoneos`; or only wheels for iOS simulators by using +`--platform iphonesimulator`. + +Building iOS wheel also requires a working macOS Python configuration. See the notes +on [macOS builds](setup.md#macos-windows) for details about configuration. + +iOS builds will honor the `IPHONEOS_DEPLOYMENT_TARGET` environment variable to set the +minimum supported API version for generated wheels. This will default to `13.0` if the +environment variable isn't set. + +If tests have been configured, the test suite will be executed on the simulator +matching the architecture of the build machine - that is, if you're building on +an ARM64 macOS machine, the ARM64 wheel will be tested on an ARM64 simulator. + ### Pyodide (WebAssembly) builds (experimental) Pre-requisite: you need to have a matching host version of Python (unlike all @@ -119,7 +147,7 @@ You must target pyodide with `--platform pyodide` (or use `--only` on the identi ### GitHub Actions [linux/mac/windows] {: #github-actions} -To build Linux, Mac, and Windows wheels using GitHub Actions, create a `.github/workflows/build_wheels.yml` file in your repo. +To build Linux, Windows, macOS, iOS and pyodide wheels using GitHub Actions, create a `.github/workflows/build_wheels.yml` file in your repo. !!! tab "Action" For GitHub Actions, `cibuildwheel` provides an action you can use. This is @@ -132,10 +160,6 @@ To build Linux, Mac, and Windows wheels using GitHub Actions, create a `.github/ {% include "../examples/github-minimal.yml" %} ``` - Use `env:` to pass [build options](options.md) and `with:` to set - `package-dir: .`, `output-dir: wheelhouse` and `config-file: ''` - locations (those values are the defaults). - !!! tab "pipx" The GitHub Actions runners have pipx installed, so you can easily build in just one line. This is internally how the action works; the main benefit of @@ -144,29 +168,7 @@ To build Linux, Mac, and Windows wheels using GitHub Actions, create a `.github/ > .github/workflows/build_wheels.yml ```yaml - name: Build - - on: [push, pull_request] - - jobs: - build_wheels: - name: Build wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - # macos-13 is an intel runner, macos-14 is apple silicon - os: [ubuntu-latest, windows-latest, macos-13, macos-14] - - steps: - - uses: actions/checkout@v4 - - - name: Build wheels - run: pipx run cibuildwheel==2.22.0 - - - uses: actions/upload-artifact@v4 - with: - name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} - path: ./wheelhouse/*.whl + {% include "../examples/github-pipx.yml" %} ``` !!! tab "Generic" @@ -176,39 +178,11 @@ To build Linux, Mac, and Windows wheels using GitHub Actions, create a `.github/ appeal to you. > .github/workflows/build_wheels.yml - - ```yaml - name: Build - - on: [push, pull_request] - - jobs: - build_wheels: - name: Build wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - # macos-13 is an intel runner, macos-14 is apple silicon - os: [ubuntu-latest, windows-latest, macos-13, macos-14] - - steps: - - uses: actions/checkout@v4 - - # Used to host cibuildwheel - - uses: actions/setup-python@v5 - - - name: Install cibuildwheel - run: python -m pip install cibuildwheel==2.22.0 - - - name: Build wheels - run: python -m cibuildwheel --output-dir wheelhouse - - - uses: actions/upload-artifact@v4 - with: - name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} - path: ./wheelhouse/*.whl - ``` - + {% + include-markdown "../README.md" + start="" + end="" + %} Commit this file, and push to GitHub - either to your default branch, or to a PR branch. The build should start automatically. diff --git a/examples/github-deploy.yml b/examples/github-deploy.yml index 433adf5b3..5c928919d 100644 --- a/examples/github-deploy.yml +++ b/examples/github-deploy.yml @@ -12,18 +12,47 @@ on: jobs: build_wheels: - name: Build wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} + name: Build wheels for ${{ matrix.os }} + runs-on: ${{ matrix.runs-on }} strategy: matrix: - # macos-13 is an intel runner, macos-14 is apple silicon - os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14] + os: [ linux-intel, linux-arm, windows, macOS-intel, macOS-arm, iOS, pyodide ] + include: + - archs: auto + platform: auto + - os: linux-intel + runs-on: ubuntu-latest + - os: linux-arm + runs-on: ubuntu-24.04-arm + - os: windows + runs-on: windows-latest + - os: macOS-intel + # macos-13 was the last x86_64 runner + runs-on: macos-13 + - os: macOS-arm + # macos-14+ (including latest) are ARM64 runners + runs-on: macos-latest + archs: auto,universal2 + - os: iOS + runs-on: macos-latest + platform: ios + - os: pyodide + runs-on: ubuntu-latest + platform: pyodide steps: - uses: actions/checkout@v4 - name: Build wheels uses: pypa/cibuildwheel@v2.22.0 + env: + CIBW_PLATFORM: ${{ matrix.platform }} + CIBW_ARCHS: ${{ matrix.archs }} + # Can also be configured directly, using `with:` + # with: + # package-dir: . + # output-dir: wheelhouse + # config-file: "{package}/pyproject.toml" - uses: actions/upload-artifact@v4 with: diff --git a/examples/github-minimal.yml b/examples/github-minimal.yml index d857b6448..39c0f56f0 100644 --- a/examples/github-minimal.yml +++ b/examples/github-minimal.yml @@ -4,21 +4,43 @@ on: [push, pull_request] jobs: build_wheels: - name: Build wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} + name: Build wheels for ${{ matrix.os }} + runs-on: ${{ matrix.runs-on }} strategy: matrix: - # macos-13 is an intel runner, macos-14 is apple silicon - os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14] + os: [ linux-intel, linux-arm, windows, macOS-intel, macOS-arm, iOS, pyodide ] + include: + - archs: auto + platform: auto + - os: linux-intel + runs-on: ubuntu-latest + - os: linux-arm + runs-on: ubuntu-22.04-arm + - os: windows + runs-on: windows-latest + - os: macOS-intel + # macos-13 was the last x86_64 runner + runs-on: macos-13 + - os: macOS-arm + # macos-14+ (including latest) are ARM64 runners + runs-on: macos-latest + archs: auto,universal2 + - os: iOS + runs-on: macos-latest + platform: ios + - os: pyodide + runs-on: ubuntu-latest + platform: pyodide steps: - uses: actions/checkout@v4 - name: Build wheels uses: pypa/cibuildwheel@v2.22.0 - # env: - # CIBW_SOME_OPTION: value - # ... + env: + CIBW_PLATFORM: ${{ matrix.platform }} + CIBW_ARCHS: ${{ matrix.archs }} + # Can also be configured directly, using `with:` # with: # package-dir: . # output-dir: wheelhouse diff --git a/examples/github-pipx.yml b/examples/github-pipx.yml new file mode 100644 index 000000000..5ca0325de --- /dev/null +++ b/examples/github-pipx.yml @@ -0,0 +1,46 @@ +name: Build + +on: [push, pull_request] + +jobs: + build_wheels: + name: Build wheels for ${{ matrix.os }} + runs-on: ${{ matrix.runs-on }} + strategy: + matrix: + os: [ linux, windows, macOS-intel, macOS-arm, iOS, pyodide ] + include: + - archs: auto + platform: auto + - os: linux + runs-on: ubuntu-latest + - os: windows + runs-on: windows-latest + - os: macOS-intel + # macos-13 was the last x86_64 runner + runs-on: macos-13 + - os: macOS-arm + # macos-14+ (including latest) are ARM64 runners + runs-on: macos-latest + archs: auto,universal2 + - os: iOS + runs-on: macos-latest + platform: ios + - os: pyodide + runs-on: ubuntu-latest + platform: pyodide + + steps: + - uses: actions/checkout@v4 + + - name: Build wheels + run: pipx run cibuildwheel==2.22.0 + # To supply options, put them in 'env' + env: + CIBW_PLATFORM: ${{ matrix.platform }} + CIBW_ARCHS: ${{ matrix.archs }} + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} + path: ./wheelhouse/*.whl diff --git a/test/test_ios.py b/test/test_ios.py new file mode 100644 index 000000000..d4953dca1 --- /dev/null +++ b/test/test_ios.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import os +import platform +import subprocess + +import pytest + +from . import test_projects, utils + +basic_project = test_projects.new_c_project() +basic_project.files["tests/__init__.py"] = "" +basic_project.files["tests/__main__.py"] = r""" +import platform +import time + +# Workaround for CPython#130294 +for i in range(0, 5): + time.sleep(1) + print("Ensure logger is running...") + +print("running tests on " + platform.machine()) +""" + + +def get_xcode_version() -> tuple[int, int]: + output = subprocess.run( + ["xcodebuild", "-version"], + text=True, + check=True, + stdout=subprocess.PIPE, + ).stdout + lines = output.splitlines() + _, version_str = lines[0].split() + + version_parts = version_str.split(".") + return (int(version_parts[0]), int(version_parts[1])) + + +@pytest.mark.parametrize( + "build_config", + [ + {"CIBW_PLATFORM": "ios"}, + {"CIBW_PLATFORM": "iphonesimulator"}, + {"CIBW_PLATFORM": "iphoneos"}, + # Also check the build frontend + {"CIBW_PLATFORM": "iphonesimulator", "CIBW_BUILD_FRONTEND": "build"}, + ], +) +def test_ios_platforms(tmp_path, capfd, build_config): + if utils.platform != "macos": + pytest.skip("this test can only run on macOS") + if get_xcode_version() < (13, 0): + pytest.skip("this test only works with Xcode 13.0 or greater") + + project_dir = tmp_path / "project" + basic_project.generate(project_dir) + + actual_wheels = utils.cibuildwheel_run( + project_dir, + add_env=dict( + CIBW_BUILD="cp313-*", + CIBW_TEST_SOURCES="tests", + CIBW_TEST_COMMAND="tests", + **build_config, + ), + ) + + captured = capfd.readouterr() + + platform_machine = platform.machine() + # Tests are only executed on device + if build_config["CIBW_PLATFORM"] != "iphoneos": + if platform_machine == "x86_64": + # Ensure that tests were run on arm64. + assert "running tests on x86_64" in captured.out + elif platform_machine == "arm64": + # Ensure that tests were run on arm64. + assert "running tests on arm64" in captured.out + + expected_wheels = set() + ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_") + + if build_config["CIBW_PLATFORM"] in {"ios", "iphoneos"}: + expected_wheels.update( + { + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl", + } + ) + + if build_config["CIBW_PLATFORM"] in {"ios", "iphonesimulator"}: + expected_wheels.update( + { + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl", + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl", + } + ) + + assert set(actual_wheels) == expected_wheels diff --git a/test/test_projects/c.py b/test/test_projects/c.py index cea72bdf5..1ac8c170f 100644 --- a/test/test_projects/c.py +++ b/test/test_projects/c.py @@ -18,7 +18,7 @@ if (!PyArg_ParseTuple(args, "s", &command)) return NULL; - sts = system(command); + sts = printf("Running system command: %s\n", command); {{ spam_c_function_add | indent(4) }} From e992cef7d5c13c02f66bdfc618751b0eb4335e21 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 25 Feb 2025 10:57:36 +0800 Subject: [PATCH 02/37] Replace use of system() in test binary module. --- test/test_emulation.py | 4 ++-- test/test_projects/c.py | 11 ++++++----- test/test_testing.py | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/test/test_emulation.py b/test/test_emulation.py index 118b34ace..2ffdc8091 100644 --- a/test/test_emulation.py +++ b/test/test_emulation.py @@ -13,8 +13,8 @@ import spam def test_spam(): - assert spam.system('python -c "exit(0)"') == 0 - assert spam.system('python -c "exit(1)"') != 0 + assert spam.filter("spam") == 0 + assert spam.filter("ham") != 0 """ diff --git a/test/test_projects/c.py b/test/test_projects/c.py index 1ac8c170f..abc66c575 100644 --- a/test/test_projects/c.py +++ b/test/test_projects/c.py @@ -10,15 +10,16 @@ {{ spam_c_top_level_add }} static PyObject * -spam_system(PyObject *self, PyObject *args) +spam_filter(PyObject *self, PyObject *args) { - const char *command; + const char *content; int sts; - if (!PyArg_ParseTuple(args, "s", &command)) + if (!PyArg_ParseTuple(args, "s", &content)) return NULL; - sts = printf("Running system command: %s\n", command); + // Spam should not be allowed through the filter. + sts = strcmp(content, "spam"); {{ spam_c_function_add | indent(4) }} @@ -27,7 +28,7 @@ /* Module initialization */ static PyMethodDef module_methods[] = { - {"system", (PyCFunction)spam_system, METH_VARARGS, + {"filter", (PyCFunction)spam_filter, METH_VARARGS, "Execute a shell command."}, {NULL} /* Sentinel */ }; diff --git a/test/test_testing.py b/test/test_testing.py index 486afe6d9..0b9c971b3 100644 --- a/test/test_testing.py +++ b/test/test_testing.py @@ -46,9 +46,9 @@ def path_contains(parent, child): class TestSpam(TestCase): - def test_system(self): - self.assertEqual(0, spam.system('python -c "exit(0)"')) - self.assertNotEqual(0, spam.system('python -c "exit(1)"')) + def test_filter(self): + self.assertEqual(0, spam.filter("spam")) + self.assertNotEqual(0, spam.filter("ham")) def test_virtualenv(self): # sys.prefix is different from sys.base_prefix when running a virtualenv From a450fbaa43b2755bff031de380e9fc53f6ef68fa Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 25 Feb 2025 11:12:08 +0800 Subject: [PATCH 03/37] Restored the 'minimal' approach of the minimal examples. --- README.md | 40 +++++++------------------------------ docs/setup.md | 8 ++++++-- examples/github-minimal.yml | 36 +++++++-------------------------- examples/github-pipx.yml | 31 ++++------------------------ 4 files changed, 24 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 7a8a09d0f..88d105e46 100644 --- a/README.md +++ b/README.md @@ -87,33 +87,11 @@ on: [push, pull_request] jobs: build_wheels: - name: Build wheels for ${{ matrix.os }} - runs-on: ${{ matrix.runs-on }} + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} strategy: matrix: - os: [ linux-intel, linux-arm, windows, macOS-intel, macOS-arm, iOS, pyodide ] - include: - - archs: auto - platform: auto - - os: linux-intel - runs-on: ubuntu-latest - - os: linux-arm - runs-on: ubuntu-24.04-arm - - os: windows - runs-on: windows-latest - - os: macOS-intel - # macos-13 was the last x86_64 runner - runs-on: macos-13 - - os: macOS-arm - # macos-14+ (including latest) are ARM64 runners - runs-on: macos-latest - archs: auto,universal2 - - os: iOS - runs-on: macos-latest - platform: ios - - os: pyodide - runs-on: ubuntu-latest - platform: pyodide + os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-latest] steps: - uses: actions/checkout@v4 @@ -126,14 +104,10 @@ jobs: - name: Build wheels run: python -m cibuildwheel --output-dir wheelhouse - env: - CIBW_PLATFORM: ${{ matrix.platform }} - CIBW_ARCHS: ${{ matrix.archs }} - # Can also be configured directly, using `with:` - # with: - # package-dir: . - # output-dir: wheelhouse - # config-file: "{package}/pyproject.toml" + # to supply options, put them in 'env', like: + # env: + # CIBW_SOME_OPTION: value + # ... - uses: actions/upload-artifact@v4 with: diff --git a/docs/setup.md b/docs/setup.md index 89481fe31..4bbc363f2 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -147,7 +147,7 @@ You must target pyodide with `--platform pyodide` (or use `--only` on the identi ### GitHub Actions [linux/mac/windows] {: #github-actions} -To build Linux, Windows, macOS, iOS and pyodide wheels using GitHub Actions, create a `.github/workflows/build_wheels.yml` file in your repo. +To build Linux, macOS, and Windows wheels using GitHub Actions, create a `.github/workflows/build_wheels.yml` file in your repo. !!! tab "Action" For GitHub Actions, `cibuildwheel` provides an action you can use. This is @@ -160,6 +160,10 @@ To build Linux, Windows, macOS, iOS and pyodide wheels using GitHub Actions, cre {% include "../examples/github-minimal.yml" %} ``` + Use `env:` to pass [build options](options.md) and `with:` to set + `package-dir: .`, `output-dir: wheelhouse` and `config-file: ''` + locations (those values are the defaults). + !!! tab "pipx" The GitHub Actions runners have pipx installed, so you can easily build in just one line. This is internally how the action works; the main benefit of @@ -188,7 +192,7 @@ Commit this file, and push to GitHub - either to your default branch, or to a PR For more info on this file, check out the [docs](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions). -[`examples/github-deploy.yml`](https://github.com/pypa/cibuildwheel/blob/main/examples/github-deploy.yml) extends this minimal example with a demonstration of how to automatically upload the built wheels to PyPI. +[`examples/github-deploy.yml`](https://github.com/pypa/cibuildwheel/blob/main/examples/github-deploy.yml) extends this minimal example to include iOS and Pyodide builds, and a demonstration of how to automatically upload the built wheels to PyPI. ### Azure Pipelines [linux/mac/windows] {: #azure-pipelines} diff --git a/examples/github-minimal.yml b/examples/github-minimal.yml index 39c0f56f0..0f51170eb 100644 --- a/examples/github-minimal.yml +++ b/examples/github-minimal.yml @@ -4,43 +4,21 @@ on: [push, pull_request] jobs: build_wheels: - name: Build wheels for ${{ matrix.os }} - runs-on: ${{ matrix.runs-on }} + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} strategy: matrix: - os: [ linux-intel, linux-arm, windows, macOS-intel, macOS-arm, iOS, pyodide ] - include: - - archs: auto - platform: auto - - os: linux-intel - runs-on: ubuntu-latest - - os: linux-arm - runs-on: ubuntu-22.04-arm - - os: windows - runs-on: windows-latest - - os: macOS-intel - # macos-13 was the last x86_64 runner - runs-on: macos-13 - - os: macOS-arm - # macos-14+ (including latest) are ARM64 runners - runs-on: macos-latest - archs: auto,universal2 - - os: iOS - runs-on: macos-latest - platform: ios - - os: pyodide - runs-on: ubuntu-latest - platform: pyodide + # macos-13 is an intel runner, macos-14 is apple silicon + os: [ ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14 ] steps: - uses: actions/checkout@v4 - name: Build wheels uses: pypa/cibuildwheel@v2.22.0 - env: - CIBW_PLATFORM: ${{ matrix.platform }} - CIBW_ARCHS: ${{ matrix.archs }} - # Can also be configured directly, using `with:` + # env: + # CIBW_SOME_OPTION: value + # ... # with: # package-dir: . # output-dir: wheelhouse diff --git a/examples/github-pipx.yml b/examples/github-pipx.yml index 5ca0325de..6345f92e0 100644 --- a/examples/github-pipx.yml +++ b/examples/github-pipx.yml @@ -4,41 +4,18 @@ on: [push, pull_request] jobs: build_wheels: - name: Build wheels for ${{ matrix.os }} - runs-on: ${{ matrix.runs-on }} + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} strategy: matrix: - os: [ linux, windows, macOS-intel, macOS-arm, iOS, pyodide ] - include: - - archs: auto - platform: auto - - os: linux - runs-on: ubuntu-latest - - os: windows - runs-on: windows-latest - - os: macOS-intel - # macos-13 was the last x86_64 runner - runs-on: macos-13 - - os: macOS-arm - # macos-14+ (including latest) are ARM64 runners - runs-on: macos-latest - archs: auto,universal2 - - os: iOS - runs-on: macos-latest - platform: ios - - os: pyodide - runs-on: ubuntu-latest - platform: pyodide + # macos-13 is an intel runner, macos-14 is apple silicon + os: [ubuntu-latest, windows-latest, macos-13, macos-14] steps: - uses: actions/checkout@v4 - name: Build wheels run: pipx run cibuildwheel==2.22.0 - # To supply options, put them in 'env' - env: - CIBW_PLATFORM: ${{ matrix.platform }} - CIBW_ARCHS: ${{ matrix.archs }} - uses: actions/upload-artifact@v4 with: From ac8ab09b989ede48f141460abf235e8480322a62 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 25 Feb 2025 12:29:52 +0800 Subject: [PATCH 04/37] Split out platform details into standalone pages, and expand iOS platform details. --- docs/contributing.md | 2 +- docs/platforms/ios.md | 51 +++++++++++++++++++++ docs/platforms/linux.md | 13 ++++++ docs/platforms/macos.md | 34 ++++++++++++++ docs/platforms/pyodide.md | 13 ++++++ docs/platforms/windows.md | 15 +++++++ docs/setup.md | 93 +++++---------------------------------- mkdocs.yml | 24 ++++++---- 8 files changed, 153 insertions(+), 92 deletions(-) create mode 100644 docs/platforms/ios.md create mode 100644 docs/platforms/linux.md create mode 100644 docs/platforms/macos.md create mode 100644 docs/platforms/pyodide.md create mode 100644 docs/platforms/windows.md diff --git a/docs/contributing.md b/docs/contributing.md index 237708999..4eea79599 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -36,7 +36,7 @@ So, if we can, I'd like to improve the experience on errors as well. In [this](h ### Running the tests -When making a change to the codebase, you can run tests locally for quicker feedback than the CI runs on a PR. You can [run them directly](#making-a-venv), but the easiest way to run tests is using [nox](https://nox.thea.codes/). +When making a change to the codebase, you can run tests locally for quicker feedback than the CI runs on a PR. You can run them directly, but the easiest way to run tests is using [nox](https://nox.thea.codes/). You can run all the tests locally by doing: diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md new file mode 100644 index 000000000..84ff9c6a4 --- /dev/null +++ b/docs/platforms/ios.md @@ -0,0 +1,51 @@ +--- +title: 'iOS' +--- + +# iOS builds + +## Pre-requisites + +You must be building on a macOS machine, with Xcode installed. The Xcode installation must have an iOS SDK available. To check if an iOS SDK is available, open the Xcode settings panel, and check the Platforms tab. + +Building iOS wheel also requires a working macOS Python configuration. See the notes on [macOS builds](./macos.md) for details about configuration. + +## Specifying an iOS build + +iOS is effectively 2 platforms - physical devices, and simulators. While the API for these two platforms are identical, the ABI is not compatible, even when dealing with a device and simulator with the same CPU architecture. For this reason, iOS support includes 3 values for `CIBW_PLATFORM`: + +* `iphoneos` (for devices); +* `iphonesimulator` (for simulators); and +* `ios` for the combination of the two. + +A full build, using the `ios` platform, will results in 3 wheels (1 device, 2 simulator): + +* An ARM64 wheel for iOS devices; +* An ARM64 wheel for the iOS simulator; and +* An x86_64 wheel for the iOS simulator. + +Alternatively, you can build only wheels for iOS devices by using the `iphoneos` platform; or only wheels for iOS simulators by using the `iphonesimulator`. + +## iOS version compatibility + +iOS builds will honor the `IPHONEOS_DEPLOYMENT_TARGET` environment variable to set the minimum supported API version for generated wheels. This will default to `13.0` if the environment variable isn't set. + +## Cross platform builds + +iOS builds are *cross platform builds*, as it not possible to run compilers and other build tools "on device". The pre-compiled iOS binaries used to support iOS builds include tooling that can convert any virtual environment into a cross platform virtual environment - that is, an environment that can run binaries on the build machine (macOS), but, if asked, will respond as if it is an iOS machine. This allows pip, build, and other build tools to perform iOS-appropriate behaviour. + +## Build frontend support + +iOS builds support both the pip and build build frontends. In principle, support for uv with the `build[uv]` frontend should be possible, but uv [doesn't currently have support for cross-platform builds](https://github.com/astral-sh/uv/issues/7957), and [doesn't have support for iOS (or Android) tags](https://github.com/astral-sh/uv/issues/8029). + +## Build environment + +The environment used to run builds does not inherit the full user environment - in particular, PATH is deliberately re-written. This is because UNIX C tooling doesn't do a great job differentiating between "macOS ARM64" and "iOS ARM64" binaries. If (for example) Homebrew is on the path when compilation commands are invoked, it's easy for a macOS version of a library to be linked into the iOS binary, rendering it useless. To prevent this, iOS builds always force PATH to a "known minimal" path, that includes only the bare system utilities, plus the current user's cargo folder (to facilitate Rust builds). + +## Tests + +If tests have been configured, the test suite will be executed on the simulator matching the architecture of the build machine - that is, if you're building on an ARM64 macOS machine, the ARM64 wheel will be tested on an ARM64 simulator. It is not possible to use cibuildwheel to test wheels on other simulators, or on physical devices. + +The iOS test environment can't support running shell scripts, so the `CIBW_TEST_COMMAND` value must be specified as if it were a command line being passed to `python -m ...`. In addition, the test itself must be run "on device", so the local project directory cannot be used to run tests. Instead, the entire project sources must be copied onto the test device; or the project must use `CIBW_TEST_SOURCES` to specify the minimum subset of files that should be copied to the test environment. + +The test process uses the same testbed used by CPython itself to run the CPython test suite. It is an Xcode project that has been configured to have a single Xcode "XCUnit" test - the result of which reports the success or failure of running `python -m `. diff --git a/docs/platforms/linux.md b/docs/platforms/linux.md new file mode 100644 index 000000000..ef86439c3 --- /dev/null +++ b/docs/platforms/linux.md @@ -0,0 +1,13 @@ +### Linux builds + +If you've got [Docker](https://www.docker.com/products/docker-desktop) installed on your development machine, you can run a Linux build. + +!!! tip + You can run the Linux build on any platform. Even Windows can run + Linux containers these days, but there are a few hoops to jump + through. Check [this document](https://docs.microsoft.com/en-us/virtualization/windowscontainers/quick-start/quick-start-windows-10-linux) + for more info. + +Because the builds are happening in manylinux Docker containers, they're perfectly reproducible. + +The only side effect to your system will be docker images being pulled. diff --git a/docs/platforms/macos.md b/docs/platforms/macos.md new file mode 100644 index 000000000..b1496c797 --- /dev/null +++ b/docs/platforms/macos.md @@ -0,0 +1,34 @@ +--- +title: 'macOS' +--- + +# macOS builds + +## Pre-requisites + +Pre-requisite: you need to have native build tools installed. + +Because the builds are happening without full isolation, there might be some differences compared to CI builds (Xcode version, OS version, local files, ...) that might prevent you from finding an issue only seen in CI. + +In order to speed-up builds, cibuildwheel will cache the tools it needs to be reused for future builds. The folder used for caching is system/user dependent and is reported in the printed preamble of each run (e.g. `Cache folder: /Users/Matt/Library/Caches/cibuildwheel`). + +You can override the cache folder using the `CIBW_CACHE_PATH` environment variable. + +!!! warning + cibuildwheel uses official python.org macOS installers for CPython butthose can only be installed globally. + + In order not to mess with your system, cibuildwheel won't install those if they are missing. Instead, it will error out with a message to let you install th missing + + CPython: + + ```console + Error: CPython 3.6 is not installed. + cibuildwheel will not perform system-wide installs when running outside of CI. + To build locally, install CPython 3.6 on this machine, or, disable this version of Python using CIBW_SKIP=cp36-macosx_* + + Download link: https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg + ``` + +## Universal builds + +By default, macOS builds will build a single architecture wheel, using the build machine's architecture. If you need to support both x86_64 and Apple Silicon, you can use the `CIBW_ARCHS` environment variable to specify the architectures you want to build, or the value `universal` to build a multi-architecture wheel. cibuildwheel can test x86_64 wheels (or the x86_64 slice of a universal wheel) when running on Apple Silicon hardware, but it is *not* possible to test Apple Silicon wheels on x86_64 hardware. diff --git a/docs/platforms/pyodide.md b/docs/platforms/pyodide.md new file mode 100644 index 000000000..a6edad681 --- /dev/null +++ b/docs/platforms/pyodide.md @@ -0,0 +1,13 @@ +--- +title: 'Pyodide' +--- + +# Pyodide (WebAssembly) builds (experimental) + +## Prerequisites + +You need to have a matching host version of Python (unlike all other cibuildwheel platforms). Linux host highly recommended; macOS hosts may work (e.g. invoking `pytest` directly in [`CIBW_TEST_COMMAND`](../options.md#test-command) is [currently failing](https://github.com/pyodide/pyodide/issues/4802)) and Windows hosts will not work. + +## Specifying a pyodide build + +You must target pyodide with `--platform pyodide` (or use `--only` on the identifier). diff --git a/docs/platforms/windows.md b/docs/platforms/windows.md new file mode 100644 index 000000000..421fa8543 --- /dev/null +++ b/docs/platforms/windows.md @@ -0,0 +1,15 @@ +--- +title: 'Windows' +--- + +# Windows builds + +## Pre-requisites + +You must have native build tools (i.e., Visual Studio) installed. + +Because the builds are happening without full isolation, there might be some differences compared to CI builds (Visual Studio version, OS version, local files, ...) that might prevent you from finding an issue only seen in CI. + +In order to speed-up builds, cibuildwheel will cache the tools it needs to be reused for future builds. The folder used for caching is system/user dependent and is reported in the printed preamble of each run (e.g. `Cache folder: C:\Users\Matt\AppData\Local\pypa\cibuildwheel\Cache`). + +You can override the cache folder using the ``CIBW_CACHE_PATH`` environment variable. diff --git a/docs/setup.md b/docs/setup.md index 4bbc363f2..e82f5a3e8 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -4,6 +4,16 @@ title: 'Setup' # Setup +## Platform support + +Each platform that cibuildwheel supports has its own prerequisites and platform-specific behaviors. cibuildwheel supports the following platforms: + +* [Linux](./platforms/linux.md) +* [Windows](./platforms/windows.md) +* [macOS](./platforms/macos.md) +* [iOS](./platforms/ios.md) +* [Experimental: Pyodide (WebAssembly)](./platforms/pyodide.md) + ## Run cibuildwheel locally (optional) {: #local} Before getting to CI setup, it can be convenient to test cibuildwheel @@ -60,89 +70,6 @@ You should see the builds taking place. You can experiment with options using en cibuildwheel ``` -### Linux builds - -If you've got [Docker](https://www.docker.com/products/docker-desktop) installed on -your development machine, you can run a Linux build. - -!!! tip - You can run the Linux build on any platform. Even Windows can run - Linux containers these days, but there are a few hoops to jump - through. Check [this document](https://docs.microsoft.com/en-us/virtualization/windowscontainers/quick-start/quick-start-windows-10-linux) - for more info. - -Because the builds are happening in manylinux Docker containers, -they're perfectly reproducible. - -The only side effect to your system will be docker images being pulled. - -### macOS / Windows builds {: #macos-windows} - -Pre-requisite: you need to have native build tools installed. - -Because the builds are happening without full isolation, there might be some -differences compared to CI builds (Xcode version, Visual Studio version, -OS version, local files, ...) that might prevent you from finding an issue only -seen in CI. - -In order to speed-up builds, cibuildwheel will cache the tools it needs to be -reused for future builds. The folder used for caching is system/user dependent and is -reported in the printed preamble of each run (e.g. "Cache folder: /Users/Matt/Library/Caches/cibuildwheel"). - -You can override the cache folder using the ``CIBW_CACHE_PATH`` environment variable. - -!!! warning - cibuildwheel uses official python.org macOS installers for CPython but - those can only be installed globally. - - In order not to mess with your system, cibuildwheel won't install those if they are - missing. Instead, it will error out with a message to let you install the missing - CPython: - - ```console - Error: CPython 3.6 is not installed. - cibuildwheel will not perform system-wide installs when running outside of CI. - To build locally, install CPython 3.6 on this machine, or, disable this version of Python using CIBW_SKIP=cp36-macosx_* - - Download link: https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg - ``` - -### iOS builds - -Pre-requisite: You must be building on a macOS machine, with Xcode installed. -The Xcode installation must have an iOS SDK available. To check if an iOS SDK -is available, open the Xcode settings panel, and check the Platforms tab. - -To build iOS wheels, pass in `--platform ios` when invoking `cibuildwheel`. This will -build three wheels: - -* An ARM64 wheel for iOS devices; -* An ARM64 wheel for the iOS simulator; and -* An x86_64 wheel for the iOS simulator. - -Alternatively, you can build only wheels for iOS devices by using -`--platform iphoneos`; or only wheels for iOS simulators by using -`--platform iphonesimulator`. - -Building iOS wheel also requires a working macOS Python configuration. See the notes -on [macOS builds](setup.md#macos-windows) for details about configuration. - -iOS builds will honor the `IPHONEOS_DEPLOYMENT_TARGET` environment variable to set the -minimum supported API version for generated wheels. This will default to `13.0` if the -environment variable isn't set. - -If tests have been configured, the test suite will be executed on the simulator -matching the architecture of the build machine - that is, if you're building on -an ARM64 macOS machine, the ARM64 wheel will be tested on an ARM64 simulator. - -### Pyodide (WebAssembly) builds (experimental) - -Pre-requisite: you need to have a matching host version of Python (unlike all -other cibuildwheel platforms). Linux host highly recommended; macOS hosts may -work (e.g. invoking `pytest` directly in [`CIBW_TEST_COMMAND`](options.md#test-command) is [currently failing](https://github.com/pyodide/pyodide/issues/4802)) and Windows hosts will not work. - -You must target pyodide with `--platform pyodide` (or use `--only` on the identifier). - ## Configure a CI service ### GitHub Actions [linux/mac/windows] {: #github-actions} diff --git a/mkdocs.yml b/mkdocs.yml index 6749726dc..1d0150736 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,14 +20,22 @@ extra_javascript: nav: - Home: index.md - - setup.md - - options.md - - deliver-to-pypi.md - - cpp_standards.md - - faq.md - - working-examples.md - - contributing.md - - changelog.md + - User guide: + - setup.md + - options.md + - deliver-to-pypi.md + - cpp_standards.md + - faq.md + - working-examples.md + - Supported Platforms: + - platforms/linux.md + - platforms/windows.md + - platforms/macos.md + - platforms/ios.md + - platforms/pyodide.md + - About the project: + - contributing.md + - changelog.md markdown_extensions: - md_in_html From 254b9090ebca251fe3a550fc851cb34606982785 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 25 Feb 2025 12:45:16 +0800 Subject: [PATCH 05/37] More doc corrections. --- README.md | 2 +- docs/platforms/macos.md | 2 +- examples/github-minimal.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 88d105e46..a13c42f8d 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Usage Example setup ------------- -To build manylinux, musllinux, macOS (producing x86_64, ARM64 and universal2 wheels), Windows, iOS and pyodide wheels on GitHub Actions, you could use this `.github/workflows/wheels.yml`: +To build manylinux, musllinux, macOS, and Windows wheels on GitHub Actions, you could use this `.github/workflows/wheels.yml`: ```yaml diff --git a/docs/platforms/macos.md b/docs/platforms/macos.md index b1496c797..359e06d4e 100644 --- a/docs/platforms/macos.md +++ b/docs/platforms/macos.md @@ -31,4 +31,4 @@ You can override the cache folder using the `CIBW_CACHE_PATH` environment variab ## Universal builds -By default, macOS builds will build a single architecture wheel, using the build machine's architecture. If you need to support both x86_64 and Apple Silicon, you can use the `CIBW_ARCHS` environment variable to specify the architectures you want to build, or the value `universal` to build a multi-architecture wheel. cibuildwheel can test x86_64 wheels (or the x86_64 slice of a universal wheel) when running on Apple Silicon hardware, but it is *not* possible to test Apple Silicon wheels on x86_64 hardware. +By default, macOS builds will build a single architecture wheel, using the build machine's architecture. If you need to support both x86_64 and Apple Silicon, you can use the `CIBW_ARCHS` environment variable to specify the architectures you want to build, or the value `universal2` to build a multi-architecture wheel. cibuildwheel can test x86_64 wheels (or the x86_64 slice of a `universal2` wheel) when running on Apple Silicon hardware, but it is *not* possible to test Apple Silicon wheels on x86_64 hardware. diff --git a/examples/github-minimal.yml b/examples/github-minimal.yml index 0f51170eb..7d2574d1e 100644 --- a/examples/github-minimal.yml +++ b/examples/github-minimal.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: # macos-13 is an intel runner, macos-14 is apple silicon - os: [ ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14 ] + os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14] steps: - uses: actions/checkout@v4 From cead9fb209a4e7cd19b7a7e18d428b24585af41f Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 26 Feb 2025 07:53:32 +0800 Subject: [PATCH 06/37] Bump support package to include fix for python/cpython#130292 --- cibuildwheel/resources/build-platforms.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cibuildwheel/resources/build-platforms.toml b/cibuildwheel/resources/build-platforms.toml index 9d7372c8f..aad18d9a1 100644 --- a/cibuildwheel/resources/build-platforms.toml +++ b/cibuildwheel/resources/build-platforms.toml @@ -166,11 +166,11 @@ python_configurations = [ [iphoneos] python_configurations = [ - { identifier = "cp313-ios_arm64_iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b4/Python-3.13-iOS-support.b4.tar.gz" }, + { identifier = "cp313-ios_arm64_iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, ] [iphonesimulator] python_configurations = [ - { identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b4/Python-3.13-iOS-support.b4.tar.gz" }, - { identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b4/Python-3.13-iOS-support.b4.tar.gz" }, + { identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, + { identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, ] From 7b8b31eebd3fe7b6b3608268c999008f6f3a0f28 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 26 Feb 2025 08:34:51 +0800 Subject: [PATCH 07/37] Ensure iOS tests are all run on the same xdist worker. --- bin/run_tests.py | 3 +++ test/test_ios.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/bin/run_tests.py b/bin/run_tests.py index 264cf267f..87898b7d2 100755 --- a/bin/run_tests.py +++ b/bin/run_tests.py @@ -42,12 +42,15 @@ sys.executable, "-m", "pytest", + "--dist", + "loadgroup", f"--numprocesses={args.num_processes}", "-x", "--durations", "0", "--timeout=2400", "test", + "-vv", ] if sys.platform.startswith("linux") and args.run_podman: diff --git a/test/test_ios.py b/test/test_ios.py index d4953dca1..a2445f490 100644 --- a/test/test_ios.py +++ b/test/test_ios.py @@ -37,6 +37,13 @@ def get_xcode_version() -> tuple[int, int]: return (int(version_parts[0]), int(version_parts[1])) +# iOS tests can't be run in parallel, because they're dependent on starting a +# simulator. It's *possible* to start multiple simulators, but probably not +# advisable to start as many simulators as there are CPUs on the test machine. +# There's also an underlying issue with the testbed starting multiple simulators +# in parallel (https://github.com/python/cpython/issues/129200); even if that +# issue is resolved, it's probably desirable to *not* parallelize iOS tests. +@pytest.mark.xdist_group(name="ios") @pytest.mark.parametrize( "build_config", [ From 9c495a866a907a9fb09670a281bbf76af70ccc7c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 26 Feb 2025 09:39:34 +0800 Subject: [PATCH 08/37] More iOS documentation tweaks. --- docs/options.md | 26 +++++++++++++------------- docs/platforms/ios.md | 23 ++++++++++++++++++++--- docs/platforms/macos.md | 8 +++----- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/docs/options.md b/docs/options.md index cb8527124..3ee5edb74 100644 --- a/docs/options.md +++ b/docs/options.md @@ -293,18 +293,18 @@ When setting the options, you can use shell-style globbing syntax, as per [fnmat
-| | macOS | Windows | Linux Intel | Linux Other | iOS | -|---------------|------------------------------------------------------------------------|-----------------------------------------------------|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| -| Python 3.8 | cp38-macosx_x86_64
cp38-macosx_universal2
cp38-macosx_arm64 | cp38-win_amd64
cp38-win32 | cp38-manylinux_x86_64
cp38-manylinux_i686
cp38-musllinux_x86_64
cp38-musllinux_i686 | cp38-manylinux_aarch64
cp38-manylinux_ppc64le
cp38-manylinux_s390x
cp38-manylinux_armv7l
cp38-musllinux_aarch64
cp38-musllinux_ppc64le
cp38-musllinux_s390x
cp38-musllinux_armv7l | | -| Python 3.9 | cp39-macosx_x86_64
cp39-macosx_universal2
cp39-macosx_arm64 | cp39-win_amd64
cp39-win32
cp39-win_arm64 | cp39-manylinux_x86_64
cp39-manylinux_i686
cp39-musllinux_x86_64
cp39-musllinux_i686 | cp39-manylinux_aarch64
cp39-manylinux_ppc64le
cp39-manylinux_s390x
cp39-manylinux_armv7l
cp39-musllinux_aarch64
cp39-musllinux_ppc64le
cp39-musllinux_s390x
cp39-musllinux_armv7l | | -| Python 3.10 | cp310-macosx_x86_64
cp310-macosx_universal2
cp310-macosx_arm64 | cp310-win_amd64
cp310-win32
cp310-win_arm64 | cp310-manylinux_x86_64
cp310-manylinux_i686
cp310-musllinux_x86_64
cp310-musllinux_i686 | cp310-manylinux_aarch64
cp310-manylinux_ppc64le
cp310-manylinux_s390x
cp310-manylinux_armv7l
cp310-musllinux_aarch64
cp310-musllinux_ppc64le
cp310-musllinux_s390x
cp310-musllinux_armv7l | | -| Python 3.11 | cp311-macosx_x86_64
cp311-macosx_universal2
cp311-macosx_arm64 | cp311-win_amd64
cp311-win32
cp311-win_arm64 | cp311-manylinux_x86_64
cp311-manylinux_i686
cp311-musllinux_x86_64
cp311-musllinux_i686 | cp311-manylinux_aarch64
cp311-manylinux_ppc64le
cp311-manylinux_s390x
cp311-manylinux_armv7l
cp311-musllinux_aarch64
cp311-musllinux_ppc64le
cp311-musllinux_s390x
cp311-musllinux_armv7l | | -| Python 3.12 | cp312-macosx_x86_64
cp312-macosx_universal2
cp312-macosx_arm64 | cp312-win_amd64
cp312-win32
cp312-win_arm64 | cp312-manylinux_x86_64
cp312-manylinux_i686
cp312-musllinux_x86_64
cp312-musllinux_i686 | cp312-manylinux_aarch64
cp312-manylinux_ppc64le
cp312-manylinux_s390x
cp312-musllinux_armv7l
cp312-musllinux_ppc64le
cp312-musllinux_s390x
cp312-musllinux_armv7l | | -| Python 3.13 | cp313-macosx_x86_64
cp313-macosx_universal2
cp313-macosx_arm64 | cp313-win_amd64
cp313-win32
cp313-win_arm64 | cp313-manylinux_x86_64
cp313-manylinux_i686
cp313-musllinux_x86_64
cp313-musllinux_i686 | cp313-manylinux_aarch64
cp313-manylinux_ppc64le
cp313-manylinux_s390x
cp313-manylinux_armv7l
cp313-musllinux_aarch64
cp313-musllinux_ppc64le
cp313-musllinux_s390x
cp313-musllinux_armv7l | cp313-ios_arm64_iphoneos
cp313-ios_arm64_iphonesimulator
cp313-ios_x86_64_iphoneos | -| PyPy3.8 v7.3 | pp38-macosx_x86_64
pp38-macosx_arm64 | pp38-win_amd64 | pp38-manylinux_x86_64
pp38-manylinux_i686 | pp38-manylinux_aarch64 | | -| PyPy3.9 v7.3 | pp39-macosx_x86_64
pp39-macosx_arm64 | pp39-win_amd64 | pp39-manylinux_x86_64
pp39-manylinux_i686 | pp39-manylinux_aarch64 | | -| PyPy3.10 v7.3 | pp310-macosx_x86_64
pp310-macosx_arm64 | pp310-win_amd64 | pp310-manylinux_x86_64
pp310-manylinux_i686 | pp310-manylinux_aarch64 | | -| PyPy3.11 v7.3 | pp311-macosx_x86_64
pp311-macosx_arm64 | pp311-win_amd64 | pp311-manylinux_x86_64
pp311-manylinux_i686 | pp311-manylinux_aarch64 | | +| | macOS | Windows | Linux Intel | Linux Other | iOS | +|---------------|------------------------------------------------------------------------|-----------------------------------------------------|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------| +| Python 3.8 | cp38-macosx_x86_64
cp38-macosx_universal2
cp38-macosx_arm64 | cp38-win_amd64
cp38-win32 | cp38-manylinux_x86_64
cp38-manylinux_i686
cp38-musllinux_x86_64
cp38-musllinux_i686 | cp38-manylinux_aarch64
cp38-manylinux_ppc64le
cp38-manylinux_s390x
cp38-manylinux_armv7l
cp38-musllinux_aarch64
cp38-musllinux_ppc64le
cp38-musllinux_s390x
cp38-musllinux_armv7l | | +| Python 3.9 | cp39-macosx_x86_64
cp39-macosx_universal2
cp39-macosx_arm64 | cp39-win_amd64
cp39-win32
cp39-win_arm64 | cp39-manylinux_x86_64
cp39-manylinux_i686
cp39-musllinux_x86_64
cp39-musllinux_i686 | cp39-manylinux_aarch64
cp39-manylinux_ppc64le
cp39-manylinux_s390x
cp39-manylinux_armv7l
cp39-musllinux_aarch64
cp39-musllinux_ppc64le
cp39-musllinux_s390x
cp39-musllinux_armv7l | | +| Python 3.10 | cp310-macosx_x86_64
cp310-macosx_universal2
cp310-macosx_arm64 | cp310-win_amd64
cp310-win32
cp310-win_arm64 | cp310-manylinux_x86_64
cp310-manylinux_i686
cp310-musllinux_x86_64
cp310-musllinux_i686 | cp310-manylinux_aarch64
cp310-manylinux_ppc64le
cp310-manylinux_s390x
cp310-manylinux_armv7l
cp310-musllinux_aarch64
cp310-musllinux_ppc64le
cp310-musllinux_s390x
cp310-musllinux_armv7l | | +| Python 3.11 | cp311-macosx_x86_64
cp311-macosx_universal2
cp311-macosx_arm64 | cp311-win_amd64
cp311-win32
cp311-win_arm64 | cp311-manylinux_x86_64
cp311-manylinux_i686
cp311-musllinux_x86_64
cp311-musllinux_i686 | cp311-manylinux_aarch64
cp311-manylinux_ppc64le
cp311-manylinux_s390x
cp311-manylinux_armv7l
cp311-musllinux_aarch64
cp311-musllinux_ppc64le
cp311-musllinux_s390x
cp311-musllinux_armv7l | | +| Python 3.12 | cp312-macosx_x86_64
cp312-macosx_universal2
cp312-macosx_arm64 | cp312-win_amd64
cp312-win32
cp312-win_arm64 | cp312-manylinux_x86_64
cp312-manylinux_i686
cp312-musllinux_x86_64
cp312-musllinux_i686 | cp312-manylinux_aarch64
cp312-manylinux_ppc64le
cp312-manylinux_s390x
cp312-musllinux_armv7l
cp312-musllinux_ppc64le
cp312-musllinux_s390x
cp312-musllinux_armv7l | | +| Python 3.13 | cp313-macosx_x86_64
cp313-macosx_universal2
cp313-macosx_arm64 | cp313-win_amd64
cp313-win32
cp313-win_arm64 | cp313-manylinux_x86_64
cp313-manylinux_i686
cp313-musllinux_x86_64
cp313-musllinux_i686 | cp313-manylinux_aarch64
cp313-manylinux_ppc64le
cp313-manylinux_s390x
cp313-manylinux_armv7l
cp313-musllinux_aarch64
cp313-musllinux_ppc64le
cp313-musllinux_s390x
cp313-musllinux_armv7l | cp313-ios_arm64_iphoneos
cp313-ios_arm64_iphonesimulator
cp313-ios_x86_64_iphonesimulator | +| PyPy3.8 v7.3 | pp38-macosx_x86_64
pp38-macosx_arm64 | pp38-win_amd64 | pp38-manylinux_x86_64
pp38-manylinux_i686 | pp38-manylinux_aarch64 | | +| PyPy3.9 v7.3 | pp39-macosx_x86_64
pp39-macosx_arm64 | pp39-win_amd64 | pp39-manylinux_x86_64
pp39-manylinux_i686 | pp39-manylinux_aarch64 | | +| PyPy3.10 v7.3 | pp310-macosx_x86_64
pp310-macosx_arm64 | pp310-win_amd64 | pp310-manylinux_x86_64
pp310-manylinux_i686 | pp310-manylinux_aarch64 | | +| PyPy3.11 v7.3 | pp311-macosx_x86_64
pp311-macosx_arm64 | pp311-win_amd64 | pp311-manylinux_x86_64
pp311-manylinux_i686 | pp311-manylinux_aarch64 | | The list of supported and currently selected build identifiers can also be retrieved by passing the `--print-build-identifiers` flag to cibuildwheel. The format is `python_tag-platform_tag`, with tags similar to those in [PEP 425](https://www.python.org/dev/peps/pep-0425/#details). @@ -1455,7 +1455,7 @@ On all platforms other than iOS, the command is run in a shell, so you can write things like `cmd1 && cmd2`. On iOS, the value of the `CIBW_TEST_COMMAND` setting is interpreted as the arguments -to pass `python -m`. Shell commands cannot be used. +to pass to `python -m`. Shell commands cannot be used. Platform-specific environment variables are also available:
`CIBW_TEST_COMMAND_MACOS` | `CIBW_TEST_COMMAND_WINDOWS` | `CIBW_TEST_COMMAND_LINUX` | `CIBW_TEST_COMMAND_IPHONEOS` | `CIBW_TEST_COMMAND_IPHONESIMULATOR` | `CIBW_TEST_COMMAND_PYODIDE` diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md index 84ff9c6a4..1244b94b8 100644 --- a/docs/platforms/ios.md +++ b/docs/platforms/ios.md @@ -6,9 +6,9 @@ title: 'iOS' ## Pre-requisites -You must be building on a macOS machine, with Xcode installed. The Xcode installation must have an iOS SDK available. To check if an iOS SDK is available, open the Xcode settings panel, and check the Platforms tab. +You must be building on a macOS machine, with Xcode installed. The Xcode installation must have an iOS SDK available, with all license agreements agreed to by the user. To check if an iOS SDK is available, open the Xcode settings panel, and check the Platforms tab. This will also ensure that license agreements have been acknowledged. -Building iOS wheel also requires a working macOS Python configuration. See the notes on [macOS builds](./macos.md) for details about configuration. +Building iOS wheel also requires a working macOS Python configuration. See the notes on [macOS builds](./macos.md) for details about configuration of the macOS environment. ## Specifying an iOS build @@ -40,7 +40,7 @@ iOS builds support both the pip and build build frontends. In principle, support ## Build environment -The environment used to run builds does not inherit the full user environment - in particular, PATH is deliberately re-written. This is because UNIX C tooling doesn't do a great job differentiating between "macOS ARM64" and "iOS ARM64" binaries. If (for example) Homebrew is on the path when compilation commands are invoked, it's easy for a macOS version of a library to be linked into the iOS binary, rendering it useless. To prevent this, iOS builds always force PATH to a "known minimal" path, that includes only the bare system utilities, plus the current user's cargo folder (to facilitate Rust builds). +The environment used to run builds does not inherit the full user environment - in particular, `PATH` is deliberately re-written. This is because UNIX C tooling doesn't do a great job differentiating between "macOS ARM64" and "iOS ARM64" binaries. If (for example) Homebrew is on the path when compilation commands are invoked, it's easy for a macOS version of a library to be linked into the iOS binary, rendering it unusable on iOS. To prevent this, iOS builds always force `PATH` to a "known minimal" path, that includes only the bare system utilities, plus the current user's cargo folder (to facilitate Rust builds). ## Tests @@ -49,3 +49,20 @@ If tests have been configured, the test suite will be executed on the simulator The iOS test environment can't support running shell scripts, so the `CIBW_TEST_COMMAND` value must be specified as if it were a command line being passed to `python -m ...`. In addition, the test itself must be run "on device", so the local project directory cannot be used to run tests. Instead, the entire project sources must be copied onto the test device; or the project must use `CIBW_TEST_SOURCES` to specify the minimum subset of files that should be copied to the test environment. The test process uses the same testbed used by CPython itself to run the CPython test suite. It is an Xcode project that has been configured to have a single Xcode "XCUnit" test - the result of which reports the success or failure of running `python -m `. + +!!! warning + iOS tests cannot be run in parallel. + + The CPython iOS test runner requires starting a simulator to run the tests. There is [a known issue with the CPpython iOS test runner](https://github.com/python/cpython/issues/129200) that can cause problems starting multiple simulators in parallel. If you attempt to start multiple testbed instances at the same time, you may see a failure that looks like: + + ```console + note: Run script build phase 'Prepare Python Binary Modules' will be run during every build because the option to run the script phase "Based on dependency analysis" is unchecked. (in target 'iOSTestbed' from project 'iOSTestbed') + note: Run script build phase 'Install Target Specific Python Standard Library' will be run during every build because the option to run the script phase "Based on dependency analysis" is unchecked. (in target 'iOSTestbed' from project 'iOSTestbed') + Found more than one new device: {'5CAA0336-9CE1-4222-BFE3-ADA405F766DE', 'DD108383-685A-4400-BF30-013AA82C4A61'} + make: *** [testios] Error 1 + program finished with exit code 2 + ``` + + However, even when this issue is resolved, you likely don't want to start too many iOS simulators on the same machine. It is advisable to either run iOS tests sequentially, or use use a feature such as [pytest's `loadgroup` mechanism](https://pytest-xdist.readthedocs.io/en/stable/distribution.html) to ensure that all iOS tests run sequentially. + + Note that this only applies to running multiple iOS test *projects* in parallel. A single test suite can run in parallel on a single iOS simulator; this limitation only applies to starting multiple independent simulators. A normal cibuildwheel run will only start one iOS simulator at a time; if you perform multiple cibuildwheel runs in parallel on the same machine, you might see this problem. diff --git a/docs/platforms/macos.md b/docs/platforms/macos.md index 1ac388b94..ba38a8585 100644 --- a/docs/platforms/macos.md +++ b/docs/platforms/macos.md @@ -15,11 +15,9 @@ In order to speed-up builds, cibuildwheel will cache the tools it needs to be re You can override the cache folder using the `CIBW_CACHE_PATH` environment variable. !!! warning - cibuildwheel uses official python.org macOS installers for CPython butthose can only be installed globally. + cibuildwheel uses official python.org macOS installers for CPython but those can only be installed globally. - In order not to mess with your system, cibuildwheel won't install those if they are missing. Instead, it will error out with a message to let you install th missing - - CPython: + In order not to mess with your system, cibuildwheel won't install those if they are missing. Instead, it will error out with a message to let you install th missing CPython: ```console Error: CPython 3.9 is not installed. @@ -31,4 +29,4 @@ You can override the cache folder using the `CIBW_CACHE_PATH` environment variab ## Universal builds -By default, macOS builds will build a single architecture wheel, using the build machine's architecture. If you need to support both x86_64 and Apple Silicon, you can use the `CIBW_ARCHS` environment variable to specify the architectures you want to build, or the value `universal2` to build a multi-architecture wheel. cibuildwheel can test x86_64 wheels (or the x86_64 slice of a `universal2` wheel) when running on Apple Silicon hardware, but it is *not* possible to test Apple Silicon wheels on x86_64 hardware. +By default, macOS builds will build a single architecture wheel, using the build machine's architecture. If you need to support both x86_64 and Apple Silicon, you can use the `CIBW_ARCHS` environment variable to specify the architectures you want to build, or the value `universal2` to build a multi-architecture wheel. cibuildwheel will test x86_64 wheels (or the x86_64 slice of a `universal2` wheel) when running on Apple Silicon hardware, but it is *not* possible to test Apple Silicon wheels on x86_64 hardware. From 457d46aecd0173b4b6e623901957813687d1f7f4 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 2 Mar 2025 12:52:45 +0800 Subject: [PATCH 09/37] Factor out common xcode version test utility. --- test/test_ios.py | 17 +---------------- test/test_macos_archs.py | 23 ++++------------------- test/utils.py | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 35 deletions(-) diff --git a/test/test_ios.py b/test/test_ios.py index a2445f490..48bde68f9 100644 --- a/test/test_ios.py +++ b/test/test_ios.py @@ -2,7 +2,6 @@ import os import platform -import subprocess import pytest @@ -23,20 +22,6 @@ """ -def get_xcode_version() -> tuple[int, int]: - output = subprocess.run( - ["xcodebuild", "-version"], - text=True, - check=True, - stdout=subprocess.PIPE, - ).stdout - lines = output.splitlines() - _, version_str = lines[0].split() - - version_parts = version_str.split(".") - return (int(version_parts[0]), int(version_parts[1])) - - # iOS tests can't be run in parallel, because they're dependent on starting a # simulator. It's *possible* to start multiple simulators, but probably not # advisable to start as many simulators as there are CPUs on the test machine. @@ -57,7 +42,7 @@ def get_xcode_version() -> tuple[int, int]: def test_ios_platforms(tmp_path, capfd, build_config): if utils.platform != "macos": pytest.skip("this test can only run on macOS") - if get_xcode_version() < (13, 0): + if utils.get_xcode_version() < (13, 0): pytest.skip("this test only works with Xcode 13.0 or greater") project_dir = tmp_path / "project" diff --git a/test/test_macos_archs.py b/test/test_macos_archs.py index 52811f794..848303ec8 100644 --- a/test/test_macos_archs.py +++ b/test/test_macos_archs.py @@ -1,5 +1,4 @@ import platform -import subprocess import pytest @@ -20,24 +19,10 @@ DEPLOYMENT_TARGET_TOO_LOW_WARNING = "Bumping MACOSX_DEPLOYMENT_TARGET" -def get_xcode_version() -> tuple[int, int]: - output = subprocess.run( - ["xcodebuild", "-version"], - text=True, - check=True, - stdout=subprocess.PIPE, - ).stdout - lines = output.splitlines() - _, version_str = lines[0].split() - - version_parts = version_str.split(".") - return (int(version_parts[0]), int(version_parts[1])) - - def test_cross_compiled_build(tmp_path): if utils.platform != "macos": pytest.skip("this test is only relevant to macos") - if get_xcode_version() < (12, 2): + if utils.get_xcode_version() < (12, 2): pytest.skip("this test only works with Xcode 12.2 or greater") project_dir = tmp_path / "project" @@ -71,7 +56,7 @@ def test_cross_compiled_build(tmp_path): def test_cross_compiled_test(tmp_path, capfd, build_universal2, test_config): if utils.platform != "macos": pytest.skip("this test is only relevant to macos") - if get_xcode_version() < (12, 2): + if utils.get_xcode_version() < (12, 2): pytest.skip("this test only works with Xcode 12.2 or greater") project_dir = tmp_path / "project" @@ -153,7 +138,7 @@ def test_deployment_target_warning_is_firing(tmp_path, capfd): def test_universal2_testing_on_x86_64(tmp_path, capfd, skip_arm64_test): if utils.platform != "macos": pytest.skip("this test is only relevant to macos") - if get_xcode_version() < (12, 2): + if utils.get_xcode_version() < (12, 2): pytest.skip("this test only works with Xcode 12.2 or greater") if platform.machine() != "x86_64": pytest.skip("this test only works on x86_64") @@ -223,7 +208,7 @@ def test_universal2_testing_on_arm64(build_frontend_env, tmp_path, capfd): def test_cp38_arm64_testing(tmp_path, capfd, request): if utils.platform != "macos": pytest.skip("this test is only relevant to macos") - if get_xcode_version() < (12, 2): + if utils.get_xcode_version() < (12, 2): pytest.skip("this test only works with Xcode 12.2 or greater") if platform.machine() != "arm64": pytest.skip("this test only works on arm64") diff --git a/test/utils.py b/test/utils.py index 8195e4923..921494e26 100644 --- a/test/utils.py +++ b/test/utils.py @@ -335,6 +335,21 @@ def get_macos_version() -> tuple[int, int]: return tuple(map(int, version_str.split(".")[:2])) # type: ignore[return-value] +def get_xcode_version() -> tuple[int, int]: + """Calls `xcodebuild -version` to retrieve the Xcode version as a 2-tuple.""" + output = subprocess.run( + ["xcodebuild", "-version"], + text=True, + check=True, + stdout=subprocess.PIPE, + ).stdout + lines = output.splitlines() + _, version_str = lines[0].split() + + version_parts = version_str.split(".") + return (int(version_parts[0]), int(version_parts[1])) + + def skip_if_pyodide(reason: str) -> Any: return pytest.mark.skipif(platform == "pyodide", reason=reason) From 05ac4ae49e1a3fc5c2cae3e5f2d096808a095cfe Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 2 Mar 2025 16:28:10 +0800 Subject: [PATCH 10/37] Simplify iOS to a single platform with an expanded interpretation of arch. --- cibuildwheel/__main__.py | 21 +---- cibuildwheel/architecture.py | 52 +++++----- cibuildwheel/ios.py | 94 ++++++------------- cibuildwheel/logger.py | 6 +- cibuildwheel/resources/build-platforms.toml | 12 +-- .../resources/cibuildwheel.schema.json | 49 +--------- cibuildwheel/typing.py | 10 +- cibuildwheel/util/packaging.py | 2 +- docs/options.md | 39 ++++---- docs/platforms/ios.md | 52 +++++----- 10 files changed, 116 insertions(+), 221 deletions(-) diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index 191ef4bb4..e16adca9c 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -94,24 +94,14 @@ def main_inner(global_options: GlobalOptions) -> None: parser.add_argument( "--platform", - choices=[ - "auto", - "linux", - "macos", - "windows", - "pyodide", - "ios", - "iphoneos", - "iphonesimulator", - ], + choices=["auto", "linux", "macos", "windows", "pyodide", "ios"], default=None, help=""" Platform to build for. Use this option to override the auto-detected platform. Specifying "macos" or "windows" only works on that operating system. "linux" works on any desktop OS, as long as Docker/Podman is installed. "pyodide" only works on linux and macOS. - "ios", "iphoneos" and "iphonesimulator" only work on macOS. Default: - auto. + "ios" only work on macOS. Default: auto. """, ) @@ -253,10 +243,7 @@ def _compute_platform_only(only: str) -> PlatformName: if "pyodide_" in only: return "pyodide" if "ios_" in only: - if "_iphonesimulator" in only: - return "iphonesimulator" - else: - return "iphoneos" + return "ios" msg = f"Invalid --only='{only}', must be a build selector with a known platform" raise errors.ConfigurationError(msg) @@ -320,8 +307,6 @@ def get_platform_module(platform: PlatformName) -> PlatformModule: return cibuildwheel.pyodide if platform == "ios": return cibuildwheel.ios - if platform == "iphoneos" or platform == "iphonesimulator": - return cibuildwheel.ios.PlatformSDKModule(platform) assert_never(platform) diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py index eb68b547f..51d1ee70e 100644 --- a/cibuildwheel/architecture.py +++ b/cibuildwheel/architecture.py @@ -18,8 +18,6 @@ "windows": "Windows", "pyodide": "Pyodide", "ios": "iOS", - "iphoneos": "iOS (Device)", - "iphonesimulator": "iOS (Simulator)", } ARCH_SYNONYMS: Final[list[dict[PlatformName, str | None]]] = [ @@ -66,6 +64,12 @@ class Architecture(StrEnum): # WebAssembly wasm32 = auto() + # iOS "multiarch" architectures that include both + # the CPU architecture and the ABI. + arm64_device = "arm64-iphoneos" + arm64_simulator = "arm64-iphonesimulator" + x86_64_simulator = "x86_64-iphonesimulator" + @staticmethod def parse_config(config: str, platform: PlatformName) -> "set[Architecture]": result = set() @@ -104,18 +108,14 @@ def native_arch(platform: PlatformName) -> "Architecture | None": if platform == "pyodide": return Architecture.wasm32 - elif platform in {"ios", "iphonesimulator"}: - # Can only build for iOS on macOS + elif platform == "ios": + # Can only build for iOS on macOS. The "native" architecture is the + # simulator for the macOS native platform. if host_platform == "macos": - # iOS Simulators matches the platform architecture. - return native_architecture - else: - return None - elif platform == "iphoneos": - # Can only build for iOS devices on macOS - if host_platform == "macos": - # iOS devices only support arm64 - return Architecture.arm64 + if native_architecture == Architecture.x86_64: + return Architecture.x86_64_simulator + else: + return Architecture.arm64_simulator else: return None @@ -141,15 +141,7 @@ def auto_archs(platform: PlatformName) -> "set[Architecture]": return set() # can't build anything on this platform result = {native_arch} - if platform == "iphoneos": - # iOS devices only support 1 architecture - result = {Architecture.arm64} - - elif platform in {"ios", "iphonesimulator"}: - # iOS defaults to building all architectures - result = {Architecture.x86_64, Architecture.arm64} - - elif platform == "linux": + if platform == "linux": if Architecture.x86_64 in result: # x86_64 machines can run i686 containers result.add(Architecture.i686) @@ -159,6 +151,14 @@ def auto_archs(platform: PlatformName) -> "set[Architecture]": elif platform == "windows" and Architecture.AMD64 in result: result.add(Architecture.x86) + elif platform == "ios": + # iOS defaults to building all architectures + result = { + Architecture.x86_64_simulator, + Architecture.arm64_simulator, + Architecture.arm64_device, + } + return result @staticmethod @@ -175,10 +175,12 @@ def all_archs(platform: PlatformName) -> "set[Architecture]": "macos": {Architecture.x86_64, Architecture.arm64, Architecture.universal2}, "windows": {Architecture.x86, Architecture.AMD64, Architecture.ARM64}, "pyodide": {Architecture.wasm32}, - "iphoneos": {Architecture.arm64}, - "iphonesimulator": {Architecture.arm64, Architecture.x86_64}, + "ios": { + Architecture.x86_64_simulator, + Architecture.arm64_simulator, + Architecture.arm64_device, + }, } - all_archs_map["ios"] = all_archs_map["iphoneos"] | all_archs_map["iphonesimulator"] return all_archs_map[platform] @staticmethod diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index 1885ef603..c4e1fb63d 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -24,7 +24,7 @@ from .macos import install_cpython as install_build_cpython from .options import Options from .selector import BuildSelector -from .typing import GenericPythonConfiguration, PathOrStr, PlatformName +from .typing import PathOrStr from .util import resources from .util.cmd import call, shell from .util.file import ( @@ -52,32 +52,28 @@ class PythonConfiguration: @property def sdk(self) -> str: - return self.identifier.split("-")[1].rsplit("_", 1)[1] + return self.multiarch.rsplit("-", 1)[1] @property def arch(self) -> str: - return self.identifier.split("-")[1].split("_", 1)[1].rsplit("_", 1)[0] + return self.multiarch.rsplit("-", 1)[0] @property def multiarch(self) -> str: - return f"{self.arch}-{self.sdk}" + return self.identifier.split("-ios_")[1] @property def is_simulator(self) -> bool: - return self.sdk.endswith("simulator") + return self.identifier.endswith("-iphonesimulator") @property def slice(self) -> str: - return { - "iphoneos": "ios-arm64", - "iphonesimulator": "ios-arm64_x86_64-simulator", - }[self.sdk] + return "ios-arm64_x86_64-simulator" if self.is_simulator else "ios-arm64" def get_python_configurations( build_selector: BuildSelector, architectures: Set[Architecture], - sdk: PlatformName | None = None, ) -> list[PythonConfiguration]: # iOS builds are always cross builds; we need to install a macOS Python as # well. Rather than duplicate the location of the URL of macOS installers, @@ -90,20 +86,15 @@ def build_url(item: dict[str, str]) -> str: # The iOS item will be something like cp313-ios_arm64_iphoneos. Drop # the iphoneos suffix, then replace ios with macosx to yield # cp313-macosx_arm64, which will be a macOS configuration item. - macos_identifier = item["identifier"].rsplit("_", 1)[0] - macos_identifier = macos_identifier.replace("ios", "macosx") + parts = item["identifier"].split("-") + macos_identifier = f"{parts[0]}-{parts[1].replace('ios', 'macosx')}" matching = [ config for config in macos_python_configs if config["identifier"] == macos_identifier ] return matching[0]["url"] # Load the platform configuration - if sdk: - full_python_configs = resources.read_python_configs(sdk) - else: - full_python_configs = resources.read_python_configs("iphoneos") - full_python_configs += resources.read_python_configs("iphonesimulator") - + full_python_configs = resources.read_python_configs("ios") # Build the configurations, annotating with macOS URL details. python_configurations = [ PythonConfiguration( @@ -117,7 +108,7 @@ def build_url(item: dict[str, str]) -> str: python_configurations = [ c for c in python_configurations - if any(c.identifier.rsplit("_", 1)[0].endswith(a.value) for a in architectures) + if any(c.identifier.endswith(f"-ios_{a.value}") for a in architectures) ] # Skip builds as required by BUILD/SKIP @@ -211,23 +202,22 @@ def cross_virtualenv( # # To prevent problems, set the PATH to isolate the build environment from # sources that could introduce incompatible binaries. - if sys.platform == "darwin": - env["PATH"] = os.pathsep.join( - [ - # The host python's binary directory - str(host_python.parent), - # The cross-platform environments binary directory - str(venv_path / "bin"), - # Cargo's binary directory (to allow for Rust compilation) - str(Path.home() / ".cargo" / "bin"), - # The bare minimum Apple system paths. - "/usr/bin", - "/bin", - "/usr/sbin", - "/sbin", - "/Library/Apple/usr/bin", - ] - ) + env["PATH"] = os.pathsep.join( + [ + # The host python's binary directory + str(host_python.parent), + # The cross-platform environments binary directory + str(venv_path / "bin"), + # Cargo's binary directory (to allow for Rust compilation) + str(Path.home() / ".cargo" / "bin"), + # The bare minimum Apple system paths. + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + "/Library/Apple/usr/bin", + ] + ) return env @@ -365,11 +355,14 @@ def setup_python( return host_install_path, env -def build(options: Options, tmp_path: Path, sdk: PlatformName | None = None) -> None: +def build(options: Options, tmp_path: Path) -> None: + if sys.platform != "darwin": + msg = "iOS binaries can only be built on macOS" + raise ValueError(msg) + python_configurations = get_python_configurations( build_selector=options.globals.build_selector, architectures=options.globals.architectures, - sdk=sdk, ) if not python_configurations: @@ -608,28 +601,3 @@ def build(options: Options, tmp_path: Path, sdk: PlatformName | None = None) -> except subprocess.CalledProcessError as error: msg = f"Command {error.cmd} failed with code {error.returncode}. {error.stdout or ''}" raise errors.FatalError(msg) from error - - -# The default iOS platform will build for all SDKs on the platform (both -# iphoneos and iphonesimulator). PlatformSDKModule allows the construction of an -# interface that behaves just like the iOS platform, but only exposes a single -# SDK. -class PlatformSDKModule: - def __init__(self, sdk: PlatformName): - self.sdk = sdk - - def get_python_configurations( - self, build_selector: BuildSelector, architectures: Set[Architecture] - ) -> Sequence[GenericPythonConfiguration]: - return get_python_configurations( - build_selector=build_selector, - architectures=architectures, - sdk=self.sdk, - ) - - def build(self, options: Options, tmp_path: Path) -> None: - build( - options=options, - tmp_path=tmp_path, - sdk=self.sdk, - ) diff --git a/cibuildwheel/logger.py b/cibuildwheel/logger.py index 391786a04..39397b20d 100644 --- a/cibuildwheel/logger.py +++ b/cibuildwheel/logger.py @@ -35,9 +35,9 @@ "macosx_universal2": "macOS Universal 2 - x86_64 and arm64", "macosx_arm64": "macOS arm64 - Apple Silicon", "pyodide_wasm32": "Pyodide", - "ios_arm64_iphoneos": "iOS Device (ARM64)", - "ios_arm64_iphonesimulator": "iOS Simulator (ARM64)", - "ios_x86_64_iphonesimulator": "iOS Simulator (x86_64)", + "ios_arm64-iphoneos": "iOS Device (ARM64)", + "ios_arm64-iphonesimulator": "iOS Simulator (ARM64)", + "ios_x86_64-iphonesimulator": "iOS Simulator (x86_64)", } diff --git a/cibuildwheel/resources/build-platforms.toml b/cibuildwheel/resources/build-platforms.toml index aad18d9a1..1a7f74136 100644 --- a/cibuildwheel/resources/build-platforms.toml +++ b/cibuildwheel/resources/build-platforms.toml @@ -164,13 +164,9 @@ python_configurations = [ { identifier = "cp312-pyodide_wasm32", version = "3.12", pyodide_version = "0.27.0", pyodide_build_version = "0.29.2", emscripten_version = "3.1.58", node_version = "v20" }, ] -[iphoneos] +[ios] python_configurations = [ - { identifier = "cp313-ios_arm64_iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, -] - -[iphonesimulator] -python_configurations = [ - { identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, - { identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, + { identifier = "cp313-ios_arm64-iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, + { identifier = "cp313-ios_x86_64-iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, + { identifier = "cp313-ios_arm64-iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, ] diff --git a/cibuildwheel/resources/cibuildwheel.schema.json b/cibuildwheel/resources/cibuildwheel.schema.json index dd3eeb005..6ccb03848 100644 --- a/cibuildwheel/resources/cibuildwheel.schema.json +++ b/cibuildwheel/resources/cibuildwheel.schema.json @@ -919,7 +919,7 @@ } } }, - "iphoneos": { + "ios": { "type": "object", "additionalProperties": false, "properties": { @@ -962,52 +962,7 @@ "test-requires": { "$ref": "#/properties/test-requires" } - } - }, - "iphonesimulator": { - "type": "object", - "additionalProperties": false, - "properties": { - "archs": { - "$ref": "#/properties/archs" - }, - "before-all": { - "$ref": "#/properties/before-all" - }, - "before-build": { - "$ref": "#/properties/before-build" - }, - "before-test": { - "$ref": "#/properties/before-test" - }, - "build-frontend": { - "$ref": "#/properties/build-frontend" - }, - "build-verbosity": { - "$ref": "#/properties/build-verbosity" - }, - "config-settings": { - "$ref": "#/properties/config-settings" - }, - "dependency-versions": { - "$ref": "#/properties/dependency-versions" - }, - "environment": { - "$ref": "#/properties/environment" - }, - "repair-wheel-command": { - "$ref": "#/properties/repair-wheel-command" - }, - "test-command": { - "$ref": "#/properties/test-command" - }, - "test-extras": { - "$ref": "#/properties/test-extras" - }, - "test-requires": { - "$ref": "#/properties/test-requires" - } - } + } } } } diff --git a/cibuildwheel/typing.py b/cibuildwheel/typing.py index 37cea6fff..420ef05f6 100644 --- a/cibuildwheel/typing.py +++ b/cibuildwheel/typing.py @@ -12,15 +12,7 @@ PathOrStr = str | os.PathLike[str] -PlatformName = Literal[ - "linux", - "macos", - "windows", - "pyodide", - "ios", - "iphoneos", - "iphonesimulator", -] +PlatformName = Literal["linux", "macos", "windows", "pyodide", "ios"] PLATFORMS: Final[frozenset[PlatformName]] = frozenset(typing.get_args(PlatformName)) diff --git a/cibuildwheel/util/packaging.py b/cibuildwheel/util/packaging.py index 0e6e755c5..78683da4e 100644 --- a/cibuildwheel/util/packaging.py +++ b/cibuildwheel/util/packaging.py @@ -70,7 +70,7 @@ def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None: specified by `identifier` that is previously built. """ - interpreter, platform = identifier.split("-") + interpreter, platform = identifier.split("-", 1) free_threaded = interpreter.endswith("t") if free_threaded: interpreter = interpreter[:-1] diff --git a/docs/options.md b/docs/options.md index 3ee5edb74..b4fb605cb 100644 --- a/docs/options.md +++ b/docs/options.md @@ -247,7 +247,7 @@ environment variables will completely override any TOML configuration. > Override the auto-detected target platform -Options: `auto` `linux` `macos` `windows` `ios` `iphoneos` `iphonesimulator` `pyodide` +Options: `auto` `linux` `macos` `windows` `ios` `pyodide` Default: `auto` @@ -255,15 +255,11 @@ Default: `auto` - For `linux`, you need [Docker or Podman](#container-engine) running, on Linux, macOS, or Windows. - For `macos` and `windows`, you need to be running on the respective system, with a working compiler toolchain installed - Xcode Command Line tools for macOS, and MSVC for Windows. -- For `ios`, `iphoneos` and `iphonesimulator`, you need to be running on macOS, with Xcode and the iOS simulator installed. - `ios` is an alias for the combination of both `iphoneos` and `iphonesimulator`. +- For `ios` you need to be running on macOS, with Xcode and the iOS simulator installed. - For `pyodide` you need to be on an x86-64 linux runner and `python3.12` must be available in `PATH`. This option can also be set using the [command-line option](#command-line) `--platform`. This option is not available in the `pyproject.toml` config. -!!! note - The separation between `iphoneos` and `iphonesimulator` is required because although the API for all iOS builds is identical, builds for physical devices and simulators must use different SDKs. You can use the `ios` platform to target *both* devices and simulators in a single build; however, configurations must be specified independently for each iOS SDK. - !!! tip You can use this option to locally debug your cibuildwheel config on Linux, instead of pushing to CI to test every change. For example: @@ -300,7 +296,7 @@ When setting the options, you can use shell-style globbing syntax, as per [fnmat | Python 3.10 | cp310-macosx_x86_64
cp310-macosx_universal2
cp310-macosx_arm64 | cp310-win_amd64
cp310-win32
cp310-win_arm64 | cp310-manylinux_x86_64
cp310-manylinux_i686
cp310-musllinux_x86_64
cp310-musllinux_i686 | cp310-manylinux_aarch64
cp310-manylinux_ppc64le
cp310-manylinux_s390x
cp310-manylinux_armv7l
cp310-musllinux_aarch64
cp310-musllinux_ppc64le
cp310-musllinux_s390x
cp310-musllinux_armv7l | | | Python 3.11 | cp311-macosx_x86_64
cp311-macosx_universal2
cp311-macosx_arm64 | cp311-win_amd64
cp311-win32
cp311-win_arm64 | cp311-manylinux_x86_64
cp311-manylinux_i686
cp311-musllinux_x86_64
cp311-musllinux_i686 | cp311-manylinux_aarch64
cp311-manylinux_ppc64le
cp311-manylinux_s390x
cp311-manylinux_armv7l
cp311-musllinux_aarch64
cp311-musllinux_ppc64le
cp311-musllinux_s390x
cp311-musllinux_armv7l | | | Python 3.12 | cp312-macosx_x86_64
cp312-macosx_universal2
cp312-macosx_arm64 | cp312-win_amd64
cp312-win32
cp312-win_arm64 | cp312-manylinux_x86_64
cp312-manylinux_i686
cp312-musllinux_x86_64
cp312-musllinux_i686 | cp312-manylinux_aarch64
cp312-manylinux_ppc64le
cp312-manylinux_s390x
cp312-musllinux_armv7l
cp312-musllinux_ppc64le
cp312-musllinux_s390x
cp312-musllinux_armv7l | | -| Python 3.13 | cp313-macosx_x86_64
cp313-macosx_universal2
cp313-macosx_arm64 | cp313-win_amd64
cp313-win32
cp313-win_arm64 | cp313-manylinux_x86_64
cp313-manylinux_i686
cp313-musllinux_x86_64
cp313-musllinux_i686 | cp313-manylinux_aarch64
cp313-manylinux_ppc64le
cp313-manylinux_s390x
cp313-manylinux_armv7l
cp313-musllinux_aarch64
cp313-musllinux_ppc64le
cp313-musllinux_s390x
cp313-musllinux_armv7l | cp313-ios_arm64_iphoneos
cp313-ios_arm64_iphonesimulator
cp313-ios_x86_64_iphonesimulator | +| Python 3.13 | cp313-macosx_x86_64
cp313-macosx_universal2
cp313-macosx_arm64 | cp313-win_amd64
cp313-win32
cp313-win_arm64 | cp313-manylinux_x86_64
cp313-manylinux_i686
cp313-musllinux_x86_64
cp313-musllinux_i686 | cp313-manylinux_aarch64
cp313-manylinux_ppc64le
cp313-manylinux_s390x
cp313-manylinux_armv7l
cp313-musllinux_aarch64
cp313-musllinux_ppc64le
cp313-musllinux_s390x
cp313-musllinux_armv7l | cp313-ios_arm64-iphoneos
cp313-ios_arm64-iphonesimulator
cp313-ios_x86_64-iphonesimulator | | PyPy3.8 v7.3 | pp38-macosx_x86_64
pp38-macosx_arm64 | pp38-win_amd64 | pp38-manylinux_x86_64
pp38-manylinux_i686 | pp38-manylinux_aarch64 | | | PyPy3.9 v7.3 | pp39-macosx_x86_64
pp39-macosx_arm64 | pp39-win_amd64 | pp39-manylinux_x86_64
pp39-manylinux_i686 | pp39-manylinux_aarch64 | | | PyPy3.10 v7.3 | pp310-macosx_x86_64
pp310-macosx_arm64 | pp310-win_amd64 | pp310-manylinux_x86_64
pp310-manylinux_i686 | pp310-manylinux_aarch64 | | @@ -440,6 +436,7 @@ Options: - macOS: `x86_64` `arm64` `universal2` - Windows: `AMD64` `x86` `ARM64` - Pyodide: `wasm32` +- iOS: `arm64-iphoneos` `arm64-iphonesimulator` `x86_64-iphonesimulator` - `auto`: The default archs for your machine - see the table below. - `auto64`: Just the 64-bit auto archs - `auto32`: Just the 32-bit auto archs @@ -457,8 +454,8 @@ Default: `auto` | Windows / ARM64 | `ARM64` | `ARM64` | `ARM64` | | | macOS / Intel | `x86_64` | `x86_64` | `x86_64` | | | macOS / Apple Silicon | `arm64` | `arm64` | `arm64` | | -| iOS on macOS / Intel | `x86_64` | `arm64` `x86_64` | `arm64` `x86_64` | | -| iOS on macOS / Apple Silicon | `arm64` | `arm64` `x86_64` | `arm64` `x86_64` | | +| iOS on macOS / Intel | `x86_64-iphonesimulator` | `arm64-iphoneos` `arm64-iphonesimulator` `x86_64-iphonesimulator` | `arm64-iphoneos` `arm64-iphonesimulator` `x86_64-iphonesimulator` | | +| iOS on macOS / Apple Silicon | `arm64-iphonesimulator` | `arm64-iphoneos` `arm64-iphonesimulator` `x86_64-iphonesimulator` | `arm64-iphoneos` `arm64-iphonesimulator` `x86_64-iphonesimulator` | | If not listed above, `auto` is the same as `native`. @@ -466,7 +463,7 @@ If not listed above, `auto` is the same as `native`. [binfmt]: https://hub.docker.com/r/tonistiigi/binfmt Platform-specific environment variables are also available:
- `CIBW_ARCHS_MACOS` | `CIBW_ARCHS_WINDOWS` | `CIBW_ARCHS_LINUX` | `CIBW_ARCHS_IPHONEOS` | `CIBW_ARCHS_IPHONESIMULATOR` + `CIBW_ARCHS_MACOS` | `CIBW_ARCHS_WINDOWS` | `CIBW_ARCHS_LINUX` | `CIBW_ARCHS_IOS` This option can also be set using the [command-line option](#command-line) `--archs`. This option cannot be set in an `overrides` section in `pyproject.toml`. @@ -759,7 +756,7 @@ a table of items, including arrays. single values. Platform-specific environment variables also available:
-`CIBW_CONFIG_SETTINGS_MACOS` | `CIBW_CONFIG_SETTINGS_WINDOWS` | `CIBW_CONFIG_SETTINGS_LINUX` | `CIBW_CONFIG_SETTINGS_IPHONEOS` | `CIBW_CONFIG_SETTINGS_IPHONESIMULATOR` | `CIBW_CONFIG_SETTINGS_PYODIDE` +`CIBW_CONFIG_SETTINGS_MACOS` | `CIBW_CONFIG_SETTINGS_WINDOWS` | `CIBW_CONFIG_SETTINGS_LINUX` | `CIBW_CONFIG_SETTINGS_IOS` | `CIBW_CONFIG_SETTINGS_PYODIDE` #### Examples @@ -790,7 +787,7 @@ You can use `$PATH` syntax to insert other variables, or the `$(pwd)` syntax to To specify more than one environment variable, separate the assignments by spaces. Platform-specific environment variables are also available:
-`CIBW_ENVIRONMENT_MACOS` | `CIBW_ENVIRONMENT_WINDOWS` | `CIBW_ENVIRONMENT_LINUX` | `CIBW_ENVIRONMENT_IPHONEOS` | `CIBW_ENVIRONMENT_IPHONESIMULATOR` | `CIBW_ENVIRONMENT_PYODIDE` +`CIBW_ENVIRONMENT_MACOS` | `CIBW_ENVIRONMENT_WINDOWS` | `CIBW_ENVIRONMENT_LINUX` | `CIBW_ENVIRONMENT_IOS` | `CIBW_ENVIRONMENT_PYODIDE` #### Examples @@ -922,7 +919,7 @@ On linux, overriding it triggers a new container launch. It cannot be overridden on macOS and Windows. Platform-specific environment variables also available:
-`CIBW_BEFORE_ALL_MACOS` | `CIBW_BEFORE_ALL_WINDOWS` | `CIBW_BEFORE_ALL_LINUX` | `CIBW_BEFORE_ALL_IPHONEOS` | `CIBW_BEFORE_ALL_IPHONESIMULATOR` | `CIBW_BEFORE_ALL_PYODIDE` +`CIBW_BEFORE_ALL_MACOS` | `CIBW_BEFORE_ALL_WINDOWS` | `CIBW_BEFORE_ALL_LINUX` | `CIBW_BEFORE_ALL_IOS` | `CIBW_BEFORE_ALL_PYODIDE` !!! note @@ -987,7 +984,7 @@ The active Python binary can be accessed using `python`, and pip with `pip`; cib The command is run in a shell, so you can write things like `cmd1 && cmd2`. Platform-specific environment variables are also available:
- `CIBW_BEFORE_BUILD_MACOS` | `CIBW_BEFORE_BUILD_WINDOWS` | `CIBW_BEFORE_BUILD_LINUX` | `CIBW_BEFORE_BUILD_IPHONEOS` | `CIBW_BEFORE_BUILD_IPHONESIMULATOR` | `CIBW_BEFORE_BUILD_PYODIDE` + `CIBW_BEFORE_BUILD_MACOS` | `CIBW_BEFORE_BUILD_WINDOWS` | `CIBW_BEFORE_BUILD_LINUX` | `CIBW_BEFORE_BUILD_IOS` | `CIBW_BEFORE_BUILD_PYODIDE` #### Examples @@ -1083,7 +1080,7 @@ The following placeholders must be used inside the command and will be replaced The command is run in a shell, so you can run multiple commands like `cmd1 && cmd2`. Platform-specific environment variables are also available:
-`CIBW_REPAIR_WHEEL_COMMAND_MACOS` | `CIBW_REPAIR_WHEEL_COMMAND_WINDOWS` | `CIBW_REPAIR_WHEEL_COMMAND_LINUX` | `CIBW_REPAIR_WHEEL_COMMAND_IPHONEOS` | `CIBW_REPAIR_WHEEL_COMMAND_IPHONESIMULATOR` | `CIBW_REPAIR_WHEEL_COMMAND_PYODIDE` +`CIBW_REPAIR_WHEEL_COMMAND_MACOS` | `CIBW_REPAIR_WHEEL_COMMAND_WINDOWS` | `CIBW_REPAIR_WHEEL_COMMAND_LINUX` | `CIBW_REPAIR_WHEEL_COMMAND_IOS` | `CIBW_REPAIR_WHEEL_COMMAND_PYODIDE` !!! tip cibuildwheel doesn't yet ship a default repair command for Windows. @@ -1391,7 +1388,7 @@ here and it will be used instead. `./constraints.txt` if that's not found. Platform-specific environment variables are also available:
-`CIBW_DEPENDENCY_VERSIONS_MACOS` | `CIBW_DEPENDENCY_VERSIONS_WINDOWS` | `CIBW_DEPENDENCY_VERSIONS_IPHONEOS` | `CIBW_DEPENDENCY_VERSIONS_IPHONESIMULATOR` | `CIBW_DEPENDENCY_VERSIONS_PYODIDE` +`CIBW_DEPENDENCY_VERSIONS_MACOS` | `CIBW_DEPENDENCY_VERSIONS_WINDOWS` | `CIBW_DEPENDENCY_VERSIONS_IOS` | `CIBW_DEPENDENCY_VERSIONS_PYODIDE` !!! note This option does not affect the tools used on the Linux build - those versions @@ -1458,7 +1455,7 @@ On iOS, the value of the `CIBW_TEST_COMMAND` setting is interpreted as the argum to pass to `python -m`. Shell commands cannot be used. Platform-specific environment variables are also available:
-`CIBW_TEST_COMMAND_MACOS` | `CIBW_TEST_COMMAND_WINDOWS` | `CIBW_TEST_COMMAND_LINUX` | `CIBW_TEST_COMMAND_IPHONEOS` | `CIBW_TEST_COMMAND_IPHONESIMULATOR` | `CIBW_TEST_COMMAND_PYODIDE` +`CIBW_TEST_COMMAND_MACOS` | `CIBW_TEST_COMMAND_WINDOWS` | `CIBW_TEST_COMMAND_LINUX` | `CIBW_TEST_COMMAND_IOS` | `CIBW_TEST_COMMAND_PYODIDE` #### Examples @@ -1515,7 +1512,7 @@ The active Python binary can be accessed using `python`, and pip with `pip`; cib The command is run in a shell, so you can write things like `cmd1 && cmd2`. Platform-specific environment variables are also available:
- `CIBW_BEFORE_TEST_MACOS` | `CIBW_BEFORE_TEST_WINDOWS` | `CIBW_BEFORE_TEST_LINUX` | `CIBW_BEFORE_TEST_IPHONEOS` | `CIBW_BEFORE_TEST_IPHONESIMULATOR` | `CIBW_BEFORE_TEST_PYODIDE` + `CIBW_BEFORE_TEST_MACOS` | `CIBW_BEFORE_TEST_WINDOWS` | `CIBW_BEFORE_TEST_LINUX` | `CIBW_BEFORE_TEST_IOS` | `CIBW_BEFORE_TEST_PYODIDE` #### Examples @@ -1606,7 +1603,7 @@ Platform-specific environment variables are also available:
Space-separated list of dependencies required for running the tests. Platform-specific environment variables are also available:
-`CIBW_TEST_REQUIRES_MACOS` | `CIBW_TEST_REQUIRES_WINDOWS` | `CIBW_TEST_REQUIRES_LINUX` | `CIBW_TEST_REQUIRES_IPHONEOS` | `CIBW_TEST_REQUIRES_IPHONESIMULATOR` | `CIBW_TEST_REQUIRES_PYODIDE` +`CIBW_TEST_REQUIRES_MACOS` | `CIBW_TEST_REQUIRES_WINDOWS` | `CIBW_TEST_REQUIRES_LINUX` | `CIBW_TEST_REQUIRES_IOS` | `CIBW_TEST_REQUIRES_PYODIDE` #### Examples @@ -1646,7 +1643,7 @@ tests. This can be used to avoid having to redefine test dependencies in `setup.cfg` or `setup.py`. Platform-specific environment variables are also available:
-`CIBW_TEST_EXTRAS_MACOS` | `CIBW_TEST_EXTRAS_WINDOWS` | `CIBW_TEST_EXTRAS_LINUX` | `CIBW_TEST_EXTRAS_IPHONEOS` | `CIBW_TEST_EXTRAS_IPHONESIMULATOR` | `CIBW_TEST_EXTRAS_PYODIDE` +`CIBW_TEST_EXTRAS_MACOS` | `CIBW_TEST_EXTRAS_WINDOWS` | `CIBW_TEST_EXTRAS_LINUX` | `CIBW_TEST_EXTRAS_IOS` | `CIBW_TEST_EXTRAS_PYODIDE` #### Examples @@ -1773,7 +1770,7 @@ export CIBW_DEBUG_TRACEBACK=TRUE A number from 1 to 3 to increase the level of verbosity (corresponding to invoking pip with `-v`, `-vv`, and `-vvv`), between -1 and -3 (`-q`, `-qq`, and `-qqq`), or just 0 (default verbosity). These flags are useful while debugging a build when the output of the actual build invoked by `pip wheel` is required. Has no effect on the `build` backend, which produces verbose output by default. Platform-specific environment variables are also available:
-`CIBW_BUILD_VERBOSITY_MACOS` | `CIBW_BUILD_VERBOSITY_WINDOWS` | `CIBW_BUILD_VERBOSITY_LINUX` | `CIBW_BUILD_VERBOSITY_IPHONEOS` | `CIBW_BUILD_VERBOSITY_IPHONESIMULATOR` | `CIBW_BUILD_VERBOSITY_PYODIDE` +`CIBW_BUILD_VERBOSITY_MACOS` | `CIBW_BUILD_VERBOSITY_WINDOWS` | `CIBW_BUILD_VERBOSITY_LINUX` | `CIBW_BUILD_VERBOSITY_IOS` | `CIBW_BUILD_VERBOSITY_PYODIDE` #### Examples diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md index 1244b94b8..01e377eb5 100644 --- a/docs/platforms/ios.md +++ b/docs/platforms/ios.md @@ -12,19 +12,36 @@ Building iOS wheel also requires a working macOS Python configuration. See the n ## Specifying an iOS build -iOS is effectively 2 platforms - physical devices, and simulators. While the API for these two platforms are identical, the ABI is not compatible, even when dealing with a device and simulator with the same CPU architecture. For this reason, iOS support includes 3 values for `CIBW_PLATFORM`: +iOS is effectively 2 platforms - physical devices, and simulators. While the API for these two platforms are identical, the ABI is not compatible, even when dealing with a device and simulator with the same CPU architecture. For this reason, the architecture specification for iOS builds includes *both* the CPU architecture *and* the ABI that is being targeted. There are three possible values for architecture on iOS; the values match those used by `sys.implementation._multiarch` when running on iOS: -* `iphoneos` (for devices); -* `iphonesimulator` (for simulators); and -* `ios` for the combination of the two. +* `arm64-iphoneos` (for physical iOS devices); +* `arm64-iphonesimulator` (for iOS simulators running on Apple Silicon macOS machines); and +* `x64_64-iphonesimulator` (for iOS simulators running on Intel macOS machines). -A full build, using the `ios` platform, will results in 3 wheels (1 device, 2 simulator): +By default, cibuildwheel will build wheels for all three of these targets. -* An ARM64 wheel for iOS devices; -* An ARM64 wheel for the iOS simulator; and -* An x86_64 wheel for the iOS simulator. +If you need to specify different compilation flags or other properties on a per-ABI or per-CPU basis, you can use [configuration overrides](..#overrides) with a `select` clause that targets the specific ABI or architecture. For example, consider the following example: -Alternatively, you can build only wheels for iOS devices by using the `iphoneos` platform; or only wheels for iOS simulators by using the `iphonesimulator`. +``` +[tool.cibuildwheel.ios] +test-sources = ["tests"] +test-requires = ["pytest"] + +[[tool.cibuildwheel.overrides]] +select = "*-iphoneos" +environment.PATH = "/path/to/special/device/details:..." + +[[tool.cibuildwheel.overrides]] +select = "*-arm64-*" +inherit.test-requires = "append" +test-requires = ["arm64-testing-helper"] +``` + +This configuration would: + + * Specify a `test-sources` and `test-requires` for all iOS targets; + * Add a `PATH` setting that will be used on physical iOS devices; and + * Add `arm64-testing-helper` to the test environment for all ARM64 devices (whether simulator or device). ## iOS version compatibility @@ -49,20 +66,3 @@ If tests have been configured, the test suite will be executed on the simulator The iOS test environment can't support running shell scripts, so the `CIBW_TEST_COMMAND` value must be specified as if it were a command line being passed to `python -m ...`. In addition, the test itself must be run "on device", so the local project directory cannot be used to run tests. Instead, the entire project sources must be copied onto the test device; or the project must use `CIBW_TEST_SOURCES` to specify the minimum subset of files that should be copied to the test environment. The test process uses the same testbed used by CPython itself to run the CPython test suite. It is an Xcode project that has been configured to have a single Xcode "XCUnit" test - the result of which reports the success or failure of running `python -m `. - -!!! warning - iOS tests cannot be run in parallel. - - The CPython iOS test runner requires starting a simulator to run the tests. There is [a known issue with the CPpython iOS test runner](https://github.com/python/cpython/issues/129200) that can cause problems starting multiple simulators in parallel. If you attempt to start multiple testbed instances at the same time, you may see a failure that looks like: - - ```console - note: Run script build phase 'Prepare Python Binary Modules' will be run during every build because the option to run the script phase "Based on dependency analysis" is unchecked. (in target 'iOSTestbed' from project 'iOSTestbed') - note: Run script build phase 'Install Target Specific Python Standard Library' will be run during every build because the option to run the script phase "Based on dependency analysis" is unchecked. (in target 'iOSTestbed' from project 'iOSTestbed') - Found more than one new device: {'5CAA0336-9CE1-4222-BFE3-ADA405F766DE', 'DD108383-685A-4400-BF30-013AA82C4A61'} - make: *** [testios] Error 1 - program finished with exit code 2 - ``` - - However, even when this issue is resolved, you likely don't want to start too many iOS simulators on the same machine. It is advisable to either run iOS tests sequentially, or use use a feature such as [pytest's `loadgroup` mechanism](https://pytest-xdist.readthedocs.io/en/stable/distribution.html) to ensure that all iOS tests run sequentially. - - Note that this only applies to running multiple iOS test *projects* in parallel. A single test suite can run in parallel on a single iOS simulator; this limitation only applies to starting multiple independent simulators. A normal cibuildwheel run will only start one iOS simulator at a time; if you perform multiple cibuildwheel runs in parallel on the same machine, you might see this problem. From 3b37676c77cbfef25e655ea79b6c5a1a98af044c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 2 Mar 2025 16:42:31 +0800 Subject: [PATCH 11/37] I guess I should update the iOS tests as well... --- test/test_ios.py | 48 ++++++++++++++++-------------------------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/test/test_ios.py b/test/test_ios.py index 48bde68f9..8faf1d033 100644 --- a/test/test_ios.py +++ b/test/test_ios.py @@ -22,21 +22,17 @@ """ -# iOS tests can't be run in parallel, because they're dependent on starting a -# simulator. It's *possible* to start multiple simulators, but probably not -# advisable to start as many simulators as there are CPUs on the test machine. -# There's also an underlying issue with the testbed starting multiple simulators -# in parallel (https://github.com/python/cpython/issues/129200); even if that -# issue is resolved, it's probably desirable to *not* parallelize iOS tests. +# iOS tests shouldn't be run in parallel, because they're dependent on starting +# a simulator. It's *possible* to start multiple simulators, but not advisable +# to start as many simulators as there are CPUs on the test machine. @pytest.mark.xdist_group(name="ios") @pytest.mark.parametrize( "build_config", [ + # Default to the pip build frontend. {"CIBW_PLATFORM": "ios"}, - {"CIBW_PLATFORM": "iphonesimulator"}, - {"CIBW_PLATFORM": "iphoneos"}, # Also check the build frontend - {"CIBW_PLATFORM": "iphonesimulator", "CIBW_BUILD_FRONTEND": "build"}, + {"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "build"}, ], ) def test_ios_platforms(tmp_path, capfd, build_config): @@ -62,30 +58,18 @@ def test_ios_platforms(tmp_path, capfd, build_config): platform_machine = platform.machine() # Tests are only executed on device - if build_config["CIBW_PLATFORM"] != "iphoneos": - if platform_machine == "x86_64": - # Ensure that tests were run on arm64. - assert "running tests on x86_64" in captured.out - elif platform_machine == "arm64": - # Ensure that tests were run on arm64. - assert "running tests on arm64" in captured.out + if platform_machine == "x86_64": + # Ensure that tests were run on arm64. + assert "running tests on x86_64" in captured.out + elif platform_machine == "arm64": + # Ensure that tests were run on arm64. + assert "running tests on arm64" in captured.out - expected_wheels = set() ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_") - - if build_config["CIBW_PLATFORM"] in {"ios", "iphoneos"}: - expected_wheels.update( - { - f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl", - } - ) - - if build_config["CIBW_PLATFORM"] in {"ios", "iphonesimulator"}: - expected_wheels.update( - { - f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl", - f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl", - } - ) + expected_wheels = { + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl", + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl", + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl", + } assert set(actual_wheels) == expected_wheels From 32b9203f72ff6b24ff35ed477eb45e919b2444b7 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 2 Mar 2025 16:58:36 +0800 Subject: [PATCH 12/37] Additional safety for missing iOS test output. --- test/test_ios.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_ios.py b/test/test_ios.py index 8faf1d033..49a9aa475 100644 --- a/test/test_ios.py +++ b/test/test_ios.py @@ -14,11 +14,12 @@ import time # Workaround for CPython#130294 -for i in range(0, 5): +for i in range(0, 10): time.sleep(1) print("Ensure logger is running...") -print("running tests on " + platform.machine()) +for i in range(0, 5): + print("running tests on " + platform.machine()) """ From c19bd4aca0d8479e18da3853dade7385d8a220bb Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 4 Mar 2025 07:35:44 +0800 Subject: [PATCH 13/37] Remove DYLD_LIBRARY_PATH from the iOS environment. --- cibuildwheel/ios.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index c4e1fb63d..a97c53292 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -218,6 +218,9 @@ def cross_virtualenv( "/Library/Apple/usr/bin", ] ) + # Also unset DYLD_LIBRARY_PATH to ensure that no macOS libraries will be + # found and linked. + env.pop("DYLD_LIBRARY_PATH", None) return env From cc51dcca444bd1c381132e19e0733f2d9037c181 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 5 Mar 2025 16:16:46 +0800 Subject: [PATCH 14/37] Make test-sources mandatory for iOS builds. --- cibuildwheel/ios.py | 39 +++++++++++---------------------------- cibuildwheel/util/file.py | 25 +++++-------------------- docs/options.md | 6 +++++- docs/platforms/ios.md | 2 +- test/test_ios.py | 35 ++++++++++++++++++++++++++++++----- 5 files changed, 52 insertions(+), 55 deletions(-) diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index a97c53292..70f34c6ef 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -31,7 +31,6 @@ CIBW_CACHE_PATH, copy_test_sources, download, - extract_tar, move_file, ) from .util.helpers import prepare_command @@ -513,35 +512,19 @@ def build(options: Options, tmp_path: Path) -> None: env=build_env, ) - if build_options.test_sources: - copy_test_sources( - build_options.test_sources, - build_options.package_dir, - testbed_path / "iOSTestbed" / "app", - ) - else: - # There is no explicit list of test sources. Copy *all* - # the test sources; however use the sdist to do this so - # that we avoid copying any .git or venv folders. + if not build_options.test_sources: + # iOS requires an explicit test-sources, as the project directory + # isn't visible on the simulator. - # Build a sdist of the project - call( - "python", - "-m", - "build", - build_options.package_dir, - "--sdist", - f"--outdir={identifier_tmp_dir}", - capture_stdout=True, - ) - src_tarball = next(identifier_tmp_dir.glob("*.tar.gz")) + msg = "Testing on iOS requires a definition of test-sources." + raise ValueError(msg) - # Unpack the source tarball into the stub testbed - extract_tar( - src_tarball, - testbed_path / "iOSTestbed" / "app", - strip=1, - ) + # Copy the test sources to the testbed app + copy_test_sources( + build_options.test_sources, + build_options.package_dir, + testbed_path / "iOSTestbed" / "app", + ) log.step("Installing test requirements...") # Install the compiled wheel (with any test extras), plus diff --git a/cibuildwheel/util/file.py b/cibuildwheel/util/file.py index 20e5938f6..e38da01eb 100644 --- a/cibuildwheel/util/file.py +++ b/cibuildwheel/util/file.py @@ -60,28 +60,13 @@ def extract_zip(zip_src: Path, dest: Path) -> None: dest.joinpath(zinfo.filename).chmod(permissions) -def strip_filter(size: int = 1) -> Callable[[tarfile.TarInfo, str], tarfile.TarInfo | None]: - """Create a tarfile filter that implements the equivalent of --strip/-C""" +def extract_tar(tar_src: Path, dest: Path) -> None: + """Extracts a tar file using the stdlib 'tar' filter. - def _filter(member: tarfile.TarInfo, _: str) -> tarfile.TarInfo | None: - parts = member.path.split("/", size) - try: - member.path = parts[size] - return member - except IndexError: - return None - - return _filter - - -def extract_tar(tar_src: Path, dest: Path, strip: int = 0) -> None: + See: https://docs.python.org/3/library/tarfile.html#tarfile.tar_filter for filter details + """ with tarfile.open(tar_src) as tar_: - if strip: - extraction_filter = strip_filter(strip) - else: - extraction_filter = getattr(tarfile, "tar_filter", (lambda member, _: member)) - - tar_.extraction_filter = extraction_filter + tar_.extraction_filter = getattr(tarfile, "tar_filter", (lambda member, _: member)) tar_.extractall(dest) diff --git a/docs/options.md b/docs/options.md index b4fb605cb..e5976ea85 100644 --- a/docs/options.md +++ b/docs/options.md @@ -1574,8 +1574,12 @@ project, required for running the tests. If specified, these files and folders will be copied into a temporary folder, and that temporary folder will be used as the working directory for running the test suite. +The use of `CIBW_TEST_SOURCES` is *required* for iOS builds. This is because the +simulator does not have access to the project directory, as it is not stored on +the simulator device. + Platform-specific environment variables are also available:
-`CIBW_TEST_SOURCES_MACOS` | `CIBW_TEST_SOURCES_WINDOWS` | `CIBW_TEST_SOURCES_LINUX` | `CIBW_TEST_SOURCES_PYODIDE` +`CIBW_TEST_SOURCES_MACOS` | `CIBW_TEST_SOURCES_WINDOWS` | `CIBW_TEST_SOURCES_LINUX` | `CIBW_TEST_SOURCES_IOS` | `CIBW_TEST_SOURCES_PYODIDE` #### Examples diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md index 01e377eb5..85bdbaff7 100644 --- a/docs/platforms/ios.md +++ b/docs/platforms/ios.md @@ -63,6 +63,6 @@ The environment used to run builds does not inherit the full user environment - If tests have been configured, the test suite will be executed on the simulator matching the architecture of the build machine - that is, if you're building on an ARM64 macOS machine, the ARM64 wheel will be tested on an ARM64 simulator. It is not possible to use cibuildwheel to test wheels on other simulators, or on physical devices. -The iOS test environment can't support running shell scripts, so the `CIBW_TEST_COMMAND` value must be specified as if it were a command line being passed to `python -m ...`. In addition, the test itself must be run "on device", so the local project directory cannot be used to run tests. Instead, the entire project sources must be copied onto the test device; or the project must use `CIBW_TEST_SOURCES` to specify the minimum subset of files that should be copied to the test environment. +The iOS test environment can't support running shell scripts, so the `CIBW_TEST_COMMAND` value must be specified as if it were a command line being passed to `python -m ...`. In addition, the project must use `CIBW_TEST_SOURCES` to specify the minimum subset of files that should be copied to the test environment. This is because the test must be run "on device", and the simulator device will not have access to the local project directory. The test process uses the same testbed used by CPython itself to run the CPython test suite. It is an Xcode project that has been configured to have a single Xcode "XCUnit" test - the result of which reports the success or failure of running `python -m `. diff --git a/test/test_ios.py b/test/test_ios.py index 49a9aa475..ea2d95216 100644 --- a/test/test_ios.py +++ b/test/test_ios.py @@ -2,6 +2,7 @@ import os import platform +import subprocess import pytest @@ -47,12 +48,12 @@ def test_ios_platforms(tmp_path, capfd, build_config): actual_wheels = utils.cibuildwheel_run( project_dir, - add_env=dict( - CIBW_BUILD="cp313-*", - CIBW_TEST_SOURCES="tests", - CIBW_TEST_COMMAND="tests", + add_env={ + "CIBW_BUILD": "cp313-*", + "CIBW_TEST_SOURCES": "tests", + "CIBW_TEST_COMMAND": "tests", **build_config, - ), + }, ) captured = capfd.readouterr() @@ -74,3 +75,27 @@ def test_ios_platforms(tmp_path, capfd, build_config): } assert set(actual_wheels) == expected_wheels + + +@pytest.mark.xdist_group(name="ios") +def test_no_test_sources(tmp_path, capfd): + if utils.platform != "macos": + pytest.skip("this test can only run on macOS") + if utils.get_xcode_version() < (13, 0): + pytest.skip("this test only works with Xcode 13.0 or greater") + + project_dir = tmp_path / "project" + basic_project.generate(project_dir) + + with pytest.raises(subprocess.CalledProcessError): + utils.cibuildwheel_run( + project_dir, + add_env={ + "CIBW_PLATFORM": "ios", + "CIBW_BUILD": "cp313-*", + "CIBW_TEST_COMMAND": "tests", + }, + ) + + captured = capfd.readouterr() + assert "Testing on iOS requires a definition of test-sources." in captured.err From be267bdc796e612d38f57870603f0df8b3786e84 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 5 Mar 2025 17:22:08 +0800 Subject: [PATCH 15/37] Updates and clarifications to documentation. --- cibuildwheel/resources/defaults.toml | 2 ++ docs/options.md | 21 ++++----------------- docs/platforms/ios.md | 14 +++++++------- docs/platforms/macos.md | 16 +++++++++++++++- examples/github-deploy.yml | 6 +++--- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/cibuildwheel/resources/defaults.toml b/cibuildwheel/resources/defaults.toml index b8e86c731..4e6e7d210 100644 --- a/cibuildwheel/resources/defaults.toml +++ b/cibuildwheel/resources/defaults.toml @@ -52,3 +52,5 @@ repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest [tool.cibuildwheel.windows] [tool.cibuildwheel.pyodide] + +[tool.cibuildwheel.ios] diff --git a/docs/options.md b/docs/options.md index e5976ea85..8573871d5 100644 --- a/docs/options.md +++ b/docs/options.md @@ -305,18 +305,6 @@ When setting the options, you can use shell-style globbing syntax, as per [fnmat The list of supported and currently selected build identifiers can also be retrieved by passing the `--print-build-identifiers` flag to cibuildwheel. The format is `python_tag-platform_tag`, with tags similar to those in [PEP 425](https://www.python.org/dev/peps/pep-0425/#details). -The lowest value you can set `MACOSX_DEPLOYMENT_TARGET` is as follows: - -| Arch | Python version range | Minimum target | -|-------|----------------------|----------------| -| Intel | CPython 3.8-3.11 | 10.9 | -| Intel | CPython 3.12+ | 10.13 | -| AS | CPython or PyPy | 11 | -| Intel | PyPy 3.8 | 10.13 | -| Intel | PyPy 3.9+ | 10.15 | - -If you set the value lower, cibuildwheel will cap it to the lowest supported value for each target as needed. - Windows arm64 platform support is experimental. For an experimental WebAssembly build with `--platform pyodide`, @@ -1448,11 +1436,9 @@ Alternatively, you can use the [`CIBW_TEST_SOURCES`](#test-sources) setting to create a temporary folder populated with a specific subset of project files to run your test suite. -On all platforms other than iOS, the command is run in a shell, so you can write -things like `cmd1 && cmd2`. +On all platforms other than iOS, the command is run in a shell, so you can write things like `cmd1 && cmd2`. -On iOS, the value of the `CIBW_TEST_COMMAND` setting is interpreted as the arguments -to pass to `python -m`. Shell commands cannot be used. +On iOS, the value of the `CIBW_TEST_COMMAND` setting is interpreted as the arguments to pass to `python -m` - that is, a Python module name, followed by arguments that will be assigned to `sys.argv`. Shell commands cannot be used. Platform-specific environment variables are also available:
`CIBW_TEST_COMMAND_MACOS` | `CIBW_TEST_COMMAND_WINDOWS` | `CIBW_TEST_COMMAND_LINUX` | `CIBW_TEST_COMMAND_IOS` | `CIBW_TEST_COMMAND_PYODIDE` @@ -1576,7 +1562,8 @@ as the working directory for running the test suite. The use of `CIBW_TEST_SOURCES` is *required* for iOS builds. This is because the simulator does not have access to the project directory, as it is not stored on -the simulator device. +the simulator device. On iOS, the files will be copied into the test application, +rather than a temporary folder. Platform-specific environment variables are also available:
`CIBW_TEST_SOURCES_MACOS` | `CIBW_TEST_SOURCES_WINDOWS` | `CIBW_TEST_SOURCES_LINUX` | `CIBW_TEST_SOURCES_IOS` | `CIBW_TEST_SOURCES_PYODIDE` diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md index 85bdbaff7..a322a3008 100644 --- a/docs/platforms/ios.md +++ b/docs/platforms/ios.md @@ -8,7 +8,7 @@ title: 'iOS' You must be building on a macOS machine, with Xcode installed. The Xcode installation must have an iOS SDK available, with all license agreements agreed to by the user. To check if an iOS SDK is available, open the Xcode settings panel, and check the Platforms tab. This will also ensure that license agreements have been acknowledged. -Building iOS wheel also requires a working macOS Python configuration. See the notes on [macOS builds](./macos.md) for details about configuration of the macOS environment. +Building iOS wheels also requires a working macOS Python installation. See the notes on [macOS builds](./macos.md) for details about configuration of the macOS environment. ## Specifying an iOS build @@ -20,7 +20,7 @@ iOS is effectively 2 platforms - physical devices, and simulators. While the API By default, cibuildwheel will build wheels for all three of these targets. -If you need to specify different compilation flags or other properties on a per-ABI or per-CPU basis, you can use [configuration overrides](..#overrides) with a `select` clause that targets the specific ABI or architecture. For example, consider the following example: +If you need to specify different compilation flags or other properties on a per-ABI or per-CPU basis, you can use [configuration overrides](/options/#overrides) with a `select` clause that targets the specific ABI or architecture. For example, consider the following example: ``` [tool.cibuildwheel.ios] @@ -32,7 +32,7 @@ select = "*-iphoneos" environment.PATH = "/path/to/special/device/details:..." [[tool.cibuildwheel.overrides]] -select = "*-arm64-*" +select = "*-arm64-iphone*" inherit.test-requires = "append" test-requires = ["arm64-testing-helper"] ``` @@ -41,7 +41,7 @@ This configuration would: * Specify a `test-sources` and `test-requires` for all iOS targets; * Add a `PATH` setting that will be used on physical iOS devices; and - * Add `arm64-testing-helper` to the test environment for all ARM64 devices (whether simulator or device). + * Add `arm64-testing-helper` to the test environment for all ARM64 iOS devices (whether simulator or device). ## iOS version compatibility @@ -49,11 +49,11 @@ iOS builds will honor the `IPHONEOS_DEPLOYMENT_TARGET` environment variable to s ## Cross platform builds -iOS builds are *cross platform builds*, as it not possible to run compilers and other build tools "on device". The pre-compiled iOS binaries used to support iOS builds include tooling that can convert any virtual environment into a cross platform virtual environment - that is, an environment that can run binaries on the build machine (macOS), but, if asked, will respond as if it is an iOS machine. This allows pip, build, and other build tools to perform iOS-appropriate behaviour. +iOS builds are *cross platform builds*, as it not possible to run compilers and other build tools "on device". The pre-compiled iOS binaries used to support iOS builds include tooling that can convert any virtual environment into a cross platform virtual environment - that is, an environment that can run binaries on the build machine (macOS), but, if asked, will respond as if it is an iOS machine. This allows `pip`, `build`, and other build tools to perform iOS-appropriate behaviour. ## Build frontend support -iOS builds support both the pip and build build frontends. In principle, support for uv with the `build[uv]` frontend should be possible, but uv [doesn't currently have support for cross-platform builds](https://github.com/astral-sh/uv/issues/7957), and [doesn't have support for iOS (or Android) tags](https://github.com/astral-sh/uv/issues/8029). +iOS builds support both the `pip` and `build` build frontends. In principle, support for `uv` with the `build[uv]` frontend should be possible, but `uv` [doesn't currently have support for cross-platform builds](https://github.com/astral-sh/uv/issues/7957), and [doesn't have support for iOS (or Android) tags](https://github.com/astral-sh/uv/issues/8029). ## Build environment @@ -63,6 +63,6 @@ The environment used to run builds does not inherit the full user environment - If tests have been configured, the test suite will be executed on the simulator matching the architecture of the build machine - that is, if you're building on an ARM64 macOS machine, the ARM64 wheel will be tested on an ARM64 simulator. It is not possible to use cibuildwheel to test wheels on other simulators, or on physical devices. -The iOS test environment can't support running shell scripts, so the `CIBW_TEST_COMMAND` value must be specified as if it were a command line being passed to `python -m ...`. In addition, the project must use `CIBW_TEST_SOURCES` to specify the minimum subset of files that should be copied to the test environment. This is because the test must be run "on device", and the simulator device will not have access to the local project directory. +The iOS test environment can't support running shell scripts, so the [`CIBW_TEST_COMMAND`](/options#test-command) value must be specified as if it were a command line being passed to `python -m ...`. In addition, the project must use [`CIBW_TEST_SOURCES`](/options#test-sources) to specify the minimum subset of files that should be copied to the test environment. This is because the test must be run "on device", and the simulator device will not have access to the local project directory. The test process uses the same testbed used by CPython itself to run the CPython test suite. It is an Xcode project that has been configured to have a single Xcode "XCUnit" test - the result of which reports the success or failure of running `python -m `. diff --git a/docs/platforms/macos.md b/docs/platforms/macos.md index ba38a8585..01e8d0ec8 100644 --- a/docs/platforms/macos.md +++ b/docs/platforms/macos.md @@ -17,7 +17,7 @@ You can override the cache folder using the `CIBW_CACHE_PATH` environment variab !!! warning cibuildwheel uses official python.org macOS installers for CPython but those can only be installed globally. - In order not to mess with your system, cibuildwheel won't install those if they are missing. Instead, it will error out with a message to let you install th missing CPython: + In order not to mess with your system, cibuildwheel won't install those if they are missing. Instead, it will error out with a message to let you install the missing CPython: ```console Error: CPython 3.9 is not installed. @@ -27,6 +27,20 @@ You can override the cache folder using the `CIBW_CACHE_PATH` environment variab Download link: https://www.python.org/ftp/python/3.9.8/python-3.9.8-macosx10.9.pkg ``` +## macOS Version Compatibility + +macOS builds will honor the `MACOSX_DEPLOYMENT_TARGET` environment variable to control the minimum supported macOS version for generated wheels. The lowest value you can set `MACOSX_DEPLOYMENT_TARGET` is as follows: + +| Arch | Python version range | Minimum target | +|-------|----------------------|----------------| +| Intel | CPython 3.8-3.11 | 10.9 | +| Intel | CPython 3.12+ | 10.13 | +| AS | CPython or PyPy | 11 | +| Intel | PyPy 3.8 | 10.13 | +| Intel | PyPy 3.9+ | 10.15 | + +If you set the value lower, cibuildwheel will cap it to the lowest supported value for each target as needed. + ## Universal builds By default, macOS builds will build a single architecture wheel, using the build machine's architecture. If you need to support both x86_64 and Apple Silicon, you can use the `CIBW_ARCHS` environment variable to specify the architectures you want to build, or the value `universal2` to build a multi-architecture wheel. cibuildwheel will test x86_64 wheels (or the x86_64 slice of a `universal2` wheel) when running on Apple Silicon hardware, but it is *not* possible to test Apple Silicon wheels on x86_64 hardware. diff --git a/examples/github-deploy.yml b/examples/github-deploy.yml index cb5852c79..119b365b2 100644 --- a/examples/github-deploy.yml +++ b/examples/github-deploy.yml @@ -26,14 +26,14 @@ jobs: runs-on: ubuntu-24.04-arm - os: windows runs-on: windows-latest - - os: macOS-intel + - os: macos-intel # macos-13 was the last x86_64 runner runs-on: macos-13 - - os: macOS-arm + - os: macos-arm # macos-14+ (including latest) are ARM64 runners runs-on: macos-latest archs: auto,universal2 - - os: iOS + - os: ios runs-on: macos-latest platform: ios - os: pyodide From 5420fafe853222d7792dfbaf3756a3aea78e3bc6 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 6 Mar 2025 09:06:33 +0800 Subject: [PATCH 16/37] Clarify what a slice is. --- cibuildwheel/ios.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index 70f34c6ef..c4cd5a17e 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -66,7 +66,8 @@ def is_simulator(self) -> bool: return self.identifier.endswith("-iphonesimulator") @property - def slice(self) -> str: + def xcframework_slice(self) -> str: + "XCframeworks include binaries for multiple ABIs; which ABI section should be used?" return "ios-arm64_x86_64-simulator" if self.is_simulator else "ios-arm64" @@ -264,7 +265,7 @@ def setup_python( host_python = ( host_install_path / "Python.xcframework" - / python_configuration.slice + / python_configuration.xcframework_slice / "bin" / f"python{python_configuration.version}" ) From fcf6f54c01bf1be8c6645bf89701de83944f0c0d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 6 Mar 2025 09:46:26 +0800 Subject: [PATCH 17/37] Normalize use of underscores in platform name. --- cibuildwheel/architecture.py | 6 +++--- cibuildwheel/ios.py | 7 ++++--- cibuildwheel/logger.py | 6 +++--- cibuildwheel/resources/build-platforms.toml | 6 +++--- docs/options.md | 10 ++++------ docs/platforms/ios.md | 12 ++++++------ 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py index 51d1ee70e..d856819cd 100644 --- a/cibuildwheel/architecture.py +++ b/cibuildwheel/architecture.py @@ -66,9 +66,9 @@ class Architecture(StrEnum): # iOS "multiarch" architectures that include both # the CPU architecture and the ABI. - arm64_device = "arm64-iphoneos" - arm64_simulator = "arm64-iphonesimulator" - x86_64_simulator = "x86_64-iphonesimulator" + arm64_device = "arm64_iphoneos" + arm64_simulator = "arm64_iphonesimulator" + x86_64_simulator = "x86_64_iphonesimulator" @staticmethod def parse_config(config: str, platform: PlatformName) -> "set[Architecture]": diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index c4cd5a17e..d7d9b4b29 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -59,11 +59,12 @@ def arch(self) -> str: @property def multiarch(self) -> str: - return self.identifier.split("-ios_")[1] + # The multiarch identifier, as reported by `sys.implementation._multiarch` + return "-".join(self.identifier.split("-ios_")[1].rsplit("_", 1)) @property def is_simulator(self) -> bool: - return self.identifier.endswith("-iphonesimulator") + return self.identifier.endswith("_iphonesimulator") @property def xcframework_slice(self) -> str: @@ -86,7 +87,7 @@ def build_url(item: dict[str, str]) -> str: # The iOS item will be something like cp313-ios_arm64_iphoneos. Drop # the iphoneos suffix, then replace ios with macosx to yield # cp313-macosx_arm64, which will be a macOS configuration item. - parts = item["identifier"].split("-") + parts = item["identifier"].rsplit("_", 1)[0].split("-") macos_identifier = f"{parts[0]}-{parts[1].replace('ios', 'macosx')}" matching = [ config for config in macos_python_configs if config["identifier"] == macos_identifier diff --git a/cibuildwheel/logger.py b/cibuildwheel/logger.py index 39397b20d..391786a04 100644 --- a/cibuildwheel/logger.py +++ b/cibuildwheel/logger.py @@ -35,9 +35,9 @@ "macosx_universal2": "macOS Universal 2 - x86_64 and arm64", "macosx_arm64": "macOS arm64 - Apple Silicon", "pyodide_wasm32": "Pyodide", - "ios_arm64-iphoneos": "iOS Device (ARM64)", - "ios_arm64-iphonesimulator": "iOS Simulator (ARM64)", - "ios_x86_64-iphonesimulator": "iOS Simulator (x86_64)", + "ios_arm64_iphoneos": "iOS Device (ARM64)", + "ios_arm64_iphonesimulator": "iOS Simulator (ARM64)", + "ios_x86_64_iphonesimulator": "iOS Simulator (x86_64)", } diff --git a/cibuildwheel/resources/build-platforms.toml b/cibuildwheel/resources/build-platforms.toml index 1a7f74136..788f6f722 100644 --- a/cibuildwheel/resources/build-platforms.toml +++ b/cibuildwheel/resources/build-platforms.toml @@ -166,7 +166,7 @@ python_configurations = [ [ios] python_configurations = [ - { identifier = "cp313-ios_arm64-iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, - { identifier = "cp313-ios_x86_64-iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, - { identifier = "cp313-ios_arm64-iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, + { identifier = "cp313-ios_arm64_iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, + { identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, + { identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, ] diff --git a/docs/options.md b/docs/options.md index 8573871d5..d2da7d1b4 100644 --- a/docs/options.md +++ b/docs/options.md @@ -296,7 +296,7 @@ When setting the options, you can use shell-style globbing syntax, as per [fnmat | Python 3.10 | cp310-macosx_x86_64
cp310-macosx_universal2
cp310-macosx_arm64 | cp310-win_amd64
cp310-win32
cp310-win_arm64 | cp310-manylinux_x86_64
cp310-manylinux_i686
cp310-musllinux_x86_64
cp310-musllinux_i686 | cp310-manylinux_aarch64
cp310-manylinux_ppc64le
cp310-manylinux_s390x
cp310-manylinux_armv7l
cp310-musllinux_aarch64
cp310-musllinux_ppc64le
cp310-musllinux_s390x
cp310-musllinux_armv7l | | | Python 3.11 | cp311-macosx_x86_64
cp311-macosx_universal2
cp311-macosx_arm64 | cp311-win_amd64
cp311-win32
cp311-win_arm64 | cp311-manylinux_x86_64
cp311-manylinux_i686
cp311-musllinux_x86_64
cp311-musllinux_i686 | cp311-manylinux_aarch64
cp311-manylinux_ppc64le
cp311-manylinux_s390x
cp311-manylinux_armv7l
cp311-musllinux_aarch64
cp311-musllinux_ppc64le
cp311-musllinux_s390x
cp311-musllinux_armv7l | | | Python 3.12 | cp312-macosx_x86_64
cp312-macosx_universal2
cp312-macosx_arm64 | cp312-win_amd64
cp312-win32
cp312-win_arm64 | cp312-manylinux_x86_64
cp312-manylinux_i686
cp312-musllinux_x86_64
cp312-musllinux_i686 | cp312-manylinux_aarch64
cp312-manylinux_ppc64le
cp312-manylinux_s390x
cp312-musllinux_armv7l
cp312-musllinux_ppc64le
cp312-musllinux_s390x
cp312-musllinux_armv7l | | -| Python 3.13 | cp313-macosx_x86_64
cp313-macosx_universal2
cp313-macosx_arm64 | cp313-win_amd64
cp313-win32
cp313-win_arm64 | cp313-manylinux_x86_64
cp313-manylinux_i686
cp313-musllinux_x86_64
cp313-musllinux_i686 | cp313-manylinux_aarch64
cp313-manylinux_ppc64le
cp313-manylinux_s390x
cp313-manylinux_armv7l
cp313-musllinux_aarch64
cp313-musllinux_ppc64le
cp313-musllinux_s390x
cp313-musllinux_armv7l | cp313-ios_arm64-iphoneos
cp313-ios_arm64-iphonesimulator
cp313-ios_x86_64-iphonesimulator | +| Python 3.13 | cp313-macosx_x86_64
cp313-macosx_universal2
cp313-macosx_arm64 | cp313-win_amd64
cp313-win32
cp313-win_arm64 | cp313-manylinux_x86_64
cp313-manylinux_i686
cp313-musllinux_x86_64
cp313-musllinux_i686 | cp313-manylinux_aarch64
cp313-manylinux_ppc64le
cp313-manylinux_s390x
cp313-manylinux_armv7l
cp313-musllinux_aarch64
cp313-musllinux_ppc64le
cp313-musllinux_s390x
cp313-musllinux_armv7l | cp313-ios_arm64_iphoneos
cp313-ios_arm64_iphonesimulator
cp313-ios_x86_64_iphonesimulator | | PyPy3.8 v7.3 | pp38-macosx_x86_64
pp38-macosx_arm64 | pp38-win_amd64 | pp38-manylinux_x86_64
pp38-manylinux_i686 | pp38-manylinux_aarch64 | | | PyPy3.9 v7.3 | pp39-macosx_x86_64
pp39-macosx_arm64 | pp39-win_amd64 | pp39-manylinux_x86_64
pp39-manylinux_i686 | pp39-manylinux_aarch64 | | | PyPy3.10 v7.3 | pp310-macosx_x86_64
pp310-macosx_arm64 | pp310-win_amd64 | pp310-manylinux_x86_64
pp310-manylinux_i686 | pp310-manylinux_aarch64 | | @@ -424,7 +424,7 @@ Options: - macOS: `x86_64` `arm64` `universal2` - Windows: `AMD64` `x86` `ARM64` - Pyodide: `wasm32` -- iOS: `arm64-iphoneos` `arm64-iphonesimulator` `x86_64-iphonesimulator` +- iOS: `arm64_iphoneos` `arm64_iphonesimulator` `x86_64_iphonesimulator` - `auto`: The default archs for your machine - see the table below. - `auto64`: Just the 64-bit auto archs - `auto32`: Just the 32-bit auto archs @@ -442,10 +442,8 @@ Default: `auto` | Windows / ARM64 | `ARM64` | `ARM64` | `ARM64` | | | macOS / Intel | `x86_64` | `x86_64` | `x86_64` | | | macOS / Apple Silicon | `arm64` | `arm64` | `arm64` | | -| iOS on macOS / Intel | `x86_64-iphonesimulator` | `arm64-iphoneos` `arm64-iphonesimulator` `x86_64-iphonesimulator` | `arm64-iphoneos` `arm64-iphonesimulator` `x86_64-iphonesimulator` | | -| iOS on macOS / Apple Silicon | `arm64-iphonesimulator` | `arm64-iphoneos` `arm64-iphonesimulator` `x86_64-iphonesimulator` | `arm64-iphoneos` `arm64-iphonesimulator` `x86_64-iphonesimulator` | | - -If not listed above, `auto` is the same as `native`. +| iOS on macOS / Intel | `x86_64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` `x86_64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` `x86_64_iphonesimulator` | | +| iOS on macOS / Apple Silicon | `arm64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` `x86_64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` `x86_64_iphonesimulator` | | [setup-qemu-action]: https://github.com/docker/setup-qemu-action [binfmt]: https://hub.docker.com/r/tonistiigi/binfmt diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md index a322a3008..6e8f52ff8 100644 --- a/docs/platforms/ios.md +++ b/docs/platforms/ios.md @@ -12,11 +12,11 @@ Building iOS wheels also requires a working macOS Python installation. See the n ## Specifying an iOS build -iOS is effectively 2 platforms - physical devices, and simulators. While the API for these two platforms are identical, the ABI is not compatible, even when dealing with a device and simulator with the same CPU architecture. For this reason, the architecture specification for iOS builds includes *both* the CPU architecture *and* the ABI that is being targeted. There are three possible values for architecture on iOS; the values match those used by `sys.implementation._multiarch` when running on iOS: +iOS is effectively 2 platforms - physical devices, and simulators. While the API for these two platforms are identical, the ABI is not compatible, even when dealing with a device and simulator with the same CPU architecture. For this reason, the architecture specification for iOS builds includes *both* the CPU architecture *and* the ABI that is being targeted. There are three possible values for architecture on iOS; the values match those used by `sys.implementation._multiarch` when running on iOS (with hyphens replaced with underscores, matching wheel filename normalization): -* `arm64-iphoneos` (for physical iOS devices); -* `arm64-iphonesimulator` (for iOS simulators running on Apple Silicon macOS machines); and -* `x64_64-iphonesimulator` (for iOS simulators running on Intel macOS machines). +* `arm64_iphoneos` (for physical iOS devices); +* `arm64_iphonesimulator` (for iOS simulators running on Apple Silicon macOS machines); and +* `x64_64_iphonesimulator` (for iOS simulators running on Intel macOS machines). By default, cibuildwheel will build wheels for all three of these targets. @@ -28,11 +28,11 @@ test-sources = ["tests"] test-requires = ["pytest"] [[tool.cibuildwheel.overrides]] -select = "*-iphoneos" +select = "*_iphoneos" environment.PATH = "/path/to/special/device/details:..." [[tool.cibuildwheel.overrides]] -select = "*-arm64-iphone*" +select = "*-ios_arm64_*" inherit.test-requires = "append" test-requires = ["arm64-testing-helper"] ``` From c4c88a7b97b1b323b7583446204a34cd4b7feeaa Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 6 Mar 2025 09:46:53 +0800 Subject: [PATCH 18/37] Modify auto target to be matching CPU only. --- cibuildwheel/architecture.py | 16 ++++++++++------ docs/options.md | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py index d856819cd..7425730e7 100644 --- a/cibuildwheel/architecture.py +++ b/cibuildwheel/architecture.py @@ -152,12 +152,16 @@ def auto_archs(platform: PlatformName) -> "set[Architecture]": result.add(Architecture.x86) elif platform == "ios": - # iOS defaults to building all architectures - result = { - Architecture.x86_64_simulator, - Architecture.arm64_simulator, - Architecture.arm64_device, - } + # iOS defaults to building all targets with the same CPU architecture + if native_arch == Architecture.x86_64: + result = { + Architecture.x86_64_simulator, + } + else: + result = { + Architecture.arm64_simulator, + Architecture.arm64_device, + } return result diff --git a/docs/options.md b/docs/options.md index d2da7d1b4..a88fc9971 100644 --- a/docs/options.md +++ b/docs/options.md @@ -442,8 +442,8 @@ Default: `auto` | Windows / ARM64 | `ARM64` | `ARM64` | `ARM64` | | | macOS / Intel | `x86_64` | `x86_64` | `x86_64` | | | macOS / Apple Silicon | `arm64` | `arm64` | `arm64` | | -| iOS on macOS / Intel | `x86_64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` `x86_64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` `x86_64_iphonesimulator` | | -| iOS on macOS / Apple Silicon | `arm64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` `x86_64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` `x86_64_iphonesimulator` | | +| iOS on macOS / Intel | `x86_64_iphonesimulator` | `x86_64_iphonesimulator` | `x86_64_iphonesimulator` | | +| iOS on macOS / Apple Silicon | `arm64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` | | [setup-qemu-action]: https://github.com/docker/setup-qemu-action [binfmt]: https://hub.docker.com/r/tonistiigi/binfmt From b542fb516bd32eec694d794e1b68a8321fdca3b9 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 6 Mar 2025 09:55:01 +0800 Subject: [PATCH 19/37] Use consistent ordering of platforms in examples. --- cibuildwheel/resources/defaults.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cibuildwheel/resources/defaults.toml b/cibuildwheel/resources/defaults.toml index 4e6e7d210..6b65e6369 100644 --- a/cibuildwheel/resources/defaults.toml +++ b/cibuildwheel/resources/defaults.toml @@ -51,6 +51,6 @@ repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest [tool.cibuildwheel.windows] -[tool.cibuildwheel.pyodide] - [tool.cibuildwheel.ios] + +[tool.cibuildwheel.pyodide] From 361b31a0fa03805bb344065bba4b8f0ea257e7d4 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 6 Mar 2025 09:55:35 +0800 Subject: [PATCH 20/37] Use consistent naming in iOS archiectures. --- cibuildwheel/architecture.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py index 7425730e7..f11df08fe 100644 --- a/cibuildwheel/architecture.py +++ b/cibuildwheel/architecture.py @@ -66,9 +66,9 @@ class Architecture(StrEnum): # iOS "multiarch" architectures that include both # the CPU architecture and the ABI. - arm64_device = "arm64_iphoneos" - arm64_simulator = "arm64_iphonesimulator" - x86_64_simulator = "x86_64_iphonesimulator" + arm64_iphoneos = auto() + arm64_iphonesimulator = auto() + x86_64_iphonesimulator = auto() @staticmethod def parse_config(config: str, platform: PlatformName) -> "set[Architecture]": @@ -113,9 +113,9 @@ def native_arch(platform: PlatformName) -> "Architecture | None": # simulator for the macOS native platform. if host_platform == "macos": if native_architecture == Architecture.x86_64: - return Architecture.x86_64_simulator + return Architecture.x86_64_iphonesimulator else: - return Architecture.arm64_simulator + return Architecture.arm64_iphonesimulator else: return None @@ -155,12 +155,12 @@ def auto_archs(platform: PlatformName) -> "set[Architecture]": # iOS defaults to building all targets with the same CPU architecture if native_arch == Architecture.x86_64: result = { - Architecture.x86_64_simulator, + Architecture.x86_64_iphonesimulator, } else: result = { - Architecture.arm64_simulator, - Architecture.arm64_device, + Architecture.arm64_iphonesimulator, + Architecture.arm64_iphoneos, } return result @@ -180,9 +180,9 @@ def all_archs(platform: PlatformName) -> "set[Architecture]": "windows": {Architecture.x86, Architecture.AMD64, Architecture.ARM64}, "pyodide": {Architecture.wasm32}, "ios": { - Architecture.x86_64_simulator, - Architecture.arm64_simulator, - Architecture.arm64_device, + Architecture.x86_64_iphonesimulator, + Architecture.arm64_iphonesimulator, + Architecture.arm64_iphoneos, }, } return all_archs_map[platform] From a99552636f6bf958240266c380de48846945c9bf Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 6 Mar 2025 09:56:01 +0800 Subject: [PATCH 21/37] Placate the linter. --- cibuildwheel/ios.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index d7d9b4b29..0a715961c 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -140,6 +140,7 @@ def install_host_cpython(tmp: Path, config: PythonConfiguration, free_threading: def cross_virtualenv( + *, py_version: str, host_python: Path, multiarch: str, @@ -150,9 +151,9 @@ def cross_virtualenv( """Create a cross-compilation virtual environment. In a cross-compilation environment, the *host* is the platform you're - targeting the *build* is the platform where you're running the compilation. - When building iOS wheels, iOS is the host machine and macOS is the build - machine. + targeting, and the *build* is the platform where you're running the + compilation. When building iOS wheels, iOS is the host machine and macOS is + the build machine. A cross-compilation virtualenv is an environment that is based on the *build* python (so that binaries can execute); but it modifies the From dca2338c585fa7a5e63bd9a38a4e4045191a9ecc Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 6 Mar 2025 10:06:23 +0800 Subject: [PATCH 22/37] Miscellaneous cleanups picked up by @joerick's review. --- cibuildwheel/ios.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index 0a715961c..beadf6315 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -83,12 +83,12 @@ def get_python_configurations( # configuration. macos_python_configs = resources.read_python_configs("macos") - def build_url(item: dict[str, str]) -> str: - # The iOS item will be something like cp313-ios_arm64_iphoneos. Drop - # the iphoneos suffix, then replace ios with macosx to yield - # cp313-macosx_arm64, which will be a macOS configuration item. - parts = item["identifier"].rsplit("_", 1)[0].split("-") - macos_identifier = f"{parts[0]}-{parts[1].replace('ios', 'macosx')}" + def build_url(config_dict: dict[str, str]) -> str: + # The iOS identifier will be something like cp313-ios_arm64_iphoneos. + # Drop the iphoneos suffix, then replace ios with macosx to yield + # cp313-macosx_arm64, which will be a macOS build identifier. + modified_ios_identifier = config_dict["identifier"].rsplit("_", 1)[0] + macos_identifier = modified_ios_identifier.replace("ios", "macosx") matching = [ config for config in macos_python_configs if config["identifier"] == macos_identifier ] @@ -338,8 +338,7 @@ def setup_python( call("pip", "--version", env=env) # Ensure that IPHONEOS_DEPLOYMENT_TARGET is set in the environment - ios_deployment_target = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0") - env["IPHONEOS_DEPLOYMENT_TARGET"] = ios_deployment_target + env.setdefault("IPHONEOS_DEPLOYMENT_TARGET", "13.0") log.step("Installing build tools...") if build_frontend == "pip": From 8f2cb91ebc4864dbee0329d72da891f43404a3bc Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 6 Mar 2025 10:20:07 +0800 Subject: [PATCH 23/37] Correct the list of expected wheels. --- test/test_ios.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/test_ios.py b/test/test_ios.py index ea2d95216..ef229cafb 100644 --- a/test/test_ios.py +++ b/test/test_ios.py @@ -58,21 +58,24 @@ def test_ios_platforms(tmp_path, capfd, build_config): captured = capfd.readouterr() + ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_") platform_machine = platform.machine() # Tests are only executed on device if platform_machine == "x86_64": # Ensure that tests were run on arm64. assert "running tests on x86_64" in captured.out + expected_wheels = { + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl", + } + elif platform_machine == "arm64": # Ensure that tests were run on arm64. assert "running tests on arm64" in captured.out - ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_") - expected_wheels = { - f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl", - f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl", - f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl", - } + expected_wheels = { + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl", + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl", + } assert set(actual_wheels) == expected_wheels From 26f217ce00bcd63bf513ba6af203da19edaec15b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 6 Mar 2025 10:53:32 +0800 Subject: [PATCH 24/37] Correct which 'native' we're actually checking. --- cibuildwheel/architecture.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py index f11df08fe..00dff7cee 100644 --- a/cibuildwheel/architecture.py +++ b/cibuildwheel/architecture.py @@ -153,7 +153,8 @@ def auto_archs(platform: PlatformName) -> "set[Architecture]": elif platform == "ios": # iOS defaults to building all targets with the same CPU architecture - if native_arch == Architecture.x86_64: + # as the native simulator architecture + if native_arch == Architecture.x86_64_iphonesimulator: result = { Architecture.x86_64_iphonesimulator, } From 039c3002f076b0dc0e2cfafbe95df3fc3d998a66 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 7 Mar 2025 12:42:04 +0800 Subject: [PATCH 25/37] Correct the docs links so they're all relative. --- docs/platforms/ios.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md index 6e8f52ff8..a58c0132b 100644 --- a/docs/platforms/ios.md +++ b/docs/platforms/ios.md @@ -20,7 +20,7 @@ iOS is effectively 2 platforms - physical devices, and simulators. While the API By default, cibuildwheel will build wheels for all three of these targets. -If you need to specify different compilation flags or other properties on a per-ABI or per-CPU basis, you can use [configuration overrides](/options/#overrides) with a `select` clause that targets the specific ABI or architecture. For example, consider the following example: +If you need to specify different compilation flags or other properties on a per-ABI or per-CPU basis, you can use [configuration overrides](../../options/#overrides) with a `select` clause that targets the specific ABI or architecture. For example, consider the following example: ``` [tool.cibuildwheel.ios] @@ -63,6 +63,6 @@ The environment used to run builds does not inherit the full user environment - If tests have been configured, the test suite will be executed on the simulator matching the architecture of the build machine - that is, if you're building on an ARM64 macOS machine, the ARM64 wheel will be tested on an ARM64 simulator. It is not possible to use cibuildwheel to test wheels on other simulators, or on physical devices. -The iOS test environment can't support running shell scripts, so the [`CIBW_TEST_COMMAND`](/options#test-command) value must be specified as if it were a command line being passed to `python -m ...`. In addition, the project must use [`CIBW_TEST_SOURCES`](/options#test-sources) to specify the minimum subset of files that should be copied to the test environment. This is because the test must be run "on device", and the simulator device will not have access to the local project directory. +The iOS test environment can't support running shell scripts, so the [`CIBW_TEST_COMMAND`](../../options#test-command) value must be specified as if it were a command line being passed to `python -m ...`. In addition, the project must use [`CIBW_TEST_SOURCES`](../../options#test-sources) to specify the minimum subset of files that should be copied to the test environment. This is because the test must be run "on device", and the simulator device will not have access to the local project directory. The test process uses the same testbed used by CPython itself to run the CPython test suite. It is an Xcode project that has been configured to have a single Xcode "XCUnit" test - the result of which reports the success or failure of running `python -m `. From 5208fc47ab51c07fe78241469eac7a0a91c45501 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 7 Mar 2025 12:45:10 +0800 Subject: [PATCH 26/37] Correct the identification of free threaded builds. --- cibuildwheel/ios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index beadf6315..f04c930cc 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -246,7 +246,7 @@ def setup_python( implementation_id = python_configuration.identifier.split("-")[0] log.step(f"Installing Build Python {implementation_id}...") if implementation_id.startswith("cp"): - free_threading = "t-iphone" in python_configuration.identifier + free_threading = "t-ios" in python_configuration.identifier build_python = install_build_cpython( tmp, python_configuration.version, From eedac55c536a1f5a41099061753b1907e58c560a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 8 Mar 2025 11:06:51 +0800 Subject: [PATCH 27/37] Use target instead of host to describe the platform we're building for. --- cibuildwheel/ios.py | 58 +++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index f04c930cc..203fbe25a 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -118,7 +118,7 @@ def build_url(config_dict: dict[str, str]) -> str: return python_configurations -def install_host_cpython(tmp: Path, config: PythonConfiguration, free_threading: bool) -> Path: +def install_target_cpython(tmp: Path, config: PythonConfiguration, free_threading: bool) -> Path: if free_threading: msg = "Free threading builds aren't available for iOS (yet)" raise ValueError(msg) @@ -142,7 +142,7 @@ def install_host_cpython(tmp: Path, config: PythonConfiguration, free_threading: def cross_virtualenv( *, py_version: str, - host_python: Path, + target_python: Path, multiarch: str, build_python: Path, venv_path: Path, @@ -150,22 +150,24 @@ def cross_virtualenv( ) -> dict[str, str]: """Create a cross-compilation virtual environment. - In a cross-compilation environment, the *host* is the platform you're - targeting, and the *build* is the platform where you're running the - compilation. When building iOS wheels, iOS is the host machine and macOS is - the build machine. + In a cross-compilation environment, the *target* is the platform where the + code will ultimately run, and the *build* is the platform where you're + running the compilation. When building iOS wheels, iOS is the target machine + and macOS is the build machine. The terminology around these machines varies + between build tools (configure uses "host" and "build"; cmake uses "target" and + "build host"). A cross-compilation virtualenv is an environment that is based on the *build* python (so that binaries can execute); but it modifies the environment at startup so that any request for platform details (such as - `sys.platform` or `sysconfig.get_platform()`) return details of the host + `sys.platform` or `sysconfig.get_platform()`) return details of the target platform. It also applies a loader patch so that any virtualenv created by the cross-compilation environment will also be a cross-compilation environment. :param py_version: The Python version (major.minor) in use - :param host_python: The path to the python binary for the host platform - :param multiarch: The multiarch tag for the host platform (i.e., the value + :param target_python: The path to the python binary for the targret platform + :param multiarch: The multiarch tag for the target platform (i.e., the value of `sys.implementation._multiarch`) :param build_python: The path to the python binary for the build platform :param venv_path: The path where the cross virtual environment should be @@ -185,9 +187,9 @@ def cross_virtualenv( # Convert the macOS virtual environment into an iOS virtual environment # using the cross-platform conversion script in the iOS distribution. - # host_python is the path to the Python binary; + # target_python is the path to the Python binary; # determine the root of the XCframework slice that is being used. - slice_path = host_python.parent.parent + slice_path = target_python.parent.parent call( "python", str(slice_path / f"platform-config/{multiarch}/make_cross_venv.py"), @@ -197,17 +199,17 @@ def cross_virtualenv( ) # When running on macOS, it's easy for the build environment to leak into - # the host environment, especially when building for ARM64 (because the - # architecture is the same as the host architecture). The primary culprit - # for this is Homebrew libraries leaking in as dependencies for iOS + # the target environment, especially when building for ARM64 (because the + # build architecture is the same as the target architecture). The primary + # culprit for this is Homebrew libraries leaking in as dependencies for iOS # libraries. # # To prevent problems, set the PATH to isolate the build environment from # sources that could introduce incompatible binaries. env["PATH"] = os.pathsep.join( [ - # The host python's binary directory - str(host_python.parent), + # The target python's binary directory + str(target_python.parent), # The cross-platform environments binary directory str(venv_path / "bin"), # Cargo's binary directory (to allow for Rust compilation) @@ -239,8 +241,8 @@ def setup_python( raise ValueError(msg) # An iOS environment requires 2 python installs - one for the build machine - # (macOS), and one for the host (iOS). We'll only ever interact with the - # *host* python, but the build Python needs to exist to act as the base + # (macOS), and one for the target (iOS). We'll only ever interact with the + # *target* python, but the build Python needs to exist to act as the base # for a cross venv. tmp.mkdir() implementation_id = python_configuration.identifier.split("-")[0] @@ -261,11 +263,11 @@ def setup_python( f"{build_python.name} not found, has {list(build_python.parent.iterdir())}" ) - log.step(f"Installing Host Python {implementation_id}...") + log.step(f"Installing Target Python {implementation_id}...") if implementation_id.startswith("cp"): - host_install_path = install_host_cpython(tmp, python_configuration, free_threading) - host_python = ( - host_install_path + target_install_path = install_target_cpython(tmp, python_configuration, free_threading) + target_python = ( + target_install_path / "Python.xcframework" / python_configuration.xcframework_slice / "bin" @@ -275,8 +277,8 @@ def setup_python( msg = "Unknown Python implementation" raise ValueError(msg) - assert host_python.exists(), ( - f"{host_python.name} not found, has {list(host_install_path.iterdir())}" + assert target_python.exists(), ( + f"{target_python.name} not found, has {list(target_install_path.iterdir())}" ) log.step("Creating cross build environment...") @@ -284,7 +286,7 @@ def setup_python( venv_path = tmp / "venv" env = cross_virtualenv( py_version=python_configuration.version, - host_python=host_python, + target_python=target_python, multiarch=python_configuration.multiarch, build_python=build_python, venv_path=venv_path, @@ -356,7 +358,7 @@ def setup_python( else: assert_never(build_frontend) - return host_install_path, env + return target_install_path, env def build(options: Options, tmp_path: Path) -> None: @@ -410,7 +412,7 @@ def build(options: Options, tmp_path: Path) -> None: build_options.dependency_constraints.get_for_python_version(config.version), ] - host_install_path, env = setup_python( + target_install_path, env = setup_python( identifier_tmp_dir / "build", config, dependency_constraint_flags, @@ -508,7 +510,7 @@ def build(options: Options, tmp_path: Path) -> None: testbed_path = identifier_tmp_dir / "testbed" call( "python", - host_install_path / "testbed", + target_install_path / "testbed", "clone", testbed_path, env=build_env, From b4e4a28f601c37b0a0f00adff31a50b0dc1a3a44 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 8 Mar 2025 11:07:39 +0800 Subject: [PATCH 28/37] Rework iOS test to remove issue with log completeness. --- test/test_ios.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/test/test_ios.py b/test/test_ios.py index ef229cafb..12c5bca41 100644 --- a/test/test_ios.py +++ b/test/test_ios.py @@ -9,18 +9,14 @@ from . import test_projects, utils basic_project = test_projects.new_c_project() -basic_project.files["tests/__init__.py"] = "" -basic_project.files["tests/__main__.py"] = r""" +basic_project.files["tests/test_platform.py"] = f""" import platform -import time +from unittest import TestCase -# Workaround for CPython#130294 -for i in range(0, 10): - time.sleep(1) - print("Ensure logger is running...") +class TestPlatform(TestCase): + def test_platform(self): + self.assertEqual(platform.machine(), "{platform.machine()}") -for i in range(0, 5): - print("running tests on " + platform.machine()) """ @@ -37,7 +33,7 @@ {"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "build"}, ], ) -def test_ios_platforms(tmp_path, capfd, build_config): +def test_ios_platforms(tmp_path, build_config): if utils.platform != "macos": pytest.skip("this test can only run on macOS") if utils.get_xcode_version() < (13, 0): @@ -51,27 +47,22 @@ def test_ios_platforms(tmp_path, capfd, build_config): add_env={ "CIBW_BUILD": "cp313-*", "CIBW_TEST_SOURCES": "tests", - "CIBW_TEST_COMMAND": "tests", + "CIBW_TEST_COMMAND": "unittest discover tests test_platform.py", **build_config, }, ) - captured = capfd.readouterr() - ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_") platform_machine = platform.machine() - # Tests are only executed on device + + # Tests are only executed on simulator. The test suite passes if it's + # running on the same architecture as the current platform. if platform_machine == "x86_64": - # Ensure that tests were run on arm64. - assert "running tests on x86_64" in captured.out expected_wheels = { f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl", } elif platform_machine == "arm64": - # Ensure that tests were run on arm64. - assert "running tests on arm64" in captured.out - expected_wheels = { f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl", f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl", From e47702652b0df5f2d26db701cf9613554ab2543c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 8 Mar 2025 18:15:58 +0800 Subject: [PATCH 29/37] Convert errors to FatalError Co-authored-by: Matthieu Darbois Co-authored-by: Joe Rickerby --- cibuildwheel/ios.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index 203fbe25a..cb1b7a671 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -121,7 +121,7 @@ def build_url(config_dict: dict[str, str]) -> str: def install_target_cpython(tmp: Path, config: PythonConfiguration, free_threading: bool) -> Path: if free_threading: msg = "Free threading builds aren't available for iOS (yet)" - raise ValueError(msg) + raise errors.FatalError(msg) # Install an iOS build of CPython ios_python_tar_gz = config.url.rsplit("/", 1)[-1] @@ -238,7 +238,7 @@ def setup_python( ) -> tuple[Path, dict[str, str]]: if build_frontend == "build[uv]": msg = "uv doesn't support iOS" - raise ValueError(msg) + raise errors.FatalError(msg) # An iOS environment requires 2 python installs - one for the build machine # (macOS), and one for the target (iOS). We'll only ever interact with the @@ -256,8 +256,8 @@ def setup_python( free_threading, ) else: - msg = "Unknown Python implementation" - raise ValueError(msg) + msg = f"Unknown Python implementation: {implementation_id}" + raise errors.FatalError(msg) assert build_python.exists(), ( f"{build_python.name} not found, has {list(build_python.parent.iterdir())}" @@ -364,7 +364,7 @@ def setup_python( def build(options: Options, tmp_path: Path) -> None: if sys.platform != "darwin": msg = "iOS binaries can only be built on macOS" - raise ValueError(msg) + raise errors.FatalError(msg) python_configurations = get_python_configurations( build_selector=options.globals.build_selector, @@ -397,7 +397,7 @@ def build(options: Options, tmp_path: Path) -> None: # uv doesn't support iOS if build_frontend.name == "build[uv]": msg = "uv doesn't support iOS" - raise ValueError(msg) + raise errors.FatalError(msg) log.build_start(config.identifier) @@ -521,7 +521,7 @@ def build(options: Options, tmp_path: Path) -> None: # isn't visible on the simulator. msg = "Testing on iOS requires a definition of test-sources." - raise ValueError(msg) + raise errors.FatalError(msg) # Copy the test sources to the testbed app copy_test_sources( From 2ebefabdd6523157191179693eb4e2ad227912c0 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 8 Mar 2025 18:19:15 +0800 Subject: [PATCH 30/37] Removed a repeated check for a valid python. --- cibuildwheel/ios.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index cb1b7a671..d18a370b9 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -264,18 +264,14 @@ def setup_python( ) log.step(f"Installing Target Python {implementation_id}...") - if implementation_id.startswith("cp"): - target_install_path = install_target_cpython(tmp, python_configuration, free_threading) - target_python = ( - target_install_path - / "Python.xcframework" - / python_configuration.xcframework_slice - / "bin" - / f"python{python_configuration.version}" - ) - else: - msg = "Unknown Python implementation" - raise ValueError(msg) + target_install_path = install_target_cpython(tmp, python_configuration, free_threading) + target_python = ( + target_install_path + / "Python.xcframework" + / python_configuration.xcframework_slice + / "bin" + / f"python{python_configuration.version}" + ) assert target_python.exists(), ( f"{target_python.name} not found, has {list(target_install_path.iterdir())}" From 2b08934d7a9c82a842ec0f7c43e2e884e79d99b6 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sun, 9 Mar 2025 10:37:32 +0800 Subject: [PATCH 31/37] Update bin/update_pythons.py to update iOS support packages. --- bin/update_pythons.py | 61 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/bin/update_pythons.py b/bin/update_pythons.py index 035acbd9b..7b8ea1474 100755 --- a/bin/update_pythons.py +++ b/bin/update_pythons.py @@ -44,13 +44,13 @@ class ConfigWinPP(TypedDict): url: str -class ConfigMacOS(TypedDict): +class ConfigApple(TypedDict): identifier: str version: str url: str -AnyConfig = ConfigWinCP | ConfigWinPP | ConfigMacOS +AnyConfig = ConfigWinCP | ConfigWinPP | ConfigApple # The following set of "Versions" classes allow the initial call to the APIs to @@ -154,7 +154,7 @@ def update_version_windows(self, spec: Specifier) -> ConfigWinCP: url=url, ) - def update_version_macos(self, spec: Specifier) -> ConfigMacOS: + def update_version_macos(self, spec: Specifier) -> ConfigApple: if self.arch not in {"64", "ARM64"}: msg = f"'{self.arch}' arch not supported yet on macOS" raise RuntimeError(msg) @@ -178,7 +178,7 @@ def update_version_macos(self, spec: Specifier) -> ConfigMacOS: if "" in rf["platform"] == "darwin" and rf["arch"] == arch ) - return ConfigMacOS( + return ConfigApple( identifier=identifier, version=f"{version.major}.{version.minor}", url=url, @@ -204,7 +204,7 @@ def __init__(self) -> None: def update_version_macos( self, identifier: str, version: Version, spec: Specifier - ) -> ConfigMacOS | None: + ) -> ConfigApple | None: # see note above on Specifier.filter unsorted_versions = spec.filter(self.versions_dict) sorted_versions = sorted(unsorted_versions, reverse=True) @@ -223,7 +223,7 @@ def update_version_macos( urls = [rf["url"] for rf in file_info if file_ident in rf["url"]] if urls: - return ConfigMacOS( + return ConfigApple( identifier=identifier, version=f"{new_version.major}.{new_version.minor}", url=urls[0], @@ -232,6 +232,48 @@ def update_version_macos( return None +class CPythonIOSVersions: + def __init__(self) -> None: + response = requests.get( + "https://api.github.com/repos/beeware/Python-Apple-support/releases", + headers={ + "Accept": "application/vnd.github+json", + "X-Github-Api-Version": "2022-11-28", + }, + ) + response.raise_for_status() + + releases_info = response.json() + self.versions_dict: dict[Version, dict[int, str]] = {} + + # Each release has a name like "3.13-b4" + for release in releases_info: + py_version, build = release["name"].split("-") + version = Version(py_version) + self.versions_dict.setdefault(version, {}) + + # There are several release assets associated with each release; + # The name of the asset will be something like + # "Python-3.11-iOS-support.b4.tar.gz". Store all builds that are + # "-iOS-support" builds, retaining the download URL. + for asset in release["assets"]: + filename, build, _, _ = asset["name"].rsplit(".", 3) + if filename.endswith("-iOS-support"): + self.versions_dict[version][int(build[1:])] = asset["browser_download_url"] + + def update_version_ios(self, identifier: str, version: Version) -> ConfigApple | None: + # Return a config using the highest build number for the given version. + urls = [url for _, url in sorted(self.versions_dict.get(version, {}).items())] + if urls: + return ConfigApple( + identifier=identifier, + version=str(version), + url=urls[-1], + ) + + return None + + # This is a universal interface to all the above Versions classes. Given an # identifier, it updates a config dict. @@ -250,6 +292,8 @@ def __init__(self) -> None: self.macos_pypy = PyPyVersions("64") self.macos_pypy_arm64 = PyPyVersions("ARM64") + self.ios_cpython = CPythonIOSVersions() + def update_config(self, config: MutableMapping[str, str]) -> None: identifier = config["identifier"] version = Version(config["version"]) @@ -282,6 +326,8 @@ def update_config(self, config: MutableMapping[str, str]) -> None: config_update = self.windows_t_arm64.update_version_windows(spec) elif "win_arm64" in identifier and identifier.startswith("cp"): config_update = self.windows_arm64.update_version_windows(spec) + elif "ios" in identifier: + config_update = self.ios_cpython.update_version_ios(identifier, version) assert config_update is not None, f"{identifier} not found!" config.update(**config_update) @@ -317,6 +363,9 @@ def update_pythons(force: bool, level: str) -> None: for config in configs["macos"]["python_configurations"]: all_versions.update_config(config) + for config in configs["ios"]["python_configurations"]: + all_versions.update_config(config) + result_toml = dump_python_configurations(configs) rich.print() # spacer From 3580d6e87ad6ff442043b60e63c76b2f771d0b53 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 10 Mar 2025 14:21:26 +0800 Subject: [PATCH 32/37] Document that iOS CI is available on other platforms. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3f68905e7..028b8e53d 100644 --- a/README.md +++ b/README.md @@ -58,12 +58,12 @@ Usage | | Linux | macOS | Windows | Linux ARM | macOS ARM | Windows ARM | iOS | |-----------------|-------|-------|---------|-----------|-----------|-------------|-----| | GitHub Actions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅² | ✅³ | -| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² | | +| Azure Pipelines | ✅ | ✅ | ✅ | | ✅ | ✅² | ✅³ | | Travis CI | ✅ | | ✅ | ✅ | | | | -| AppVeyor | ✅ | ✅ | ✅ | | ✅ | ✅² | | -| CircleCI | ✅ | ✅ | | ✅ | ✅ | | | -| Gitlab CI | ✅ | ✅ | ✅ | ✅¹ | ✅ | | | -| Cirrus CI | ✅ | ✅ | ✅ | ✅ | ✅ | | | +| AppVeyor | ✅ | ✅ | ✅ | | ✅ | ✅² | ✅³ | +| CircleCI | ✅ | ✅ | | ✅ | ✅ | | ✅³ | +| Gitlab CI | ✅ | ✅ | ✅ | ✅¹ | ✅ | | ✅³ | +| Cirrus CI | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅³ | ¹ [Requires emulation](https://cibuildwheel.pypa.io/en/stable/faq/#emulation), distributed separately. Other services may also support Linux ARM through emulation or third-party build hosts, but these are not tested in our CI.
² [Uses cross-compilation](https://cibuildwheel.pypa.io/en/stable/faq/#windows-arm64). It is not possible to test `arm64` on this CI platform.
From b39c6eb999a77e20b9106fd86edf993d65ac3626 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 10 Mar 2025 14:21:51 +0800 Subject: [PATCH 33/37] Restore a comment needed for some platforms. --- docs/options.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/options.md b/docs/options.md index a88fc9971..6099212ad 100644 --- a/docs/options.md +++ b/docs/options.md @@ -445,6 +445,8 @@ Default: `auto` | iOS on macOS / Intel | `x86_64_iphonesimulator` | `x86_64_iphonesimulator` | `x86_64_iphonesimulator` | | | iOS on macOS / Apple Silicon | `arm64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` | | +If not listed above, `auto` is the same as `native`. + [setup-qemu-action]: https://github.com/docker/setup-qemu-action [binfmt]: https://hub.docker.com/r/tonistiigi/binfmt From 6b9b033a8cb2526994a759fbe523ac6565056d46 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 11 Mar 2025 06:26:18 +0800 Subject: [PATCH 34/37] Small cleanups identified in code review Co-authored-by: Joe Rickerby --- cibuildwheel/architecture.py | 12 +++--------- cibuildwheel/ios.py | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py index 00dff7cee..3d98b5187 100644 --- a/cibuildwheel/architecture.py +++ b/cibuildwheel/architecture.py @@ -154,15 +154,9 @@ def auto_archs(platform: PlatformName) -> "set[Architecture]": elif platform == "ios": # iOS defaults to building all targets with the same CPU architecture # as the native simulator architecture - if native_arch == Architecture.x86_64_iphonesimulator: - result = { - Architecture.x86_64_iphonesimulator, - } - else: - result = { - Architecture.arm64_iphonesimulator, - Architecture.arm64_iphoneos, - } + # build actual iOS wheels on arm64 + if native_arch == Architecture.arm64_iphonesimulator: + result.add(Architecture.arm64_iphoneos) return result diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index d18a370b9..0bed2d810 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -166,7 +166,7 @@ def cross_virtualenv( environment. :param py_version: The Python version (major.minor) in use - :param target_python: The path to the python binary for the targret platform + :param target_python: The path to the python binary for the target platform :param multiarch: The multiarch tag for the target platform (i.e., the value of `sys.implementation._multiarch`) :param build_python: The path to the python binary for the build platform From f29439b04c075b632c2f08b4b5f17e0c563450eb Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 11 Mar 2025 08:21:43 +0800 Subject: [PATCH 35/37] Simplify logic to appease linter. --- cibuildwheel/architecture.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py index 3d98b5187..de17c75dd 100644 --- a/cibuildwheel/architecture.py +++ b/cibuildwheel/architecture.py @@ -151,12 +151,9 @@ def auto_archs(platform: PlatformName) -> "set[Architecture]": elif platform == "windows" and Architecture.AMD64 in result: result.add(Architecture.x86) - elif platform == "ios": - # iOS defaults to building all targets with the same CPU architecture - # as the native simulator architecture - # build actual iOS wheels on arm64 - if native_arch == Architecture.arm64_iphonesimulator: - result.add(Architecture.arm64_iphoneos) + elif platform == "ios" and native_arch == Architecture.arm64_iphonesimulator: + # Also build the device wheel if we're on ARM64. + result.add(Architecture.arm64_iphoneos) return result From 4a5fef51bcc3b01ee797ec12d189f1bbc69b5658 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 11 Mar 2025 12:12:28 +0800 Subject: [PATCH 36/37] Modify dependency constraint handling to use new API. --- cibuildwheel/ios.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py index 0bed2d810..e3b6c0a47 100644 --- a/cibuildwheel/ios.py +++ b/cibuildwheel/ios.py @@ -401,12 +401,12 @@ def build(options: Options, tmp_path: Path) -> None: identifier_tmp_dir.mkdir() built_wheel_dir = identifier_tmp_dir / "built_wheel" - dependency_constraint_flags: Sequence[PathOrStr] = [] - if build_options.dependency_constraints: - dependency_constraint_flags = [ - "-c", - build_options.dependency_constraints.get_for_python_version(config.version), - ] + constraints_path = build_options.dependency_constraints.get_for_python_version( + version=config.version, tmp_dir=identifier_tmp_dir + ) + dependency_constraint_flags: Sequence[PathOrStr] = ( + ["-c", constraints_path] if constraints_path else [] + ) target_install_path, env = setup_python( identifier_tmp_dir / "build", @@ -445,11 +445,8 @@ def build(options: Options, tmp_path: Path) -> None: build_env = env.copy() build_env["VIRTUALENV_PIP"] = pip_version - if build_options.dependency_constraints: - constraint_path = build_options.dependency_constraints.get_for_python_version( - config.version - ) - combine_constraints(build_env, constraint_path, None) + if constraints_path: + combine_constraints(build_env, constraints_path, None) if build_frontend.name == "pip": # Path.resolve() is needed. Without it pip wheel may try to From 6a8f410a0b4767a9737462603dd7e36ce37381d1 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 11 Mar 2025 12:46:49 +0800 Subject: [PATCH 37/37] Cosmetic change to trigger a CI rebuild. --- test/test_ios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_ios.py b/test/test_ios.py index 12c5bca41..6dc7b40d3 100644 --- a/test/test_ios.py +++ b/test/test_ios.py @@ -27,7 +27,7 @@ def test_platform(self): @pytest.mark.parametrize( "build_config", [ - # Default to the pip build frontend. + # Default to the pip build frontend {"CIBW_PLATFORM": "ios"}, # Also check the build frontend {"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "build"},