diff --git a/README.md b/README.md index 0a391a733..028b8e53d 100644 --- a/README.md +++ b/README.md @@ -24,18 +24,18 @@ 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.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.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.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.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.
@@ -55,18 +55,19 @@ 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. @@ -75,6 +76,7 @@ Example setup To build manylinux, musllinux, macOS, and Windows wheels on GitHub Actions, you could use this `.github/workflows/wheels.yml`: + ```yaml name: Build @@ -102,12 +104,14 @@ jobs: # to supply options, put them in 'env', like: # env: # CIBW_SOME_OPTION: value + # ... - 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 ec4229381..b82a91136 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -322,6 +322,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/bin/run_tests.py b/bin/run_tests.py index e4ea6311d..2aed4f01a 100755 --- a/bin/run_tests.py +++ b/bin/run_tests.py @@ -41,12 +41,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/bin/update_pythons.py b/bin/update_pythons.py index 035acbd9b..7b8ea1474 100755 --- a/bin/update_pythons.py +++ b/bin/update_pythons.py @@ -44,13 +44,13 @@ class ConfigWinPP(TypedDict): url: str -class ConfigMacOS(TypedDict): +class ConfigApple(TypedDict): identifier: str version: str url: str -AnyConfig = ConfigWinCP | ConfigWinPP | ConfigMacOS +AnyConfig = ConfigWinCP | ConfigWinPP | ConfigApple # The following set of "Versions" classes allow the initial call to the APIs to @@ -154,7 +154,7 @@ def update_version_windows(self, spec: Specifier) -> ConfigWinCP: url=url, ) - def update_version_macos(self, spec: Specifier) -> ConfigMacOS: + def update_version_macos(self, spec: Specifier) -> ConfigApple: if self.arch not in {"64", "ARM64"}: msg = f"'{self.arch}' arch not supported yet on macOS" raise RuntimeError(msg) @@ -178,7 +178,7 @@ def update_version_macos(self, spec: Specifier) -> ConfigMacOS: if "" in rf["platform"] == "darwin" and rf["arch"] == arch ) - return ConfigMacOS( + return ConfigApple( identifier=identifier, version=f"{version.major}.{version.minor}", url=url, @@ -204,7 +204,7 @@ def __init__(self) -> None: def update_version_macos( self, identifier: str, version: Version, spec: Specifier - ) -> ConfigMacOS | None: + ) -> ConfigApple | None: # see note above on Specifier.filter unsorted_versions = spec.filter(self.versions_dict) sorted_versions = sorted(unsorted_versions, reverse=True) @@ -223,7 +223,7 @@ def update_version_macos( urls = [rf["url"] for rf in file_info if file_ident in rf["url"]] if urls: - return ConfigMacOS( + return ConfigApple( identifier=identifier, version=f"{new_version.major}.{new_version.minor}", url=urls[0], @@ -232,6 +232,48 @@ def update_version_macos( return None +class CPythonIOSVersions: + def __init__(self) -> None: + response = requests.get( + "https://api.github.com/repos/beeware/Python-Apple-support/releases", + headers={ + "Accept": "application/vnd.github+json", + "X-Github-Api-Version": "2022-11-28", + }, + ) + response.raise_for_status() + + releases_info = response.json() + self.versions_dict: dict[Version, dict[int, str]] = {} + + # Each release has a name like "3.13-b4" + for release in releases_info: + py_version, build = release["name"].split("-") + version = Version(py_version) + self.versions_dict.setdefault(version, {}) + + # There are several release assets associated with each release; + # The name of the asset will be something like + # "Python-3.11-iOS-support.b4.tar.gz". Store all builds that are + # "-iOS-support" builds, retaining the download URL. + for asset in release["assets"]: + filename, build, _, _ = asset["name"].rsplit(".", 3) + if filename.endswith("-iOS-support"): + self.versions_dict[version][int(build[1:])] = asset["browser_download_url"] + + def update_version_ios(self, identifier: str, version: Version) -> ConfigApple | None: + # Return a config using the highest build number for the given version. + urls = [url for _, url in sorted(self.versions_dict.get(version, {}).items())] + if urls: + return ConfigApple( + identifier=identifier, + version=str(version), + url=urls[-1], + ) + + return None + + # This is a universal interface to all the above Versions classes. Given an # identifier, it updates a config dict. @@ -250,6 +292,8 @@ def __init__(self) -> None: self.macos_pypy = PyPyVersions("64") self.macos_pypy_arm64 = PyPyVersions("ARM64") + self.ios_cpython = CPythonIOSVersions() + def update_config(self, config: MutableMapping[str, str]) -> None: identifier = config["identifier"] version = Version(config["version"]) @@ -282,6 +326,8 @@ def update_config(self, config: MutableMapping[str, str]) -> None: config_update = self.windows_t_arm64.update_version_windows(spec) elif "win_arm64" in identifier and identifier.startswith("cp"): config_update = self.windows_arm64.update_version_windows(spec) + elif "ios" in identifier: + config_update = self.ios_cpython.update_version_ios(identifier, version) assert config_update is not None, f"{identifier} not found!" config.update(**config_update) @@ -317,6 +363,9 @@ def update_pythons(force: bool, level: str) -> None: for config in configs["macos"]["python_configurations"]: all_versions.update_config(config) + for config in configs["ios"]["python_configurations"]: + all_versions.update_config(config) + result_toml = dump_python_configurations(configs) rich.print() # spacer diff --git a/cibuildwheel/__main__.py b/cibuildwheel/__main__.py index fa3245741..e16adca9c 100644 --- a/cibuildwheel/__main__.py +++ b/cibuildwheel/__main__.py @@ -15,6 +15,7 @@ from typing import Any, Protocol, TextIO, assert_never import cibuildwheel +import cibuildwheel.ios import cibuildwheel.linux import cibuildwheel.macos import cibuildwheel.pyodide @@ -93,13 +94,14 @@ def main_inner(global_options: GlobalOptions) -> None: parser.add_argument( "--platform", - choices=["auto", "linux", "macos", "windows", "pyodide"], + 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, 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" only work on macOS. Default: auto. """, ) @@ -240,6 +242,8 @@ def _compute_platform_only(only: str) -> PlatformName: return "windows" if "pyodide_" in only: return "pyodide" + if "ios_" in only: + return "ios" msg = f"Invalid --only='{only}', must be a build selector with a known platform" raise errors.ConfigurationError(msg) @@ -301,6 +305,8 @@ def get_platform_module(platform: PlatformName) -> PlatformModule: return cibuildwheel.macos if platform == "pyodide": return cibuildwheel.pyodide + if platform == "ios": + return cibuildwheel.ios assert_never(platform) diff --git a/cibuildwheel/architecture.py b/cibuildwheel/architecture.py index 7af966ade..de17c75dd 100644 --- a/cibuildwheel/architecture.py +++ b/cibuildwheel/architecture.py @@ -17,6 +17,7 @@ "macos": "macOS", "windows": "Windows", "pyodide": "Pyodide", + "ios": "iOS", } ARCH_SYNONYMS: Final[list[dict[PlatformName, str | None]]] = [ @@ -63,6 +64,12 @@ class Architecture(StrEnum): # WebAssembly wasm32 = auto() + # iOS "multiarch" architectures that include both + # the CPU architecture and the ABI. + arm64_iphoneos = auto() + arm64_iphonesimulator = auto() + x86_64_iphonesimulator = auto() + @staticmethod def parse_config(config: str, platform: PlatformName) -> "set[Architecture]": result = set() @@ -89,8 +96,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 = ( @@ -99,8 +106,18 @@ 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 == "ios": + # Can only build for iOS on macOS. The "native" architecture is the + # simulator for the macOS native platform. + if host_platform == "macos": + if native_architecture == Architecture.x86_64: + return Architecture.x86_64_iphonesimulator + else: + return Architecture.arm64_iphonesimulator + 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 @@ -131,9 +148,13 @@ def auto_archs(platform: PlatformName) -> "set[Architecture]": 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) + elif platform == "ios" and native_arch == Architecture.arm64_iphonesimulator: + # Also build the device wheel if we're on ARM64. + result.add(Architecture.arm64_iphoneos) + return result @staticmethod @@ -150,6 +171,11 @@ 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}, + "ios": { + Architecture.x86_64_iphonesimulator, + Architecture.arm64_iphonesimulator, + Architecture.arm64_iphoneos, + }, } return all_archs_map[platform] diff --git a/cibuildwheel/ios.py b/cibuildwheel/ios.py new file mode 100644 index 000000000..e3b6c0a47 --- /dev/null +++ b/cibuildwheel/ios.py @@ -0,0 +1,586 @@ +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 PathOrStr +from .util import resources +from .util.cmd import call, shell +from .util.file import ( + CIBW_CACHE_PATH, + copy_test_sources, + download, + 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.multiarch.rsplit("-", 1)[1] + + @property + def arch(self) -> str: + return self.multiarch.rsplit("-", 1)[0] + + @property + def multiarch(self) -> str: + # The multiarch identifier, as reported by `sys.implementation._multiarch` + return "-".join(self.identifier.split("-ios_")[1].rsplit("_", 1)) + + @property + def is_simulator(self) -> bool: + return self.identifier.endswith("_iphonesimulator") + + @property + def xcframework_slice(self) -> str: + "XCframeworks include binaries for multiple ABIs; which ABI section should be used?" + return "ios-arm64_x86_64-simulator" if self.is_simulator else "ios-arm64" + + +def get_python_configurations( + build_selector: BuildSelector, + architectures: Set[Architecture], +) -> 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(config_dict: dict[str, str]) -> str: + # The iOS identifier will be something like cp313-ios_arm64_iphoneos. + # Drop the iphoneos suffix, then replace ios with macosx to yield + # cp313-macosx_arm64, which will be a macOS build identifier. + modified_ios_identifier = config_dict["identifier"].rsplit("_", 1)[0] + macos_identifier = modified_ios_identifier.replace("ios", "macosx") + matching = [ + config for config in macos_python_configs if config["identifier"] == macos_identifier + ] + return matching[0]["url"] + + # Load the platform configuration + full_python_configs = resources.read_python_configs("ios") + # 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.endswith(f"-ios_{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_target_cpython(tmp: Path, config: PythonConfiguration, free_threading: bool) -> Path: + if free_threading: + msg = "Free threading builds aren't available for iOS (yet)" + raise errors.FatalError(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, + target_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 *target* is the platform where the + code will ultimately run, and the *build* is the platform where you're + running the compilation. When building iOS wheels, iOS is the target machine + and macOS is the build machine. The terminology around these machines varies + between build tools (configure uses "host" and "build"; cmake uses "target" and + "build host"). + + A cross-compilation virtualenv is an environment that is based on the + *build* python (so that binaries can execute); but it modifies the + environment at startup so that any request for platform details (such as + `sys.platform` or `sysconfig.get_platform()`) return details of the target + platform. It also applies a loader patch so that any virtualenv created by + the cross-compilation environment will also be a cross-compilation + environment. + + :param py_version: The Python version (major.minor) in use + :param target_python: The path to the python binary for the target platform + :param multiarch: The multiarch tag for the target platform (i.e., the value + of `sys.implementation._multiarch`) + :param build_python: The path to the python binary for the build platform + :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. + + # target_python is the path to the Python binary; + # determine the root of the XCframework slice that is being used. + slice_path = target_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 target environment, especially when building for ARM64 (because the + # build architecture is the same as the target architecture). The primary + # culprit for this is Homebrew libraries leaking in as dependencies for iOS + # libraries. + # + # To prevent problems, set the PATH to isolate the build environment from + # sources that could introduce incompatible binaries. + env["PATH"] = os.pathsep.join( + [ + # The target python's binary directory + str(target_python.parent), + # The cross-platform environments binary directory + str(venv_path / "bin"), + # Cargo's binary directory (to allow for Rust compilation) + str(Path.home() / ".cargo" / "bin"), + # The bare minimum Apple system paths. + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + "/Library/Apple/usr/bin", + ] + ) + # Also unset DYLD_LIBRARY_PATH to ensure that no macOS libraries will be + # found and linked. + env.pop("DYLD_LIBRARY_PATH", None) + + return env + + +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 errors.FatalError(msg) + + # An iOS environment requires 2 python installs - one for the build machine + # (macOS), and one for the target (iOS). We'll only ever interact with the + # *target* python, but the build Python needs to exist to act as the base + # for a cross venv. + tmp.mkdir() + implementation_id = python_configuration.identifier.split("-")[0] + log.step(f"Installing Build Python {implementation_id}...") + if implementation_id.startswith("cp"): + free_threading = "t-ios" in python_configuration.identifier + build_python = install_build_cpython( + tmp, + python_configuration.version, + python_configuration.build_url, + free_threading, + ) + else: + msg = f"Unknown Python implementation: {implementation_id}" + raise errors.FatalError(msg) + + assert build_python.exists(), ( + f"{build_python.name} not found, has {list(build_python.parent.iterdir())}" + ) + + log.step(f"Installing Target Python {implementation_id}...") + target_install_path = install_target_cpython(tmp, python_configuration, free_threading) + target_python = ( + target_install_path + / "Python.xcframework" + / python_configuration.xcframework_slice + / "bin" + / f"python{python_configuration.version}" + ) + + assert target_python.exists(), ( + f"{target_python.name} not found, has {list(target_install_path.iterdir())}" + ) + + log.step("Creating cross build environment...") + + venv_path = tmp / "venv" + env = cross_virtualenv( + py_version=python_configuration.version, + target_python=target_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 + env.setdefault("IPHONEOS_DEPLOYMENT_TARGET", "13.0") + + 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 target_install_path, env + + +def build(options: Options, tmp_path: Path) -> None: + if sys.platform != "darwin": + msg = "iOS binaries can only be built on macOS" + raise errors.FatalError(msg) + + python_configurations = get_python_configurations( + build_selector=options.globals.build_selector, + architectures=options.globals.architectures, + ) + + 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 errors.FatalError(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" + + constraints_path = build_options.dependency_constraints.get_for_python_version( + version=config.version, tmp_dir=identifier_tmp_dir + ) + dependency_constraint_flags: Sequence[PathOrStr] = ( + ["-c", constraints_path] if constraints_path else [] + ) + + target_install_path, env = setup_python( + identifier_tmp_dir / "build", + 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 constraints_path: + combine_constraints(build_env, constraints_path, None) + + if build_frontend.name == "pip": + # Path.resolve() is needed. Without it pip wheel may try to + # 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", + target_install_path / "testbed", + "clone", + testbed_path, + env=build_env, + ) + + if not build_options.test_sources: + # iOS requires an explicit test-sources, as the project directory + # isn't visible on the simulator. + + msg = "Testing on iOS requires a definition of test-sources." + raise errors.FatalError(msg) + + # Copy the test sources to the testbed app + copy_test_sources( + build_options.test_sources, + build_options.package_dir, + testbed_path / "iOSTestbed" / "app", + ) + + log.step("Installing test requirements...") + # Install the compiled wheel (with any test extras), plus + # 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 diff --git a/cibuildwheel/logger.py b/cibuildwheel/logger.py index ec50da8ab..391786a04 100644 --- a/cibuildwheel/logger.py +++ b/cibuildwheel/logger.py @@ -35,6 +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)", } diff --git a/cibuildwheel/resources/build-platforms.toml b/cibuildwheel/resources/build-platforms.toml index e3078f991..b5fa071a2 100644 --- a/cibuildwheel/resources/build-platforms.toml +++ b/cibuildwheel/resources/build-platforms.toml @@ -163,3 +163,10 @@ 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" }, ] + +[ios] +python_configurations = [ + { identifier = "cp313-ios_arm64_iphoneos", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, + { identifier = "cp313-ios_x86_64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, + { identifier = "cp313-ios_arm64_iphonesimulator", version = "3.13", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.13-b5/Python-3.13-iOS-support.b5.tar.gz" }, +] diff --git a/cibuildwheel/resources/cibuildwheel.schema.json b/cibuildwheel/resources/cibuildwheel.schema.json index 2a811d2fe..4a84606cf 100644 --- a/cibuildwheel/resources/cibuildwheel.schema.json +++ b/cibuildwheel/resources/cibuildwheel.schema.json @@ -956,6 +956,51 @@ "$ref": "#/properties/test-requires" } } + }, + "ios": { + "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/resources/defaults.toml b/cibuildwheel/resources/defaults.toml index b8e86c731..6b65e6369 100644 --- a/cibuildwheel/resources/defaults.toml +++ b/cibuildwheel/resources/defaults.toml @@ -51,4 +51,6 @@ repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest [tool.cibuildwheel.windows] +[tool.cibuildwheel.ios] + [tool.cibuildwheel.pyodide] diff --git a/cibuildwheel/typing.py b/cibuildwheel/typing.py index 87923c1e5..420ef05f6 100644 --- a/cibuildwheel/typing.py +++ b/cibuildwheel/typing.py @@ -12,7 +12,7 @@ PathOrStr = str | os.PathLike[str] -PlatformName = Literal["linux", "macos", "windows", "pyodide"] +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 2c8795b8d..f6619dd92 100644 --- a/cibuildwheel/util/packaging.py +++ b/cibuildwheel/util/packaging.py @@ -134,7 +134,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] @@ -157,8 +157,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/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/options.md b/docs/options.md index 0dfaf0a72..080c66bb8 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` `pyodide` Default: `auto` @@ -255,6 +255,7 @@ 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` 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. @@ -288,34 +289,22 @@ When setting the options, you can use shell-style globbing syntax, as per [fnmat
-| | macOS | Windows | Linux Intel | Linux Other | -|---------------|------------------------------------------------------------------------|-----------------------------------------------------|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 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.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). -The lowest value you can set `MACOSX_DEPLOYMENT_TARGET` is as follows: - -| Arch | Python version range | Minimum target | -|-------|----------------------|----------------| -| Intel | CPython 3.8-3.11 | 10.9 | -| Intel | CPython 3.12+ | 10.13 | -| AS | CPython or PyPy | 11 | -| Intel | PyPy 3.8 | 10.13 | -| Intel | PyPy 3.9+ | 10.15 | - -If you set the value lower, cibuildwheel will cap it to the lowest supported value for each target as needed. - Windows arm64 platform support is experimental. For an experimental WebAssembly build with `--platform pyodide`, @@ -435,6 +424,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 @@ -452,6 +442,8 @@ Default: `auto` | Windows / ARM64 | `ARM64` | `ARM64` | `ARM64` | | | macOS / Intel | `x86_64` | `x86_64` | `x86_64` | | | macOS / Apple Silicon | `arm64` | `arm64` | `arm64` | | +| iOS on macOS / Intel | `x86_64_iphonesimulator` | `x86_64_iphonesimulator` | `x86_64_iphonesimulator` | | +| iOS on macOS / Apple Silicon | `arm64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` | `arm64_iphoneos` `arm64_iphonesimulator` | | If not listed above, `auto` is the same as `native`. @@ -459,7 +451,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_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`. @@ -674,8 +666,8 @@ possible, both through `--installer=uv` passed to build, as well as when making 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]`. You cannot use uv currently on Windows for ARM or for -musllinux on s390x as binaries are not provided by uv. Legacy dependencies like +`cibuildwheel[uv]`. You 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. Pyodide ignores this setting, as only "build" is supported. @@ -752,7 +744,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_IOS` | `CIBW_CONFIG_SETTINGS_PYODIDE` #### Examples @@ -783,7 +775,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_IOS` | `CIBW_ENVIRONMENT_PYODIDE` #### Examples @@ -915,7 +907,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_IOS` | `CIBW_BEFORE_ALL_PYODIDE` !!! note @@ -980,7 +972,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_IOS` | `CIBW_BEFORE_BUILD_PYODIDE` #### Examples @@ -1061,6 +1053,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. @@ -1075,7 +1068,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_IOS` | `CIBW_REPAIR_WHEEL_COMMAND_PYODIDE` !!! tip cibuildwheel doesn't yet ship a default repair command for Windows. @@ -1385,7 +1378,7 @@ specifiers inline with the `packages: SPECIFIER...` syntax. `./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_IOS` | `CIBW_DEPENDENCY_VERSIONS_PYODIDE` !!! note This option does not affect the tools used on the Linux build - those versions @@ -1442,7 +1435,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 @@ -1461,10 +1454,12 @@ 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 to `python -m` - that is, a Python module name, followed by arguments that will be assigned to `sys.argv`. Shell commands cannot be used. Platform-specific environment variables are also available:
-`CIBW_TEST_COMMAND_MACOS` | `CIBW_TEST_COMMAND_WINDOWS` | `CIBW_TEST_COMMAND_LINUX` | `CIBW_TEST_COMMAND_PYODIDE` +`CIBW_TEST_COMMAND_MACOS` | `CIBW_TEST_COMMAND_WINDOWS` | `CIBW_TEST_COMMAND_LINUX` | `CIBW_TEST_COMMAND_IOS` | `CIBW_TEST_COMMAND_PYODIDE` #### Examples @@ -1521,7 +1516,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_IOS` | `CIBW_BEFORE_TEST_PYODIDE` #### Examples @@ -1583,8 +1578,13 @@ project, required for running the tests. If specified, these files and folders will be copied into a temporary folder, and that temporary folder will be used as the working directory for running the test suite. +The use of `CIBW_TEST_SOURCES` is *required* for iOS builds. This is because the +simulator does not have access to the project directory, as it is not stored on +the simulator device. On iOS, the files will be copied into the test application, +rather than a temporary folder. + Platform-specific environment variables are also available:
-`CIBW_TEST_SOURCES_MACOS` | `CIBW_TEST_SOURCES_WINDOWS` | `CIBW_TEST_SOURCES_LINUX` | `CIBW_TEST_SOURCES_PYODIDE` +`CIBW_TEST_SOURCES_MACOS` | `CIBW_TEST_SOURCES_WINDOWS` | `CIBW_TEST_SOURCES_LINUX` | `CIBW_TEST_SOURCES_IOS` | `CIBW_TEST_SOURCES_PYODIDE` #### Examples @@ -1612,7 +1612,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_IOS` | `CIBW_TEST_REQUIRES_PYODIDE` #### Examples @@ -1652,7 +1652,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_IOS` | `CIBW_TEST_EXTRAS_PYODIDE` #### Examples @@ -1779,7 +1779,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_IOS` | `CIBW_BUILD_VERBOSITY_PYODIDE` #### Examples diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md new file mode 100644 index 000000000..a58c0132b --- /dev/null +++ b/docs/platforms/ios.md @@ -0,0 +1,68 @@ +--- +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, 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 wheels also requires a working macOS Python installation. See the notes on [macOS builds](./macos.md) for details about configuration of the macOS environment. + +## Specifying an iOS build + +iOS is effectively 2 platforms - physical devices, and simulators. While the API for these two platforms are identical, the ABI is not compatible, even when dealing with a device and simulator with the same CPU architecture. For this reason, the architecture specification for iOS builds includes *both* the CPU architecture *and* the ABI that is being targeted. There are three possible values for architecture on iOS; the values match those used by `sys.implementation._multiarch` when running on iOS (with hyphens replaced with underscores, matching wheel filename normalization): + +* `arm64_iphoneos` (for physical iOS devices); +* `arm64_iphonesimulator` (for iOS simulators running on Apple Silicon macOS machines); and +* `x64_64_iphonesimulator` (for iOS simulators running on Intel macOS machines). + +By default, cibuildwheel will build wheels for all three of these targets. + +If you need to specify different compilation flags or other properties on a per-ABI or per-CPU basis, you can use [configuration overrides](../../options/#overrides) with a `select` clause that targets the specific ABI or architecture. For example, consider the following example: + +``` +[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 = "*-ios_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 iOS devices (whether simulator or device). + +## 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 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 + +If tests have been configured, the test suite will be executed on the simulator matching the architecture of the build machine - that is, if you're building on an ARM64 macOS machine, the ARM64 wheel will be tested on an ARM64 simulator. It is not possible to use cibuildwheel to test wheels on other simulators, or on physical devices. + +The iOS test environment can't support running shell scripts, so the [`CIBW_TEST_COMMAND`](../../options#test-command) value must be specified as if it were a command line being passed to `python -m ...`. In addition, the project must use [`CIBW_TEST_SOURCES`](../../options#test-sources) to specify the minimum subset of files that should be copied to the test environment. This is because the test must be run "on device", and the simulator device will not have access to the local project directory. + +The 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..01e8d0ec8 --- /dev/null +++ b/docs/platforms/macos.md @@ -0,0 +1,46 @@ +--- +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 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.9 is not installed. + cibuildwheel will not perform system-wide installs when running outside of CI. + To build locally, install CPython 3.9 on this machine, or, disable this version of Python using CIBW_SKIP=cp39-macosx_* + + Download link: https://www.python.org/ftp/python/3.9.8/python-3.9.8-macosx10.9.pkg + ``` + +## macOS Version Compatibility + +macOS builds will honor the `MACOSX_DEPLOYMENT_TARGET` environment variable to control the minimum supported macOS version for generated wheels. The lowest value you can set `MACOSX_DEPLOYMENT_TARGET` is as follows: + +| Arch | Python version range | Minimum target | +|-------|----------------------|----------------| +| Intel | CPython 3.8-3.11 | 10.9 | +| Intel | CPython 3.12+ | 10.13 | +| AS | CPython or PyPy | 11 | +| Intel | PyPy 3.8 | 10.13 | +| Intel | PyPy 3.9+ | 10.15 | + +If you set the value lower, cibuildwheel will cap it to the lowest supported value for each target as needed. + +## Universal builds + +By default, macOS builds will build a single architecture wheel, using the build machine's architecture. If you need to support both x86_64 and Apple Silicon, you can use the `CIBW_ARCHS` environment variable to specify the architectures you want to build, or the value `universal2` to build a multi-architecture wheel. cibuildwheel will test x86_64 wheels (or the x86_64 slice of a `universal2` wheel) when running on Apple Silicon hardware, but it is *not* possible to test Apple Silicon wheels on x86_64 hardware. diff --git a/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 5b534f1b8..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,66 +70,11 @@ 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 - -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.9 is not installed. - cibuildwheel will not perform system-wide installs when running outside of CI. - To build locally, install CPython 3.9 on this machine, or, disable this version of Python using CIBW_SKIP=cp39-macosx_* - - Download link: https://www.python.org/ftp/python/3.9.8/python-3.9.8-macosx10.9.pkg - ``` - -### 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} -To build Linux, Mac, and Windows 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 @@ -144,29 +99,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.23.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,45 +109,17 @@ 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.23.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. 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-deploy.yml b/examples/github-deploy.yml index 2061be4d2..119b365b2 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.23.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 91c2a7edf..530ea05ce 100644 --- a/examples/github-minimal.yml +++ b/examples/github-minimal.yml @@ -18,7 +18,7 @@ jobs: uses: pypa/cibuildwheel@v2.23.0 # env: # CIBW_SOME_OPTION: value - # ... + # ... # with: # package-dir: . # output-dir: wheelhouse diff --git a/examples/github-pipx.yml b/examples/github-pipx.yml new file mode 100644 index 000000000..0e365cc9c --- /dev/null +++ b/examples/github-pipx.yml @@ -0,0 +1,23 @@ +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.23.0 + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} + path: ./wheelhouse/*.whl 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 diff --git a/test/test_emulation.py b/test/test_emulation.py index a8926c0bd..70242a510 100644 --- a/test/test_emulation.py +++ b/test/test_emulation.py @@ -11,8 +11,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_ios.py b/test/test_ios.py new file mode 100644 index 000000000..6dc7b40d3 --- /dev/null +++ b/test/test_ios.py @@ -0,0 +1,95 @@ +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/test_platform.py"] = f""" +import platform +from unittest import TestCase + +class TestPlatform(TestCase): + def test_platform(self): + self.assertEqual(platform.machine(), "{platform.machine()}") + +""" + + +# 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"}, + # Also check the build frontend + {"CIBW_PLATFORM": "ios", "CIBW_BUILD_FRONTEND": "build"}, + ], +) +def test_ios_platforms(tmp_path, build_config): + if utils.platform != "macos": + pytest.skip("this test can only run on macOS") + if utils.get_xcode_version() < (13, 0): + 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={ + "CIBW_BUILD": "cp313-*", + "CIBW_TEST_SOURCES": "tests", + "CIBW_TEST_COMMAND": "unittest discover tests test_platform.py", + **build_config, + }, + ) + + ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_") + platform_machine = platform.machine() + + # Tests are only executed on simulator. The test suite passes if it's + # running on the same architecture as the current platform. + if platform_machine == "x86_64": + expected_wheels = { + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl", + } + + elif platform_machine == "arm64": + expected_wheels = { + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl", + f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl", + } + + assert set(actual_wheels) == expected_wheels + + +@pytest.mark.xdist_group(name="ios") +def test_no_test_sources(tmp_path, capfd): + if utils.platform != "macos": + pytest.skip("this test can only run on macOS") + if utils.get_xcode_version() < (13, 0): + pytest.skip("this test only works with Xcode 13.0 or greater") + + project_dir = tmp_path / "project" + basic_project.generate(project_dir) + + with pytest.raises(subprocess.CalledProcessError): + utils.cibuildwheel_run( + project_dir, + add_env={ + "CIBW_PLATFORM": "ios", + "CIBW_BUILD": "cp313-*", + "CIBW_TEST_COMMAND": "tests", + }, + ) + + captured = capfd.readouterr() + assert "Testing on iOS requires a definition of test-sources." in captured.err 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/test_projects/c.py b/test/test_projects/c.py index 05882ec1e..55d759875 100644 --- a/test/test_projects/c.py +++ b/test/test_projects/c.py @@ -8,15 +8,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 = system(command); + // Spam should not be allowed through the filter. + sts = strcmp(content, "spam"); {{ spam_c_function_add | indent(4) }} @@ -25,7 +26,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 e35e7e0f3..4dfda9141 100644 --- a/test/test_testing.py +++ b/test/test_testing.py @@ -44,9 +44,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 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)