diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4188fd65..d93c8f13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ ci: autoupdate_schedule: monthly +exclude: "^cibuildwheel/resources/ios-support/(?!make_cross_venv\\.py)" repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/README.md b/README.md index 62d3c73a..1c88168a 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,13 @@ While cibuildwheel itself requires a recent Python version to run (we support th | | 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 | Android | iOS | Pyodide | | ------------------------ | ----------- | ------------------- | ------------- | ------------- | -------------- | ------------------------------ | ---------------------------- | ------------------------------- | ------------------------------- | ----------------------------- | ------------------------------ | ------- | --- | -------------- | -| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | N/A | N/A | N/A | -| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | N/A | N/A | N/A | -| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | N/A | N/A | N/A | -| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | N/A | N/A | ✅3 | -| CPython 3.13 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | ✅ | ✅ | ✅ | -| CPython 3.14 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | ✅ | ✅ | ✅4 | -| CPython 3.156 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | ✅ | N/A | N/A | +| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | N/A | N/A | N/A | +| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | N/A | N/A | N/A | +| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | N/A | N/A | N/A | +| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | N/A | N/A | ✅3 | +| CPython 3.13 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | ✅ | ✅ | ✅ | +| CPython 3.14 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | ✅ | ✅ | ✅4 | +| CPython 3.156 | ✅ | ✅ | ✅ | ✅ | ✅2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅5 | ✅ | ✅ | N/A | | PyPy 3.9 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅1 | ✅1 | ✅1 | N/A | N/A | N/A | N/A | N/A | N/A | | PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅1 | ✅1 | ✅1 | N/A | N/A | N/A | N/A | N/A | N/A | | PyPy 3.11 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅1 | ✅1 | ✅1 | N/A | N/A | N/A | N/A | N/A | N/A | diff --git a/bin/update_pythons.py b/bin/update_pythons.py index b99a079e..f32afa14 100755 --- a/bin/update_pythons.py +++ b/bin/update_pythons.py @@ -488,7 +488,14 @@ class AllVersions: versions = self.maven if identifier.startswith("cp313") else self.cpython config_update = versions.update_version_android(identifier, spec) elif "ios" in identifier: - config_update = self.ios_cpython.update_version_ios(identifier, version) + # Python 3.13 and 3.14 are released by BeeWare on GitHub. + # Python 3.15 and newer have official releases on python.org. + if identifier.startswith(("cp313", "cp314")): + config_update = self.ios_cpython.update_version_ios(identifier, version) + else: + config_update = self.cpython.update_version( + identifier, spec, "iOS-XCframework.tar.gz" + ) elif "pyodide" in identifier: config_update = self.pyodide.update_version_pyodide( identifier, version, spec, config["node_version"] diff --git a/cibuildwheel/platforms/ios.py b/cibuildwheel/platforms/ios.py index 40c66b87..9efc6e25 100644 --- a/cibuildwheel/platforms/ios.py +++ b/cibuildwheel/platforms/ios.py @@ -11,6 +11,7 @@ from pathlib import Path from typing import assert_never from filelock import FileLock +from packaging.version import Version from cibuildwheel import errors from cibuildwheel.architecture import Architecture @@ -183,18 +184,37 @@ def cross_virtualenv( ) # Convert the macOS virtual environment into an iOS virtual environment - # using the cross-platform conversion script in the iOS distribution. + # using make_cross_venv.py. # 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, - ) + if Version(py_version) >= Version("3.15"): + # python.org 3.15+ distributions: sysconfig data lives in the + # stdlib directory (lib-/python); cross-venv scripts + # come from cibuildwheel resources. + arch = multiarch.split("-", maxsplit=1)[0] + stdlib_path = slice_path / f"lib-{arch}" / f"python{py_version}" + call( + "python", + str(resources.IOS_SUPPORT_FILES / "make_cross_venv.py"), + str(venv_path), + str(stdlib_path), + str(resources.IOS_SUPPORT_FILES), + env=env, + cwd=venv_path, + ) + else: + # BeeWare distributions: platform-config already contains both + # sysconfig data and the make_cross_venv.py script. + platform_config_path = slice_path / f"platform-config/{multiarch}" + call( + "python", + str(platform_config_path / "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 @@ -312,8 +332,8 @@ def setup_python( / f"python{python_configuration.version}" ) - assert target_python.exists(), ( - f"{target_python.name} not found, has {list(target_install_path.iterdir())}" + assert target_python.parent.exists(), ( + f"{target_python.parent} not found, has {list(target_install_path.iterdir())}" ) log.step("Creating cross build environment...") diff --git a/cibuildwheel/resources/build-platforms.toml b/cibuildwheel/resources/build-platforms.toml index 701f191d..13c1a296 100644 --- a/cibuildwheel/resources/build-platforms.toml +++ b/cibuildwheel/resources/build-platforms.toml @@ -243,4 +243,7 @@ python_configurations = [ { identifier = "cp314-ios_arm64_iphoneos", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz" }, { identifier = "cp314-ios_x86_64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz" }, { identifier = "cp314-ios_arm64_iphonesimulator", version = "3.14", url = "https://github.com/beeware/Python-Apple-support/releases/download/3.14-b9/Python-3.14-iOS-support.b9.tar.gz" }, + { identifier = "cp315-ios_arm64_iphoneos", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz" }, + { identifier = "cp315-ios_x86_64_iphonesimulator", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz" }, + { identifier = "cp315-ios_arm64_iphonesimulator", version = "3.15", url = "https://www.python.org/ftp/python/3.15.0/python-3.15.0b1-iOS-XCframework.tar.gz" }, ] diff --git a/cibuildwheel/resources/ios-support/_cross_venv.py b/cibuildwheel/resources/ios-support/_cross_venv.py new file mode 100644 index 00000000..1a1842b4 --- /dev/null +++ b/cibuildwheel/resources/ios-support/_cross_venv.py @@ -0,0 +1,103 @@ +import shutil +import sys +import sysconfig +from pathlib import Path + +SITE_PACKAGE_PATH = Path(__file__).parent + +########################################################################### +# importlib module patches +########################################################################### + + +def patch_env_create(env): + """ + Patch the process of creating virtual environments to ensure that the cross + environment modification files are also copied as part of environment + creation. + """ + old_pip_env_create = env._PipBackend.create + + def pip_env_create(self, path, *args, **kwargs): + result = old_pip_env_create(self, path, *args, **kwargs) + # Copy any _cross_*.pth or _cross_*.py file, plus the cross-platform + # sysconfigdata module and sysconfig_vars JSON to the new environment. + data_name = sysconfig._get_sysconfigdata_name() + json_name = data_name.replace("_sysconfigdata", "_sysconfig_vars") + for filename in [ + "_cross_venv.pth", + "_cross_venv.py", + f"_cross_{sys.implementation._multiarch.replace('-', '_')}.py", + f"{data_name}.py", + f"{json_name}.json", + ]: + src = SITE_PACKAGE_PATH / filename + target = Path(path) / src.relative_to(SITE_PACKAGE_PATH.parent.parent.parent) + if not target.exists(): + shutil.copy(src, target) + return result + + env._PipBackend.create = pip_env_create + + +# Import hook that patches the creation of virtual environments by `build` +# +# The approach used here is the same as the one used by virtualenv to patch +# distutils (but without support for the older load_module API). +# https://docs.python.org/3/library/importlib.html#setting-up-an-importer +_BUILD_PATCH = ("build.env",) + + +class _Finder: + """A meta path finder that allows patching the imported build modules.""" + + fullname = None + + # lock[0] is threading.Lock(), but initialized lazily to avoid importing + # threading very early at startup, because there are gevent-based + # applications that need to be first to import threading by themselves. + # See https://github.com/pypa/virtualenv/issues/1895 for details. + lock = [] # noqa: RUF012 + + def find_spec(self, fullname, path, target=None): + if fullname in _BUILD_PATCH and self.fullname is None: + # initialize lock[0] lazily + if len(self.lock) == 0: + import threading + + lock = threading.Lock() + # there is possibility that two threads T1 and T2 are + # simultaneously running into find_spec, observing .lock as + # empty, and further going into hereby initialization. However + # due to the GIL, list.append() operation is atomic and this + # way only one of the threads will "win" to put the lock + # - that every thread will use - into .lock[0]. + # https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe + self.lock.append(lock) + + from functools import partial + from importlib.util import find_spec + + with self.lock[0]: + self.fullname = fullname + try: + spec = find_spec(fullname, path) + if spec is not None: + # https://www.python.org/dev/peps/pep-0451/#how-loading-will-work + old = spec.loader.exec_module + func = self.exec_module + if old is not func: + spec.loader.exec_module = partial(func, old) + return spec + finally: + self.fullname = None + return None + + @staticmethod + def exec_module(old, module): + old(module) + if module.__name__ in _BUILD_PATCH: + patch_env_create(module) + + +sys.meta_path.insert(0, _Finder()) diff --git a/cibuildwheel/resources/ios-support/arm64-iphoneos/_cross_arm64_iphoneos.py b/cibuildwheel/resources/ios-support/arm64-iphoneos/_cross_arm64_iphoneos.py new file mode 100644 index 00000000..ec1c128b --- /dev/null +++ b/cibuildwheel/resources/ios-support/arm64-iphoneos/_cross_arm64_iphoneos.py @@ -0,0 +1,79 @@ +# A site package that turns a macOS virtual environment +# into an arm64 iphoneos cross-platform virtual environment +import platform +import subprocess +import sys +import sysconfig + +########################################################################### +# sys module patches +########################################################################### +sys.cross_compiling = True +sys.platform = "ios" +sys.implementation._multiarch = "arm64-iphoneos" +sys.base_prefix = sysconfig._get_sysconfigdata()["prefix"] +sys.base_exec_prefix = sysconfig._get_sysconfigdata()["prefix"] + +########################################################################### +# subprocess module patches +########################################################################### +subprocess._can_fork_exec = True + + +########################################################################### +# platform module patches +########################################################################### + + +def cross_system(): + return "iOS" + + +def cross_uname(): + return platform.uname_result( + system="iOS", + node="build", + release="13.0", + version="", + machine="arm64", + ) + + +def cross_ios_ver(system="", release="", model="", is_simulator=False): + if system == "": + system = "iOS" + if release == "": + release = "13.0" + if model == "": + model = "iphoneos" + + return platform.IOSVersionInfo(system, release, model, False) + + +platform.system = cross_system +platform.uname = cross_uname +platform.ios_ver = cross_ios_ver + + +########################################################################### +# sysconfig module patches +########################################################################### + + +def cross_get_platform(): + return "ios-13.0-arm64-iphoneos" + + +def cross_get_sysconfigdata_name(): + return "_sysconfigdata__ios_arm64-iphoneos" + + +sysconfig.get_platform = cross_get_platform +sysconfig._get_sysconfigdata_name = cross_get_sysconfigdata_name + +# Ensure module-level values cached at time of import are updated. +sysconfig._BASE_PREFIX = sys.base_prefix +sysconfig._BASE_EXEC_PREFIX = sys.base_exec_prefix + +# Force sysconfig data to be loaded (and cached). +sysconfig._init_config_vars() diff --git a/cibuildwheel/resources/ios-support/arm64-iphoneos/sitecustomize.py b/cibuildwheel/resources/ios-support/arm64-iphoneos/sitecustomize.py new file mode 100644 index 00000000..6f02a983 --- /dev/null +++ b/cibuildwheel/resources/ios-support/arm64-iphoneos/sitecustomize.py @@ -0,0 +1,22 @@ +# A site customization that can be used to trick pip into installing packages +# cross-platform. If the folder containing this file is on your PYTHONPATH when +# you invoke python, the interpreter will behave as if it were running on +# arm64 iphoneos. +import sys +import os + +# Apply the cross-platform patch +import _cross_arm64_iphoneos +import _cross_venv + + +# Call the next sitecustomize script if there is one +# (https://nedbatchelder.com/blog/201001/running_code_at_python_startup.html). +del sys.modules["sitecustomize"] +this_dir = os.path.dirname(__file__) +path_index = sys.path.index(this_dir) +del sys.path[path_index] +try: + import sitecustomize # noqa: F401 +finally: + sys.path.insert(path_index, this_dir) diff --git a/cibuildwheel/resources/ios-support/arm64-iphonesimulator/_cross_arm64_iphonesimulator.py b/cibuildwheel/resources/ios-support/arm64-iphonesimulator/_cross_arm64_iphonesimulator.py new file mode 100644 index 00000000..113b2b13 --- /dev/null +++ b/cibuildwheel/resources/ios-support/arm64-iphonesimulator/_cross_arm64_iphonesimulator.py @@ -0,0 +1,79 @@ +# A site package that turns a macOS virtual environment +# into an arm64 iphonesimulator cross-platform virtual environment +import platform +import subprocess +import sys +import sysconfig + +########################################################################### +# sys module patches +########################################################################### +sys.cross_compiling = True +sys.platform = "ios" +sys.implementation._multiarch = "arm64-iphonesimulator" +sys.base_prefix = sysconfig._get_sysconfigdata()["prefix"] +sys.base_exec_prefix = sysconfig._get_sysconfigdata()["prefix"] + +########################################################################### +# subprocess module patches +########################################################################### +subprocess._can_fork_exec = True + + +########################################################################### +# platform module patches +########################################################################### + + +def cross_system(): + return "iOS" + + +def cross_uname(): + return platform.uname_result( + system="iOS", + node="build", + release="13.0", + version="", + machine="arm64", + ) + + +def cross_ios_ver(system="", release="", model="", is_simulator=False): + if system == "": + system = "iOS" + if release == "": + release = "13.0" + if model == "": + model = "iphonesimulator" + + return platform.IOSVersionInfo(system, release, model, True) + + +platform.system = cross_system +platform.uname = cross_uname +platform.ios_ver = cross_ios_ver + + +########################################################################### +# sysconfig module patches +########################################################################### + + +def cross_get_platform(): + return "ios-13.0-arm64-iphonesimulator" + + +def cross_get_sysconfigdata_name(): + return "_sysconfigdata__ios_arm64-iphonesimulator" + + +sysconfig.get_platform = cross_get_platform +sysconfig._get_sysconfigdata_name = cross_get_sysconfigdata_name + +# Ensure module-level values cached at time of import are updated. +sysconfig._BASE_PREFIX = sys.base_prefix +sysconfig._BASE_EXEC_PREFIX = sys.base_exec_prefix + +# Force sysconfig data to be loaded (and cached). +sysconfig._init_config_vars() diff --git a/cibuildwheel/resources/ios-support/arm64-iphonesimulator/sitecustomize.py b/cibuildwheel/resources/ios-support/arm64-iphonesimulator/sitecustomize.py new file mode 100644 index 00000000..b76ad42c --- /dev/null +++ b/cibuildwheel/resources/ios-support/arm64-iphonesimulator/sitecustomize.py @@ -0,0 +1,22 @@ +# A site customization that can be used to trick pip into installing packages +# cross-platform. If the folder containing this file is on your PYTHONPATH when +# you invoke python, the interpreter will behave as if it were running on +# arm64 iphonesimulator. +import sys +import os + +# Apply the cross-platform patch +import _cross_arm64_iphonesimulator +import _cross_venv + + +# Call the next sitecustomize script if there is one +# (https://nedbatchelder.com/blog/201001/running_code_at_python_startup.html). +del sys.modules["sitecustomize"] +this_dir = os.path.dirname(__file__) +path_index = sys.path.index(this_dir) +del sys.path[path_index] +try: + import sitecustomize # noqa: F401 +finally: + sys.path.insert(path_index, this_dir) diff --git a/cibuildwheel/resources/ios-support/make_cross_venv.py b/cibuildwheel/resources/ios-support/make_cross_venv.py new file mode 100644 index 00000000..2226ffeb --- /dev/null +++ b/cibuildwheel/resources/ios-support/make_cross_venv.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import json +import pprint +import shutil +import sys +from importlib import util as importlib_util +from pathlib import Path +from typing import Any + + +def localized_vars(orig_vars: dict[str, Any], slice_path: Path) -> dict[str, Any]: + """Update (where possible) any references to build-time variables with the + best guess of the installed location. + """ + # The host's sysconfigdata will include references to build-time variables. + # Update these to refer to the current known install location. + orig_prefix = orig_vars["prefix"] + localized_vars = {} + for key, value in orig_vars.items(): + final = value + if isinstance(value, str): + # Replace any reference to the build installation prefix + final = final.replace(orig_prefix, str(slice_path)) + # Replace any reference to the build-time Framework location + final = final.replace("-F .", f"-F {slice_path}") + localized_vars[key] = final + + return localized_vars + + +def localize_sysconfigdata(sysconfig_path: Path, venv_site_packages: Path) -> None: + """Localize a sysconfigdata python module. + + :param sysconfig_path: The platform config that contains the + sysconfigdata module to localize. + :param venv_site_packages: The site packages folder where the localized + sysconfigdata module should be output. + """ + # Find the "_sysconfigdata_*.py" file in the platform config + sysconfigdata_path = next(sysconfig_path.glob("_sysconfigdata_*.py")) + + # Import the sysconfigdata module + spec = importlib_util.spec_from_file_location(sysconfigdata_path.stem, sysconfigdata_path) + if spec is None: + msg = f"Unable to load spec for {sysconfigdata_path}" + raise ValueError(msg) + if spec.loader is None: + msg = f"Spec for {sysconfigdata_path} does not define a loader" + raise ValueError(msg) + sysconfigdata = importlib_util.module_from_spec(spec) + spec.loader.exec_module(sysconfigdata) + + # Write the updated sysconfigdata module into the cross-platform site. + slice_path = sysconfigdata_path.parent.parent.parent + with (venv_site_packages / sysconfigdata_path.name).open("w") as f: + f.write(f"# Generated from {sysconfigdata_path}\n") + f.write("build_time_vars = ") + pprint.pprint( + localized_vars(sysconfigdata.build_time_vars, slice_path), stream=f, compact=True + ) + + +def localize_sysconfig_vars(sysconfig_path: Path, venv_site_packages: Path) -> None: + """Localize a sysconfig_vars.json file. + + :param sysconfig_path: The platform config that contains the + sysconfigdata module to localize. + :param venv_site_packages: The site-packages folder where the localized + sysconfig_vars.json file should be output. + """ + # Find the "_sysconfig_vars_*.json" file in the platform config + sysconfig_vars_path = next(sysconfig_path.glob("_sysconfig_vars_*.json")) + + with sysconfig_vars_path.open("rb") as f: + build_time_vars = json.load(f) + + slice_path = sysconfig_vars_path.parent.parent.parent + with (venv_site_packages / sysconfig_vars_path.name).open("w") as f: + json.dump(localized_vars(build_time_vars, slice_path), f, indent=2) + + +def make_cross_venv( + venv_path: Path, + sysconfig_path: Path, + platform_config_path: Path, +) -> None: + """Convert a virtual environment into a cross-platform environment. + + :param venv_path: The path to the root of the venv. + :param sysconfig_path: The path containing sysconfigdata files for the + target platform. + :param platform_config_path: The path containing the cross-venv support + scripts (_cross_venv.py and the multiarch-specific _cross_*.py). + """ + if not venv_path.exists(): + msg = f"Virtual environment {venv_path} does not exist." + raise ValueError(msg) + if not (venv_path / "bin/python3").exists(): + msg = f"{venv_path} does not appear to be a virtual environment." + raise ValueError(msg) + + LIB_PATH = f"lib/python{sys.version_info[0]}.{sys.version_info[1]}" + + # Derive the multiarch tag from the sysconfigdata filename if available; + # otherwise fall back to the directory name (BeeWare convention). + sysconfigdata_files = list(sysconfig_path.glob("_sysconfigdata_*.py")) + if sysconfigdata_files: + multiarch_tag = sysconfigdata_files[0].stem.split("_ios_")[1] + cross_multiarch = f"_cross_{multiarch_tag.replace('-', '_')}" + else: + multiarch_tag = sysconfig_path.name + cross_multiarch = f"_cross_{multiarch_tag.replace('-', '_')}" + + print( + f"Converting {venv_path} into a {multiarch_tag} environment... ", + end="", + ) + + # Update path references in the sysconfigdata to reflect local conditions. + venv_site_packages = venv_path / LIB_PATH / "site-packages" + localize_sysconfigdata(sysconfig_path, venv_site_packages) + localize_sysconfig_vars(sysconfig_path, venv_site_packages) + # The multiarch-specific script may be directly in platform_config_path (BeeWare + # convention, where all files are co-located) or in a named subdirectory + # (cibuildwheel resource layout, where each multiarch has its own subdir). + multiarch_script = platform_config_path / f"{cross_multiarch}.py" + if not multiarch_script.exists(): + multiarch_script = platform_config_path / multiarch_tag / f"{cross_multiarch}.py" + shutil.copy( + multiarch_script, + venv_site_packages / f"{cross_multiarch}.py", + ) + shutil.copy( + platform_config_path / "_cross_venv.py", + venv_site_packages / "_cross_venv.py", + ) + # Write the .pth file that will enable the cross-env modifications + (venv_site_packages / "_cross_venv.pth").write_text( + f"import {cross_multiarch}; import _cross_venv\n" + ) + + print("done.") + + +if __name__ == "__main__": + try: + sysconfig_path = Path(sys.argv[2]).resolve() + except IndexError: + sysconfig_path = Path(__file__).parent + + try: + platform_config_path = Path(sys.argv[3]).resolve() + except IndexError: + platform_config_path = sysconfig_path + + try: + venv_path = Path(sys.argv[1]).resolve() + make_cross_venv(venv_path, sysconfig_path, platform_config_path) + except IndexError: + print(""" +Convert a virtual environment into a cross-platform environment. + +Usage: + make_cross_venv () () + +The sysconfig path is the path that contains the `_sysconfigdata-*.py` file +for the platform being targeted. If an explicit sysconfig path isn't +provided, it is assumed the directory containing the make_cross_venv script +also contains the sysconfig data for the interpreter. + +The platform config path is the path that contains the cross-venv support +scripts. If an explicit platform config path isn't provided, it is assumed +that the sysconfig path also contains the platform configuration files. +""") diff --git a/cibuildwheel/resources/ios-support/x86_64-iphonesimulator/_cross_x86_64_iphonesimulator.py b/cibuildwheel/resources/ios-support/x86_64-iphonesimulator/_cross_x86_64_iphonesimulator.py new file mode 100644 index 00000000..02c7ac93 --- /dev/null +++ b/cibuildwheel/resources/ios-support/x86_64-iphonesimulator/_cross_x86_64_iphonesimulator.py @@ -0,0 +1,79 @@ +# A site package that turns a macOS virtual environment +# into an x86_64 iphonesimulator cross-platform virtual environment +import platform +import subprocess +import sys +import sysconfig + +########################################################################### +# sys module patches +########################################################################### +sys.cross_compiling = True +sys.platform = "ios" +sys.implementation._multiarch = "x86_64-iphonesimulator" +sys.base_prefix = sysconfig._get_sysconfigdata()["prefix"] +sys.base_exec_prefix = sysconfig._get_sysconfigdata()["prefix"] + +########################################################################### +# subprocess module patches +########################################################################### +subprocess._can_fork_exec = True + + +########################################################################### +# platform module patches +########################################################################### + + +def cross_system(): + return "iOS" + + +def cross_uname(): + return platform.uname_result( + system="iOS", + node="build", + release="13.0", + version="", + machine="x86_64", + ) + + +def cross_ios_ver(system="", release="", model="", is_simulator=False): + if system == "": + system = "iOS" + if release == "": + release = "13.0" + if model == "": + model = "iphonesimulator" + + return platform.IOSVersionInfo(system, release, model, True) + + +platform.system = cross_system +platform.uname = cross_uname +platform.ios_ver = cross_ios_ver + + +########################################################################### +# sysconfig module patches +########################################################################### + + +def cross_get_platform(): + return "ios-13.0-x86_64-iphonesimulator" + + +def cross_get_sysconfigdata_name(): + return "_sysconfigdata__ios_x86_64-iphonesimulator" + + +sysconfig.get_platform = cross_get_platform +sysconfig._get_sysconfigdata_name = cross_get_sysconfigdata_name + +# Ensure module-level values cached at time of import are updated. +sysconfig._BASE_PREFIX = sys.base_prefix +sysconfig._BASE_EXEC_PREFIX = sys.base_exec_prefix + +# Force sysconfig data to be loaded (and cached). +sysconfig._init_config_vars() diff --git a/cibuildwheel/resources/ios-support/x86_64-iphonesimulator/sitecustomize.py b/cibuildwheel/resources/ios-support/x86_64-iphonesimulator/sitecustomize.py new file mode 100644 index 00000000..7e6b8b2d --- /dev/null +++ b/cibuildwheel/resources/ios-support/x86_64-iphonesimulator/sitecustomize.py @@ -0,0 +1,22 @@ +# A site customization that can be used to trick pip into installing packages +# cross-platform. If the folder containing this file is on your PYTHONPATH when +# you invoke python, the interpreter will behave as if it were running on +# x86_64 iphonesimulator. +import sys +import os + +# Apply the cross-platform patch +import _cross_x86_64_iphonesimulator +import _cross_venv + + +# Call the next sitecustomize script if there is one +# (https://nedbatchelder.com/blog/201001/running_code_at_python_startup.html). +del sys.modules["sitecustomize"] +this_dir = os.path.dirname(__file__) +path_index = sys.path.index(this_dir) +del sys.path[path_index] +try: + import sitecustomize # noqa: F401 +finally: + sys.path.insert(path_index, this_dir) diff --git a/cibuildwheel/util/resources.py b/cibuildwheel/util/resources.py index 37775b18..7523f921 100644 --- a/cibuildwheel/util/resources.py +++ b/cibuildwheel/util/resources.py @@ -18,6 +18,7 @@ VIRTUALENV: Final[Path] = PATH / "virtualenv.toml" CIBUILDWHEEL_SCHEMA: Final[Path] = PATH / "cibuildwheel.schema.json" PYTHON_BUILD_STANDALONE_RELEASES: Final[Path] = PATH / "python-build-standalone-releases.json" TEST_FAIL_CWD_FILE: Final[Path] = PATH / "testing_temp_dir_file.py" +IOS_SUPPORT_FILES: Final[Path] = PATH / "ios-support" # this value is cached because it's used a lot in unit tests diff --git a/pyproject.toml b/pyproject.toml index fd84620b..cfb1ba20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -243,7 +243,10 @@ flake8-annotations.mypy-init-return = true ignore = ["PC170", "PP303"] [tool.check-wheel-contents] -ignore = ["W002"] # constraints-*.txt are allowed to be duplicates of one another +ignore = [ + "W002", # constraints-*.txt are allowed to be duplicates of one another + "W004", # "-" is fine for our iOS support files for now +] [tool.codespell] ignore-words-list = [ diff --git a/test/utils.py b/test/utils.py index d451f750..a38a8839 100644 --- a/test/utils.py +++ b/test/utils.py @@ -270,18 +270,15 @@ def _expected_wheels( python_abi_tags = ["cp313-cp313"] if EnableGroup.PyodidePrerelease in enable_groups: python_abi_tags.append("cp314-cp314") - elif platform == "android" and python_abi_tags is None: + elif (platform == "android" and python_abi_tags is None) or ( + platform == "ios" and python_abi_tags is None + ): python_abi_tags = [ "cp313-cp313", "cp314-cp314", ] if EnableGroup.CPythonPrerelease in enable_groups: python_abi_tags += ["cp315-cp315"] - elif platform == "ios" and python_abi_tags is None: - python_abi_tags = [ - "cp313-cp313", - "cp314-cp314", - ] elif python_abi_tags is None: python_abi_tags = [ "cp39-cp39",