diff --git a/Makefile b/Makefile index 44cf4ad..d0f36fa 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ BUILD_NUMBER=custom # of a release cycle, as official binaries won't be published. # PYTHON_MICRO_VERSION is the full version number, without any alpha/beta/rc suffix. (e.g., 3.10.0) # PYTHON_VER is the major/minor version (e.g., 3.10) -PYTHON_VERSION=3.14.0rc3 +PYTHON_VERSION=3.14.0 PYTHON_PKG_VERSION=$(PYTHON_VERSION) PYTHON_MICRO_VERSION=$(shell echo $(PYTHON_VERSION) | grep -Eo "\d+\.\d+\.\d+") PYTHON_PKG_MICRO_VERSION=$(shell echo $(PYTHON_PKG_VERSION) | grep -Eo "\d+\.\d+\.\d+") diff --git a/patch/Python/Python.patch b/patch/Python/Python.patch index 89f2a54..e40920c 100644 --- a/patch/Python/Python.patch +++ b/patch/Python/Python.patch @@ -1,86 +1,34 @@ ---- /dev/null +diff --git a/Apple/__main__.py b/Apple/__main__.py +index 96c2d34fbe0..eea74ee2209 100644 +--- a/Apple/__main__.py +++ b/Apple/__main__.py -@@ -0,0 +1,1057 @@ -+#!/usr/bin/env python3 -+########################################################################## -+# Apple XCframework build script -+# -+# This script simplifies the process of configuring, compiling and packaging an -+# XCframework for an Apple platform. -+# +@@ -5,17 +5,19 @@ + # This script simplifies the process of configuring, compiling and packaging an + # XCframework for an Apple platform. + # +-# At present, it only supports iOS, but it has been constructed so that it +-# could be used on any Apple platform. +# At present, it supports iOS, tvOS, visionOS and watchOS, but it has been +# constructed so that it could be used on any Apple platform. -+# -+# The simplest entry point is: -+# -+# $ python Apple ci iOS -+# + # + # The simplest entry point is: + # + # $ python Apple ci iOS + # +# (replace iOS with tvOS, visionOS or watchOS as required.) +# -+# which will: -+# * Clean any pre-existing build artefacts -+# * Configure and make a Python that can be used for the build + # which will: + # * Clean any pre-existing build artefacts + # * Configure and make a Python that can be used for the build +-# * Configure and make a Python for each supported iOS architecture and ABI +# * Configure and make a Python for each supported iOS/tvOS architecture and ABI -+# * Combine the outputs of the builds from the previous step into a single -+# XCframework, merging binaries into a "fat" binary if necessary -+# * Clone a copy of the testbed, configured to use the XCframework -+# * Construct a tarball containing the release artefacts -+# * Run the test suite using the generated XCframework. -+# -+# This is the complete sequence that would be needed in CI to build and test -+# a candidate release artefact. -+# -+# Each individual step can be invoked individually - there are commands to -+# clean, configure-build, make-build, configure-host, make-host, package, and -+# test. -+# -+# There is also a build command that can be used to combine the configure and -+# make steps for the build Python, an individual host, all hosts, or all -+# builds. -+########################################################################## -+from __future__ import annotations -+ -+import argparse -+import os -+import platform -+import re -+import shlex -+import shutil -+import signal -+import subprocess -+import sys -+import sysconfig -+import time -+from collections.abc import Sequence -+from contextlib import contextmanager -+from datetime import datetime, timezone -+from os.path import basename, relpath -+from pathlib import Path -+from subprocess import CalledProcessError -+from typing import Callable -+ -+EnvironmentT = dict[str, str] -+ArgsT = Sequence[str | Path] -+ -+SCRIPT_NAME = Path(__file__).name -+PYTHON_DIR = Path(__file__).resolve().parent.parent -+ -+CROSS_BUILD_DIR = PYTHON_DIR / "cross-build" -+ -+HOSTS: dict[str, dict[str, dict[str, str]]] = { -+ # Structure of this data: -+ # * Platform identifier -+ # * an XCframework slice that must exist for that platform -+ # * a host triple: the multiarch spec for that host -+ "iOS": { -+ "ios-arm64": { -+ "arm64-apple-ios": "arm64-iphoneos", -+ }, -+ "ios-arm64_x86_64-simulator": { -+ "arm64-apple-ios-simulator": "arm64-iphonesimulator", -+ "x86_64-apple-ios-simulator": "x86_64-iphonesimulator", -+ }, -+ }, + # * Combine the outputs of the builds from the previous step into a single + # XCframework, merging binaries into a "fat" binary if necessary + # * Clone a copy of the testbed, configured to use the XCframework +@@ -76,6 +78,32 @@ + "x86_64-apple-ios-simulator": "x86_64-iphonesimulator", + }, + }, + "tvOS": { + "tvos-arm64": { + "arm64-apple-tvos": "arm64-appletvos", @@ -107,67 +55,13 @@ + "x86_64-apple-watchos-simulator": "x86_64-watchsimulator", + }, + }, -+} -+ -+ -+def subdir(name: str, create: bool = False) -> Path: -+ """Ensure that a cross-build directory for the given name exists.""" -+ path = CROSS_BUILD_DIR / name -+ if not path.exists(): -+ if not create: -+ sys.exit( -+ f"{path} does not exist. Create it by running the appropriate " -+ f"`configure` subcommand of {SCRIPT_NAME}." -+ ) -+ else: -+ path.mkdir(parents=True) -+ return path -+ -+ -+def run( -+ command: ArgsT, -+ *, -+ host: str | None = None, -+ env: EnvironmentT | None = None, -+ log: bool | None = True, -+ **kwargs, -+) -> subprocess.CompletedProcess: -+ """Run a command in an Apple development environment. -+ -+ Optionally logs the executed command to the console. -+ """ -+ kwargs.setdefault("check", True) -+ if env is None: -+ env = os.environ.copy() -+ -+ if host: -+ host_env = apple_env(host) -+ print_env(host_env) -+ env.update(host_env) -+ -+ if log: -+ print(">", join_command(command)) -+ return subprocess.run(command, env=env, **kwargs) -+ -+ -+def join_command(args: str | Path | ArgsT) -> str: -+ """Format a command so it can be copied into a shell. -+ -+ Similar to `shlex.join`, but also accepts arguments which are Paths, or a -+ single string/Path outside of a list. -+ """ -+ if isinstance(args, (str, Path)): -+ return str(args) -+ else: -+ return shlex.join(map(str, args)) -+ -+ -+def print_env(env: EnvironmentT) -> None: -+ """Format the environment so it can be pasted into a shell.""" -+ for key, value in sorted(env.items()): -+ print(f"export {key}={shlex.quote(value)}") -+ -+ + } + + +@@ -137,12 +165,25 @@ + print(f"export {key}={shlex.quote(value)}") + + +def platform_for_host(host): + """Determine the platform for a given host triple.""" + for plat, slices in HOSTS.items(): @@ -178,561 +72,121 @@ + raise KeyError(host) + + -+def apple_env(host: str) -> EnvironmentT: -+ """Construct an Apple development environment for the given host.""" -+ env = { -+ "PATH": ":".join( -+ [ + def apple_env(host: str) -> EnvironmentT: + """Construct an Apple development environment for the given host.""" + env = { + "PATH": ":".join( + [ +- str(PYTHON_DIR / "Apple/iOS/Resources/bin"), + str( + PYTHON_DIR + / f"Apple/{platform_for_host(host)}/Resources/bin" + ), -+ str(subdir(host) / "prefix"), -+ "/usr/bin", -+ "/bin", -+ "/usr/sbin", -+ "/sbin", -+ "/Library/Apple/usr/bin", -+ ] -+ ), -+ } -+ -+ return env -+ -+ -+def delete_path(name: str) -> None: -+ """Delete the named cross-build directory, if it exists.""" -+ path = CROSS_BUILD_DIR / name -+ if path.exists(): -+ print(f"Deleting {path} ...") -+ shutil.rmtree(path) -+ -+ -+def all_host_triples(platform: str) -> list[str]: -+ """Return all host triples for the given platform. -+ -+ The host triples are the platform definitions used as input to configure -+ (e.g., "arm64-apple-ios-simulator"). -+ """ -+ triples = [] -+ for slice_name, slice_parts in HOSTS[platform].items(): -+ triples.extend(list(slice_parts)) -+ return triples -+ -+ -+def clean(context: argparse.Namespace, target: str = "all") -> None: -+ """The implementation of the "clean" command.""" -+ # If we're explicitly targeting the build, there's no platform or -+ # distribution artefacts. If we're cleaning tests, we keep all built -+ # artefacts. Otherwise, the built artefacts must be dirty, so we remove -+ # them. -+ if target not in {"build", "test"}: -+ paths = ["dist", context.platform] + list(HOSTS[context.platform]) -+ else: -+ paths = [] -+ -+ if target in {"all", "build"}: -+ paths.append("build") -+ -+ if target in {"all", "hosts"}: -+ paths.extend(all_host_triples(context.platform)) -+ elif target not in {"build", "test", "package"}: -+ paths.append(target) -+ -+ if target in {"all", "hosts", "test"}: -+ paths.extend( -+ [ -+ path.name -+ for path in CROSS_BUILD_DIR.glob( -+ f"{context.platform}-testbed.*" -+ ) -+ ] -+ ) -+ -+ for path in paths: -+ delete_path(path) -+ -+ -+def build_python_path() -> Path: -+ """The path to the build Python binary.""" -+ build_dir = subdir("build") -+ binary = build_dir / "python" -+ if not binary.is_file(): -+ binary = binary.with_suffix(".exe") -+ if not binary.is_file(): -+ raise FileNotFoundError( -+ f"Unable to find `python(.exe)` in {build_dir}" -+ ) -+ -+ return binary -+ -+ -+@contextmanager -+def group(text: str): -+ """A context manager that outputs a log marker around a section of a build. -+ -+ If running in a GitHub Actions environment, the GitHub syntax for -+ collapsible log sections is used. -+ """ -+ if "GITHUB_ACTIONS" in os.environ: -+ print(f"::group::{text}") -+ else: -+ print(f"===== {text} " + "=" * (70 - len(text))) -+ -+ yield -+ -+ if "GITHUB_ACTIONS" in os.environ: -+ print("::endgroup::") -+ else: -+ print() -+ -+ -+@contextmanager -+def cwd(subdir: Path): -+ """A context manager that sets the current working directory.""" -+ orig = os.getcwd() -+ os.chdir(subdir) -+ yield -+ os.chdir(orig) -+ -+ -+def configure_build_python(context: argparse.Namespace) -> None: -+ """The implementation of the "configure-build" command.""" -+ if context.clean: -+ clean(context, "build") -+ -+ with ( -+ group("Configuring build Python"), -+ cwd(subdir("build", create=True)), -+ ): -+ command = [relpath(PYTHON_DIR / "configure")] -+ if context.args: -+ command.extend(context.args) -+ run(command) -+ -+ -+def make_build_python(context: argparse.Namespace) -> None: -+ """The implementation of the "make-build" command.""" -+ with ( -+ group("Compiling build Python"), -+ cwd(subdir("build")), -+ ): -+ run(["make", "-j", str(os.cpu_count())]) -+ -+ -+def apple_target(host: str) -> str: -+ """Return the Apple platform identifier for a given host triple.""" -+ for _, platform_slices in HOSTS.items(): -+ for slice_name, slice_parts in platform_slices.items(): -+ for host_triple, multiarch in slice_parts.items(): -+ if host == host_triple: -+ return ".".join(multiarch.split("-")[::-1]) -+ -+ raise KeyError(host) -+ -+ -+def apple_multiarch(host: str) -> str: -+ """Return the multiarch descriptor for a given host triple.""" -+ for _, platform_slices in HOSTS.items(): -+ for slice_name, slice_parts in platform_slices.items(): -+ for host_triple, multiarch in slice_parts.items(): -+ if host == host_triple: -+ return multiarch -+ -+ raise KeyError(host) -+ -+ -+def unpack_deps( -+ platform: str, -+ host: str, -+ prefix_dir: Path, -+ cache_dir: Path, -+) -> None: -+ """Unpack binary dependencies into a provided directory. -+ -+ Downloads binaries if they aren't already present. Downloads will be stored -+ in provided cache directory. -+ + str(subdir(host) / "prefix"), + "/usr/bin", + "/bin", +@@ -309,8 +350,8 @@ + Downloads binaries if they aren't already present. Downloads will be stored + in provided cache directory. + +- On iOS, as a safety mechanism, any dynamic libraries will be purged from +- the unpacked dependencies. + On non-macOS platforms, as a safety mechanism, any dynamic libraries will be + purged from the unpacked dependencies. -+ """ -+ deps_url = "https://github.com/beeware/cpython-apple-source-deps/releases/download" -+ for name_ver in [ -+ "BZip2-1.0.8-2", -+ "libFFI-3.4.7-2", -+ "OpenSSL-3.0.17-1", -+ "XZ-5.6.4-2", -+ "mpdecimal-4.0.0-2", -+ "zstd-1.5.7-1", -+ ]: -+ filename = f"{name_ver.lower()}-{apple_target(host)}.tar.gz" -+ archive_path = download( -+ f"{deps_url}/{name_ver}/{filename}", -+ target_dir=cache_dir, -+ ) -+ shutil.unpack_archive(archive_path, prefix_dir) -+ + """ + # To create new builds of these dependencies, usually all that's necessary + # is to push a tag to the cpython-apple-source-deps repository, and GitHub +@@ -335,9 +376,9 @@ + ) + shutil.unpack_archive(archive_path, prefix_dir) + +- # Dynamic libraries will be preferentially linked over static; +- # On iOS, ensure that no dylibs are available in the prefix folder. +- if platform == "iOS": + # Dynamic libraries will be preferentially linked over static; On non-macOS + # platforms, ensure that no dylibs are available in the prefix folder. + if platform != "macOS": -+ for dylib in prefix_dir.glob("**/*.dylib"): -+ dylib.unlink() -+ -+ -+def download(url: str, target_dir: Path) -> Path: -+ """Download the specified URL into the given directory. -+ -+ :return: The path to the downloaded archive. -+ """ -+ target_path = Path(target_dir).resolve() -+ target_path.mkdir(exist_ok=True, parents=True) -+ -+ out_path = target_path / basename(url) -+ if not Path(out_path).is_file(): -+ run( -+ [ -+ "curl", -+ "-Lf", -+ "--retry", -+ "5", -+ "--retry-all-errors", -+ "-o", -+ out_path, -+ url, -+ ] -+ ) -+ else: -+ print(f"Using cached version of {basename(url)}") -+ return out_path -+ -+ -+def configure_host_python( -+ context: argparse.Namespace, -+ host: str | None = None, -+) -> None: -+ """The implementation of the "configure-host" command.""" -+ if host is None: -+ host = context.host -+ -+ if context.clean: -+ clean(context, host) -+ -+ host_dir = subdir(host, create=True) -+ prefix_dir = host_dir / "prefix" -+ -+ with group(f"Downloading dependencies ({host})"): -+ if not prefix_dir.exists(): -+ prefix_dir.mkdir() -+ unpack_deps(context.platform, host, prefix_dir, context.cache_dir) -+ else: -+ print("Dependencies already installed") -+ -+ with ( -+ group(f"Configuring host Python ({host})"), -+ cwd(host_dir), -+ ): -+ command = [ -+ # Basic cross-compiling configuration -+ relpath(PYTHON_DIR / "configure"), -+ f"--host={host}", -+ f"--build={sysconfig.get_config_var('BUILD_GNU_TYPE')}", -+ f"--with-build-python={build_python_path()}", -+ "--with-system-libmpdec", + for dylib in prefix_dir.glob("**/*.dylib"): + dylib.unlink() + +@@ -401,6 +442,7 @@ + f"--build={sysconfig.get_config_var('BUILD_GNU_TYPE')}", + f"--with-build-python={build_python_path()}", + "--with-system-libmpdec", + "--enable-ipv6", -+ "--enable-framework", -+ # Dependent libraries. -+ f"--with-openssl={prefix_dir}", -+ f"LIBLZMA_CFLAGS=-I{prefix_dir}/include", -+ f"LIBLZMA_LIBS=-L{prefix_dir}/lib -llzma", -+ f"LIBFFI_CFLAGS=-I{prefix_dir}/include", -+ f"LIBFFI_LIBS=-L{prefix_dir}/lib -lffi", -+ f"LIBMPDEC_CFLAGS=-I{prefix_dir}/include", -+ f"LIBMPDEC_LIBS=-L{prefix_dir}/lib -lmpdec", -+ f"LIBZSTD_CFLAGS=-I{prefix_dir}/include", -+ f"LIBZSTD_LIBS=-L{prefix_dir}/lib -lzstd", -+ ] -+ -+ if context.args: -+ command.extend(context.args) -+ run(command, host=host) -+ -+ -+def make_host_python( -+ context: argparse.Namespace, -+ host: str | None = None, -+) -> None: -+ """The implementation of the "make-host" command.""" -+ if host is None: -+ host = context.host -+ -+ with ( -+ group(f"Compiling host Python ({host})"), -+ cwd(subdir(host)), -+ ): -+ run(["make", "-j", str(os.cpu_count())], host=host) -+ run(["make", "install"], host=host) -+ -+ -+def framework_path(host_triple: str, multiarch: str) -> Path: -+ """The path to a built single-architecture framework product. -+ -+ :param host_triple: The host triple (e.g., arm64-apple-ios-simulator) -+ :param multiarch: The multiarch identifier (e.g., arm64-simulator) -+ """ + "--enable-framework", + # Dependent libraries. + f"--with-openssl={prefix_dir}", +@@ -441,7 +483,10 @@ + :param host_triple: The host triple (e.g., arm64-apple-ios-simulator) + :param multiarch: The multiarch identifier (e.g., arm64-simulator) + """ +- return CROSS_BUILD_DIR / f"{host_triple}/Apple/iOS/Frameworks/{multiarch}" + return ( + CROSS_BUILD_DIR + / f"{host_triple}/Apple/{platform_for_host(host_triple)}/Frameworks/{multiarch}" + ) -+ -+ -+def package_version(prefix_path: Path) -> str: -+ """Extract the Python version being built from patchlevel.h.""" -+ for path in prefix_path.glob("**/patchlevel.h"): -+ text = path.read_text(encoding="utf-8") -+ if match := re.search( -+ r'\n\s*#define\s+PY_VERSION\s+"(.+)"\s*\n', text -+ ): -+ version = match[1] -+ # If not building against a tagged commit, add a timestamp to the -+ # version. Follow the PyPA version number rules, as this will make -+ # it easier to process with other tools. The version will have a -+ # `+` suffix once any official release has been made; a freshly -+ # forked main branch will have a version of 3.X.0a0. -+ if version.endswith("a0"): -+ version += "+" -+ if version.endswith("+"): -+ version += datetime.now(timezone.utc).strftime("%Y%m%d.%H%M%S") -+ -+ return version -+ -+ sys.exit("Unable to determine Python version being packaged.") -+ -+ -+def lib_platform_files(dirname, names): + + + def package_version(prefix_path: Path) -> str: +@@ -468,8 +513,7 @@ + + + def lib_platform_files(dirname, names): +- """A file filter that ignores platform-specific files in the lib directory. +- """ + """A file filter that ignores platform-specific files in the lib directory.""" -+ path = Path(dirname) -+ if ( -+ path.parts[-3] == "lib" -+ and path.parts[-2].startswith("python") -+ and path.parts[-1] == "lib-dynload" -+ ): -+ return names -+ elif path.parts[-2] == "lib" and path.parts[-1].startswith("python"): -+ ignored_names = set( -+ name -+ for name in names -+ if ( -+ name.startswith("_sysconfigdata_") -+ or name.startswith("_sysconfig_vars_") -+ or name == "build-details.json" -+ ) -+ ) -+ else: -+ ignored_names = set() -+ -+ return ignored_names -+ -+ -+def lib_non_platform_files(dirname, names): -+ """A file filter that ignores anything *except* platform-specific files -+ in the lib directory. -+ """ -+ path = Path(dirname) -+ if path.parts[-2] == "lib" and path.parts[-1].startswith("python"): + path = Path(dirname) + if ( + path.parts[-3] == "lib" +@@ -499,7 +543,9 @@ + """ + path = Path(dirname) + if path.parts[-2] == "lib" and path.parts[-1].startswith("python"): +- return set(names) - lib_platform_files(dirname, names) - {"lib-dynload"} + return ( + set(names) - lib_platform_files(dirname, names) - {"lib-dynload"} + ) -+ else: -+ return set() -+ -+ -+def create_xcframework(platform: str) -> str: -+ """Build an XCframework from the component parts for the platform. -+ -+ :return: The version number of the Python verion that was packaged. -+ """ -+ package_path = CROSS_BUILD_DIR / platform -+ try: -+ package_path.mkdir() -+ except FileExistsError: -+ raise RuntimeError( -+ f"{platform} XCframework already exists; do you need to run with --clean?" -+ ) from None -+ -+ frameworks = [] -+ # Merge Frameworks for each component SDK. If there's only one architecture -+ # for the SDK, we can use the compiled Python.framework as-is. However, if -+ # there's more than architecture, we need to merge the individual built -+ # frameworks into a merged "fat" framework. -+ for slice_name, slice_parts in HOSTS[platform].items(): -+ # Some parts are the same across all slices, so we use can any of the -+ # host frameworks as the source for the merged version. Use the first -+ # one on the list, as it's as representative as any other. -+ first_host_triple, first_multiarch = next(iter(slice_parts.items())) -+ first_framework = ( -+ framework_path(first_host_triple, first_multiarch) -+ / "Python.framework" -+ ) -+ -+ if len(slice_parts) == 1: -+ # The first framework is the only framework, so copy it. -+ print(f"Copying framework for {slice_name}...") -+ frameworks.append(first_framework) -+ else: -+ print(f"Merging framework for {slice_name}...") -+ slice_path = CROSS_BUILD_DIR / slice_name -+ slice_framework = slice_path / "Python.framework" -+ slice_framework.mkdir(exist_ok=True, parents=True) -+ -+ # Copy the Info.plist -+ shutil.copy( -+ first_framework / "Info.plist", -+ slice_framework / "Info.plist", -+ ) -+ -+ # Copy the headers -+ shutil.copytree( -+ first_framework / "Headers", -+ slice_framework / "Headers", -+ ) -+ -+ # Create the "fat" library binary for the slice -+ run( -+ ["lipo", "-create", "-output", slice_framework / "Python"] -+ + [ -+ ( -+ framework_path(host_triple, multiarch) -+ / "Python.framework/Python" -+ ) -+ for host_triple, multiarch in slice_parts.items() -+ ] -+ ) -+ -+ # Add this merged slice to the list to be added to the XCframework -+ frameworks.append(slice_framework) -+ -+ print() -+ print("Build XCframework...") -+ cmd = [ -+ "xcodebuild", -+ "-create-xcframework", -+ "-output", -+ package_path / "Python.xcframework", -+ ] -+ for framework in frameworks: -+ cmd.extend(["-framework", framework]) -+ -+ run(cmd) -+ -+ # Extract the package version from the merged framework -+ version = package_version(package_path / "Python.xcframework") -+ version_tag = ".".join(version.split(".")[:2]) -+ -+ # On non-macOS platforms, each framework in XCframework only contains the -+ # headers, libPython, plus an Info.plist. Other resources like the standard -+ # library and binary shims aren't allowed to live in framework; they need -+ # to be copied in separately. -+ print() -+ print("Copy additional resources...") -+ has_common_stdlib = False -+ for slice_name, slice_parts in HOSTS[platform].items(): -+ # Some parts are the same across all slices, so we can any of the -+ # host frameworks as the source for the merged version. -+ first_host_triple, first_multiarch = next(iter(slice_parts.items())) -+ first_path = framework_path(first_host_triple, first_multiarch) -+ first_framework = first_path / "Python.framework" -+ -+ slice_path = package_path / f"Python.xcframework/{slice_name}" -+ slice_framework = slice_path / "Python.framework" -+ -+ # Copy the binary helpers -+ print(f" - {slice_name} binaries") -+ shutil.copytree(first_path / "bin", slice_path / "bin") -+ -+ # Copy the include path (this will be a symlink to the framework headers) -+ print(f" - {slice_name} include files") -+ shutil.copytree( -+ first_path / "include", -+ slice_path / "include", -+ symlinks=True, -+ ) -+ -+ # Copy in the cross-architecture pyconfig.h -+ shutil.copy( -+ PYTHON_DIR / f"Apple/{platform}/Resources/pyconfig.h", -+ slice_framework / "Headers/pyconfig.h", -+ ) -+ -+ print(f" - {slice_name} architecture-specific files") -+ for host_triple, multiarch in slice_parts.items(): -+ print(f" - {multiarch} standard library") -+ arch, _ = multiarch.split("-", 1) -+ -+ if not has_common_stdlib: -+ print(" - using this architecture as the common stdlib") -+ shutil.copytree( -+ framework_path(host_triple, multiarch) / "lib", -+ package_path / "Python.xcframework/lib", -+ ignore=lib_platform_files, -+ ) -+ has_common_stdlib = True -+ -+ shutil.copytree( -+ framework_path(host_triple, multiarch) / "lib", -+ slice_path / f"lib-{arch}", -+ ignore=lib_non_platform_files, -+ ) -+ -+ # Copy the host's pyconfig.h to an architecture-specific name. -+ arch = multiarch.split("-")[0] -+ host_path = ( -+ CROSS_BUILD_DIR -+ / host_triple + else: + return set() + +@@ -646,7 +692,7 @@ + host_path = ( + CROSS_BUILD_DIR + / host_triple +- / "Apple/iOS/Frameworks" + / f"Apple/{platform}/Frameworks" -+ / multiarch -+ ) -+ host_framework = host_path / "Python.framework" -+ shutil.copy( -+ host_framework / "Headers/pyconfig.h", -+ slice_framework / f"Headers/pyconfig-{arch}.h", -+ ) -+ -+ # Apple identifies certain libraries as "security risks"; if you -+ # statically link those libraries into a Framework, you become -+ # responsible for providing a privacy manifest for that framework. -+ xcprivacy_file = { + / multiarch + ) + host_framework = host_path / "Python.framework" +@@ -659,7 +705,8 @@ + # statically link those libraries into a Framework, you become + # responsible for providing a privacy manifest for that framework. + xcprivacy_file = { +- "OpenSSL": subdir(host_triple) / "prefix/share/OpenSSL.xcprivacy" + "OpenSSL": subdir(host_triple) + / "prefix/share/OpenSSL.xcprivacy" -+ } -+ print(f" - {multiarch} xcprivacy files") -+ for module, lib in [ -+ ("_hashlib", "OpenSSL"), -+ ("_ssl", "OpenSSL"), -+ ]: -+ shutil.copy( -+ xcprivacy_file[lib], -+ slice_path -+ / f"lib-{arch}/python{version_tag}/lib-dynload/{module}.xcprivacy", -+ ) -+ -+ print(" - build tools") -+ shutil.copytree( -+ PYTHON_DIR / "Apple/testbed/Python.xcframework/build", -+ package_path / "Python.xcframework/build", -+ ) -+ -+ return version -+ -+ -+def package(context: argparse.Namespace) -> None: -+ """The implementation of the "package" command.""" -+ if context.clean: -+ clean(context, "package") -+ -+ with group("Building package"): -+ # Create an XCframework -+ version = create_xcframework(context.platform) -+ + } + print(f" - {multiarch} xcprivacy files") + for module, lib in [ +@@ -690,20 +737,22 @@ + # Create an XCframework + version = create_xcframework(context.platform) + +- # Clone testbed +- print() +- run( +- [ +- sys.executable, +- "Apple/testbed", +- "clone", +- "--platform", +- context.platform, +- "--framework", +- CROSS_BUILD_DIR / context.platform / "Python.xcframework", +- CROSS_BUILD_DIR / context.platform / "testbed", +- ] +- ) + # watchOS doesn't have a testbed (yet!) + if context.platform != "watchOS": + # Clone testbed @@ -749,808 +203,37 @@ + CROSS_BUILD_DIR / context.platform / "testbed", + ] + ) -+ -+ # Build the final archive -+ archive_name = ( -+ CROSS_BUILD_DIR -+ / "dist" -+ / f"python-{version}-{context.platform}-XCframework" -+ ) -+ -+ print() -+ print("Create package archive...") -+ shutil.make_archive( -+ str(CROSS_BUILD_DIR / archive_name), -+ format="gztar", -+ root_dir=CROSS_BUILD_DIR / context.platform, -+ base_dir=".", -+ ) -+ print() -+ print(f"{archive_name.relative_to(PYTHON_DIR)}.tar.gz created.") -+ -+ -+def build(context: argparse.Namespace, host: str | None = None) -> None: -+ """The implementation of the "build" command.""" -+ if host is None: -+ host = context.host -+ -+ if context.clean: -+ clean(context, host) -+ -+ if host in {"all", "build"}: -+ for step in [ -+ configure_build_python, -+ make_build_python, -+ ]: -+ step(context) -+ -+ if host == "build": -+ hosts = [] -+ elif host in {"all", "hosts"}: -+ hosts = all_host_triples(context.platform) -+ else: -+ hosts = [host] -+ -+ for step_host in hosts: -+ for step in [ -+ configure_host_python, -+ make_host_python, -+ ]: -+ step(context, host=step_host) -+ -+ if host in {"all", "hosts"}: -+ package(context) -+ -+ -+def test(context: argparse.Namespace, host: str | None = None) -> None: -+ """The implementation of the "test" command.""" -+ if host is None: -+ host = context.host -+ -+ if context.clean: -+ clean(context, "test") -+ -+ with group(f"Test {'XCframework' if host in {'all', 'hosts'} else host}"): -+ timestamp = str(time.time_ns())[:-6] -+ testbed_dir = ( -+ CROSS_BUILD_DIR / f"{context.platform}-testbed.{timestamp}" -+ ) -+ if host in {"all", "hosts"}: -+ framework_path = ( -+ CROSS_BUILD_DIR / context.platform / "Python.xcframework" -+ ) -+ else: -+ build_arch = platform.machine() -+ host_arch = host.split("-")[0] -+ -+ if not host.endswith("-simulator"): -+ print("Skipping test suite non-simulator build.") -+ return -+ elif build_arch != host_arch: -+ print( -+ f"Skipping test suite for an {host_arch} build " -+ f"on an {build_arch} machine." -+ ) -+ return -+ else: -+ framework_path = ( -+ CROSS_BUILD_DIR -+ / host -+ / f"Apple/{context.platform}" -+ / f"Frameworks/{apple_multiarch(host)}" -+ ) -+ -+ run( -+ [ -+ sys.executable, -+ "Apple/testbed", -+ "clone", -+ "--platform", -+ context.platform, -+ "--framework", -+ framework_path, -+ testbed_dir, -+ ] -+ ) -+ -+ run( -+ [ -+ sys.executable, -+ testbed_dir, -+ "run", -+ "--verbose", -+ ] -+ + ( -+ ["--simulator", str(context.simulator)] -+ if context.simulator -+ else [] -+ ) -+ + [ -+ "--", -+ "test", -+ "--slow-ci" if context.slow else "--fast-ci", -+ "--single-process", -+ "--no-randomize", -+ # Timeout handling requires subprocesses; explicitly setting -+ # the timeout to -1 disables the faulthandler. -+ "--timeout=-1", -+ # Adding Python options requires the use of a subprocess to -+ # start a new Python interpreter. -+ "--dont-add-python-opts", -+ ] -+ ) -+ -+ -+def ci(context: argparse.Namespace) -> None: -+ """The implementation of the "ci" command.""" -+ clean(context, "all") -+ build(context, host="all") -+ test(context, host="all") -+ -+ -+def parse_args() -> argparse.Namespace: -+ parser = argparse.ArgumentParser( -+ description=( -+ "A tool for managing the build, package and test process of " -+ "CPython on Apple platforms." -+ ), -+ ) -+ parser.suggest_on_error = True -+ subcommands = parser.add_subparsers(dest="subcommand", required=True) -+ -+ clean = subcommands.add_parser( -+ "clean", -+ help="Delete all build directories", -+ ) -+ -+ configure_build = subcommands.add_parser( -+ "configure-build", help="Run `configure` for the build Python" -+ ) -+ subcommands.add_parser( -+ "make-build", help="Run `make` for the build Python" -+ ) -+ configure_host = subcommands.add_parser( -+ "configure-host", -+ help="Run `configure` for a specific platform and target", -+ ) -+ make_host = subcommands.add_parser( -+ "make-host", -+ help="Run `make` for a specific platform and target", -+ ) -+ package = subcommands.add_parser( -+ "package", -+ help="Create a release package for the platform", -+ ) -+ build = subcommands.add_parser( -+ "build", -+ help="Build all platform targets and create the XCframework", -+ ) -+ test = subcommands.add_parser( -+ "test", -+ help="Run the testbed for a specific platform", -+ ) -+ ci = subcommands.add_parser( -+ "ci", -+ help="Run build, package, and test", -+ ) -+ -+ # platform argument -+ for cmd in [clean, configure_host, make_host, package, build, test, ci]: -+ cmd.add_argument( -+ "platform", -+ choices=HOSTS.keys(), -+ help="The target platform to build", -+ ) -+ -+ # host triple argument -+ for cmd in [configure_host, make_host]: -+ cmd.add_argument( -+ "host", -+ help="The host triple to build (e.g., arm64-apple-ios-simulator)", -+ ) -+ # optional host triple argument -+ for cmd in [clean, build, test]: -+ cmd.add_argument( -+ "host", -+ nargs="?", -+ default="all", -+ help=( -+ "The host triple to build (e.g., arm64-apple-ios-simulator), " -+ "or 'build' for just the build platform, or 'hosts' for all " -+ "host platforms, or 'all' for the build platform and all " -+ "hosts. Defaults to 'all'" -+ ), -+ ) -+ -+ # --clean option -+ for cmd in [configure_build, configure_host, build, package, test, ci]: -+ cmd.add_argument( -+ "--clean", -+ action="store_true", -+ default=False, -+ dest="clean", -+ help="Delete the relevant build directories first", -+ ) -+ -+ # --cache-dir option -+ for cmd in [configure_host, build, ci]: -+ cmd.add_argument( -+ "--cache-dir", -+ default="./cross-build/downloads", -+ help="The directory to store cached downloads.", -+ ) -+ -+ # --simulator option -+ for cmd in [test, ci]: -+ cmd.add_argument( -+ "--simulator", -+ help=( -+ "The name of the simulator to use (eg: 'iPhone 16e'). Defaults to " -+ "the most recently released 'entry level' iPhone device. Device " -+ "architecture and OS version can also be specified; e.g., " -+ "`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would run on " -+ "an ARM64 iPhone 16 Pro simulator running iOS 26.0." -+ ), -+ ) -+ cmd.add_argument( -+ "--slow", -+ action="store_true", -+ help="Run tests with --slow-ci options.", -+ ) -+ -+ for subcommand in [configure_build, configure_host, build, ci]: -+ subcommand.add_argument( -+ "args", nargs="*", help="Extra arguments to pass to `configure`" -+ ) -+ -+ return parser.parse_args() -+ -+ -+def print_called_process_error(e: subprocess.CalledProcessError) -> None: -+ for stream_name in ["stdout", "stderr"]: -+ content = getattr(e, stream_name) -+ stream = getattr(sys, stream_name) -+ if content: -+ stream.write(content) -+ if not content.endswith("\n"): -+ stream.write("\n") -+ -+ # shlex uses single quotes, so we surround the command with double quotes. -+ print( -+ f'Command "{join_command(e.cmd)}" returned exit status {e.returncode}' -+ ) -+ -+ -+def main() -> None: -+ # Handle SIGTERM the same way as SIGINT. This ensures that if we're -+ # terminated by the buildbot worker, we'll make an attempt to clean up our -+ # subprocesses. -+ def signal_handler(*args): -+ os.kill(os.getpid(), signal.SIGINT) -+ -+ signal.signal(signal.SIGTERM, signal_handler) -+ -+ # Process command line arguments -+ context = parse_args() -+ dispatch: dict[str, Callable] = { -+ "clean": clean, -+ "configure-build": configure_build_python, -+ "make-build": make_build_python, -+ "configure-host": configure_host_python, -+ "make-host": make_host_python, -+ "package": package, -+ "build": build, -+ "test": test, -+ "ci": ci, -+ } -+ -+ try: -+ dispatch[context.subcommand](context) -+ except CalledProcessError as e: -+ print() -+ print_called_process_error(e) -+ sys.exit(1) -+ except RuntimeError as e: -+ print() -+ print(e) -+ sys.exit(2) -+ -+ -+if __name__ == "__main__": -+ main() ---- /dev/null -+++ b/Apple/iOS/README.md -@@ -0,0 +1,328 @@ -+# Python on iOS README -+ -+**iOS support is [tier 3](https://peps.python.org/pep-0011/#tier-3).** -+ -+This document provides a quick overview of some iOS specific features in the -+Python distribution. -+ -+These instructions are only needed if you're planning to compile Python for iOS -+yourself. Most users should *not* need to do this. If you're looking to -+experiment with writing an iOS app in Python, tools such as [BeeWare's -+Briefcase](https://briefcase.readthedocs.io) and [Kivy's -+Buildozer](https://buildozer.readthedocs.io) will provide a much more -+approachable user experience. -+ -+## Compilers for building on iOS -+ -+Building for iOS requires the use of Apple's Xcode tooling. It is strongly -+recommended that you use the most recent stable release of Xcode. This will -+require the use of the most (or second-most) recently released macOS version, -+as Apple does not maintain Xcode for older macOS versions. The Xcode Command -+Line Tools are not sufficient for iOS development; you need a *full* Xcode -+install. -+ -+If you want to run your code on the iOS simulator, you'll also need to install -+an iOS Simulator Platform. You should be prompted to select an iOS Simulator -+Platform when you first run Xcode. Alternatively, you can add an iOS Simulator -+Platform by selecting an open the Platforms tab of the Xcode Settings panel. -+ -+## Building Python on iOS -+ -+### ABIs and Architectures -+ -+iOS apps can be deployed on physical devices, and on the iOS simulator. Although -+the API used on these devices is identical, the ABI is different - you need to -+link against different libraries for an iOS device build (`iphoneos`) or an -+iOS simulator build (`iphonesimulator`). -+ -+Apple uses the `XCframework` format to allow specifying a single dependency -+that supports multiple ABIs. An `XCframework` is a wrapper around multiple -+ABI-specific frameworks that share a common API. -+ -+iOS can also support different CPU architectures within each ABI. At present, -+there is only a single supported architecture on physical devices - ARM64. -+However, the *simulator* supports 2 architectures - ARM64 (for running on Apple -+Silicon machines), and x86_64 (for running on older Intel-based machines). -+ -+To support multiple CPU architectures on a single platform, Apple uses a "fat -+binary" format - a single physical file that contains support for multiple -+architectures. It is possible to compile and use a "thin" single architecture -+version of a binary for testing purposes; however, the "thin" binary will not be -+portable to machines using other architectures. -+ -+### Building a multi-architecture iOS XCframework -+ -+The `Apple` subfolder of the Python repository acts as a build script that -+can be used to coordinate the compilation of a complete iOS XCframework. To use -+it, run:: -+ -+ python Apple build iOS -+ -+This will: -+ -+* Configure and compile a version of Python to run on the build machine -+* Download pre-compiled binary dependencies for each platform -+* Configure and build a `Python.framework` for each required architecture and -+ iOS SDK -+* Merge the multiple `Python.framework` folders into a single `Python.xcframework` -+* Produce a `.tar.gz` archive in the `cross-build/dist` folder containing -+ the `Python.xcframework`, plus a copy of the Testbed app pre-configured to -+ use the XCframework. -+ -+The `Apple` build script has other entry points that will perform the -+individual parts of the overall `build` target, plus targets to test the -+build, clean the `cross-build` folder of iOS build products, and perform a -+complete "build and test" CI run. The `--clean` flag can also be used on -+individual commands to ensure that a stale build product are removed before -+building. -+ -+### Building a single-architecture framework -+ -+If you're using the `Apple` build script, you won't need to build -+individual frameworks. However, if you do need to manually configure an iOS -+Python build for a single framework, the following options are available. -+ -+#### iOS specific arguments to configure -+ -+* `--enable-framework[=DIR]` -+ -+ This argument specifies the location where the Python.framework will be -+ installed. If `DIR` is not specified, the framework will be installed into -+ a subdirectory of the `iOS/Frameworks` folder. -+ -+ This argument *must* be provided when configuring iOS builds. iOS does not -+ support non-framework builds. -+ -+* `--with-framework-name=NAME` -+ -+ Specify the name for the Python framework; defaults to `Python`. -+ -+ > [!NOTE] -+ > Unless you know what you're doing, changing the name of the Python -+ > framework on iOS is not advised. If you use this option, you won't be able -+ > to run the `Apple` build script without making significant manual -+ > alterations, and you won't be able to use any binary packages unless you -+ > compile them yourself using your own framework name. -+ -+#### Building Python for iOS -+ -+The Python build system will create a `Python.framework` that supports a -+*single* ABI with a *single* architecture. Unlike macOS, iOS does not allow a -+framework to contain non-library content, so the iOS build will produce a -+`bin` and `lib` folder in the same output folder as `Python.framework`. -+The `lib` folder will be needed at runtime to support the Python library. -+ -+If you want to use Python in a real iOS project, you need to produce multiple -+`Python.framework` builds, one for each ABI and architecture. iOS builds of -+Python *must* be constructed as framework builds. To support this, you must -+provide the `--enable-framework` flag when configuring the build. The build -+also requires the use of cross-compilation. The minimal commands for building -+Python for the ARM64 iOS simulator will look something like: -+``` -+export PATH="$(pwd)/Apple/iOS/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" -+./configure \ -+ --enable-framework \ -+ --host=arm64-apple-ios-simulator \ -+ --build=arm64-apple-darwin \ -+ --with-build-python=/path/to/python.exe -+make -+make install -+``` -+ -+In this invocation: -+ -+* `Apple/iOS/Resources/bin` has been added to the path, providing some shims for the -+ compilers and linkers needed by the build. Xcode requires the use of `xcrun` -+ to invoke compiler tooling. However, if `xcrun` is pre-evaluated and the -+ result passed to `configure`, these results can embed user- and -+ version-specific paths into the sysconfig data, which limits the portability -+ of the compiled Python. Alternatively, if `xcrun` is used *as* the compiler, -+ it requires that compiler variables like `CC` include spaces, which can -+ cause significant problems with many C configuration systems which assume that -+ `CC` will be a single executable. -+ -+ To work around this problem, the `Apple/iOS/Resources/bin` folder contains some -+ wrapper scripts that present as simple compilers and linkers, but wrap -+ underlying calls to `xcrun`. This allows configure to use a `CC` -+ definition without spaces, and without user- or version-specific paths, while -+ retaining the ability to adapt to the local Xcode install. These scripts are -+ included in the `bin` directory of an iOS install. -+ -+ These scripts will, by default, use the currently active Xcode installation. -+ If you want to use a different Xcode installation, you can use -+ `xcode-select` to set a new default Xcode globally, or you can use the -+ `DEVELOPER_DIR` environment variable to specify an Xcode install. The -+ scripts will use the default `iphoneos`/`iphonesimulator` SDK version for -+ the select Xcode install; if you want to use a different SDK, you can set the -+ `IOS_SDK_VERSION` environment variable. (e.g, setting -+ `IOS_SDK_VERSION=17.1` would cause the scripts to use the `iphoneos17.1` -+ and `iphonesimulator17.1` SDKs, regardless of the Xcode default.) -+ -+ The path has also been cleared of any user customizations. A common source of -+ bugs is for tools like Homebrew to accidentally leak macOS binaries into an iOS -+ build. Resetting the path to a known "bare bones" value is the easiest way to -+ avoid these problems. -+ -+* `--host` is the architecture and ABI that you want to build, in GNU compiler -+ triple format. This will be one of: -+ -+ - `arm64-apple-ios` for ARM64 iOS devices. -+ - `arm64-apple-ios-simulator` for the iOS simulator running on Apple -+ Silicon devices. -+ - `x86_64-apple-ios-simulator` for the iOS simulator running on Intel -+ devices. -+ -+* `--build` is the GNU compiler triple for the machine that will be running -+ the compiler. This is one of: -+ -+ - `arm64-apple-darwin` for Apple Silicon devices. -+ - `x86_64-apple-darwin` for Intel devices. -+ -+* `/path/to/python.exe` is the path to a Python binary on the machine that -+ will be running the compiler. This is needed because the Python compilation -+ process involves running some Python code. On a normal desktop build of -+ Python, you can compile a python interpreter and then use that interpreter to -+ run Python code. However, the binaries produced for iOS won't run on macOS, so -+ you need to provide an external Python interpreter. This interpreter must be -+ the same version as the Python that is being compiled. To be completely safe, -+ this should be the *exact* same commit hash. However, the longer a Python -+ release has been stable, the more likely it is that this constraint can be -+ relaxed - the same micro version will often be sufficient. -+ -+* The `install` target for iOS builds is slightly different to other -+ platforms. On most platforms, `make install` will install the build into -+ the final runtime location. This won't be the case for iOS, as the final -+ runtime location will be on a physical device. -+ -+ However, you still need to run the `install` target for iOS builds, as it -+ performs some final framework assembly steps. The location specified with -+ `--enable-framework` will be the location where `make install` will -+ assemble the complete iOS framework. This completed framework can then -+ be copied and relocated as required. -+ -+For a full CPython build, you also need to specify the paths to iOS builds of -+the binary libraries that CPython depends on (such as XZ, LibFFI and OpenSSL). -+This can be done by defining library specific environment variables (such as -+`LIBLZMA_CFLAGS`, `LIBLZMA_LIBS`), and the `--with-openssl` configure -+option. Versions of these libraries pre-compiled for iOS can be found in [this -+repository](https://github.com/beeware/cpython-apple-source-deps/releases). -+LibFFI is especially important, as many parts of the standard library -+(including the `platform`, `sysconfig` and `webbrowser` modules) require -+the use of the `ctypes` module at runtime. -+ -+By default, Python will be compiled with an iOS deployment target (i.e., the -+minimum supported iOS version) of 13.0. To specify a different deployment -+target, provide the version number as part of the `--host` argument - for -+example, `--host=arm64-apple-ios15.4-simulator` would compile an ARM64 -+simulator build with a deployment target of 15.4. -+ -+## Testing Python on iOS -+ -+### Testing a multi-architecture framework -+ -+Once you have a built an XCframework, you can test that framework by running: -+ -+ $ python Apple test iOS -+ -+### Testing a single-architecture framework -+ -+The `Apple/testbed` folder that contains an Xcode project that is able to run -+the Python test suite on Apple platforms. This project converts the Python test -+suite into a single test case in Xcode's XCTest framework. The single XCTest -+passes if the test suite passes. -+ -+To run the test suite, configure a Python build for an iOS simulator (i.e., -+`--host=arm64-apple-ios-simulator` or `--host=x86_64-apple-ios-simulator` -+), specifying a framework build (i.e. `--enable-framework`). Ensure that your -+`PATH` has been configured to include the `Apple/iOS/Resources/bin` folder and -+exclude any non-iOS tools, then run: -+``` -+make all -+make install -+make testios -+``` -+ -+This will: -+ -+* Build an iOS framework for your chosen architecture; -+* Finalize the single-platform framework; -+* Make a clean copy of the testbed project; -+* Install the Python iOS framework into the copy of the testbed project; and -+* Run the test suite on an "entry-level device" simulator (i.e., an iPhone SE, -+ iPhone 16e, or a similar). -+ -+On success, the test suite will exit and report successful completion of the -+test suite. On a 2022 M1 MacBook Pro, the test suite takes approximately 15 -+minutes to run; a couple of extra minutes is required to compile the testbed -+project, and then boot and prepare the iOS simulator. -+ -+### Debugging test failures -+ -+Running `python Apple test iOS` generates a standalone version of the -+`Apple/testbed` project, and runs the full test suite. It does this using -+`Apple/testbed` itself - the folder is an executable module that can be used -+to create and run a clone of the testbed project. The standalone version of the -+testbed will be created in a directory named -+`cross-build/iOS-testbed.`. -+ -+You can generate your own standalone testbed instance by running: -+``` -+python cross-build/iOS/testbed clone my-testbed -+``` -+ -+In this invocation, `my-testbed` is the name of the folder for the new -+testbed clone. -+ -+If you've built your own XCframework, or you only want to test a single architecture, -+you can construct a standalone testbed instance by running: -+``` -+python Apple/testbed clone --platform iOS --framework my-testbed -+``` -+ -+The framework path can be the path path to a `Python.xcframework`, or the -+path to a folder that contains a single-platform `Python.framework`. -+ -+You can then use the `my-testbed` folder to run the Python test suite, -+passing in any command line arguments you may require. For example, if you're -+trying to diagnose a failure in the `os` module, you might run: -+``` -+python my-testbed run -- test -W test_os -+``` -+ -+This is the equivalent of running `python -m test -W test_os` on a desktop -+Python build. Any arguments after the `--` will be passed to testbed as if -+they were arguments to `python -m` on a desktop machine. -+ -+### Testing in Xcode -+ -+You can also open the testbed project in Xcode by running: -+``` -+open my-testbed/iOSTestbed.xcodeproj -+``` -+ -+This will allow you to use the full Xcode suite of tools for debugging. -+ -+The arguments used to run the test suite are defined as part of the test plan. -+To modify the test plan, select the test plan node of the project tree (it -+should be the first child of the root node), and select the "Configurations" -+tab. Modify the "Arguments Passed On Launch" value to change the testing -+arguments. -+ -+The test plan also disables parallel testing, and specifies the use of the -+`Testbed.lldbinit` file for providing configuration of the debugger. The -+default debugger configuration disables automatic breakpoints on the -+`SIGINT`, `SIGUSR1`, `SIGUSR2`, and `SIGXFSZ` signals. -+ -+### Testing on an iOS device -+ -+To test on an iOS device, the app needs to be signed with known developer -+credentials. To obtain these credentials, you must have an iOS Developer -+account, and your Xcode install will need to be logged into your account (see -+the Accounts tab of the Preferences dialog). -+ -+Once the project is open, and you're signed into your Apple Developer account, -+select the root node of the project tree (labeled "iOSTestbed"), then the -+"Signing & Capabilities" tab in the details page. Select a development team -+(this will likely be your own name), and plug in a physical device to your -+macOS machine with a USB cable. You should then be able to select your physical -+device from the list of targets in the pulldown in the Xcode titlebar. ---- /dev/null + + # Build the final archive + archive_name = ( +diff --git a/Apple/iOS/Resources/Info.plist.in b/Apple/iOS/Resources/Info.plist.in +index c3e261ecd9e..26ef7a95de4 100644 +--- a/Apple/iOS/Resources/Info.plist.in +++ b/Apple/iOS/Resources/Info.plist.in -@@ -0,0 +1,34 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleExecutable -+ Python -+ CFBundleGetInfoString -+ Python Runtime and Library -+ CFBundleIdentifier -+ @PYTHONFRAMEWORKIDENTIFIER@ -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundleName -+ Python -+ CFBundlePackageType -+ FMWK -+ CFBundleShortVersionString +@@ -17,13 +17,13 @@ + CFBundlePackageType + FMWK + CFBundleShortVersionString +- @VERSION@ + %VERSION% -+ CFBundleLongVersionString -+ %VERSION%, (c) 2001-2024 Python Software Foundation. -+ CFBundleSignature -+ ???? -+ CFBundleVersion + CFBundleLongVersionString + %VERSION%, (c) 2001-2024 Python Software Foundation. + CFBundleSignature + ???? + CFBundleVersion +- 1 + %VERSION% -+ CFBundleSupportedPlatforms -+ -+ iPhoneOS -+ -+ MinimumOSVersion -+ @IPHONEOS_DEPLOYMENT_TARGET@ -+ -+ ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-ar -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-clang -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-cpp -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} -E "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch arm64 "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} strip -arch arm64 "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch x86_64 "$@" ---- /dev/null -+++ b/Apple/iOS/Resources/pyconfig.h -@@ -0,0 +1,7 @@ -+#ifdef __arm64__ -+#include "pyconfig-arm64.h" -+#endif -+ -+#ifdef __x86_64__ -+#include "pyconfig-x86_64.h" -+#endif ---- /dev/null + CFBundleSupportedPlatforms + + iPhoneOS +diff --git a/Apple/testbed/Python.xcframework/Info.plist b/Apple/testbed/Python.xcframework/Info.plist +index c6418de6e74..0587f4735f7 100644 +--- a/Apple/testbed/Python.xcframework/Info.plist +++ b/Apple/testbed/Python.xcframework/Info.plist -@@ -0,0 +1,136 @@ -+ -+ -+ -+ -+ AvailableLibraries -+ -+ -+ BinaryPath -+ Python.framework/Python -+ LibraryIdentifier -+ ios-arm64 -+ LibraryPath -+ Python.framework -+ SupportedArchitectures -+ -+ arm64 -+ -+ SupportedPlatform -+ ios -+ -+ -+ BinaryPath -+ Python.framework/Python -+ LibraryIdentifier -+ ios-arm64_x86_64-simulator -+ LibraryPath -+ Python.framework -+ SupportedArchitectures -+ -+ arm64 -+ x86_64 -+ -+ SupportedPlatform -+ ios -+ SupportedPlatformVariant -+ simulator -+ +@@ -35,6 +35,98 @@ + SupportedPlatformVariant + simulator + + + BinaryPath + Python.framework/Python @@ -1643,42 +326,9 @@ + SupportedPlatform + watchos + -+ -+ CFBundlePackageType -+ XFWK -+ XCFrameworkFormatVersion -+ 1.0 -+ -+ ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/build/iOS-dylib-Info-template.plist -@@ -0,0 +1,26 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleExecutable -+ -+ CFBundleIdentifier -+ -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundlePackageType -+ APPL -+ CFBundleShortVersionString -+ 1.0 -+ CFBundleSupportedPlatforms -+ -+ iPhoneOS -+ -+ MinimumOSVersion -+ 13.0 -+ CFBundleVersion -+ 1 -+ -+ + + CFBundlePackageType + XFWK --- /dev/null +++ b/Apple/testbed/Python.xcframework/build/tvOS-dylib-Info-template.plist @@ -0,0 +1,26 @@ @@ -1696,1854 +346,232 @@ + 6.0 + CFBundlePackageType + APPL -+ CFBundleShortVersionString -+ 1.0 -+ CFBundleSupportedPlatforms -+ -+ tvOS -+ -+ MinimumOSVersion -+ 9.0 -+ CFBundleVersion -+ 1 -+ -+ ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/build/utils.sh -@@ -0,0 +1,179 @@ -+# Utility methods for use in an Xcode project. -+# -+# An iOS XCframework cannot include any content other than the library binary -+# and relevant metadata. However, Python requires a standard library at runtime. -+# Therefore, it is necessary to add a build step to an Xcode app target that -+# processes the standard library and puts the content into the final app. -+# -+# In general, these tools will be invoked after bundle resources have been -+# copied into the app, but before framework embedding (and signing). -+# -+# The following is an example script, assuming that: -+# * Python.xcframework is in the root of the project -+# * There is an `app` folder that contains the app code -+# * There is an `app_packages` folder that contains installed Python packages. -+# ----- -+# set -e -+# source $PROJECT_DIR/Python.xcframework/build/build_utils.sh -+# install_python Python.xcframework app app_packages -+# ----- -+ -+# Copy the standard library from the XCframework into the app bundle. -+# -+# Accepts one argument: -+# 1. The path, relative to the root of the Xcode project, where the Python -+# XCframework can be found. -+install_stdlib() { -+ PYTHON_XCFRAMEWORK_PATH=$1 -+ -+ mkdir -p "$CODESIGNING_FOLDER_PATH/python/lib" -+ if [ "$EFFECTIVE_PLATFORM_NAME" = "-iphonesimulator" ]; then -+ echo "Installing Python modules for iOS Simulator" -+ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/ios-arm64-simulator" ]; then -+ SLICE_FOLDER="ios-arm64-simulator" -+ else -+ SLICE_FOLDER="ios-arm64_x86_64-simulator" -+ fi -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-iphoneos" ]; then -+ echo "Installing Python modules for iOS Device" -+ SLICE_FOLDER="ios-arm64" -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-appletvsimulator" ]; then -+ echo "Installing Python modules for tvOS Simulator" -+ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/tvos-arm64-simulator" ]; then -+ SLICE_FOLDER="tvos-arm64-simulator" -+ else -+ SLICE_FOLDER="tvos-arm64_x86_64-simulator" -+ fi -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-appletvos" ]; then -+ echo "Installing Python modules for tvOS Device" -+ SLICE_FOLDER="tvos-arm64" -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-watchsimulator" ]; then -+ echo "Installing Python modules for watchOS Simulator" -+ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/watchos-arm64-simulator" ]; then -+ SLICE_FOLDER="watchos-arm64-simulator" -+ else -+ SLICE_FOLDER="watchos-arm64_x86_64-simulator" -+ fi -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-watchos" ]; then -+ echo "Installing Python modules for watchOS Device" -+ SLICE_FOLDER="watchos-arm64" -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-xrsimulator" ]; then -+ echo "Installing Python modules for visionOS Simulator" -+ SLICE_FOLDER="xros-arm64-simulator" -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-xros" ]; then -+ echo "Installing Python modules for visionOS Device" -+ SLICE_FOLDER="xros-arm64" -+ else -+ echo "Unsupported platform name $EFFECTIVE_PLATFORM_NAME" -+ exit 1 -+ fi -+ -+ # If the XCframework has a shared lib folder, then it's a full framework. -+ # Copy both the common and slice-specific part of the lib directory. -+ # Otherwise, it's a single-arch framework; use the "full" lib folder. -+ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib" ]; then -+ rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" -+ rsync -au "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib-$ARCHS/" "$CODESIGNING_FOLDER_PATH/python/lib/" -+ else -+ rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" -+ fi -+} -+ -+# Convert a single .so library into a framework that iOS can load. -+# -+# Accepts three arguments: -+# 1. The path, relative to the root of the Xcode project, where the Python -+# XCframework can be found. -+# 2. The base path, relative to the installed location in the app bundle, that -+# needs to be processed. Any .so file found in this path (or a subdirectory -+# of it) will be processed. -+# 2. The full path to a single .so file to process. This path should include -+# the base path. -+install_dylib () { -+ PYTHON_XCFRAMEWORK_PATH=$1 -+ INSTALL_BASE=$2 -+ FULL_EXT=$3 -+ -+ # The name of the extension file -+ EXT=$(basename "$FULL_EXT") -+ # The name and location of the module -+ MODULE_PATH=$(dirname "$FULL_EXT") -+ MODULE_NAME=$(echo $EXT | cut -d "." -f 1) -+ # The location of the extension file, relative to the bundle -+ RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} -+ # The path to the extension file, relative to the install base -+ PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/} -+ # The full dotted name of the extension module, constructed from the file path. -+ FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d "." -f 1 | tr "/" "."); -+ # A bundle identifier; not actually used, but required by Xcode framework packaging -+ FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr "_" "-") -+ # The name of the framework folder. -+ FRAMEWORK_FOLDER="Frameworks/$FULL_MODULE_NAME.framework" -+ -+ # If the framework folder doesn't exist, create it. -+ if [ ! -d "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ]; then -+ echo "Creating framework for $RELATIVE_EXT" -+ mkdir -p "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" -+ cp "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/build/$PLATFORM_FAMILY_NAME-dylib-Info-template.plist" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" -+ plutil -replace CFBundleExecutable -string "$FULL_MODULE_NAME" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" -+ plutil -replace CFBundleIdentifier -string "$FRAMEWORK_BUNDLE_ID" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" -+ fi -+ -+ echo "Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME" -+ mv "$FULL_EXT" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" -+ # Create a placeholder .fwork file where the .so was -+ echo "$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" > ${FULL_EXT%.so}.fwork -+ # Create a back reference to the .so file location in the framework -+ echo "${RELATIVE_EXT%.so}.fwork" > "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin" -+ -+ # If the framework provides an xcprivacy file, install it. -+ if [ -e "$MODULE_PATH/$MODULE_NAME.xcprivacy" ]; then -+ echo "Installing XCPrivacy file for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME" -+ XCPRIVACY_FILE="$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/PrivacyInfo.xcprivacy" -+ if [ -e "$XCPRIVACY_FILE" ]; then -+ rm -rf "$XCPRIVACY_FILE" -+ fi -+ mv "$MODULE_PATH/$MODULE_NAME.xcprivacy" "$XCPRIVACY_FILE" -+ fi -+ -+ echo "Signing framework as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)..." -+ /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" -+} -+ -+# Process all the dynamic libraries in a path into Framework format. -+# -+# Accepts two arguments: -+# 1. The path, relative to the root of the Xcode project, where the Python -+# XCframework can be found. -+# 2. The base path, relative to the installed location in the app bundle, that -+# needs to be processed. Any .so file found in this path (or a subdirectory -+# of it) will be processed. -+process_dylibs () { -+ PYTHON_XCFRAMEWORK_PATH=$1 -+ LIB_PATH=$2 -+ find "$CODESIGNING_FOLDER_PATH/$LIB_PATH" -name "*.so" | while read FULL_EXT; do -+ install_dylib $PYTHON_XCFRAMEWORK_PATH "$LIB_PATH/" "$FULL_EXT" -+ done -+} -+ -+# The entry point for post-processing a Python XCframework. -+# -+# Accepts 1 or more arguments: -+# 1. The path, relative to the root of the Xcode project, where the Python -+# XCframework can be found. If the XCframework is in the root of the project, -+# 2+. The path of a package, relative to the root of the packaged app, that contains -+# library content that should be processed for binary libraries. -+install_python() { -+ PYTHON_XCFRAMEWORK_PATH=$1 -+ shift -+ -+ install_stdlib $PYTHON_XCFRAMEWORK_PATH -+ PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib") -+ echo "Install Python $PYTHON_VER standard library extension modules..." -+ process_dylibs $PYTHON_XCFRAMEWORK_PATH python/lib/$PYTHON_VER/lib-dynload -+ -+ for package_path in $@; do -+ echo "Installing $package_path extension modules ..." -+ process_dylibs $PYTHON_XCFRAMEWORK_PATH $package_path -+ done -+} ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/build/watchOS-dylib-Info-template.plist -@@ -0,0 +1,26 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleExecutable -+ -+ CFBundleIdentifier -+ -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundlePackageType -+ APPL -+ CFBundleShortVersionString -+ 1.0 -+ CFBundleSupportedPlatforms -+ -+ watchOS -+ -+ MinimumOSVersion -+ 4.0 -+ CFBundleVersion -+ 1 -+ -+ ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/build/xrOS-dylib-Info-template.plist -@@ -0,0 +1,30 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundleExecutable -+ -+ CFBundleIdentifier -+ -+ CFBundlePackageType -+ FMWK -+ CFBundleShortVersionString -+ 1.0 -+ CFBundleSupportedPlatforms -+ -+ XROS -+ -+ CFBundleVersion -+ 1 -+ MinimumOSVersion -+ 2.0 -+ UIDeviceFamily -+ -+ 7 -+ -+ -+ ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/ios-arm64/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. -+ -+It should be used as a target for `--enable-framework` when compiling an iOS on-device -+build for testing purposes. ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. -+ -+It should be used as a target for `--enable-framework` when compiling an iOS simulator -+build for testing purposes (either x86_64 or ARM64). ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/tvos-arm64/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. -+ -+It should be used as a target for `--enable-framework` when compiling a tvOS -+on-device build for testing purposes. ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/tvos-arm64_x86_64-simulator/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. -+ -+It should be used as a target for `--enable-framework` when compiling a tvOS -+simulator build for testing purposes (either x86_64 or ARM64). ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/watchos-arm64_32/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. -+ -+It should be used as a target for `--enable-framework` when compiling a watchOS on-device -+build for testing purposes. ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/watchos-arm64_x86_64-simulator/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. -+ -+It should be used as a target for `--enable-framework` when compiling a watchOS -+simulator build for testing purposes (either x86_64 or ARM64). ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/xros-arm64-simulator/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. -+ -+It should be used as a target for `--enable-framework` when compiling an visionOS simulator -+build for testing purposes (either x86_64 or ARM64). ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/xros-arm64/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. -+ -+It should be used as a target for `--enable-framework` when compiling an visionOS on-device -+build for testing purposes. ---- /dev/null -+++ b/Apple/testbed/Testbed.lldbinit -@@ -0,0 +1,4 @@ -+process handle SIGINT -n true -p true -s false -+process handle SIGUSR1 -n true -p true -s false -+process handle SIGUSR2 -n true -p true -s false -+process handle SIGXFSZ -n true -p true -s false ---- /dev/null -+++ b/Apple/testbed/TestbedTests/TestbedTests.m -@@ -0,0 +1,197 @@ -+#import -+#import -+ -+@interface TestbedTests : XCTestCase -+ -+@end -+ -+@implementation TestbedTests -+ -+ -+- (void)testPython { -+ const char **argv; -+ int exit_code; -+ int failed; -+ PyStatus status; -+ PyPreConfig preconfig; -+ PyConfig config; -+ PyObject *app_packages_path; -+ PyObject *method_args; -+ PyObject *result; -+ PyObject *site_module; -+ PyObject *site_addsitedir_attr; -+ PyObject *sys_module; -+ PyObject *sys_path_attr; -+ NSArray *test_args; -+ NSString *python_home; -+ NSString *path; -+ wchar_t *wtmp_str; -+ -+ NSString *resourcePath = [[NSBundle mainBundle] resourcePath]; -+ -+ // Set some other common environment indicators to disable color, as the -+ // Xcode log can't display color. Stdout will report that it is *not* a -+ // TTY. -+ setenv("NO_COLOR", "1", true); -+ setenv("PYTHON_COLORS", "0", true); -+ -+ // Arguments to pass into the test suite runner. -+ // argv[0] must identify the process; any subsequent arg -+ // will be handled as if it were an argument to `python -m test` -+ // The processInfo arguments contain the binary that is running, -+ // followed by the arguments defined in the test plan. This means: -+ // run_module = test_args[1] -+ // argv = ["Testbed"] + test_args[2:] -+ test_args = [[NSProcessInfo processInfo] arguments]; -+ if (test_args == NULL) { -+ NSLog(@"Unable to identify test arguments."); -+ } -+ NSLog(@"Test arguments: %@", test_args); -+ argv = malloc(sizeof(char *) * ([test_args count] - 1)); -+ argv[0] = "Testbed"; -+ for (int i = 1; i < [test_args count] - 1; i++) { -+ argv[i] = [[test_args objectAtIndex:i+1] UTF8String]; -+ } -+ -+ // Generate an isolated Python configuration. -+ NSLog(@"Configuring isolated Python..."); -+ PyPreConfig_InitIsolatedConfig(&preconfig); -+ PyConfig_InitIsolatedConfig(&config); -+ -+ // Configure the Python interpreter: -+ // Enforce UTF-8 encoding for stderr, stdout, file-system encoding and locale. -+ // See https://docs.python.org/3/library/os.html#python-utf-8-mode. -+ preconfig.utf8_mode = 1; -+ // Use the system logger for stdout/err -+ config.use_system_logger = 1; -+ // Don't buffer stdio. We want output to appears in the log immediately -+ config.buffered_stdio = 0; -+ // Don't write bytecode; we can't modify the app bundle -+ // after it has been signed. -+ config.write_bytecode = 0; -+ // Ensure that signal handlers are installed -+ config.install_signal_handlers = 1; -+ // Run the test module. -+ config.run_module = Py_DecodeLocale([[test_args objectAtIndex:1] UTF8String], NULL); -+ // For debugging - enable verbose mode. -+ // config.verbose = 1; -+ -+ NSLog(@"Pre-initializing Python runtime..."); -+ status = Py_PreInitialize(&preconfig); -+ if (PyStatus_Exception(status)) { -+ XCTFail(@"Unable to pre-initialize Python interpreter: %s", status.err_msg); -+ PyConfig_Clear(&config); -+ return; -+ } -+ -+ // Set the home for the Python interpreter -+ python_home = [NSString stringWithFormat:@"%@/python", resourcePath, nil]; -+ NSLog(@"PythonHome: %@", python_home); -+ wtmp_str = Py_DecodeLocale([python_home UTF8String], NULL); -+ status = PyConfig_SetString(&config, &config.home, wtmp_str); -+ if (PyStatus_Exception(status)) { -+ XCTFail(@"Unable to set PYTHONHOME: %s", status.err_msg); -+ PyConfig_Clear(&config); -+ return; -+ } -+ PyMem_RawFree(wtmp_str); -+ -+ // Read the site config -+ status = PyConfig_Read(&config); -+ if (PyStatus_Exception(status)) { -+ XCTFail(@"Unable to read site config: %s", status.err_msg); -+ PyConfig_Clear(&config); -+ return; -+ } -+ -+ NSLog(@"Configure argc/argv..."); -+ status = PyConfig_SetBytesArgv(&config, [test_args count] - 1, (char**) argv); -+ if (PyStatus_Exception(status)) { -+ XCTFail(@"Unable to configure argc/argv: %s", status.err_msg); -+ PyConfig_Clear(&config); -+ return; -+ } -+ -+ NSLog(@"Initializing Python runtime..."); -+ status = Py_InitializeFromConfig(&config); -+ if (PyStatus_Exception(status)) { -+ XCTFail(@"Unable to initialize Python interpreter: %s", status.err_msg); -+ PyConfig_Clear(&config); -+ return; -+ } -+ -+ // Add app_packages as a site directory. This both adds to sys.path, -+ // and ensures that any .pth files in that directory will be executed. -+ site_module = PyImport_ImportModule("site"); -+ if (site_module == NULL) { -+ XCTFail(@"Could not import site module"); -+ return; -+ } -+ -+ site_addsitedir_attr = PyObject_GetAttrString(site_module, "addsitedir"); -+ if (site_addsitedir_attr == NULL || !PyCallable_Check(site_addsitedir_attr)) { -+ XCTFail(@"Could not access site.addsitedir"); -+ return; -+ } -+ -+ path = [NSString stringWithFormat:@"%@/app_packages", resourcePath, nil]; -+ NSLog(@"App packages path: %@", path); -+ wtmp_str = Py_DecodeLocale([path UTF8String], NULL); -+ app_packages_path = PyUnicode_FromWideChar(wtmp_str, wcslen(wtmp_str)); -+ if (app_packages_path == NULL) { -+ XCTFail(@"Could not convert app_packages path to unicode"); -+ return; -+ } -+ PyMem_RawFree(wtmp_str); -+ -+ method_args = Py_BuildValue("(O)", app_packages_path); -+ if (method_args == NULL) { -+ XCTFail(@"Could not create arguments for site.addsitedir"); -+ return; -+ } -+ -+ result = PyObject_CallObject(site_addsitedir_attr, method_args); -+ if (result == NULL) { -+ XCTFail(@"Could not add app_packages directory using site.addsitedir"); -+ return; -+ } -+ -+ // Add test code to sys.path -+ sys_module = PyImport_ImportModule("sys"); -+ if (sys_module == NULL) { -+ XCTFail(@"Could not import sys module"); -+ return; -+ } -+ -+ sys_path_attr = PyObject_GetAttrString(sys_module, "path"); -+ if (sys_path_attr == NULL) { -+ XCTFail(@"Could not access sys.path"); -+ return; -+ } -+ -+ path = [NSString stringWithFormat:@"%@/app", resourcePath, nil]; -+ NSLog(@"App path: %@", path); -+ wtmp_str = Py_DecodeLocale([path UTF8String], NULL); -+ failed = PyList_Insert(sys_path_attr, 0, PyUnicode_FromString([path UTF8String])); -+ if (failed) { -+ XCTFail(@"Unable to add app to sys.path"); -+ return; -+ } -+ PyMem_RawFree(wtmp_str); -+ -+ // Ensure the working directory is the app folder. -+ chdir([path UTF8String]); -+ -+ // Start the test suite. Print a separator to differentiate Python startup logs from app logs -+ NSLog(@"---------------------------------------------------------------------------"); -+ -+ exit_code = Py_RunMain(); -+ XCTAssertEqual(exit_code, 0, @"Test suite did not pass"); -+ -+ NSLog(@"---------------------------------------------------------------------------"); -+ -+ Py_Finalize(); -+} -+ -+ -+@end ---- /dev/null -+++ b/Apple/testbed/__main__.py -@@ -0,0 +1,436 @@ -+import argparse -+import json -+import re -+import shutil -+import subprocess -+import sys -+from pathlib import Path -+ -+TEST_SLICES = { -+ "iOS": "ios-arm64_x86_64-simulator", -+ "tvOS": "tvos-arm64_x86_64-simulator", -+ "visionOS": "xros-arm64-simulator", -+ "watchOS": "watchos-arm64_x86_64-simulator", -+} -+ -+DECODE_ARGS = ("UTF-8", "backslashreplace") -+ -+# The system log prefixes each line: -+# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ... -+# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ... -+ -+LOG_PREFIX_REGEX = re.compile( -+ r"^\d{4}-\d{2}-\d{2}" # YYYY-MM-DD -+ r"\s+\d+:\d{2}:\d{2}\.\d+\+\d{4}" # HH:MM:SS.ssssss+ZZZZ -+ r"\s+.*Testbed\[\d+:\w+\]" # Process/thread ID -+) -+ -+ -+# Select a simulator device to use. -+def select_simulator_device(platform): -+ # List the testing simulators, in JSON format -+ raw_json = subprocess.check_output(["xcrun", "simctl", "list", "-j"]) -+ json_data = json.loads(raw_json) -+ -+ if platform == "iOS": -+ # Any iOS device will do; we'll look for "SE" devices - but the name isn't -+ # consistent over time. Older Xcode versions will use "iPhone SE (Nth -+ # generation)"; As of 2025, they've started using "iPhone 16e". -+ # -+ # When Xcode is updated after a new release, new devices will be available -+ # and old ones will be dropped from the set available on the latest iOS -+ # version. Select the one with the highest minimum runtime version - this -+ # is an indicator of the "newest" released device, which should always be -+ # supported on the "most recent" iOS version. -+ se_simulators = sorted( -+ (devicetype["minRuntimeVersion"], devicetype["name"]) -+ for devicetype in json_data["devicetypes"] -+ if devicetype["productFamily"] == "iPhone" -+ and ( -+ ( -+ "iPhone " in devicetype["name"] -+ and devicetype["name"].endswith("e") -+ ) -+ or "iPhone SE " in devicetype["name"] -+ ) -+ ) -+ simulator = se_simulators[-1][1] -+ elif platform == "tvOS": -+ # Find the most recent tvOS release. -+ simulators = sorted( -+ (devicetype["minRuntimeVersion"], devicetype["name"]) -+ for devicetype in json_data["devicetypes"] -+ if devicetype["productFamily"] == "Apple TV" -+ ) -+ simulator = simulators[-1][1] -+ elif platform == "visionOS": -+ # Find the most recent visionOS release. -+ simulators = sorted( -+ (devicetype["minRuntimeVersion"], devicetype["name"]) -+ for devicetype in json_data["devicetypes"] -+ if devicetype["productFamily"] == "Apple Vision" -+ ) -+ simulator = simulators[-1][1] -+ elif platform == "watchOS": -+ raise NotImplementedError(f"Don't know how to launch watchOS (yet)") -+ else: -+ raise ValueError(f"Unknown platform {platform}") -+ -+ return simulator -+ -+ -+def xcode_test(location: Path, platform: str, simulator: str, verbose: bool): -+ # Build and run the test suite on the named simulator. -+ args = [ -+ "-project", -+ str(location / f"{platform}Testbed.xcodeproj"), -+ "-scheme", -+ f"{platform}Testbed", -+ "-destination", -+ f"platform={platform} Simulator,name={simulator}", -+ "-derivedDataPath", -+ str(location / "DerivedData"), -+ ] -+ verbosity_args = [] if verbose else ["-quiet"] -+ -+ print("Building test project...") -+ subprocess.run( -+ ["xcodebuild", "build-for-testing"] + args + verbosity_args, -+ check=True, -+ ) -+ -+ print("Running test project...") -+ # Test execution *can't* be run -quiet; verbose mode -+ # is how we see the output of the test output. -+ process = subprocess.Popen( -+ ["xcodebuild", "test-without-building"] + args, -+ stdout=subprocess.PIPE, -+ stderr=subprocess.STDOUT, -+ ) -+ while line := (process.stdout.readline()).decode(*DECODE_ARGS): -+ # Strip the timestamp/process prefix from each log line -+ line = LOG_PREFIX_REGEX.sub("", line) -+ sys.stdout.write(line) -+ sys.stdout.flush() -+ -+ status = process.wait(timeout=5) -+ exit(status) -+ -+ -+def copy(src, tgt): -+ """An all-purpose copy. -+ -+ If src is a file, it is copied. If src is a symlink, it is copied *as a -+ symlink*. If src is a directory, the full tree is duplicated, with symlinks -+ being preserved. -+ """ -+ if src.is_file() or src.is_symlink(): -+ shutil.copyfile(src, tgt, follow_symlinks=False) -+ else: -+ shutil.copytree(src, tgt, symlinks=True) -+ -+ -+def clone_testbed( -+ source: Path, -+ target: Path, -+ framework: Path, -+ platform: str, -+ apps: list[Path], -+) -> None: -+ if target.exists(): -+ print(f"{target} already exists; aborting without creating project.") -+ sys.exit(10) -+ -+ if framework is None: -+ if not ( -+ source / "Python.xcframework" / TEST_SLICES[platform] / "bin" -+ ).is_dir(): -+ print( -+ f"The testbed being cloned ({source}) does not contain " -+ "a framework with slices. Re-run with --framework" -+ ) -+ sys.exit(11) -+ else: -+ if not framework.is_dir(): -+ print(f"{framework} does not exist.") -+ sys.exit(12) -+ elif not ( -+ framework.suffix == ".xcframework" -+ or (framework / "Python.framework").is_dir() -+ ): -+ print( -+ f"{framework} is not an XCframework, " -+ f"or a simulator slice of a framework build." -+ ) -+ sys.exit(13) -+ -+ print("Cloning testbed project:") -+ print(f" Cloning {source}...", end="") -+ # Only copy the files for the platform being cloned plus the files common -+ # to all platforms. The XCframework will be copied later, if needed. -+ target.mkdir(parents=True) -+ -+ for name in [ -+ "__main__.py", -+ "TestbedTests", -+ "Testbed.lldbinit", -+ f"{platform}Testbed", -+ f"{platform}Testbed.xcodeproj", -+ f"{platform}Testbed.xctestplan", -+ ]: -+ copy(source / name, target / name) -+ -+ print(" done") -+ -+ orig_xc_framework_path = source / "Python.xcframework" -+ xc_framework_path = target / "Python.xcframework" -+ test_framework_path = xc_framework_path / TEST_SLICES[platform] -+ if framework is not None: -+ if framework.suffix == ".xcframework": -+ print(" Installing XCFramework...", end="") -+ xc_framework_path.symlink_to( -+ framework.relative_to(xc_framework_path.parent, walk_up=True) -+ ) -+ print(" done") -+ else: -+ print(" Installing simulator framework...", end="") -+ # We're only installing a slice of a framework; we need -+ # to do a full tree copy to make sure we don't damage -+ # symlinked content. -+ shutil.copytree(orig_xc_framework_path, xc_framework_path) -+ if test_framework_path.is_dir(): -+ shutil.rmtree(test_framework_path) -+ else: -+ test_framework_path.unlink(missing_ok=True) -+ test_framework_path.symlink_to( -+ framework.relative_to(test_framework_path.parent, walk_up=True) -+ ) -+ print(" done") -+ else: -+ copy(orig_xc_framework_path, xc_framework_path) -+ -+ if ( -+ xc_framework_path.is_symlink() -+ and not xc_framework_path.readlink().is_absolute() -+ ): -+ # XCFramework is a relative symlink. Rewrite the symlink relative -+ # to the new location. -+ print(" Rewriting symlink to XCframework...", end="") -+ resolved_xc_framework_path = ( -+ source / xc_framework_path.readlink() -+ ).resolve() -+ xc_framework_path.unlink() -+ xc_framework_path.symlink_to( -+ resolved_xc_framework_path.relative_to( -+ xc_framework_path.parent, walk_up=True -+ ) -+ ) -+ print(" done") -+ elif ( -+ test_framework_path.is_symlink() -+ and not test_framework_path.readlink().is_absolute() -+ ): -+ print(" Rewriting symlink to simulator framework...", end="") -+ # Simulator framework is a relative symlink. Rewrite the symlink -+ # relative to the new location. -+ orig_test_framework_path = ( -+ source / "Python.XCframework" / test_framework_path.readlink() -+ ).resolve() -+ test_framework_path.unlink() -+ test_framework_path.symlink_to( -+ orig_test_framework_path.relative_to( -+ test_framework_path.parent, walk_up=True -+ ) -+ ) -+ print(" done") -+ else: -+ print(" Using pre-existing Python framework.") -+ -+ for app_src in apps: -+ print(f" Installing app {app_src.name!r}...", end="") -+ app_target = target / f"Testbed/app/{app_src.name}" -+ if app_target.is_dir(): -+ shutil.rmtree(app_target) -+ shutil.copytree(app_src, app_target) -+ print(" done") -+ -+ print(f"Successfully cloned testbed: {target.resolve()}") -+ -+ -+def update_test_plan(testbed_path, platform, args): -+ # Modify the test plan to use the requested test arguments. -+ test_plan_path = testbed_path / f"{platform}Testbed.xctestplan" -+ with test_plan_path.open("r", encoding="utf-8") as f: -+ test_plan = json.load(f) -+ -+ test_plan["defaultOptions"]["commandLineArgumentEntries"] = [ -+ {"argument": arg} for arg in args -+ ] -+ -+ with test_plan_path.open("w", encoding="utf-8") as f: -+ json.dump(test_plan, f, indent=2) -+ -+ -+def run_testbed( -+ platform: str, -+ simulator: str | None, -+ args: list[str], -+ verbose: bool = False, -+): -+ location = Path(__file__).parent -+ print("Updating test plan...", end="") -+ update_test_plan(location, platform, args) -+ print(" done.") -+ -+ if simulator is None: -+ simulator = select_simulator_device(platform) -+ print(f"Running test on {simulator}") -+ -+ xcode_test( -+ location, -+ platform=platform, -+ simulator=simulator, -+ verbose=verbose, -+ ) -+ -+ -+def main(): -+ # Look for directories like `iOSTestbed` as an indicator of the platforms -+ # that the testbed folder supports. The original source testbed can support -+ # many platforms, but when cloned, only one platform is preserved. -+ available_platforms = [ -+ platform -+ for platform in ["iOS", "tvOS", "visionOS", "watchOS"] -+ if (Path(__file__).parent / f"{platform}Testbed").is_dir() -+ ] -+ -+ parser = argparse.ArgumentParser( -+ description=( -+ "Manages the process of testing an Apple Python project through Xcode." -+ ), -+ ) -+ -+ subcommands = parser.add_subparsers(dest="subcommand") -+ clone = subcommands.add_parser( -+ "clone", -+ description=( -+ "Clone the testbed project, copying in a Python framework and" -+ "any specified application code." -+ ), -+ help="Clone a testbed project to a new location.", -+ ) -+ clone.add_argument( -+ "--framework", -+ help=( -+ "The location of the XCFramework (or simulator-only slice of an " -+ "XCFramework) to use when running the testbed" -+ ), -+ ) -+ clone.add_argument( -+ "--platform", -+ dest="platform", -+ choices=available_platforms, -+ default=available_platforms[0], -+ help=f"The platform to target (default: {available_platforms[0]})", -+ ) -+ clone.add_argument( -+ "--app", -+ dest="apps", -+ action="append", -+ default=[], -+ help="The location of any code to include in the testbed project", -+ ) -+ clone.add_argument( -+ "location", -+ help="The path where the testbed will be cloned.", -+ ) -+ -+ run = subcommands.add_parser( -+ "run", -+ usage="%(prog)s [-h] [--simulator SIMULATOR] -- [ ...]", -+ description=( -+ "Run a testbed project. The arguments provided after `--` will be " -+ "passed to the running test process as if they were arguments to " -+ "`python -m`." -+ ), -+ help="Run a testbed project", -+ ) -+ run.add_argument( -+ "--platform", -+ dest="platform", -+ choices=available_platforms, -+ default=available_platforms[0], -+ help=f"The platform to target (default: {available_platforms[0]})", -+ ) -+ run.add_argument( -+ "--simulator", -+ help=( -+ "The name of the simulator to use (eg: 'iPhone 16e'). Defaults to " -+ "the most recently released 'entry level' iPhone device. Device " -+ "architecture and OS version can also be specified; e.g., " -+ "`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would run on " -+ "an ARM64 iPhone 16 Pro simulator running iOS 26.0." -+ ), -+ ) -+ run.add_argument( -+ "-v", -+ "--verbose", -+ action="store_true", -+ help="Enable verbose output", -+ ) -+ -+ try: -+ pos = sys.argv.index("--") -+ testbed_args = sys.argv[1:pos] -+ test_args = sys.argv[pos + 1 :] -+ except ValueError: -+ testbed_args = sys.argv[1:] -+ test_args = [] -+ -+ context = parser.parse_args(testbed_args) -+ -+ if context.subcommand == "clone": -+ clone_testbed( -+ source=Path(__file__).parent.resolve(), -+ target=Path(context.location).resolve(), -+ framework=Path(context.framework).resolve() -+ if context.framework -+ else None, -+ platform=context.platform, -+ apps=[Path(app) for app in context.apps], -+ ) -+ elif context.subcommand == "run": -+ if test_args: -+ if not ( -+ Path(__file__).parent -+ / "Python.xcframework" -+ / TEST_SLICES[context.platform] -+ / "bin" -+ ).is_dir(): -+ print( -+ f"Testbed does not contain a compiled Python framework. Use " -+ f"`python {sys.argv[0]} clone ...` to create a runnable " -+ f"clone of this testbed." -+ ) -+ sys.exit(20) -+ -+ run_testbed( -+ platform=context.platform, -+ simulator=context.simulator, -+ verbose=context.verbose, -+ args=test_args, -+ ) -+ else: -+ print( -+ f"Must specify test arguments (e.g., {sys.argv[0]} run -- test)" -+ ) -+ print() -+ parser.print_help(sys.stderr) -+ sys.exit(21) -+ else: -+ parser.print_help(sys.stderr) -+ sys.exit(1) -+ -+ -+if __name__ == "__main__": -+ main() ---- /dev/null -+++ b/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj -@@ -0,0 +1,557 @@ -+// !$*UTF8*$! -+{ -+ archiveVersion = 1; -+ classes = { -+ }; -+ objectVersion = 56; -+ objects = { -+ -+/* Begin PBXBuildFile section */ -+ 607A66172B0EFA380010BFC8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66162B0EFA380010BFC8 /* AppDelegate.m */; }; -+ 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607A66212B0EFA390010BFC8 /* Assets.xcassets */; }; -+ 607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */; }; -+ 607A66282B0EFA390010BFC8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66272B0EFA390010BFC8 /* main.m */; }; -+ 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */; }; -+ 607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; -+ 607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; -+ 607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; -+ 607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; -+ 608619542CB77BA900F46182 /* app_packages in Resources */ = {isa = PBXBuildFile; fileRef = 608619532CB77BA900F46182 /* app_packages */; }; -+ 608619562CB7819B00F46182 /* app in Resources */ = {isa = PBXBuildFile; fileRef = 608619552CB7819B00F46182 /* app */; }; -+/* End PBXBuildFile section */ -+ -+/* Begin PBXContainerItemProxy section */ -+ 607A662E2B0EFA3A0010BFC8 /* PBXContainerItemProxy */ = { -+ isa = PBXContainerItemProxy; -+ containerPortal = 607A660A2B0EFA380010BFC8 /* Project object */; -+ proxyType = 1; -+ remoteGlobalIDString = 607A66112B0EFA380010BFC8; -+ remoteInfo = iOSTestbed; -+ }; -+/* End PBXContainerItemProxy section */ -+ -+/* Begin PBXCopyFilesBuildPhase section */ -+ 607A664E2B0EFC080010BFC8 /* Embed Frameworks */ = { -+ isa = PBXCopyFilesBuildPhase; -+ buildActionMask = 2147483647; -+ dstPath = ""; -+ dstSubfolderSpec = 10; -+ files = ( -+ 607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */, -+ ); -+ name = "Embed Frameworks"; -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */ = { -+ isa = PBXCopyFilesBuildPhase; -+ buildActionMask = 2147483647; -+ dstPath = ""; -+ dstSubfolderSpec = 10; -+ files = ( -+ 607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */, -+ ); -+ name = "Embed Frameworks"; -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXCopyFilesBuildPhase section */ -+ -+/* Begin PBXFileReference section */ -+ 607A66122B0EFA380010BFC8 /* iOSTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; }; -+ 607A66152B0EFA380010BFC8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; -+ 607A66162B0EFA380010BFC8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; -+ 607A66212B0EFA390010BFC8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; -+ 607A66242B0EFA390010BFC8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; -+ 607A66272B0EFA390010BFC8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; -+ 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSTestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; -+ 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestbedTests.m; sourceTree = ""; }; -+ 607A664A2B0EFB310010BFC8 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; -+ 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = ""; }; -+ 608619532CB77BA900F46182 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = ""; }; -+ 608619552CB7819B00F46182 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = ""; }; -+ 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = iOSTestbed.xctestplan; sourceTree = ""; }; -+/* End PBXFileReference section */ -+ -+/* Begin PBXFrameworksBuildPhase section */ -+ 607A660F2B0EFA380010BFC8 /* Frameworks */ = { -+ isa = PBXFrameworksBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ 607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+ 607A662A2B0EFA3A0010BFC8 /* Frameworks */ = { -+ isa = PBXFrameworksBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ 607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXFrameworksBuildPhase section */ -+ -+/* Begin PBXGroup section */ -+ 607A66092B0EFA380010BFC8 = { -+ isa = PBXGroup; -+ children = ( -+ 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */, -+ 607A664A2B0EFB310010BFC8 /* Python.xcframework */, -+ 607A66142B0EFA380010BFC8 /* iOSTestbed */, -+ 607A66302B0EFA3A0010BFC8 /* TestbedTests */, -+ 607A66132B0EFA380010BFC8 /* Products */, -+ 607A664F2B0EFFE00010BFC8 /* Frameworks */, -+ ); -+ sourceTree = ""; -+ }; -+ 607A66132B0EFA380010BFC8 /* Products */ = { -+ isa = PBXGroup; -+ children = ( -+ 607A66122B0EFA380010BFC8 /* iOSTestbed.app */, -+ 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */, -+ ); -+ name = Products; -+ sourceTree = ""; -+ }; -+ 607A66142B0EFA380010BFC8 /* iOSTestbed */ = { -+ isa = PBXGroup; -+ children = ( -+ 608619552CB7819B00F46182 /* app */, -+ 608619532CB77BA900F46182 /* app_packages */, -+ 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */, -+ 607A66152B0EFA380010BFC8 /* AppDelegate.h */, -+ 607A66162B0EFA380010BFC8 /* AppDelegate.m */, -+ 607A66212B0EFA390010BFC8 /* Assets.xcassets */, -+ 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */, -+ 607A66272B0EFA390010BFC8 /* main.m */, -+ ); -+ path = iOSTestbed; -+ sourceTree = ""; -+ }; -+ 607A66302B0EFA3A0010BFC8 /* TestbedTests */ = { -+ isa = PBXGroup; -+ children = ( -+ 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */, -+ ); -+ path = TestbedTests; -+ sourceTree = ""; -+ }; -+ 607A664F2B0EFFE00010BFC8 /* Frameworks */ = { -+ isa = PBXGroup; -+ children = ( -+ ); -+ name = Frameworks; -+ sourceTree = ""; -+ }; -+/* End PBXGroup section */ -+ -+/* Begin PBXNativeTarget section */ -+ 607A66112B0EFA380010BFC8 /* iOSTestbed */ = { -+ isa = PBXNativeTarget; -+ buildConfigurationList = 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbed" */; -+ buildPhases = ( -+ 607A660E2B0EFA380010BFC8 /* Sources */, -+ 607A660F2B0EFA380010BFC8 /* Frameworks */, -+ 607A66102B0EFA380010BFC8 /* Resources */, -+ 607A66552B0F061D0010BFC8 /* Process Python libraries */, -+ 607A664E2B0EFC080010BFC8 /* Embed Frameworks */, -+ ); -+ buildRules = ( -+ ); -+ dependencies = ( -+ ); -+ name = iOSTestbed; -+ productName = iOSTestbed; -+ productReference = 607A66122B0EFA380010BFC8 /* iOSTestbed.app */; -+ productType = "com.apple.product-type.application"; -+ }; -+ 607A662C2B0EFA3A0010BFC8 /* iOSTestbedTests */ = { -+ isa = PBXNativeTarget; -+ buildConfigurationList = 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbedTests" */; -+ buildPhases = ( -+ 607A66292B0EFA3A0010BFC8 /* Sources */, -+ 607A662A2B0EFA3A0010BFC8 /* Frameworks */, -+ 607A662B2B0EFA3A0010BFC8 /* Resources */, -+ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */, -+ ); -+ buildRules = ( -+ ); -+ dependencies = ( -+ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */, -+ ); -+ name = iOSTestbedTests; -+ productName = iOSTestbedTests; -+ productReference = 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */; -+ productType = "com.apple.product-type.bundle.unit-test"; -+ }; -+/* End PBXNativeTarget section */ -+ -+/* Begin PBXProject section */ -+ 607A660A2B0EFA380010BFC8 /* Project object */ = { -+ isa = PBXProject; -+ attributes = { -+ BuildIndependentTargetsInParallel = 1; -+ LastUpgradeCheck = 1500; -+ TargetAttributes = { -+ 607A66112B0EFA380010BFC8 = { -+ CreatedOnToolsVersion = 15.0.1; -+ }; -+ 607A662C2B0EFA3A0010BFC8 = { -+ CreatedOnToolsVersion = 15.0.1; -+ TestTargetID = 607A66112B0EFA380010BFC8; -+ }; -+ }; -+ }; -+ buildConfigurationList = 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "iOSTestbed" */; -+ compatibilityVersion = "Xcode 14.0"; -+ developmentRegion = en; -+ hasScannedForEncodings = 0; -+ knownRegions = ( -+ en, -+ Base, -+ ); -+ mainGroup = 607A66092B0EFA380010BFC8; -+ productRefGroup = 607A66132B0EFA380010BFC8 /* Products */; -+ projectDirPath = ""; -+ projectRoot = ""; -+ targets = ( -+ 607A66112B0EFA380010BFC8 /* iOSTestbed */, -+ 607A662C2B0EFA3A0010BFC8 /* iOSTestbedTests */, -+ ); -+ }; -+/* End PBXProject section */ -+ -+/* Begin PBXResourcesBuildPhase section */ -+ 607A66102B0EFA380010BFC8 /* Resources */ = { -+ isa = PBXResourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ 607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */, -+ 608619562CB7819B00F46182 /* app in Resources */, -+ 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */, -+ 608619542CB77BA900F46182 /* app_packages in Resources */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+ 607A662B2B0EFA3A0010BFC8 /* Resources */ = { -+ isa = PBXResourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXResourcesBuildPhase section */ -+ -+/* Begin PBXShellScriptBuildPhase section */ -+ 607A66552B0F061D0010BFC8 /* Process Python libraries */ = { -+ isa = PBXShellScriptBuildPhase; -+ alwaysOutOfDate = 1; -+ buildActionMask = 2147483647; -+ files = ( -+ ); -+ inputFileListPaths = ( -+ ); -+ inputPaths = ( -+ ); -+ name = "Process Python libraries"; -+ outputFileListPaths = ( -+ ); -+ outputPaths = ( -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ shellPath = /bin/sh; -+ shellScript = "set -e\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\ninstall_python Python.xcframework app app_packages\n"; -+ showEnvVarsInLog = 0; -+ }; -+/* End PBXShellScriptBuildPhase section */ -+ -+/* Begin PBXSourcesBuildPhase section */ -+ 607A660E2B0EFA380010BFC8 /* Sources */ = { -+ isa = PBXSourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ 607A66172B0EFA380010BFC8 /* AppDelegate.m in Sources */, -+ 607A66282B0EFA390010BFC8 /* main.m in Sources */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+ 607A66292B0EFA3A0010BFC8 /* Sources */ = { -+ isa = PBXSourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXSourcesBuildPhase section */ -+ -+/* Begin PBXTargetDependency section */ -+ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */ = { -+ isa = PBXTargetDependency; -+ target = 607A66112B0EFA380010BFC8 /* iOSTestbed */; -+ targetProxy = 607A662E2B0EFA3A0010BFC8 /* PBXContainerItemProxy */; -+ }; -+/* End PBXTargetDependency section */ -+ -+/* Begin PBXVariantGroup section */ -+ 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */ = { -+ isa = PBXVariantGroup; -+ children = ( -+ 607A66242B0EFA390010BFC8 /* Base */, -+ ); -+ name = LaunchScreen.storyboard; -+ sourceTree = ""; -+ }; -+/* End PBXVariantGroup section */ -+ -+/* Begin XCBuildConfiguration section */ -+ 607A663F2B0EFA3A0010BFC8 /* Debug */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ALWAYS_SEARCH_USER_PATHS = NO; -+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; -+ CLANG_ANALYZER_NONNULL = YES; -+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; -+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; -+ CLANG_ENABLE_MODULES = YES; -+ CLANG_ENABLE_OBJC_ARC = YES; -+ CLANG_ENABLE_OBJC_WEAK = YES; -+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; -+ CLANG_WARN_BOOL_CONVERSION = YES; -+ CLANG_WARN_COMMA = YES; -+ CLANG_WARN_CONSTANT_CONVERSION = YES; -+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; -+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; -+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; -+ CLANG_WARN_EMPTY_BODY = YES; -+ CLANG_WARN_ENUM_CONVERSION = YES; -+ CLANG_WARN_INFINITE_RECURSION = YES; -+ CLANG_WARN_INT_CONVERSION = YES; -+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; -+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; -+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; -+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; -+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; -+ CLANG_WARN_STRICT_PROTOTYPES = YES; -+ CLANG_WARN_SUSPICIOUS_MOVE = YES; -+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; -+ CLANG_WARN_UNREACHABLE_CODE = YES; -+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; -+ COPY_PHASE_STRIP = NO; -+ DEBUG_INFORMATION_FORMAT = dwarf; -+ ENABLE_STRICT_OBJC_MSGSEND = YES; -+ ENABLE_TESTABILITY = YES; -+ ENABLE_USER_SCRIPT_SANDBOXING = YES; -+ GCC_C_LANGUAGE_STANDARD = gnu17; -+ GCC_DYNAMIC_NO_PIC = NO; -+ GCC_NO_COMMON_BLOCKS = YES; -+ GCC_OPTIMIZATION_LEVEL = 0; -+ GCC_PREPROCESSOR_DEFINITIONS = ( -+ "DEBUG=1", -+ "$(inherited)", -+ ); -+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; -+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; -+ GCC_WARN_UNDECLARED_SELECTOR = YES; -+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; -+ GCC_WARN_UNUSED_FUNCTION = YES; -+ GCC_WARN_UNUSED_VARIABLE = YES; -+ IPHONEOS_DEPLOYMENT_TARGET = 13.0; -+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; -+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; -+ MTL_FAST_MATH = YES; -+ ONLY_ACTIVE_ARCH = YES; -+ SDKROOT = iphoneos; -+ }; -+ name = Debug; -+ }; -+ 607A66402B0EFA3A0010BFC8 /* Release */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ALWAYS_SEARCH_USER_PATHS = NO; -+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; -+ CLANG_ANALYZER_NONNULL = YES; -+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; -+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; -+ CLANG_ENABLE_MODULES = YES; -+ CLANG_ENABLE_OBJC_ARC = YES; -+ CLANG_ENABLE_OBJC_WEAK = YES; -+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; -+ CLANG_WARN_BOOL_CONVERSION = YES; -+ CLANG_WARN_COMMA = YES; -+ CLANG_WARN_CONSTANT_CONVERSION = YES; -+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; -+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; -+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; -+ CLANG_WARN_EMPTY_BODY = YES; -+ CLANG_WARN_ENUM_CONVERSION = YES; -+ CLANG_WARN_INFINITE_RECURSION = YES; -+ CLANG_WARN_INT_CONVERSION = YES; -+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; -+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; -+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; -+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; -+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; -+ CLANG_WARN_STRICT_PROTOTYPES = YES; -+ CLANG_WARN_SUSPICIOUS_MOVE = YES; -+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; -+ CLANG_WARN_UNREACHABLE_CODE = YES; -+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; -+ COPY_PHASE_STRIP = NO; -+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; -+ ENABLE_NS_ASSERTIONS = NO; -+ ENABLE_STRICT_OBJC_MSGSEND = YES; -+ ENABLE_USER_SCRIPT_SANDBOXING = YES; -+ GCC_C_LANGUAGE_STANDARD = gnu17; -+ GCC_NO_COMMON_BLOCKS = YES; -+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; -+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; -+ GCC_WARN_UNDECLARED_SELECTOR = YES; -+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; -+ GCC_WARN_UNUSED_FUNCTION = YES; -+ GCC_WARN_UNUSED_VARIABLE = YES; -+ IPHONEOS_DEPLOYMENT_TARGET = 13.0; -+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; -+ MTL_ENABLE_DEBUG_INFO = NO; -+ MTL_FAST_MATH = YES; -+ SDKROOT = iphoneos; -+ VALIDATE_PRODUCT = YES; -+ }; -+ name = Release; -+ }; -+ 607A66422B0EFA3A0010BFC8 /* Debug */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; -+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ DEVELOPMENT_TEAM = ""; -+ ENABLE_USER_SCRIPT_SANDBOXING = NO; -+ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; -+ INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist"; -+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; -+ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; -+ INFOPLIST_KEY_UIMainStoryboardFile = Main; -+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; -+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; -+ IPHONEOS_DEPLOYMENT_TARGET = 13.0; -+ LD_RUNPATH_SEARCH_PATHS = ( -+ "$(inherited)", -+ "@executable_path/Frameworks", -+ ); -+ MARKETING_VERSION = 3.13.0a1; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = YES; -+ TARGETED_DEVICE_FAMILY = "1,2"; -+ }; -+ name = Debug; -+ }; -+ 607A66432B0EFA3A0010BFC8 /* Release */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; -+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ DEVELOPMENT_TEAM = ""; -+ ENABLE_TESTABILITY = YES; -+ ENABLE_USER_SCRIPT_SANDBOXING = NO; -+ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; -+ INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist"; -+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; -+ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; -+ INFOPLIST_KEY_UIMainStoryboardFile = Main; -+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; -+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; -+ IPHONEOS_DEPLOYMENT_TARGET = 13.0; -+ LD_RUNPATH_SEARCH_PATHS = ( -+ "$(inherited)", -+ "@executable_path/Frameworks", -+ ); -+ MARKETING_VERSION = 3.13.0a1; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = YES; -+ TARGETED_DEVICE_FAMILY = "1,2"; -+ }; -+ name = Release; -+ }; -+ 607A66452B0EFA3A0010BFC8 /* Debug */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ BUNDLE_LOADER = "$(TEST_HOST)"; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ DEVELOPMENT_TEAM = 3HEZE76D99; -+ GENERATE_INFOPLIST_FILE = YES; -+ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; -+ IPHONEOS_DEPLOYMENT_TARGET = 13.0; -+ MARKETING_VERSION = 1.0; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = NO; -+ TARGETED_DEVICE_FAMILY = "1,2"; -+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSTestbed"; -+ }; -+ name = Debug; -+ }; -+ 607A66462B0EFA3A0010BFC8 /* Release */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ BUNDLE_LOADER = "$(TEST_HOST)"; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ DEVELOPMENT_TEAM = 3HEZE76D99; -+ GENERATE_INFOPLIST_FILE = YES; -+ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; -+ IPHONEOS_DEPLOYMENT_TARGET = 13.0; -+ MARKETING_VERSION = 1.0; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = NO; -+ TARGETED_DEVICE_FAMILY = "1,2"; -+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSTestbed"; -+ }; -+ name = Release; -+ }; -+/* End XCBuildConfiguration section */ -+ -+/* Begin XCConfigurationList section */ -+ 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "iOSTestbed" */ = { -+ isa = XCConfigurationList; -+ buildConfigurations = ( -+ 607A663F2B0EFA3A0010BFC8 /* Debug */, -+ 607A66402B0EFA3A0010BFC8 /* Release */, -+ ); -+ defaultConfigurationIsVisible = 0; -+ defaultConfigurationName = Release; -+ }; -+ 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbed" */ = { -+ isa = XCConfigurationList; -+ buildConfigurations = ( -+ 607A66422B0EFA3A0010BFC8 /* Debug */, -+ 607A66432B0EFA3A0010BFC8 /* Release */, -+ ); -+ defaultConfigurationIsVisible = 0; -+ defaultConfigurationName = Release; -+ }; -+ 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbedTests" */ = { -+ isa = XCConfigurationList; -+ buildConfigurations = ( -+ 607A66452B0EFA3A0010BFC8 /* Debug */, -+ 607A66462B0EFA3A0010BFC8 /* Release */, -+ ); -+ defaultConfigurationIsVisible = 0; -+ defaultConfigurationName = Release; -+ }; -+/* End XCConfigurationList section */ -+ }; -+ rootObject = 607A660A2B0EFA380010BFC8 /* Project object */; -+} ---- /dev/null -+++ b/Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme -@@ -0,0 +1,97 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ ---- /dev/null -+++ b/Apple/testbed/iOSTestbed.xctestplan -@@ -0,0 +1,46 @@ -+{ -+ "configurations" : [ -+ { -+ "id" : "F5A95CE4-1ADE-4A6E-A0E1-CDBAE26DF0C5", -+ "name" : "Test Scheme Action", -+ "options" : { -+ -+ } -+ } -+ ], -+ "defaultOptions" : { -+ "commandLineArgumentEntries" : [ -+ { -+ "argument" : "test" -+ }, -+ { -+ "argument" : "-uall" -+ }, -+ { -+ "argument" : "--single-process" -+ }, -+ { -+ "argument" : "--rerun" -+ }, -+ { -+ "argument" : "-W" -+ } -+ ], -+ "targetForVariableExpansion" : { -+ "containerPath" : "container:iOSTestbed.xcodeproj", -+ "identifier" : "607A66112B0EFA380010BFC8", -+ "name" : "iOSTestbed" -+ } -+ }, -+ "testTargets" : [ -+ { -+ "parallelizable" : false, -+ "target" : { -+ "containerPath" : "container:iOSTestbed.xcodeproj", -+ "identifier" : "607A662C2B0EFA3A0010BFC8", -+ "name" : "iOSTestbedTests" -+ } -+ } -+ ], -+ "version" : 1 -+} ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/AppDelegate.h -@@ -0,0 +1,11 @@ -+// -+// AppDelegate.h -+// iOSTestbed -+// -+ -+#import -+ -+@interface AppDelegate : UIResponder -+ -+ -+@end ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/AppDelegate.m -@@ -0,0 +1,19 @@ -+// -+// AppDelegate.m -+// iOSTestbed -+// -+ -+#import "AppDelegate.h" -+ -+@interface AppDelegate () -+ -+@end -+ -+@implementation AppDelegate -+ -+ -+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { -+ return YES; -+} -+ -+@end ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json -@@ -0,0 +1,11 @@ -+{ -+ "colors" : [ -+ { -+ "idiom" : "universal" -+ } -+ ], -+ "info" : { -+ "author" : "xcode", -+ "version" : 1 -+ } -+} ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json -@@ -0,0 +1,13 @@ -+{ -+ "images" : [ -+ { -+ "idiom" : "universal", -+ "platform" : "ios", -+ "size" : "1024x1024" -+ } -+ ], -+ "info" : { -+ "author" : "xcode", -+ "version" : 1 -+ } -+} ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/Assets.xcassets/Contents.json -@@ -0,0 +1,6 @@ -+{ -+ "info" : { -+ "author" : "xcode", -+ "version" : 1 -+ } -+} ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard -@@ -0,0 +1,9 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/app/README -@@ -0,0 +1,7 @@ -+This folder can contain any Python application code. -+ -+During the build, any binary modules found in this folder will be processed into -+Framework form. -+ -+When the test suite runs, this folder will be on the PYTHONPATH, and will be the -+working directory for the test suite. ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/app_packages/README -@@ -0,0 +1,7 @@ -+This folder can be a target for installing any Python dependencies needed by the -+test suite. -+ -+During the build, any binary modules found in this folder will be processed into -+Framework form. -+ -+When the test suite runs, this folder will be on the PYTHONPATH. ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSupportedPlatforms ++ ++ tvOS ++ ++ MinimumOSVersion ++ 9.0 ++ CFBundleVersion ++ 1 ++ ++ +diff --git a/Apple/testbed/Python.xcframework/build/utils.sh b/Apple/testbed/Python.xcframework/build/utils.sh +index 961c46d014b..76172162487 100755 +--- a/Apple/testbed/Python.xcframework/build/utils.sh ++++ b/Apple/testbed/Python.xcframework/build/utils.sh +@@ -34,9 +34,38 @@ + else + SLICE_FOLDER="ios-arm64_x86_64-simulator" + fi +- else ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-iphoneos" ]; then + echo "Installing Python modules for iOS Device" + SLICE_FOLDER="ios-arm64" ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-appletvsimulator" ]; then ++ echo "Installing Python modules for tvOS Simulator" ++ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/tvos-arm64-simulator" ]; then ++ SLICE_FOLDER="tvos-arm64-simulator" ++ else ++ SLICE_FOLDER="tvos-arm64_x86_64-simulator" ++ fi ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-appletvos" ]; then ++ echo "Installing Python modules for tvOS Device" ++ SLICE_FOLDER="tvos-arm64" ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-watchsimulator" ]; then ++ echo "Installing Python modules for watchOS Simulator" ++ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/watchos-arm64-simulator" ]; then ++ SLICE_FOLDER="watchos-arm64-simulator" ++ else ++ SLICE_FOLDER="watchos-arm64_x86_64-simulator" ++ fi ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-watchos" ]; then ++ echo "Installing Python modules for watchOS Device" ++ SLICE_FOLDER="watchos-arm64" ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-xrsimulator" ]; then ++ echo "Installing Python modules for visionOS Simulator" ++ SLICE_FOLDER="xros-arm64-simulator" ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-xros" ]; then ++ echo "Installing Python modules for visionOS Device" ++ SLICE_FOLDER="xros-arm64" ++ else ++ echo "Unsupported platform name $EFFECTIVE_PLATFORM_NAME" ++ exit 1 + fi + + # If the XCframework has a shared lib folder, then it's a full framework. --- /dev/null -+++ b/Apple/testbed/iOSTestbed/iOSTestbed-Info.plist -@@ -0,0 +1,52 @@ ++++ b/Apple/testbed/Python.xcframework/build/watchOS-dylib-Info-template.plist +@@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en -+ CFBundleDisplayName -+ ${PRODUCT_NAME} + CFBundleExecutable -+ ${EXECUTABLE_NAME} ++ + CFBundleIdentifier -+ org.python.iOSTestbed ++ + CFBundleInfoDictionaryVersion + 6.0 -+ CFBundleName -+ ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 -+ CFBundleSignature -+ ???? ++ CFBundleSupportedPlatforms ++ ++ watchOS ++ ++ MinimumOSVersion ++ 4.0 + CFBundleVersion + 1 -+ LSRequiresIPhoneOS -+ -+ UIRequiresFullScreen -+ -+ UILaunchStoryboardName -+ Launch Screen -+ UISupportedInterfaceOrientations ++ ++ +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/build/xrOS-dylib-Info-template.plist +@@ -0,0 +1,30 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundleExecutable ++ ++ CFBundleIdentifier ++ ++ CFBundlePackageType ++ FMWK ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSupportedPlatforms + -+ UIInterfaceOrientationPortrait -+ UIInterfaceOrientationLandscapeLeft -+ UIInterfaceOrientationLandscapeRight ++ XROS + -+ UISupportedInterfaceOrientations~ipad ++ CFBundleVersion ++ 1 ++ MinimumOSVersion ++ 2.0 ++ UIDeviceFamily + -+ UIInterfaceOrientationPortrait -+ UIInterfaceOrientationPortraitUpsideDown -+ UIInterfaceOrientationLandscapeLeft -+ UIInterfaceOrientationLandscapeRight ++ 7 + -+ UIApplicationSceneManifest -+ -+ UIApplicationSupportsMultipleScenes -+ -+ UISceneConfigurations -+ -+ + + --- /dev/null -+++ b/Apple/testbed/iOSTestbed/main.m -@@ -0,0 +1,16 @@ -+// -+// main.m -+// iOSTestbed -+// ++++ b/Apple/testbed/Python.xcframework/tvos-arm64/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. + -+#import -+#import "AppDelegate.h" ++It should be used as a target for `--enable-framework` when compiling a tvOS ++on-device build for testing purposes. +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/tvos-arm64_x86_64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. + -+int main(int argc, char * argv[]) { -+ NSString * appDelegateClassName; -+ @autoreleasepool { -+ appDelegateClassName = NSStringFromClass([AppDelegate class]); ++It should be used as a target for `--enable-framework` when compiling a tvOS ++simulator build for testing purposes (either x86_64 or ARM64). +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/watchos-arm64_32/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. + -+ return UIApplicationMain(argc, argv, nil, appDelegateClassName); -+ } -+} ++It should be used as a target for `--enable-framework` when compiling a watchOS on-device ++build for testing purposes. +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/watchos-arm64_x86_64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a watchOS ++simulator build for testing purposes (either x86_64 or ARM64). +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/xros-arm64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling an visionOS simulator ++build for testing purposes (either x86_64 or ARM64). +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/xros-arm64/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling an visionOS on-device ++build for testing purposes. +diff --git a/Apple/testbed/__main__.py b/Apple/testbed/__main__.py +index 4a1333380cd..41877db5b4f 100644 +--- a/Apple/testbed/__main__.py ++++ b/Apple/testbed/__main__.py +@@ -8,6 +8,9 @@ + + TEST_SLICES = { + "iOS": "ios-arm64_x86_64-simulator", ++ "tvOS": "tvos-arm64_x86_64-simulator", ++ "visionOS": "xros-arm64-simulator", ++ "watchOS": "watchos-arm64_x86_64-simulator", + } + + DECODE_ARGS = ("UTF-8", "backslashreplace") +@@ -19,7 +22,7 @@ + LOG_PREFIX_REGEX = re.compile( + r"^\d{4}-\d{2}-\d{2}" # YYYY-MM-DD + r"\s+\d+:\d{2}:\d{2}\.\d+\+\d{4}" # HH:MM:SS.ssssss+ZZZZ +- r"\s+iOSTestbed\[\d+:\w+\]" # Process/thread ID ++ r"\s+.*Testbed\[\d+:\w+\]" # Process/thread ID + ) + + +@@ -52,6 +55,24 @@ + ) + ) + simulator = se_simulators[-1][1] ++ elif platform == "tvOS": ++ # Find the most recent tvOS release. ++ simulators = sorted( ++ (devicetype["minRuntimeVersion"], devicetype["name"]) ++ for devicetype in json_data["devicetypes"] ++ if devicetype["productFamily"] == "Apple TV" ++ ) ++ simulator = simulators[-1][1] ++ elif platform == "visionOS": ++ # Find the most recent visionOS release. ++ simulators = sorted( ++ (devicetype["minRuntimeVersion"], devicetype["name"]) ++ for devicetype in json_data["devicetypes"] ++ if devicetype["productFamily"] == "Apple Vision" ++ ) ++ simulator = simulators[-1][1] ++ elif platform == "watchOS": ++ raise NotImplementedError(f"Don't know how to launch watchOS (yet)") + else: + raise ValueError(f"Unknown platform {platform}") + +@@ -279,7 +300,7 @@ + # many platforms, but when cloned, only one platform is preserved. + available_platforms = [ + platform +- for platform in ["iOS"] ++ for platform in ["iOS", "tvOS", "visionOS", "watchOS"] + if (Path(__file__).parent / f"{platform}Testbed").is_dir() + ] + +@@ -329,7 +350,7 @@ + usage="%(prog)s [-h] [--simulator SIMULATOR] -- [ ...]", + description=( + "Run a testbed project. The arguments provided after `--` will be " +- "passed to the running iOS process as if they were arguments to " ++ "passed to the running test process as if they were arguments to " + "`python -m`." + ), + help="Run a testbed project", --- /dev/null +++ b/Apple/testbed/tvOSTestbed.xcodeproj/project.pbxproj @@ -0,0 +1,505 @@ @@ -5806,262 +2834,6 @@ +#ifdef __x86_64__ +#include "pyconfig-x86_64.h" +#endif -diff --git a/Doc/using/ios.rst b/Doc/using/ios.rst -index 91cfed16f0e..5e4033fb6ce 100644 ---- a/Doc/using/ios.rst -+++ b/Doc/using/ios.rst -@@ -170,7 +170,7 @@ - To add Python to an iOS Xcode project: - - 1. Build or obtain a Python ``XCFramework``. See the instructions in -- :source:`iOS/README.rst` (in the CPython source distribution) for details on -+ :source:`Apple/iOS/README.md` (in the CPython source distribution) for details on - how to build a Python ``XCFramework``. At a minimum, you will need a build - that supports ``arm64-apple-ios``, plus one of either - ``arm64-apple-ios-simulator`` or ``x86_64-apple-ios-simulator``. -@@ -180,22 +180,19 @@ - of your project; however, you can use any other location that you want by - adjusting paths as needed. - --3. Drag the ``iOS/Resources/dylib-Info-template.plist`` file into your project, -- and ensure it is associated with the app target. -- --4. Add your application code as a folder in your Xcode project. In the -+3. Add your application code as a folder in your Xcode project. In the - following instructions, we'll assume that your user code is in a folder - named ``app`` in the root of your project; you can use any other location by - adjusting paths as needed. Ensure that this folder is associated with your - app target. - --5. Select the app target by selecting the root node of your Xcode project, then -+4. Select the app target by selecting the root node of your Xcode project, then - the target name in the sidebar that appears. - --6. In the "General" settings, under "Frameworks, Libraries and Embedded -+5. In the "General" settings, under "Frameworks, Libraries and Embedded - Content", add ``Python.xcframework``, with "Embed & Sign" selected. - --7. In the "Build Settings" tab, modify the following: -+6. In the "Build Settings" tab, modify the following: - - - Build Options - -@@ -211,86 +208,24 @@ - - * Quoted Include In Framework Header: No - --8. Add a build step that copies the Python standard library into your app. In -- the "Build Phases" tab, add a new "Run Script" build step *before* the -- "Embed Frameworks" step, but *after* the "Copy Bundle Resources" step. Name -- the step "Install Target Specific Python Standard Library", disable the -- "Based on dependency analysis" checkbox, and set the script content to: -+7. Add a build step that processes the Python standard library, and your own -+ Python binary dependencies. In the "Build Phases" tab, add a new "Run -+ Script" build step *before* the "Embed Frameworks" step, but *after* the -+ "Copy Bundle Resources" step. Name the step "Process Python libraries", -+ disable the "Based on dependency analysis" checkbox, and set the script -+ content to: - - .. code-block:: bash - -- set -e -- -- mkdir -p "$CODESIGNING_FOLDER_PATH/python/lib" -- if [ "$EFFECTIVE_PLATFORM_NAME" = "-iphonesimulator" ]; then -- echo "Installing Python modules for iOS Simulator" -- rsync -au --delete "$PROJECT_DIR/Python.xcframework/ios-arm64_x86_64-simulator/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" -- else -- echo "Installing Python modules for iOS Device" -- rsync -au --delete "$PROJECT_DIR/Python.xcframework/ios-arm64/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" -- fi -+ set -e -+ source $PROJECT_DIR/Python.xcframework/build/build_utils.sh -+ install_python Python.xcframework app - -- Note that the name of the simulator "slice" in the XCframework may be -- different, depending the CPU architectures your ``XCFramework`` supports. -+ If you have placed your XCframework somewhere other than the root of your -+ project, modify the path to the first argument. - --9. Add a second build step that processes the binary extension modules in the -- standard library into "Framework" format. Add a "Run Script" build step -- *directly after* the one you added in step 8, named "Prepare Python Binary -- Modules". It should also have "Based on dependency analysis" unchecked, with -- the following script content: -- -- .. code-block:: bash -- -- set -e -- -- install_dylib () { -- INSTALL_BASE=$1 -- FULL_EXT=$2 -- -- # The name of the extension file -- EXT=$(basename "$FULL_EXT") -- # The location of the extension file, relative to the bundle -- RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} -- # The path to the extension file, relative to the install base -- PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/} -- # The full dotted name of the extension module, constructed from the file path. -- FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d "." -f 1 | tr "/" "."); -- # A bundle identifier; not actually used, but required by Xcode framework packaging -- FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr "_" "-") -- # The name of the framework folder. -- FRAMEWORK_FOLDER="Frameworks/$FULL_MODULE_NAME.framework" -- -- # If the framework folder doesn't exist, create it. -- if [ ! -d "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ]; then -- echo "Creating framework for $RELATIVE_EXT" -- mkdir -p "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" -- cp "$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" -- plutil -replace CFBundleExecutable -string "$FULL_MODULE_NAME" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" -- plutil -replace CFBundleIdentifier -string "$FRAMEWORK_BUNDLE_ID" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" -- fi -- -- echo "Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME" -- mv "$FULL_EXT" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" -- # Create a placeholder .fwork file where the .so was -- echo "$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" > ${FULL_EXT%.so}.fwork -- # Create a back reference to the .so file location in the framework -- echo "${RELATIVE_EXT%.so}.fwork" > "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin" -- } -- -- PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib") -- echo "Install Python $PYTHON_VER standard library extension modules..." -- find "$CODESIGNING_FOLDER_PATH/python/lib/$PYTHON_VER/lib-dynload" -name "*.so" | while read FULL_EXT; do -- install_dylib python/lib/$PYTHON_VER/lib-dynload/ "$FULL_EXT" -- done -- -- # Clean up dylib template -- rm -f "$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist" -- -- echo "Signing frameworks as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)..." -- find "$CODESIGNING_FOLDER_PATH/Frameworks" -name "*.framework" -exec /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der "{}" \; -- --10. Add Objective C code to initialize and use a Python interpreter in embedded -- mode. You should ensure that: -+8. Add Objective C code to initialize and use a Python interpreter in embedded -+ mode. You should ensure that: - - * UTF-8 mode (:c:member:`PyPreConfig.utf8_mode`) is *enabled*; - * Buffered stdio (:c:member:`PyConfig.buffered_stdio`) is *disabled*; -@@ -309,22 +244,19 @@ - Your app's bundle location can be determined using ``[[NSBundle mainBundle] - resourcePath]``. - --Steps 8, 9 and 10 of these instructions assume that you have a single folder of -+Steps 7 and 8 of these instructions assume that you have a single folder of - pure Python application code, named ``app``. If you have third-party binary - modules in your app, some additional steps will be required: - - * You need to ensure that any folders containing third-party binaries are -- either associated with the app target, or copied in as part of step 8. Step 8 -- should also purge any binaries that are not appropriate for the platform a -- specific build is targeting (i.e., delete any device binaries if you're -- building an app targeting the simulator). -- --* Any folders that contain third-party binaries must be processed into -- framework form by step 9. The invocation of ``install_dylib`` that processes -- the ``lib-dynload`` folder can be copied and adapted for this purpose. -+ either associated with the app target, or are explicitly copied as part of -+ step 7. Step 7 should also purge any binaries that are not appropriate for -+ the platform a specific build is targeting (i.e., delete any device binaries -+ if you're building an app targeting the simulator). - --* If you're using a separate folder for third-party packages, ensure that folder -- is included as part of the :envvar:`PYTHONPATH` configuration in step 10. -+* If you're using a separate folder for third-party packages, ensure that -+ folder is added to the end of the call to ``install_python`` in step 7, and -+ as part of the :envvar:`PYTHONPATH` configuration in step 8. - - * If any of the folders that contain third-party packages will contain ``.pth`` - files, you should add that folder as a *site directory* (using -@@ -334,25 +266,30 @@ - Testing a Python package - ------------------------ - --The CPython source tree contains :source:`a testbed project ` that -+The CPython source tree contains :source:`a testbed project ` that - is used to run the CPython test suite on the iOS simulator. This testbed can also - be used as a testbed project for running your Python library's test suite on iOS. - --After building or obtaining an iOS XCFramework (See :source:`iOS/README.rst` --for details), create a clone of the Python iOS testbed project by running: -+After building or obtaining an iOS XCFramework (see :source:`Apple/iOS/README.md` -+for details), create a clone of the Python iOS testbed project. If you used the -+``Apple`` build script to build the XCframework, you can run: -+ -+.. code-block:: bash -+ -+ $ python cross-build/iOS/testbed clone --app --app app-testbed -+ -+Or, if you've sourced your own XCframework, by running: - - .. code-block:: bash - -- $ python iOS/testbed clone --framework --app --app app-testbed -+ $ python Apple/testbed clone --platform iOS --framework --app --app app-testbed - --You will need to modify the ``iOS/testbed`` reference to point to that --directory in the CPython source tree; any folders specified with the ``--app`` --flag will be copied into the cloned testbed project. The resulting testbed will --be created in the ``app-testbed`` folder. In this example, the ``module1`` and --``module2`` would be importable modules at runtime. If your project has --additional dependencies, they can be installed into the --``app-testbed/iOSTestbed/app_packages`` folder (using ``pip install --target --app-testbed/iOSTestbed/app_packages`` or similar). -+Any folders specified with the ``--app`` flag will be copied into the cloned -+testbed project. The resulting testbed will be created in the ``app-testbed`` -+folder. In this example, the ``module1`` and ``module2`` would be importable -+modules at runtime. If your project has additional dependencies, they can be -+installed into the ``app-testbed/Testbed/app_packages`` folder (using ``pip -+install --target app-testbed/Testbed/app_packages`` or similar). - - You can then use the ``app-testbed`` folder to run the test suite for your app, - For example, if ``module1.tests`` was the entry point to your test suite, you -@@ -381,7 +318,7 @@ - arguments. - - The test plan also disables parallel testing, and specifies the use of the --``iOSTestbed.lldbinit`` file for providing configuration of the debugger. The -+``Testbed.lldbinit`` file for providing configuration of the debugger. The - default debugger configuration disables automatic breakpoints on the - ``SIGINT``, ``SIGUSR1``, ``SIGUSR2``, and ``SIGXFSZ`` signals. - -@@ -391,7 +328,12 @@ - The only mechanism for distributing apps to third-party iOS devices is to - submit the app to the iOS App Store; apps submitted for distribution must pass - Apple's app review process. This process includes a set of automated validation --rules that inspect the submitted application bundle for problematic code. -+rules that inspect the submitted application bundle for problematic code. There -+are some steps that must be taken to ensure that your app will be able to pass -+these validation steps. -+ -+Incompatible code in the standard library -+----------------------------------------- - - The Python standard library contains some code that is known to violate these - automated rules. While these violations appear to be false positives, Apple's -@@ -402,3 +344,18 @@ - :source:`a patch file ` that will remove - all code that is known to cause issues with the App Store review process. This - patch is applied automatically when building for iOS. -+ -+Privacy manifests -+----------------- -+ -+In April 2025, Apple introduced a requirement for `certain third-party -+libraries to provide a Privacy Manifest -+`__. -+As a result, if you have a binary module that uses one of the affected -+libraries, you must provide an ``.xcprivacy`` file for that library. -+OpenSSL is one library affected by this requirement, but there are others. -+ -+If you produce a binary module named ``mymodule.so``, and use you the Xcode -+build script described in step 7 above, you can place a ``mymodule.xcprivacy`` -+file next to ``mymodule.so``, and the privacy manifest will be installed into -+the required location when the binary module is converted into a framework. diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 823a3692fd1..69550af2087 100644 --- a/Lib/ctypes/__init__.py @@ -6124,10 +2896,10 @@ index 8bcd741c446..db11cbe0945 100644 suffix.replace(".so", ".fwork") for suffix in _imp.extension_suffixes() diff --git a/Lib/platform.py b/Lib/platform.py -index 86141f072d2..5c77e30d8b5 100644 +index 784b6b749b7..4d3be1aca18 100644 --- a/Lib/platform.py +++ b/Lib/platform.py -@@ -534,6 +534,78 @@ +@@ -539,6 +539,78 @@ return IOSVersionInfo(system, release, model, is_simulator) @@ -6206,7 +2978,7 @@ index 86141f072d2..5c77e30d8b5 100644 def _java_getprop(name, default): """This private helper is deprecated in 3.13 and will be removed in 3.15""" from java.lang import System -@@ -733,7 +805,7 @@ +@@ -738,7 +810,7 @@ default in case the command should fail. """ @@ -6215,7 +2987,7 @@ index 86141f072d2..5c77e30d8b5 100644 # XXX Others too ? return default -@@ -897,14 +969,30 @@ +@@ -902,14 +974,30 @@ csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0) return 'Alpha' if cpu_number >= 128 else 'VAX' @@ -6249,7 +3021,7 @@ index 86141f072d2..5c77e30d8b5 100644 def from_subprocess(): """ Fall back to `uname -p` -@@ -1064,9 +1152,15 @@ +@@ -1069,9 +1157,15 @@ system = 'Android' release = android_ver().release @@ -6266,7 +3038,7 @@ index 86141f072d2..5c77e30d8b5 100644 vals = system, node, release, version, machine # Replace 'unknown' values with the more portable '' -@@ -1356,6 +1450,12 @@ +@@ -1361,6 +1455,12 @@ # macOS and iOS both report as a "Darwin" kernel if sys.platform == "ios": system, release, _, _ = ios_ver() @@ -6308,7 +3080,7 @@ index 54c2eb515b6..e02063aefea 100644 if _mswindows: import _winapi diff --git a/Lib/sysconfig/__init__.py b/Lib/sysconfig/__init__.py -index f93b98dd681..07ec7853f85 100644 +index 2ecbff222fe..542d3f21cae 100644 --- a/Lib/sysconfig/__init__.py +++ b/Lib/sysconfig/__init__.py @@ -23,6 +23,9 @@ @@ -6330,7 +3102,7 @@ index f93b98dd681..07ec7853f85 100644 return None def joinuser(*args): -@@ -730,6 +733,18 @@ +@@ -734,6 +737,18 @@ release = get_config_vars().get("IPHONEOS_DEPLOYMENT_TARGET", "13.0") osname = sys.platform machine = sys.implementation._multiarch @@ -6366,10 +3138,10 @@ index 1c1cbd03d02..1378985de4a 100644 else: extension_loader = "ExtensionFileLoader" diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py -index a719e49ef37..c0cacd2a8d5 100644 +index 88f61103512..4b44e03319c 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py -@@ -563,7 +563,7 @@ +@@ -573,7 +573,7 @@ sys.platform == "android", f"Android blocks {name} with SELinux" ) @@ -6378,7 +3150,7 @@ index a719e49ef37..c0cacd2a8d5 100644 unix_shell = '/system/bin/sh' if is_android else '/bin/sh' else: unix_shell = None -@@ -582,7 +582,7 @@ +@@ -592,7 +592,7 @@ def skip_wasi_stack_overflow(): return unittest.skipIf(is_wasi, "Exhausts stack on WASI") @@ -6413,7 +3185,7 @@ index 15603dc3d77..bff6c0fb95f 100644 if WINDOWS: KNOWN_LIBRARIES = ["KERNEL32.DLL"] diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py -index 6224c989e65..c5ccf225662 100644 +index 187a3d54809..3c6195bea69 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -271,13 +271,21 @@ @@ -6483,7 +3255,7 @@ index f2e2394089d..2efbbfb0014 100644 if objc: # If objc exists, we know ctypes is also importable. diff --git a/Makefile.pre.in b/Makefile.pre.in -index 764eef5be3e..19a332ffdcb 100644 +index ba039794c88..19a332ffdcb 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -209,6 +209,12 @@ @@ -6499,39 +3271,6 @@ index 764eef5be3e..19a332ffdcb 100644 # Option to install to strip binaries STRIPFLAG=-s -@@ -2315,7 +2321,7 @@ - fi - - # Clone the testbed project into the XCFOLDER -- $(PYTHON_FOR_BUILD) $(srcdir)/iOS/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)" -+ $(PYTHON_FOR_BUILD) $(srcdir)/Apple/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)" - - # Run the testbed project - $(PYTHON_FOR_BUILD) "$(XCFOLDER)" run --verbose -- test -uall --single-process --rerun -W -@@ -3219,10 +3225,10 @@ - -find build -type f -a ! -name '*.gc??' -exec rm -f {} ';' - -rm -f Include/pydtrace_probes.h - -rm -f profile-gen-stamp -- -rm -rf iOS/testbed/Python.xcframework/ios-*/bin -- -rm -rf iOS/testbed/Python.xcframework/ios-*/lib -- -rm -rf iOS/testbed/Python.xcframework/ios-*/include -- -rm -rf iOS/testbed/Python.xcframework/ios-*/Python.framework -+ -rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/bin -+ -rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/lib -+ -rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/include -+ -rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/Python.framework - - .PHONY: profile-removal - profile-removal: -@@ -3248,7 +3254,7 @@ - config.cache config.log pyconfig.h Modules/config.c - -rm -rf build platform - -rm -rf $(PYTHONFRAMEWORKDIR) -- -rm -rf iOS/Frameworks -+ -rm -rf Apple/iOS/Frameworks - -rm -rf iOSTestbed.* - -rm -f python-config.py python-config - -rm -rf cross-build diff --git a/Misc/platform_triplet.c b/Misc/platform_triplet.c index f5cd73bdea8..6c1863c943b 100644 --- a/Misc/platform_triplet.c @@ -6592,7 +3331,7 @@ index 1bb6a05dc11..49febd56a37 100755 none--*) # None (no kernel, i.e. freestanding / bare metal), diff --git a/configure b/configure -index 6383271b477..b177687b0bc 100755 +index d31c24dffa2..b177687b0bc 100755 --- a/configure +++ b/configure @@ -982,6 +982,10 @@ @@ -6708,7 +3447,7 @@ index 6383271b477..b177687b0bc 100755 yes) case $ac_sys_system in - Darwin) enableval=/Library/Frameworks ;; -- iOS) enableval=iOS/Frameworks/\$\(MULTIARCH\) ;; +- iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; + Darwin) enableval=/Library/Frameworks ;; + iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; + tvOS) enableval=Apple/tvOS/Frameworks/\$\(MULTIARCH\) ;; @@ -6727,15 +3466,10 @@ index 6383271b477..b177687b0bc 100755 *) PYTHONFRAMEWORK= PYTHONFRAMEWORKDIR=no-framework -@@ -4470,9 +4533,54 @@ +@@ -4474,6 +4537,51 @@ + + ac_config_files="$ac_config_files Apple/iOS/Resources/Info.plist" - prefix=$PYTHONFRAMEWORKPREFIX - PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" -- RESSRCDIR=iOS/Resources -+ RESSRCDIR=Apple/iOS/Resources -+ -+ ac_config_files="$ac_config_files Apple/iOS/Resources/Info.plist" -+ + ;; + tvOS) : + FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" @@ -6778,12 +3512,12 @@ index 6383271b477..b177687b0bc 100755 + prefix=$PYTHONFRAMEWORKPREFIX + PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" + RESSRCDIR=Apple/watchOS/Resources - -- ac_config_files="$ac_config_files iOS/Resources/Info.plist" ++ + ac_config_files="$ac_config_files Apple/watchOS/Resources/Info.plist" - ++ ;; *) + as_fn_error $? "Unknown platform for framework build" "$LINENO" 5 @@ -4485,6 +4593,9 @@ e) case $ac_sys_system in @@ -7284,12 +4018,10 @@ index 6383271b477..b177687b0bc 100755 -@@ -35303,7 +35539,10 @@ - "Mac/PythonLauncher/Makefile") CONFIG_FILES="$CONFIG_FILES Mac/PythonLauncher/Makefile" ;; +@@ -35304,6 +35540,9 @@ "Mac/Resources/framework/Info.plist") CONFIG_FILES="$CONFIG_FILES Mac/Resources/framework/Info.plist" ;; "Mac/Resources/app/Info.plist") CONFIG_FILES="$CONFIG_FILES Mac/Resources/app/Info.plist" ;; -- "iOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES iOS/Resources/Info.plist" ;; -+ "Apple/iOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/iOS/Resources/Info.plist" ;; + "Apple/iOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/iOS/Resources/Info.plist" ;; + "Apple/tvOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/tvOS/Resources/Info.plist" ;; + "Apple/visionOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/visionOS/Resources/Info.plist" ;; + "Apple/watchOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/watchOS/Resources/Info.plist" ;; @@ -7297,7 +4029,7 @@ index 6383271b477..b177687b0bc 100755 "Misc/python.pc") CONFIG_FILES="$CONFIG_FILES Misc/python.pc" ;; "Misc/python-embed.pc") CONFIG_FILES="$CONFIG_FILES Misc/python-embed.pc" ;; diff --git a/configure.ac b/configure.ac -index 42d94776cc7..4cf79576a55 100644 +index af7a9623d7b..4cf79576a55 100644 --- a/configure.ac +++ b/configure.ac @@ -330,6 +330,15 @@ @@ -7402,7 +4134,7 @@ index 42d94776cc7..4cf79576a55 100644 yes) case $ac_sys_system in - Darwin) enableval=/Library/Frameworks ;; -- iOS) enableval=iOS/Frameworks/\$\(MULTIARCH\) ;; +- iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; + Darwin) enableval=/Library/Frameworks ;; + iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; + tvOS) enableval=Apple/tvOS/Frameworks/\$\(MULTIARCH\) ;; @@ -7421,16 +4153,10 @@ index 42d94776cc7..4cf79576a55 100644 *) PYTHONFRAMEWORK= PYTHONFRAMEWORKDIR=no-framework -@@ -666,9 +725,51 @@ +@@ -670,6 +729,48 @@ - prefix=$PYTHONFRAMEWORKPREFIX - PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" -- RESSRCDIR=iOS/Resources -+ RESSRCDIR=Apple/iOS/Resources - -- AC_CONFIG_FILES([iOS/Resources/Info.plist]) -+ AC_CONFIG_FILES([Apple/iOS/Resources/Info.plist]) -+ ;; + AC_CONFIG_FILES([Apple/iOS/Resources/Info.plist]) + ;; + tvOS) : + FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" + FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " @@ -7472,9 +4198,10 @@ index 42d94776cc7..4cf79576a55 100644 + RESSRCDIR=Apple/watchOS/Resources + + AC_CONFIG_FILES([Apple/watchOS/Resources/Info.plist]) - ;; ++ ;; *) AC_MSG_ERROR([Unknown platform for framework build]) + ;; @@ -678,6 +779,9 @@ ],[ case $ac_sys_system in