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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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()) """