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:
co-authored by
Russell Keith-Magee
parent
a7b3f178c3
commit
45cfffd3ee
@@ -1,5 +1,6 @@
|
|||||||
ci:
|
ci:
|
||||||
autoupdate_schedule: monthly
|
autoupdate_schedule: monthly
|
||||||
|
exclude: "^cibuildwheel/resources/ios-support/(?!make_cross_venv\\.py)"
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
|||||||
@@ -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 |
|
| | 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.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.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.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.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.13 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | ✅ | ✅ | ✅ |
|
||||||
| CPython 3.14 | ✅ | ✅ | ✅ | ✅ | ✅<sup>2</sup> | ✅ | ✅ | ✅ | ✅ | ✅ | ✅<sup>5</sup> | ✅ | ✅ | ✅<sup>4</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.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.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.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 |
|
| 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 |
|
||||||
|
|||||||
@@ -488,7 +488,14 @@ class AllVersions:
|
|||||||
versions = self.maven if identifier.startswith("cp313") else self.cpython
|
versions = self.maven if identifier.startswith("cp313") else self.cpython
|
||||||
config_update = versions.update_version_android(identifier, spec)
|
config_update = versions.update_version_android(identifier, spec)
|
||||||
elif "ios" in identifier:
|
elif "ios" in identifier:
|
||||||
|
# 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)
|
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:
|
elif "pyodide" in identifier:
|
||||||
config_update = self.pyodide.update_version_pyodide(
|
config_update = self.pyodide.update_version_pyodide(
|
||||||
identifier, version, spec, config["node_version"]
|
identifier, version, spec, config["node_version"]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from pathlib import Path
|
|||||||
from typing import assert_never
|
from typing import assert_never
|
||||||
|
|
||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
|
from packaging.version import Version
|
||||||
|
|
||||||
from cibuildwheel import errors
|
from cibuildwheel import errors
|
||||||
from cibuildwheel.architecture import Architecture
|
from cibuildwheel.architecture import Architecture
|
||||||
@@ -183,14 +184,33 @@ def cross_virtualenv(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Convert the macOS virtual environment into an iOS virtual environment
|
# 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;
|
# target_python is the path to the Python binary;
|
||||||
# determine the root of the XCframework slice that is being used.
|
# determine the root of the XCframework slice that is being used.
|
||||||
slice_path = target_python.parent.parent
|
slice_path = target_python.parent.parent
|
||||||
|
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(
|
call(
|
||||||
"python",
|
"python",
|
||||||
str(slice_path / f"platform-config/{multiarch}/make_cross_venv.py"),
|
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),
|
str(venv_path),
|
||||||
env=env,
|
env=env,
|
||||||
cwd=venv_path,
|
cwd=venv_path,
|
||||||
@@ -312,8 +332,8 @@ def setup_python(
|
|||||||
/ f"python{python_configuration.version}"
|
/ f"python{python_configuration.version}"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert target_python.exists(), (
|
assert target_python.parent.exists(), (
|
||||||
f"{target_python.name} not found, has {list(target_install_path.iterdir())}"
|
f"{target_python.parent} not found, has {list(target_install_path.iterdir())}"
|
||||||
)
|
)
|
||||||
|
|
||||||
log.step("Creating cross build environment...")
|
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_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_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 = "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)
|
||||||
+79
@@ -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.
|
||||||
|
""")
|
||||||
+79
@@ -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)
|
||||||
@@ -18,6 +18,7 @@ VIRTUALENV: Final[Path] = PATH / "virtualenv.toml"
|
|||||||
CIBUILDWHEEL_SCHEMA: Final[Path] = PATH / "cibuildwheel.schema.json"
|
CIBUILDWHEEL_SCHEMA: Final[Path] = PATH / "cibuildwheel.schema.json"
|
||||||
PYTHON_BUILD_STANDALONE_RELEASES: Final[Path] = PATH / "python-build-standalone-releases.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"
|
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
|
# this value is cached because it's used a lot in unit tests
|
||||||
|
|||||||
+4
-1
@@ -243,7 +243,10 @@ flake8-annotations.mypy-init-return = true
|
|||||||
ignore = ["PC170", "PP303"]
|
ignore = ["PC170", "PP303"]
|
||||||
|
|
||||||
[tool.check-wheel-contents]
|
[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]
|
[tool.codespell]
|
||||||
ignore-words-list = [
|
ignore-words-list = [
|
||||||
|
|||||||
+3
-6
@@ -270,18 +270,15 @@ def _expected_wheels(
|
|||||||
python_abi_tags = ["cp313-cp313"]
|
python_abi_tags = ["cp313-cp313"]
|
||||||
if EnableGroup.PyodidePrerelease in enable_groups:
|
if EnableGroup.PyodidePrerelease in enable_groups:
|
||||||
python_abi_tags.append("cp314-cp314")
|
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 = [
|
python_abi_tags = [
|
||||||
"cp313-cp313",
|
"cp313-cp313",
|
||||||
"cp314-cp314",
|
"cp314-cp314",
|
||||||
]
|
]
|
||||||
if EnableGroup.CPythonPrerelease in enable_groups:
|
if EnableGroup.CPythonPrerelease in enable_groups:
|
||||||
python_abi_tags += ["cp315-cp315"]
|
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:
|
elif python_abi_tags is None:
|
||||||
python_abi_tags = [
|
python_abi_tags = [
|
||||||
"cp39-cp39",
|
"cp39-cp39",
|
||||||
|
|||||||
Reference in New Issue
Block a user