feat: add CPython 3.15 support for iOS (#2857)

* feat: add CPython 3.15 support for iOS

Assisted-by: OpenCode:Kimi-K2.6
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

* fix: copy in iOS support files

Assisted-by: Copilot:claude-sonnet-4.6
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

* fix: skip directories when copying iOS support files

When copying multiarch-specific support files (e.g. _cross_arm64_iphoneos.py),
the code was trying to copy all items in the directory including __pycache__
directories. The shutil.copy() function only works with files, not directories,
which caused an IsADirectoryError.

This fix adds a check to only copy files, skipping any directories like
__pycache__ that may have been created by Python imports.

Assisted-by: Copilot:claude-haiku-4.5

* fix: - in dir fine for now

Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

* fix: apply review suggestions

Assisted-by: Copilot:claude-sonnet-4.6
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

* fix: only do this on 3.15+

Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

* Ensure target Python directory exists before build

Add assertion to check if target Python directory exists.

* fix(ios): use stdlib dir directly instead of copying to platform-config

Remove the _inject_support_files workaround for python.org 3.15+
distributions. Instead of copying sysconfig files from the stdlib into
a synthetic platform-config/ directory, pass the stdlib directory
directly to make_cross_venv.py.

make_cross_venv.py is updated to derive the multiarch tag from the
_sysconfigdata_ filename rather than assuming it comes from the
directory name.

Co-authored-by: Russell Keith-Magee <russell@keith-magee.com>
Assisted-by: OpenCode:Kimi-K2.6
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>

* Minor format cleanups on pre-commit skipped files.

* Clarify path names in make_cross_venv script.

---------

Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
Co-authored-by: Russell Keith-Magee <russell@keith-magee.com>
This commit is contained in:
Henry Schreiner
2026-05-23 10:47:49 -04:00
committed by GitHub
co-authored by Russell Keith-Magee
parent a7b3f178c3
commit 45cfffd3ee
16 changed files with 638 additions and 25 deletions
+1
View File
@@ -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
+7 -7
View File
@@ -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<br/>musllinux x86_64 | manylinux<br/>musllinux i686 | manylinux<br/>musllinux aarch64 | manylinux<br/>musllinux ppc64le | manylinux<br/>musllinux s390x | manylinux<br/>musllinux armv7l | Android | iOS | Pyodide |
| ------------------------ | ----------- | ------------------- | ------------- | ------------- | -------------- | ------------------------------ | ---------------------------- | ------------------------------- | ------------------------------- | ----------------------------- | ------------------------------ | ------- | --- | -------------- |
| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | N/A |
| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | N/A |
| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | N/A |
| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | ✅<sup>3</sup> |
| CPython 3.13 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | ✅ | ✅ | ✅ |
| CPython 3.14 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | ✅ | ✅ | ✅<sup>4</sup> |
| CPython 3.15<sup>6</sup> | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | ✅ | N/A | N/A |
| CPython 3.9 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | N/A |
| CPython 3.10 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | N/A |
| CPython 3.11 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | N/A |
| CPython 3.12 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | N/A | N/A | ✅<sup>3</sup> |
| CPython 3.13 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | ✅ | ✅ | ✅ |
| CPython 3.14 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | ✅ | ✅ | ✅<sup>4</sup> |
| CPython 3.15<sup>6</sup> | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | ✅ | | N/A |
| PyPy 3.9 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅<sup>1</sup> | N/A | N/A | N/A | N/A | N/A | N/A |
| PyPy 3.10 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅<sup>1</sup> | N/A | N/A | N/A | N/A | N/A | N/A |
| PyPy 3.11 v7.3 | ✅ | ✅ | ✅ | N/A | N/A | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅<sup>1</sup> | N/A | N/A | N/A | N/A | N/A | N/A |
+8 -1
View File
@@ -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"]
+30 -10
View File
@@ -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-<arch>/python<version>); 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...")
@@ -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" },
]
@@ -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())
@@ -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()
@@ -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)
@@ -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()
@@ -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)
@@ -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 <venv> (<sysconfig path>) (<platform config path>)
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.
""")
@@ -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()
@@ -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)
+1
View File
@@ -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
+4 -1
View File
@@ -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 = [
+3 -6
View File
@@ -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",