From 4a11615f0f6b8c96e2e0351a2b30a0effe31f728 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 21 Aug 2025 11:11:08 +0800 Subject: [PATCH 01/15] Migrate testbed location and add cross-platform build script. --- .gitignore | 18 +- Apple/__main__.py | 892 ++++++++++++++++++ {iOS => Apple/iOS}/README.rst | 217 ++--- {iOS => Apple/iOS}/Resources/Info.plist.in | 0 .../iOS}/Resources/bin/arm64-apple-ios-ar | 0 .../iOS}/Resources/bin/arm64-apple-ios-clang | 0 .../Resources/bin/arm64-apple-ios-clang++ | 0 .../iOS}/Resources/bin/arm64-apple-ios-cpp | 0 .../bin/arm64-apple-ios-simulator-ar | 0 .../bin/arm64-apple-ios-simulator-clang | 0 .../bin/arm64-apple-ios-simulator-clang++ | 0 .../bin/arm64-apple-ios-simulator-cpp | 0 .../bin/arm64-apple-ios-simulator-strip | 0 .../iOS}/Resources/bin/arm64-apple-ios-strip | 0 .../bin/x86_64-apple-ios-simulator-ar | 0 .../bin/x86_64-apple-ios-simulator-clang | 0 .../bin/x86_64-apple-ios-simulator-clang++ | 0 .../bin/x86_64-apple-ios-simulator-cpp | 0 .../bin/x86_64-apple-ios-simulator-strip | 0 .../iOS/Resources}/dylib-Info-template.plist | 2 +- {iOS => Apple/iOS}/Resources/pyconfig.h | 0 .../testbed/Python.xcframework/Info.plist | 0 .../Python.xcframework/ios-arm64/README | 0 .../ios-arm64_x86_64-simulator/README | 0 .../testbed/Testbed.lldbinit | 0 .../testbed/TestbedTests/TestbedTests.m | 8 +- {iOS => Apple}/testbed/__main__.py | 195 ++-- .../iOSTestbed.xcodeproj/project.pbxproj | 14 +- .../xcschemes/iOSTestbed.xcscheme | 2 +- {iOS => Apple}/testbed/iOSTestbed.xctestplan | 0 .../testbed/iOSTestbed/AppDelegate.h | 0 .../testbed/iOSTestbed/AppDelegate.m | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../iOSTestbed/Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 {iOS => Apple}/testbed/iOSTestbed/app/README | 2 +- .../testbed/iOSTestbed/app_packages/README | 2 +- .../iOSTestbed}/dylib-Info-template.plist | 2 +- .../testbed/iOSTestbed/iOSTestbed-Info.plist | 0 {iOS => Apple}/testbed/iOSTestbed/main.m | 0 Doc/using/ios.rst | 36 +- Mac/Resources/framework/Info.plist.in | 2 + Makefile.pre.in | 4 +- ...-08-27-11-14-53.gh-issue-138171.Suz8ob.rst | 3 + configure | 8 +- configure.ac | 6 +- 47 files changed, 1188 insertions(+), 225 deletions(-) create mode 100644 Apple/__main__.py rename {iOS => Apple/iOS}/README.rst (69%) rename {iOS => Apple/iOS}/Resources/Info.plist.in (100%) rename {iOS => Apple/iOS}/Resources/bin/arm64-apple-ios-ar (100%) rename {iOS => Apple/iOS}/Resources/bin/arm64-apple-ios-clang (100%) rename {iOS => Apple/iOS}/Resources/bin/arm64-apple-ios-clang++ (100%) rename {iOS => Apple/iOS}/Resources/bin/arm64-apple-ios-cpp (100%) rename {iOS => Apple/iOS}/Resources/bin/arm64-apple-ios-simulator-ar (100%) rename {iOS => Apple/iOS}/Resources/bin/arm64-apple-ios-simulator-clang (100%) rename {iOS => Apple/iOS}/Resources/bin/arm64-apple-ios-simulator-clang++ (100%) rename {iOS => Apple/iOS}/Resources/bin/arm64-apple-ios-simulator-cpp (100%) rename {iOS => Apple/iOS}/Resources/bin/arm64-apple-ios-simulator-strip (100%) rename {iOS => Apple/iOS}/Resources/bin/arm64-apple-ios-strip (100%) rename {iOS => Apple/iOS}/Resources/bin/x86_64-apple-ios-simulator-ar (100%) rename {iOS => Apple/iOS}/Resources/bin/x86_64-apple-ios-simulator-clang (100%) rename {iOS => Apple/iOS}/Resources/bin/x86_64-apple-ios-simulator-clang++ (100%) rename {iOS => Apple/iOS}/Resources/bin/x86_64-apple-ios-simulator-cpp (100%) rename {iOS => Apple/iOS}/Resources/bin/x86_64-apple-ios-simulator-strip (100%) rename {iOS/testbed/iOSTestbed => Apple/iOS/Resources}/dylib-Info-template.plist (96%) rename {iOS => Apple/iOS}/Resources/pyconfig.h (100%) rename {iOS => Apple}/testbed/Python.xcframework/Info.plist (100%) rename {iOS => Apple}/testbed/Python.xcframework/ios-arm64/README (100%) rename {iOS => Apple}/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README (100%) rename iOS/testbed/iOSTestbed.lldbinit => Apple/testbed/Testbed.lldbinit (100%) rename iOS/testbed/iOSTestbedTests/iOSTestbedTests.m => Apple/testbed/TestbedTests/TestbedTests.m (97%) rename {iOS => Apple}/testbed/__main__.py (59%) rename {iOS => Apple}/testbed/iOSTestbed.xcodeproj/project.pbxproj (97%) rename {iOS => Apple}/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme (97%) rename {iOS => Apple}/testbed/iOSTestbed.xctestplan (100%) rename {iOS => Apple}/testbed/iOSTestbed/AppDelegate.h (100%) rename {iOS => Apple}/testbed/iOSTestbed/AppDelegate.m (100%) rename {iOS => Apple}/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename {iOS => Apple}/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {iOS => Apple}/testbed/iOSTestbed/Assets.xcassets/Contents.json (100%) rename {iOS => Apple}/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard (100%) rename {iOS => Apple}/testbed/iOSTestbed/app/README (92%) rename {iOS => Apple}/testbed/iOSTestbed/app_packages/README (92%) rename {iOS/Resources => Apple/testbed/iOSTestbed}/dylib-Info-template.plist (96%) rename {iOS => Apple}/testbed/iOSTestbed/iOSTestbed-Info.plist (100%) rename {iOS => Apple}/testbed/iOSTestbed/main.m (100%) create mode 100644 Misc/NEWS.d/next/Tools-Demos/2025-08-27-11-14-53.gh-issue-138171.Suz8ob.rst diff --git a/.gitignore b/.gitignore index e842676d866bf8..394a1fdb9d90c8 100644 --- a/.gitignore +++ b/.gitignore @@ -71,15 +71,15 @@ Lib/test/data/* /Makefile /Makefile.pre /iOSTestbed.* -iOS/Frameworks/ -iOS/Resources/Info.plist -iOS/testbed/build -iOS/testbed/Python.xcframework/ios-*/bin -iOS/testbed/Python.xcframework/ios-*/include -iOS/testbed/Python.xcframework/ios-*/lib -iOS/testbed/Python.xcframework/ios-*/Python.framework -iOS/testbed/iOSTestbed.xcodeproj/project.xcworkspace -iOS/testbed/iOSTestbed.xcodeproj/xcuserdata +Apple/iOS/Frameworks/ +Apple/iOS/Resources/Info.plist +Apple/testbed/build +Apple/testbed/Python.xcframework/*-*/bin +Apple/testbed/Python.xcframework/*-*/include +Apple/testbed/Python.xcframework/*-*/lib +Apple/testbed/Python.xcframework/*-*/Python.framework +Apple/testbed/*Testbed.xcodeproj/project.xcworkspace +Apple/testbed/*Testbed.xcodeproj/xcuserdata Mac/Makefile Mac/PythonLauncher/Info.plist Mac/PythonLauncher/Makefile diff --git a/Apple/__main__.py b/Apple/__main__.py new file mode 100644 index 00000000000000..d9e7c53db4afe7 --- /dev/null +++ b/Apple/__main__.py @@ -0,0 +1,892 @@ +#!/usr/bin/env python3 +########################################################################## +# Apple XCframework build script +# +# 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. +# +# The simplest entry point is: +# +# $ python Apple ci iOS +# +# 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 +# * 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. +########################################################################## + +import argparse +import os +import platform +import re +import shlex +import shutil +import signal +import subprocess +import sys +import sysconfig +import time +from contextlib import contextmanager +from datetime import datetime, timezone +from os.path import basename, relpath +from pathlib import Path +from subprocess import CalledProcessError + + +SCRIPT_NAME = Path(__file__).name +PYTHON_DIR = Path(__file__).resolve().parent.parent + +CROSS_BUILD_DIR = PYTHON_DIR / "cross-build" + +HOSTS = { + # 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", + }, + }, +} + + +# 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 install_signal_handler(): + def signal_handler(*args): + os.kill(os.getpid(), signal.SIGINT) + + signal.signal(signal.SIGTERM, signal_handler) + + +def subdir(host, create=False): + path = CROSS_BUILD_DIR / host + 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, *, host=None, env=None, log=True, **kwargs): + 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) + + +# Format a command so it can be copied into a shell. Like shlex.join, but also +# accepts arguments which are Paths, or a single string/Path outside of a list. +def join_command(args): + if isinstance(args, (str, Path)): + return str(args) + else: + return shlex.join(map(str, args)) + + +# Format the environment so it can be pasted into a shell. +def print_env(env): + for key, value in sorted(env.items()): + print(f"export {key}={shlex.quote(value)}") + + +def apple_env(host): + env = { + "PATH": ":".join( + [ + str(PYTHON_DIR / "Apple/iOS/Resources/bin"), + str(subdir(host) / "prefix"), + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + "/Library/Apple/usr/bin", + ] + ), + } + + return env + + +def delete_path(name): + path = CROSS_BUILD_DIR / name + if path.exists(): + print(f"Deleting {path} ...") + shutil.rmtree(path) + + +def all_host_triples(platform): + triples = [] + for slice_name, slice_parts in HOSTS[platform].items(): + triples.extend(list(slice_parts)) + return triples + + +def clean(context, target="all"): + # 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(): + """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, subdir=None): + 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): + orig = os.getcwd() + os.chdir(subdir) + yield + os.chdir(orig) + + +def configure_build_python(context): + 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): + with ( + group("Compiling build Python"), + cwd(subdir("build")), + ): + run(["make", "-j", str(os.cpu_count())]) + + +def apple_target(host): + """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): + """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(host, prefix_dir, cache_dir=None): + 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.16-2", + "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 if cache_dir else prefix_dir, + ) + shutil.unpack_archive(archive_path, prefix_dir) + if cache_dir is None: + os.remove(archive_path) + + # Dynamic libraries will be preferentially linked over static; + # ensure that no dylibs are available in the prefix folder. + for dylib in prefix_dir.glob("**/*.dylib"): + dylib.unlink() + + +def download(url, target_dir): + 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, host=None): + 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(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", + "--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, host=None): + 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 multiarch_path(host_triple, multiarch): + return CROSS_BUILD_DIR / f"{host_triple}/Apple/iOS/Frameworks/{multiarch}" + + +def package_version(prefix_path): + 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. + 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 create_xcframework(context): + package_path = CROSS_BUILD_DIR / context.platform + package_path.mkdir(exist_ok=True) + + 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[context.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. 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 = ( + multiarch_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"] + + [ + ( + multiarch_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") + + # 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...") + for slice_name, slice_parts in HOSTS[context.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 = multiarch_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/{context.platform}/Resources/pyconfig.h", + slice_framework / "Headers/pyconfig.h", + ) + + # Copy the lib folder. If there's only one slice, we can copy the .so + # binary modules as is. Otherwise, we ignore .so files, and merge them + # into fat binaries in the next step. + print(f" - {slice_name} standard library") + shutil.copytree( + first_path / "lib", + slice_path / "lib", + ignore=( + None + if len(slice_parts) == 1 + else shutil.ignore_patterns("*.so") + ), + ) + + # If there's more than one slice, merge binary .so modules. + if len(slice_parts) > 1: + print(f" - {slice_name} merging binary modules") + lib_dirs = [ + CROSS_BUILD_DIR + / f"{host_triple}/Apple/iOS/Frameworks" + / f"{multiarch}/lib" + for host_triple, multiarch in slice_parts.items() + ] + + # The list of .so binary modules should be the same in each slice. + # Find all .so files in each slice; then sort and zip those lists. + # Zipping with strict=True means any length discrepancy will raise + # an error. + for lib_set in zip( + *(sorted(lib_dir.glob("**/*.so")) for lib_dir in lib_dirs), + strict=True, + ): + # An additional safety check - not only must the two lists of + # libraries be the same length, but they must have the same + # module names. Raise an error if there's any discrepancy. + relative_libs = set( + lib.relative_to(lib_dir.parent) + for lib_dir, lib in zip(lib_dirs, lib_set) + ) + if len(relative_libs) != 1: + raise RuntimeError( + f"Cannot merge non-matching libraries: {relative_libs}" + ) + + # Merge the per-arch .so files into a single "fat" binary. + relative_lib = next(iter(relative_libs)) + run( + [ + "lipo", + "-create", + "-output", + slice_path / relative_lib, + ] + + [ + ( + CROSS_BUILD_DIR + / f"{host_triple}/Apple/iOS/Frameworks/{multiarch}" + / relative_lib + ) + for host_triple, multiarch in slice_parts.items() + ] + ) + + print(f" - {slice_name} architecture-specific files") + for host_triple, multiarch in slice_parts.items(): + # Copy the host's pyconfig.h to an architecture-specific name. + arch = multiarch.split("-")[0] + host_path = ( + CROSS_BUILD_DIR + / host_triple + / "Apple/iOS/Frameworks" + / multiarch + ) + host_framework = host_path / "Python.framework" + shutil.copy( + host_framework / "Headers/pyconfig.h", + slice_framework / f"Headers/pyconfig-{arch}.h", + ) + + # Copy any files (such as sysconfig) that are multiarch-specific. + for path in host_path.glob(f"lib/**/*_{multiarch}.*"): + shutil.copy( + path, + slice_path / (path.relative_to(host_path)), + ) + + return version + + +def package(context): + if context.clean: + clean(context, "package") + + with group("Building package"): + # Create an XCframework + version = create_xcframework(context) + + # 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", + ] + ) + + # Build the final archive + archive_name = ( + CROSS_BUILD_DIR + / "dist" + / f"python-{version}-{context.platform}-XCframework" + ) + + print() + print("Create package archive...") + shutil.make_archive( + 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, host=None): + 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, host=None): + 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", context.simulator] if context.simulator else []) + + [ + "--", + "test", + "-uall", + "--single-process", + "--rerun", + "-W", + ] + ) + + +def ci(context): + clean(context, "all") + build(context, host="all") + test(context, host="all") + + +def parse_args(): + parser = argparse.ArgumentParser() + 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." + ), + ) + + 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): + 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(): + install_signal_handler() + + context = parse_args() + dispatch = { + "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_called_process_error(e) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/iOS/README.rst b/Apple/iOS/README.rst similarity index 69% rename from iOS/README.rst rename to Apple/iOS/README.rst index 4d38e5d7c307d1..9126ed2d3c182a 100644 --- a/iOS/README.rst +++ b/Apple/iOS/README.rst @@ -30,30 +30,6 @@ 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. -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``. - - .. admonition:: Use this option with care! - - 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 ``make testios`` target 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 on iOS ====================== @@ -80,9 +56,67 @@ 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 iOS build + +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``. + + .. admonition:: Use this option with care! + + 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 @@ -96,7 +130,7 @@ 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)/iOS/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" + $ 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 \ @@ -107,7 +141,7 @@ Python for the ARM64 iOS simulator will look something like:: In this invocation: -* ``iOS/Resources/bin`` has been added to the path, providing some shims for the +* ``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 @@ -117,7 +151,7 @@ In this invocation: cause significant problems with many C configuration systems which assume that ``CC`` will be a single executable. - To work around this problem, the ``iOS/Resources/bin`` folder contains some + 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 @@ -177,15 +211,14 @@ In this invocation: 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 (XZ, BZip2, LibFFI and OpenSSL). -This can be done by defining the ``LIBLZMA_CFLAGS``, ``LIBLZMA_LIBS``, -``BZIP2_CFLAGS``, ``BZIP2_LIBS``, ``LIBFFI_CFLAGS``, and ``LIBFFI_LIBS`` -environment variables, and the ``--with-openssl`` configure option. Versions of -these libraries pre-compiled for iOS can be found in `this repository -`__. 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. +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 `__. +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 @@ -193,84 +226,28 @@ 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. -Merge thin frameworks into fat frameworks ------------------------------------------ - -Once you've built a ``Python.framework`` for each ABI and architecture, you -must produce a "fat" framework for each ABI that contains all the architectures -for that ABI. - -The ``iphoneos`` build only needs to support a single architecture, so it can be -used without modification. - -If you only want to support a single simulator architecture, (e.g., only support -ARM64 simulators), you can use a single architecture ``Python.framework`` build. -However, if you want to create ``Python.xcframework`` that supports *all* -architectures, you'll need to merge the ``iphonesimulator`` builds for ARM64 and -x86_64 into a single "fat" framework. - -The "fat" framework can be constructed by performing a directory merge of the -content of the two "thin" ``Python.framework`` directories, plus the ``bin`` and -``lib`` folders for each thin framework. When performing this merge: - -* The pure Python standard library content is identical for each architecture, - except for a handful of platform-specific files (such as the ``sysconfig`` - module). Ensure that the "fat" framework has the union of all standard library - files. - -* Any binary files in the standard library, plus the main - ``libPython3.X.dylib``, can be merged using the ``lipo`` tool, provide by - Xcode:: - - $ lipo -create -output module.dylib path/to/x86_64/module.dylib path/to/arm64/module.dylib - -* The header files will be identical on both architectures, except for - ``pyconfig.h``. Copy all the headers from one platform (say, arm64), rename - ``pyconfig.h`` to ``pyconfig-arm64.h``, and copy the ``pyconfig.h`` for the - other architecture into the merged header folder as ``pyconfig-x86_64.h``. - Then copy the ``iOS/Resources/pyconfig.h`` file from the CPython sources into - the merged headers folder. This will allow the two Python architectures to - share a common ``pyconfig.h`` header file. - -At this point, you should have 2 Python.framework folders - one for ``iphoneos``, -and one for ``iphonesimulator`` that is a merge of x86+64 and ARM64 content. - -Merge frameworks into an XCframework ------------------------------------- - -Now that we have 2 (potentially fat) ABI-specific frameworks, we can merge those -frameworks into a single ``XCframework``. - -The initial skeleton of an ``XCframework`` is built using:: - - xcodebuild -create-xcframework -output Python.xcframework -framework path/to/iphoneos/Python.framework -framework path/to/iphonesimulator/Python.framework - -Then, copy the ``bin`` and ``lib`` folders into the architecture-specific slices of -the XCframework:: +Testing Python on iOS +===================== - cp path/to/iphoneos/bin Python.xcframework/ios-arm64 - cp path/to/iphoneos/lib Python.xcframework/ios-arm64 +Testing a multi-architecture framework +-------------------------------------- - cp path/to/iphonesimulator/bin Python.xcframework/ios-arm64_x86_64-simulator - cp path/to/iphonesimulator/lib Python.xcframework/ios-arm64_x86_64-simulator +Once you have a built an XCframework, you can test that framework by running: -Note that the name of the architecture-specific slice for the simulator will -depend on the CPU architecture(s) that you build. + $ python Apple iOS test -You now have a Python.xcframework that can be used in a project. +Testing a single-architecture framework +--------------------------------------- -Testing Python on iOS -===================== - -The ``iOS/testbed`` folder that contains an Xcode project that is able to run -the iOS test suite. 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. +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 ``iOS/Resources/bin`` folder and +``PATH`` has been configured to include the ``Apple/iOS/Resources/bin`` folder and exclude any non-iOS tools, then run:: $ make all @@ -283,7 +260,8 @@ This will: * 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 "iPhone SE (3rd generation)" simulator. +* 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 @@ -293,18 +271,27 @@ project, and then boot and prepare the iOS simulator. Debugging test failures ----------------------- -Running ``make testios`` generates a standalone version of the ``iOS/testbed`` -project, and runs the full test suite. It does this using ``iOS/testbed`` -itself - the folder is an executable module that can be used to create and run -a clone of the testbed project. +Running ``python Apple iOS test`` 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 iOS/testbed clone --framework iOS/Frameworks/arm64-iphonesimulator my-testbed + $ 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 -This invocation assumes that ``iOS/Frameworks/arm64-iphonesimulator`` is the -path to the iOS simulator framework for your platform (ARM64 in this case); -``my-testbed`` is the name of the folder for the new testbed clone. +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 @@ -332,7 +319,7 @@ 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 -``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. diff --git a/iOS/Resources/Info.plist.in b/Apple/iOS/Resources/Info.plist.in similarity index 100% rename from iOS/Resources/Info.plist.in rename to Apple/iOS/Resources/Info.plist.in diff --git a/iOS/Resources/bin/arm64-apple-ios-ar b/Apple/iOS/Resources/bin/arm64-apple-ios-ar similarity index 100% rename from iOS/Resources/bin/arm64-apple-ios-ar rename to Apple/iOS/Resources/bin/arm64-apple-ios-ar diff --git a/iOS/Resources/bin/arm64-apple-ios-clang b/Apple/iOS/Resources/bin/arm64-apple-ios-clang similarity index 100% rename from iOS/Resources/bin/arm64-apple-ios-clang rename to Apple/iOS/Resources/bin/arm64-apple-ios-clang diff --git a/iOS/Resources/bin/arm64-apple-ios-clang++ b/Apple/iOS/Resources/bin/arm64-apple-ios-clang++ similarity index 100% rename from iOS/Resources/bin/arm64-apple-ios-clang++ rename to Apple/iOS/Resources/bin/arm64-apple-ios-clang++ diff --git a/iOS/Resources/bin/arm64-apple-ios-cpp b/Apple/iOS/Resources/bin/arm64-apple-ios-cpp similarity index 100% rename from iOS/Resources/bin/arm64-apple-ios-cpp rename to Apple/iOS/Resources/bin/arm64-apple-ios-cpp diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-ar b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar similarity index 100% rename from iOS/Resources/bin/arm64-apple-ios-simulator-ar rename to Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-clang b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang similarity index 100% rename from iOS/Resources/bin/arm64-apple-ios-simulator-clang rename to Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ similarity index 100% rename from iOS/Resources/bin/arm64-apple-ios-simulator-clang++ rename to Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-cpp b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp similarity index 100% rename from iOS/Resources/bin/arm64-apple-ios-simulator-cpp rename to Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-strip b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip similarity index 100% rename from iOS/Resources/bin/arm64-apple-ios-simulator-strip rename to Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip diff --git a/iOS/Resources/bin/arm64-apple-ios-strip b/Apple/iOS/Resources/bin/arm64-apple-ios-strip similarity index 100% rename from iOS/Resources/bin/arm64-apple-ios-strip rename to Apple/iOS/Resources/bin/arm64-apple-ios-strip diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-ar b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar similarity index 100% rename from iOS/Resources/bin/x86_64-apple-ios-simulator-ar rename to Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-clang b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang similarity index 100% rename from iOS/Resources/bin/x86_64-apple-ios-simulator-clang rename to Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ similarity index 100% rename from iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ rename to Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp similarity index 100% rename from iOS/Resources/bin/x86_64-apple-ios-simulator-cpp rename to Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-strip b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip similarity index 100% rename from iOS/Resources/bin/x86_64-apple-ios-simulator-strip rename to Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip diff --git a/iOS/testbed/iOSTestbed/dylib-Info-template.plist b/Apple/iOS/Resources/dylib-Info-template.plist similarity index 96% rename from iOS/testbed/iOSTestbed/dylib-Info-template.plist rename to Apple/iOS/Resources/dylib-Info-template.plist index f652e272f71c88..d6caa01c1e44b9 100644 --- a/iOS/testbed/iOSTestbed/dylib-Info-template.plist +++ b/Apple/iOS/Resources/dylib-Info-template.plist @@ -19,7 +19,7 @@ iPhoneOS MinimumOSVersion - 12.0 + 13.0 CFBundleVersion 1 diff --git a/iOS/Resources/pyconfig.h b/Apple/iOS/Resources/pyconfig.h similarity index 100% rename from iOS/Resources/pyconfig.h rename to Apple/iOS/Resources/pyconfig.h diff --git a/iOS/testbed/Python.xcframework/Info.plist b/Apple/testbed/Python.xcframework/Info.plist similarity index 100% rename from iOS/testbed/Python.xcframework/Info.plist rename to Apple/testbed/Python.xcframework/Info.plist diff --git a/iOS/testbed/Python.xcframework/ios-arm64/README b/Apple/testbed/Python.xcframework/ios-arm64/README similarity index 100% rename from iOS/testbed/Python.xcframework/ios-arm64/README rename to Apple/testbed/Python.xcframework/ios-arm64/README diff --git a/iOS/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README b/Apple/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README similarity index 100% rename from iOS/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README rename to Apple/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README diff --git a/iOS/testbed/iOSTestbed.lldbinit b/Apple/testbed/Testbed.lldbinit similarity index 100% rename from iOS/testbed/iOSTestbed.lldbinit rename to Apple/testbed/Testbed.lldbinit diff --git a/iOS/testbed/iOSTestbedTests/iOSTestbedTests.m b/Apple/testbed/TestbedTests/TestbedTests.m similarity index 97% rename from iOS/testbed/iOSTestbedTests/iOSTestbedTests.m rename to Apple/testbed/TestbedTests/TestbedTests.m index d3159f5c2e155c..80741097e4c80d 100644 --- a/iOS/testbed/iOSTestbedTests/iOSTestbedTests.m +++ b/Apple/testbed/TestbedTests/TestbedTests.m @@ -1,11 +1,11 @@ #import #import -@interface iOSTestbedTests : XCTestCase +@interface TestbedTests : XCTestCase @end -@implementation iOSTestbedTests +@implementation TestbedTests - (void)testPython { @@ -41,14 +41,14 @@ - (void)testPython { // 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 = ["iOSTestbed"] + test_args[2:] + // 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] = "iOSTestbed"; + argv[0] = "Testbed"; for (int i = 1; i < [test_args count] - 1; i++) { argv[i] = [[test_args objectAtIndex:i+1] UTF8String]; } diff --git a/iOS/testbed/__main__.py b/Apple/testbed/__main__.py similarity index 59% rename from iOS/testbed/__main__.py rename to Apple/testbed/__main__.py index 6a4d9c76d162b4..4a1333380cdb6d 100644 --- a/iOS/testbed/__main__.py +++ b/Apple/testbed/__main__.py @@ -6,6 +6,9 @@ import sys from pathlib import Path +TEST_SLICES = { + "iOS": "ios-arm64_x86_64-simulator", +} DECODE_ARGS = ("UTF-8", "backslashreplace") @@ -21,45 +24,49 @@ # Select a simulator device to use. -def select_simulator_device(): +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) - # Any 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") + 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"] ) - or "iPhone SE " in devicetype["name"] ) - ) + simulator = se_simulators[-1][1] + else: + raise ValueError(f"Unknown platform {platform}") - return se_simulators[-1][1] + return simulator -def xcode_test(location, simulator, verbose): +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 / "iOSTestbed.xcodeproj"), + str(location / f"{platform}Testbed.xcodeproj"), "-scheme", - "iOSTestbed", + f"{platform}Testbed", "-destination", - f"platform=iOS Simulator,name={simulator}", + f"platform={platform} Simulator,name={simulator}", "-derivedDataPath", str(location / "DerivedData"), ] @@ -89,10 +96,24 @@ def xcode_test(location, simulator, verbose): 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(): @@ -101,11 +122,11 @@ def clone_testbed( if framework is None: if not ( - source / "Python.xcframework/ios-arm64_x86_64-simulator/bin" + source / "Python.xcframework" / TEST_SLICES[platform] / "bin" ).is_dir(): print( f"The testbed being cloned ({source}) does not contain " - f"a simulator framework. Re-run with --framework" + "a framework with slices. Re-run with --framework" ) sys.exit(11) else: @@ -124,33 +145,49 @@ def clone_testbed( print("Cloning testbed project:") print(f" Cloning {source}...", end="") - shutil.copytree(source, target, symlinks=True) + # 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" - sim_framework_path = xc_framework_path / "ios-arm64_x86_64-simulator" + test_framework_path = xc_framework_path / TEST_SLICES[platform] if framework is not None: if framework.suffix == ".xcframework": print(" Installing XCFramework...", end="") - if xc_framework_path.is_dir(): - shutil.rmtree(xc_framework_path) - else: - xc_framework_path.unlink(missing_ok=True) xc_framework_path.symlink_to( framework.relative_to(xc_framework_path.parent, walk_up=True) ) print(" done") else: print(" Installing simulator framework...", end="") - if sim_framework_path.is_dir(): - shutil.rmtree(sim_framework_path) + # 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: - sim_framework_path.unlink(missing_ok=True) - sim_framework_path.symlink_to( - framework.relative_to(sim_framework_path.parent, walk_up=True) + 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() @@ -158,39 +195,39 @@ def clone_testbed( # XCFramework is a relative symlink. Rewrite the symlink relative # to the new location. print(" Rewriting symlink to XCframework...", end="") - orig_xc_framework_path = ( + resolved_xc_framework_path = ( source / xc_framework_path.readlink() ).resolve() xc_framework_path.unlink() xc_framework_path.symlink_to( - orig_xc_framework_path.relative_to( + resolved_xc_framework_path.relative_to( xc_framework_path.parent, walk_up=True ) ) print(" done") elif ( - sim_framework_path.is_symlink() - and not sim_framework_path.readlink().is_absolute() + 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_sim_framework_path = ( - source / "Python.XCframework" / sim_framework_path.readlink() + orig_test_framework_path = ( + source / "Python.XCframework" / test_framework_path.readlink() ).resolve() - sim_framework_path.unlink() - sim_framework_path.symlink_to( - orig_sim_framework_path.relative_to( - sim_framework_path.parent, walk_up=True + 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 iOS framework.") + print(" Using pre-existing Python framework.") for app_src in apps: print(f" Installing app {app_src.name!r}...", end="") - app_target = target / f"iOSTestbed/app/{app_src.name}" + app_target = target / f"Testbed/app/{app_src.name}" if app_target.is_dir(): shutil.rmtree(app_target) shutil.copytree(app_src, app_target) @@ -199,9 +236,9 @@ def clone_testbed( print(f"Successfully cloned testbed: {target.resolve()}") -def update_test_plan(testbed_path, args): +def update_test_plan(testbed_path, platform, args): # Modify the test plan to use the requested test arguments. - test_plan_path = testbed_path / "iOSTestbed.xctestplan" + 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) @@ -213,32 +250,50 @@ def update_test_plan(testbed_path, args): json.dump(test_plan, f, indent=2) -def run_testbed(simulator: str | None, args: list[str], verbose: bool = False): +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, args) + update_test_plan(location, platform, args) print(" done.") if simulator is None: - simulator = select_simulator_device() + simulator = select_simulator_device(platform) print(f"Running test on {simulator}") - xcode_test(location, simulator=simulator, verbose=verbose) + 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"] + if (Path(__file__).parent / f"{platform}Testbed").is_dir() + ] + parser = argparse.ArgumentParser( description=( - "Manages the process of testing a Python project in the iOS simulator." + "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 an iOS Python framework and" + "Clone the testbed project, copying in a Python framework and" "any specified application code." ), help="Clone a testbed project to a new location.", @@ -250,6 +305,13 @@ def main(): "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", @@ -272,6 +334,13 @@ def main(): ), 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=( @@ -306,22 +375,26 @@ def main(): 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/ios-arm64_x86_64-simulator/bin" + / "Python.xcframework" + / TEST_SLICES[context.platform] + / "bin" ).is_dir(): print( - f"Testbed does not contain a compiled iOS framework. Use " + 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, diff --git a/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj b/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj similarity index 97% rename from iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj rename to Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj index 18cdafd8127520..0a7b968aa143d1 100644 --- a/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj +++ b/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj @@ -11,7 +11,7 @@ 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 /* iOSTestbedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66312B0EFA3A0010BFC8 /* iOSTestbedTests.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 */; }; @@ -64,7 +64,7 @@ 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 /* iOSTestbedTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iOSTestbedTests.m; sourceTree = ""; }; + 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 = ""; }; 607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "dylib-Info-template.plist"; sourceTree = ""; }; 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = ""; }; @@ -99,7 +99,7 @@ 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */, 607A664A2B0EFB310010BFC8 /* Python.xcframework */, 607A66142B0EFA380010BFC8 /* iOSTestbed */, - 607A66302B0EFA3A0010BFC8 /* iOSTestbedTests */, + 607A66302B0EFA3A0010BFC8 /* TestbedTests */, 607A66132B0EFA380010BFC8 /* Products */, 607A664F2B0EFFE00010BFC8 /* Frameworks */, ); @@ -130,12 +130,12 @@ path = iOSTestbed; sourceTree = ""; }; - 607A66302B0EFA3A0010BFC8 /* iOSTestbedTests */ = { + 607A66302B0EFA3A0010BFC8 /* TestbedTests */ = { isa = PBXGroup; children = ( - 607A66312B0EFA3A0010BFC8 /* iOSTestbedTests.m */, + 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */, ); - path = iOSTestbedTests; + path = TestbedTests; sourceTree = ""; }; 607A664F2B0EFFE00010BFC8 /* Frameworks */ = { @@ -303,7 +303,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 607A66322B0EFA3A0010BFC8 /* iOSTestbedTests.m in Sources */, + 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iOS/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme b/Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme similarity index 97% rename from iOS/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme rename to Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme index d093a46f02e95d..3c330a4152bf92 100644 --- a/iOS/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme +++ b/Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme @@ -27,7 +27,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - customLLDBInitFile = "/Users/rkm/projects/pyspamsum/localtest/iOSTestbed.lldbinit" + customLLDBInitFile = "$(SOURCE_ROOT)/Testbed.lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> iPhoneOS MinimumOSVersion - 12.0 + 13.0 CFBundleVersion 1 diff --git a/iOS/testbed/iOSTestbed/iOSTestbed-Info.plist b/Apple/testbed/iOSTestbed/iOSTestbed-Info.plist similarity index 100% rename from iOS/testbed/iOSTestbed/iOSTestbed-Info.plist rename to Apple/testbed/iOSTestbed/iOSTestbed-Info.plist diff --git a/iOS/testbed/iOSTestbed/main.m b/Apple/testbed/iOSTestbed/main.m similarity index 100% rename from iOS/testbed/iOSTestbed/main.m rename to Apple/testbed/iOSTestbed/main.m diff --git a/Doc/using/ios.rst b/Doc/using/ios.rst index 91cfed16f0e415..ba6f478d26b323 100644 --- a/Doc/using/ios.rst +++ b/Doc/using/ios.rst @@ -170,7 +170,7 @@ helpful. 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.rst` (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,7 +180,7 @@ To add Python to an iOS Xcode project: 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, +3. Drag the ``Apple/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 @@ -264,6 +264,7 @@ To add Python to an iOS Xcode project: 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" @@ -334,25 +335,30 @@ modules in your app, some additional steps will be required: 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.rst` +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 +387,7 @@ 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 -``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. diff --git a/Mac/Resources/framework/Info.plist.in b/Mac/Resources/framework/Info.plist.in index 4c42971ed90ee4..fbf747affe9af8 100644 --- a/Mac/Resources/framework/Info.plist.in +++ b/Mac/Resources/framework/Info.plist.in @@ -24,6 +24,8 @@ ???? CFBundleVersion %VERSION% + MinimumOSVersion + @MACOSX_DEPLOYMENT_TARGET@ CFBundleAllowMixedLocalizations diff --git a/Makefile.pre.in b/Makefile.pre.in index bcf19654adfb35..c99f92c49c6e9b 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -2319,7 +2319,7 @@ testios: 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 @@ -3247,7 +3247,7 @@ clobber: clean 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/NEWS.d/next/Tools-Demos/2025-08-27-11-14-53.gh-issue-138171.Suz8ob.rst b/Misc/NEWS.d/next/Tools-Demos/2025-08-27-11-14-53.gh-issue-138171.Suz8ob.rst new file mode 100644 index 00000000000000..0a933ec754cdac --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2025-08-27-11-14-53.gh-issue-138171.Suz8ob.rst @@ -0,0 +1,3 @@ +A script for building an iOS XCframework was added. As part of this change, +the top level ``iOS`` folder has been moved to be a subdirectory of the +``Apple`` folder. diff --git a/configure b/configure index bdeab8a6d126a3..0a70e75d50996f 100755 --- a/configure +++ b/configure @@ -4359,7 +4359,7 @@ then : yes) case $ac_sys_system in Darwin) enableval=/Library/Frameworks ;; - iOS) enableval=iOS/Frameworks/\$\(MULTIARCH\) ;; + iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; *) as_fn_error $? "Unknown platform for framework build" "$LINENO" 5 esac esac @@ -4470,9 +4470,9 @@ then : prefix=$PYTHONFRAMEWORKPREFIX PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" - RESSRCDIR=iOS/Resources + RESSRCDIR=Apple/iOS/Resources - ac_config_files="$ac_config_files iOS/Resources/Info.plist" + ac_config_files="$ac_config_files Apple/iOS/Resources/Info.plist" ;; *) @@ -35222,7 +35222,7 @@ do "Mac/PythonLauncher/Makefile") CONFIG_FILES="$CONFIG_FILES Mac/PythonLauncher/Makefile" ;; "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" ;; "Makefile.pre") CONFIG_FILES="$CONFIG_FILES Makefile.pre" ;; "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 991fa40746be78..4bb56070d95aee 100644 --- a/configure.ac +++ b/configure.ac @@ -559,7 +559,7 @@ AC_ARG_ENABLE([framework], yes) case $ac_sys_system in Darwin) enableval=/Library/Frameworks ;; - iOS) enableval=iOS/Frameworks/\$\(MULTIARCH\) ;; + iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; *) AC_MSG_ERROR([Unknown platform for framework build]) esac esac @@ -666,9 +666,9 @@ AC_ARG_ENABLE([framework], 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_MSG_ERROR([Unknown platform for framework build]) From b89f46f69b4168d8aa3a4910a360a8acafe85a3e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 27 Aug 2025 15:05:47 +0800 Subject: [PATCH 02/15] As a buildbot workaround, retain a copy of the compiler shims in the old location. --- iOS/Resources/bin/bin/arm64-apple-ios-ar | 2 ++ iOS/Resources/bin/bin/arm64-apple-ios-clang | 2 ++ iOS/Resources/bin/bin/arm64-apple-ios-clang++ | 2 ++ iOS/Resources/bin/bin/arm64-apple-ios-cpp | 2 ++ iOS/Resources/bin/bin/arm64-apple-ios-simulator-ar | 2 ++ iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang | 2 ++ iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang++ | 2 ++ iOS/Resources/bin/bin/arm64-apple-ios-simulator-cpp | 2 ++ iOS/Resources/bin/bin/arm64-apple-ios-simulator-strip | 2 ++ iOS/Resources/bin/bin/arm64-apple-ios-strip | 2 ++ iOS/Resources/bin/bin/x86_64-apple-ios-simulator-ar | 2 ++ iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang | 2 ++ iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang++ | 2 ++ iOS/Resources/bin/bin/x86_64-apple-ios-simulator-cpp | 2 ++ iOS/Resources/bin/bin/x86_64-apple-ios-simulator-strip | 2 ++ 15 files changed, 30 insertions(+) create mode 100755 iOS/Resources/bin/bin/arm64-apple-ios-ar create mode 100755 iOS/Resources/bin/bin/arm64-apple-ios-clang create mode 100755 iOS/Resources/bin/bin/arm64-apple-ios-clang++ create mode 100755 iOS/Resources/bin/bin/arm64-apple-ios-cpp create mode 100755 iOS/Resources/bin/bin/arm64-apple-ios-simulator-ar create mode 100755 iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang create mode 100755 iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang++ create mode 100755 iOS/Resources/bin/bin/arm64-apple-ios-simulator-cpp create mode 100755 iOS/Resources/bin/bin/arm64-apple-ios-simulator-strip create mode 100755 iOS/Resources/bin/bin/arm64-apple-ios-strip create mode 100755 iOS/Resources/bin/bin/x86_64-apple-ios-simulator-ar create mode 100755 iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang create mode 100755 iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang++ create mode 100755 iOS/Resources/bin/bin/x86_64-apple-ios-simulator-cpp create mode 100755 iOS/Resources/bin/bin/x86_64-apple-ios-simulator-strip diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-ar b/iOS/Resources/bin/bin/arm64-apple-ios-ar new file mode 100755 index 00000000000000..3cf3eb218741fa --- /dev/null +++ b/iOS/Resources/bin/bin/arm64-apple-ios-ar @@ -0,0 +1,2 @@ +#!/bin/sh +xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@" diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-clang b/iOS/Resources/bin/bin/arm64-apple-ios-clang new file mode 100755 index 00000000000000..f50d5b5142fc76 --- /dev/null +++ b/iOS/Resources/bin/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} "$@" diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-clang++ b/iOS/Resources/bin/bin/arm64-apple-ios-clang++ new file mode 100755 index 00000000000000..0794731d7dcbda --- /dev/null +++ b/iOS/Resources/bin/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} "$@" diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-cpp b/iOS/Resources/bin/bin/arm64-apple-ios-cpp new file mode 100755 index 00000000000000..24fa1506bab827 --- /dev/null +++ b/iOS/Resources/bin/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 "$@" diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-simulator-ar b/iOS/Resources/bin/bin/arm64-apple-ios-simulator-ar new file mode 100755 index 00000000000000..b836b6db9025bb --- /dev/null +++ b/iOS/Resources/bin/bin/arm64-apple-ios-simulator-ar @@ -0,0 +1,2 @@ +#!/bin/sh +xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang b/iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang new file mode 100755 index 00000000000000..4891a00876e0bd --- /dev/null +++ b/iOS/Resources/bin/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 "$@" diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang++ b/iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang++ new file mode 100755 index 00000000000000..58b2a5f6f18c2b --- /dev/null +++ b/iOS/Resources/bin/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 "$@" diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-simulator-cpp b/iOS/Resources/bin/bin/arm64-apple-ios-simulator-cpp new file mode 100755 index 00000000000000..c9df94e8b7c837 --- /dev/null +++ b/iOS/Resources/bin/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 "$@" diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-simulator-strip b/iOS/Resources/bin/bin/arm64-apple-ios-simulator-strip new file mode 100755 index 00000000000000..fd59d309b73a20 --- /dev/null +++ b/iOS/Resources/bin/bin/arm64-apple-ios-simulator-strip @@ -0,0 +1,2 @@ +#!/bin/sh +xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch arm64 "$@" diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-strip b/iOS/Resources/bin/bin/arm64-apple-ios-strip new file mode 100755 index 00000000000000..75e823a3d02d61 --- /dev/null +++ b/iOS/Resources/bin/bin/arm64-apple-ios-strip @@ -0,0 +1,2 @@ +#!/bin/sh +xcrun --sdk iphoneos${IOS_SDK_VERSION} strip -arch arm64 "$@" diff --git a/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-ar b/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-ar new file mode 100755 index 00000000000000..b836b6db9025bb --- /dev/null +++ b/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-ar @@ -0,0 +1,2 @@ +#!/bin/sh +xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" diff --git a/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang b/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang new file mode 100755 index 00000000000000..f4739a7b945d01 --- /dev/null +++ b/iOS/Resources/bin/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 "$@" diff --git a/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang++ b/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang++ new file mode 100755 index 00000000000000..c348ae4c10395b --- /dev/null +++ b/iOS/Resources/bin/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 "$@" diff --git a/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-cpp b/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-cpp new file mode 100755 index 00000000000000..6d7f8084c9fdcc --- /dev/null +++ b/iOS/Resources/bin/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 "$@" diff --git a/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-strip b/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-strip new file mode 100755 index 00000000000000..c5cfb28929195a --- /dev/null +++ b/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-strip @@ -0,0 +1,2 @@ +#!/bin/sh +xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch x86_64 "$@" From 68d671bc4a7cc83e371aad264f2229f72e251164 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 27 Aug 2025 15:24:55 +0800 Subject: [PATCH 03/15] ...and get the legacy location correct. --- iOS/Resources/bin/{bin => }/arm64-apple-ios-ar | 0 iOS/Resources/bin/{bin => }/arm64-apple-ios-clang | 0 iOS/Resources/bin/{bin => }/arm64-apple-ios-clang++ | 0 iOS/Resources/bin/{bin => }/arm64-apple-ios-cpp | 0 iOS/Resources/bin/{bin => }/arm64-apple-ios-simulator-ar | 0 iOS/Resources/bin/{bin => }/arm64-apple-ios-simulator-clang | 0 iOS/Resources/bin/{bin => }/arm64-apple-ios-simulator-clang++ | 0 iOS/Resources/bin/{bin => }/arm64-apple-ios-simulator-cpp | 0 iOS/Resources/bin/{bin => }/arm64-apple-ios-simulator-strip | 0 iOS/Resources/bin/{bin => }/arm64-apple-ios-strip | 0 iOS/Resources/bin/{bin => }/x86_64-apple-ios-simulator-ar | 0 iOS/Resources/bin/{bin => }/x86_64-apple-ios-simulator-clang | 0 iOS/Resources/bin/{bin => }/x86_64-apple-ios-simulator-clang++ | 0 iOS/Resources/bin/{bin => }/x86_64-apple-ios-simulator-cpp | 0 iOS/Resources/bin/{bin => }/x86_64-apple-ios-simulator-strip | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename iOS/Resources/bin/{bin => }/arm64-apple-ios-ar (100%) rename iOS/Resources/bin/{bin => }/arm64-apple-ios-clang (100%) rename iOS/Resources/bin/{bin => }/arm64-apple-ios-clang++ (100%) rename iOS/Resources/bin/{bin => }/arm64-apple-ios-cpp (100%) rename iOS/Resources/bin/{bin => }/arm64-apple-ios-simulator-ar (100%) rename iOS/Resources/bin/{bin => }/arm64-apple-ios-simulator-clang (100%) rename iOS/Resources/bin/{bin => }/arm64-apple-ios-simulator-clang++ (100%) rename iOS/Resources/bin/{bin => }/arm64-apple-ios-simulator-cpp (100%) rename iOS/Resources/bin/{bin => }/arm64-apple-ios-simulator-strip (100%) rename iOS/Resources/bin/{bin => }/arm64-apple-ios-strip (100%) rename iOS/Resources/bin/{bin => }/x86_64-apple-ios-simulator-ar (100%) rename iOS/Resources/bin/{bin => }/x86_64-apple-ios-simulator-clang (100%) rename iOS/Resources/bin/{bin => }/x86_64-apple-ios-simulator-clang++ (100%) rename iOS/Resources/bin/{bin => }/x86_64-apple-ios-simulator-cpp (100%) rename iOS/Resources/bin/{bin => }/x86_64-apple-ios-simulator-strip (100%) diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-ar b/iOS/Resources/bin/arm64-apple-ios-ar similarity index 100% rename from iOS/Resources/bin/bin/arm64-apple-ios-ar rename to iOS/Resources/bin/arm64-apple-ios-ar diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-clang b/iOS/Resources/bin/arm64-apple-ios-clang similarity index 100% rename from iOS/Resources/bin/bin/arm64-apple-ios-clang rename to iOS/Resources/bin/arm64-apple-ios-clang diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-clang++ b/iOS/Resources/bin/arm64-apple-ios-clang++ similarity index 100% rename from iOS/Resources/bin/bin/arm64-apple-ios-clang++ rename to iOS/Resources/bin/arm64-apple-ios-clang++ diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-cpp b/iOS/Resources/bin/arm64-apple-ios-cpp similarity index 100% rename from iOS/Resources/bin/bin/arm64-apple-ios-cpp rename to iOS/Resources/bin/arm64-apple-ios-cpp diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-simulator-ar b/iOS/Resources/bin/arm64-apple-ios-simulator-ar similarity index 100% rename from iOS/Resources/bin/bin/arm64-apple-ios-simulator-ar rename to iOS/Resources/bin/arm64-apple-ios-simulator-ar diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang b/iOS/Resources/bin/arm64-apple-ios-simulator-clang similarity index 100% rename from iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang rename to iOS/Resources/bin/arm64-apple-ios-simulator-clang diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang++ b/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ similarity index 100% rename from iOS/Resources/bin/bin/arm64-apple-ios-simulator-clang++ rename to iOS/Resources/bin/arm64-apple-ios-simulator-clang++ diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-simulator-cpp b/iOS/Resources/bin/arm64-apple-ios-simulator-cpp similarity index 100% rename from iOS/Resources/bin/bin/arm64-apple-ios-simulator-cpp rename to iOS/Resources/bin/arm64-apple-ios-simulator-cpp diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-simulator-strip b/iOS/Resources/bin/arm64-apple-ios-simulator-strip similarity index 100% rename from iOS/Resources/bin/bin/arm64-apple-ios-simulator-strip rename to iOS/Resources/bin/arm64-apple-ios-simulator-strip diff --git a/iOS/Resources/bin/bin/arm64-apple-ios-strip b/iOS/Resources/bin/arm64-apple-ios-strip similarity index 100% rename from iOS/Resources/bin/bin/arm64-apple-ios-strip rename to iOS/Resources/bin/arm64-apple-ios-strip diff --git a/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-ar b/iOS/Resources/bin/x86_64-apple-ios-simulator-ar similarity index 100% rename from iOS/Resources/bin/bin/x86_64-apple-ios-simulator-ar rename to iOS/Resources/bin/x86_64-apple-ios-simulator-ar diff --git a/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang b/iOS/Resources/bin/x86_64-apple-ios-simulator-clang similarity index 100% rename from iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang rename to iOS/Resources/bin/x86_64-apple-ios-simulator-clang diff --git a/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang++ b/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ similarity index 100% rename from iOS/Resources/bin/bin/x86_64-apple-ios-simulator-clang++ rename to iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ diff --git a/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-cpp b/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp similarity index 100% rename from iOS/Resources/bin/bin/x86_64-apple-ios-simulator-cpp rename to iOS/Resources/bin/x86_64-apple-ios-simulator-cpp diff --git a/iOS/Resources/bin/bin/x86_64-apple-ios-simulator-strip b/iOS/Resources/bin/x86_64-apple-ios-simulator-strip similarity index 100% rename from iOS/Resources/bin/bin/x86_64-apple-ios-simulator-strip rename to iOS/Resources/bin/x86_64-apple-ios-simulator-strip From 39856d14f7c0c9ace35732cf6ba1f9c509586a60 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 28 Aug 2025 06:16:13 +0800 Subject: [PATCH 04/15] Updates to CODEOWNERS, gitignore and clean targets. --- .github/CODEOWNERS | 2 +- .gitignore | 8 ++++---- Makefile.pre.in | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7d26f8b526c3a0..33f21f2ca1e729 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -149,7 +149,7 @@ Lib/test/test_android.py @mhsmith @freakboy3742 # iOS Doc/using/ios.rst @freakboy3742 Lib/_ios_support.py @freakboy3742 -iOS/ @freakboy3742 +Apple/ @freakboy3742 # macOS Mac/ @python/macos-team diff --git a/.gitignore b/.gitignore index 394a1fdb9d90c8..2bf4925647ddcd 100644 --- a/.gitignore +++ b/.gitignore @@ -74,10 +74,10 @@ Lib/test/data/* Apple/iOS/Frameworks/ Apple/iOS/Resources/Info.plist Apple/testbed/build -Apple/testbed/Python.xcframework/*-*/bin -Apple/testbed/Python.xcframework/*-*/include -Apple/testbed/Python.xcframework/*-*/lib -Apple/testbed/Python.xcframework/*-*/Python.framework +Apple/testbed/Python.xcframework/*/bin +Apple/testbed/Python.xcframework/*/include +Apple/testbed/Python.xcframework/*/lib +Apple/testbed/Python.xcframework/*/Python.framework Apple/testbed/*Testbed.xcodeproj/project.xcworkspace Apple/testbed/*Testbed.xcodeproj/xcuserdata Mac/Makefile diff --git a/Makefile.pre.in b/Makefile.pre.in index c99f92c49c6e9b..3eb33b7705450d 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -3218,10 +3218,10 @@ clean-retain-profile: pycremoval -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: From f6a836c019a4896d58f35396e1654e416769d2c7 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 29 Aug 2025 07:25:36 +0800 Subject: [PATCH 05/15] Apply suggestions from code review Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Apple/__main__.py | 8 ++++---- Apple/iOS/README.rst | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Apple/__main__.py b/Apple/__main__.py index d9e7c53db4afe7..cb595f597c2b3f 100644 --- a/Apple/__main__.py +++ b/Apple/__main__.py @@ -250,7 +250,7 @@ def make_build_python(context): def apple_target(host): - """Return the apple platform identifier for a given host triple.""" + """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(): @@ -409,7 +409,7 @@ def create_xcframework(context): # 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[context.platform].items(): - # Some parts are the same across all slices, so we can any of the + # 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())) @@ -788,14 +788,14 @@ def parse_args(): cmd.add_argument( "platform", choices=HOSTS.keys(), - help="The target platform to build.", + 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).", + help="The host triple to build (e.g., arm64-apple-ios-simulator)", ) # optional host triple argument for cmd in [clean, build, test]: diff --git a/Apple/iOS/README.rst b/Apple/iOS/README.rst index 9126ed2d3c182a..3aff7944459361 100644 --- a/Apple/iOS/README.rst +++ b/Apple/iOS/README.rst @@ -63,7 +63,7 @@ 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 iOS build + python Apple iOS build This will: From c692e9abb2eb0d9394c0cb00f6559603fc9b1466 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 29 Aug 2025 08:40:31 +0800 Subject: [PATCH 06/15] Convert README to Markdown. --- Apple/iOS/{README.rst => README.md} | 271 +++++++++++++--------------- Doc/using/ios.rst | 4 +- 2 files changed, 132 insertions(+), 143 deletions(-) rename Apple/iOS/{README.rst => README.md} (53%) diff --git a/Apple/iOS/README.rst b/Apple/iOS/README.md similarity index 53% rename from Apple/iOS/README.rst rename to Apple/iOS/README.md index 3aff7944459361..c3bf0b7a50c6e2 100644 --- a/Apple/iOS/README.rst +++ b/Apple/iOS/README.md @@ -1,22 +1,18 @@ -==================== -Python on iOS README -==================== +# Python on iOS README -:Authors: - Russell Keith-Magee (2023-11) +**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 -`__ and `Kivy's Buildozer -`__ will provide a much more approachable -user experience. +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 -============================= +## 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 @@ -30,19 +26,17 @@ 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 -====================== +## Building Python on iOS -ABIs and Architectures ----------------------- +### 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``). +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 +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, @@ -56,10 +50,9 @@ 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 ---------------------------------------------- +### Building a multi-architecture iOS XCframework -The ``Apple`` subfolder of the Python repository acts as a build script that +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:: @@ -69,126 +62,123 @@ 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 +* 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 +* 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 +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 ----------------------------------------- +### Building a single-architecture framework -If you're using the ``Apple`` build script, you won't need to build +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 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +#### iOS specific arguments to configure -* ``--enable-framework[=DIR]`` +* `--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. + 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`` +* `--with-framework-name=NAME` - Specify the name for the Python framework; defaults to ``Python``. + Specify the name for the Python framework; defaults to `Python`. - .. admonition:: Use this option with care! + > [!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. - 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 -Building Python for iOS -~~~~~~~~~~~~~~~~~~~~~~~ - -The Python build system will create a ``Python.framework`` that supports a +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. +`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.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 +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 \ +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 + 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 +* `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 + 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. + `CC` will be a single executable. - To work around this problem, the ``Apple/iOS/Resources/bin`` folder contains some + 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`` + 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. + 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 + `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.) + `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 +* `--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 + - `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 + - `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 +* `--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. + - `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 +* `/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 @@ -199,60 +189,58 @@ In this invocation: 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 `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 + 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 + `--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 `__. +`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. +(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 +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 Python on iOS -Testing a multi-architecture framework --------------------------------------- +### Testing a multi-architecture framework Once you have a built an XCframework, you can test that framework by running: $ python Apple iOS test -Testing a single-architecture framework ---------------------------------------- +### Testing a single-architecture framework -The ``Apple/testbed`` folder that contains an Xcode project that is able to run +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 +`--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: @@ -268,47 +256,49 @@ 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 ------------------------ +### Debugging test failures -Running ``python Apple iOS test`` 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 +Running `python Apple iOS test` 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:: +`cross-build/iOS-testbed.`. - $ python cross-build/iOS/testbed clone my-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 +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:: +you can construct a standalone testbed instance by running: +``` + python Apple/testbed clone --platform iOS --framework my-testbed +``` - $ 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`. -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, +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. +trying to diagnose a failure in the `os` module, you might run: +``` + python my-testbed run -- test -W test_os +``` -Testing in Xcode -^^^^^^^^^^^^^^^^ +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. -You can also open the testbed project in Xcode by running:: +### Testing in Xcode - $ open my-testbed/iOSTestbed.xcodeproj +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. @@ -319,12 +309,11 @@ 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 +`Testbed.lldbinit` file for providing configuration of the debugger. The default debugger configuration disables automatic breakpoints on the -``SIGINT``, ``SIGUSR1``, ``SIGUSR2``, and ``SIGXFSZ`` signals. +`SIGINT`, `SIGUSR1`, `SIGUSR2`, and `SIGXFSZ` signals. -Testing on an iOS device -^^^^^^^^^^^^^^^^^^^^^^^^ +### 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 diff --git a/Doc/using/ios.rst b/Doc/using/ios.rst index ba6f478d26b323..af214e5fd465b4 100644 --- a/Doc/using/ios.rst +++ b/Doc/using/ios.rst @@ -170,7 +170,7 @@ helpful. To add Python to an iOS Xcode project: 1. Build or obtain a Python ``XCFramework``. See the instructions in - :source:`Apple/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``. @@ -339,7 +339,7 @@ The CPython source tree contains :source:`a testbed project ` 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:`Apple/iOS/README.rst` +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: From fe39aa43a2b5575fbb9a7e66382c89616e847fa1 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 29 Aug 2025 08:43:18 +0800 Subject: [PATCH 07/15] Add type hints and more docstrings. --- Apple/__main__.py | 222 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 157 insertions(+), 65 deletions(-) diff --git a/Apple/__main__.py b/Apple/__main__.py index cb595f597c2b3f..535349b23843bb 100644 --- a/Apple/__main__.py +++ b/Apple/__main__.py @@ -33,6 +33,7 @@ # make steps for the build Python, an individual host, all hosts, or all # builds. ########################################################################## +from __future__ import annotations import argparse import os @@ -45,19 +46,23 @@ 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 = { +HOSTS: dict[str, dict[str, dict[str, str]]] = { # Structure of this data: # * Platform identifier # * an XCframework slice that must exist for that platform @@ -74,17 +79,9 @@ } -# 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 install_signal_handler(): - def signal_handler(*args): - os.kill(os.getpid(), signal.SIGINT) - - signal.signal(signal.SIGTERM, signal_handler) - - -def subdir(host, create=False): - path = CROSS_BUILD_DIR / host +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( @@ -96,7 +93,18 @@ def subdir(host, create=False): return path -def run(command, *, host=None, env=None, log=True, **kwargs): +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() @@ -111,22 +119,26 @@ def run(command, *, host=None, env=None, log=True, **kwargs): return subprocess.run(command, env=env, **kwargs) -# Format a command so it can be copied into a shell. Like shlex.join, but also -# accepts arguments which are Paths, or a single string/Path outside of a list. -def join_command(args): +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)) -# Format the environment so it can be pasted into a shell. -def print_env(env): +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)}") -def apple_env(host): +def apple_env(host: str) -> EnvironmentT: + """Construct an Apple development environment for the given host.""" env = { "PATH": ":".join( [ @@ -144,21 +156,28 @@ def apple_env(host): return env -def delete_path(name): +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): +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, target="all"): +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 @@ -190,7 +209,7 @@ def clean(context, target="all"): delete_path(path) -def build_python_path(): +def build_python_path() -> Path: """The path to the build Python binary.""" build_dir = subdir("build") binary = build_dir / "python" @@ -205,7 +224,12 @@ def build_python_path(): @contextmanager -def group(text, subdir=None): +def group(text: str): + """A context manager that ouptut 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: @@ -220,14 +244,16 @@ def group(text, subdir=None): @contextmanager -def cwd(subdir): +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): +def configure_build_python(context: argparse.Namespace) -> None: + """The implementation of the "configure-build" command.""" if context.clean: clean(context, "build") @@ -241,7 +267,8 @@ def configure_build_python(context): run(command) -def make_build_python(context): +def make_build_python(context: argparse.Namespace) -> None: + """The implementation of the "make-build" command.""" with ( group("Compiling build Python"), cwd(subdir("build")), @@ -249,7 +276,7 @@ def make_build_python(context): run(["make", "-j", str(os.cpu_count())]) -def apple_target(host): +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(): @@ -260,7 +287,7 @@ def apple_target(host): raise KeyError(host) -def apple_multiarch(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(): @@ -271,7 +298,20 @@ def apple_multiarch(host): raise KeyError(host) -def unpack_deps(host, prefix_dir, cache_dir=None): +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. + + On iOS, 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", @@ -284,19 +324,22 @@ def unpack_deps(host, prefix_dir, cache_dir=None): filename = f"{name_ver.lower()}-{apple_target(host)}.tar.gz" archive_path = download( f"{deps_url}/{name_ver}/{filename}", - target_dir=cache_dir if cache_dir else prefix_dir, + target_dir=cache_dir, ) shutil.unpack_archive(archive_path, prefix_dir) - if cache_dir is None: - os.remove(archive_path) # Dynamic libraries will be preferentially linked over static; - # ensure that no dylibs are available in the prefix folder. - for dylib in prefix_dir.glob("**/*.dylib"): - dylib.unlink() + # On iOS, ensure that no dylibs are available in the prefix folder. + if platform == "iOS": + 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. -def download(url, target_dir): + :return: The path to the downloaded archive. + """ target_path = Path(target_dir).resolve() target_path.mkdir(exist_ok=True, parents=True) @@ -319,7 +362,11 @@ def download(url, target_dir): return out_path -def configure_host_python(context, host=None): +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 @@ -332,7 +379,7 @@ def configure_host_python(context, host=None): with group(f"Downloading dependencies ({host})"): if not prefix_dir.exists(): prefix_dir.mkdir() - unpack_deps(host, prefix_dir, context.cache_dir) + unpack_deps(context.platform, host, prefix_dir, context.cache_dir) else: print("Dependencies already installed") @@ -365,7 +412,11 @@ def configure_host_python(context, host=None): run(command, host=host) -def make_host_python(context, host=None): +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 @@ -377,11 +428,17 @@ def make_host_python(context, host=None): run(["make", "install"], host=host) -def multiarch_path(host_triple, multiarch): +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) + """ return CROSS_BUILD_DIR / f"{host_triple}/Apple/iOS/Frameworks/{multiarch}" -def package_version(prefix_path): +def package_version(prefix_path: Path) -> str: + """Extract the Python version being build from patchlevel.h.""" for path in prefix_path.glob("**/patchlevel.h"): text = path.read_text(encoding="utf-8") if match := re.search( @@ -399,22 +456,31 @@ def package_version(prefix_path): sys.exit("Unable to determine Python version being packaged.") -def create_xcframework(context): - package_path = CROSS_BUILD_DIR / context.platform - package_path.mkdir(exist_ok=True) +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[context.platform].items(): + 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 = ( - multiarch_path(first_host_triple, first_multiarch) + framework_path(first_host_triple, first_multiarch) / "Python.framework" ) @@ -445,7 +511,7 @@ def create_xcframework(context): ["lipo", "-create", "-output", slice_framework / "Python"] + [ ( - multiarch_path(host_triple, multiarch) + framework_path(host_triple, multiarch) / "Python.framework/Python" ) for host_triple, multiarch in slice_parts.items() @@ -477,11 +543,11 @@ def create_xcframework(context): # to be copied in separately. print() print("Copy additional resources...") - for slice_name, slice_parts in HOSTS[context.platform].items(): + 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 = multiarch_path(first_host_triple, first_multiarch) + first_path = framework_path(first_host_triple, first_multiarch) first_framework = first_path / "Python.framework" slice_path = package_path / f"Python.xcframework/{slice_name}" @@ -501,7 +567,7 @@ def create_xcframework(context): # Copy in the cross-architecture pyconfig.h shutil.copy( - PYTHON_DIR / f"Apple/{context.platform}/Resources/pyconfig.h", + PYTHON_DIR / f"Apple/{platform}/Resources/pyconfig.h", slice_framework / "Headers/pyconfig.h", ) @@ -547,7 +613,7 @@ def create_xcframework(context): if len(relative_libs) != 1: raise RuntimeError( f"Cannot merge non-matching libraries: {relative_libs}" - ) + ) from None # Merge the per-arch .so files into a single "fat" binary. relative_lib = next(iter(relative_libs)) @@ -594,13 +660,14 @@ def create_xcframework(context): return version -def package(context): +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) + version = create_xcframework(context.platform) # Clone testbed print() @@ -627,7 +694,7 @@ def package(context): print() print("Create package archive...") shutil.make_archive( - CROSS_BUILD_DIR / archive_name, + str(CROSS_BUILD_DIR / archive_name), format="gztar", root_dir=CROSS_BUILD_DIR / context.platform, base_dir=".", @@ -636,7 +703,8 @@ def package(context): print(f"{archive_name.relative_to(PYTHON_DIR)}.tar.gz created.") -def build(context, host=None): +def build(context: argparse.Namespace, host: str | None = None) -> None: + """The implementation of the "build" command.""" if host is None: host = context.host @@ -668,7 +736,8 @@ def build(context, host=None): package(context) -def test(context, host=None): +def test(context: argparse.Namespace, host: str | None = None) -> None: + """The implementation of the "test" command.""" if host is None: host = context.host @@ -725,7 +794,11 @@ def test(context, host=None): "run", "--verbose", ] - + (["--simulator", context.simulator] if context.simulator else []) + + ( + ["--simulator", str(context.simulator)] + if context.simulator + else [] + ) + [ "--", "test", @@ -737,14 +810,21 @@ def test(context, host=None): ) -def ci(context): +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(): - parser = argparse.ArgumentParser() +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "A tool for managing the build, package and test process of " + "CPython on Apple platforms." + ), + suggest_on_error=True, + ) subcommands = parser.add_subparsers(dest="subcommand", required=True) clean = subcommands.add_parser( @@ -850,7 +930,7 @@ def parse_args(): return parser.parse_args() -def print_called_process_error(e): +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) @@ -865,11 +945,18 @@ def print_called_process_error(e): ) -def main(): - install_signal_handler() +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 = { + dispatch: dict[str, Callable] = { "clean": clean, "configure-build": configure_build_python, "make-build": make_build_python, @@ -884,8 +971,13 @@ def main(): 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__": From ceff9278d237b4dfcffe1ffe359c548ec284890a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 29 Aug 2025 08:46:47 +0800 Subject: [PATCH 08/15] Minor markdown fixes. --- Apple/iOS/README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Apple/iOS/README.md b/Apple/iOS/README.md index c3bf0b7a50c6e2..01833e6c050406 100644 --- a/Apple/iOS/README.md +++ b/Apple/iOS/README.md @@ -97,7 +97,7 @@ Python build for a single framework, the following options are available. Specify the name for the Python framework; defaults to `Python`. - > [!Note] + > [!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 @@ -119,14 +119,14 @@ 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 +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: @@ -237,9 +237,9 @@ To run the test suite, configure a Python build for an iOS simulator (i.e., `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 +make all +make install +make testios ``` This will: @@ -267,7 +267,7 @@ testbed will be created in a directory named You can generate your own standalone testbed instance by running: ``` - python cross-build/iOS/testbed clone my-testbed +python cross-build/iOS/testbed clone my-testbed ``` In this invocation, `my-testbed` is the name of the folder for the new @@ -276,7 +276,7 @@ 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 +python Apple/testbed clone --platform iOS --framework my-testbed ``` The framework path can be the path path to a `Python.xcframework`, or the @@ -286,7 +286,7 @@ 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 +python my-testbed run -- test -W test_os ``` This is the equivalent of running `python -m test -W test_os` on a desktop @@ -297,7 +297,7 @@ they were arguments to `python -m` on a desktop machine. You can also open the testbed project in Xcode by running: ``` - open my-testbed/iOSTestbed.xcodeproj +open my-testbed/iOSTestbed.xcodeproj ``` This will allow you to use the full Xcode suite of tools for debugging. From e856075e20914617ecbb036a0694ac791f22b4eb Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 10 Sep 2025 19:04:16 +0800 Subject: [PATCH 09/15] Corrected minor typos found in review. --- Apple/__main__.py | 6 +++--- Apple/iOS/README.md | 6 +++--- Mac/Resources/framework/Info.plist.in | 2 -- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Apple/__main__.py b/Apple/__main__.py index 535349b23843bb..43782487fe1869 100644 --- a/Apple/__main__.py +++ b/Apple/__main__.py @@ -225,7 +225,7 @@ def build_python_path() -> Path: @contextmanager def group(text: str): - """A context manager that ouptut a log marker around a section of a build. + """A context manager that output 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. @@ -438,7 +438,7 @@ def framework_path(host_triple: str, multiarch: str) -> Path: def package_version(prefix_path: Path) -> str: - """Extract the Python version being build from patchlevel.h.""" + """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( @@ -823,8 +823,8 @@ def parse_args() -> argparse.Namespace: "A tool for managing the build, package and test process of " "CPython on Apple platforms." ), - suggest_on_error=True, ) + parser.suggest_on_error = True subcommands = parser.add_subparsers(dest="subcommand", required=True) clean = subcommands.add_parser( diff --git a/Apple/iOS/README.md b/Apple/iOS/README.md index 01833e6c050406..124a05657aae09 100644 --- a/Apple/iOS/README.md +++ b/Apple/iOS/README.md @@ -56,7 +56,7 @@ 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 iOS build + python Apple build iOS This will: @@ -222,7 +222,7 @@ simulator build with a deployment target of 15.4. Once you have a built an XCframework, you can test that framework by running: - $ python Apple iOS test + $ python Apple test iOS ### Testing a single-architecture framework @@ -258,7 +258,7 @@ project, and then boot and prepare the iOS simulator. ### Debugging test failures -Running `python Apple iOS test` generates a standalone version of the +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 diff --git a/Mac/Resources/framework/Info.plist.in b/Mac/Resources/framework/Info.plist.in index fbf747affe9af8..4c42971ed90ee4 100644 --- a/Mac/Resources/framework/Info.plist.in +++ b/Mac/Resources/framework/Info.plist.in @@ -24,8 +24,6 @@ ???? CFBundleVersion %VERSION% - MinimumOSVersion - @MACOSX_DEPLOYMENT_TARGET@ CFBundleAllowMixedLocalizations From 2606b8f519869e1ccb91b3ebaa6877bcdbce7168 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 10 Sep 2025 19:32:22 +0800 Subject: [PATCH 10/15] Correct handling of pre-release Python versions. --- Apple/__main__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Apple/__main__.py b/Apple/__main__.py index 43782487fe1869..635c23e18208fe 100644 --- a/Apple/__main__.py +++ b/Apple/__main__.py @@ -447,7 +447,11 @@ def package_version(prefix_path: Path) -> str: 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. + # 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") From 2114dd570ea6537ffe1a67141f5a1630495b0767 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 17 Sep 2025 14:00:35 +0100 Subject: [PATCH 11/15] Remove redundant copies of the standard library. --- Apple/__main__.py | 137 +++++++++--------- .../{ => build}/dylib-Info-template.plist | 0 Apple/iOS/Resources/build/utils.sh | 130 +++++++++++++++++ .../iOSTestbed.xcodeproj/project.pbxproj | 33 +---- .../iOSTestbed/dylib-Info-template.plist | 26 ---- 5 files changed, 201 insertions(+), 125 deletions(-) rename Apple/iOS/Resources/{ => build}/dylib-Info-template.plist (100%) create mode 100755 Apple/iOS/Resources/build/utils.sh delete mode 100644 Apple/testbed/iOSTestbed/dylib-Info-template.plist diff --git a/Apple/__main__.py b/Apple/__main__.py index 635c23e18208fe..f04a5a7d4a1241 100644 --- a/Apple/__main__.py +++ b/Apple/__main__.py @@ -225,7 +225,7 @@ def build_python_path() -> Path: @contextmanager def group(text: str): - """A context manager that output a log marker around a section of a build. + """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. @@ -460,6 +460,43 @@ def package_version(prefix_path: Path) -> str: sys.exit("Unable to determine Python version being packaged.") +def lib_platform_files(dirname, names): + """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"): + 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. @@ -547,6 +584,7 @@ def create_xcframework(platform: str) -> str: # 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. @@ -575,71 +613,26 @@ def create_xcframework(platform: str) -> str: slice_framework / "Headers/pyconfig.h", ) - # Copy the lib folder. If there's only one slice, we can copy the .so - # binary modules as is. Otherwise, we ignore .so files, and merge them - # into fat binaries in the next step. - print(f" - {slice_name} standard library") - shutil.copytree( - first_path / "lib", - slice_path / "lib", - ignore=( - None - if len(slice_parts) == 1 - else shutil.ignore_patterns("*.so") - ), - ) - - # If there's more than one slice, merge binary .so modules. - if len(slice_parts) > 1: - print(f" - {slice_name} merging binary modules") - lib_dirs = [ - CROSS_BUILD_DIR - / f"{host_triple}/Apple/iOS/Frameworks" - / f"{multiarch}/lib" - for host_triple, multiarch in slice_parts.items() - ] - - # The list of .so binary modules should be the same in each slice. - # Find all .so files in each slice; then sort and zip those lists. - # Zipping with strict=True means any length discrepancy will raise - # an error. - for lib_set in zip( - *(sorted(lib_dir.glob("**/*.so")) for lib_dir in lib_dirs), - strict=True, - ): - # An additional safety check - not only must the two lists of - # libraries be the same length, but they must have the same - # module names. Raise an error if there's any discrepancy. - relative_libs = set( - lib.relative_to(lib_dir.parent) - for lib_dir, lib in zip(lib_dirs, lib_set) - ) - if len(relative_libs) != 1: - raise RuntimeError( - f"Cannot merge non-matching libraries: {relative_libs}" - ) from None - - # Merge the per-arch .so files into a single "fat" binary. - relative_lib = next(iter(relative_libs)) - run( - [ - "lipo", - "-create", - "-output", - slice_path / relative_lib, - ] - + [ - ( - CROSS_BUILD_DIR - / f"{host_triple}/Apple/iOS/Frameworks/{multiarch}" - / relative_lib - ) - for host_triple, multiarch in slice_parts.items() - ] - ) - 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 = ( @@ -654,12 +647,11 @@ def create_xcframework(platform: str) -> str: slice_framework / f"Headers/pyconfig-{arch}.h", ) - # Copy any files (such as sysconfig) that are multiarch-specific. - for path in host_path.glob(f"lib/**/*_{multiarch}.*"): - shutil.copy( - path, - slice_path / (path.relative_to(host_path)), - ) + print(" - build tools") + shutil.copytree( + PYTHON_DIR / "Apple/iOS/Resources/build", + package_path / "Python.xcframework/build", + ) return version @@ -925,6 +917,11 @@ def parse_args() -> argparse.Namespace: "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( diff --git a/Apple/iOS/Resources/dylib-Info-template.plist b/Apple/iOS/Resources/build/dylib-Info-template.plist similarity index 100% rename from Apple/iOS/Resources/dylib-Info-template.plist rename to Apple/iOS/Resources/build/dylib-Info-template.plist diff --git a/Apple/iOS/Resources/build/utils.sh b/Apple/iOS/Resources/build/utils.sh new file mode 100755 index 00000000000000..42459ca1bb4124 --- /dev/null +++ b/Apple/iOS/Resources/build/utils.sh @@ -0,0 +1,130 @@ +# 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/lib-$ARCHS" + else + SLICE_FOLDER="ios-arm64_x86_64-simulator/lib-$ARCHS" + fi + else + echo "Installing Python modules for iOS Device" + SLICE_FOLDER="ios-arm64/lib-$ARCHS/" + fi + + rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" + rsync -au "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/" "$CODESIGNING_FOLDER_PATH/python/lib/" +} + +# 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 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/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" + + 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 +} diff --git a/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj b/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj index 0a7b968aa143d1..f8835a3bc587df 100644 --- a/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj +++ b/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 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, ); }; }; - 607A66582B0F079F0010BFC8 /* dylib-Info-template.plist in Resources */ = {isa = PBXBuildFile; fileRef = 607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */; }; 608619542CB77BA900F46182 /* app_packages in Resources */ = {isa = PBXBuildFile; fileRef = 608619532CB77BA900F46182 /* app_packages */; }; 608619562CB7819B00F46182 /* app in Resources */ = {isa = PBXBuildFile; fileRef = 608619552CB7819B00F46182 /* app */; }; /* End PBXBuildFile section */ @@ -66,7 +65,6 @@ 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 = ""; }; - 607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "dylib-Info-template.plist"; 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 = ""; }; @@ -120,7 +118,6 @@ 608619552CB7819B00F46182 /* app */, 608619532CB77BA900F46182 /* app_packages */, 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */, - 607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */, 607A66152B0EFA380010BFC8 /* AppDelegate.h */, 607A66162B0EFA380010BFC8 /* AppDelegate.m */, 607A66212B0EFA390010BFC8 /* Assets.xcassets */, @@ -155,8 +152,7 @@ 607A660E2B0EFA380010BFC8 /* Sources */, 607A660F2B0EFA380010BFC8 /* Frameworks */, 607A66102B0EFA380010BFC8 /* Resources */, - 607A66552B0F061D0010BFC8 /* Install Target Specific Python Standard Library */, - 607A66562B0F06200010BFC8 /* Prepare Python Binary Modules */, + 607A66552B0F061D0010BFC8 /* Process Python libraries */, 607A664E2B0EFC080010BFC8 /* Embed Frameworks */, ); buildRules = ( @@ -230,7 +226,6 @@ buildActionMask = 2147483647; files = ( 607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */, - 607A66582B0F079F0010BFC8 /* dylib-Info-template.plist in Resources */, 608619562CB7819B00F46182 /* app in Resources */, 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */, 608619542CB77BA900F46182 /* app_packages in Resources */, @@ -247,7 +242,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 607A66552B0F061D0010BFC8 /* Install Target Specific Python Standard Library */ = { + 607A66552B0F061D0010BFC8 /* Process Python libraries */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -257,34 +252,14 @@ ); inputPaths = ( ); - name = "Install Target Specific Python Standard Library"; + name = "Process Python libraries"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\n\nmkdir -p \"$CODESIGNING_FOLDER_PATH/python/lib\"\nif [ \"$EFFECTIVE_PLATFORM_NAME\" = \"-iphonesimulator\" ]; then\n echo \"Installing Python modules for iOS Simulator\"\n rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64_x86_64-simulator/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\" \nelse\n echo \"Installing Python modules for iOS Device\"\n rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\" \nfi\n"; - showEnvVarsInLog = 0; - }; - 607A66562B0F06200010BFC8 /* Prepare Python Binary Modules */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Prepare Python Binary Modules"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "set -e\n\ninstall_dylib () {\n INSTALL_BASE=$1\n FULL_EXT=$2\n\n # The name of the extension file\n EXT=$(basename \"$FULL_EXT\")\n # The location of the extension file, relative to the bundle\n RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} \n # The path to the extension file, relative to the install base\n PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/}\n # The full dotted name of the extension module, constructed from the file path.\n FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d \".\" -f 1 | tr \"/\" \".\"); \n # A bundle identifier; not actually used, but required by Xcode framework packaging\n FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr \"_\" \"-\")\n # The name of the framework folder.\n FRAMEWORK_FOLDER=\"Frameworks/$FULL_MODULE_NAME.framework\"\n\n # If the framework folder doesn't exist, create it.\n if [ ! -d \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\" ]; then\n echo \"Creating framework for $RELATIVE_EXT\" \n mkdir -p \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\"\n cp \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n plutil -replace CFBundleExecutable -string \"$FULL_MODULE_NAME\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n plutil -replace CFBundleIdentifier -string \"$FRAMEWORK_BUNDLE_ID\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n fi\n \n echo \"Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" \n mv \"$FULL_EXT\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\"\n # Create a placeholder .fwork file where the .so was\n echo \"$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" > ${FULL_EXT%.so}.fwork\n # Create a back reference to the .so file location in the framework\n echo \"${RELATIVE_EXT%.so}.fwork\" > \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin\" \n}\n\nPYTHON_VER=$(ls -1 \"$CODESIGNING_FOLDER_PATH/python/lib\")\necho \"Install Python $PYTHON_VER standard library extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/python/lib/$PYTHON_VER/lib-dynload\" -name \"*.so\" | while read FULL_EXT; do\n install_dylib python/lib/$PYTHON_VER/lib-dynload/ \"$FULL_EXT\"\ndone\necho \"Install app package extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/app_packages\" -name \"*.so\" | while read FULL_EXT; do\n install_dylib app_packages/ \"$FULL_EXT\"\ndone\necho \"Install app extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/app\" -name \"*.so\" | while read FULL_EXT; do\n install_dylib app/ \"$FULL_EXT\"\ndone\n\n# Clean up dylib template \nrm -f \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\"\necho \"Signing frameworks as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)...\"\nfind \"$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 \"{}\" \\; \n"; + shellScript = "set -e\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\ninstall_python Python.xcframework app app_packages\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/Apple/testbed/iOSTestbed/dylib-Info-template.plist b/Apple/testbed/iOSTestbed/dylib-Info-template.plist deleted file mode 100644 index d6caa01c1e44b9..00000000000000 --- a/Apple/testbed/iOSTestbed/dylib-Info-template.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - - CFBundleIdentifier - - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSupportedPlatforms - - iPhoneOS - - MinimumOSVersion - 13.0 - CFBundleVersion - 1 - - From d56a3deb8a2aaf79e4de233f3fd01820f559c485 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 17 Sep 2025 14:01:17 +0100 Subject: [PATCH 12/15] Update documentation to reflect new install entry script. --- Doc/using/ios.rst | 121 ++++++++++------------------------------------ 1 file changed, 26 insertions(+), 95 deletions(-) diff --git a/Doc/using/ios.rst b/Doc/using/ios.rst index af214e5fd465b4..c02dac444dd7cc 100644 --- a/Doc/using/ios.rst +++ b/Doc/using/ios.rst @@ -180,22 +180,19 @@ To add Python to an iOS Xcode project: of your project; however, you can use any other location that you want by adjusting paths as needed. -3. Drag the ``Apple/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,87 +208,24 @@ To add Python to an iOS Xcode project: * 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*; @@ -310,22 +244,19 @@ To add Python to an iOS Xcode project: 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. - -* 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. + 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 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 From 4feeabe58c6cf98af2729b01124f344745476956 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 17 Sep 2025 16:54:37 +0100 Subject: [PATCH 13/15] Correct handling of --fast-ci and --slow-ci options. --- Apple/__main__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Apple/__main__.py b/Apple/__main__.py index f04a5a7d4a1241..5bdee5396e1c5b 100644 --- a/Apple/__main__.py +++ b/Apple/__main__.py @@ -798,10 +798,15 @@ def test(context: argparse.Namespace, host: str | None = None) -> None: + [ "--", "test", - "-uall", + "--slow-ci" if context.slow else "--fast-ci", "--single-process", - "--rerun", - "-W", + "--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", ] ) From ebbbf3241f81b009ec063487b148b5238b73b067 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 17 Sep 2025 17:16:23 +0100 Subject: [PATCH 14/15] Restore the CODEOWNERS entry for the top level iOS folder. --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 28201b36d10886..96727d45d074db 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -151,6 +151,7 @@ Lib/test/test_android.py @mhsmith @freakboy3742 Doc/using/ios.rst @freakboy3742 Lib/_ios_support.py @freakboy3742 Apple/ @freakboy3742 +iOS/ @freakboy3742 # macOS Mac/ @python/macos-team From e0f6b5a5b074e7e05b7bb321db060052e905a2f9 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 19 Sep 2025 12:01:13 +0100 Subject: [PATCH 15/15] Move build script into stub XCframework so that 'make testios' has access. --- Apple/__main__.py | 2 +- .../build/iOS-dylib-Info-template.plist} | 0 .../Python.xcframework}/build/utils.sh | 19 +++++++++++++------ 3 files changed, 14 insertions(+), 7 deletions(-) rename Apple/{iOS/Resources/build/dylib-Info-template.plist => testbed/Python.xcframework/build/iOS-dylib-Info-template.plist} (100%) rename Apple/{iOS/Resources => testbed/Python.xcframework}/build/utils.sh (85%) diff --git a/Apple/__main__.py b/Apple/__main__.py index 5bdee5396e1c5b..fc19b31be97bb2 100644 --- a/Apple/__main__.py +++ b/Apple/__main__.py @@ -649,7 +649,7 @@ def create_xcframework(platform: str) -> str: print(" - build tools") shutil.copytree( - PYTHON_DIR / "Apple/iOS/Resources/build", + PYTHON_DIR / "Apple/testbed/Python.xcframework/build", package_path / "Python.xcframework/build", ) diff --git a/Apple/iOS/Resources/build/dylib-Info-template.plist b/Apple/testbed/Python.xcframework/build/iOS-dylib-Info-template.plist similarity index 100% rename from Apple/iOS/Resources/build/dylib-Info-template.plist rename to Apple/testbed/Python.xcframework/build/iOS-dylib-Info-template.plist diff --git a/Apple/iOS/Resources/build/utils.sh b/Apple/testbed/Python.xcframework/build/utils.sh similarity index 85% rename from Apple/iOS/Resources/build/utils.sh rename to Apple/testbed/Python.xcframework/build/utils.sh index 42459ca1bb4124..9cfe74720f26d4 100755 --- a/Apple/iOS/Resources/build/utils.sh +++ b/Apple/testbed/Python.xcframework/build/utils.sh @@ -30,17 +30,24 @@ install_stdlib() { 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/lib-$ARCHS" + SLICE_FOLDER="ios-arm64-simulator" else - SLICE_FOLDER="ios-arm64_x86_64-simulator/lib-$ARCHS" + SLICE_FOLDER="ios-arm64_x86_64-simulator" fi else echo "Installing Python modules for iOS Device" - SLICE_FOLDER="ios-arm64/lib-$ARCHS/" + SLICE_FOLDER="ios-arm64" fi - rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" - rsync -au "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/" "$CODESIGNING_FOLDER_PATH/python/lib/" + # 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. @@ -75,7 +82,7 @@ install_dylib () { 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/dylib-Info-template.plist" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" + 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