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",