diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e8b17d5c..34bff62f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -140,11 +140,6 @@ jobs: fail-fast: false matrix: platform: ["macOS", "iOS"] - include: - - briefcase-run-args: - - - platform: iOS - briefcase-run-args: ' -d "iPhone 17e::iOS 26.5"' steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -186,7 +181,7 @@ jobs: PYTHON_VER: ${{ needs.config.outputs.PYTHON_VER }} BUILD_NUMBER: ${{ needs.config.outputs.BUILD_NUMBER }} run: | - briefcase run ${{ matrix.platform }} Xcode --test ${{ matrix.briefcase-run-args }} -C support_package=\'../dist/Python-${PYTHON_VER}-${{ matrix.platform }}-support.${BUILD_NUMBER}.tar.gz\' + briefcase run ${{ matrix.platform }} Xcode --test -C support_package=\'../dist/Python-${PYTHON_VER}-${{ matrix.platform }}-support.${BUILD_NUMBER}.tar.gz\' cpython-testbed: name: CPython testbed (${{ matrix.platform }}) @@ -195,10 +190,7 @@ jobs: strategy: fail-fast: false matrix: - platform: ["iOS", "tvOS"] - include: - - platform: "iOS" - testbed-args: '--simulator "iPhone 17e,arch=arm64,OS=26.5"' + platform: ["iOS", "tvOS", "watchOS"] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/Makefile b/Makefile index 866d1032..e2abfdef 100644 --- a/Makefile +++ b/Makefile @@ -286,7 +286,7 @@ $$(PYTHON_SRCDIR-$(target))/configure: \ # Apply target Python patches cd $$(PYTHON_SRCDIR-$(target)) && patch -p1 < $(PROJECT_DIR)/patch/Python/Python.patch # Make sure the binary scripts are executable - chmod 755 $$(PYTHON_SRCDIR-$(target))/Apple/$(os)/Resources/bin/* + chmod 755 $$(PYTHON_SRCDIR-$(target))/Platforms/Apple/$(os)/Resources/bin/* # Touch the configure script to ensure that Make identifies it as up to date. touch $$(PYTHON_SRCDIR-$(target))/configure @@ -294,7 +294,7 @@ $$(PYTHON_SRCDIR-$(target))/Makefile: \ $$(PYTHON_SRCDIR-$(target))/configure # Configure target Python cd $$(PYTHON_SRCDIR-$(target)) && \ - PATH="$(PROJECT_DIR)/$$(PYTHON_SRCDIR-$(target))/Apple/$(os)/Resources/bin:$(PATH)" \ + PATH="$(PROJECT_DIR)/$$(PYTHON_SRCDIR-$(target))/Platforms/Apple/$(os)/Resources/bin:$(PATH)" \ ./configure \ LIBLZMA_CFLAGS="-I$$(XZ_INSTALL-$(target))/include" \ LIBLZMA_LIBS="-L$$(XZ_INSTALL-$(target))/lib -llzma" \ @@ -315,14 +315,14 @@ $$(PYTHON_SRCDIR-$(target))/Makefile: \ $$(PYTHON_SRCDIR-$(target))/python.exe: $$(PYTHON_SRCDIR-$(target))/Makefile @echo ">>> Build Python for $(target)" cd $$(PYTHON_SRCDIR-$(target)) && \ - PATH="$(PROJECT_DIR)/$$(PYTHON_SRCDIR-$(target))/Apple/$(os)/Resources/bin:$(PATH)" \ + PATH="$(PROJECT_DIR)/$$(PYTHON_SRCDIR-$(target))/Platforms/Apple/$(os)/Resources/bin:$(PATH)" \ make all \ 2>&1 | tee -a ../python-$(PYTHON_VERSION).build.log $$(PYTHON_LIB-$(target)): $$(PYTHON_SRCDIR-$(target))/python.exe @echo ">>> Install Python for $(target)" cd $$(PYTHON_SRCDIR-$(target)) && \ - PATH="$(PROJECT_DIR)/$$(PYTHON_SRCDIR-$(target))/Apple/$(os)/Resources/bin:$(PATH)" \ + PATH="$(PROJECT_DIR)/$$(PYTHON_SRCDIR-$(target))/Platforms/Apple/$(os)/Resources/bin:$(PATH)" \ make install \ 2>&1 | tee -a ../python-$(PYTHON_VERSION).install.log @@ -488,7 +488,7 @@ $$(PYTHON_INCLUDE-$(sdk))/pyconfig.h: $$(PYTHON_LIB-$(sdk)) $$(foreach target,$$(SDK_TARGETS-$(sdk)),cp $$(PYTHON_INCLUDE-$$(target))/pyconfig.h $$(PYTHON_INCLUDE-$(sdk))/pyconfig-$$(ARCH-$$(target)).h; ) # Copy the cross-target header from the source folder of the first target in the $(sdk) SDK - cp $$(PYTHON_SRCDIR-$$(firstword $$(SDK_TARGETS-$(sdk))))/Apple/$(os)/Resources/pyconfig.h $$(PYTHON_INCLUDE-$(sdk))/pyconfig.h + cp $$(PYTHON_SRCDIR-$$(firstword $$(SDK_TARGETS-$(sdk))))/Platforms/Apple/$(os)/Resources/pyconfig.h $$(PYTHON_INCLUDE-$(sdk))/pyconfig.h $$(PYTHON_PLATFORM_CONFIG-$(sdk))/sitecustomize.py: $$(PYTHON_LIB-$(sdk)) $$(PYTHON_FRAMEWORK-$(sdk))/Info.plist $$(PYTHON_INCLUDE-$(sdk))/pyconfig.h $$(foreach target,$$(SDK_TARGETS-$(sdk)),$$(PYTHON_PLATFORM_SITECUSTOMIZE-$$(target))) @@ -647,8 +647,8 @@ $$(PYTHON_XCFRAMEWORK-$(os))/Info.plist: \ @echo ">>> Install build tools for $(os)" mkdir $$(PYTHON_XCFRAMEWORK-$(os))/build - cp $$(PYTHON_SRCDIR-$$(firstword $$(SDK_TARGETS-$$(firstword $$(SDKS-$(os))))))/Apple/testbed/Python.xcframework/build/utils.sh $$(PYTHON_XCFRAMEWORK-$(os))/build - cp $$(PYTHON_SRCDIR-$$(firstword $$(SDK_TARGETS-$$(firstword $$(SDKS-$(os))))))/Apple/testbed/Python.xcframework/build/$$(PLATFORM_NAME-$(os))-dylib-Info-template.plist $$(PYTHON_XCFRAMEWORK-$(os))/build + cp $$(PYTHON_SRCDIR-$$(firstword $$(SDK_TARGETS-$$(firstword $$(SDKS-$(os))))))/Platforms/Apple/testbed/Python.xcframework/build/utils.sh $$(PYTHON_XCFRAMEWORK-$(os))/build + cp $$(PYTHON_SRCDIR-$$(firstword $$(SDK_TARGETS-$$(firstword $$(SDKS-$(os))))))/Platforms/Apple/testbed/Python.xcframework/build/$$(PLATFORM_NAME-$(os))-dylib-Info-template.plist $$(PYTHON_XCFRAMEWORK-$(os))/build @echo ">>> Install stdlib for $(os)" mkdir -p $$(PYTHON_XCFRAMEWORK-$(os))/lib @@ -669,9 +669,9 @@ $$(PYTHON_XCFRAMEWORK-$(os))/Info.plist: \ # Create symlink for dylib $$(foreach sdk,$$(SDKS-$(os)),ln -si ../Python.framework/Python $$(PYTHON_XCFRAMEWORK-$(os))/$$(SDK_SLICE-$$(sdk))/lib/libpython$(PYTHON_VER).dylib; ) -ifeq ($(filter $(os),iOS tvOS),$(os)) +ifeq ($(filter $(os),iOS tvOS watchOS),$(os)) @echo ">>> Clone testbed project for $(os)" - $(HOST_PYTHON) $$(PYTHON_SRCDIR-$$(firstword $$(SDK_TARGETS-$$(firstword $$(SDKS-$(os))))))/Apple/testbed clone --platform $(os) --framework $$(PYTHON_XCFRAMEWORK-$(os)) support/$(PYTHON_VER)/$(os)/testbed + $(HOST_PYTHON) $$(PYTHON_SRCDIR-$$(firstword $$(SDK_TARGETS-$$(firstword $$(SDKS-$(os))))))/Platforms/Apple/testbed clone --platform $(os) --framework $$(PYTHON_XCFRAMEWORK-$(os)) support/$(PYTHON_VER)/$(os)/testbed endif @echo ">>> Create VERSIONS file for $(os)" diff --git a/README.md b/README.md index 85fcbedc..d15837ca 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This is a meta-package for building a version of Python that can be embedded int - [Python 3.13](https://github.com/beeware/Python-Apple-support/tree/3.13) - [Python 3.14](https://github.com/beeware/Python-Apple-support/tree/3.14) - [Python 3.15](https://github.com/beeware/Python-Apple-support/tree/3.15) +- [Python 3.16](https://github.com/beeware/Python-Apple-support/tree/3.16) It works by downloading, patching, and building a fat binary of Python and selected pre-requisites, and packaging them as frameworks that can be incorporated into an Xcode project. The binary modules in the Python standard library are distributed as binaries that can be dynamically loaded at runtime. @@ -35,7 +36,7 @@ The binaries support x86_64 and arm64 for macOS; arm64 for iOS and appleTV devic - tvOS 12.0 or later, on: - Apple TV (4th gen or later) -- watchOS 4.0 or later, on: +- watchOS 9.0 or later, on: - Apple Watch (4th gen or later) - visionOS 2.0 or later, on: diff --git a/patch/Python/Python.patch b/patch/Python/Python.patch index 742ad15e..40dd2e91 100644 --- a/patch/Python/Python.patch +++ b/patch/Python/Python.patch @@ -1,26 +1,30 @@ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml -index ad91fe68a33..25dffd4d899 100644 +index ad91fe68a33..a177bfae949 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml -@@ -2,6 +2,10 @@ +@@ -1,8 +1,12 @@ + repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.1 +- rev: v0.9.1 ++ rev: a27a2e47c7751b639d2b5badf0ef6ff11fee893f # frozen: v0.15.4 hooks: -+ - id: ruff -+ name: Run Ruff (lint) on Apple/ -+ args: [--exit-non-zero-on-fix, --config=Apple/.ruff.toml] -+ files: ^Apple/ - - id: ruff +- - id: ruff ++ - id: ruff-check ++ name: Run Ruff (lint) on Platforms/Apple/ ++ args: [--exit-non-zero-on-fix, --config=Platforms/Apple/.ruff.toml] ++ files: ^Platforms/Apple/ ++ - id: ruff-check name: Run Ruff (lint) on Doc/ args: [--exit-non-zero-on-fix] + files: ^Doc/ @@ -14,6 +18,10 @@ name: Run Ruff (lint) on Argument Clinic args: [--exit-non-zero-on-fix, --config=Tools/clinic/.ruff.toml] files: ^Tools/clinic/|Lib/test/test_clinic.py + - id: ruff-format -+ name: Run Ruff (format) on Apple/ -+ args: [--config=Apple/.ruff.toml] -+ files: ^Apple ++ name: Run Ruff (format) on Platforms/Apple/ ++ args: [--exit-non-zero-on-fix, --config=Platforms/Apple/.ruff.toml] ++ files: ^Platforms/Apple/ - id: ruff-format name: Run Ruff (format) on Doc/ args: [--check] @@ -42,9741 +46,10428 @@ index 00000000000..1c015fa8841 +# To override this, use ``fix = false`` in a subdirectory's config file +# or ``--no-fix`` on the command line. +fix = true -diff --git a/Apple/.ruff.toml b/Apple/.ruff.toml +diff --git a/Doc/includes/wasm-ios-notavail.rst b/Doc/includes/wasm-ios-notavail.rst new file mode 100644 -index 00000000000..712c207749e +index 00000000000..c820665f5e4 --- /dev/null -+++ b/Apple/.ruff.toml -@@ -0,0 +1,22 @@ -+extend = "../.ruff.toml" # Inherit the project-wide settings ++++ b/Doc/includes/wasm-ios-notavail.rst +@@ -0,0 +1,8 @@ ++.. include for modules that don't work on WASM or iOS + -+[format] -+preview = true -+docstring-code-format = true ++.. availability:: not WASI, not iOS. + -+[lint] -+select = [ -+ "C4", # flake8-comprehensions -+ "E", # pycodestyle -+ "F", # pyflakes -+ "I", # isort -+ "ISC", # flake8-implicit-str-concat -+ "LOG", # flake8-logging -+ "PGH", # pygrep-hooks -+ "PT", # flake8-pytest-style -+ "PYI", # flake8-pyi -+ "RUF100", # Ban unused `# noqa` comments -+ # "UP", # pyupgrade -+ "W", # pycodestyle -+ "YTT", # flake8-2020 -+] -diff --git a/Apple/__main__.py b/Apple/__main__.py -new file mode 100644 -index 00000000000..ea00143256a ---- /dev/null -+++ b/Apple/__main__.py -@@ -0,0 +1,1164 @@ -+#!/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 supports iOS, tvOS, visionOS and watchOS, but it has been -+# constructed so that it could be used on any Apple platform. -+# -+# The simplest entry point is: -+# -+# $ python Apple ci iOS -+# -+# (replace iOS with tvOS, visionOS or watchOS as required.) -+# -+# which will: -+# * Clean any pre-existing build artefacts -+# * Configure and make a Python that can be used for the build -+# * Configure and make a Python for each supported iOS/tvOS/watchOS/visionOS -+# architecture and ABI -+# * Combine the outputs of the builds from the previous step into a single -+# XCframework, merging binaries into a "fat" binary if necessary -+# * Clone a copy of the testbed, configured to use the XCframework -+# * Construct a tarball containing the release artefacts -+# * Run the test suite using the generated XCframework. -+# -+# This is the complete sequence that would be needed in CI to build and test -+# a candidate release artefact. -+# -+# Each individual step can be invoked individually - there are commands to -+# clean, configure-build, make-build, configure-host, make-host, package, and -+# test. -+# -+# There is also a build command that can be used to combine the configure and -+# make steps for the build Python, an individual host, all hosts, or all -+# builds. -+########################################################################## -+from __future__ import annotations ++ This module does not work or is not available on WebAssembly platforms, or ++ on iOS. See :ref:`wasm-availability` for more information on WASM ++ availability; see :ref:`iOS-availability` for more information on iOS ++ availability. +diff --git a/Doc/includes/wasm-notavail.rst b/Doc/includes/wasm-notavail.rst +index e680e1f9b43..c1b79d2a4a0 100644 +--- a/Doc/includes/wasm-notavail.rst ++++ b/Doc/includes/wasm-notavail.rst +@@ -1,7 +1,6 @@ + .. include for modules that don't work on WASM + +-.. availability:: not Emscripten, not WASI. ++.. availability:: not WASI. + +- This module does not work or is not available on WebAssembly platforms +- ``wasm32-emscripten`` and ``wasm32-wasi``. See ++ This module does not work or is not available on WebAssembly. See + :ref:`wasm-availability` for more information. +diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst +index 2ebda3d3396..91ea6150fb1 100644 +--- a/Doc/library/curses.rst ++++ b/Doc/library/curses.rst +@@ -21,6 +21,8 @@ + designed to match the API of ncurses, an open-source curses library hosted on + Linux and the BSD variants of Unix. + ++.. include:: ../includes/wasm-ios-notavail.rst + -+import argparse -+import os -+import platform -+import re -+import shlex -+import shutil -+import signal -+import subprocess -+import sys -+import sysconfig -+import time -+from collections.abc import Callable, Sequence -+from contextlib import contextmanager -+from datetime import datetime, timezone -+from os.path import basename, relpath -+from pathlib import Path -+from subprocess import CalledProcessError + .. note:: + + Whenever the documentation mentions a *character* it can be specified +diff --git a/Doc/library/dbm.rst b/Doc/library/dbm.rst +index 53f186952ce..68a8c30aa99 100644 +--- a/Doc/library/dbm.rst ++++ b/Doc/library/dbm.rst +@@ -14,6 +14,7 @@ + is a `third party interface `_ to + the Oracle Berkeley DB. + ++.. include:: ../includes/wasm-ios-notavail.rst + + .. exception:: error + +@@ -398,4 +399,3 @@ + .. method:: dumbdbm.close() + + Close the database. +- +diff --git a/Doc/library/ensurepip.rst b/Doc/library/ensurepip.rst +index 10773ee5f76..692ba9eb6a9 100644 +--- a/Doc/library/ensurepip.rst ++++ b/Doc/library/ensurepip.rst +@@ -38,7 +38,7 @@ + :pep:`453`: Explicit bootstrapping of pip in Python installations + The original rationale and specification for this module. + +-.. include:: ../includes/wasm-notavail.rst ++.. include:: ../includes/wasm-ios-notavail.rst + + Command line interface + ---------------------- +diff --git a/Doc/library/fcntl.rst b/Doc/library/fcntl.rst +index d23a105cd5b..1faef54c116 100644 +--- a/Doc/library/fcntl.rst ++++ b/Doc/library/fcntl.rst +@@ -18,7 +18,7 @@ + See the :manpage:`fcntl(2)` and :manpage:`ioctl(2)` Unix manual pages + for full details. + +-.. availability:: Unix, not Emscripten, not WASI. ++.. availability:: Unix, not WASI. + + All functions in this module take a file descriptor *fd* as their first + argument. This can be an integer file descriptor, such as returned by +diff --git a/Doc/library/grp.rst b/Doc/library/grp.rst +index 57a77d51a02..f1157e189a3 100644 +--- a/Doc/library/grp.rst ++++ b/Doc/library/grp.rst +@@ -10,7 +10,7 @@ + This module provides access to the Unix group database. It is available on all + Unix versions. + +-.. availability:: Unix, not Emscripten, not WASI. ++.. availability:: Unix, not WASI, not iOS. + + Group database entries are reported as a tuple-like object, whose attributes + correspond to the members of the ``group`` structure (Attribute field below, see +diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst +index a9d7665ac20..6611bd5018c 100644 +--- a/Doc/library/importlib.rst ++++ b/Doc/library/importlib.rst +@@ -1233,6 +1233,69 @@ + and how the module's :attr:`~module.__file__` is populated. + + ++.. class:: AppleFrameworkLoader(name, path) + -+EnvironmentT = dict[str, str] -+ArgsT = Sequence[str | Path] ++ A specialization of :class:`importlib.machinery.ExtensionFileLoader` that ++ is able to load extension modules in Framework format. + -+SCRIPT_NAME = Path(__file__).name -+PYTHON_DIR = Path(__file__).resolve().parent.parent ++ For compatibility with the iOS App Store, *all* binary modules in an iOS app ++ must be dynamic libraries, contained in a framework with appropriate ++ metadata, stored in the ``Frameworks`` folder of the packaged app. There can ++ be only a single binary per framework, and there can be no executable binary ++ material outside the Frameworks folder. + -+CROSS_BUILD_DIR = PYTHON_DIR / "cross-build" ++ To accomodate this requirement, when running on iOS, extension module ++ binaries are *not* packaged as ``.so`` files on ``sys.path``, but as ++ individual standalone frameworks. To discover those frameworks, this loader ++ is be registered against the ``.fwork`` file extension, with a ``.fwork`` ++ file acting as a placeholder in the original location of the binary on ++ ``sys.path``. The ``.fwork`` file contains the path of the actual binary in ++ the ``Frameworks`` folder, relative to the app bundle. To allow for ++ resolving a framework-packaged binary back to the original location, the ++ framework is expected to contain a ``.origin`` file that contains the ++ location of the ``.fwork`` file, relative to the app bundle. + -+HOSTS: dict[str, dict[str, dict[str, str]]] = { -+ # Structure of this data: -+ # * Platform identifier -+ # * an XCframework slice that must exist for that platform -+ # * a host triple: the multiarch spec for that host -+ "iOS": { -+ "ios-arm64": { -+ "arm64-apple-ios": "arm64-iphoneos", -+ }, -+ "ios-arm64_x86_64-simulator": { -+ "arm64-apple-ios-simulator": "arm64-iphonesimulator", -+ "x86_64-apple-ios-simulator": "x86_64-iphonesimulator", -+ }, -+ }, -+ "tvOS": { -+ "tvos-arm64": { -+ "arm64-apple-tvos": "arm64-appletvos", -+ }, -+ "tvos-arm64_x86_64-simulator": { -+ "arm64-apple-tvos-simulator": "arm64-appletvsimulator", -+ "x86_64-apple-tvos-simulator": "x86_64-appletvsimulator", -+ }, -+ }, -+ "visionOS": { -+ "xros-arm64": { -+ "arm64-apple-xros": "arm64-xros", -+ }, -+ "xros-arm64-simulator": { -+ "arm64-apple-xros-simulator": "arm64-xrsimulator", -+ }, -+ }, -+ "watchOS": { -+ "watchos-arm64_32": { -+ "arm64_32-apple-watchos": "arm64_32-watchos", -+ }, -+ "watchos-arm64_x86_64-simulator": { -+ "arm64-apple-watchos-simulator": "arm64-watchsimulator", -+ "x86_64-apple-watchos-simulator": "x86_64-watchsimulator", -+ }, -+ }, -+} ++ For example, consider the case of an import ``from foo.bar import _whiz``, ++ where ``_whiz`` is implemented with the binary module ++ ``sources/foo/bar/_whiz.abi3.so``, with ``sources`` being the location ++ registered on ``sys.path``, relative to the application bundle. This module ++ *must* be distributed as ++ ``Frameworks/foo.bar._whiz.framework/foo.bar._whiz`` (creating the framework ++ name from the full import path of the module), with an ``Info.plist`` file ++ in the ``.framework`` directory identifying the binary as a framework. The ++ ``foo.bar._whiz`` module would be represented in the original location with ++ a ``sources/foo/bar/_whiz.abi3.fwork`` marker file, containing the path ++ ``Frameworks/foo.bar._whiz/foo.bar._whiz``. The framework would also contain ++ ``Frameworks/foo.bar._whiz.framework/foo.bar._whiz.origin``, containing the ++ path to the ``.fwork`` file. + ++ When a module is loaded with this loader, the ``__file__`` for the module ++ will report as the location of the ``.fwork`` file. This allows code to use ++ the ``__file__`` of a module as an anchor for file system traveral. ++ However, the spec origin will reference the location of the *actual* binary ++ in the ``.framework`` folder. + -+def subdir(name: str, create: bool = False) -> Path: -+ """Ensure that a cross-build directory for the given name exists.""" -+ path = CROSS_BUILD_DIR / name -+ if not path.exists(): -+ if not create: -+ sys.exit( -+ f"{path} does not exist. Create it by running the appropriate " -+ f"`configure` subcommand of {SCRIPT_NAME}." -+ ) -+ else: -+ path.mkdir(parents=True) -+ return path -+ -+ -+def run( -+ command: ArgsT, -+ *, -+ host: str | None = None, -+ env: EnvironmentT | None = None, -+ log: bool | None = True, -+ **kwargs, -+) -> subprocess.CompletedProcess: -+ """Run a command in an Apple development environment. -+ -+ Optionally logs the executed command to the console. -+ """ -+ kwargs.setdefault("check", True) -+ if env is None: -+ env = os.environ.copy() -+ -+ if host: -+ host_env = apple_env(host) -+ print_env(host_env) -+ env.update(host_env) -+ -+ if log: -+ print(">", join_command(command)) -+ return subprocess.run(command, env=env, **kwargs) -+ -+ -+def join_command(args: str | Path | ArgsT) -> str: -+ """Format a command so it can be copied into a shell. -+ -+ Similar to `shlex.join`, but also accepts arguments which are Paths, or a -+ single string/Path outside of a list. -+ """ -+ if isinstance(args, (str, Path)): -+ return str(args) -+ else: -+ return shlex.join(map(str, args)) -+ -+ -+def print_env(env: EnvironmentT) -> None: -+ """Format the environment so it can be pasted into a shell.""" -+ for key, value in sorted(env.items()): -+ print(f"export {key}={shlex.quote(value)}") -+ -+ -+def platform_for_host(host): -+ """Determine the platform for a given host triple.""" -+ for plat, slices in HOSTS.items(): -+ for _, candidates in slices.items(): -+ for candidate in candidates: -+ if candidate == host: -+ return plat -+ raise KeyError(host) -+ -+ -+def apple_env(host: str) -> EnvironmentT: -+ """Construct an Apple development environment for the given host.""" -+ env = { -+ "PATH": ":".join([ -+ str(PYTHON_DIR / f"Apple/{platform_for_host(host)}/Resources/bin"), -+ str(subdir(host) / "prefix"), -+ "/usr/bin", -+ "/bin", -+ "/usr/sbin", -+ "/sbin", -+ "/Library/Apple/usr/bin", -+ ]), -+ } -+ -+ return env -+ -+ -+def delete_path(name: str) -> None: -+ """Delete the named cross-build directory, if it exists.""" -+ path = CROSS_BUILD_DIR / name -+ if path.exists(): -+ print(f"Deleting {path} ...") -+ shutil.rmtree(path) -+ -+ -+def all_host_triples(platform: str) -> list[str]: -+ """Return all host triples for the given platform. ++ The Xcode project building the app is responsible for converting any ``.so`` ++ files from wherever they exist in the ``PYTHONPATH`` into frameworks in the ++ ``Frameworks`` folder (including stripping extensions from the module file, ++ the addition of framework metadata, and signing the resulting framework), ++ and creating the ``.fwork`` and ``.origin`` files. This will usually be done ++ with a build step in the Xcode project; see the iOS documentation for ++ details on how to construct this build step. + -+ 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 ++ .. versionadded:: 3.13 + ++ .. availability:: iOS. + -+def clean(context: argparse.Namespace, target: str | None = None) -> None: -+ """The implementation of the "clean" command.""" -+ if target is None: -+ target = context.host ++ .. attribute:: name + -+ # 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 = [] ++ Name of the module the loader supports. + -+ if target in {"all", "build"}: -+ paths.append("build") ++ .. attribute:: path + -+ if target in {"all", "hosts"}: -+ paths.extend(all_host_triples(context.platform)) -+ elif target not in {"build", "test", "package"}: -+ paths.append(target) ++ Path to the ``.fwork`` file for the extension module. + -+ 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) + :mod:`importlib.util` -- Utility code for importers + --------------------------------------------------- + +diff --git a/Doc/library/intro.rst b/Doc/library/intro.rst +index 5a4c9b8b16a..ffc8939d211 100644 +--- a/Doc/library/intro.rst ++++ b/Doc/library/intro.rst +@@ -58,7 +58,7 @@ + operating system. + + * If not separately noted, all functions that claim "Availability: Unix" are +- supported on macOS, which builds on a Unix core. ++ supported on macOS and iOS, both of which build on a Unix core. + + * If an availability note contains both a minimum Kernel version and a minimum + libc version, then both conditions must hold. For example a feature with note +@@ -119,3 +119,44 @@ + .. _wasmtime: https://wasmtime.dev/ + .. _Pyodide: https://pyodide.org/ + .. _PyScript: https://pyscript.net/ + ++.. _iOS-availability: + -+def build_python_path() -> Path: -+ """The path to the build Python binary.""" -+ build_dir = subdir("build") -+ binary = build_dir / "python" -+ if not binary.is_file(): -+ binary = binary.with_suffix(".exe") -+ if not binary.is_file(): -+ raise FileNotFoundError( -+ f"Unable to find `python(.exe)` in {build_dir}" -+ ) ++iOS ++--- + -+ return binary ++iOS is, in most respects, a POSIX operating system. File I/O, socket handling, ++and threading all behave as they would on any POSIX operating system. However, ++there are several major differences between iOS and other POSIX systems. + ++* iOS can only use Python in "embedded" mode. There is no Python REPL, and no ++ ability to execute binaries that are part of the normal Python developer ++ experience, such as :program:`pip`. To add Python code to your iOS app, you must use ++ the :ref:`Python embedding API ` to add a Python interpreter to an ++ iOS app created with Xcode. See the :ref:`iOS usage guide ` for ++ more details. + -+@contextmanager -+def group(text: str): -+ """A context manager that outputs a log marker around a section of a build. ++* An iOS app cannot use any form of subprocessing, background processing, or ++ inter-process communication. If an iOS app attempts to create a subprocess, ++ the process creating the subprocess will either lock up, or crash. An iOS app ++ has no visibility of other applications that are running, nor any ability to ++ communicate with other running applications, outside of the iOS-specific APIs ++ that exist for this purpose. + -+ If running in a GitHub Actions environment, the GitHub syntax for -+ collapsible log sections is used. -+ """ -+ if "GITHUB_ACTIONS" in os.environ: -+ print(f"::group::{text}") -+ else: -+ print(f"===== {text} " + "=" * (70 - len(text))) ++* iOS apps have limited access to modify system resources (such as the system ++ clock). These resources will often be *readable*, but attempts to modify ++ those resources will usually fail. + -+ yield ++* iOS apps have a limited concept of console input and output. ``stdout`` and ++ ``stderr`` *exist*, and content written to ``stdout`` and ``stderr`` will be ++ visible in logs when running in Xcode, but this content *won't* be recorded ++ in the system log. If a user who has installed your app provides their app ++ logs as a diagnostic aid, they will not include any detail written to ++ ``stdout`` or ``stderr``. + -+ if "GITHUB_ACTIONS" in os.environ: -+ print("::endgroup::") -+ else: -+ print() ++ iOS apps have no concept of ``stdin`` at all. While iOS apps can have a ++ keyboard, this is a software feature, not something that is attached to ++ ``stdin``. + ++ As a result, Python library that involve console manipulation (such as ++ :mod:`curses` and :mod:`readline`) are not available on iOS. +diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst +index 3fec86080c5..0a8e21510c4 100644 +--- a/Doc/library/multiprocessing.rst ++++ b/Doc/library/multiprocessing.rst +@@ -8,7 +8,7 @@ + + -------------- + +-.. include:: ../includes/wasm-notavail.rst ++.. include:: ../includes/wasm-ios-notavail.rst + + Introduction + ------------ +diff --git a/Doc/library/os.rst b/Doc/library/os.rst +index 6537ae298b9..0e904ef8615 100644 +--- a/Doc/library/os.rst ++++ b/Doc/library/os.rst +@@ -34,12 +34,13 @@ + + * On VxWorks, os.popen, os.fork, os.execv and os.spawn*p* are not supported. + +-* On WebAssembly platforms ``wasm32-emscripten`` and ``wasm32-wasi``, large +- parts of the :mod:`os` module are not available or behave differently. API +- related to processes (e.g. :func:`~os.fork`, :func:`~os.execve`), signals +- (e.g. :func:`~os.kill`, :func:`~os.wait`), and resources +- (e.g. :func:`~os.nice`) are not available. Others like :func:`~os.getuid` +- and :func:`~os.getpid` are emulated or stubs. ++* On WebAssembly platforms ``wasm32-emscripten`` and ``wasm32-wasi``, and on ++ iOS, large parts of the :mod:`os` module are not available or behave ++ differently. API related to processes (e.g. :func:`~os.fork`, ++ :func:`~os.execve`) and resources (e.g. :func:`~os.nice`) are not available. ++ Others like :func:`~os.getuid` and :func:`~os.getpid` are emulated or stubs. ++ WebAssembly platforms also lack support for signals (e.g. :func:`~os.kill`, ++ :func:`~os.wait`). + + + .. note:: +@@ -785,6 +786,11 @@ + :func:`socket.gethostname` or even + ``socket.gethostbyaddr(socket.gethostname())``. + ++ On macOS, iOS and Android, this returns the *kernel* name and version (i.e., ++ ``'Darwin'`` on macOS and iOS; ``'Linux'`` on Android). :func:`platform.uname()` ++ can be used to get the user-facing operating system name and version on iOS and ++ Android. + -+@contextmanager -+def cwd(subdir: Path): -+ """A context manager that sets the current working directory.""" -+ orig = os.getcwd() -+ os.chdir(subdir) -+ yield -+ os.chdir(orig) -+ -+ -+def configure_build_python(context: argparse.Namespace) -> None: -+ """The implementation of the "configure-build" command.""" -+ if context.clean: -+ clean(context, "build") -+ -+ with ( -+ group("Configuring build Python"), -+ cwd(subdir("build", create=True)), -+ ): -+ command = [relpath(PYTHON_DIR / "configure")] -+ if context.args: -+ command.extend(context.args) -+ run(command) -+ -+ -+def make_build_python(context: argparse.Namespace) -> None: -+ """The implementation of the "make-build" command.""" -+ with ( -+ group("Compiling build Python"), -+ cwd(subdir("build")), -+ ): -+ run(["make", "-j", str(os.cpu_count())]) -+ -+ -+def pythoninfo_build_python(context: argparse.Namespace) -> None: -+ """The implementation of the "pythoninfo-build" command.""" -+ with ( -+ group("Display build info of the build Python"), -+ cwd(subdir("build")), -+ ): -+ run(["make", "pythoninfo"]) -+ -+ -+def apple_target(host: str) -> str: -+ """Return the Apple platform identifier for a given host triple.""" -+ for _, platform_slices in HOSTS.items(): -+ for slice_name, slice_parts in platform_slices.items(): -+ for host_triple, multiarch in slice_parts.items(): -+ if host == host_triple: -+ return ".".join(multiarch.split("-")[::-1]) -+ -+ raise KeyError(host) -+ -+ -+def apple_multiarch(host: str) -> str: -+ """Return the multiarch descriptor for a given host triple.""" -+ for _, platform_slices in HOSTS.items(): -+ for slice_name, slice_parts in platform_slices.items(): -+ for host_triple, multiarch in slice_parts.items(): -+ if host == host_triple: -+ return multiarch -+ -+ raise KeyError(host) -+ -+ -+def unpack_deps( -+ platform: str, -+ host: str, -+ prefix_dir: Path, -+ cache_dir: Path, -+) -> None: -+ """Unpack binary dependencies into a provided directory. -+ -+ Downloads binaries if they aren't already present. Downloads will be stored -+ in provided cache directory. -+ -+ On non-macOS platforms, as a safety mechanism, any dynamic libraries will -+ be purged from the unpacked dependencies. -+ """ -+ # To create new builds of these dependencies, usually all that's necessary -+ # is to push a tag to the cpython-apple-source-deps repository, and GitHub -+ # Actions will do the rest. -+ # -+ # If you're a member of the Python core team, and you'd like to be able to -+ # push these tags yourself, please contact Malcolm Smith or Russell -+ # Keith-Magee. -+ 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.19-1", -+ "XZ-5.6.4-2", -+ "mpdecimal-4.0.0-2", -+ "zstd-1.5.7-1", -+ ]: -+ filename = f"{name_ver.lower()}-{apple_target(host)}.tar.gz" -+ archive_path = download( -+ f"{deps_url}/{name_ver}/{filename}", -+ target_dir=cache_dir, -+ ) -+ shutil.unpack_archive(archive_path, prefix_dir) -+ -+ # Dynamic libraries will be preferentially linked over static; -+ # 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. -+ -+ :return: The path to the downloaded archive. -+ """ -+ target_path = Path(target_dir).resolve() -+ target_path.mkdir(exist_ok=True, parents=True) -+ -+ out_path = target_path / basename(url) -+ if not Path(out_path).is_file(): -+ run([ -+ "curl", -+ "-Lf", -+ "--retry", -+ "5", -+ "--retry-all-errors", -+ "-o", -+ out_path, -+ url, -+ ]) -+ else: -+ print(f"Using cached version of {basename(url)}") -+ return out_path -+ + .. availability:: Unix. + + .. versionchanged:: 3.3 +@@ -3999,7 +4005,7 @@ + + .. audit-event:: os.exec path,args,env os.execl + +- .. availability:: Unix, Windows, not Emscripten, not WASI. ++ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. + + .. versionchanged:: 3.3 + Added support for specifying *path* as an open file descriptor +@@ -4202,7 +4208,7 @@ + for technical details of why we're surfacing this longstanding + platform compatibility problem to developers. + +- .. availability:: POSIX, not Emscripten, not WASI. ++ .. availability:: POSIX, not Emscripten, not WASI, not iOS. + + + .. function:: forkpty() +@@ -4229,7 +4235,7 @@ + threads, this now raises a :exc:`DeprecationWarning`. See the + longer explanation on :func:`os.fork`. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. function:: kill(pid, sig, /) +@@ -4252,7 +4258,7 @@ + + .. audit-event:: os.kill pid,sig os.kill + +- .. availability:: Unix, Windows, not Emscripten, not WASI. ++ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. + + .. versionchanged:: 3.2 + Added Windows support. +@@ -4268,7 +4274,7 @@ + + .. audit-event:: os.killpg pgid,sig os.killpg + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. function:: nice(increment, /) +@@ -4305,7 +4311,7 @@ + Lock program segments into memory. The value of *op* (defined in + ````) determines which segments are locked. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. function:: popen(cmd, mode='r', buffering=-1) +@@ -4337,7 +4343,7 @@ + documentation for more powerful ways to manage and communicate with + subprocesses. + +- .. availability:: not Emscripten, not WASI. ++ .. availability:: not Emscripten, not WASI, not iOS. + + .. note:: + The :ref:`Python UTF-8 Mode ` affects encodings used +@@ -4432,7 +4438,7 @@ + + .. versionadded:: 3.8 + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + .. function:: posix_spawnp(path, argv, env, *, file_actions=None, \ + setpgroup=None, resetids=False, setsid=False, setsigmask=(), \ +@@ -4448,7 +4454,7 @@ + + .. versionadded:: 3.8 + +- .. availability:: POSIX, not Emscripten, not WASI. ++ .. availability:: POSIX, not Emscripten, not WASI, not iOS. + + See :func:`posix_spawn` documentation. + +@@ -4481,7 +4487,7 @@ + + There is no way to unregister a function. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + .. versionadded:: 3.7 + +@@ -4550,7 +4556,7 @@ + + .. audit-event:: os.spawn mode,path,args,env os.spawnl + +- .. availability:: Unix, Windows, not Emscripten, not WASI. ++ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. + + :func:`spawnlp`, :func:`spawnlpe`, :func:`spawnvp` + and :func:`spawnvpe` are not available on Windows. :func:`spawnle` and +@@ -4674,7 +4680,7 @@ + + .. audit-event:: os.system command os.system + +- .. availability:: Unix, Windows, not Emscripten, not WASI. ++ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. + + + .. function:: times() +@@ -4718,7 +4724,7 @@ + :func:`waitstatus_to_exitcode` can be used to convert the exit status into an + exit code. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + .. seealso:: + +@@ -4752,7 +4758,10 @@ + Otherwise, if there are no matching children + that could be waited for, :exc:`ChildProcessError` is raised. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + -+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 ++ .. note:: ++ This function is not available on macOS. + + .. note:: + This function is not available on macOS. +@@ -4793,7 +4802,7 @@ + :func:`waitstatus_to_exitcode` can be used to convert the exit status into an + exit code. + +- .. availability:: Unix, Windows, not Emscripten, not WASI. ++ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. + + .. versionchanged:: 3.5 + If the system call is interrupted and the signal handler does not raise an +@@ -4813,7 +4822,7 @@ + :func:`waitstatus_to_exitcode` can be used to convert the exit status into an + exitcode. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. function:: wait4(pid, options) +@@ -4827,7 +4836,7 @@ + :func:`waitstatus_to_exitcode` can be used to convert the exit status into an + exitcode. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. data:: P_PID +@@ -4844,7 +4853,7 @@ + * :data:`!P_PIDFD` - wait for the child identified by the file descriptor + *id* (a process file descriptor created with :func:`pidfd_open`). + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + .. note:: :data:`!P_PIDFD` is only available on Linux >= 5.4. + +@@ -4859,7 +4868,7 @@ + :func:`waitid` causes child processes to be reported if they have been + continued from a job control stop since they were last reported. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. data:: WEXITED +@@ -4870,7 +4879,7 @@ + The other ``wait*`` functions always report children that have terminated, + so this option is not available for them. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + .. versionadded:: 3.3 + +@@ -4882,7 +4891,7 @@ + + This option is not available for the other ``wait*`` functions. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + .. versionadded:: 3.3 + +@@ -4895,7 +4904,7 @@ + + This option is not available for :func:`waitid`. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. data:: WNOHANG +@@ -4904,7 +4913,7 @@ + :func:`waitid` to return right away if no child process status is available + immediately. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. data:: WNOWAIT +@@ -4914,7 +4923,7 @@ + + This option is not available for the other ``wait*`` functions. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. data:: CLD_EXITED +@@ -4927,7 +4936,7 @@ + These are the possible values for :attr:`!si_code` in the result returned by + :func:`waitid`. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + .. versionadded:: 3.3 + +@@ -4962,7 +4971,7 @@ + :func:`WIFEXITED`, :func:`WEXITSTATUS`, :func:`WIFSIGNALED`, + :func:`WTERMSIG`, :func:`WIFSTOPPED`, :func:`WSTOPSIG` functions. + +- .. availability:: Unix, Windows, not Emscripten, not WASI. ++ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. + + .. versionadded:: 3.9 + +@@ -4978,7 +4987,7 @@ + + This function should be employed only if :func:`WIFSIGNALED` is true. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. function:: WIFCONTINUED(status) +@@ -4989,7 +4998,7 @@ + + See :data:`WCONTINUED` option. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. function:: WIFSTOPPED(status) +@@ -5001,14 +5010,14 @@ + done using :data:`WUNTRACED` option or when the process is being traced (see + :manpage:`ptrace(2)`). + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + .. function:: WIFSIGNALED(status) + + Return ``True`` if the process was terminated by a signal, otherwise return + ``False``. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. function:: WIFEXITED(status) +@@ -5017,7 +5026,7 @@ + by calling ``exit()`` or ``_exit()``, or by returning from ``main()``; + otherwise return ``False``. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. function:: WEXITSTATUS(status) +@@ -5026,7 +5035,7 @@ + + This function should be employed only if :func:`WIFEXITED` is true. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. function:: WSTOPSIG(status) +@@ -5035,7 +5044,7 @@ + + This function should be employed only if :func:`WIFSTOPPED` is true. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + .. function:: WTERMSIG(status) +@@ -5044,7 +5053,7 @@ + + This function should be employed only if :func:`WIFSIGNALED` is true. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not Emscripten, not WASI, not iOS. + + + Interface to the scheduler +diff --git a/Doc/library/platform.rst b/Doc/library/platform.rst +index 2f5bf53bc5c..0cc5e532711 100644 +--- a/Doc/library/platform.rst ++++ b/Doc/library/platform.rst +@@ -148,6 +148,9 @@ + Returns the system/OS name, such as ``'Linux'``, ``'Darwin'``, ``'Java'``, + ``'Windows'``. An empty string is returned if the value cannot be determined. + ++ On iOS and Android, this returns the user-facing OS name (i.e, ``'iOS``, ++ ``'iPadOS'`` or ``'Android'``). To obtain the kernel name (``'Darwin'`` or ++ ``'Linux'``), use :func:`os.uname()`. + + .. function:: system_alias(system, release, version) + +@@ -161,6 +164,8 @@ + Returns the system's release version, e.g. ``'#3 on degas'``. An empty string is + returned if the value cannot be determined. + ++ On iOS and Android, this is the user-facing OS version. To obtain the ++ Darwin or Linux kernel version, use :func:`os.uname()`. + + .. function:: uname() + +@@ -234,7 +239,6 @@ + macOS Platform + -------------- + +- + .. function:: mac_ver(release='', versioninfo=('','',''), machine='') + + Get macOS version information and return it as tuple ``(release, versioninfo, +@@ -244,6 +248,24 @@ + Entries which cannot be determined are set to ``''``. All tuple entries are + strings. + ++iOS Platform ++------------ + -+ if context.clean: -+ clean(context, host) ++.. function:: ios_ver(system='', release='', model='', is_simulator=False) + -+ host_dir = subdir(host, create=True) -+ prefix_dir = host_dir / "prefix" ++ Get iOS version information and return it as a ++ :func:`~collections.namedtuple` with the following attributes: + -+ with group(f"Downloading dependencies ({host})"): -+ if not prefix_dir.exists(): -+ prefix_dir.mkdir() -+ cache_dir = ( -+ Path(context.cache_dir).resolve() -+ if context.cache_dir -+ else CROSS_BUILD_DIR / "downloads" -+ ) -+ unpack_deps(context.platform, host, prefix_dir, cache_dir) -+ else: -+ print("Dependencies already installed") ++ * ``system`` is the OS name; either ``'iOS'`` or ``'iPadOS'``. ++ * ``release`` is the iOS version number as a string (e.g., ``'17.2'``). ++ * ``model`` is the device model identifier; this will be a string like ++ ``'iPhone13,2'`` for a physical device, or ``'iPhone'`` on a simulator. ++ * ``is_simulator`` is a boolean describing if the app is running on a ++ simulator or a physical device. + -+ 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_LDFLAGS=-L{prefix_dir}/lib -lmpdec", -+ ] ++ Entries which cannot be determined are set to the defaults given as ++ parameters. + -+ if context.args: -+ command.extend(context.args) -+ run(command, host=host) + + Unix Platforms + -------------- +diff --git a/Doc/library/pwd.rst b/Doc/library/pwd.rst +index 98ca174d9e3..d71d7212cfd 100644 +--- a/Doc/library/pwd.rst ++++ b/Doc/library/pwd.rst +@@ -10,7 +10,7 @@ + This module provides access to the Unix user account and password database. It + is available on all Unix versions. + +-.. availability:: Unix, not Emscripten, not WASI. ++.. availability:: Unix, not WASI, not iOS. + + Password database entries are reported as a tuple-like object, whose attributes + correspond to the members of the ``passwd`` structure (Attribute field below, +diff --git a/Doc/library/readline.rst b/Doc/library/readline.rst +index 5479ccf7f62..069dba24c29 100644 +--- a/Doc/library/readline.rst ++++ b/Doc/library/readline.rst +@@ -24,6 +24,8 @@ + allowable constructs of that file, and the capabilities of the + Readline library in general. + ++.. include:: ../includes/wasm-ios-notavail.rst + + .. note:: + + The underlying Readline library API may be implemented by +diff --git a/Doc/library/resource.rst b/Doc/library/resource.rst +index 02009d82104..0515d205bbc 100644 +--- a/Doc/library/resource.rst ++++ b/Doc/library/resource.rst +@@ -13,7 +13,7 @@ + This module provides basic mechanisms for measuring and controlling system + resources utilized by a program. + +-.. availability:: Unix, not Emscripten, not WASI. ++.. availability:: Unix, not WASI. + + Symbolic constants are used to specify particular system resources and to + request usage information about either the current process or its children. +diff --git a/Doc/library/signal.rst b/Doc/library/signal.rst +index 641a6c021c1..79c4948e99e 100644 +--- a/Doc/library/signal.rst ++++ b/Doc/library/signal.rst +@@ -26,9 +26,9 @@ + underlying implementation), with the exception of the handler for + :const:`SIGCHLD`, which follows the underlying implementation. + +-On WebAssembly platforms ``wasm32-emscripten`` and ``wasm32-wasi``, signals +-are emulated and therefore behave differently. Several functions and signals +-are not available on these platforms. ++On WebAssembly platforms, signals are emulated and therefore behave ++differently. Several functions and signals are not available on these ++platforms. + + Execution of Python signal handlers + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst +index d29e3c20e10..4a627e3f3f9 100644 +--- a/Doc/library/socket.rst ++++ b/Doc/library/socket.rst +@@ -1244,7 +1244,7 @@ + buffer. Raises :exc:`OverflowError` if *length* is outside the + permissible range of values. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not WASI. + + Most Unix platforms. + +@@ -1267,7 +1267,7 @@ + amount of ancillary data that can be received, since additional + data may be able to fit into the padding area. + +- .. availability:: Unix, not Emscripten, not WASI. ++ .. availability:: Unix, not WASI. + + most Unix platforms. + +@@ -1307,7 +1307,7 @@ + (index int, name string) tuples. + :exc:`OSError` if the system call fails. + +- .. availability:: Unix, Windows, not Emscripten, not WASI. ++ .. availability:: Unix, Windows, not WASI. + + .. versionadded:: 3.3 + +@@ -1334,7 +1334,7 @@ + interface name. + :exc:`OSError` if no interface with the given name exists. + +- .. availability:: Unix, Windows, not Emscripten, not WASI. ++ .. availability:: Unix, Windows, not WASI. + + .. versionadded:: 3.3 + +@@ -1351,7 +1351,7 @@ + interface index number. + :exc:`OSError` if no interface with the given index exists. + +- .. availability:: Unix, Windows, not Emscripten, not WASI. ++ .. availability:: Unix, Windows, not WASI. + + .. versionadded:: 3.3 + +@@ -1368,7 +1368,7 @@ + The *fds* parameter is a sequence of file descriptors. + Consult :meth:`~socket.sendmsg` for the documentation of these parameters. + +- .. availability:: Unix, Windows, not Emscripten, not WASI. ++ .. availability:: Unix, Windows, not WASI. + + Unix platforms supporting :meth:`~socket.sendmsg` + and :const:`SCM_RIGHTS` mechanism. +@@ -1382,7 +1382,7 @@ + Return ``(msg, list(fds), flags, addr)``. + Consult :meth:`~socket.recvmsg` for the documentation of these parameters. + +- .. availability:: Unix, Windows, not Emscripten, not WASI. ++ .. availability:: Unix, Windows, not WASI. + + Unix platforms supporting :meth:`~socket.sendmsg` + and :const:`SCM_RIGHTS` mechanism. +diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst +index 552af2ac9d3..8d8782e6a76 100644 +--- a/Doc/library/subprocess.rst ++++ b/Doc/library/subprocess.rst +@@ -25,7 +25,7 @@ + + :pep:`324` -- PEP proposing the subprocess module + +-.. include:: ../includes/wasm-notavail.rst ++.. include:: ../includes/wasm-ios-notavail.rst + + Using the :mod:`subprocess` Module + ---------------------------------- +diff --git a/Doc/library/syslog.rst b/Doc/library/syslog.rst +index 79b808ab63c..332b58413d3 100644 +--- a/Doc/library/syslog.rst ++++ b/Doc/library/syslog.rst +@@ -11,7 +11,7 @@ + Refer to the Unix manual pages for a detailed description of the ``syslog`` + facility. + +-.. availability:: Unix, not Emscripten, not WASI. ++.. availability:: Unix, not WASI, not iOS. + + This module wraps the system ``syslog`` family of routines. A pure Python + library that can speak to a syslog server is available in the +diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst +index b32b4af1aa8..69daa381013 100644 +--- a/Doc/library/urllib.parse.rst ++++ b/Doc/library/urllib.parse.rst +@@ -22,11 +22,19 @@ + + The module has been designed to match the internet RFC on Relative Uniform + Resource Locators. It supports the following URL schemes: ``file``, ``ftp``, +-``gopher``, ``hdl``, ``http``, ``https``, ``imap``, ``mailto``, ``mms``, ++``gopher``, ``hdl``, ``http``, ``https``, ``imap``, ``itms-services``, ``mailto``, ``mms``, + ``news``, ``nntp``, ``prospero``, ``rsync``, ``rtsp``, ``rtsps``, ``rtspu``, + ``sftp``, ``shttp``, ``sip``, ``sips``, ``snews``, ``svn``, ``svn+ssh``, + ``telnet``, ``wais``, ``ws``, ``wss``. + ++.. impl-detail:: + -+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 ++ The inclusion of the ``itms-services`` URL scheme can prevent an app from ++ passing Apple's App Store review process for the macOS and iOS App Stores. ++ Handling for the ``itms-services`` scheme is always removed on iOS; on ++ macOS, it *may* be removed if CPython has been built with the ++ :option:`--with-app-store-compliance` option. + -+ with ( -+ group(f"Compiling host Python ({host})"), -+ cwd(subdir(host)), -+ ): -+ run(["make"], host=host) -+ run(["make", "install"], host=host) + The :mod:`urllib.parse` module defines functions that fall into two broad + categories: URL parsing and URL quoting. These are covered in detail in + the following sections. +diff --git a/Doc/library/venv.rst b/Doc/library/venv.rst +index 9eb0e9d191c..cc403f1bb7b 100644 +--- a/Doc/library/venv.rst ++++ b/Doc/library/venv.rst +@@ -56,7 +56,7 @@ + `Python Packaging User Guide: Creating and using virtual environments + `__ + +-.. include:: ../includes/wasm-notavail.rst ++.. include:: ../includes/wasm-ios-notavail.rst + + Creating virtual environments + ----------------------------- +diff --git a/Doc/library/webbrowser.rst b/Doc/library/webbrowser.rst +index 9c3ff5efa2e..b99b28b2053 100644 +--- a/Doc/library/webbrowser.rst ++++ b/Doc/library/webbrowser.rst +@@ -33,6 +33,13 @@ + browsers are not available on Unix, the controlling process will launch a new + browser and wait. + ++On iOS, the :envvar:`BROWSER` environment variable, as well as any arguments ++controlling autoraise, browser preference, and new tab/window creation will be ++ignored. Web pages will *always* be opened in the user's preferred browser, in ++a new tab, with the browser being brought to the foreground. The use of the ++:mod:`webbrowser` module on iOS requires the :mod:`ctypes` module. If ++:mod:`ctypes` isn't available, calls to :func:`.open` will fail. + + .. program:: webbrowser + + The script :program:`webbrowser` can be used as a command-line interface for the +@@ -166,6 +173,8 @@ + +------------------------+-----------------------------------------+-------+ + | ``'chromium-browser'`` | :class:`Chromium('chromium-browser')` | | + +------------------------+-----------------------------------------+-------+ ++| ``'iosbrowser'`` | ``IOSBrowser`` | \(4) | +++------------------------+-----------------------------------------+-------+ + + Notes: + +@@ -180,7 +189,11 @@ + Only on Windows platforms. + + (3) +- Only on macOS platform. ++ Only on macOS. + -+def framework_path(host_triple: str, multiarch: str) -> Path: -+ """The path to a built single-architecture framework product. ++(4) ++ Only on iOS. + -+ :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/{platform_for_host(host_triple)}" -+ / f"Frameworks/{multiarch}" -+ ) + + .. versionadded:: 3.3 + Support for Chrome/Chromium has been added. +@@ -193,6 +206,9 @@ + .. deprecated-removed:: 3.11 3.13 + :class:`MacOSX` is deprecated, use :class:`MacOSXOSAScript` instead. + ++.. versionchanged:: 3.13 ++ Support for iOS has been added. + + Here are some simple examples:: + + url = 'https://docs.python.org/' +diff --git a/Doc/tools/extensions/availability.py b/Doc/tools/extensions/availability.py +index 225b3438b94..3164d735a3a 100644 +--- a/Doc/tools/extensions/availability.py ++++ b/Doc/tools/extensions/availability.py +@@ -24,6 +24,7 @@ + "DragonFlyBSD", + "Emscripten", + "FreeBSD", ++ "iOS", + "Linux", + "macOS", + "NetBSD", +diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst +index 6df75ac9e0b..8883a7af9c0 100644 +--- a/Doc/using/configure.rst ++++ b/Doc/using/configure.rst +@@ -638,7 +638,7 @@ + macOS Options + ------------- + +-See ``Mac/README.rst``. ++See :source:`Mac/README.rst`. + + .. option:: --enable-universalsdk + .. option:: --enable-universalsdk=SDKDIR +@@ -679,6 +679,31 @@ + Specify the name for the python framework on macOS only valid when + :option:`--enable-framework` is set (default: ``Python``). + ++.. option:: --with-app-store-compliance ++.. option:: --with-app-store-compliance=PATCH-FILE + -+def package_version(prefix_path: Path) -> str: -+ """Extract the Python version being built from patchlevel.h.""" -+ for path in prefix_path.glob("**/patchlevel.h"): -+ text = path.read_text(encoding="utf-8") -+ if match := re.search( -+ r'\n\s*#define\s+PY_VERSION\s+"(.+)"\s*\n', text -+ ): -+ version = match[1] -+ # If not building against a tagged commit, add a timestamp to the -+ # version. Follow the PyPA version number rules, as this will make -+ # it easier to process with other tools. The version will have a -+ # `+` suffix once any official release has been made; a freshly -+ # forked main branch will have a version of 3.X.0a0. -+ if version.endswith("a0"): -+ version += "+" -+ if version.endswith("+"): -+ version += datetime.now(timezone.utc).strftime("%Y%m%d.%H%M%S") ++ The Python standard library contains strings that are known to trigger ++ automated inspection tool errors when submitted for distribution by ++ the macOS and iOS App Stores. If enabled, this option will apply the list of ++ patches that are known to correct app store compliance. A custom patch ++ file can also be specified. This option is disabled by default. + -+ return version ++ .. versionadded:: 3.13 + -+ sys.exit("Unable to determine Python version being packaged.") ++iOS Options ++----------- + ++See :source:`iOS/README.rst`. + -+def lib_platform_files(dirname, names): -+ """A file filter that ignores platform-specific files in lib.""" -+ 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 = { -+ name -+ for name in names -+ if ( -+ name.startswith("_sysconfigdata_") -+ or name.startswith("_sysconfig_vars_") -+ or name == "build-details.json" -+ ) -+ } -+ elif path.parts[-1] == "lib": -+ ignored_names = { -+ name -+ for name in names -+ if name.startswith("libpython") and name.endswith(".dylib") -+ } -+ else: -+ ignored_names = set() ++.. option:: --enable-framework=INSTALLDIR + -+ return ignored_names ++ Create a Python.framework. Unlike macOS, the *INSTALLDIR* argument ++ specifying the installation path is mandatory. + ++.. option:: --with-framework-name=FRAMEWORK + -+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() ++ Specify the name for the framework (default: ``Python``). + + + Cross Compiling Options + ----------------------- +diff --git a/Doc/using/index.rst b/Doc/using/index.rst +index e1a3111f36a..f55a12f1ab8 100644 +--- a/Doc/using/index.rst ++++ b/Doc/using/index.rst +@@ -18,4 +18,5 @@ + configure.rst + windows.rst + mac.rst ++ ios.rst + editors.rst +diff --git a/Doc/using/ios.rst b/Doc/using/ios.rst +new file mode 100644 +index 00000000000..0f29de64e41 +--- /dev/null ++++ b/Doc/using/ios.rst +@@ -0,0 +1,359 @@ ++.. _using-ios: + -+def create_xcframework(platform: str) -> str: -+ """Build an XCframework from the component parts for the platform. ++=================== ++Using Python on iOS ++=================== + -+ :return: The version number of the Python version 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 ++:Authors: ++ Russell Keith-Magee (2024-03) + -+ frameworks = [] -+ # Merge Frameworks for each component SDK. If there's only one architecture -+ # for the SDK, we can use the compiled Python.framework as-is. However, if -+ # there's more than architecture, we need to merge the individual built -+ # frameworks into a merged "fat" framework. -+ for slice_name, slice_parts in HOSTS[platform].items(): -+ # Some parts are the same across all slices, so we use can any of the -+ # host frameworks as the source for the merged version. Use the first -+ # one on the list, as it's as representative as any other. -+ first_host_triple, first_multiarch = next(iter(slice_parts.items())) -+ first_framework = ( -+ framework_path(first_host_triple, first_multiarch) -+ / "Python.framework" -+ ) ++Python on iOS is unlike Python on desktop platforms. On a desktop platform, ++Python is generally installed as a system resource that can be used by any user ++of that computer. Users then interact with Python by running a :program:`python` ++executable and entering commands at an interactive prompt, or by running a ++Python script. + -+ 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) ++On iOS, there is no concept of installing as a system resource. The only unit ++of software distribution is an "app". There is also no console where you could ++run a :program:`python` executable, or interact with a Python REPL. + -+ # Copy the Info.plist -+ shutil.copy( -+ first_framework / "Info.plist", -+ slice_framework / "Info.plist", -+ ) ++As a result, the only way you can use Python on iOS is in embedded mode - that ++is, by writing a native iOS application, and embedding a Python interpreter ++using ``libPython``, and invoking Python code using the :ref:`Python embedding ++API `. The full Python interpreter, the standard library, and all ++your Python code is then packaged as a standalone bundle that can be ++distributed via the iOS App Store. + -+ # Copy the headers -+ shutil.copytree( -+ first_framework / "Headers", -+ slice_framework / "Headers", -+ ) ++If you're looking to experiment for the first time with writing an iOS app in ++Python, projects such as `BeeWare `__ and `Kivy ++`__ will provide a much more approachable user experience. ++These projects manage the complexities associated with getting an iOS project ++running, so you only need to deal with the Python code itself. + -+ # Create the "fat" library binary for the slice -+ run( -+ ["lipo", "-create", "-output", slice_framework / "Python"] -+ + [ -+ ( -+ framework_path(host_triple, multiarch) -+ / "Python.framework/Python" -+ ) -+ for host_triple, multiarch in slice_parts.items() -+ ] -+ ) ++Python at runtime on iOS ++======================== + -+ # Add this merged slice to the list to be added to the XCframework -+ frameworks.append(slice_framework) ++iOS version compatibility ++------------------------- + -+ print() -+ print("Build XCframework...") -+ cmd = [ -+ "xcodebuild", -+ "-create-xcframework", -+ "-output", -+ package_path / "Python.xcframework", -+ ] -+ for framework in frameworks: -+ cmd.extend(["-framework", framework]) ++The minimum supported iOS version is specified at compile time, using the ++:option:`--host` option to ``configure``. By default, when compiled for iOS, ++Python will be compiled with a minimum supported iOS version of 13.0. To use a ++different miniumum iOS version, provide the version number as part of the ++:option:`!--host` argument - for example, ++``--host=arm64-apple-ios15.4-simulator`` would compile an ARM64 simulator build ++with a deployment target of 15.4. + -+ run(cmd) ++Platform identification ++----------------------- + -+ # Extract the package version from the merged framework -+ version = package_version(package_path / "Python.xcframework") -+ version_tag = ".".join(version.split(".")[:2]) ++When executing on iOS, ``sys.platform`` will report as ``ios``. This value will ++be returned on an iPhone or iPad, regardless of whether the app is running on ++the simulator or a physical device. + -+ # On non-macOS platforms, each framework in XCframework only contains the -+ # headers, libPython, plus an Info.plist. Other resources like the standard -+ # library and binary shims aren't allowed to live in framework; they need -+ # to be copied in separately. -+ print() -+ print("Copy additional resources...") -+ has_common_stdlib = False -+ for slice_name, slice_parts in HOSTS[platform].items(): -+ # Some parts are the same across all slices, so we can any of the -+ # host frameworks as the source for the merged version. -+ first_host_triple, first_multiarch = next(iter(slice_parts.items())) -+ first_path = framework_path(first_host_triple, first_multiarch) -+ first_framework = first_path / "Python.framework" ++Information about the specific runtime environment, including the iOS version, ++device model, and whether the device is a simulator, can be obtained using ++:func:`platform.ios_ver`. :func:`platform.system` will report ``iOS`` or ++``iPadOS``, depending on the device. + -+ slice_path = package_path / f"Python.xcframework/{slice_name}" -+ slice_framework = slice_path / "Python.framework" ++:func:`os.uname` reports kernel-level details; it will report a name of ++``Darwin``. + -+ # Copy the binary helpers -+ print(f" - {slice_name} binaries") -+ shutil.copytree(first_path / "bin", slice_path / "bin") ++Standard library availability ++----------------------------- + -+ # Copy the include path (a symlink to the framework headers) -+ print(f" - {slice_name} include files") -+ shutil.copytree( -+ first_path / "include", -+ slice_path / "include", -+ symlinks=True, -+ ) ++The Python standard library has some notable omissions and restrictions on ++iOS. See the :ref:`API availability guide for iOS ` for ++details. + -+ # Copy in the cross-architecture pyconfig.h -+ shutil.copy( -+ PYTHON_DIR / f"Apple/{platform}/Resources/pyconfig.h", -+ slice_framework / "Headers/pyconfig.h", -+ ) ++Binary extension modules ++------------------------ + -+ print(f" - {slice_name} shared library") -+ # Create a simlink for the fat library -+ shared_lib = slice_path / f"lib/libpython{version_tag}.dylib" -+ shared_lib.parent.mkdir() -+ shared_lib.symlink_to("../Python.framework/Python") ++One notable difference about iOS as a platform is that App Store distribution ++imposes hard requirements on the packaging of an application. One of these ++requirements governs how binary extension modules are distributed. + -+ print(f" - {slice_name} architecture-specific files") -+ for host_triple, multiarch in slice_parts.items(): -+ print(f" - {multiarch} standard library") -+ arch, _ = multiarch.split("-", 1) ++The iOS App Store requires that *all* binary modules in an iOS app must be ++dynamic libraries, contained in a framework with appropriate metadata, stored ++in the ``Frameworks`` folder of the packaged app. There can be only a single ++binary per framework, and there can be no executable binary material outside ++the ``Frameworks`` folder. + -+ 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, -+ symlinks=True, -+ ) -+ has_common_stdlib = True ++This conflicts with the usual Python approach for distributing binaries, which ++allows a binary extension module to be loaded from any location on ++``sys.path``. To ensure compliance with App Store policies, an iOS project must ++post-process any Python packages, converting ``.so`` binary modules into ++individual standalone frameworks with appropriate metadata and signing. For ++details on how to perform this post-processing, see the guide for :ref:`adding ++Python to your project `. + -+ shutil.copytree( -+ framework_path(host_triple, multiarch) / "lib", -+ slice_path / f"lib-{arch}", -+ ignore=lib_non_platform_files, -+ symlinks=True, -+ ) ++To help Python discover binaries in their new location, the original ``.so`` ++file on ``sys.path`` is replaced with a ``.fwork`` file. This file is a text ++file containing the location of the framework binary, relative to the app ++bundle. To allow the framework to resolve back to the original location, the ++framework must contain a ``.origin`` file that contains the location of the ++``.fwork`` file, relative to the app bundle. + -+ # Copy the host's pyconfig.h to an architecture-specific name. -+ arch = multiarch.split("-")[0] -+ host_path = ( -+ CROSS_BUILD_DIR -+ / host_triple -+ / f"Apple/{platform}/Frameworks" -+ / multiarch -+ ) -+ host_framework = host_path / "Python.framework" -+ shutil.copy( -+ host_framework / "Headers/pyconfig.h", -+ slice_framework / f"Headers/pyconfig-{arch}.h", -+ ) ++For example, consider the case of an import ``from foo.bar import _whiz``, ++where ``_whiz`` is implemented with the binary module ++``sources/foo/bar/_whiz.abi3.so``, with ``sources`` being the location ++registered on ``sys.path``, relative to the application bundle. This module ++*must* be distributed as ``Frameworks/foo.bar._whiz.framework/foo.bar._whiz`` ++(creating the framework name from the full import path of the module), with an ++``Info.plist`` file in the ``.framework`` directory identifying the binary as a ++framework. The ``foo.bar._whiz`` module would be represented in the original ++location with a ``sources/foo/bar/_whiz.abi3.fwork`` marker file, containing ++the path ``Frameworks/foo.bar._whiz/foo.bar._whiz``. The framework would also ++contain ``Frameworks/foo.bar._whiz.framework/foo.bar._whiz.origin``, containing ++the path to the ``.fwork`` file. + -+ # Apple identifies certain libraries as "security risks"; if you -+ # statically link those libraries into a Framework, you become -+ # responsible for providing a privacy manifest for that framework. -+ xcprivacy_file = { -+ "OpenSSL": subdir(host_triple) -+ / "prefix/share/OpenSSL.xcprivacy" -+ } -+ print(f" - {multiarch} xcprivacy files") -+ for module, lib in [ -+ ("_hashlib", "OpenSSL"), -+ ("_ssl", "OpenSSL"), -+ ]: -+ shutil.copy( -+ xcprivacy_file[lib], -+ slice_path -+ / f"lib-{arch}/python{version_tag}" -+ / f"lib-dynload/{module}.xcprivacy", -+ ) ++When running on iOS, the Python interpreter will install an ++:class:`~importlib.machinery.AppleFrameworkLoader` that is able to read and ++import ``.fwork`` files. Once imported, the ``__file__`` attribute of the ++binary module will report as the location of the ``.fwork`` file. However, the ++:class:`~importlib.machinery.ModuleSpec` for the loaded module will report the ++``origin`` as the location of the binary in the framework folder. + -+ print(" - build tools") -+ shutil.copytree( -+ PYTHON_DIR / "Apple/testbed/Python.xcframework/build", -+ package_path / "Python.xcframework/build", -+ ) ++Compiler stub binaries ++---------------------- + -+ return version ++Xcode doesn't expose explicit compilers for iOS; instead, it uses an ``xcrun`` ++script that resolves to a full compiler path (e.g., ``xcrun --sdk iphoneos ++clang`` to get the ``clang`` for an iPhone device). However, using this script ++poses two problems: + ++* The output of ``xcrun`` includes paths that are machine specific, resulting ++ in a sysconfig module that cannot be shared between users; and + -+def package(context: argparse.Namespace) -> None: -+ """The implementation of the "package" command.""" -+ if context.clean: -+ clean(context, "package") ++* It results in ``CC``/``CPP``/``LD``/``AR`` definitions that include spaces. ++ There is a lot of C ecosystem tooling that assumes that you can split a ++ command line at the first space to get the path to the compiler executable; ++ this isn't the case when using ``xcrun``. + -+ with group("Building package"): -+ # Create an XCframework -+ version = create_xcframework(context.platform) ++To avoid these problems, Python provided stubs for these tools. These stubs are ++shell script wrappers around the underingly ``xcrun`` tools, distributed in a ++``bin`` folder distributed alongside the compiled iOS framework. These scripts ++are relocatable, and will always resolve to the appropriate local system paths. ++By including these scripts in the bin folder that accompanies a framework, the ++contents of the ``sysconfig`` module becomes useful for end-users to compile ++their own modules. When compiling third-party Python modules for iOS, you ++should ensure these stub binaries are on your path. + -+ # watchOS doesn't have a testbed (yet!) -+ if context.platform != "watchOS": -+ # 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", -+ ]) ++Installing Python on iOS ++======================== + -+ # Build the final archive -+ archive_name = ( -+ CROSS_BUILD_DIR -+ / "dist" -+ / f"python-{version}-{context.platform}-XCframework" -+ ) ++Tools for building iOS apps ++--------------------------- + -+ print() -+ print("Create package archive...") -+ shutil.make_archive( -+ str(CROSS_BUILD_DIR / archive_name), -+ format="gztar", -+ root_dir=CROSS_BUILD_DIR / context.platform, -+ base_dir=".", -+ ) -+ print() -+ print(f"{archive_name.relative_to(PYTHON_DIR)}.tar.gz created.") ++Building for iOS requires the use of Apple's Xcode tooling. It is strongly ++recommended that you use the most recent stable release of Xcode. This will ++require the use of the most (or second-most) recently released macOS version, ++as Apple does not maintain Xcode for older macOS versions. The Xcode Command ++Line Tools are not sufficient for iOS development; you need a *full* Xcode ++install. + ++If you want to run your code on the iOS simulator, you'll also need to install ++an iOS Simulator Platform. You should be prompted to select an iOS Simulator ++Platform when you first run Xcode. Alternatively, you can add an iOS Simulator ++Platform by selecting from the Platforms tab of the Xcode Settings panel. + -+def build(context: argparse.Namespace, host: str | None = None) -> None: -+ """The implementation of the "build" command.""" -+ if host is None: -+ host = context.host ++.. _adding-ios: + -+ if context.clean: -+ clean(context, host) ++Adding Python to an iOS project ++------------------------------- + -+ if host in {"all", "build"}: -+ for step in [ -+ configure_build_python, -+ make_build_python, -+ pythoninfo_build_python, -+ ]: -+ step(context) ++Python can be added to any iOS project, using either Swift or Objective C. The ++following examples will use Objective C; if you are using Swift, you may find a ++library like `PythonKit `__ to be ++helpful. + -+ if host == "build": -+ hosts = [] -+ elif host in {"all", "hosts"}: -+ hosts = all_host_triples(context.platform) -+ else: -+ hosts = [host] ++To add Python to an iOS Xcode project: + -+ for step_host in hosts: -+ for step in [ -+ configure_host_python, -+ make_host_python, -+ ]: -+ step(context, host=step_host) ++1. Build or obtain a Python ``XCFramework``. See the instructions in ++ :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``. + -+ if host == "all": -+ package(context) ++2. Drag the ``XCframework`` into your iOS project. In the following ++ instructions, we'll assume you've dropped the ``XCframework`` into the root ++ of your project; however, you can use any other location that you want by ++ adjusting paths as needed. + ++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. + -+def test(context: argparse.Namespace, host: str | None = None) -> None: -+ """The implementation of the "test" command.""" -+ if host is None: -+ host = context.host ++4. Select the app target by selecting the root node of your Xcode project, then ++ the target name in the sidebar that appears. + -+ if context.clean: -+ clean(context, "test") ++5. In the "General" settings, under "Frameworks, Libraries and Embedded ++ Content", add ``Python.xcframework``, with "Embed & Sign" selected. + -+ 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] ++6. In the "Build Settings" tab, modify the following: + -+ 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)}" -+ ) ++ - Build Options + -+ run([ -+ sys.executable, -+ "Apple/testbed", -+ "clone", -+ "--platform", -+ context.platform, -+ "--framework", -+ framework_path, -+ testbed_dir, -+ ]) ++ * User Script Sandboxing: No ++ * Enable Testability: Yes + -+ run( -+ [ -+ sys.executable, -+ testbed_dir, -+ "run", -+ "--verbose", -+ ] -+ + ( -+ ["--simulator", str(context.simulator)] -+ if context.simulator -+ else [] -+ ) -+ + [ -+ "--", -+ "test", -+ # f"--{context.ci_mode}-ci", -+ "-uall", -+ "--rerun", -+ "--single-process", -+ "-W", -+ # 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", -+ ] -+ ) ++ - Search Paths + ++ * Framework Search Paths: ``$(PROJECT_DIR)`` ++ * Header Search Paths: ``"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers"`` + -+def apple_sim_host(platform_name: str) -> str: -+ """Determine the native simulator target for this platform.""" -+ for _, slice_parts in HOSTS[platform_name].items(): -+ for host_triple in slice_parts: -+ parts = host_triple.split("-") -+ if parts[0] == platform.machine() and parts[-1] == "simulator": -+ return host_triple ++ - Apple Clang - Warnings - All languages + -+ raise KeyError(platform_name) ++ * Quoted Include In Framework Header: No + ++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: + -+def ci(context: argparse.Namespace) -> None: -+ """The implementation of the "ci" command. ++ .. code-block:: bash + -+ In "Fast" mode, this compiles the build python, and the simulator for the -+ build machine's architecture; and runs the test suite with `--fast-ci` -+ configuration. ++ set -e ++ source $PROJECT_DIR/Python.xcframework/build/build_utils.sh ++ install_python Python.xcframework app + -+ In "Slow" mode, it compiles the build python, plus all candidate -+ architectures (both device and simulator); then runs the test suite with -+ `--slow-ci` configuration. -+ """ -+ clean(context, "all") -+ if context.ci_mode == "slow": -+ # In slow mode, build and test the full XCframework -+ build(context, host="all") -+ test(context, host="all") -+ else: -+ # In fast mode, just build the simulator platform. -+ sim_host = apple_sim_host(context.platform) -+ build(context, host="build") -+ build(context, host=sim_host) -+ test(context, host=sim_host) ++ If you have placed your XCframework somewhere other than the root of your ++ project, modify the path to the first argument. + ++8. Add Objective C code to initialize and use a Python interpreter in embedded ++ mode. You should ensure that: + -+def parse_args() -> argparse.Namespace: -+ parser = argparse.ArgumentParser( -+ description=( -+ "A tool for managing the build, package and test process of " -+ "CPython on Apple platforms." -+ ), -+ ) -+ parser.suggest_on_error = True -+ subcommands = parser.add_subparsers(dest="subcommand", required=True) ++ * UTF-8 mode (:c:member:`PyPreConfig.utf8_mode`) is *enabled*; ++ * Buffered stdio (:c:member:`PyConfig.buffered_stdio`) is *disabled*; ++ * Writing bytecode (:c:member:`PyConfig.write_bytecode`) is *disabled*; ++ * Signal handlers (:c:member:`PyConfig.install_signal_handlers`) are *enabled*; ++ * :envvar:`PYTHONHOME` for the interpreter is configured to point at the ++ ``python`` subfolder of your app's bundle; and ++ * The :envvar:`PYTHONPATH` for the interpreter includes: + -+ clean = subcommands.add_parser( -+ "clean", -+ help="Delete all build directories", -+ ) ++ - the ``python/lib/python3.X`` subfolder of your app's bundle, ++ - the ``python/lib/python3.X/lib-dynload`` subfolder of your app's bundle, and ++ - the ``app`` subfolder of your app's bundle + -+ configure_build = subcommands.add_parser( -+ "configure-build", help="Run `configure` for the build Python" -+ ) -+ make_build = subcommands.add_parser( -+ "make-build", help="Run `make` for the build Python" -+ ) -+ pythoninfo_build = subcommands.add_parser( -+ "pythoninfo-build", help="Display build info of 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", -+ ) ++ Your app's bundle location can be determined using ``[[NSBundle mainBundle] ++ resourcePath]``. + -+ # 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", -+ ) ++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: + -+ # 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'" -+ ), -+ ) ++* You need to ensure that any folders containing third-party binaries are ++ 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 targetting (i.e., delete any device binaries ++ if you're building app app targeting the simulator). + -+ # --cross-build-dir argument -+ for cmd in [ -+ clean, -+ configure_build, -+ make_build, -+ pythoninfo_build, -+ configure_host, -+ make_host, -+ build, -+ package, -+ test, -+ ci, -+ ]: -+ cmd.add_argument( -+ "--cross-build-dir", -+ action="store", -+ default=os.environ.get("CROSS_BUILD_DIR"), -+ dest="cross_build_dir", -+ type=Path, -+ help=( -+ "Path to the cross-build directory " -+ f"(default: {CROSS_BUILD_DIR}). Can also be set " -+ "with the CROSS_BUILD_DIR environment variable." -+ ), -+ ) ++* 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. + -+ # --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", -+ ) ++* If any of the folders that contain third-party packages will contain ``.pth`` ++ files, you should add that folder as a *site directory* (using ++ :meth:`site.addsitedir`), rather than adding to :envvar:`PYTHONPATH` or ++ :attr:`sys.path` directly. + -+ # --cache-dir option -+ for cmd in [configure_host, build, ci]: -+ cmd.add_argument( -+ "--cache-dir", -+ default=os.environ.get("CACHE_DIR"), -+ help="The directory to store cached downloads.", -+ ) ++Testing a Python package ++------------------------ + -+ # --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." -+ ), -+ ) -+ group = cmd.add_mutually_exclusive_group() -+ group.add_argument( -+ "--fast-ci", -+ action="store_const", -+ dest="ci_mode", -+ const="fast", -+ help="Add test arguments for GitHub Actions", -+ ) -+ group.add_argument( -+ "--slow-ci", -+ action="store_const", -+ dest="ci_mode", -+ const="slow", -+ help="Add test arguments for buildbots", -+ ) ++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. + -+ for subcommand in [configure_build, configure_host, build, ci]: -+ subcommand.add_argument( -+ "args", nargs="*", help="Extra arguments to pass to `configure`" -+ ) ++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: + -+ return parser.parse_args() ++.. code-block:: bash + ++ $ python cross-build/iOS/testbed clone --app --app app-testbed + -+def print_called_process_error(e: subprocess.CalledProcessError) -> None: -+ for stream_name in ["stdout", "stderr"]: -+ content = getattr(e, stream_name) -+ stream = getattr(sys, stream_name) -+ if content: -+ stream.write(content) -+ if not content.endswith("\n"): -+ stream.write("\n") ++Or, if you've sourced your own XCframework, by running: + -+ # shlex uses single quotes, so we surround the command with double quotes. -+ print( -+ f'Command "{join_command(e.cmd)}" returned exit status {e.returncode}' -+ ) ++.. code-block:: bash + ++ $ python Apple/testbed clone --platform iOS --framework --app --app app-testbed + -+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) ++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). + -+ signal.signal(signal.SIGTERM, signal_handler) ++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 ++could run: + -+ # Process command line arguments -+ context = parse_args() ++.. code-block:: bash + -+ # Set the CROSS_BUILD_DIR if an argument was provided -+ if context.cross_build_dir: -+ global CROSS_BUILD_DIR -+ CROSS_BUILD_DIR = context.cross_build_dir.resolve() ++ $ python app-testbed run -- module1.tests + -+ dispatch: dict[str, Callable] = { -+ "clean": clean, -+ "configure-build": configure_build_python, -+ "make-build": make_build_python, -+ "pythoninfo-build": pythoninfo_build_python, -+ "configure-host": configure_host_python, -+ "make-host": make_host_python, -+ "package": package, -+ "build": build, -+ "test": test, -+ "ci": ci, -+ } ++This is the equivalent of running ``python -m module1.tests`` on a desktop ++Python build. Any arguments after the ``--`` will be passed to the testbed as ++if they were arguments to ``python -m`` on a desktop machine. + -+ 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) ++You can also open the testbed project in Xcode by running: + ++.. code-block:: bash + -+if __name__ == "__main__": -+ # Under the buildbot, stdout is not a TTY, but we must still flush after -+ # every line to make sure our output appears in the correct order relative -+ # to the output of our subprocesses. -+ for stream in [sys.stdout, sys.stderr]: -+ stream.reconfigure(line_buffering=True) ++ $ open app-testbed/iOSTestbed.xcodeproj + -+ main() -diff --git a/Apple/iOS/README.md b/Apple/iOS/README.md -new file mode 100644 -index 00000000000..7ee257b5d64 ---- /dev/null -+++ b/Apple/iOS/README.md -@@ -0,0 +1,339 @@ -+# Python on iOS README ++This will allow you to use the full Xcode suite of tools for debugging. + -+**iOS support is [tier 3](https://peps.python.org/pep-0011/#tier-3).** ++The arguments used to run the test suite are defined as part of the test plan. ++To modify the test plan, select the test plan node of the project tree (it ++should be the first child of the root node), and select the "Configurations" ++tab. Modify the "Arguments Passed On Launch" value to change the testing ++arguments. + -+This document provides a quick overview of some iOS specific features in the -+Python distribution. ++The test plan also disables parallel testing, and specifies the use of the ++``Testbed.lldbinit`` file for providing configuration of the debugger. The ++default debugger configuration disables automatic breakpoints on the ++``SIGINT``, ``SIGUSR1``, ``SIGUSR2``, and ``SIGXFSZ`` signals. + -+These instructions are only needed if you're planning to compile Python for iOS -+yourself. Most users should *not* need to do this. If you're looking to -+experiment with writing an iOS app in Python, tools such as [BeeWare's -+Briefcase](https://briefcase.readthedocs.io) and [Kivy's -+Buildozer](https://buildozer.readthedocs.io) will provide a much more -+approachable user experience. ++App Store Compliance ++==================== + -+## Compilers for building on iOS ++The only mechanism for distributing apps to third-party iOS devices is to ++submit the app to the iOS App Store; apps submitted for distribution must pass ++Apple's app review process. This process includes a set of automated validation ++rules that inspect the submitted application bundle for problematic code. There ++are some steps that must be taken to ensure that your app will be able to pass ++these validation steps. + -+Building for iOS requires the use of Apple's Xcode tooling. It is strongly -+recommended that you use the most recent stable release of Xcode. This will -+require the use of the most (or second-most) recently released macOS version, -+as Apple does not maintain Xcode for older macOS versions. The Xcode Command -+Line Tools are not sufficient for iOS development; you need a *full* Xcode -+install. ++Incompatible code in the standard library ++----------------------------------------- + -+If you want to run your code on the iOS simulator, you'll also need to install -+an iOS Simulator Platform. You should be prompted to select an iOS Simulator -+Platform when you first run Xcode. Alternatively, you can add an iOS Simulator -+Platform by selecting an open the Platforms tab of the Xcode Settings panel. ++The Python standard library contains some code that is known to violate these ++automated rules. While these violations appear to be false positives, Apple's ++review rules cannot be challenged; so, it is necessary to modify the Python ++standard library for an app to pass App Store review. + -+## Building Python on iOS ++The Python source tree contains ++:source:`a patch file ` that will remove ++all code that is known to cause issues with the App Store review process. This ++patch is applied automatically when building for iOS. + -+### ABIs and Architectures ++Privacy manifests ++----------------- + -+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`). ++In April 2025, Apple introduced a requirement for `certain third-party ++libraries to provide a Privacy Manifest ++`__. ++As a result, if you have a binary module that uses one of the affected ++libraries, you must provide an ``.xcprivacy`` file for that library. ++OpenSSL is one library affected by this requirement, but there are others. + -+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. ++If you produce a binary module named ``mymodule.so``, and use you the Xcode ++build script described in step 7 above, you can place a ``mymodule.xcprivacy`` ++file next to ``mymodule.so``, and the privacy manifest will be installed into ++the required location when the binary module is converted into a framework. +diff --git a/Doc/using/mac.rst b/Doc/using/mac.rst +index 8b67652d1df..2dfac075843 100644 +--- a/Doc/using/mac.rst ++++ b/Doc/using/mac.rst +@@ -188,6 +188,28 @@ + * `PyInstaller `__: A cross-platform packaging tool that creates + a single file or folder as a distributable artifact. + ++App Store Compliance ++-------------------- + -+iOS can also support different CPU architectures within each ABI. At present, -+there is only a single supported architecture on physical devices - ARM64. -+However, the *simulator* supports 2 architectures - ARM64 (for running on Apple -+Silicon machines), and x86_64 (for running on older Intel-based machines). ++Apps submitted for distribution through the macOS App Store must pass Apple's ++app review process. This process includes a set of automated validation rules ++that inspect the submitted application bundle for problematic code. + -+To support multiple CPU architectures on a single platform, Apple uses a "fat -+binary" format - a single physical file that contains support for multiple -+architectures. It is possible to compile and use a "thin" single architecture -+version of a binary for testing purposes; however, the "thin" binary will not be -+portable to machines using other architectures. ++The Python standard library contains some code that is known to violate these ++automated rules. While these violations appear to be false positives, Apple's ++review rules cannot be challenged. Therefore, it is necessary to modify the ++Python standard library for an app to pass App Store review. + -+### Building a multi-architecture iOS XCframework ++The Python source tree contains ++:source:`a patch file ` that will remove ++all code that is known to cause issues with the App Store review process. This ++patch is applied automatically when CPython is configured with the ++:option:`--with-app-store-compliance` option. + -+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:: ++This patch is not normally required to use CPython on a Mac; nor is it required ++if you are distributing an app *outside* the macOS App Store. It is *only* ++required if you are using the macOS App Store as a distribution channel. + -+ python Apple build iOS + Other Resources + =============== + +diff --git a/Lib/_apple_support.py b/Lib/_apple_support.py +new file mode 100644 +index 00000000000..92febdcf587 +--- /dev/null ++++ b/Lib/_apple_support.py +@@ -0,0 +1,66 @@ ++import io ++import sys + -+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. ++def init_streams(log_write, stdout_level, stderr_level): ++ # Redirect stdout and stderr to the Apple system log. This method is ++ # invoked by init_apple_streams() (initconfig.c) if config->use_system_logger ++ # is enabled. ++ sys.stdout = SystemLog(log_write, stdout_level, errors=sys.stderr.errors) ++ sys.stderr = SystemLog(log_write, stderr_level, errors=sys.stderr.errors) + -+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 ++class SystemLog(io.TextIOWrapper): ++ def __init__(self, log_write, level, **kwargs): ++ kwargs.setdefault("encoding", "UTF-8") ++ kwargs.setdefault("line_buffering", True) ++ super().__init__(LogStream(log_write, level), **kwargs) + -+If you're using the `Apple` build script, you won't need to build -+individual frameworks. However, if you do need to manually configure an iOS -+Python build for a single framework, the following options are available. -+ -+#### iOS specific arguments to configure -+ -+* `--enable-framework[=DIR]` -+ -+ This argument specifies the location where the Python.framework will be -+ installed. If `DIR` is not specified, the framework will be installed into -+ a subdirectory of the `iOS/Frameworks` folder. -+ -+ This argument *must* be provided when configuring iOS builds. iOS does not -+ support non-framework builds. -+ -+* `--with-framework-name=NAME` -+ -+ Specify the name for the Python framework; defaults to `Python`. -+ -+ > [!NOTE] -+ > Unless you know what you're doing, changing the name of the Python -+ > framework on iOS is not advised. If you use this option, you won't be able -+ > to run the `Apple` build script without making significant manual -+ > alterations, and you won't be able to use any binary packages unless you -+ > compile them yourself using your own framework name. -+ -+#### Building Python for iOS ++ def __repr__(self): ++ return f"" + -+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. ++ def write(self, s): ++ if not isinstance(s, str): ++ raise TypeError( ++ f"write() argument must be str, not {type(s).__name__}") + -+If you want to use Python in a real iOS project, you need to produce multiple -+`Python.framework` builds, one for each ABI and architecture. iOS builds of -+Python *must* be constructed as framework builds. To support this, you must -+provide the `--enable-framework` flag when configuring the build. The build -+also requires the use of cross-compilation. The minimal commands for building -+Python for the ARM64 iOS simulator will look something like: -+``` -+export PATH="$(pwd)/Apple/iOS/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" -+./configure \ -+ --enable-framework \ -+ --host=arm64-apple-ios-simulator \ -+ --build=arm64-apple-darwin \ -+ --with-build-python=/path/to/python.exe -+make -+make install -+``` ++ # In case `s` is a str subclass that writes itself to stdout or stderr ++ # when we call its methods, convert it to an actual str. ++ s = str.__str__(s) + -+In this invocation: ++ # We want to emit one log message per line, so split ++ # the string before sending it to the superclass. ++ for line in s.splitlines(keepends=True): ++ super().write(line) + -+* `Apple/iOS/Resources/bin` has been added to the path, providing some shims for the -+ compilers and linkers needed by the build. Xcode requires the use of `xcrun` -+ to invoke compiler tooling. However, if `xcrun` is pre-evaluated and the -+ result passed to `configure`, these results can embed user- and -+ version-specific paths into the sysconfig data, which limits the portability -+ of the compiled Python. Alternatively, if `xcrun` is used *as* the compiler, -+ it requires that compiler variables like `CC` include spaces, which can -+ cause significant problems with many C configuration systems which assume that -+ `CC` will be a single executable. ++ return len(s) + -+ To work around this problem, the `Apple/iOS/Resources/bin` folder contains some -+ wrapper scripts that present as simple compilers and linkers, but wrap -+ underlying calls to `xcrun`. This allows configure to use a `CC` -+ definition without spaces, and without user- or version-specific paths, while -+ retaining the ability to adapt to the local Xcode install. These scripts are -+ included in the `bin` directory of an iOS install. + -+ These scripts will, by default, use the currently active Xcode installation. -+ If you want to use a different Xcode installation, you can use -+ `xcode-select` to set a new default Xcode globally, or you can use the -+ `DEVELOPER_DIR` environment variable to specify an Xcode install. The -+ scripts will use the default `iphoneos`/`iphonesimulator` SDK version for -+ the select Xcode install; if you want to use a different SDK, you can set the -+ `IOS_SDK_VERSION` environment variable. (e.g, setting -+ `IOS_SDK_VERSION=17.1` would cause the scripts to use the `iphoneos17.1` -+ and `iphonesimulator17.1` SDKs, regardless of the Xcode default.) ++class LogStream(io.RawIOBase): ++ def __init__(self, log_write, level): ++ self.log_write = log_write ++ self.level = level + -+ 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. ++ def __repr__(self): ++ return f"" + -+* `--host` is the architecture and ABI that you want to build, in GNU compiler -+ triple format. This will be one of: ++ def writable(self): ++ return True + -+ - `arm64-apple-ios` for ARM64 iOS devices. -+ - `arm64-apple-ios-simulator` for the iOS simulator running on Apple -+ Silicon devices. -+ - `x86_64-apple-ios-simulator` for the iOS simulator running on Intel -+ devices. ++ def write(self, b): ++ if type(b) is not bytes: ++ try: ++ b = bytes(memoryview(b)) ++ except TypeError: ++ raise TypeError( ++ f"write() argument must be bytes-like, not {type(b).__name__}" ++ ) from None + -+* `--build` is the GNU compiler triple for the machine that will be running -+ the compiler. This is one of: ++ # Writing an empty string to the stream should have no effect. ++ if b: ++ # Encode null bytes using "modified UTF-8" to avoid truncating the ++ # message. This should not affect the return value, as the caller ++ # may be expecting it to match the length of the input. ++ self.log_write(self.level, b.replace(b"\x00", b"\xc0\x80")) + -+ - `arm64-apple-darwin` for Apple Silicon devices. -+ - `x86_64-apple-darwin` for Intel devices. ++ return len(b) +diff --git a/Lib/_ios_support.py b/Lib/_ios_support.py +new file mode 100644 +index 00000000000..542f5a14910 +--- /dev/null ++++ b/Lib/_ios_support.py +@@ -0,0 +1,74 @@ ++import sys ++try: ++ from ctypes import cdll, c_void_p, c_char_p, util ++except ImportError: ++ # ctypes is an optional module. If it's not present, we're limited in what ++ # we can tell about the system, but we don't want to prevent the module ++ # from working. ++ print("ctypes isn't available; iOS system calls will not be available") ++ objc = None ++else: ++ # ctypes is available. Load the ObjC library, and wrap the objc_getClass, ++ # sel_registerName methods ++ # find_library() resolves a system library through the dyld shared cache, ++ # which needs _dyld_shared_cache_contains_path(); that is unavailable before ++ # iOS 14, so fall back to the bare name, which dyld resolves by itself. ++ lib = util.find_library("objc") or "libobjc.dylib" + -+* `/path/to/python.exe` is the path to a Python binary on the machine that -+ will be running the compiler. This is needed because the Python compilation -+ process involves running some Python code. On a normal desktop build of -+ Python, you can compile a python interpreter and then use that interpreter to -+ run Python code. However, the binaries produced for iOS won't run on macOS, so -+ you need to provide an external Python interpreter. This interpreter must be -+ the same version as the Python that is being compiled. To be completely safe, -+ this should be the *exact* same commit hash. However, the longer a Python -+ release has been stable, the more likely it is that this constraint can be -+ relaxed - the same micro version will often be sufficient. ++ try: ++ objc = cdll.LoadLibrary(lib) ++ except OSError: ++ raise ImportError("ObjC runtime library couldn't be loaded") ++ objc.objc_getClass.restype = c_void_p ++ objc.objc_getClass.argtypes = [c_char_p] ++ objc.sel_registerName.restype = c_void_p ++ objc.sel_registerName.argtypes = [c_char_p] + -+* The `install` target for iOS builds is slightly different to other -+ platforms. On most platforms, `make install` will install the build into -+ the final runtime location. This won't be the case for iOS, as the final -+ runtime location will be on a physical device. + -+ However, you still need to run the `install` target for iOS builds, as it -+ performs some final framework assembly steps. The location specified with -+ `--enable-framework` will be the location where `make install` will -+ assemble the complete iOS framework. This completed framework can then -+ be copied and relocated as required. ++def get_platform_ios(): ++ # Determine if this is a simulator using the multiarch value ++ is_simulator = sys.implementation._multiarch.endswith("simulator") + -+For a full CPython build, you also need to specify the paths to iOS builds of -+the binary libraries that CPython depends on (such as XZ, LibFFI and OpenSSL). -+This can be done by defining library specific environment variables (such as -+`LIBLZMA_CFLAGS`, `LIBLZMA_LIBS`), and the `--with-openssl` configure -+option. Versions of these libraries pre-compiled for iOS can be found in [this -+repository](https://github.com/beeware/cpython-apple-source-deps/releases). -+LibFFI is especially important, as many parts of the standard library -+(including the `platform`, `sysconfig` and `webbrowser` modules) require -+the use of the `ctypes` module at runtime. ++ # We can't use ctypes; abort ++ if not objc: ++ return None + -+By default, Python will be compiled with an iOS deployment target (i.e., the -+minimum supported iOS version) of 13.0. To specify a different deployment -+target, provide the version number as part of the `--host` argument - for -+example, `--host=arm64-apple-ios15.4-simulator` would compile an ARM64 -+simulator build with a deployment target of 15.4. ++ # Most of the methods return ObjC objects ++ objc.objc_msgSend.restype = c_void_p ++ # All the methods used have no arguments. ++ objc.objc_msgSend.argtypes = [c_void_p, c_void_p] + -+## Testing Python on iOS ++ # Equivalent of: ++ # device = [UIDevice currentDevice] ++ UIDevice = objc.objc_getClass(b"UIDevice") ++ SEL_currentDevice = objc.sel_registerName(b"currentDevice") ++ device = objc.objc_msgSend(UIDevice, SEL_currentDevice) + -+### Testing a multi-architecture framework ++ # Equivalent of: ++ # device_systemVersion = [device systemVersion] ++ SEL_systemVersion = objc.sel_registerName(b"systemVersion") ++ device_systemVersion = objc.objc_msgSend(device, SEL_systemVersion) + -+Once you have a built an XCframework, you can test that framework by running: ++ # Equivalent of: ++ # device_systemName = [device systemName] ++ SEL_systemName = objc.sel_registerName(b"systemName") ++ device_systemName = objc.objc_msgSend(device, SEL_systemName) + -+ $ python Apple test iOS ++ # Equivalent of: ++ # device_model = [device model] ++ SEL_model = objc.sel_registerName(b"model") ++ device_model = objc.objc_msgSend(device, SEL_model) + -+This test will attempt to find an "SE-class" simulator (i.e., an iPhone SE, or -+iPhone 16e, or similar), and run the test suite on the most recent version of -+iOS that is available. You can specify a simulator using the `--simulator` -+command line argument, providing the name of the simulator (e.g., `--simulator -+'iPhone 16 Pro'`). You can also use this argument to control the OS version used -+for testing; `--simulator 'iPhone 16 Pro,OS=18.2'` would attempt to run the -+tests on an iPhone 16 Pro running iOS 18.2. ++ # UTF8String returns a const char*; ++ SEL_UTF8String = objc.sel_registerName(b"UTF8String") ++ objc.objc_msgSend.restype = c_char_p + -+If the test runner is executed on GitHub Actions, the `GITHUB_ACTIONS` -+environment variable will be exposed to the iOS process at runtime. ++ # Equivalent of: ++ # system = [device_systemName UTF8String] ++ # release = [device_systemVersion UTF8String] ++ # model = [device_model UTF8String] ++ system = objc.objc_msgSend(device_systemName, SEL_UTF8String).decode() ++ release = objc.objc_msgSend(device_systemVersion, SEL_UTF8String).decode() ++ model = objc.objc_msgSend(device_model, SEL_UTF8String).decode() + -+### Testing a single-architecture framework ++ return system, release, model, is_simulator +diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py +index 6cedee74236..0e88cffc74f 100644 +--- a/Lib/ctypes/__init__.py ++++ b/Lib/ctypes/__init__.py +@@ -346,6 +346,17 @@ + winmode=None): + if name: + name = _os.fspath(name) + -+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. ++ # If the filename that has been provided is an iOS/tvOS/watchOS ++ # .fwork file, dereference the location to the true origin of the ++ # binary. ++ if name.endswith(".fwork"): ++ with open(name) as f: ++ name = _os.path.join( ++ _os.path.dirname(_sys.executable), ++ f.read().strip() ++ ) + -+To run the test suite, configure a Python build for an iOS simulator (i.e., -+`--host=arm64-apple-ios-simulator` or `--host=x86_64-apple-ios-simulator` -+), specifying a framework build (i.e. `--enable-framework`). Ensure that your -+`PATH` has been configured to include the `Apple/iOS/Resources/bin` folder and -+exclude any non-iOS tools, then run: -+``` -+make all -+make install -+make testios -+``` -+ -+This will: + self._name = name + flags = self._func_flags_ + if use_errno: +diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py +index c550883e7c7..12d7428fe9a 100644 +--- a/Lib/ctypes/util.py ++++ b/Lib/ctypes/util.py +@@ -67,7 +67,7 @@ + return fname + return None + +-elif os.name == "posix" and sys.platform == "darwin": ++elif os.name == "posix" and sys.platform in {"darwin", "ios", "tvos", "watchos"}: + from ctypes.macholib.dyld import dyld_find as _dyld_find + def find_library(name): + possible = ['lib%s.dylib' % name, +diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py +index 6e4a087a10c..f0cd49b6773 100644 +--- a/Lib/importlib/_bootstrap_external.py ++++ b/Lib/importlib/_bootstrap_external.py +@@ -52,7 +52,7 @@ + + # Bootstrap-related code ###################################################### + _CASE_INSENSITIVE_PLATFORMS_STR_KEY = 'win', +-_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY = 'cygwin', 'darwin' ++_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY = 'cygwin', 'darwin', 'ios', 'tvos', 'watchos' + _CASE_INSENSITIVE_PLATFORMS = (_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY + + _CASE_INSENSITIVE_PLATFORMS_STR_KEY) + +@@ -1698,6 +1698,46 @@ + return f'FileFinder({self.path!r})' + + ++class AppleFrameworkLoader(ExtensionFileLoader): ++ """A loader for modules that have been packaged as frameworks for ++ compatibility with Apple's iOS App Store policies. ++ """ ++ def create_module(self, spec): ++ # If the ModuleSpec has been created by the FileFinder, it will have ++ # been created with an origin pointing to the .fwork file. We need to ++ # redirect this to the location in the Frameworks folder, using the ++ # content of the .fwork file. ++ if spec.origin.endswith(".fwork"): ++ with _io.FileIO(spec.origin, 'r') as file: ++ framework_binary = file.read().decode().strip() ++ bundle_path = _path_split(sys.executable)[0] ++ spec.origin = _path_join(bundle_path, framework_binary) + -+* Build an iOS framework for your chosen architecture; -+* Finalize the single-platform framework; -+* Make a clean copy of the testbed project; -+* Install the Python iOS framework into the copy of the testbed project; and -+* Run the test suite on an "entry-level device" simulator (i.e., an iPhone SE, -+ iPhone 16e, or a similar). ++ # If the loader is created based on the spec for a loaded module, the ++ # path will be pointing at the Framework location. If this occurs, ++ # get the original .fwork location to use as the module's __file__. ++ if self.path.endswith(".fwork"): ++ path = self.path ++ else: ++ with _io.FileIO(self.path + ".origin", 'r') as file: ++ origin = file.read().decode().strip() ++ bundle_path = _path_split(sys.executable)[0] ++ path = _path_join(bundle_path, origin) + -+On success, the test suite will exit and report successful completion of the -+test suite. On a 2022 M1 MacBook Pro, the test suite takes approximately 15 -+minutes to run; a couple of extra minutes is required to compile the testbed -+project, and then boot and prepare the iOS simulator. ++ module = _bootstrap._call_with_frames_removed(_imp.create_dynamic, spec) + -+### Debugging test failures ++ _bootstrap._verbose_message( ++ "Apple framework extension module {!r} loaded from {!r} (path {!r})", ++ spec.name, ++ spec.origin, ++ path, ++ ) + -+Running `python Apple test iOS` generates a standalone version of the -+`Apple/testbed` project, and runs the full test suite. It does this using -+`Apple/testbed` itself - the folder is an executable module that can be used -+to create and run a clone of the testbed project. The standalone version of the -+testbed will be created in a directory named -+`cross-build/iOS-testbed.`. ++ # Ensure that the __file__ points at the .fwork location ++ module.__file__ = path + -+You can generate your own standalone testbed instance by running: -+``` -+python cross-build/iOS/testbed clone my-testbed -+``` ++ return module + -+In this invocation, `my-testbed` is the name of the folder for the new -+testbed clone. + # Import setup ############################################################### + + def _fix_up_module(ns, name, pathname, cpathname=None): +@@ -1730,10 +1770,17 @@ + + Each item is a tuple (loader, suffixes). + """ +- extensions = ExtensionFileLoader, _imp.extension_suffixes() ++ if sys.platform in {"ios", "tvos", "watchos"}: ++ extension_loaders = [(AppleFrameworkLoader, [ ++ suffix.replace(".so", ".fwork") ++ for suffix in _imp.extension_suffixes() ++ ])] ++ else: ++ extension_loaders = [] ++ extension_loaders.append((ExtensionFileLoader, _imp.extension_suffixes())) + source = SourceFileLoader, SOURCE_SUFFIXES + bytecode = SourcelessFileLoader, BYTECODE_SUFFIXES +- return [extensions, source, bytecode] ++ return extension_loaders + [source, bytecode] + + + def _set_bootstrap_module(_bootstrap_module): +diff --git a/Lib/importlib/abc.py b/Lib/importlib/abc.py +index b56fa94eb9c..37fef357fe2 100644 +--- a/Lib/importlib/abc.py ++++ b/Lib/importlib/abc.py +@@ -180,7 +180,11 @@ + else: + return self.source_to_code(source, path) + +-_register(ExecutionLoader, machinery.ExtensionFileLoader) ++_register( ++ ExecutionLoader, ++ machinery.ExtensionFileLoader, ++ machinery.AppleFrameworkLoader, ++) + + + class FileLoader(_bootstrap_external.FileLoader, ResourceLoader, ExecutionLoader): +diff --git a/Lib/importlib/machinery.py b/Lib/importlib/machinery.py +index d9a19a13f7b..fbd30b159fb 100644 +--- a/Lib/importlib/machinery.py ++++ b/Lib/importlib/machinery.py +@@ -12,6 +12,7 @@ + from ._bootstrap_external import SourceFileLoader + from ._bootstrap_external import SourcelessFileLoader + from ._bootstrap_external import ExtensionFileLoader ++from ._bootstrap_external import AppleFrameworkLoader + from ._bootstrap_external import NamespaceLoader + + +diff --git a/Lib/inspect.py b/Lib/inspect.py +index 2a8d9b053cc..4d0bbcb3835 100644 +--- a/Lib/inspect.py ++++ b/Lib/inspect.py +@@ -961,6 +961,10 @@ + elif any(filename.endswith(s) for s in + importlib.machinery.EXTENSION_SUFFIXES): + return None ++ elif filename.endswith(".fwork"): ++ # Apple mobile framework markers are another type of non-source file ++ return None + -+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 -+``` + # return a filename found in the linecache even if it doesn't exist on disk + if filename in linecache.cache: + return filename +@@ -991,6 +995,7 @@ + return object + if hasattr(object, '__module__'): + return sys.modules.get(object.__module__) + -+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`. + # Try the filename to modulename cache + if _filename is not None and _filename in modulesbyfile: + return sys.modules.get(modulesbyfile[_filename]) +@@ -1084,7 +1089,7 @@ + # Allow filenames in form of "" to pass through. + # `doctest` monkeypatches `linecache` module to enable + # inspection, so let `linecache.getlines` to be called. +- if not (file.startswith('<') and file.endswith('>')): ++ if (not (file.startswith('<') and file.endswith('>'))) or file.endswith('.fwork'): + raise OSError('source code not available') + + module = getmodule(object, file) +diff --git a/Lib/modulefinder.py b/Lib/modulefinder.py +index a0a020f9eeb..ac478ee7f51 100644 +--- a/Lib/modulefinder.py ++++ b/Lib/modulefinder.py +@@ -72,7 +72,12 @@ + if isinstance(spec.loader, importlib.machinery.SourceFileLoader): + kind = _PY_SOURCE + +- elif isinstance(spec.loader, importlib.machinery.ExtensionFileLoader): ++ elif isinstance( ++ spec.loader, ( ++ importlib.machinery.ExtensionFileLoader, ++ importlib.machinery.AppleFrameworkLoader, ++ ) ++ ): + kind = _C_EXTENSION + + elif isinstance(spec.loader, importlib.machinery.SourcelessFileLoader): +diff --git a/Lib/platform.py b/Lib/platform.py +index b86e6834911..115c2f6d21c 100755 +--- a/Lib/platform.py ++++ b/Lib/platform.py +@@ -498,6 +498,78 @@ + # If that also doesn't work return the default values + return release, versioninfo, machine + + -+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 -+``` ++# A namedtuple for iOS version information. ++IOSVersionInfo = collections.namedtuple( ++ "IOSVersionInfo", ++ ["system", "release", "model", "is_simulator"] ++) + -+This is the equivalent of running `python -m test -W test_os` on a desktop -+Python build. Any arguments after the `--` will be passed to testbed as if -+they were arguments to `python -m` on a desktop machine. + -+### Testing in Xcode ++def ios_ver(system="", release="", model="", is_simulator=False): ++ """Get iOS version information, and return it as a namedtuple: ++ (system, release, model, is_simulator). + -+You can also open the testbed project in Xcode by running: -+``` -+open my-testbed/iOSTestbed.xcodeproj -+``` ++ If values can't be determined, they are set to values provided as ++ parameters. ++ """ ++ if sys.platform == "ios": ++ import _ios_support ++ result = _ios_support.get_platform_ios() ++ if result is not None: ++ return IOSVersionInfo(*result) + -+This will allow you to use the full Xcode suite of tools for debugging. ++ return IOSVersionInfo(system, release, model, is_simulator) + -+The arguments used to run the test suite are defined as part of the test plan. -+To modify the test plan, select the test plan node of the project tree (it -+should be the first child of the root node), and select the "Configurations" -+tab. Modify the "Arguments Passed On Launch" value to change the testing -+arguments. + -+The test plan also disables parallel testing, and specifies the use of the -+`Testbed.lldbinit` file for providing configuration of the debugger. The -+default debugger configuration disables automatic breakpoints on the -+`SIGINT`, `SIGUSR1`, `SIGUSR2`, and `SIGXFSZ` signals. ++# A namedtuple for tvOS version information. ++TVOSVersionInfo = collections.namedtuple( ++ "TVOSVersionInfo", ++ ["system", "release", "model", "is_simulator"] ++) + -+### Testing on an iOS device + -+To test on an iOS device, the app needs to be signed with known developer -+credentials. To obtain these credentials, you must have an iOS Developer -+account, and your Xcode install will need to be logged into your account (see -+the Accounts tab of the Preferences dialog). ++def tvos_ver(system="", release="", model="", is_simulator=False): ++ """Get tvOS version information, and return it as a namedtuple: ++ (system, release, model, is_simulator). + -+Once the project is open, and you're signed into your Apple Developer account, -+select the root node of the project tree (labeled "iOSTestbed"), then the -+"Signing & Capabilities" tab in the details page. Select a development team -+(this will likely be your own name), and plug in a physical device to your -+macOS machine with a USB cable. You should then be able to select your physical -+device from the list of targets in the pulldown in the Xcode titlebar. -diff --git a/Apple/iOS/Resources/Info.plist.in b/Apple/iOS/Resources/Info.plist.in -new file mode 100644 -index 00000000000..26ef7a95de4 ---- /dev/null -+++ b/Apple/iOS/Resources/Info.plist.in -@@ -0,0 +1,34 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleExecutable -+ Python -+ CFBundleGetInfoString -+ Python Runtime and Library -+ CFBundleIdentifier -+ @PYTHONFRAMEWORKIDENTIFIER@ -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundleName -+ Python -+ CFBundlePackageType -+ FMWK -+ CFBundleShortVersionString -+ %VERSION% -+ CFBundleLongVersionString -+ %VERSION%, (c) 2001-2024 Python Software Foundation. -+ CFBundleSignature -+ ???? -+ CFBundleVersion -+ %VERSION% -+ CFBundleSupportedPlatforms -+ -+ iPhoneOS -+ -+ MinimumOSVersion -+ @IPHONEOS_DEPLOYMENT_TARGET@ -+ -+ -diff --git a/Apple/iOS/Resources/bin/arm64-apple-ios-ar b/Apple/iOS/Resources/bin/arm64-apple-ios-ar -new file mode 100755 -index 00000000000..3cf3eb21874 ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-ar -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@" -diff --git a/Apple/iOS/Resources/bin/arm64-apple-ios-clang b/Apple/iOS/Resources/bin/arm64-apple-ios-clang -new file mode 100755 -index 00000000000..f50d5b5142f ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-clang -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" -diff --git a/Apple/iOS/Resources/bin/arm64-apple-ios-clang++ b/Apple/iOS/Resources/bin/arm64-apple-ios-clang++ -new file mode 100755 -index 00000000000..0794731d7dc ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" -diff --git a/Apple/iOS/Resources/bin/arm64-apple-ios-cpp b/Apple/iOS/Resources/bin/arm64-apple-ios-cpp -new file mode 100755 -index 00000000000..24fa1506bab ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-cpp -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} -E "$@" -diff --git a/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar -new file mode 100755 -index 00000000000..b836b6db902 ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" -diff --git a/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang -new file mode 100755 -index 00000000000..4891a00876e ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ -new file mode 100755 -index 00000000000..58b2a5f6f18 ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp -new file mode 100755 -index 00000000000..c9df94e8b7c ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" -diff --git a/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip -new file mode 100755 -index 00000000000..fd59d309b73 ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch arm64 "$@" -diff --git a/Apple/iOS/Resources/bin/arm64-apple-ios-strip b/Apple/iOS/Resources/bin/arm64-apple-ios-strip -new file mode 100755 -index 00000000000..75e823a3d02 ---- /dev/null -+++ b/Apple/iOS/Resources/bin/arm64-apple-ios-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} strip -arch arm64 "$@" -diff --git a/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar -new file mode 100755 -index 00000000000..b836b6db902 ---- /dev/null -+++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" -diff --git a/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang -new file mode 100755 -index 00000000000..f4739a7b945 ---- /dev/null -+++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ -new file mode 100755 -index 00000000000..c348ae4c103 ---- /dev/null -+++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp -new file mode 100755 -index 00000000000..6d7f8084c9f ---- /dev/null -+++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" -diff --git a/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip -new file mode 100755 -index 00000000000..c5cfb289291 ---- /dev/null -+++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch x86_64 "$@" -diff --git a/Apple/iOS/Resources/pyconfig.h b/Apple/iOS/Resources/pyconfig.h -new file mode 100644 -index 00000000000..4acff2c6051 ---- /dev/null -+++ b/Apple/iOS/Resources/pyconfig.h -@@ -0,0 +1,7 @@ -+#ifdef __arm64__ -+#include "pyconfig-arm64.h" -+#endif ++ If values can't be determined, they are set to values provided as ++ parameters. ++ """ ++ if sys.platform == "tvos": ++ # TODO: Can the iOS implementation be used here? ++ import _ios_support ++ result = _ios_support.get_platform_ios() ++ if result is not None: ++ return TVOSVersionInfo(*result) + -+#ifdef __x86_64__ -+#include "pyconfig-x86_64.h" -+#endif -diff --git a/Apple/testbed/Python.xcframework/Info.plist b/Apple/testbed/Python.xcframework/Info.plist -new file mode 100644 -index 00000000000..f90105a6d70 ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/Info.plist -@@ -0,0 +1,106 @@ -+ -+ -+ -+ -+ AvailableLibraries -+ -+ -+ BinaryPath -+ Python.framework/Python -+ LibraryIdentifier -+ ios-arm64 -+ LibraryPath -+ Python.framework -+ SupportedArchitectures -+ -+ arm64 -+ -+ SupportedPlatform -+ ios -+ -+ -+ BinaryPath -+ Python.framework/Python -+ LibraryIdentifier -+ ios-arm64_x86_64-simulator -+ LibraryPath -+ Python.framework -+ SupportedArchitectures -+ -+ arm64 -+ x86_64 -+ -+ SupportedPlatform -+ ios -+ SupportedPlatformVariant -+ simulator -+ -+ -+ BinaryPath -+ Python.framework/Python -+ LibraryIdentifier -+ tvos-arm64 -+ LibraryPath -+ Python.framework -+ SupportedArchitectures -+ -+ arm64 -+ -+ SupportedPlatform -+ tvos -+ -+ -+ BinaryPath -+ Python.framework/Python -+ LibraryIdentifier -+ tvos-arm64_x86_64-simulator -+ LibraryPath -+ Python.framework -+ SupportedArchitectures -+ -+ arm64 -+ x86_64 -+ -+ SupportedPlatform -+ tvos -+ SupportedPlatformVariant -+ simulator -+ -+ -+ BinaryPath -+ Python.framework/Python -+ LibraryIdentifier -+ watchos-arm64_x86_64-simulator -+ LibraryPath -+ Python.framework -+ SupportedArchitectures -+ -+ arm64 -+ x86_64 -+ -+ SupportedPlatform -+ watchos -+ SupportedPlatformVariant -+ simulator -+ -+ -+ BinaryPath -+ Python.framework/Python -+ LibraryIdentifier -+ watchos-arm64_32 -+ LibraryPath -+ Python.framework -+ SupportedArchitectures -+ -+ arm64_32 -+ -+ SupportedPlatform -+ watchos -+ -+ -+ CFBundlePackageType -+ XFWK -+ XCFrameworkFormatVersion -+ 1.0 -+ -+ -diff --git a/Apple/testbed/Python.xcframework/build/iOS-dylib-Info-template.plist b/Apple/testbed/Python.xcframework/build/iOS-dylib-Info-template.plist -new file mode 100644 -index 00000000000..d6caa01c1e4 ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/build/iOS-dylib-Info-template.plist -@@ -0,0 +1,26 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleExecutable -+ -+ CFBundleIdentifier -+ -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundlePackageType -+ APPL -+ CFBundleShortVersionString -+ 1.0 -+ CFBundleSupportedPlatforms -+ -+ iPhoneOS -+ -+ MinimumOSVersion -+ 13.0 -+ CFBundleVersion -+ 1 -+ -+ -diff --git a/Apple/testbed/Python.xcframework/build/tvOS-dylib-Info-template.plist b/Apple/testbed/Python.xcframework/build/tvOS-dylib-Info-template.plist -new file mode 100644 -index 00000000000..a20d476fa7b ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/build/tvOS-dylib-Info-template.plist -@@ -0,0 +1,26 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleExecutable -+ -+ CFBundleIdentifier -+ -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundlePackageType -+ APPL -+ CFBundleShortVersionString -+ 1.0 -+ CFBundleSupportedPlatforms -+ -+ tvOS -+ -+ MinimumOSVersion -+ 9.0 -+ CFBundleVersion -+ 1 -+ -+ -diff --git a/Apple/testbed/Python.xcframework/build/utils.sh b/Apple/testbed/Python.xcframework/build/utils.sh -new file mode 100755 -index 00000000000..2c3c8008512 ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/build/utils.sh -@@ -0,0 +1,174 @@ -+# 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 -+# ----- ++ return TVOSVersionInfo(system, release, model, is_simulator) + -+# Copy the standard library from the XCframework into the app bundle. -+# -+# Accepts one argument: -+# 1. The path, relative to the root of the Xcode project, where the Python -+# XCframework can be found. -+install_stdlib() { -+ PYTHON_XCFRAMEWORK_PATH=$1 + -+ mkdir -p "$CODESIGNING_FOLDER_PATH/python/lib" -+ if [ "$EFFECTIVE_PLATFORM_NAME" = "-iphonesimulator" ]; then -+ echo "Installing Python modules for iOS Simulator" -+ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/ios-arm64-simulator" ]; then -+ SLICE_FOLDER="ios-arm64-simulator" -+ else -+ SLICE_FOLDER="ios-arm64_x86_64-simulator" -+ fi -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-iphoneos" ]; then -+ echo "Installing Python modules for iOS Device" -+ SLICE_FOLDER="ios-arm64" -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-appletvsimulator" ]; then -+ echo "Installing Python modules for tvOS Simulator" -+ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/tvos-arm64-simulator" ]; then -+ SLICE_FOLDER="tvos-arm64-simulator" -+ else -+ SLICE_FOLDER="tvos-arm64_x86_64-simulator" -+ fi -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-appletvos" ]; then -+ echo "Installing Python modules for tvOS Device" -+ SLICE_FOLDER="tvos-arm64" -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-watchsimulator" ]; then -+ echo "Installing Python modules for watchOS Simulator" -+ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/watchos-arm64-simulator" ]; then -+ SLICE_FOLDER="watchos-arm64-simulator" -+ else -+ SLICE_FOLDER="watchos-arm64_x86_64-simulator" -+ fi -+ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-watchos" ]; then -+ echo "Installing Python modules for watchOS Device" -+ SLICE_FOLDER="watchos-arm64" -+ else -+ echo "Unsupported platform name $EFFECTIVE_PLATFORM_NAME" -+ exit 1 -+ fi ++# A namedtuple for watchOS version information. ++WatchOSVersionInfo = collections.namedtuple( ++ "WatchOSVersionInfo", ++ ["system", "release", "model", "is_simulator"] ++) + -+ # 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. -+ # Don't include any libpython symlink; that can't be included at runtime -+ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib" ]; then -+ rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" --exclude 'libpython*.dylib' -+ rsync -au "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib-$ARCHS/" "$CODESIGNING_FOLDER_PATH/python/lib/" --exclude 'libpython*.dylib' -+ else -+ rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" --exclude 'libpython*.dylib' -+ fi -+} + -+# Convert a single .so library into a framework that iOS can load. -+# -+# Accepts three arguments: -+# 1. The path, relative to the root of the Xcode project, where the Python -+# XCframework can be found. -+# 2. The base path, relative to the installed location in the app bundle, that -+# needs to be processed. Any .so file found in this path (or a subdirectory -+# of it) will be processed. -+# 2. The full path to a single .so file to process. This path should include -+# the base path. -+install_dylib () { -+ PYTHON_XCFRAMEWORK_PATH=$1 -+ INSTALL_BASE=$2 -+ FULL_EXT=$3 ++def watchos_ver(system="", release="", model="", is_simulator=False): ++ """Get watchOS version information, and return it as a namedtuple: ++ (system, release, model, is_simulator). + -+ # The name of the extension file -+ EXT=$(basename "$FULL_EXT") -+ # The name and location of the module -+ MODULE_PATH=$(dirname "$FULL_EXT") -+ MODULE_NAME=$(echo $EXT | cut -d "." -f 1) -+ # The location of the extension file, relative to the bundle -+ RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} -+ # The path to the extension file, relative to the install base -+ PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/} -+ # The full dotted name of the extension module, constructed from the file path. -+ FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d "." -f 1 | tr "/" "."); -+ # A bundle identifier; not actually used, but required by Xcode framework packaging -+ FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr "_" "-") -+ # The name of the framework folder. -+ FRAMEWORK_FOLDER="Frameworks/$FULL_MODULE_NAME.framework" ++ If values can't be determined, they are set to values provided as ++ parameters. ++ """ ++ if sys.platform == "watchos": ++ # TODO: Can the iOS implementation be used here? ++ import _ios_support ++ result = _ios_support.get_platform_ios() ++ if result is not None: ++ return WatchOSVersionInfo(*result) + -+ # If the framework folder doesn't exist, create it. -+ if [ ! -d "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ]; then -+ echo "Creating framework for $RELATIVE_EXT" -+ mkdir -p "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" -+ cp "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/build/$PLATFORM_FAMILY_NAME-dylib-Info-template.plist" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" -+ plutil -replace CFBundleExecutable -string "$FULL_MODULE_NAME" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" -+ plutil -replace CFBundleIdentifier -string "$FRAMEWORK_BUNDLE_ID" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" -+ fi ++ return WatchOSVersionInfo(system, release, model, is_simulator) + -+ echo "Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME" -+ mv "$FULL_EXT" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" -+ # Create a placeholder .fwork file where the .so was -+ echo "$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" > ${FULL_EXT%.so}.fwork -+ # Create a back reference to the .so file location in the framework -+ echo "${RELATIVE_EXT%.so}.fwork" > "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin" + -+ # If the framework provides an xcprivacy file, install it. -+ if [ -e "$MODULE_PATH/$MODULE_NAME.xcprivacy" ]; then -+ echo "Installing XCPrivacy file for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME" -+ XCPRIVACY_FILE="$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/PrivacyInfo.xcprivacy" -+ if [ -e "$XCPRIVACY_FILE" ]; then -+ rm -rf "$XCPRIVACY_FILE" -+ fi -+ mv "$MODULE_PATH/$MODULE_NAME.xcprivacy" "$XCPRIVACY_FILE" -+ fi + def _java_getprop(name, default): + + from java.lang import System +@@ -613,7 +685,7 @@ + if cleaned == platform: + break + platform = cleaned +- while platform[-1] == '-': ++ while platform and platform[-1] == '-': + platform = platform[:-1] + + return platform +@@ -654,7 +726,7 @@ + default in case the command should fail. + + """ +- if sys.platform in ('dos', 'win32', 'win16'): ++ if sys.platform in {'dos', 'win32', 'win16', 'ios', 'tvos', 'watchos'}: + # XXX Others too ? + return default + +@@ -816,6 +888,25 @@ + csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0) + return 'Alpha' if cpu_number >= 128 else 'VAX' + ++ # On the iOS/tvOS/watchOS simulator, os.uname returns the architecture as ++ # uname.machine. On device it returns the model name for some reason; but ++ # there's only one CPU architecture for devices, so we know the right ++ # answer. ++ def get_ios(): ++ if sys.implementation._multiarch.endswith("simulator"): ++ return os.uname().machine ++ return 'arm64' + -+ 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 ++ def get_tvos(): ++ if sys.implementation._multiarch.endswith("simulator"): ++ return os.uname().machine ++ return 'arm64' + -+ install_stdlib $PYTHON_XCFRAMEWORK_PATH -+ PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib" | grep -E "^python3\.\d+$") -+ echo "Install Python $PYTHON_VER standard library extension modules..." -+ process_dylibs $PYTHON_XCFRAMEWORK_PATH python/lib/$PYTHON_VER/lib-dynload ++ def get_watchos(): ++ if sys.implementation._multiarch.endswith("simulator"): ++ return os.uname().machine ++ return 'arm64_32' + -+ for package_path in $@; do -+ echo "Installing $package_path extension modules ..." -+ process_dylibs $PYTHON_XCFRAMEWORK_PATH $package_path -+ done -+} -diff --git a/Apple/testbed/Python.xcframework/build/watchOS-dylib-Info-template.plist b/Apple/testbed/Python.xcframework/build/watchOS-dylib-Info-template.plist -new file mode 100644 -index 00000000000..6f8c0bc2095 ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/build/watchOS-dylib-Info-template.plist -@@ -0,0 +1,26 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleExecutable -+ -+ CFBundleIdentifier -+ -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundlePackageType -+ APPL -+ CFBundleShortVersionString -+ 1.0 -+ CFBundleSupportedPlatforms -+ -+ watchOS -+ -+ MinimumOSVersion -+ 4.0 -+ CFBundleVersion -+ 1 -+ -+ -diff --git a/Apple/testbed/Python.xcframework/ios-arm64/README b/Apple/testbed/Python.xcframework/ios-arm64/README -new file mode 100644 -index 00000000000..c1b076d12cd ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/ios-arm64/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. + def from_subprocess(): + """ + Fall back to `uname -p` +@@ -970,6 +1061,14 @@ + system = 'Windows' + release = 'Vista' + ++ # Normalize responses on Apple mobile platforms ++ if sys.platform == 'ios': ++ system, release, _, _ = ios_ver() ++ if sys.platform == 'tvos': ++ system, release, _, _ = tvos_ver() ++ if sys.platform == 'watchos': ++ system, release, _, _ = watchos_ver() + -+It should be used as a target for `--enable-framework` when compiling an iOS on-device -+build for testing purposes. -diff --git a/Apple/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README b/Apple/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README -new file mode 100644 -index 00000000000..ae334e5d769 ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. + vals = system, node, release, version, machine + # Replace 'unknown' values with the more portable '' + _uname_cache = uname_result(*map(_unknown_as_blank, vals)) +@@ -1249,11 +1348,18 @@ + system, release, version = system_alias(system, release, version) + + if system == 'Darwin': +- # macOS (darwin kernel) +- macos_release = mac_ver()[0] +- if macos_release: +- system = 'macOS' +- release = macos_release ++ # macOS and iOS both report as a "Darwin" kernel ++ if sys.platform == "ios": ++ system, release, _, _ = ios_ver() ++ elif sys.platform == "tvos": ++ system, release, _, _ = tvos_ver() ++ elif sys.platform == "watchos": ++ system, release, _, _ = watchos_ver() ++ else: ++ macos_release = mac_ver()[0] ++ if macos_release: ++ system = 'macOS' ++ release = macos_release + + if system == 'Windows': + # MS platforms +diff --git a/Lib/site.py b/Lib/site.py +index aed254ad504..ecbb2f57b3c 100644 +--- a/Lib/site.py ++++ b/Lib/site.py +@@ -287,8 +287,8 @@ + if env_base: + return env_base + +- # Emscripten, VxWorks, and WASI have no home directories +- if sys.platform in {"emscripten", "vxworks", "wasi"}: ++ # Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories ++ if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}: + return None + + def joinuser(*args): +diff --git a/Lib/subprocess.py b/Lib/subprocess.py +index 881a9ce800a..e209f51013a 100644 +--- a/Lib/subprocess.py ++++ b/Lib/subprocess.py +@@ -74,8 +74,8 @@ + else: + _mswindows = True + +-# wasm32-emscripten and wasm32-wasi do not support processes +-_can_fork_exec = sys.platform not in {"emscripten", "wasi"} ++# some platforms do not support subprocesses ++_can_fork_exec = sys.platform not in {"emscripten", "wasi", "ios", "tvos", "watchos"} + + if _mswindows: + import _winapi +@@ -103,18 +103,22 @@ + if _can_fork_exec: + from _posixsubprocess import fork_exec as _fork_exec + # used in methods that are called by __del__ +- _waitpid = os.waitpid +- _waitstatus_to_exitcode = os.waitstatus_to_exitcode +- _WIFSTOPPED = os.WIFSTOPPED +- _WSTOPSIG = os.WSTOPSIG +- _WNOHANG = os.WNOHANG ++ class _del_safe: ++ waitpid = os.waitpid ++ waitstatus_to_exitcode = os.waitstatus_to_exitcode ++ WIFSTOPPED = os.WIFSTOPPED ++ WSTOPSIG = os.WSTOPSIG ++ WNOHANG = os.WNOHANG ++ ECHILD = errno.ECHILD + else: +- _fork_exec = None +- _waitpid = None +- _waitstatus_to_exitcode = None +- _WIFSTOPPED = None +- _WSTOPSIG = None +- _WNOHANG = None ++ class _del_safe: ++ waitpid = None ++ waitstatus_to_exitcode = None ++ WIFSTOPPED = None ++ WSTOPSIG = None ++ WNOHANG = None ++ ECHILD = errno.ECHILD + -+It should be used as a target for `--enable-framework` when compiling an iOS simulator -+build for testing purposes (either x86_64 or ARM64). -diff --git a/Apple/testbed/Python.xcframework/tvos-arm64/README b/Apple/testbed/Python.xcframework/tvos-arm64/README -new file mode 100644 -index 00000000000..ebd648d04bd ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/tvos-arm64/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. + import select + import selectors + +@@ -1958,20 +1962,16 @@ + raise child_exception_type(err_msg) + + +- def _handle_exitstatus(self, sts, +- _waitstatus_to_exitcode=_waitstatus_to_exitcode, +- _WIFSTOPPED=_WIFSTOPPED, +- _WSTOPSIG=_WSTOPSIG): ++ def _handle_exitstatus(self, sts, _del_safe=_del_safe): + """All callers to this function MUST hold self._waitpid_lock.""" + # This method is called (indirectly) by __del__, so it cannot + # refer to anything outside of its local scope. +- if _WIFSTOPPED(sts): +- self.returncode = -_WSTOPSIG(sts) ++ if _del_safe.WIFSTOPPED(sts): ++ self.returncode = -_del_safe.WSTOPSIG(sts) + else: +- self.returncode = _waitstatus_to_exitcode(sts) ++ self.returncode = _del_safe.waitstatus_to_exitcode(sts) + +- def _internal_poll(self, _deadstate=None, _waitpid=_waitpid, +- _WNOHANG=_WNOHANG, _ECHILD=errno.ECHILD): ++ def _internal_poll(self, _deadstate=None, _del_safe=_del_safe): + """Check if child process has terminated. Returns returncode + attribute. + +@@ -1987,13 +1987,13 @@ + try: + if self.returncode is not None: + return self.returncode # Another thread waited. +- pid, sts = _waitpid(self.pid, _WNOHANG) ++ pid, sts = _del_safe.waitpid(self.pid, _del_safe.WNOHANG) + if pid == self.pid: + self._handle_exitstatus(sts) + except OSError as e: + if _deadstate is not None: + self.returncode = _deadstate +- elif e.errno == _ECHILD: ++ elif e.errno == _del_safe.ECHILD: + # This happens if SIGCLD is set to be ignored or + # waiting for child processes has otherwise been + # disabled for our process. This child is dead, we +diff --git a/Lib/sysconfig.py b/Lib/sysconfig.py +index acc8d4d1826..7d4ebe3ccfa 100644 +--- a/Lib/sysconfig.py ++++ b/Lib/sysconfig.py +@@ -21,6 +21,7 @@ + + # Keys for get_config_var() that are never converted to Python integers. + _ALWAYS_STR = { ++ 'IPHONEOS_DEPLOYMENT_TARGET', + 'MACOSX_DEPLOYMENT_TARGET', + } + +@@ -57,6 +58,7 @@ + 'scripts': '{base}/Scripts', + 'data': '{base}', + }, + -+It should be used as a target for `--enable-framework` when compiling a tvOS -+on-device build for testing purposes. -diff --git a/Apple/testbed/Python.xcframework/tvos-arm64_x86_64-simulator/README b/Apple/testbed/Python.xcframework/tvos-arm64_x86_64-simulator/README -new file mode 100644 -index 00000000000..f8163468d8c ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/tvos-arm64_x86_64-simulator/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. + # Downstream distributors can overwrite the default install scheme. + # This is done to support downstream modifications where distributors change + # the installation layout (eg. different site-packages directory). +@@ -112,8 +114,8 @@ + if env_base: + return env_base + +- # Emscripten, VxWorks, and WASI have no home directories +- if sys.platform in {"emscripten", "vxworks", "wasi"}: ++ # Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories ++ if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}: + return None + + def joinuser(*args): +@@ -299,6 +301,7 @@ + 'home': 'posix_home', + 'user': 'osx_framework_user', + } + -+It should be used as a target for `--enable-framework` when compiling a tvOS -+simulator build for testing purposes (either x86_64 or ARM64). -diff --git a/Apple/testbed/Python.xcframework/watchos-arm64_32/README b/Apple/testbed/Python.xcframework/watchos-arm64_32/README -new file mode 100644 -index 00000000000..696af231df3 ---- /dev/null -+++ b/Apple/testbed/Python.xcframework/watchos-arm64_32/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. + return { + 'prefix': 'posix_prefix', + 'home': 'posix_home', +@@ -831,10 +834,23 @@ + if m: + release = m.group() + elif osname[:6] == "darwin": +- import _osx_support +- osname, release, machine = _osx_support.get_platform_osx( +- get_config_vars(), +- osname, release, machine) ++ if sys.platform == "ios": ++ release = get_config_vars().get("IPHONEOS_DEPLOYMENT_TARGET", "13.0") ++ osname = sys.platform ++ machine = sys.implementation._multiarch ++ elif sys.platform == "tvos": ++ release = get_config_vars().get("TVOS_DEPLOYMENT_TARGET", "9.0") ++ osname = sys.platform ++ machine = sys.implementation._multiarch ++ elif sys.platform == "watchos": ++ release = get_config_vars().get("WATCHOS_DEPLOYMENT_TARGET", "4.0") ++ osname = sys.platform ++ machine = sys.implementation._multiarch ++ else: ++ import _osx_support ++ osname, release, machine = _osx_support.get_platform_osx( ++ get_config_vars(), ++ osname, release, machine) + + return f"{osname}-{release}-{machine}" + +diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py +index 5361475a14f..4a1f280566c 100644 +--- a/Lib/test/pythoninfo.py ++++ b/Lib/test/pythoninfo.py +@@ -287,6 +287,7 @@ + "HOMEDRIVE", + "HOMEPATH", + "IDLESTARTUP", ++ "IPHONEOS_DEPLOYMENT_TARGET", + "LANG", + "LDFLAGS", + "LDSHARED", +diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py +index 1ee8ffc1b7a..c6aab6f728b 100644 +--- a/Lib/test/support/__init__.py ++++ b/Lib/test/support/__init__.py +@@ -59,6 +59,7 @@ + "Py_DEBUG", "EXCEEDS_RECURSION_LIMIT", "C_RECURSION_LIMIT", + "skip_on_s390x", + "BrokenIter", ++ "on_github_actions" + ] + + +@@ -1238,6 +1239,8 @@ + opcode.ENABLE_SPECIALIZATION, "requires specialization")(test) + + ++on_github_actions = "GITHUB_ACTIONS" in os.environ + -+It should be used as a target for `--enable-framework` when compiling a watchOS on-device -+build for testing purposes. -diff --git a/Apple/testbed/Python.xcframework/watchos-arm64_x86_64-simulator/README b/Apple/testbed/Python.xcframework/watchos-arm64_x86_64-simulator/README + #======================================================================= + # Check for the presence of docstrings. + +diff --git a/Lib/test/support/os_helper.py b/Lib/test/support/os_helper.py +index 9eb0af1d285..645fbbefbb9 100644 +--- a/Lib/test/support/os_helper.py ++++ b/Lib/test/support/os_helper.py +@@ -22,8 +22,8 @@ + + # TESTFN_UNICODE is a non-ascii filename + TESTFN_UNICODE = TESTFN_ASCII + "-\xe0\xf2\u0258\u0141\u011f" +-if sys.platform == 'darwin': +- # In Mac OS X's VFS API file names are, by definition, canonically ++if support.is_apple: ++ # On Apple's VFS API file names are, by definition, canonically + # decomposed Unicode, encoded using UTF-8. See QA1173: + # http://developer.apple.com/mac/library/qa/qa2001/qa1173.html + import unicodedata +@@ -48,8 +48,8 @@ + 'encoding (%s). Unicode filename tests may not be effective' + % (TESTFN_UNENCODABLE, sys.getfilesystemencoding())) + TESTFN_UNENCODABLE = None +-# macOS and Emscripten deny unencodable filenames (invalid utf-8) +-elif sys.platform not in {'darwin', 'emscripten', 'wasi'}: ++# Apple and Emscripten deny unencodable filenames (invalid utf-8) ++elif not support.is_apple and sys.platform not in {"emscripten", "wasi"}: + try: + # ascii and utf-8 cannot encode the byte 0xff + b'\xff'.decode(sys.getfilesystemencoding()) +@@ -633,7 +633,8 @@ + if hasattr(os, 'sysconf'): + try: + MAXFD = os.sysconf("SC_OPEN_MAX") +- except OSError: ++ except (OSError, ValueError): ++ # gh-118201: ValueError is raised intermittently on iOS + pass + + old_modes = None +diff --git a/Lib/test/test_apple.py b/Lib/test/test_apple.py new file mode 100644 -index 00000000000..d38e1e98276 +index 00000000000..f14db75e2f2 --- /dev/null -+++ b/Apple/testbed/Python.xcframework/watchos-arm64_x86_64-simulator/README -@@ -0,0 +1,4 @@ -+This directory is intentionally empty. ++++ b/Lib/test/test_apple.py +@@ -0,0 +1,155 @@ ++import unittest ++from _apple_support import SystemLog ++from test.support import is_apple_mobile ++from unittest.mock import Mock, call + -+It should be used as a target for `--enable-framework` when compiling a watchOS -+simulator build for testing purposes (either x86_64 or ARM64). -diff --git a/Apple/testbed/Testbed.lldbinit b/Apple/testbed/Testbed.lldbinit -new file mode 100644 -index 00000000000..4cf00dd0f9d ---- /dev/null -+++ b/Apple/testbed/Testbed.lldbinit -@@ -0,0 +1,4 @@ -+process handle SIGINT -n true -p true -s false -+process handle SIGUSR1 -n true -p true -s false -+process handle SIGUSR2 -n true -p true -s false -+process handle SIGXFSZ -n true -p true -s false -diff --git a/Apple/testbed/TestbedTests/TestbedTests.m b/Apple/testbed/TestbedTests/TestbedTests.m -new file mode 100644 -index 00000000000..cc0d9224042 ---- /dev/null -+++ b/Apple/testbed/TestbedTests/TestbedTests.m -@@ -0,0 +1,198 @@ -+#import -+#import ++if not is_apple_mobile: ++ raise unittest.SkipTest("iOS-specific") + -+@interface TestbedTests : XCTestCase + -+@end ++# Test redirection of stdout and stderr to the Apple system log. ++class TestAppleSystemLogOutput(unittest.TestCase): ++ maxDiff = None + -+@implementation TestbedTests ++ def assert_writes(self, output): ++ self.assertEqual( ++ self.log_write.mock_calls, ++ [ ++ call(self.log_level, line) ++ for line in output ++ ] ++ ) + ++ self.log_write.reset_mock() + -+- (void)testPython { -+ const char **argv; -+ int exit_code; -+ int failed; -+ PyStatus status; -+ PyPreConfig preconfig; -+ PyConfig config; -+ PyObject *app_packages_path; -+ PyObject *method_args; -+ PyObject *result; -+ PyObject *site_module; -+ PyObject *site_addsitedir_attr; -+ PyObject *sys_module; -+ PyObject *sys_path_attr; -+ NSArray *test_args; -+ NSString *python_home; -+ NSString *path; -+ wchar_t *wtmp_str; ++ def setUp(self): ++ self.log_write = Mock() ++ self.log_level = 42 ++ self.log = SystemLog(self.log_write, self.log_level, errors="replace") + -+ NSString *resourcePath = [[NSBundle mainBundle] resourcePath]; ++ def test_repr(self): ++ self.assertEqual(repr(self.log), "") ++ self.assertEqual(repr(self.log.buffer), "") + -+ // Set some other common environment indicators to disable color, as the -+ // Xcode log can't display color. Stdout will report that it is *not* a -+ // TTY. -+ setenv("NO_COLOR", "1", true); -+ setenv("PYTHON_COLORS", "0", true); ++ def test_log_config(self): ++ self.assertIs(self.log.writable(), True) ++ self.assertIs(self.log.readable(), False) + -+ if (getenv("GITHUB_ACTIONS")) { -+ NSLog(@"Running in a GitHub Actions environment"); -+ } -+ // Arguments to pass into the test suite runner. -+ // argv[0] must identify the process; any subsequent arg -+ // will be handled as if it were an argument to `python -m test` -+ // The processInfo arguments contain the binary that is running, -+ // followed by the arguments defined in the test plan. This means: -+ // run_module = test_args[1] -+ // argv = ["Testbed"] + test_args[2:] -+ test_args = [[NSProcessInfo processInfo] arguments]; -+ if (test_args == NULL) { -+ NSLog(@"Unable to identify test arguments."); -+ } -+ NSLog(@"Test arguments: %@", test_args); -+ argv = malloc(sizeof(char *) * ([test_args count] - 1)); -+ argv[0] = "Testbed"; -+ for (int i = 1; i < [test_args count] - 1; i++) { -+ argv[i] = [[test_args objectAtIndex:i+1] UTF8String]; -+ } ++ self.assertEqual("UTF-8", self.log.encoding) ++ self.assertEqual("replace", self.log.errors) + -+ // Generate an isolated Python configuration. -+ NSLog(@"Configuring isolated Python..."); -+ PyPreConfig_InitIsolatedConfig(&preconfig); -+ PyConfig_InitIsolatedConfig(&config); ++ self.assertIs(self.log.line_buffering, True) ++ self.assertIs(self.log.write_through, False) + -+ // Configure the Python interpreter: -+ // Enforce UTF-8 encoding for stderr, stdout, file-system encoding and locale. -+ // See https://docs.python.org/3/library/os.html#python-utf-8-mode. -+ preconfig.utf8_mode = 1; -+ // Don't buffer stdio. We want output to appears in the log immediately -+ config.buffered_stdio = 0; -+ // Don't write bytecode; we can't modify the app bundle -+ // after it has been signed. -+ config.write_bytecode = 0; -+ // Ensure that signal handlers are installed -+ config.install_signal_handlers = 1; -+ // Run the test module. -+ config.run_module = Py_DecodeLocale([[test_args objectAtIndex:1] UTF8String], NULL); -+ // For debugging - enable verbose mode. -+ // config.verbose = 1; ++ def test_empty_str(self): ++ self.log.write("") ++ self.log.flush() + -+ NSLog(@"Pre-initializing Python runtime..."); -+ status = Py_PreInitialize(&preconfig); -+ if (PyStatus_Exception(status)) { -+ XCTFail(@"Unable to pre-initialize Python interpreter: %s", status.err_msg); -+ PyConfig_Clear(&config); -+ return; -+ } -+ -+ // Set the home for the Python interpreter -+ python_home = [NSString stringWithFormat:@"%@/python", resourcePath, nil]; -+ NSLog(@"PythonHome: %@", python_home); -+ wtmp_str = Py_DecodeLocale([python_home UTF8String], NULL); -+ status = PyConfig_SetString(&config, &config.home, wtmp_str); -+ if (PyStatus_Exception(status)) { -+ XCTFail(@"Unable to set PYTHONHOME: %s", status.err_msg); -+ PyConfig_Clear(&config); -+ return; -+ } -+ PyMem_RawFree(wtmp_str); -+ -+ // Read the site config -+ status = PyConfig_Read(&config); -+ if (PyStatus_Exception(status)) { -+ XCTFail(@"Unable to read site config: %s", status.err_msg); -+ PyConfig_Clear(&config); -+ return; -+ } -+ -+ NSLog(@"Configure argc/argv..."); -+ status = PyConfig_SetBytesArgv(&config, [test_args count] - 1, (char**) argv); -+ if (PyStatus_Exception(status)) { -+ XCTFail(@"Unable to configure argc/argv: %s", status.err_msg); -+ PyConfig_Clear(&config); -+ return; -+ } -+ -+ NSLog(@"Initializing Python runtime..."); -+ status = Py_InitializeFromConfig(&config); -+ if (PyStatus_Exception(status)) { -+ XCTFail(@"Unable to initialize Python interpreter: %s", status.err_msg); -+ PyConfig_Clear(&config); -+ return; -+ } -+ -+ // Add app_packages as a site directory. This both adds to sys.path, -+ // and ensures that any .pth files in that directory will be executed. -+ site_module = PyImport_ImportModule("site"); -+ if (site_module == NULL) { -+ XCTFail(@"Could not import site module"); -+ return; -+ } ++ self.assert_writes([]) + -+ site_addsitedir_attr = PyObject_GetAttrString(site_module, "addsitedir"); -+ if (site_addsitedir_attr == NULL || !PyCallable_Check(site_addsitedir_attr)) { -+ XCTFail(@"Could not access site.addsitedir"); -+ return; -+ } ++ def test_simple_str(self): ++ self.log.write("hello world\n") + -+ path = [NSString stringWithFormat:@"%@/app_packages", resourcePath, nil]; -+ NSLog(@"App packages path: %@", path); -+ wtmp_str = Py_DecodeLocale([path UTF8String], NULL); -+ app_packages_path = PyUnicode_FromWideChar(wtmp_str, wcslen(wtmp_str)); -+ if (app_packages_path == NULL) { -+ XCTFail(@"Could not convert app_packages path to unicode"); -+ return; -+ } -+ PyMem_RawFree(wtmp_str); ++ self.assert_writes([b"hello world\n"]) + -+ method_args = Py_BuildValue("(O)", app_packages_path); -+ if (method_args == NULL) { -+ XCTFail(@"Could not create arguments for site.addsitedir"); -+ return; -+ } ++ def test_buffered_str(self): ++ self.log.write("h") ++ self.log.write("ello") ++ self.log.write(" ") ++ self.log.write("world\n") ++ self.log.write("goodbye.") ++ self.log.flush() + -+ result = PyObject_CallObject(site_addsitedir_attr, method_args); -+ if (result == NULL) { -+ XCTFail(@"Could not add app_packages directory using site.addsitedir"); -+ return; -+ } ++ self.assert_writes([b"hello world\n", b"goodbye."]) + -+ // Add test code to sys.path -+ sys_module = PyImport_ImportModule("sys"); -+ if (sys_module == NULL) { -+ XCTFail(@"Could not import sys module"); -+ return; -+ } ++ def test_manual_flush(self): ++ self.log.write("Hello") + -+ sys_path_attr = PyObject_GetAttrString(sys_module, "path"); -+ if (sys_path_attr == NULL) { -+ XCTFail(@"Could not access sys.path"); -+ return; -+ } ++ self.assert_writes([]) + -+ path = [NSString stringWithFormat:@"%@/app", resourcePath, nil]; -+ NSLog(@"App path: %@", path); -+ wtmp_str = Py_DecodeLocale([path UTF8String], NULL); -+ failed = PyList_Insert(sys_path_attr, 0, PyUnicode_FromString([path UTF8String])); -+ if (failed) { -+ XCTFail(@"Unable to add app to sys.path"); -+ return; -+ } -+ PyMem_RawFree(wtmp_str); ++ self.log.write(" world\nHere for a while...\nGoodbye") ++ self.assert_writes([b"Hello world\n", b"Here for a while...\n"]) + -+ // Ensure the working directory is the app folder. -+ chdir([path UTF8String]); ++ self.log.write(" world\nHello again") ++ self.assert_writes([b"Goodbye world\n"]) + -+ // Start the test suite. Print a separator to differentiate Python startup logs from app logs -+ NSLog(@"---------------------------------------------------------------------------"); ++ self.log.flush() ++ self.assert_writes([b"Hello again"]) + -+ exit_code = Py_RunMain(); -+ XCTAssertEqual(exit_code, 0, @"Test suite did not pass"); ++ def test_non_ascii(self): ++ # Spanish ++ self.log.write("ol\u00e9\n") ++ self.assert_writes([b"ol\xc3\xa9\n"]) + -+ NSLog(@"---------------------------------------------------------------------------"); ++ # Chinese ++ self.log.write("\u4e2d\u6587\n") ++ self.assert_writes([b"\xe4\xb8\xad\xe6\x96\x87\n"]) + -+ Py_Finalize(); -+} ++ # Printing Non-BMP emoji ++ self.log.write("\U0001f600\n") ++ self.assert_writes([b"\xf0\x9f\x98\x80\n"]) + ++ # Non-encodable surrogates are replaced ++ self.log.write("\ud800\udc00\n") ++ self.assert_writes([b"??\n"]) + -+@end -diff --git a/Apple/testbed/__main__.py b/Apple/testbed/__main__.py -new file mode 100644 -index 00000000000..643034e9bca ---- /dev/null -+++ b/Apple/testbed/__main__.py -@@ -0,0 +1,462 @@ -+import argparse -+import json -+import os -+import re -+import shlex -+import shutil -+import subprocess -+import sys -+from pathlib import Path ++ def test_modified_null(self): ++ # Null characters are logged using "modified UTF-8". ++ self.log.write("\u0000\n") ++ self.assert_writes([b"\xc0\x80\n"]) ++ self.log.write("a\u0000\n") ++ self.assert_writes([b"a\xc0\x80\n"]) ++ self.log.write("\u0000b\n") ++ self.assert_writes([b"\xc0\x80b\n"]) ++ self.log.write("a\u0000b\n") ++ self.assert_writes([b"a\xc0\x80b\n"]) + -+TEST_SLICES = { -+ "iOS": "ios-arm64_x86_64-simulator", -+ "tvOS": "tvos-arm64_x86_64-simulator", -+ "visionOS": "xros-arm64-simulator", -+ "watchOS": "watchos-arm64_x86_64-simulator", -+} ++ def test_nonstandard_str(self): ++ # String subclasses are accepted, but they should be converted ++ # to a standard str without calling any of their methods. ++ class CustomStr(str): ++ def splitlines(self, *args, **kwargs): ++ raise AssertionError() + -+DECODE_ARGS = ("UTF-8", "backslashreplace") ++ def __len__(self): ++ raise AssertionError() + -+# The system log prefixes each line: -+# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ... -+# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ... ++ def __str__(self): ++ raise AssertionError() + -+LOG_PREFIX_REGEX = re.compile( -+ r"^\d{4}-\d{2}-\d{2}" # YYYY-MM-DD -+ r"\s+\d+:\d{2}:\d{2}\.\d+\+\d{4}" # HH:MM:SS.ssssss+ZZZZ -+ r"\s+.*Testbed\[\d+:\w+\] " # Process/thread ID -+) ++ self.log.write(CustomStr("custom\n")) ++ self.assert_writes([b"custom\n"]) + ++ def test_non_str(self): ++ # Non-string classes are not accepted. ++ for obj in [b"", b"hello", None, 42]: ++ with self.subTest(obj=obj): ++ with self.assertRaisesRegex( ++ TypeError, ++ fr"write\(\) argument must be str, not " ++ fr"{type(obj).__name__}" ++ ): ++ self.log.write(obj) + -+# Select a simulator device to use. -+def select_simulator_device(platform): -+ # List the testing simulators, in JSON format -+ raw_json = subprocess.check_output(["xcrun", "simctl", "list", "-j"]) -+ json_data = json.loads(raw_json) ++ def test_byteslike_in_buffer(self): ++ # The underlying buffer *can* accept bytes-like objects ++ self.log.buffer.write(bytearray(b"hello")) ++ self.log.flush() + -+ if platform == "iOS": -+ # Any iOS device will do; we'll look for "SE" devices - but the name -+ # isn't consistent over time. Older Xcode versions will use "iPhone SE -+ # (Nth generation)"; As of 2025, they've started using "iPhone 16e". -+ # -+ # When Xcode is updated after a new release, new devices will be -+ # available and old ones will be dropped from the set available on the -+ # latest iOS version. Select the one with the highest minimum runtime -+ # version - this is an indicator of the "newest" released device, which -+ # should always be supported on the "most recent" iOS version. -+ se_simulators = sorted( -+ (devicetype["minRuntimeVersion"], devicetype["name"]) -+ for devicetype in json_data["devicetypes"] -+ if devicetype["productFamily"] == "iPhone" -+ and ( -+ ( -+ "iPhone " in devicetype["name"] -+ and devicetype["name"].endswith("e") -+ ) -+ or "iPhone SE " in devicetype["name"] -+ ) -+ ) -+ simulator = se_simulators[-1][1] -+ elif platform == "tvOS": -+ # Find the most recent tvOS release. -+ simulators = sorted( -+ (devicetype["minRuntimeVersion"], devicetype["name"]) -+ for devicetype in json_data["devicetypes"] -+ if devicetype["productFamily"] == "Apple TV" -+ ) -+ simulator = simulators[-1][1] -+ elif platform == "visionOS": -+ # Find the most recent visionOS release. -+ simulators = sorted( -+ (devicetype["minRuntimeVersion"], devicetype["name"]) -+ for devicetype in json_data["devicetypes"] -+ if devicetype["productFamily"] == "Apple Vision" -+ ) -+ simulator = simulators[-1][1] -+ elif platform == "watchOS": -+ raise NotImplementedError("Don't know how to launch watchOS (yet)") -+ else: -+ raise ValueError(f"Unknown platform {platform}") ++ self.log.buffer.write(b"") ++ self.log.flush() + -+ return simulator ++ self.log.buffer.write(b"goodbye") ++ self.log.flush() + ++ self.assert_writes([b"hello", b"goodbye"]) + -+def xcode_test(location: Path, platform: str, simulator: str, verbose: bool): -+ # Build and run the test suite on the named simulator. -+ args = [ -+ "-project", -+ str(location / f"{platform}Testbed.xcodeproj"), -+ "-scheme", -+ f"{platform}Testbed", -+ "-destination", -+ f"platform={platform} Simulator,name={simulator}", -+ "-derivedDataPath", -+ str(location / "DerivedData"), -+ ] -+ verbosity_args = [] if verbose else ["-quiet"] -+ -+ print("Building test project...") -+ subprocess.run( -+ ["xcodebuild", "build-for-testing"] + args + verbosity_args, -+ check=True, -+ ) -+ -+ # Any environment variable prefixed with TEST_RUNNER_ is exposed into the -+ # test runner environment. There are some variables (like those identifying -+ # CI platforms) that can be useful to have access to. -+ test_env = os.environ.copy() -+ if "GITHUB_ACTIONS" in os.environ: -+ test_env["TEST_RUNNER_GITHUB_ACTIONS"] = os.environ["GITHUB_ACTIONS"] -+ -+ print("Running test project...") -+ # Test execution *can't* be run -quiet; verbose mode -+ # is how we see the output of the test output. -+ process = subprocess.Popen( -+ ["xcodebuild", "test-without-building"] + args, -+ stdout=subprocess.PIPE, -+ stderr=subprocess.STDOUT, -+ env=test_env, -+ ) -+ while line := (process.stdout.readline()).decode(*DECODE_ARGS): -+ # Strip the timestamp/process prefix from each log line -+ line = LOG_PREFIX_REGEX.sub("", line) -+ sys.stdout.write(line) -+ sys.stdout.flush() -+ -+ status = process.wait(timeout=5) -+ exit(status) -+ -+ -+def copy(src, tgt): -+ """An all-purpose copy. -+ -+ If src is a file, it is copied. If src is a symlink, it is copied *as a -+ symlink*. If src is a directory, the full tree is duplicated, with symlinks -+ being preserved. -+ """ -+ if src.is_file() or src.is_symlink(): -+ shutil.copyfile(src, tgt, follow_symlinks=False) -+ else: -+ shutil.copytree(src, tgt, symlinks=True) -+ ++ def test_non_byteslike_in_buffer(self): ++ for obj in ["hello", None, 42]: ++ with self.subTest(obj=obj): ++ with self.assertRaisesRegex( ++ TypeError, ++ fr"write\(\) argument must be bytes-like, not " ++ fr"{type(obj).__name__}" ++ ): ++ self.log.buffer.write(obj) +diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py +index abf425f5ef0..ffcde82b63e 100644 +--- a/Lib/test/test_asyncio/test_events.py ++++ b/Lib/test/test_asyncio/test_events.py +@@ -1894,6 +1894,7 @@ + else: + self.assertEqual(-signal.SIGKILL, returncode) + ++ @support.requires_subprocess() + def test_subprocess_exec(self): + prog = os.path.join(os.path.dirname(__file__), 'echo.py') + +@@ -1915,6 +1916,7 @@ + self.check_killed(proto.returncode) + self.assertEqual(b'Python The Winner', proto.data[1]) + ++ @support.requires_subprocess() + def test_subprocess_interactive(self): + prog = os.path.join(os.path.dirname(__file__), 'echo.py') + +@@ -1942,6 +1944,7 @@ + self.loop.run_until_complete(proto.completed) + self.check_killed(proto.returncode) + ++ @support.requires_subprocess() + def test_subprocess_shell(self): + connect = self.loop.subprocess_shell( + functools.partial(MySubprocessProtocol, self.loop), +@@ -1958,6 +1961,7 @@ + self.assertEqual(proto.data[2], b'') + transp.close() + ++ @support.requires_subprocess() + def test_subprocess_exitcode(self): + connect = self.loop.subprocess_shell( + functools.partial(MySubprocessProtocol, self.loop), +@@ -1969,6 +1973,7 @@ + self.assertEqual(7, proto.returncode) + transp.close() + ++ @support.requires_subprocess() + def test_subprocess_close_after_finish(self): + connect = self.loop.subprocess_shell( + functools.partial(MySubprocessProtocol, self.loop), +@@ -1983,6 +1988,7 @@ + self.assertEqual(7, proto.returncode) + self.assertIsNone(transp.close()) + ++ @support.requires_subprocess() + def test_subprocess_kill(self): + prog = os.path.join(os.path.dirname(__file__), 'echo.py') + +@@ -1999,6 +2005,7 @@ + self.check_killed(proto.returncode) + transp.close() + ++ @support.requires_subprocess() + def test_subprocess_terminate(self): + prog = os.path.join(os.path.dirname(__file__), 'echo.py') + +@@ -2016,6 +2023,7 @@ + transp.close() + + @unittest.skipIf(sys.platform == 'win32', "Don't have SIGHUP") ++ @support.requires_subprocess() + def test_subprocess_send_signal(self): + # bpo-31034: Make sure that we get the default signal handler (killing + # the process). The parent process may have decided to ignore SIGHUP, +@@ -2040,6 +2048,7 @@ + finally: + signal.signal(signal.SIGHUP, old_handler) + ++ @support.requires_subprocess() + def test_subprocess_stderr(self): + prog = os.path.join(os.path.dirname(__file__), 'echo2.py') + +@@ -2061,6 +2070,7 @@ + self.assertTrue(proto.data[2].startswith(b'ERR:test'), proto.data[2]) + self.assertEqual(0, proto.returncode) + ++ @support.requires_subprocess() + def test_subprocess_stderr_redirect_to_stdout(self): + prog = os.path.join(os.path.dirname(__file__), 'echo2.py') + +@@ -2086,6 +2096,7 @@ + transp.close() + self.assertEqual(0, proto.returncode) + ++ @support.requires_subprocess() + def test_subprocess_close_client_stream(self): + prog = os.path.join(os.path.dirname(__file__), 'echo3.py') + +@@ -2120,6 +2131,7 @@ + self.loop.run_until_complete(proto.completed) + self.check_killed(proto.returncode) + ++ @support.requires_subprocess() + def test_subprocess_wait_no_same_group(self): + # start the new process in a new session + connect = self.loop.subprocess_shell( +@@ -2132,6 +2144,7 @@ + self.assertEqual(7, proto.returncode) + transp.close() + ++ @support.requires_subprocess() + def test_subprocess_exec_invalid_args(self): + async def connect(**kwds): + await self.loop.subprocess_exec( +@@ -2145,6 +2158,7 @@ + with self.assertRaises(ValueError): + self.loop.run_until_complete(connect(shell=True)) + ++ @support.requires_subprocess() + def test_subprocess_shell_invalid_args(self): + + async def connect(cmd=None, **kwds): +diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py +index 686fef8377f..4f59c31dfe4 100644 +--- a/Lib/test/test_asyncio/test_streams.py ++++ b/Lib/test/test_asyncio/test_streams.py +@@ -10,7 +10,6 @@ + import unittest + from unittest import mock + import warnings +-from test.support import socket_helper + try: + import ssl + except ImportError: +@@ -18,6 +17,7 @@ + + import asyncio + from test.test_asyncio import utils as test_utils ++from test.support import requires_subprocess, socket_helper + + + def tearDownModule(): +@@ -770,6 +770,7 @@ + self.assertEqual(msg2, b"hello world 2!\n") + + @unittest.skipIf(sys.platform == 'win32', "Don't have pipes") ++ @requires_subprocess() + def test_read_all_from_pipe_reader(self): + # See asyncio issue 168. This test is derived from the example + # subprocess_attach_read_pipe.py, but we configure the +diff --git a/Lib/test/test_asyncio/test_subprocess.py b/Lib/test/test_asyncio/test_subprocess.py +index 859d2932c33..ed43895fd68 100644 +--- a/Lib/test/test_asyncio/test_subprocess.py ++++ b/Lib/test/test_asyncio/test_subprocess.py +@@ -47,6 +47,7 @@ + self._proc.pid = -1 + + ++@support.requires_subprocess() + class SubprocessTransportTests(test_utils.TestCase): + def setUp(self): + super().setUp() +@@ -110,6 +111,7 @@ + transport.close() + + ++@support.requires_subprocess() + class SubprocessMixin: + + def test_stdin_stdout(self): +diff --git a/Lib/test/test_asyncio/test_unix_events.py b/Lib/test/test_asyncio/test_unix_events.py +index 35c924a0cd6..9452213c685 100644 +--- a/Lib/test/test_asyncio/test_unix_events.py ++++ b/Lib/test/test_asyncio/test_unix_events.py +@@ -1873,7 +1873,7 @@ + wsock.close() + + +-@unittest.skipUnless(hasattr(os, 'fork'), 'requires os.fork()') ++@support.requires_fork() + class TestFork(unittest.IsolatedAsyncioTestCase): + + async def test_fork_not_share_event_loop(self): +diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py +index f284e665d6f..90d9cdd45c0 100644 +--- a/Lib/test/test_capi/test_misc.py ++++ b/Lib/test/test_capi/test_misc.py +@@ -1949,6 +1949,13 @@ + self.addCleanup(os.close, r) + self.addCleanup(os.close, w) + ++ # Apple extensions must be distributed as frameworks. This requires ++ # a specialist loader. ++ if support.is_apple_mobile: ++ loader = "AppleFrameworkLoader" ++ else: ++ loader = "ExtensionFileLoader" + -+def clone_testbed( -+ source: Path, -+ target: Path, -+ framework: Path, -+ platform: str, -+ apps: list[Path], -+) -> None: -+ if target.exists(): -+ print(f"{target} already exists; aborting without creating project.") -+ sys.exit(10) -+ -+ if framework is None: -+ if not ( -+ source / "Python.xcframework" / TEST_SLICES[platform] / "bin" -+ ).is_dir(): -+ print( -+ f"The testbed being cloned ({source}) does not contain " -+ "a framework with slices. Re-run with --framework" -+ ) -+ sys.exit(11) -+ else: -+ if not framework.is_dir(): -+ print(f"{framework} does not exist.") -+ sys.exit(12) -+ elif not ( -+ framework.suffix == ".xcframework" -+ or (framework / "Python.framework").is_dir() -+ ): -+ print( -+ f"{framework} is not an XCframework, " -+ f"or a simulator slice of a framework build." -+ ) -+ sys.exit(13) -+ -+ print("Cloning testbed project:") -+ print(f" Cloning {source}...", end="") -+ # Only copy the files for the platform being cloned plus the files common -+ # to all platforms. The XCframework will be copied later, if needed. -+ target.mkdir(parents=True) -+ -+ for name in [ -+ "__main__.py", -+ "TestbedTests", -+ "Testbed.lldbinit", -+ f"{platform}Testbed", -+ f"{platform}Testbed.xcodeproj", -+ f"{platform}Testbed.xctestplan", -+ ]: -+ copy(source / name, target / name) -+ -+ print(" done") -+ -+ orig_xc_framework_path = source / "Python.xcframework" -+ xc_framework_path = target / "Python.xcframework" -+ test_framework_path = xc_framework_path / TEST_SLICES[platform] -+ if framework is not None: -+ if framework.suffix == ".xcframework": -+ print(" Installing XCFramework...", end="") -+ xc_framework_path.symlink_to( -+ framework.relative_to(xc_framework_path.parent, walk_up=True) -+ ) -+ print(" done") + script = textwrap.dedent(f""" + import importlib.machinery + import importlib.util +@@ -1956,7 +1963,7 @@ + + fullname = '_test_module_state_shared' + origin = importlib.util.find_spec('_testmultiphase').origin +- loader = importlib.machinery.ExtensionFileLoader(fullname, origin) ++ loader = importlib.machinery.{loader}(fullname, origin) + spec = importlib.util.spec_from_loader(fullname, loader) + module = importlib.util.module_from_spec(spec) + attr_id = str(id(module.Error)).encode() +@@ -2130,7 +2137,12 @@ + def setUp(self): + fullname = '_testmultiphase_meth_state_access' # XXX + origin = importlib.util.find_spec('_testmultiphase').origin +- loader = importlib.machinery.ExtensionFileLoader(fullname, origin) ++ # Apple extensions must be distributed as frameworks. This requires ++ # a specialist loader. ++ if support.is_apple_mobile: ++ loader = importlib.machinery.AppleFrameworkLoader(fullname, origin) + else: -+ print(" Installing simulator framework...", end="") -+ # We're only installing a slice of a framework; we need -+ # to do a full tree copy to make sure we don't damage -+ # symlinked content. -+ shutil.copytree(orig_xc_framework_path, xc_framework_path) -+ if test_framework_path.is_dir(): -+ shutil.rmtree(test_framework_path) -+ else: -+ test_framework_path.unlink(missing_ok=True) -+ test_framework_path.symlink_to( -+ framework.relative_to(test_framework_path.parent, walk_up=True) -+ ) -+ print(" done") -+ else: -+ copy(orig_xc_framework_path, xc_framework_path) -+ ++ loader = importlib.machinery.ExtensionFileLoader(fullname, origin) + spec = importlib.util.spec_from_loader(fullname, loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) +diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py +index 7109e3d164e..6f7e102c2ff 100644 +--- a/Lib/test/test_cmd_line_script.py ++++ b/Lib/test/test_cmd_line_script.py +@@ -14,8 +14,7 @@ + + import textwrap + from test import support +-from test.support import import_helper +-from test.support import os_helper ++from test.support import import_helper, is_apple, os_helper + from test.support.script_helper import ( + make_pkg, make_script, make_zip_pkg, make_zip_script, + assert_python_ok, assert_python_failure, spawn_python, kill_python) +@@ -555,12 +554,17 @@ + self.assertTrue(text[3].startswith('NameError')) + + def test_non_ascii(self): +- # Mac OS X denies the creation of a file with an invalid UTF-8 name. ++ # Apple platforms deny the creation of a file with an invalid UTF-8 name. + # Windows allows creating a name with an arbitrary bytes name, but + # Python cannot a undecodable bytes argument to a subprocess. +- # WASI does not permit invalid UTF-8 names. +- if (os_helper.TESTFN_UNDECODABLE +- and sys.platform not in ('win32', 'darwin', 'emscripten', 'wasi')): ++ # Emscripten/WASI does not permit invalid UTF-8 names. + if ( -+ xc_framework_path.is_symlink() -+ and not xc_framework_path.readlink().is_absolute() ++ os_helper.TESTFN_UNDECODABLE ++ and sys.platform not in { ++ "win32", "emscripten", "wasi" ++ } ++ and not is_apple + ): -+ # XCFramework is a relative symlink. Rewrite the symlink relative -+ # to the new location. -+ print(" Rewriting symlink to XCframework...", end="") -+ resolved_xc_framework_path = ( -+ source / xc_framework_path.readlink() -+ ).resolve() -+ xc_framework_path.unlink() -+ xc_framework_path.symlink_to( -+ resolved_xc_framework_path.relative_to( -+ xc_framework_path.parent, walk_up=True -+ ) -+ ) -+ print(" done") -+ elif ( -+ test_framework_path.is_symlink() -+ and not test_framework_path.readlink().is_absolute() + name = os.fsdecode(os_helper.TESTFN_UNDECODABLE) + elif os_helper.TESTFN_NONASCII: + name = os_helper.TESTFN_NONASCII +diff --git a/Lib/test/test_concurrent_futures/test_thread_pool.py b/Lib/test/test_concurrent_futures/test_thread_pool.py +index 6e4a4b7caff..d1357ea7fcd 100644 +--- a/Lib/test/test_concurrent_futures/test_thread_pool.py ++++ b/Lib/test/test_concurrent_futures/test_thread_pool.py +@@ -49,6 +49,7 @@ + self.assertEqual(len(executor._threads), 1) + executor.shutdown(wait=True) + ++ @support.requires_fork() + @unittest.skipUnless(hasattr(os, 'register_at_fork'), 'need os.register_at_fork') + @support.requires_resource('cpu') + def test_hang_global_shutdown_lock(self): +diff --git a/Lib/test/test_fcntl.py b/Lib/test/test_fcntl.py +index 203dd6fe57d..6d734d05245 100644 +--- a/Lib/test/test_fcntl.py ++++ b/Lib/test/test_fcntl.py +@@ -6,7 +6,9 @@ + import struct + import sys + import unittest +-from test.support import verbose, cpython_only, get_pagesize ++from test.support import ( ++ cpython_only, get_pagesize, is_apple, requires_subprocess, verbose ++) + from test.support.import_helper import import_module + from test.support.os_helper import TESTFN, unlink + +@@ -56,8 +58,10 @@ + else: + start_len = "qq" + +- if (sys.platform.startswith(('netbsd', 'freebsd', 'openbsd')) +- or sys.platform == 'darwin'): ++ if ( ++ sys.platform.startswith(('netbsd', 'freebsd', 'openbsd')) ++ or is_apple + ): -+ print(" Rewriting symlink to simulator framework...", end="") -+ # Simulator framework is a relative symlink. Rewrite the symlink -+ # relative to the new location. -+ orig_test_framework_path = ( -+ source / "Python.XCframework" / test_framework_path.readlink() -+ ).resolve() -+ test_framework_path.unlink() -+ test_framework_path.symlink_to( -+ orig_test_framework_path.relative_to( -+ test_framework_path.parent, walk_up=True -+ ) -+ ) -+ print(" done") -+ else: -+ print(" Using pre-existing Python framework.") -+ -+ for app_src in apps: -+ print(f" Installing app {app_src.name!r}...", end="") -+ app_target = target / f"Testbed/app/{app_src.name}" -+ if app_target.is_dir(): -+ shutil.rmtree(app_target) -+ shutil.copytree(app_src, app_target) -+ print(" done") -+ -+ print(f"Successfully cloned testbed: {target.resolve()}") -+ -+ -+def update_test_plan(testbed_path, platform, args): -+ # Modify the test plan to use the requested test arguments. -+ test_plan_path = testbed_path / f"{platform}Testbed.xctestplan" -+ with test_plan_path.open("r", encoding="utf-8") as f: -+ test_plan = json.load(f) -+ -+ test_plan["defaultOptions"]["commandLineArgumentEntries"] = [ -+ {"argument": shlex.quote(arg)} for arg in args -+ ] -+ -+ with test_plan_path.open("w", encoding="utf-8") as f: -+ json.dump(test_plan, f, indent=2) -+ -+ -+def run_testbed( -+ platform: str, -+ simulator: str | None, -+ args: list[str], -+ verbose: bool = False, -+): -+ location = Path(__file__).parent -+ print("Updating test plan...", end="") -+ update_test_plan(location, platform, args) -+ print(" done.") -+ -+ # xcodebuild doesn't guarantee that the CoreSimulatorService daemon is -+ # running prior to using a simulator, but calling `xcrun simctl list` does. -+ # Determining the default simulator that *would* be used is a cheap action; -+ # so use that as a way to ensure that the CoreSimulatorService daemon is -+ # running. -+ default_simulator = select_simulator_device(platform) -+ if simulator is None: -+ simulator = default_simulator -+ print(f"Running test on {simulator}") -+ -+ xcode_test( -+ location, -+ platform=platform, -+ simulator=simulator, -+ verbose=verbose, -+ ) -+ -+ -+def main(): -+ # Look for directories like `iOSTestbed` as an indicator of the platforms -+ # that the testbed folder supports. The original source testbed can support -+ # many platforms, but when cloned, only one platform is preserved. -+ available_platforms = [ -+ platform -+ for platform in ["iOS", "tvOS", "visionOS", "watchOS"] -+ if (Path(__file__).parent / f"{platform}Testbed").is_dir() -+ ] -+ -+ parser = argparse.ArgumentParser( -+ description=( -+ "Manages the process of testing an Apple Python project " -+ "through Xcode." -+ ), -+ ) + if struct.calcsize('l') == 8: + off_t = 'l' + pid_t = 'i' +@@ -157,6 +161,7 @@ + self.assertRaises(TypeError, fcntl.flock, 'spam', fcntl.LOCK_SH) + + @unittest.skipIf(platform.system() == "AIX", "AIX returns PermissionError") ++ @requires_subprocess() + def test_lockf_exclusive(self): + self.f = open(TESTFN, 'wb+') + cmd = fcntl.LOCK_EX | fcntl.LOCK_NB +@@ -169,6 +174,7 @@ + self.assertEqual(p.exitcode, 0) + + @unittest.skipIf(platform.system() == "AIX", "AIX returns PermissionError") ++ @requires_subprocess() + def test_lockf_share(self): + self.f = open(TESTFN, 'wb+') + cmd = fcntl.LOCK_SH | fcntl.LOCK_NB +diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py +index 7542f015f78..f1eff9430f7 100644 +--- a/Lib/test/test_ftplib.py ++++ b/Lib/test/test_ftplib.py +@@ -18,6 +18,7 @@ + + from unittest import mock, TestCase, skipUnless + from test import support ++from test.support import requires_subprocess + from test.support import threading_helper + from test.support import socket_helper + from test.support import warnings_helper +@@ -903,6 +904,7 @@ + + + @skipUnless(ssl, "SSL not available") ++@requires_subprocess() + class TestTLS_FTPClassMixin(TestFTPClass): + """Repeat TestFTPClass tests starting the TLS layer for both control + and data connections first. +@@ -919,6 +921,7 @@ + + + @skipUnless(ssl, "SSL not available") ++@requires_subprocess() + class TestTLS_FTPClass(TestCase): + """Specific TLS_FTP class tests.""" + +diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py +index 81bb5bb288e..dddf5e8cd93 100644 +--- a/Lib/test/test_gc.py ++++ b/Lib/test/test_gc.py +@@ -1188,6 +1188,7 @@ + self.assertEqual(len(gc.garbage), 0) + + ++ @requires_subprocess() + @unittest.skipIf(BUILD_WITH_NDEBUG, + 'built with -NDEBUG') + def test_refcount_errors(self): +diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py +index 1cec587e875..e18e84d08cf 100644 +--- a/Lib/test/test_genericpath.py ++++ b/Lib/test/test_genericpath.py +@@ -8,9 +8,9 @@ + import unittest + import warnings + from test import support +-from test.support import is_emscripten +-from test.support import os_helper +-from test.support import warnings_helper ++from test.support import ( ++ is_apple, is_emscripten, os_helper, warnings_helper ++) + from test.support.script_helper import assert_python_ok + from test.support.os_helper import FakePath + +@@ -506,12 +506,16 @@ + self.assertIsInstance(abspath(path), str) + + def test_nonascii_abspath(self): +- if (os_helper.TESTFN_UNDECODABLE +- # macOS and Emscripten deny the creation of a directory with an +- # invalid UTF-8 name. Windows allows creating a directory with an +- # arbitrary bytes name, but fails to enter this directory +- # (when the bytes name is used). +- and sys.platform not in ('win32', 'darwin', 'emscripten', 'wasi')): ++ if ( ++ os_helper.TESTFN_UNDECODABLE ++ # Apple platforms and Emscripten/WASI deny the creation of a ++ # directory with an invalid UTF-8 name. Windows allows creating a ++ # directory with an arbitrary bytes name, but fails to enter this ++ # directory (when the bytes name is used). ++ and sys.platform not in { ++ "win32", "emscripten", "wasi" ++ } and not is_apple ++ ): + name = os_helper.TESTFN_UNDECODABLE + elif os_helper.TESTFN_NONASCII: + name = os_helper.TESTFN_NONASCII +diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py +index 96fc9ca574e..30da4bd4723 100644 +--- a/Lib/test/test_httpservers.py ++++ b/Lib/test/test_httpservers.py +@@ -31,8 +31,9 @@ + + import unittest + from test import support +-from test.support import os_helper +-from test.support import threading_helper ++from test.support import ( ++ is_apple, os_helper, requires_subprocess, threading_helper ++) + + support.requires_working_socket(module=True) + +@@ -411,8 +412,8 @@ + reader.close() + return body + +- @unittest.skipIf(sys.platform == 'darwin', +- 'undecodable name cannot always be decoded on macOS') ++ @unittest.skipIf(is_apple, ++ 'undecodable name cannot always be decoded on Apple platforms') + @unittest.skipIf(sys.platform == 'win32', + 'undecodable name cannot be decoded on win32') + @unittest.skipUnless(os_helper.TESTFN_UNDECODABLE, +@@ -423,11 +424,11 @@ + with open(os.path.join(self.tempdir, filename), 'wb') as f: + f.write(os_helper.TESTFN_UNDECODABLE) + response = self.request(self.base_url + '/') +- if sys.platform == 'darwin': +- # On Mac OS the HFS+ filesystem replaces bytes that aren't valid +- # UTF-8 into a percent-encoded value. ++ if is_apple: ++ # On Apple platforms the HFS+ filesystem replaces bytes that ++ # aren't valid UTF-8 into a percent-encoded value. + for name in os.listdir(self.tempdir): +- if name != 'test': # Ignore a filename created in setUp(). ++ if name != 'test': # Ignore a filename created in setUp(). + filename = name + break + body = self.check_status_and_reason(response, HTTPStatus.OK) +@@ -712,6 +713,7 @@ + + @unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0, + "This test can't be run reliably as root (issue #13308).") ++@requires_subprocess() + class CGIHTTPServerTestCase(BaseTestCase): + class request_handler(NoLogRequestHandler, CGIHTTPRequestHandler): + def run_cgi(self): +diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py +index 1ac756f1b15..a0cb51afcba 100644 +--- a/Lib/test/test_import/__init__.py ++++ b/Lib/test/test_import/__init__.py +@@ -5,7 +5,11 @@ + import importlib.util + from importlib._bootstrap_external import _get_sourcefile + from importlib.machinery import ( +- BuiltinImporter, ExtensionFileLoader, FrozenImporter, SourceFileLoader, ++ AppleFrameworkLoader, ++ BuiltinImporter, ++ ExtensionFileLoader, ++ FrozenImporter, ++ SourceFileLoader, + ) + import marshal + import os +@@ -26,7 +30,7 @@ + + from test.support import os_helper + from test.support import ( +- STDLIB_DIR, swap_attr, swap_item, cpython_only, is_emscripten, ++ STDLIB_DIR, swap_attr, swap_item, cpython_only, is_apple_mobile, is_emscripten, + is_wasi, run_in_subinterp, run_in_subinterp_with_config) + from test.support.import_helper import ( + forget, make_legacy_pyc, unlink, unload, ready_to_import, +@@ -63,6 +67,7 @@ + MODULE_KINDS = { + BuiltinImporter: 'built-in', + ExtensionFileLoader: 'extension', ++ AppleFrameworkLoader: 'framework extension', + FrozenImporter: 'frozen', + SourceFileLoader: 'pure Python', + } +@@ -88,7 +93,12 @@ + assert module.__spec__.origin == 'built-in', module.__spec__ + + def require_extension(module, *, skip=False): +- _require_loader(module, ExtensionFileLoader, skip) ++ # Apple extensions must be distributed as frameworks. This requires ++ # a specialist loader. ++ if is_apple_mobile: ++ _require_loader(module, AppleFrameworkLoader, skip) ++ else: ++ _require_loader(module, ExtensionFileLoader, skip) + + def require_frozen(module, *, skip=True): + module = _require_loader(module, FrozenImporter, skip) +@@ -131,7 +141,8 @@ + # it to its nominal state. + sys.modules.pop('_testsinglephase', None) + _orig._clear_globals() +- _testinternalcapi.clear_extension('_testsinglephase', _orig.__file__) ++ origin = _orig.__spec__.origin ++ _testinternalcapi.clear_extension('_testsinglephase', origin) + import _testsinglephase + + +@@ -354,10 +365,14 @@ + from _testcapi import i_dont_exist + self.assertEqual(cm.exception.name, '_testcapi') + if hasattr(_testcapi, "__file__"): +- self.assertEqual(cm.exception.path, _testcapi.__file__) ++ # The path on the exception is strictly the spec origin, not the ++ # module's __file__. For most cases, these are the same; but on ++ # iOS, the Framework relocation process results in the exception ++ # being raised from the spec location. ++ self.assertEqual(cm.exception.path, _testcapi.__spec__.origin) + self.assertRegex( + str(cm.exception), +- r"cannot import name 'i_dont_exist' from '_testcapi' \(.*\.(so|pyd)\)" ++ r"cannot import name 'i_dont_exist' from '_testcapi' \(.*(\.(so|pyd))?\)" + ) + else: + self.assertEqual( +@@ -1719,6 +1734,14 @@ + os.set_blocking(r, False) + return (r, w) + ++ def create_extension_loader(self, modname, filename): ++ # Apple extensions must be distributed as frameworks. This requires ++ # a specialist loader. ++ if is_apple_mobile: ++ return AppleFrameworkLoader(modname, filename) ++ else: ++ return ExtensionFileLoader(modname, filename) + -+ subcommands = parser.add_subparsers(dest="subcommand") -+ clone = subcommands.add_parser( -+ "clone", -+ description=( -+ "Clone the testbed project, copying in a Python framework and" -+ "any specified application code." -+ ), -+ help="Clone a testbed project to a new location.", -+ ) -+ clone.add_argument( -+ "--framework", -+ help=( -+ "The location of the XCFramework (or simulator-only slice of an " -+ "XCFramework) to use when running the testbed" -+ ), -+ ) -+ clone.add_argument( -+ "--platform", -+ dest="platform", -+ choices=available_platforms, -+ default=available_platforms[0], -+ help=f"The platform to target (default: {available_platforms[0]})", -+ ) -+ clone.add_argument( -+ "--app", -+ dest="apps", -+ action="append", -+ default=[], -+ help="The location of any code to include in the testbed project", -+ ) -+ clone.add_argument( -+ "location", -+ help="The path where the testbed will be cloned.", -+ ) + def import_script(self, name, fd, filename=None, check_override=None): + override_text = '' + if check_override is not None: +@@ -1727,12 +1750,19 @@ + _imp._override_multi_interp_extensions_check({check_override}) + ''' + if filename: ++ # Apple extensions must be distributed as frameworks. This requires ++ # a specialist loader. ++ if is_apple_mobile: ++ loader = "AppleFrameworkLoader" ++ else: ++ loader = "ExtensionFileLoader" + -+ run = subcommands.add_parser( -+ "run", -+ usage=( -+ "%(prog)s [-h] [--simulator SIMULATOR] -- " -+ " [ ...]" -+ ), -+ description=( -+ "Run a testbed project. The arguments provided after `--` will be " -+ "passed to the running test process as if they were arguments to " -+ "`python -m`." -+ ), -+ help="Run a testbed project", -+ ) -+ run.add_argument( -+ "--platform", -+ dest="platform", -+ choices=available_platforms, -+ default=available_platforms[0], -+ help=f"The platform to target (default: {available_platforms[0]})", -+ ) -+ run.add_argument( -+ "--simulator", -+ help=( -+ "The name of the simulator to use (eg: 'iPhone 16e'). Defaults to " -+ "the most recently released 'entry level' iPhone device. Device " -+ "architecture and OS version can also be specified; e.g., " -+ "`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would run on " -+ "an ARM64 iPhone 16 Pro simulator running iOS 26.0." -+ ), -+ ) -+ run.add_argument( -+ "-v", -+ "--verbose", -+ action="store_true", -+ help="Enable verbose output", -+ ) + return textwrap.dedent(f''' + from importlib.util import spec_from_loader, module_from_spec +- from importlib.machinery import ExtensionFileLoader ++ from importlib.machinery import {loader} + import os, sys + {override_text} +- loader = ExtensionFileLoader({name!r}, {filename!r}) ++ loader = {loader}({name!r}, {filename!r}) + spec = spec_from_loader({name!r}, loader) + try: + module = module_from_spec(spec) +@@ -1916,7 +1946,7 @@ + # and Py_MOD_GIL_NOT_USED + modname = '_test_non_isolated' + filename = _testmultiphase.__file__ +- loader = ExtensionFileLoader(modname, filename) ++ loader = self.create_extension_loader(modname, filename) + spec = importlib.util.spec_from_loader(modname, loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) +@@ -1932,33 +1962,31 @@ + + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_multi_init_extension_per_interpreter_gil_compat(self): +- # _test_shared_gil_only: +- # Explicit Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED (default) +- # and Py_MOD_GIL_NOT_USED +- # _test_no_multiple_interpreter_slot: +- # No Py_mod_multiple_interpreters slot +- # and Py_MOD_GIL_NOT_USED +- for modname in ('_test_shared_gil_only', +- '_test_no_multiple_interpreter_slot'): +- with self.subTest(modname=modname): +- +- filename = _testmultiphase.__file__ +- loader = ExtensionFileLoader(modname, filename) +- spec = importlib.util.spec_from_loader(modname, loader) +- module = importlib.util.module_from_spec(spec) +- loader.exec_module(module) +- sys.modules[modname] = module +- +- require_extension(module) +- with self.subTest(f'{modname}: isolated, strict'): +- self.check_incompatible_here(modname, filename, +- isolated=True) +- with self.subTest(f'{modname}: not isolated, strict'): +- self.check_compatible_here(modname, filename, +- strict=True, isolated=False) +- with self.subTest(f'{modname}: not isolated, not strict'): +- self.check_compatible_here( +- modname, filename, strict=False, isolated=False) ++ modname = '_test_shared_gil_only' ++ filename = _testmultiphase.__file__ ++ loader = self.create_extension_loader(modname, filename) ++ spec = importlib.util.spec_from_loader(modname, loader) ++ module = importlib.util.module_from_spec(spec) ++ loader.exec_module(module) ++ sys.modules[modname] = module + -+ try: -+ pos = sys.argv.index("--") -+ testbed_args = sys.argv[1:pos] -+ test_args = sys.argv[pos + 1 :] -+ except ValueError: -+ testbed_args = sys.argv[1:] -+ test_args = [] ++ filename = _testmultiphase.__file__ ++ loader = ExtensionFileLoader(modname, filename) ++ spec = importlib.util.spec_from_loader(modname, loader) ++ module = importlib.util.module_from_spec(spec) ++ loader.exec_module(module) ++ sys.modules[modname] = module + -+ context = parser.parse_args(testbed_args) ++ require_extension(module) ++ with self.subTest(f'{modname}: isolated, strict'): ++ self.check_incompatible_here(modname, filename, ++ isolated=True) ++ with self.subTest(f'{modname}: not isolated, strict'): ++ self.check_compatible_here(modname, filename, ++ strict=True, isolated=False) ++ with self.subTest(f'{modname}: not isolated, not strict'): ++ self.check_compatible_here( ++ modname, filename, strict=False, isolated=False) + + def test_python_compat(self): + module = 'threading' +@@ -2074,10 +2102,25 @@ + @classmethod + def setUpClass(cls): + spec = importlib.util.find_spec(cls.NAME) +- from importlib.machinery import ExtensionFileLoader +- cls.FILE = spec.origin + cls.LOADER = type(spec.loader) +- assert cls.LOADER is ExtensionFileLoader + -+ if context.subcommand == "clone": -+ clone_testbed( -+ source=Path(__file__).parent.resolve(), -+ target=Path(context.location).resolve(), -+ framework=Path(context.framework).resolve() -+ if context.framework -+ else None, -+ platform=context.platform, -+ apps=[Path(app) for app in context.apps], -+ ) -+ elif context.subcommand == "run": -+ if test_args: -+ if not ( -+ Path(__file__).parent -+ / "Python.xcframework" -+ / TEST_SLICES[context.platform] -+ / "bin" -+ ).is_dir(): -+ print( -+ "Testbed does not contain a compiled Python framework. " -+ f"Use `python {sys.argv[0]} clone ...` to create a " -+ "runnable clone of this testbed." -+ ) -+ sys.exit(20) ++ # Apple extensions must be distributed as frameworks. This requires ++ # a specialist loader, and we need to differentiate between the ++ # spec.origin and the original file location. ++ if is_apple_mobile: ++ assert cls.LOADER is AppleFrameworkLoader + -+ run_testbed( -+ platform=context.platform, -+ simulator=context.simulator, -+ verbose=context.verbose, -+ args=test_args, -+ ) ++ cls.ORIGIN = spec.origin ++ with open(spec.origin + ".origin", "r") as f: ++ cls.FILE = os.path.join( ++ os.path.dirname(sys.executable), ++ f.read().strip() ++ ) + else: -+ print( -+ "Must specify test arguments " -+ f"(e.g., {sys.argv[0]} run -- test)" -+ ) -+ print() -+ parser.print_help(sys.stderr) -+ sys.exit(21) -+ else: -+ parser.print_help(sys.stderr) -+ sys.exit(1) -+ ++ assert cls.LOADER is ExtensionFileLoader + -+if __name__ == "__main__": -+ # Under the buildbot, stdout is not a TTY, but we must still flush after -+ # every line to make sure our output appears in the correct order relative -+ # to the output of our subprocesses. -+ for stream in [sys.stdout, sys.stderr]: -+ stream.reconfigure(line_buffering=True) -+ main() -diff --git a/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj b/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj -new file mode 100644 -index 00000000000..1f9e3f23a02 ---- /dev/null -+++ b/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj -@@ -0,0 +1,563 @@ -+// !$*UTF8*$! -+{ -+ archiveVersion = 1; -+ classes = { -+ }; -+ objectVersion = 56; -+ objects = { ++ cls.ORIGIN = spec.origin ++ cls.FILE = spec.origin + + # Start fresh. + cls.clean_up() +@@ -2093,14 +2136,15 @@ + @classmethod + def clean_up(cls): + name = cls.NAME +- filename = cls.FILE + if name in sys.modules: + if hasattr(sys.modules[name], '_clear_globals'): +- assert sys.modules[name].__file__ == filename ++ assert sys.modules[name].__file__ == cls.FILE, \ ++ f"{sys.modules[name].__file__} != {cls.FILE}" + -+/* Begin PBXBuildFile section */ -+ 607A66172B0EFA380010BFC8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66162B0EFA380010BFC8 /* AppDelegate.m */; }; -+ 71CB095279CCE565BAFB69C3 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D995ADFDE8B3EF5DDEB37874 /* SceneDelegate.m */; }; -+ 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607A66212B0EFA390010BFC8 /* Assets.xcassets */; }; -+ 607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */; }; -+ 607A66282B0EFA390010BFC8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66272B0EFA390010BFC8 /* main.m */; }; -+ 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */; }; -+ 607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; -+ 607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; -+ 607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; -+ 607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; -+ 608619542CB77BA900F46182 /* app_packages in Resources */ = {isa = PBXBuildFile; fileRef = 608619532CB77BA900F46182 /* app_packages */; }; -+ 608619562CB7819B00F46182 /* app in Resources */ = {isa = PBXBuildFile; fileRef = 608619552CB7819B00F46182 /* app */; }; -+/* End PBXBuildFile section */ + sys.modules[name]._clear_globals() + del sys.modules[name] + # Clear all internally cached data for the extension. +- _testinternalcapi.clear_extension(name, filename) ++ _testinternalcapi.clear_extension(name, cls.ORIGIN) + + ######################### + # helpers +@@ -2108,7 +2152,7 @@ + def add_module_cleanup(self, name): + def clean_up(): + # Clear all internally cached data for the extension. +- _testinternalcapi.clear_extension(name, self.FILE) ++ _testinternalcapi.clear_extension(name, self.ORIGIN) + self.addCleanup(clean_up) + + def _load_dynamic(self, name, path): +@@ -2131,7 +2175,7 @@ + except AttributeError: + already_loaded = self.already_loaded = {} + assert name not in already_loaded +- mod = self._load_dynamic(name, self.FILE) ++ mod = self._load_dynamic(name, self.ORIGIN) + self.assertNotIn(mod, already_loaded.values()) + already_loaded[name] = mod + return types.SimpleNamespace( +@@ -2143,7 +2187,7 @@ + def re_load(self, name, mod): + assert sys.modules[name] is mod + assert mod.__dict__ == mod.__dict__ +- reloaded = self._load_dynamic(name, self.FILE) ++ reloaded = self._load_dynamic(name, self.ORIGIN) + return types.SimpleNamespace( + name=name, + module=reloaded, +@@ -2163,7 +2207,7 @@ + name = {self.NAME!r} + if name in sys.modules: + sys.modules.pop(name)._clear_globals() +- _testinternalcapi.clear_extension(name, {self.FILE!r}) ++ _testinternalcapi.clear_extension(name, {self.ORIGIN!r}) + ''')) + _interpreters.destroy(interpid) + self.addCleanup(clean_up) +@@ -2180,7 +2224,7 @@ + postcleanup = f''' + {import_} + mod._clear_globals() +- _testinternalcapi.clear_extension(name, {self.FILE!r}) ++ _testinternalcapi.clear_extension(name, {self.ORIGIN!r}) + ''' + + try: +@@ -2218,7 +2262,7 @@ + # mod.__name__ might not match, but the spec will. + self.assertEqual(mod.__spec__.name, loaded.name) + self.assertEqual(mod.__file__, self.FILE) +- self.assertEqual(mod.__spec__.origin, self.FILE) ++ self.assertEqual(mod.__spec__.origin, self.ORIGIN) + if not isolated: + self.assertTrue(issubclass(mod.error, Exception)) + self.assertEqual(mod.int_const, 1969) +@@ -2612,7 +2656,7 @@ + # First, load in the main interpreter but then completely clear it. + loaded_main = self.load(self.NAME) + loaded_main.module._clear_globals() +- _testinternalcapi.clear_extension(self.NAME, self.FILE) ++ _testinternalcapi.clear_extension(self.NAME, self.ORIGIN) + + # At this point: + # * alive in 0 interpreters +diff --git a/Lib/test/test_importlib/extension/test_finder.py b/Lib/test/test_importlib/extension/test_finder.py +index 3de120958fd..cdc8884d668 100644 +--- a/Lib/test/test_importlib/extension/test_finder.py ++++ b/Lib/test/test_importlib/extension/test_finder.py +@@ -1,3 +1,4 @@ ++from test.support import is_apple_mobile + from test.test_importlib import abc, util + + machinery = util.import_importlib('importlib.machinery') +@@ -19,9 +20,27 @@ + ) + + def find_spec(self, fullname): +- importer = self.machinery.FileFinder(util.EXTENSIONS.path, +- (self.machinery.ExtensionFileLoader, +- self.machinery.EXTENSION_SUFFIXES)) ++ if is_apple_mobile: ++ # Apple mobile platforms require a specialist loader that uses ++ # .fwork files as placeholders for the true `.so` files. ++ loaders = [ ++ ( ++ self.machinery.AppleFrameworkLoader, ++ [ ++ ext.replace(".so", ".fwork") ++ for ext in self.machinery.EXTENSION_SUFFIXES ++ ] ++ ) ++ ] ++ else: ++ loaders = [ ++ ( ++ self.machinery.ExtensionFileLoader, ++ self.machinery.EXTENSION_SUFFIXES ++ ) ++ ] + -+/* Begin PBXContainerItemProxy section */ -+ 607A662E2B0EFA3A0010BFC8 /* PBXContainerItemProxy */ = { -+ isa = PBXContainerItemProxy; -+ containerPortal = 607A660A2B0EFA380010BFC8 /* Project object */; -+ proxyType = 1; -+ remoteGlobalIDString = 607A66112B0EFA380010BFC8; -+ remoteInfo = iOSTestbed; -+ }; -+/* End PBXContainerItemProxy section */ ++ importer = self.machinery.FileFinder(util.EXTENSIONS.path, *loaders) + + return importer.find_spec(fullname) + +diff --git a/Lib/test/test_importlib/extension/test_loader.py b/Lib/test/test_importlib/extension/test_loader.py +index 12f9e43d123..7e8c9c8184a 100644 +--- a/Lib/test/test_importlib/extension/test_loader.py ++++ b/Lib/test/test_importlib/extension/test_loader.py +@@ -1,3 +1,4 @@ ++from test.support import is_apple_mobile + from warnings import catch_warnings + from test.test_importlib import abc, util + +@@ -25,8 +26,15 @@ + raise unittest.SkipTest( + f"{util.EXTENSIONS.name} is a builtin module" + ) +- self.loader = self.machinery.ExtensionFileLoader(util.EXTENSIONS.name, +- util.EXTENSIONS.file_path) + -+/* Begin PBXCopyFilesBuildPhase section */ -+ 607A664E2B0EFC080010BFC8 /* Embed Frameworks */ = { -+ isa = PBXCopyFilesBuildPhase; -+ buildActionMask = 2147483647; -+ dstPath = ""; -+ dstSubfolderSpec = 10; -+ files = ( -+ 607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */, -+ ); -+ name = "Embed Frameworks"; -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */ = { -+ isa = PBXCopyFilesBuildPhase; -+ buildActionMask = 2147483647; -+ dstPath = ""; -+ dstSubfolderSpec = 10; -+ files = ( -+ 607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */, -+ ); -+ name = "Embed Frameworks"; -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXCopyFilesBuildPhase section */ ++ # Apple extensions must be distributed as frameworks. This requires ++ # a specialist loader. ++ if is_apple_mobile: ++ self.LoaderClass = self.machinery.AppleFrameworkLoader ++ else: ++ self.LoaderClass = self.machinery.ExtensionFileLoader + -+/* Begin PBXFileReference section */ -+ 607A66122B0EFA380010BFC8 /* iOSTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; }; -+ 607A66152B0EFA380010BFC8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; -+ 607A66162B0EFA380010BFC8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; -+ 83DC5E71F9D64737365A3BE4 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; -+ D995ADFDE8B3EF5DDEB37874 /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; -+ 607A66212B0EFA390010BFC8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; -+ 607A66242B0EFA390010BFC8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; -+ 607A66272B0EFA390010BFC8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; -+ 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSTestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; -+ 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestbedTests.m; sourceTree = ""; }; -+ 607A664A2B0EFB310010BFC8 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; -+ 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = ""; }; -+ 608619532CB77BA900F46182 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = ""; }; -+ 608619552CB7819B00F46182 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = ""; }; -+ 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = iOSTestbed.xctestplan; sourceTree = ""; }; -+/* End PBXFileReference section */ ++ self.loader = self.LoaderClass(util.EXTENSIONS.name, util.EXTENSIONS.file_path) + + def load_module(self, fullname): + with warnings.catch_warnings(): +@@ -34,13 +42,11 @@ + return self.loader.load_module(fullname) + + def test_equality(self): +- other = self.machinery.ExtensionFileLoader(util.EXTENSIONS.name, +- util.EXTENSIONS.file_path) ++ other = self.LoaderClass(util.EXTENSIONS.name, util.EXTENSIONS.file_path) + self.assertEqual(self.loader, other) + + def test_inequality(self): +- other = self.machinery.ExtensionFileLoader('_' + util.EXTENSIONS.name, +- util.EXTENSIONS.file_path) ++ other = self.LoaderClass('_' + util.EXTENSIONS.name, util.EXTENSIONS.file_path) + self.assertNotEqual(self.loader, other) + + def test_load_module_API(self): +@@ -60,8 +66,7 @@ + ('__package__', '')]: + self.assertEqual(getattr(module, attr), value) + self.assertIn(util.EXTENSIONS.name, sys.modules) +- self.assertIsInstance(module.__loader__, +- self.machinery.ExtensionFileLoader) ++ self.assertIsInstance(module.__loader__, self.LoaderClass) + + # No extension module as __init__ available for testing. + test_package = None +@@ -88,7 +93,7 @@ + self.assertFalse(self.loader.is_package(util.EXTENSIONS.name)) + for suffix in self.machinery.EXTENSION_SUFFIXES: + path = os.path.join('some', 'path', 'pkg', '__init__' + suffix) +- loader = self.machinery.ExtensionFileLoader('pkg', path) ++ loader = self.LoaderClass('pkg', path) + self.assertTrue(loader.is_package('pkg')) + + +@@ -103,6 +108,14 @@ + def setUp(self): + if not self.machinery.EXTENSION_SUFFIXES or not util.EXTENSIONS: + raise unittest.SkipTest("Requires dynamic loading support.") + -+/* Begin PBXFrameworksBuildPhase section */ -+ 607A660F2B0EFA380010BFC8 /* Frameworks */ = { -+ isa = PBXFrameworksBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ 607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+ 607A662A2B0EFA3A0010BFC8 /* Frameworks */ = { -+ isa = PBXFrameworksBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ 607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXFrameworksBuildPhase section */ ++ # Apple extensions must be distributed as frameworks. This requires ++ # a specialist loader. ++ if is_apple_mobile: ++ self.LoaderClass = self.machinery.AppleFrameworkLoader ++ else: ++ self.LoaderClass = self.machinery.ExtensionFileLoader + -+/* Begin PBXGroup section */ -+ 607A66092B0EFA380010BFC8 = { -+ isa = PBXGroup; -+ children = ( -+ 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */, -+ 607A664A2B0EFB310010BFC8 /* Python.xcframework */, -+ 607A66142B0EFA380010BFC8 /* iOSTestbed */, -+ 607A66302B0EFA3A0010BFC8 /* TestbedTests */, -+ 607A66132B0EFA380010BFC8 /* Products */, -+ 607A664F2B0EFFE00010BFC8 /* Frameworks */, -+ ); -+ sourceTree = ""; -+ }; -+ 607A66132B0EFA380010BFC8 /* Products */ = { -+ isa = PBXGroup; -+ children = ( -+ 607A66122B0EFA380010BFC8 /* iOSTestbed.app */, -+ 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */, -+ ); -+ name = Products; -+ sourceTree = ""; -+ }; -+ 607A66142B0EFA380010BFC8 /* iOSTestbed */ = { -+ isa = PBXGroup; -+ children = ( -+ 608619552CB7819B00F46182 /* app */, -+ 608619532CB77BA900F46182 /* app_packages */, -+ 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */, -+ 607A66152B0EFA380010BFC8 /* AppDelegate.h */, -+ 607A66162B0EFA380010BFC8 /* AppDelegate.m */, -+ 83DC5E71F9D64737365A3BE4 /* SceneDelegate.h */, -+ D995ADFDE8B3EF5DDEB37874 /* SceneDelegate.m */, -+ 607A66212B0EFA390010BFC8 /* Assets.xcassets */, -+ 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */, -+ 607A66272B0EFA390010BFC8 /* main.m */, -+ ); -+ path = iOSTestbed; -+ sourceTree = ""; -+ }; -+ 607A66302B0EFA3A0010BFC8 /* TestbedTests */ = { -+ isa = PBXGroup; -+ children = ( -+ 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */, -+ ); -+ path = TestbedTests; -+ sourceTree = ""; -+ }; -+ 607A664F2B0EFFE00010BFC8 /* Frameworks */ = { -+ isa = PBXGroup; -+ children = ( -+ ); -+ name = Frameworks; -+ sourceTree = ""; -+ }; -+/* End PBXGroup section */ + self.name = '_testsinglephase' + if self.name in sys.builtin_module_names: + raise unittest.SkipTest( +@@ -111,8 +124,8 @@ + finder = self.machinery.FileFinder(None) + self.spec = importlib.util.find_spec(self.name) + assert self.spec +- self.loader = self.machinery.ExtensionFileLoader( +- self.name, self.spec.origin) + -+/* Begin PBXNativeTarget section */ -+ 607A66112B0EFA380010BFC8 /* iOSTestbed */ = { -+ isa = PBXNativeTarget; -+ buildConfigurationList = 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbed" */; -+ buildPhases = ( -+ 607A660E2B0EFA380010BFC8 /* Sources */, -+ 607A660F2B0EFA380010BFC8 /* Frameworks */, -+ 607A66102B0EFA380010BFC8 /* Resources */, -+ 607A66552B0F061D0010BFC8 /* Process Python libraries */, -+ 607A664E2B0EFC080010BFC8 /* Embed Frameworks */, -+ ); -+ buildRules = ( -+ ); -+ dependencies = ( -+ ); -+ name = iOSTestbed; -+ productName = iOSTestbed; -+ productReference = 607A66122B0EFA380010BFC8 /* iOSTestbed.app */; -+ productType = "com.apple.product-type.application"; -+ }; -+ 607A662C2B0EFA3A0010BFC8 /* iOSTestbedTests */ = { -+ isa = PBXNativeTarget; -+ buildConfigurationList = 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbedTests" */; -+ buildPhases = ( -+ 607A66292B0EFA3A0010BFC8 /* Sources */, -+ 607A662A2B0EFA3A0010BFC8 /* Frameworks */, -+ 607A662B2B0EFA3A0010BFC8 /* Resources */, -+ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */, -+ ); -+ buildRules = ( -+ ); -+ dependencies = ( -+ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */, -+ ); -+ name = iOSTestbedTests; -+ productName = iOSTestbedTests; -+ productReference = 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */; -+ productType = "com.apple.product-type.bundle.unit-test"; -+ }; -+/* End PBXNativeTarget section */ ++ self.loader = self.LoaderClass(self.name, self.spec.origin) + + def load_module(self): + with warnings.catch_warnings(): +@@ -122,7 +135,7 @@ + def load_module_by_name(self, fullname): + # Load a module from the test extension by name. + origin = self.spec.origin +- loader = self.machinery.ExtensionFileLoader(fullname, origin) ++ loader = self.LoaderClass(fullname, origin) + spec = importlib.util.spec_from_loader(fullname, loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) +@@ -139,8 +152,7 @@ + with self.assertRaises(AttributeError): + module.__path__ + self.assertIs(module, sys.modules[self.name]) +- self.assertIsInstance(module.__loader__, +- self.machinery.ExtensionFileLoader) ++ self.assertIsInstance(module.__loader__, self.LoaderClass) + + # No extension module as __init__ available for testing. + test_package = None +@@ -184,6 +196,14 @@ + def setUp(self): + if not self.machinery.EXTENSION_SUFFIXES or not util.EXTENSIONS: + raise unittest.SkipTest("Requires dynamic loading support.") + -+/* Begin PBXProject section */ -+ 607A660A2B0EFA380010BFC8 /* Project object */ = { -+ isa = PBXProject; -+ attributes = { -+ BuildIndependentTargetsInParallel = 1; -+ LastUpgradeCheck = 1500; -+ TargetAttributes = { -+ 607A66112B0EFA380010BFC8 = { -+ CreatedOnToolsVersion = 15.0.1; -+ }; -+ 607A662C2B0EFA3A0010BFC8 = { -+ CreatedOnToolsVersion = 15.0.1; -+ TestTargetID = 607A66112B0EFA380010BFC8; -+ }; -+ }; -+ }; -+ buildConfigurationList = 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "iOSTestbed" */; -+ compatibilityVersion = "Xcode 14.0"; -+ developmentRegion = en; -+ hasScannedForEncodings = 0; -+ knownRegions = ( -+ en, -+ Base, -+ ); -+ mainGroup = 607A66092B0EFA380010BFC8; -+ productRefGroup = 607A66132B0EFA380010BFC8 /* Products */; -+ projectDirPath = ""; -+ projectRoot = ""; -+ targets = ( -+ 607A66112B0EFA380010BFC8 /* iOSTestbed */, -+ 607A662C2B0EFA3A0010BFC8 /* iOSTestbedTests */, -+ ); -+ }; -+/* End PBXProject section */ ++ # Apple extensions must be distributed as frameworks. This requires ++ # a specialist loader. ++ if is_apple_mobile: ++ self.LoaderClass = self.machinery.AppleFrameworkLoader ++ else: ++ self.LoaderClass = self.machinery.ExtensionFileLoader + -+/* Begin PBXResourcesBuildPhase section */ -+ 607A66102B0EFA380010BFC8 /* Resources */ = { -+ isa = PBXResourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ 607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */, -+ 608619562CB7819B00F46182 /* app in Resources */, -+ 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */, -+ 608619542CB77BA900F46182 /* app_packages in Resources */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+ 607A662B2B0EFA3A0010BFC8 /* Resources */ = { -+ isa = PBXResourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXResourcesBuildPhase section */ + self.name = '_testmultiphase' + if self.name in sys.builtin_module_names: + raise unittest.SkipTest( +@@ -192,8 +212,7 @@ + finder = self.machinery.FileFinder(None) + self.spec = importlib.util.find_spec(self.name) + assert self.spec +- self.loader = self.machinery.ExtensionFileLoader( +- self.name, self.spec.origin) ++ self.loader = self.LoaderClass(self.name, self.spec.origin) + + def load_module(self): + # Load the module from the test extension. +@@ -204,7 +223,7 @@ + def load_module_by_name(self, fullname): + # Load a module from the test extension by name. + origin = self.spec.origin +- loader = self.machinery.ExtensionFileLoader(fullname, origin) ++ loader = self.LoaderClass(fullname, origin) + spec = importlib.util.spec_from_loader(fullname, loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) +@@ -230,8 +249,7 @@ + with self.assertRaises(AttributeError): + module.__path__ + self.assertIs(module, sys.modules[self.name]) +- self.assertIsInstance(module.__loader__, +- self.machinery.ExtensionFileLoader) ++ self.assertIsInstance(module.__loader__, self.LoaderClass) + + def test_functionality(self): + # Test basic functionality of stuff defined in an extension module. +diff --git a/Lib/test/test_importlib/test_util.py b/Lib/test/test_importlib/test_util.py +index 553e2087421..e23949aaa22 100644 +--- a/Lib/test/test_importlib/test_util.py ++++ b/Lib/test/test_importlib/test_util.py +@@ -703,13 +703,20 @@ + + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_incomplete_multi_phase_init_module(self): ++ # Apple extensions must be distributed as frameworks. This requires ++ # a specialist loader. ++ if support.is_apple_mobile: ++ loader = "AppleFrameworkLoader" ++ else: ++ loader = "ExtensionFileLoader" + -+/* Begin PBXShellScriptBuildPhase section */ -+ 607A66552B0F061D0010BFC8 /* Process Python libraries */ = { -+ isa = PBXShellScriptBuildPhase; -+ alwaysOutOfDate = 1; -+ buildActionMask = 2147483647; -+ files = ( -+ ); -+ inputFileListPaths = ( -+ ); -+ inputPaths = ( -+ ); -+ name = "Process Python libraries"; -+ outputFileListPaths = ( -+ ); -+ outputPaths = ( -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ shellPath = /bin/sh; -+ shellScript = "set -e\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\ninstall_python Python.xcframework app app_packages\n"; -+ showEnvVarsInLog = 0; -+ }; -+/* End PBXShellScriptBuildPhase section */ + prescript = textwrap.dedent(f''' + from importlib.util import spec_from_loader, module_from_spec +- from importlib.machinery import ExtensionFileLoader ++ from importlib.machinery import {loader} + + name = '_test_shared_gil_only' + filename = {_testmultiphase.__file__!r} +- loader = ExtensionFileLoader(name, filename) ++ loader = {loader}(name, filename) + spec = spec_from_loader(name, loader) + + ''') +diff --git a/Lib/test/test_importlib/util.py b/Lib/test/test_importlib/util.py +index a900cc1dddf..89272484009 100644 +--- a/Lib/test/test_importlib/util.py ++++ b/Lib/test/test_importlib/util.py +@@ -8,6 +8,7 @@ + import os.path + from test import support + from test.support import import_helper ++from test.support import is_apple_mobile + from test.support import os_helper + import unittest + import sys +@@ -43,6 +44,11 @@ + global EXTENSIONS + for path in sys.path: + for ext in machinery.EXTENSION_SUFFIXES: ++ # Apple mobile platforms mechanically load .so files, ++ # but the findable files are labelled .fwork ++ if is_apple_mobile: ++ ext = ext.replace(".so", ".fwork") + -+/* Begin PBXSourcesBuildPhase section */ -+ 607A660E2B0EFA380010BFC8 /* Sources */ = { -+ isa = PBXSourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ 607A66172B0EFA380010BFC8 /* AppDelegate.m in Sources */, -+ 71CB095279CCE565BAFB69C3 /* SceneDelegate.m in Sources */, -+ 607A66282B0EFA390010BFC8 /* main.m in Sources */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+ 607A66292B0EFA3A0010BFC8 /* Sources */ = { -+ isa = PBXSourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXSourcesBuildPhase section */ + filename = EXTENSIONS.name + ext + file_path = os.path.join(path, filename) + if os.path.exists(file_path): +diff --git a/Lib/test/test_interpreters.py b/Lib/test/test_interpreters.py +index 0cd9e721b20..ffa58230425 100644 +--- a/Lib/test/test_interpreters.py ++++ b/Lib/test/test_interpreters.py +@@ -752,6 +752,7 @@ + + class FinalizationTests(TestBase): + ++ @support.requires_subprocess() + def test_gh_109793(self): + import subprocess + argv = [sys.executable, '-c', '''if True: +diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py +index d85040a3083..9d634acfd3e 100644 +--- a/Lib/test/test_io.py ++++ b/Lib/test/test_io.py +@@ -39,11 +39,9 @@ + from test import support + from test.support.script_helper import ( + assert_python_ok, assert_python_failure, run_python_until_end) +-from test.support import import_helper +-from test.support import os_helper +-from test.support import threading_helper +-from test.support import warnings_helper +-from test.support import skip_if_sanitizer ++from test.support import ( ++ import_helper, is_apple, os_helper, skip_if_sanitizer, threading_helper, warnings_helper ++) + from test.support.os_helper import FakePath + + import codecs +@@ -631,10 +629,10 @@ + self.read_ops(f, True) + + def test_large_file_ops(self): +- # On Windows and Mac OSX this test consumes large resources; It takes +- # a long time to build the >2 GiB file and takes >2 GiB of disk space +- # therefore the resource must be enabled to run this test. +- if sys.platform[:3] == 'win' or sys.platform == 'darwin': ++ # On Windows and Apple platforms this test consumes large resources; It ++ # takes a long time to build the >2 GiB file and takes >2 GiB of disk ++ # space therefore the resource must be enabled to run this test. ++ if sys.platform[:3] == 'win' or is_apple: + support.requires( + 'largefile', + 'test requires %s bytes and a long time to run' % self.LARGE) +diff --git a/Lib/test/test_lib2to3/test_parser.py b/Lib/test/test_lib2to3/test_parser.py +index 2c798b181fd..e12ed1e9389 100644 +--- a/Lib/test/test_lib2to3/test_parser.py ++++ b/Lib/test/test_lib2to3/test_parser.py +@@ -62,9 +62,7 @@ + shutil.rmtree(tmpdir) + + @unittest.skipIf(sys.executable is None, 'sys.executable required') +- @unittest.skipIf( +- sys.platform in {'emscripten', 'wasi'}, 'requires working subprocess' +- ) ++ @test.support.requires_subprocess() + def test_load_grammar_from_subprocess(self): + tmpdir = tempfile.mkdtemp() + tmpsubdir = os.path.join(tmpdir, 'subdir') +diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py +index 3d9d6d5d0ac..9c759170450 100644 +--- a/Lib/test/test_marshal.py ++++ b/Lib/test/test_marshal.py +@@ -1,5 +1,5 @@ + from test import support +-from test.support import os_helper, requires_debug_ranges ++from test.support import is_apple_mobile, os_helper, requires_debug_ranges + from test.support.script_helper import assert_python_ok + import array + import io +@@ -260,7 +260,7 @@ + #if os.name == 'nt' and support.Py_DEBUG: + if os.name == 'nt': + MAX_MARSHAL_STACK_DEPTH = 1000 +- elif sys.platform == 'wasi': ++ elif sys.platform == 'wasi' or is_apple_mobile: + MAX_MARSHAL_STACK_DEPTH = 1500 + else: + MAX_MARSHAL_STACK_DEPTH = 2000 +diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py +index 1867e8c957f..f75e40940e4 100644 +--- a/Lib/test/test_mmap.py ++++ b/Lib/test/test_mmap.py +@@ -1,5 +1,5 @@ + from test.support import ( +- requires, _2G, _4G, gc_collect, cpython_only, is_emscripten ++ requires, _2G, _4G, gc_collect, cpython_only, is_emscripten, is_apple, + ) + from test.support.import_helper import import_module + from test.support.os_helper import TESTFN, unlink +@@ -1009,7 +1009,7 @@ + unlink(TESTFN) + + def _make_test_file(self, num_zeroes, tail): +- if sys.platform[:3] == 'win' or sys.platform == 'darwin': ++ if sys.platform[:3] == 'win' or is_apple: + requires('largefile', + 'test requires %s bytes and a long time to run' % str(0x180000000)) + f = open(TESTFN, 'w+b') +diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py +index 1d6d92e0235..90afbf1d14a 100644 +--- a/Lib/test/test_os.py ++++ b/Lib/test/test_os.py +@@ -2372,6 +2372,7 @@ + support.is_emscripten or support.is_wasi, + "musl libc issue on Emscripten/WASI, bpo-46390" + ) ++ @unittest.skipIf(support.is_apple_mobile, "gh-118201: Test is flaky on iOS") + def test_fpathconf(self): + self.check(os.pathconf, "PC_NAME_MAX") + self.check(os.fpathconf, "PC_NAME_MAX") +@@ -3946,6 +3947,7 @@ + self.assertGreaterEqual(size.columns, 0) + self.assertGreaterEqual(size.lines, 0) + ++ @support.requires_subprocess() + def test_stty_match(self): + """Check if stty returns the same results + +diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py +index 4a16ff6939c..b29731fbb42 100644 +--- a/Lib/test/test_platform.py ++++ b/Lib/test/test_platform.py +@@ -10,6 +10,14 @@ + from test import support + from test.support import os_helper + ++try: ++ # Some of the iOS tests need ctypes to operate. ++ # Confirm that the ctypes module is available ++ # is available. ++ import _ctypes ++except ImportError: ++ _ctypes = None + -+/* Begin PBXTargetDependency section */ -+ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */ = { -+ isa = PBXTargetDependency; -+ target = 607A66112B0EFA380010BFC8 /* iOSTestbed */; -+ targetProxy = 607A662E2B0EFA3A0010BFC8 /* PBXContainerItemProxy */; -+ }; -+/* End PBXTargetDependency section */ + FEDORA_OS_RELEASE = """\ + NAME=Fedora + VERSION="32 (Thirty Two)" +@@ -219,6 +227,30 @@ + self.assertEqual(res[-1], res.processor) + self.assertEqual(len(res), 6) + ++ if os.name == "posix": ++ uname = os.uname() ++ self.assertEqual(res.node, uname.nodename) ++ self.assertEqual(res.version, uname.version) ++ self.assertEqual(res.machine, uname.machine) + -+/* Begin PBXVariantGroup section */ -+ 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */ = { -+ isa = PBXVariantGroup; -+ children = ( -+ 607A66242B0EFA390010BFC8 /* Base */, -+ ); -+ name = LaunchScreen.storyboard; -+ sourceTree = ""; -+ }; -+/* End PBXVariantGroup section */ ++ if sys.platform == "android": ++ self.assertEqual(res.system, "Android") ++ self.assertEqual(res.release, platform.android_ver().release) ++ elif sys.platform == "ios": ++ # Platform module needs ctypes for full operation. If ctypes ++ # isn't available, there's no ObjC module, and dummy values are ++ # returned. ++ if _ctypes: ++ self.assertIn(res.system, {"iOS", "iPadOS"}) ++ self.assertEqual(res.release, platform.ios_ver().release) ++ else: ++ self.assertEqual(res.system, "") ++ self.assertEqual(res.release, "") ++ else: ++ self.assertEqual(res.system, uname.sysname) ++ self.assertEqual(res.release, uname.release) + -+/* Begin XCBuildConfiguration section */ -+ 607A663F2B0EFA3A0010BFC8 /* Debug */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ALWAYS_SEARCH_USER_PATHS = NO; -+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; -+ CLANG_ANALYZER_NONNULL = YES; -+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; -+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; -+ CLANG_ENABLE_MODULES = YES; -+ CLANG_ENABLE_OBJC_ARC = YES; -+ CLANG_ENABLE_OBJC_WEAK = YES; -+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; -+ CLANG_WARN_BOOL_CONVERSION = YES; -+ CLANG_WARN_COMMA = YES; -+ CLANG_WARN_CONSTANT_CONVERSION = YES; -+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; -+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; -+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; -+ CLANG_WARN_EMPTY_BODY = YES; -+ CLANG_WARN_ENUM_CONVERSION = YES; -+ CLANG_WARN_INFINITE_RECURSION = YES; -+ CLANG_WARN_INT_CONVERSION = YES; -+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; -+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; -+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; -+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; -+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; -+ CLANG_WARN_STRICT_PROTOTYPES = YES; -+ CLANG_WARN_SUSPICIOUS_MOVE = YES; -+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; -+ CLANG_WARN_UNREACHABLE_CODE = YES; -+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; -+ COPY_PHASE_STRIP = NO; -+ DEBUG_INFORMATION_FORMAT = dwarf; -+ ENABLE_STRICT_OBJC_MSGSEND = YES; -+ ENABLE_TESTABILITY = YES; -+ ENABLE_USER_SCRIPT_SANDBOXING = YES; -+ GCC_C_LANGUAGE_STANDARD = gnu17; -+ GCC_DYNAMIC_NO_PIC = NO; -+ GCC_NO_COMMON_BLOCKS = YES; -+ GCC_OPTIMIZATION_LEVEL = 0; -+ GCC_PREPROCESSOR_DEFINITIONS = ( -+ "DEBUG=1", -+ "$(inherited)", -+ ); -+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; -+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; -+ GCC_WARN_UNDECLARED_SELECTOR = YES; -+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; -+ GCC_WARN_UNUSED_FUNCTION = YES; -+ GCC_WARN_UNUSED_VARIABLE = YES; -+ IPHONEOS_DEPLOYMENT_TARGET = 15.0; -+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; -+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; -+ MTL_FAST_MATH = YES; -+ ONLY_ACTIVE_ARCH = YES; -+ SDKROOT = iphoneos; -+ }; -+ name = Debug; -+ }; -+ 607A66402B0EFA3A0010BFC8 /* Release */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ALWAYS_SEARCH_USER_PATHS = NO; -+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; -+ CLANG_ANALYZER_NONNULL = YES; -+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; -+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; -+ CLANG_ENABLE_MODULES = YES; -+ CLANG_ENABLE_OBJC_ARC = YES; -+ CLANG_ENABLE_OBJC_WEAK = YES; -+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; -+ CLANG_WARN_BOOL_CONVERSION = YES; -+ CLANG_WARN_COMMA = YES; -+ CLANG_WARN_CONSTANT_CONVERSION = YES; -+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; -+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; -+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; -+ CLANG_WARN_EMPTY_BODY = YES; -+ CLANG_WARN_ENUM_CONVERSION = YES; -+ CLANG_WARN_INFINITE_RECURSION = YES; -+ CLANG_WARN_INT_CONVERSION = YES; -+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; -+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; -+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; -+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; -+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; -+ CLANG_WARN_STRICT_PROTOTYPES = YES; -+ CLANG_WARN_SUSPICIOUS_MOVE = YES; -+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; -+ CLANG_WARN_UNREACHABLE_CODE = YES; -+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; -+ COPY_PHASE_STRIP = NO; -+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; -+ ENABLE_NS_ASSERTIONS = NO; -+ ENABLE_STRICT_OBJC_MSGSEND = YES; -+ ENABLE_USER_SCRIPT_SANDBOXING = YES; -+ GCC_C_LANGUAGE_STANDARD = gnu17; -+ GCC_NO_COMMON_BLOCKS = YES; -+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; -+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; -+ GCC_WARN_UNDECLARED_SELECTOR = YES; -+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; -+ GCC_WARN_UNUSED_FUNCTION = YES; -+ GCC_WARN_UNUSED_VARIABLE = YES; -+ IPHONEOS_DEPLOYMENT_TARGET = 15.0; -+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; -+ MTL_ENABLE_DEBUG_INFO = NO; -+ MTL_FAST_MATH = YES; -+ SDKROOT = iphoneos; -+ VALIDATE_PRODUCT = YES; -+ }; -+ name = Release; -+ }; -+ 607A66422B0EFA3A0010BFC8 /* Debug */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; -+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ DEVELOPMENT_TEAM = ""; -+ ENABLE_USER_SCRIPT_SANDBOXING = NO; -+ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; -+ INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist"; -+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; -+ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; -+ INFOPLIST_KEY_UIMainStoryboardFile = Main; -+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; -+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; -+ IPHONEOS_DEPLOYMENT_TARGET = 15.0; -+ LD_RUNPATH_SEARCH_PATHS = ( -+ "$(inherited)", -+ "@executable_path/Frameworks", -+ ); -+ MARKETING_VERSION = 3.13.0a1; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = YES; -+ TARGETED_DEVICE_FAMILY = "1,2"; -+ }; -+ name = Debug; -+ }; -+ 607A66432B0EFA3A0010BFC8 /* Release */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; -+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ DEVELOPMENT_TEAM = ""; -+ ENABLE_TESTABILITY = YES; -+ ENABLE_USER_SCRIPT_SANDBOXING = NO; -+ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; -+ INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist"; -+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; -+ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; -+ INFOPLIST_KEY_UIMainStoryboardFile = Main; -+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; -+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; -+ IPHONEOS_DEPLOYMENT_TARGET = 15.0; -+ LD_RUNPATH_SEARCH_PATHS = ( -+ "$(inherited)", -+ "@executable_path/Frameworks", -+ ); -+ MARKETING_VERSION = 3.13.0a1; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = YES; -+ TARGETED_DEVICE_FAMILY = "1,2"; -+ }; -+ name = Release; -+ }; -+ 607A66452B0EFA3A0010BFC8 /* Debug */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ BUNDLE_LOADER = "$(TEST_HOST)"; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ DEVELOPMENT_TEAM = 3HEZE76D99; -+ GENERATE_INFOPLIST_FILE = YES; -+ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; -+ IPHONEOS_DEPLOYMENT_TARGET = 15.0; -+ MARKETING_VERSION = 1.0; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = NO; -+ TARGETED_DEVICE_FAMILY = "1,2"; -+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSTestbed"; -+ }; -+ name = Debug; -+ }; -+ 607A66462B0EFA3A0010BFC8 /* Release */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ BUNDLE_LOADER = "$(TEST_HOST)"; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ DEVELOPMENT_TEAM = 3HEZE76D99; -+ GENERATE_INFOPLIST_FILE = YES; -+ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; -+ IPHONEOS_DEPLOYMENT_TARGET = 15.0; -+ MARKETING_VERSION = 1.0; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = NO; -+ TARGETED_DEVICE_FAMILY = "1,2"; -+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSTestbed"; -+ }; -+ name = Release; -+ }; -+/* End XCBuildConfiguration section */ + -+/* Begin XCConfigurationList section */ -+ 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "iOSTestbed" */ = { -+ isa = XCConfigurationList; -+ buildConfigurations = ( -+ 607A663F2B0EFA3A0010BFC8 /* Debug */, -+ 607A66402B0EFA3A0010BFC8 /* Release */, -+ ); -+ defaultConfigurationIsVisible = 0; -+ defaultConfigurationName = Release; -+ }; -+ 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbed" */ = { -+ isa = XCConfigurationList; -+ buildConfigurations = ( -+ 607A66422B0EFA3A0010BFC8 /* Debug */, -+ 607A66432B0EFA3A0010BFC8 /* Release */, -+ ); -+ defaultConfigurationIsVisible = 0; -+ defaultConfigurationName = Release; -+ }; -+ 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbedTests" */ = { -+ isa = XCConfigurationList; -+ buildConfigurations = ( -+ 607A66452B0EFA3A0010BFC8 /* Debug */, -+ 607A66462B0EFA3A0010BFC8 /* Release */, -+ ); -+ defaultConfigurationIsVisible = 0; -+ defaultConfigurationName = Release; -+ }; -+/* End XCConfigurationList section */ -+ }; -+ rootObject = 607A660A2B0EFA380010BFC8 /* Project object */; -+} -diff --git a/Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme b/Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme -new file mode 100644 -index 00000000000..3c330a4152b ---- /dev/null -+++ b/Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme -@@ -0,0 +1,97 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/Apple/testbed/iOSTestbed.xctestplan b/Apple/testbed/iOSTestbed.xctestplan -new file mode 100644 -index 00000000000..0c4ab9eb2ba ---- /dev/null -+++ b/Apple/testbed/iOSTestbed.xctestplan -@@ -0,0 +1,46 @@ -+{ -+ "configurations" : [ -+ { -+ "id" : "F5A95CE4-1ADE-4A6E-A0E1-CDBAE26DF0C5", -+ "name" : "Test Scheme Action", -+ "options" : { -+ -+ } -+ } -+ ], -+ "defaultOptions" : { -+ "commandLineArgumentEntries" : [ -+ { -+ "argument" : "test" -+ }, -+ { -+ "argument" : "-uall" -+ }, -+ { -+ "argument" : "--single-process" -+ }, -+ { -+ "argument" : "--rerun" -+ }, -+ { -+ "argument" : "-W" -+ } -+ ], -+ "targetForVariableExpansion" : { -+ "containerPath" : "container:iOSTestbed.xcodeproj", -+ "identifier" : "607A66112B0EFA380010BFC8", -+ "name" : "iOSTestbed" -+ } -+ }, -+ "testTargets" : [ -+ { -+ "parallelizable" : false, -+ "target" : { -+ "containerPath" : "container:iOSTestbed.xcodeproj", -+ "identifier" : "607A662C2B0EFA3A0010BFC8", -+ "name" : "iOSTestbedTests" -+ } -+ } -+ ], -+ "version" : 1 -+} -diff --git a/Apple/testbed/iOSTestbed/AppDelegate.h b/Apple/testbed/iOSTestbed/AppDelegate.h -new file mode 100644 -index 00000000000..f695b3b5efc ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/AppDelegate.h -@@ -0,0 +1,11 @@ -+// -+// AppDelegate.h -+// iOSTestbed -+// -+ -+#import + @unittest.skipUnless(sys.platform.startswith('win'), "windows only test") + def test_uname_win32_without_wmi(self): + def raises_oserror(*a): +@@ -404,6 +436,56 @@ + # parent + support.wait_process(pid, exitcode=0) + ++ def test_ios_ver(self): ++ result = platform.ios_ver() + -+@interface AppDelegate : UIResponder ++ # ios_ver is only fully available on iOS where ctypes is available. ++ if sys.platform == "ios" and _ctypes: ++ system, release, model, is_simulator = result ++ # Result is a namedtuple ++ self.assertEqual(result.system, system) ++ self.assertEqual(result.release, release) ++ self.assertEqual(result.model, model) ++ self.assertEqual(result.is_simulator, is_simulator) + ++ # We can't assert specific values without reproducing the logic of ++ # ios_ver(), so we check that the values are broadly what we expect. + -+@end -diff --git a/Apple/testbed/iOSTestbed/AppDelegate.m b/Apple/testbed/iOSTestbed/AppDelegate.m -new file mode 100644 -index 00000000000..fbe71bae23b ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/AppDelegate.m -@@ -0,0 +1,24 @@ -+// -+// AppDelegate.m -+// iOSTestbed -+// ++ # System is either iOS or iPadOS, depending on the test device ++ self.assertIn(system, {"iOS", "iPadOS"}) + -+#import "AppDelegate.h" -+#import "SceneDelegate.h" ++ # Release is a numeric version specifier with at least 2 parts ++ parts = release.split(".") ++ self.assertGreaterEqual(len(parts), 2) ++ self.assertTrue(all(part.isdigit() for part in parts)) + -+@interface AppDelegate () ++ # If this is a simulator, we get a high level device descriptor ++ # with no identifying model number. If this is a physical device, ++ # we get a model descriptor like "iPhone13,1" ++ if is_simulator: ++ self.assertIn(model, {"iPhone", "iPad"}) ++ else: ++ self.assertTrue( ++ (model.startswith("iPhone") or model.startswith("iPad")) ++ and "," in model ++ ) + -+@end ++ self.assertEqual(type(is_simulator), bool) ++ else: ++ # On non-iOS platforms, calling ios_ver doesn't fail; you get ++ # default values ++ self.assertEqual(result.system, "") ++ self.assertEqual(result.release, "") ++ self.assertEqual(result.model, "") ++ self.assertFalse(result.is_simulator) + -+@implementation AppDelegate ++ # Check the fallback values can be overridden by arguments ++ override = platform.ios_ver("Foo", "Bar", "Whiz", True) ++ self.assertEqual(override.system, "Foo") ++ self.assertEqual(override.release, "Bar") ++ self.assertEqual(override.model, "Whiz") ++ self.assertTrue(override.is_simulator) + -+- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { -+ UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; -+ configuration.delegateClass = [SceneDelegate class]; -+ return configuration; -+} + @unittest.skipIf(support.is_emscripten, "Does not apply to Emscripten") + def test_libc_ver(self): + # check that libc_ver(executable) doesn't raise an exception +@@ -499,7 +581,8 @@ + 'root:xnu-4570.71.2~1/RELEASE_X86_64'), + 'x86_64', 'i386') + arch = ('64bit', '') +- with mock.patch.object(platform, 'uname', return_value=uname), \ ++ with mock.patch.object(sys, "platform", "darwin"), \ ++ mock.patch.object(platform, 'uname', return_value=uname), \ + mock.patch.object(platform, 'architecture', return_value=arch): + for mac_ver, expected_terse, expected in [ + # darwin: mac_ver() returns empty strings +diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py +index e225b8919d1..5d8920ddd60 100644 +--- a/Lib/test/test_posix.py ++++ b/Lib/test/test_posix.py +@@ -1,7 +1,7 @@ + "Test posix functions" + + from test import support +-from test.support import import_helper ++from test.support import is_apple + from test.support import os_helper + from test.support import warnings_helper + from test.support.script_helper import assert_python_ok +@@ -568,6 +568,7 @@ + + @unittest.skipUnless(hasattr(posix, 'confstr'), + 'test needs posix.confstr()') ++ @unittest.skipIf(support.is_apple_mobile, "gh-118201: Test is flaky on iOS") + def test_confstr(self): + self.assertRaises(ValueError, posix.confstr, "CS_garbage") + self.assertEqual(len(posix.confstr("CS_PATH")) > 0, True) +@@ -796,9 +797,10 @@ + check_stat(uid, gid) + self.assertRaises(OSError, chown_func, first_param, 0, -1) + check_stat(uid, gid) +- if 0 not in os.getgroups(): +- self.assertRaises(OSError, chown_func, first_param, -1, 0) +- check_stat(uid, gid) ++ if hasattr(os, 'getgroups'): ++ if 0 not in os.getgroups(): ++ self.assertRaises(OSError, chown_func, first_param, -1, 0) ++ check_stat(uid, gid) + # test illegal types + for t in str, float: + self.assertRaises(TypeError, chown_func, first_param, t(uid), gid) +@@ -1264,8 +1266,8 @@ + self.assertIsInstance(lo, int) + self.assertIsInstance(hi, int) + self.assertGreaterEqual(hi, lo) +- # OSX evidently just returns 15 without checking the argument. +- if sys.platform != "darwin": ++ # Apple plaforms return 15 without checking the argument. ++ if not is_apple: + self.assertRaises(OSError, posix.sched_get_priority_min, -23) + self.assertRaises(OSError, posix.sched_get_priority_max, -23) + +@@ -2058,11 +2060,13 @@ + + + @unittest.skipUnless(hasattr(os, 'posix_spawn'), "test needs os.posix_spawn") ++@support.requires_subprocess() + class TestPosixSpawn(unittest.TestCase, _PosixSpawnMixin): + spawn_func = getattr(posix, 'posix_spawn', None) + + + @unittest.skipUnless(hasattr(os, 'posix_spawnp'), "test needs os.posix_spawnp") ++@support.requires_subprocess() + class TestPosixSpawnP(unittest.TestCase, _PosixSpawnMixin): + spawn_func = getattr(posix, 'posix_spawnp', None) + +diff --git a/Lib/test/test_pty.py b/Lib/test/test_pty.py +index 51e3a46d0df..3f2bac0155f 100644 +--- a/Lib/test/test_pty.py ++++ b/Lib/test/test_pty.py +@@ -1,12 +1,17 @@ +-from test.support import verbose, reap_children +-from test.support.os_helper import TESTFN, unlink ++import sys ++import unittest ++from test.support import ( ++ is_apple_mobile, is_emscripten, is_wasi, reap_children, verbose ++) + from test.support.import_helper import import_module ++from test.support.os_helper import TESTFN, unlink + +-# Skip these tests if termios or fcntl are not available ++# Skip these tests if termios is not available + import_module('termios') +-# fcntl is a proxy for not being one of the wasm32 platforms even though we +-# don't use this module... a proper check for what crashes those is needed. +-import_module("fcntl") + -+- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { -+} -+ -+@end -diff --git a/Apple/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json b/Apple/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json -new file mode 100644 -index 00000000000..eb878970081 ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json -@@ -0,0 +1,11 @@ -+{ -+ "colors" : [ -+ { -+ "idiom" : "universal" -+ } -+ ], -+ "info" : { -+ "author" : "xcode", -+ "version" : 1 -+ } -+} -diff --git a/Apple/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json b/Apple/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json -new file mode 100644 -index 00000000000..13613e3ee1a ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json -@@ -0,0 +1,13 @@ -+{ -+ "images" : [ -+ { -+ "idiom" : "universal", -+ "platform" : "ios", -+ "size" : "1024x1024" -+ } -+ ], -+ "info" : { -+ "author" : "xcode", -+ "version" : 1 -+ } -+} -diff --git a/Apple/testbed/iOSTestbed/Assets.xcassets/Contents.json b/Apple/testbed/iOSTestbed/Assets.xcassets/Contents.json -new file mode 100644 -index 00000000000..73c00596a7f ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/Assets.xcassets/Contents.json -@@ -0,0 +1,6 @@ -+{ -+ "info" : { -+ "author" : "xcode", -+ "version" : 1 -+ } -+} -diff --git a/Apple/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard b/Apple/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard -new file mode 100644 -index 00000000000..5daafe73a86 ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard -@@ -0,0 +1,9 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/Apple/testbed/iOSTestbed/SceneDelegate.h b/Apple/testbed/iOSTestbed/SceneDelegate.h -new file mode 100644 -index 00000000000..264aacadca1 ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/SceneDelegate.h -@@ -0,0 +1,12 @@ -+// -+// SceneDelegate.h -+// iOSTestbed -+// -+ -+#import -+ -+@interface SceneDelegate : UIResponder -+ -+@property (strong, nonatomic) UIWindow *window; -+ -+@end -diff --git a/Apple/testbed/iOSTestbed/SceneDelegate.m b/Apple/testbed/iOSTestbed/SceneDelegate.m -new file mode 100644 -index 00000000000..e82e447985d ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/SceneDelegate.m -@@ -0,0 +1,21 @@ -+// -+// SceneDelegate.m -+// iOSTestbed -+// -+ -+#import "SceneDelegate.h" -+ -+@interface SceneDelegate () -+ -+@end -+ -+@implementation SceneDelegate -+ -+- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { -+ UIWindowScene *windowScene = (UIWindowScene *)scene; -+ self.window = [[UIWindow alloc] initWithWindowScene:windowScene]; -+ self.window.rootViewController = [[UIViewController alloc] init]; -+ [self.window makeKeyAndVisible]; -+} -+ -+@end -diff --git a/Apple/testbed/iOSTestbed/app/README b/Apple/testbed/iOSTestbed/app/README -new file mode 100644 -index 00000000000..46c0e8e2a29 ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/app/README -@@ -0,0 +1,7 @@ -+This folder can contain any Python application code. -+ -+During the build, any binary modules found in this folder will be processed into -+Framework form. -+ -+When the test suite runs, this folder will be on the PYTHONPATH, and will be the -+working directory for the test suite. -diff --git a/Apple/testbed/iOSTestbed/app_packages/README b/Apple/testbed/iOSTestbed/app_packages/README -new file mode 100644 -index 00000000000..02c2beccfbd ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/app_packages/README -@@ -0,0 +1,7 @@ -+This folder can be a target for installing any Python dependencies needed by the -+test suite. -+ -+During the build, any binary modules found in this folder will be processed into -+Framework form. -+ -+When the test suite runs, this folder will be on the PYTHONPATH. -diff --git a/Apple/testbed/iOSTestbed/iOSTestbed-Info.plist b/Apple/testbed/iOSTestbed/iOSTestbed-Info.plist -new file mode 100644 -index 00000000000..3a560656972 ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/iOSTestbed-Info.plist -@@ -0,0 +1,62 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleDisplayName -+ ${PRODUCT_NAME} -+ CFBundleExecutable -+ ${EXECUTABLE_NAME} -+ CFBundleIdentifier -+ org.python.iOSTestbed -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundleName -+ ${PRODUCT_NAME} -+ CFBundlePackageType -+ APPL -+ CFBundleShortVersionString -+ 1.0 -+ CFBundleSignature -+ ???? -+ CFBundleVersion -+ 1 -+ LSRequiresIPhoneOS -+ -+ UIRequiresFullScreen -+ -+ UILaunchStoryboardName -+ Launch Screen -+ UISupportedInterfaceOrientations -+ -+ UIInterfaceOrientationPortrait -+ UIInterfaceOrientationLandscapeLeft -+ UIInterfaceOrientationLandscapeRight -+ -+ UISupportedInterfaceOrientations~ipad -+ -+ UIInterfaceOrientationPortrait -+ UIInterfaceOrientationPortraitUpsideDown -+ UIInterfaceOrientationLandscapeLeft -+ UIInterfaceOrientationLandscapeRight -+ -+ UIApplicationSceneManifest -+ -+ UIApplicationSupportsMultipleScenes -+ -+ UISceneConfigurations -+ -+ UIWindowSceneSessionRoleApplication -+ -+ -+ UISceneConfigurationName -+ Default Configuration -+ UISceneDelegateClassName -+ SceneDelegate -+ -+ -+ -+ -+ -+ -diff --git a/Apple/testbed/iOSTestbed/main.m b/Apple/testbed/iOSTestbed/main.m -new file mode 100644 -index 00000000000..e32bd78c9b4 ---- /dev/null -+++ b/Apple/testbed/iOSTestbed/main.m -@@ -0,0 +1,16 @@ -+// -+// main.m -+// iOSTestbed -+// -+ -+#import -+#import "AppDelegate.h" -+ -+int main(int argc, char * argv[]) { -+ NSString * appDelegateClassName; -+ @autoreleasepool { -+ appDelegateClassName = NSStringFromClass([AppDelegate class]); -+ -+ return UIApplicationMain(argc, argv, nil, appDelegateClassName); -+ } -+} -diff --git a/Apple/testbed/tvOSTestbed.xcodeproj/project.pbxproj b/Apple/testbed/tvOSTestbed.xcodeproj/project.pbxproj -new file mode 100644 -index 00000000000..85e7047dfb7 ---- /dev/null -+++ b/Apple/testbed/tvOSTestbed.xcodeproj/project.pbxproj -@@ -0,0 +1,506 @@ -+// !$*UTF8*$! -+{ -+ archiveVersion = 1; -+ classes = { -+ }; -+ objectVersion = 77; -+ objects = { -+ -+/* Begin PBXBuildFile section */ -+ EE7C8A1E2DCD6FF3003206DB /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE7C8A1C2DCD6FF3003206DB /* Python.xcframework */; }; -+ EE7C8A1F2DCD70CD003206DB /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE7C8A1C2DCD6FF3003206DB /* Python.xcframework */; }; -+ EE7C8A202DCD70CD003206DB /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EE7C8A1C2DCD6FF3003206DB /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; -+/* End PBXBuildFile section */ -+ -+/* Begin PBXContainerItemProxy section */ -+ EE989E662DCD6E7A0036B268 /* PBXContainerItemProxy */ = { -+ isa = PBXContainerItemProxy; -+ containerPortal = EE989E462DCD6E780036B268 /* Project object */; -+ proxyType = 1; -+ remoteGlobalIDString = EE989E4D2DCD6E780036B268; -+ remoteInfo = tvOSTestbed; -+ }; -+/* End PBXContainerItemProxy section */ -+ -+/* Begin PBXCopyFilesBuildPhase section */ -+ EE7C8A212DCD70CD003206DB /* Embed Frameworks */ = { -+ isa = PBXCopyFilesBuildPhase; -+ buildActionMask = 2147483647; -+ dstPath = ""; -+ dstSubfolderSpec = 10; -+ files = ( -+ EE7C8A202DCD70CD003206DB /* Python.xcframework in Embed Frameworks */, -+ ); -+ name = "Embed Frameworks"; -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXCopyFilesBuildPhase section */ -+ -+/* Begin PBXFileReference section */ -+ 6077B3802E82A4BE00E3D6A3 /* tvOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = tvOSTestbed.xctestplan; sourceTree = ""; }; -+ EE7C8A1C2DCD6FF3003206DB /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; -+ EE989E4E2DCD6E780036B268 /* tvOSTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = tvOSTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; }; -+ EE989E652DCD6E7A0036B268 /* TestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; -+/* End PBXFileReference section */ -+ -+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ -+ 6077B37F2E81892A00E3D6A3 /* Exceptions for "tvOSTestbed" folder in "tvOSTestbed" target */ = { -+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; -+ membershipExceptions = ( -+ "tvOSTestbed-Info.plist", -+ ); -+ target = EE989E4D2DCD6E780036B268 /* tvOSTestbed */; -+ }; -+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ -+ -+/* Begin PBXFileSystemSynchronizedRootGroup section */ -+ EE989E502DCD6E780036B268 /* tvOSTestbed */ = { -+ isa = PBXFileSystemSynchronizedRootGroup; -+ exceptions = ( -+ 6077B37F2E81892A00E3D6A3 /* Exceptions for "tvOSTestbed" folder in "tvOSTestbed" target */, -+ ); -+ explicitFolders = ( -+ app, -+ app_packages, -+ ); -+ path = tvOSTestbed; -+ sourceTree = ""; -+ }; -+ EE989E682DCD6E7A0036B268 /* TestbedTests */ = { -+ isa = PBXFileSystemSynchronizedRootGroup; -+ path = TestbedTests; -+ sourceTree = ""; -+ }; -+/* End PBXFileSystemSynchronizedRootGroup section */ -+ -+/* Begin PBXFrameworksBuildPhase section */ -+ EE989E4B2DCD6E780036B268 /* Frameworks */ = { -+ isa = PBXFrameworksBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ EE7C8A1F2DCD70CD003206DB /* Python.xcframework in Frameworks */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+ EE989E622DCD6E7A0036B268 /* Frameworks */ = { -+ isa = PBXFrameworksBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ EE7C8A1E2DCD6FF3003206DB /* Python.xcframework in Frameworks */, -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXFrameworksBuildPhase section */ -+ -+/* Begin PBXGroup section */ -+ EE989E452DCD6E780036B268 = { -+ isa = PBXGroup; -+ children = ( -+ 6077B3802E82A4BE00E3D6A3 /* tvOSTestbed.xctestplan */, -+ EE7C8A1C2DCD6FF3003206DB /* Python.xcframework */, -+ EE989E502DCD6E780036B268 /* tvOSTestbed */, -+ EE989E682DCD6E7A0036B268 /* TestbedTests */, -+ EE989E4F2DCD6E780036B268 /* Products */, -+ ); -+ sourceTree = ""; -+ }; -+ EE989E4F2DCD6E780036B268 /* Products */ = { -+ isa = PBXGroup; -+ children = ( -+ EE989E4E2DCD6E780036B268 /* tvOSTestbed.app */, -+ EE989E652DCD6E7A0036B268 /* TestbedTests.xctest */, -+ ); -+ name = Products; -+ sourceTree = ""; -+ }; -+/* End PBXGroup section */ -+ -+/* Begin PBXNativeTarget section */ -+ EE989E4D2DCD6E780036B268 /* tvOSTestbed */ = { -+ isa = PBXNativeTarget; -+ buildConfigurationList = EE989E792DCD6E7A0036B268 /* Build configuration list for PBXNativeTarget "tvOSTestbed" */; -+ buildPhases = ( -+ EE989E4A2DCD6E780036B268 /* Sources */, -+ EE989E4B2DCD6E780036B268 /* Frameworks */, -+ EE989E4C2DCD6E780036B268 /* Resources */, -+ EE7C8A222DCD70F4003206DB /* Process Python libraries */, -+ EE7C8A212DCD70CD003206DB /* Embed Frameworks */, -+ ); -+ buildRules = ( -+ ); -+ dependencies = ( -+ ); -+ fileSystemSynchronizedGroups = ( -+ EE989E502DCD6E780036B268 /* tvOSTestbed */, -+ ); -+ name = tvOSTestbed; -+ packageProductDependencies = ( -+ ); -+ productName = tvOSTestbed; -+ productReference = EE989E4E2DCD6E780036B268 /* tvOSTestbed.app */; -+ productType = "com.apple.product-type.application"; -+ }; -+ EE989E642DCD6E7A0036B268 /* TestbedTests */ = { -+ isa = PBXNativeTarget; -+ buildConfigurationList = EE989E7C2DCD6E7A0036B268 /* Build configuration list for PBXNativeTarget "TestbedTests" */; -+ buildPhases = ( -+ EE989E612DCD6E7A0036B268 /* Sources */, -+ EE989E622DCD6E7A0036B268 /* Frameworks */, -+ EE989E632DCD6E7A0036B268 /* Resources */, -+ ); -+ buildRules = ( -+ ); -+ dependencies = ( -+ EE989E672DCD6E7A0036B268 /* PBXTargetDependency */, -+ ); -+ fileSystemSynchronizedGroups = ( -+ EE989E682DCD6E7A0036B268 /* TestbedTests */, -+ ); -+ name = TestbedTests; -+ packageProductDependencies = ( -+ ); -+ productName = TestbedTests; -+ productReference = EE989E652DCD6E7A0036B268 /* TestbedTests.xctest */; -+ productType = "com.apple.product-type.bundle.unit-test"; -+ }; -+/* End PBXNativeTarget section */ -+ -+/* Begin PBXProject section */ -+ EE989E462DCD6E780036B268 /* Project object */ = { -+ isa = PBXProject; -+ attributes = { -+ BuildIndependentTargetsInParallel = 1; -+ LastUpgradeCheck = 1620; -+ TargetAttributes = { -+ EE989E4D2DCD6E780036B268 = { -+ CreatedOnToolsVersion = 16.2; -+ }; -+ EE989E642DCD6E7A0036B268 = { -+ CreatedOnToolsVersion = 16.2; -+ TestTargetID = EE989E4D2DCD6E780036B268; -+ }; -+ }; -+ }; -+ buildConfigurationList = EE989E492DCD6E780036B268 /* Build configuration list for PBXProject "tvOSTestbed" */; -+ developmentRegion = en; -+ hasScannedForEncodings = 0; -+ knownRegions = ( -+ en, -+ Base, -+ ); -+ mainGroup = EE989E452DCD6E780036B268; -+ minimizedProjectReferenceProxies = 1; -+ preferredProjectObjectVersion = 77; -+ productRefGroup = EE989E4F2DCD6E780036B268 /* Products */; -+ projectDirPath = ""; -+ projectRoot = ""; -+ targets = ( -+ EE989E4D2DCD6E780036B268 /* tvOSTestbed */, -+ EE989E642DCD6E7A0036B268 /* TestbedTests */, -+ ); -+ }; -+/* End PBXProject section */ -+ -+/* Begin PBXResourcesBuildPhase section */ -+ EE989E4C2DCD6E780036B268 /* Resources */ = { -+ isa = PBXResourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+ EE989E632DCD6E7A0036B268 /* Resources */ = { -+ isa = PBXResourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXResourcesBuildPhase section */ -+ -+/* Begin PBXShellScriptBuildPhase section */ -+ EE7C8A222DCD70F4003206DB /* Process Python libraries */ = { -+ isa = PBXShellScriptBuildPhase; -+ alwaysOutOfDate = 1; -+ buildActionMask = 2147483647; -+ files = ( -+ ); -+ inputFileListPaths = ( -+ ); -+ inputPaths = ( -+ ); -+ name = "Process Python libraries"; -+ outputFileListPaths = ( -+ ); -+ outputPaths = ( -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ shellPath = /bin/sh; -+ shellScript = "set -e\n\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\n\ninstall_python Python.xcframework app app_packages\n"; -+ showEnvVarsInLog = 0; -+ }; -+/* End PBXShellScriptBuildPhase section */ -+ -+/* Begin PBXSourcesBuildPhase section */ -+ EE989E4A2DCD6E780036B268 /* Sources */ = { -+ isa = PBXSourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+ EE989E612DCD6E7A0036B268 /* Sources */ = { -+ isa = PBXSourcesBuildPhase; -+ buildActionMask = 2147483647; -+ files = ( -+ ); -+ runOnlyForDeploymentPostprocessing = 0; -+ }; -+/* End PBXSourcesBuildPhase section */ -+ -+/* Begin PBXTargetDependency section */ -+ EE989E672DCD6E7A0036B268 /* PBXTargetDependency */ = { -+ isa = PBXTargetDependency; -+ target = EE989E4D2DCD6E780036B268 /* tvOSTestbed */; -+ targetProxy = EE989E662DCD6E7A0036B268 /* PBXContainerItemProxy */; -+ }; -+/* End PBXTargetDependency section */ -+ -+/* Begin XCBuildConfiguration section */ -+ EE989E772DCD6E7A0036B268 /* Debug */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ALWAYS_SEARCH_USER_PATHS = NO; -+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; -+ CLANG_ANALYZER_NONNULL = YES; -+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; -+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; -+ CLANG_ENABLE_MODULES = YES; -+ CLANG_ENABLE_OBJC_ARC = YES; -+ CLANG_ENABLE_OBJC_WEAK = YES; -+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; -+ CLANG_WARN_BOOL_CONVERSION = YES; -+ CLANG_WARN_COMMA = YES; -+ CLANG_WARN_CONSTANT_CONVERSION = YES; -+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; -+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; -+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; -+ CLANG_WARN_EMPTY_BODY = YES; -+ CLANG_WARN_ENUM_CONVERSION = YES; -+ CLANG_WARN_INFINITE_RECURSION = YES; -+ CLANG_WARN_INT_CONVERSION = YES; -+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; -+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; -+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; -+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; -+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; -+ CLANG_WARN_STRICT_PROTOTYPES = YES; -+ CLANG_WARN_SUSPICIOUS_MOVE = YES; -+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; -+ CLANG_WARN_UNREACHABLE_CODE = YES; -+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; -+ COPY_PHASE_STRIP = NO; -+ DEBUG_INFORMATION_FORMAT = dwarf; -+ ENABLE_STRICT_OBJC_MSGSEND = YES; -+ ENABLE_TESTABILITY = YES; -+ ENABLE_USER_SCRIPT_SANDBOXING = NO; -+ FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)"; -+ GCC_C_LANGUAGE_STANDARD = gnu17; -+ GCC_DYNAMIC_NO_PIC = NO; -+ GCC_NO_COMMON_BLOCKS = YES; -+ GCC_OPTIMIZATION_LEVEL = 0; -+ GCC_PREPROCESSOR_DEFINITIONS = ( -+ "DEBUG=1", -+ "$(inherited)", -+ ); -+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; -+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; -+ GCC_WARN_UNDECLARED_SELECTOR = YES; -+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; -+ GCC_WARN_UNUSED_FUNCTION = YES; -+ GCC_WARN_UNUSED_VARIABLE = YES; -+ HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/Python.framework/Headers"; -+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; -+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; -+ MTL_FAST_MATH = YES; -+ ONLY_ACTIVE_ARCH = YES; -+ SDKROOT = appletvos; -+ TVOS_DEPLOYMENT_TARGET = 18.2; -+ }; -+ name = Debug; -+ }; -+ EE989E782DCD6E7A0036B268 /* Release */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ALWAYS_SEARCH_USER_PATHS = NO; -+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; -+ CLANG_ANALYZER_NONNULL = YES; -+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; -+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; -+ CLANG_ENABLE_MODULES = YES; -+ CLANG_ENABLE_OBJC_ARC = YES; -+ CLANG_ENABLE_OBJC_WEAK = YES; -+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; -+ CLANG_WARN_BOOL_CONVERSION = YES; -+ CLANG_WARN_COMMA = YES; -+ CLANG_WARN_CONSTANT_CONVERSION = YES; -+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; -+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; -+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; -+ CLANG_WARN_EMPTY_BODY = YES; -+ CLANG_WARN_ENUM_CONVERSION = YES; -+ CLANG_WARN_INFINITE_RECURSION = YES; -+ CLANG_WARN_INT_CONVERSION = YES; -+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; -+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; -+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; -+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; -+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; -+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; -+ CLANG_WARN_STRICT_PROTOTYPES = YES; -+ CLANG_WARN_SUSPICIOUS_MOVE = YES; -+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; -+ CLANG_WARN_UNREACHABLE_CODE = YES; -+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; -+ COPY_PHASE_STRIP = NO; -+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; -+ ENABLE_NS_ASSERTIONS = NO; -+ ENABLE_STRICT_OBJC_MSGSEND = YES; -+ ENABLE_TESTABILITY = YES; -+ ENABLE_USER_SCRIPT_SANDBOXING = NO; -+ FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)"; -+ GCC_C_LANGUAGE_STANDARD = gnu17; -+ GCC_NO_COMMON_BLOCKS = YES; -+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; -+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; -+ GCC_WARN_UNDECLARED_SELECTOR = YES; -+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; -+ GCC_WARN_UNUSED_FUNCTION = YES; -+ GCC_WARN_UNUSED_VARIABLE = YES; -+ HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/Python.framework/Headers"; -+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; -+ MTL_ENABLE_DEBUG_INFO = NO; -+ MTL_FAST_MATH = YES; -+ SDKROOT = appletvos; -+ TVOS_DEPLOYMENT_TARGET = 18.2; -+ VALIDATE_PRODUCT = YES; -+ }; -+ name = Release; -+ }; -+ EE989E7A2DCD6E7A0036B268 /* Debug */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; -+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ GENERATE_INFOPLIST_FILE = NO; -+ INFOPLIST_FILE = "tvOSTestbed/tvOSTestbed-Info.plist"; -+ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; -+ INFOPLIST_KEY_UIMainStoryboardFile = Main; -+ INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; -+ LD_RUNPATH_SEARCH_PATHS = ( -+ "$(inherited)", -+ "@executable_path/Frameworks", -+ ); -+ MARKETING_VERSION = 1.0; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.tvOSTestbed; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = YES; -+ TARGETED_DEVICE_FAMILY = 3; -+ }; -+ name = Debug; -+ }; -+ EE989E7B2DCD6E7A0036B268 /* Release */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; -+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ GENERATE_INFOPLIST_FILE = NO; -+ INFOPLIST_FILE = "tvOSTestbed/tvOSTestbed-Info.plist"; -+ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; -+ INFOPLIST_KEY_UIMainStoryboardFile = Main; -+ INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; -+ LD_RUNPATH_SEARCH_PATHS = ( -+ "$(inherited)", -+ "@executable_path/Frameworks", -+ ); -+ MARKETING_VERSION = 1.0; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.tvOSTestbed; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = YES; -+ TARGETED_DEVICE_FAMILY = 3; -+ }; -+ name = Release; -+ }; -+ EE989E7D2DCD6E7A0036B268 /* Debug */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ BUNDLE_LOADER = "$(TEST_HOST)"; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ GENERATE_INFOPLIST_FILE = YES; -+ MARKETING_VERSION = 1.0; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.TestbedTests; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = NO; -+ TARGETED_DEVICE_FAMILY = 3; -+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tvOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/tvOSTestbed"; -+ TVOS_DEPLOYMENT_TARGET = 18.2; -+ }; -+ name = Debug; -+ }; -+ EE989E7E2DCD6E7A0036B268 /* Release */ = { -+ isa = XCBuildConfiguration; -+ buildSettings = { -+ BUNDLE_LOADER = "$(TEST_HOST)"; -+ CODE_SIGN_STYLE = Automatic; -+ CURRENT_PROJECT_VERSION = 1; -+ GENERATE_INFOPLIST_FILE = YES; -+ MARKETING_VERSION = 1.0; -+ PRODUCT_BUNDLE_IDENTIFIER = org.python.TestbedTests; -+ PRODUCT_NAME = "$(TARGET_NAME)"; -+ SWIFT_EMIT_LOC_STRINGS = NO; -+ TARGETED_DEVICE_FAMILY = 3; -+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tvOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/tvOSTestbed"; -+ TVOS_DEPLOYMENT_TARGET = 18.2; -+ }; -+ name = Release; -+ }; -+/* End XCBuildConfiguration section */ -+ -+/* Begin XCConfigurationList section */ -+ EE989E492DCD6E780036B268 /* Build configuration list for PBXProject "tvOSTestbed" */ = { -+ isa = XCConfigurationList; -+ buildConfigurations = ( -+ EE989E772DCD6E7A0036B268 /* Debug */, -+ EE989E782DCD6E7A0036B268 /* Release */, -+ ); -+ defaultConfigurationIsVisible = 0; -+ defaultConfigurationName = Release; -+ }; -+ EE989E792DCD6E7A0036B268 /* Build configuration list for PBXNativeTarget "tvOSTestbed" */ = { -+ isa = XCConfigurationList; -+ buildConfigurations = ( -+ EE989E7A2DCD6E7A0036B268 /* Debug */, -+ EE989E7B2DCD6E7A0036B268 /* Release */, -+ ); -+ defaultConfigurationIsVisible = 0; -+ defaultConfigurationName = Release; -+ }; -+ EE989E7C2DCD6E7A0036B268 /* Build configuration list for PBXNativeTarget "TestbedTests" */ = { -+ isa = XCConfigurationList; -+ buildConfigurations = ( -+ EE989E7D2DCD6E7A0036B268 /* Debug */, -+ EE989E7E2DCD6E7A0036B268 /* Release */, -+ ); -+ defaultConfigurationIsVisible = 0; -+ defaultConfigurationName = Release; -+ }; -+/* End XCConfigurationList section */ -+ }; -+ rootObject = EE989E462DCD6E780036B268 /* Project object */; -+} -diff --git a/Apple/testbed/tvOSTestbed.xcodeproj/xcshareddata/xcschemes/tvOSTestbed.xcscheme b/Apple/testbed/tvOSTestbed.xcodeproj/xcshareddata/xcschemes/tvOSTestbed.xcscheme -new file mode 100644 -index 00000000000..c3f3f894a1f ---- /dev/null -+++ b/Apple/testbed/tvOSTestbed.xcodeproj/xcshareddata/xcschemes/tvOSTestbed.xcscheme -@@ -0,0 +1,97 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/Apple/testbed/tvOSTestbed.xctestplan b/Apple/testbed/tvOSTestbed.xctestplan -new file mode 100644 -index 00000000000..f996facc178 ---- /dev/null -+++ b/Apple/testbed/tvOSTestbed.xctestplan -@@ -0,0 +1,46 @@ -+{ -+ "configurations" : [ -+ { -+ "id" : "F5A95CE4-1ADE-4A6E-A0E1-CDBAE26DF0C5", -+ "name" : "Test Scheme Action", -+ "options" : { -+ -+ } -+ } -+ ], -+ "defaultOptions" : { -+ "commandLineArgumentEntries" : [ -+ { -+ "argument" : "test" -+ }, -+ { -+ "argument" : "-uall" -+ }, -+ { -+ "argument" : "--single-process" -+ }, -+ { -+ "argument" : "--rerun" -+ }, -+ { -+ "argument" : "-W" -+ } -+ ], -+ "targetForVariableExpansion" : { -+ "containerPath" : "container:tvOSTestbed.xcodeproj", -+ "identifier" : "607A66112B0EFA380010BFC8", -+ "name" : "tvOSTestbed" -+ } -+ }, -+ "testTargets" : [ -+ { -+ "parallelizable" : false, -+ "target" : { -+ "containerPath" : "container:tvOSTestbed.xcodeproj", -+ "identifier" : "EE989E642DCD6E7A0036B268", -+ "name" : "TestbedTests" -+ } -+ } -+ ], -+ "version" : 1 -+} -\ No newline at end of file -diff --git a/Apple/testbed/tvOSTestbed/AppDelegate.h b/Apple/testbed/tvOSTestbed/AppDelegate.h -new file mode 100644 -index 00000000000..112c9ed64b8 ---- /dev/null -+++ b/Apple/testbed/tvOSTestbed/AppDelegate.h -@@ -0,0 +1,11 @@ -+// -+// AppDelegate.h -+// tvOSTestbed -+// -+ -+#import -+ -+@interface AppDelegate : UIResponder -+ -+ -+@end -diff --git a/Apple/testbed/tvOSTestbed/AppDelegate.m b/Apple/testbed/tvOSTestbed/AppDelegate.m -new file mode 100644 -index 00000000000..bd91fb2d7d6 ---- /dev/null -+++ b/Apple/testbed/tvOSTestbed/AppDelegate.m -@@ -0,0 +1,19 @@ -+// -+// AppDelegate.m -+// tvOSTestbed -+// -+ -+#import "AppDelegate.h" -+ -+@interface AppDelegate () -+ -+@end -+ -+@implementation AppDelegate -+ -+ -+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { -+ return YES; -+} -+ -+@end -diff --git a/Apple/testbed/tvOSTestbed/Base.lproj/LaunchScreen.storyboard b/Apple/testbed/tvOSTestbed/Base.lproj/LaunchScreen.storyboard -new file mode 100644 -index 00000000000..660ba53de4f ---- /dev/null -+++ b/Apple/testbed/tvOSTestbed/Base.lproj/LaunchScreen.storyboard -@@ -0,0 +1,24 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/Apple/testbed/tvOSTestbed/app/README b/Apple/testbed/tvOSTestbed/app/README -new file mode 100644 -index 00000000000..46c0e8e2a29 ---- /dev/null -+++ b/Apple/testbed/tvOSTestbed/app/README -@@ -0,0 +1,7 @@ -+This folder can contain any Python application code. -+ -+During the build, any binary modules found in this folder will be processed into -+Framework form. -+ -+When the test suite runs, this folder will be on the PYTHONPATH, and will be the -+working directory for the test suite. -diff --git a/Apple/testbed/tvOSTestbed/app_packages/README b/Apple/testbed/tvOSTestbed/app_packages/README -new file mode 100644 -index 00000000000..02c2beccfbd ---- /dev/null -+++ b/Apple/testbed/tvOSTestbed/app_packages/README -@@ -0,0 +1,7 @@ -+This folder can be a target for installing any Python dependencies needed by the -+test suite. -+ -+During the build, any binary modules found in this folder will be processed into -+Framework form. -+ -+When the test suite runs, this folder will be on the PYTHONPATH. -diff --git a/Apple/testbed/tvOSTestbed/main.m b/Apple/testbed/tvOSTestbed/main.m -new file mode 100644 -index 00000000000..d5808fbb933 ---- /dev/null -+++ b/Apple/testbed/tvOSTestbed/main.m -@@ -0,0 +1,16 @@ -+// -+// main.m -+// tvOSTestbed -+// -+ -+#import -+#import "AppDelegate.h" -+ -+int main(int argc, char * argv[]) { -+ NSString * appDelegateClassName; -+ @autoreleasepool { -+ appDelegateClassName = NSStringFromClass([AppDelegate class]); -+ -+ return UIApplicationMain(argc, argv, nil, appDelegateClassName); -+ } -+} -diff --git a/Apple/testbed/tvOSTestbed/tvOSTestbed-Info.plist b/Apple/testbed/tvOSTestbed/tvOSTestbed-Info.plist -new file mode 100644 -index 00000000000..f08f6098999 ---- /dev/null -+++ b/Apple/testbed/tvOSTestbed/tvOSTestbed-Info.plist -@@ -0,0 +1,52 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleDisplayName -+ ${PRODUCT_NAME} -+ CFBundleExecutable -+ ${EXECUTABLE_NAME} -+ CFBundleIdentifier -+ org.python.tvOSTestbed -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundleName -+ ${PRODUCT_NAME} -+ CFBundlePackageType -+ APPL -+ CFBundleShortVersionString -+ 1.0 -+ CFBundleSignature -+ ???? -+ CFBundleVersion -+ 1 -+ LSRequiresIPhoneOS -+ -+ UIRequiresFullScreen -+ -+ UILaunchStoryboardName -+ Launch Screen -+ UISupportedInterfaceOrientations -+ -+ UIInterfaceOrientationPortrait -+ UIInterfaceOrientationLandscapeLeft -+ UIInterfaceOrientationLandscapeRight -+ -+ UISupportedInterfaceOrientations~ipad -+ -+ UIInterfaceOrientationPortrait -+ UIInterfaceOrientationPortraitUpsideDown -+ UIInterfaceOrientationLandscapeLeft -+ UIInterfaceOrientationLandscapeRight -+ -+ UIApplicationSceneManifest -+ -+ UIApplicationSupportsMultipleScenes -+ -+ UISceneConfigurations -+ -+ -+ -+ -diff --git a/Apple/tvOS/README.rst b/Apple/tvOS/README.rst -new file mode 100644 -index 00000000000..1f793252caf ---- /dev/null -+++ b/Apple/tvOS/README.rst -@@ -0,0 +1,108 @@ -+===================== -+Python on tvOS README -+===================== -+ -+:Authors: -+ Russell Keith-Magee (2023-11) -+ -+This document provides a quick overview of some tvOS specific features in the -+Python distribution. -+ -+Compilers for building on tvOS -+============================== -+ -+Building for tvOS requires the use of Apple's Xcode tooling. It is strongly -+recommended that you use the most recent stable release of Xcode, on the -+most recently released macOS. -+ -+tvOS specific arguments to configure -+=================================== -+ -+* ``--enable-framework[=DIR]`` -+ -+ This argument specifies the location where the Python.framework will -+ be installed. -+ -+* ``--with-framework-name=NAME`` -+ -+ Specify the name for the python framework, defaults to ``Python``. -+ -+ -+Building and using Python on tvOS -+================================= -+ -+ABIs and Architectures -+---------------------- -+ -+tvOS apps can be deployed on physical devices, and on the tvOS simulator. -+Although the API used on these devices is identical, the ABI is different - you -+need to link against different libraries for an tvOS device build -+(``appletvos``) or an tvOS simulator build (``appletvsimulator``). 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. -+ -+tvOS can also support different CPU architectures within each ABI. At present, -+there is only a single support ed architecture on physical devices - ARM64. -+However, the *simulator* supports 2 architectures - ARM64 (for running on Apple -+Silicon machines), and x86_64 (for running on older Intel-based machines.) -+ -+To support multiple CPU architectures on a single platform, Apple uses a "fat -+binary" format - a single physical file that contains support for multiple -+architectures. -+ -+How do I build Python for tvOS? -+------------------------------- -+ -+The Python build system will build a ``Python.framework`` that supports a -+*single* ABI with a *single* architecture. If you want to use Python in an tvOS -+project, you need to: -+ -+1. Produce multiple ``Python.framework`` builds, one for each ABI and architecture; -+2. Merge the binaries for each architecture on a given ABI into a single "fat" binary; -+3. Merge the "fat" frameworks for each ABI into a single XCframework. -+ -+tvOS builds of Python *must* be constructed as framework builds. To support this, -+you must provide the ``--enable-framework`` flag when configuring the build. -+ -+The build also requires the use of cross-compilation. The commands for building -+Python for tvOS will look somethign like:: -+ -+ $ ./configure \ -+ --enable-framework=/path/to/install \ -+ --host=aarch64-apple-tvos \ -+ --build=aarch64-apple-darwin \ -+ --with-build-python=/path/to/python.exe -+ $ make -+ $ make install -+ -+In this invocation: -+ -+* ``/path/to/install`` is the location where the final Python.framework will be -+ output. -+ -+* ``--host`` is the architecture and ABI that you want to build, in GNU compiler -+ triple format. This will be one of: -+ -+ - ``aarch64-apple-tvos`` for ARM64 tvOS devices. -+ - ``aarch64-apple-tvos-simulator`` for the tvOS simulator running on Apple -+ Silicon devices. -+ - ``x86_64-apple-tvos-simulator`` for the tvOS simulator running on Intel -+ devices. -+ -+* ``--build`` is the GNU compiler triple for the machine that will be running -+ the compiler. This is one of: -+ -+ - ``aarch64-apple-darwin`` for Apple Silicon devices. -+ - ``x86_64-apple-darwin`` for Intel devices. -+ -+* ``/path/to/python.exe`` is the path to a Python binary on the machine that -+ will be running the compiler. This is needed because the Python compilation -+ process involves running some Python code. On a normal desktop build of -+ Python, you can compile a python interpreter and then use that interpreter to -+ run Python code. However, the binaries produced for tvOS won't run on macOS, so -+ you need to provide an external Python interpreter. This interpreter must be -+ the version as the Python that is being compiled. -+ -+Using a framework-based Python on tvOS -+====================================== -diff --git a/Apple/tvOS/Resources/Info.plist.in b/Apple/tvOS/Resources/Info.plist.in -new file mode 100644 -index 00000000000..ab3050804b8 ---- /dev/null -+++ b/Apple/tvOS/Resources/Info.plist.in -@@ -0,0 +1,34 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleExecutable -+ Python -+ CFBundleGetInfoString -+ Python Runtime and Library -+ CFBundleIdentifier -+ @PYTHONFRAMEWORKIDENTIFIER@ -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundleName -+ Python -+ CFBundlePackageType -+ FMWK -+ CFBundleShortVersionString -+ %VERSION% -+ CFBundleLongVersionString -+ %VERSION%, (c) 2001-2024 Python Software Foundation. -+ CFBundleSignature -+ ???? -+ CFBundleVersion -+ 1 -+ CFBundleSupportedPlatforms -+ -+ tvOS -+ -+ MinimumOSVersion -+ @TVOS_DEPLOYMENT_TARGET@ -+ -+ -diff --git a/Apple/tvOS/Resources/bin/arm64-apple-tvos-ar b/Apple/tvOS/Resources/bin/arm64-apple-tvos-ar -new file mode 100755 -index 00000000000..e302748a13c ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-ar -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvos${TVOS_SDK_VERSION} ar "$@" -diff --git a/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang b/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang -new file mode 100755 -index 00000000000..7fb6d3d901c ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvos${TVOS_SDK_VERSION} clang -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET} "$@" -diff --git a/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang++ -new file mode 100755 -index 00000000000..33bfb1367c3 ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvos${TVOS_SDK_VERSION} clang++ -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET} "$@" -diff --git a/Apple/tvOS/Resources/bin/arm64-apple-tvos-cpp b/Apple/tvOS/Resources/bin/arm64-apple-tvos-cpp -new file mode 100755 -index 00000000000..641c1bc8d18 ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-cpp -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvos${TVOS_SDK_VERSION} clang -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET} -E "$@" -diff --git a/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-ar b/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-ar -new file mode 100755 -index 00000000000..87ef5015aae ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-ar -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} ar "$@" -diff --git a/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang b/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang -new file mode 100755 -index 00000000000..c8719cb0318 ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang++ -new file mode 100755 -index 00000000000..e3f0e720f7f ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang++ -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-cpp b/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-cpp -new file mode 100755 -index 00000000000..f9a37b72a61 ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-cpp -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator -E "$@" -diff --git a/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-strip b/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-strip -new file mode 100755 -index 00000000000..a8cce95233e ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} strip -arch arm64 "$@" -diff --git a/Apple/tvOS/Resources/bin/arm64-apple-tvos-strip b/Apple/tvOS/Resources/bin/arm64-apple-tvos-strip -new file mode 100755 -index 00000000000..ee1d2b95ff1 ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${TVOS_SDK_VERSION} strip -arch arm64 "$@" -diff --git a/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-ar b/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-ar -new file mode 100755 -index 00000000000..87ef5015aae ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-ar -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} ar "$@" -diff --git a/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang b/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang -new file mode 100755 -index 00000000000..ea0cc26cbd9 ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang -target x86_64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang++ b/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang++ -new file mode 100755 -index 00000000000..f18f3603169 ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang++ -target x86_64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-cpp b/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-cpp -new file mode 100755 -index 00000000000..b98054d1ce2 ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-cpp -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang -target x86_64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator -E "$@" -diff --git a/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-strip b/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-strip -new file mode 100755 -index 00000000000..f6a884b4aef ---- /dev/null -+++ b/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} strip -arch x86_64 "$@" -diff --git a/Apple/tvOS/Resources/pyconfig.h b/Apple/tvOS/Resources/pyconfig.h -new file mode 100644 -index 00000000000..4acff2c6051 ---- /dev/null -+++ b/Apple/tvOS/Resources/pyconfig.h -@@ -0,0 +1,7 @@ -+#ifdef __arm64__ -+#include "pyconfig-arm64.h" -+#endif -+ -+#ifdef __x86_64__ -+#include "pyconfig-x86_64.h" -+#endif -diff --git a/Apple/watchOS/README.rst b/Apple/watchOS/README.rst -new file mode 100644 -index 00000000000..35221478452 ---- /dev/null -+++ b/Apple/watchOS/README.rst -@@ -0,0 +1,108 @@ -+======================== -+Python on watchOS README -+======================== -+ -+:Authors: -+ Russell Keith-Magee (2023-11) -+ -+This document provides a quick overview of some watchOS specific features in the -+Python distribution. -+ -+Compilers for building on watchOS -+================================= -+ -+Building for watchOS requires the use of Apple's Xcode tooling. It is strongly -+recommended that you use the most recent stable release of Xcode, on the -+most recently released macOS. -+ -+watchOS specific arguments to configure -+======================================= -+ -+* ``--enable-framework[=DIR]`` -+ -+ This argument specifies the location where the Python.framework will -+ be installed. -+ -+* ``--with-framework-name=NAME`` -+ -+ Specify the name for the python framework, defaults to ``Python``. -+ -+ -+Building and using Python on watchOS -+==================================== -+ -+ABIs and Architectures -+---------------------- -+ -+watchOS apps can be deployed on physical devices, and on the watchOS simulator. -+Although the API used on these devices is identical, the ABI is different - you -+need to link against different libraries for an watchOS device build -+(``watchos``) or an watchOS simulator build (``watchsimulator``). 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. -+ -+watchOS can also support different CPU architectures within each ABI. At present, -+there is only a single support ed architecture on physical devices - ARM64. -+However, the *simulator* supports 2 architectures - ARM64 (for running on Apple -+Silicon machines), and x86_64 (for running on older Intel-based machines.) -+ -+To support multiple CPU architectures on a single platform, Apple uses a "fat -+binary" format - a single physical file that contains support for multiple -+architectures. -+ -+How do I build Python for watchOS? -+------------------------------- -+ -+The Python build system will build a ``Python.framework`` that supports a -+*single* ABI with a *single* architecture. If you want to use Python in an watchOS -+project, you need to: -+ -+1. Produce multiple ``Python.framework`` builds, one for each ABI and architecture; -+2. Merge the binaries for each architecture on a given ABI into a single "fat" binary; -+3. Merge the "fat" frameworks for each ABI into a single XCframework. -+ -+watchOS builds of Python *must* be constructed as framework builds. To support this, -+you must provide the ``--enable-framework`` flag when configuring the build. -+ -+The build also requires the use of cross-compilation. The commands for building -+Python for watchOS will look somethign like:: -+ -+ $ ./configure \ -+ --enable-framework=/path/to/install \ -+ --host=aarch64-apple-watchos \ -+ --build=aarch64-apple-darwin \ -+ --with-build-python=/path/to/python.exe -+ $ make -+ $ make install -+ -+In this invocation: -+ -+* ``/path/to/install`` is the location where the final Python.framework will be -+ output. -+ -+* ``--host`` is the architecture and ABI that you want to build, in GNU compiler -+ triple format. This will be one of: -+ -+ - ``arm64_32-apple-watchos`` for ARM64-32 watchOS devices. -+ - ``aarch64-apple-watchos-simulator`` for the watchOS simulator running on Apple -+ Silicon devices. -+ - ``x86_64-apple-watchos-simulator`` for the watchOS simulator running on Intel -+ devices. -+ -+* ``--build`` is the GNU compiler triple for the machine that will be running -+ the compiler. This is one of: -+ -+ - ``aarch64-apple-darwin`` for Apple Silicon devices. -+ - ``x86_64-apple-darwin`` for Intel devices. -+ -+* ``/path/to/python.exe`` is the path to a Python binary on the machine that -+ will be running the compiler. This is needed because the Python compilation -+ process involves running some Python code. On a normal desktop build of -+ Python, you can compile a python interpreter and then use that interpreter to -+ run Python code. However, the binaries produced for watchOS won't run on macOS, so -+ you need to provide an external Python interpreter. This interpreter must be -+ the version as the Python that is being compiled. -+ -+Using a framework-based Python on watchOS -+====================================== -diff --git a/Apple/watchOS/Resources/Info.plist.in b/Apple/watchOS/Resources/Info.plist.in -new file mode 100644 -index 00000000000..e83ddfd2a43 ---- /dev/null -+++ b/Apple/watchOS/Resources/Info.plist.in -@@ -0,0 +1,34 @@ -+ -+ -+ -+ -+ CFBundleDevelopmentRegion -+ en -+ CFBundleExecutable -+ Python -+ CFBundleGetInfoString -+ Python Runtime and Library -+ CFBundleIdentifier -+ @PYTHONFRAMEWORKIDENTIFIER@ -+ CFBundleInfoDictionaryVersion -+ 6.0 -+ CFBundleName -+ Python -+ CFBundlePackageType -+ FMWK -+ CFBundleShortVersionString -+ %VERSION% -+ CFBundleLongVersionString -+ %VERSION%, (c) 2001-2023 Python Software Foundation. -+ CFBundleSignature -+ ???? -+ CFBundleVersion -+ %VERSION% -+ CFBundleSupportedPlatforms -+ -+ watchOS -+ -+ MinimumOSVersion -+ @WATCHOS_DEPLOYMENT_TARGET@ -+ -+ -diff --git a/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-ar b/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-ar -new file mode 100755 -index 00000000000..dda2b211bd5 ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-ar -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} ar "$@" -diff --git a/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang b/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang -new file mode 100755 -index 00000000000..fe834d3efe4 ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang -target arm64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang++ b/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang++ -new file mode 100755 -index 00000000000..757f3a26d8f ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang++ -target arm64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-cpp b/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-cpp -new file mode 100755 -index 00000000000..fdb57d9e010 ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-cpp -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchsimulator clang -target arm64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator -E "$@" -diff --git a/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-strip b/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-strip -new file mode 100755 -index 00000000000..e28e3f7597a ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} strip -arch arm64 "$@" -diff --git a/Apple/watchOS/Resources/bin/arm64-apple-watchos-strip b/Apple/watchOS/Resources/bin/arm64-apple-watchos-strip -new file mode 100755 -index 00000000000..efe5a1260ad ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/arm64-apple-watchos-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk watchos${WATCHOS_SDK_VERSION} strip -arch arm64 "$@" -diff --git a/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-ar b/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-ar -new file mode 100755 -index 00000000000..029f9a32073 ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-ar -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchos${WATCHOS_SDK_VERSION} ar "$@" -diff --git a/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang b/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang -new file mode 100755 -index 00000000000..285036d4010 ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchos${WATCHOS_SDK_VERSION} clang -target arm64_32-apple-watchos${WATCHOS_DEPLOYMENT_TARGET} "$@" -diff --git a/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang++ b/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang++ -new file mode 100755 -index 00000000000..c8f60ebec51 ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchos${WATCHOS_SDK_VERSION} clang++ -target arm64_32-apple-watchos${WATCHOS_DEPLOYMENT_TARGET} "$@" -diff --git a/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-cpp b/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-cpp -new file mode 100755 -index 00000000000..b411fc25aa4 ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-cpp -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchos${WATCHOS_SDK_VERSION} clang -target arm64_32-apple-watchos${WATCHOS_DEPLOYMENT_TARGET} -E "$@" -diff --git a/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-ar b/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-ar -new file mode 100755 -index 00000000000..dda2b211bd5 ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-ar -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} ar "$@" -diff --git a/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang b/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang -new file mode 100755 -index 00000000000..4776b9b5348 ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang -target x86_64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang++ b/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang++ -new file mode 100755 -index 00000000000..e9b0c5f4b87 ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang++ -target x86_64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-cpp b/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-cpp -new file mode 100755 -index 00000000000..d3b821c5f7f ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-cpp -@@ -0,0 +1,2 @@ -+#!/bin/bash -+xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang -target x86_64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator -E "$@" -diff --git a/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-strip b/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-strip -new file mode 100755 -index 00000000000..105c78281f9 ---- /dev/null -+++ b/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} strip -arch x86_64 "$@" -diff --git a/Apple/watchOS/Resources/pyconfig.h b/Apple/watchOS/Resources/pyconfig.h -new file mode 100644 -index 00000000000..f842b987b2e ---- /dev/null -+++ b/Apple/watchOS/Resources/pyconfig.h -@@ -0,0 +1,11 @@ -+#ifdef __arm64__ -+# ifdef __LP64__ -+#include "pyconfig-arm64.h" -+# else -+#include "pyconfig-arm64_32.h" -+# endif -+#endif -+ -+#ifdef __x86_64__ -+#include "pyconfig-x86_64.h" -+#endif -diff --git a/Doc/includes/wasm-ios-notavail.rst b/Doc/includes/wasm-ios-notavail.rst -new file mode 100644 -index 00000000000..c820665f5e4 ---- /dev/null -+++ b/Doc/includes/wasm-ios-notavail.rst -@@ -0,0 +1,8 @@ -+.. include for modules that don't work on WASM or iOS -+ -+.. availability:: not WASI, not iOS. -+ -+ This module does not work or is not available on WebAssembly platforms, or -+ on iOS. See :ref:`wasm-availability` for more information on WASM -+ availability; see :ref:`iOS-availability` for more information on iOS -+ availability. -diff --git a/Doc/includes/wasm-notavail.rst b/Doc/includes/wasm-notavail.rst -index e680e1f9b43..c1b79d2a4a0 100644 ---- a/Doc/includes/wasm-notavail.rst -+++ b/Doc/includes/wasm-notavail.rst -@@ -1,7 +1,6 @@ - .. include for modules that don't work on WASM - --.. availability:: not Emscripten, not WASI. -+.. availability:: not WASI. - -- This module does not work or is not available on WebAssembly platforms -- ``wasm32-emscripten`` and ``wasm32-wasi``. See -+ This module does not work or is not available on WebAssembly. See - :ref:`wasm-availability` for more information. -diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst -index 2ebda3d3396..91ea6150fb1 100644 ---- a/Doc/library/curses.rst -+++ b/Doc/library/curses.rst -@@ -21,6 +21,8 @@ - designed to match the API of ncurses, an open-source curses library hosted on - Linux and the BSD variants of Unix. - -+.. include:: ../includes/wasm-ios-notavail.rst -+ - .. note:: - - Whenever the documentation mentions a *character* it can be specified -diff --git a/Doc/library/dbm.rst b/Doc/library/dbm.rst -index 53f186952ce..68a8c30aa99 100644 ---- a/Doc/library/dbm.rst -+++ b/Doc/library/dbm.rst -@@ -14,6 +14,7 @@ - is a `third party interface `_ to - the Oracle Berkeley DB. - -+.. include:: ../includes/wasm-ios-notavail.rst - - .. exception:: error - -@@ -398,4 +399,3 @@ - .. method:: dumbdbm.close() - - Close the database. -- -diff --git a/Doc/library/ensurepip.rst b/Doc/library/ensurepip.rst -index 10773ee5f76..692ba9eb6a9 100644 ---- a/Doc/library/ensurepip.rst -+++ b/Doc/library/ensurepip.rst -@@ -38,7 +38,7 @@ - :pep:`453`: Explicit bootstrapping of pip in Python installations - The original rationale and specification for this module. - --.. include:: ../includes/wasm-notavail.rst -+.. include:: ../includes/wasm-ios-notavail.rst - - Command line interface - ---------------------- -diff --git a/Doc/library/fcntl.rst b/Doc/library/fcntl.rst -index d23a105cd5b..1faef54c116 100644 ---- a/Doc/library/fcntl.rst -+++ b/Doc/library/fcntl.rst -@@ -18,7 +18,7 @@ - See the :manpage:`fcntl(2)` and :manpage:`ioctl(2)` Unix manual pages - for full details. - --.. availability:: Unix, not Emscripten, not WASI. -+.. availability:: Unix, not WASI. - - All functions in this module take a file descriptor *fd* as their first - argument. This can be an integer file descriptor, such as returned by -diff --git a/Doc/library/grp.rst b/Doc/library/grp.rst -index 57a77d51a02..f1157e189a3 100644 ---- a/Doc/library/grp.rst -+++ b/Doc/library/grp.rst -@@ -10,7 +10,7 @@ - This module provides access to the Unix group database. It is available on all - Unix versions. - --.. availability:: Unix, not Emscripten, not WASI. -+.. availability:: Unix, not WASI, not iOS. - - Group database entries are reported as a tuple-like object, whose attributes - correspond to the members of the ``group`` structure (Attribute field below, see -diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst -index a9d7665ac20..6611bd5018c 100644 ---- a/Doc/library/importlib.rst -+++ b/Doc/library/importlib.rst -@@ -1233,6 +1233,69 @@ - and how the module's :attr:`~module.__file__` is populated. - - -+.. class:: AppleFrameworkLoader(name, path) -+ -+ A specialization of :class:`importlib.machinery.ExtensionFileLoader` that -+ is able to load extension modules in Framework format. -+ -+ For compatibility with the iOS App Store, *all* binary modules in an iOS app -+ must be dynamic libraries, contained in a framework with appropriate -+ metadata, stored in the ``Frameworks`` folder of the packaged app. There can -+ be only a single binary per framework, and there can be no executable binary -+ material outside the Frameworks folder. -+ -+ To accomodate this requirement, when running on iOS, extension module -+ binaries are *not* packaged as ``.so`` files on ``sys.path``, but as -+ individual standalone frameworks. To discover those frameworks, this loader -+ is be registered against the ``.fwork`` file extension, with a ``.fwork`` -+ file acting as a placeholder in the original location of the binary on -+ ``sys.path``. The ``.fwork`` file contains the path of the actual binary in -+ the ``Frameworks`` folder, relative to the app bundle. To allow for -+ resolving a framework-packaged binary back to the original location, the -+ framework is expected to contain a ``.origin`` file that contains the -+ location of the ``.fwork`` file, relative to the app bundle. -+ -+ For example, consider the case of an import ``from foo.bar import _whiz``, -+ where ``_whiz`` is implemented with the binary module -+ ``sources/foo/bar/_whiz.abi3.so``, with ``sources`` being the location -+ registered on ``sys.path``, relative to the application bundle. This module -+ *must* be distributed as -+ ``Frameworks/foo.bar._whiz.framework/foo.bar._whiz`` (creating the framework -+ name from the full import path of the module), with an ``Info.plist`` file -+ in the ``.framework`` directory identifying the binary as a framework. The -+ ``foo.bar._whiz`` module would be represented in the original location with -+ a ``sources/foo/bar/_whiz.abi3.fwork`` marker file, containing the path -+ ``Frameworks/foo.bar._whiz/foo.bar._whiz``. The framework would also contain -+ ``Frameworks/foo.bar._whiz.framework/foo.bar._whiz.origin``, containing the -+ path to the ``.fwork`` file. -+ -+ When a module is loaded with this loader, the ``__file__`` for the module -+ will report as the location of the ``.fwork`` file. This allows code to use -+ the ``__file__`` of a module as an anchor for file system traveral. -+ However, the spec origin will reference the location of the *actual* binary -+ in the ``.framework`` folder. -+ -+ The Xcode project building the app is responsible for converting any ``.so`` -+ files from wherever they exist in the ``PYTHONPATH`` into frameworks in the -+ ``Frameworks`` folder (including stripping extensions from the module file, -+ the addition of framework metadata, and signing the resulting framework), -+ and creating the ``.fwork`` and ``.origin`` files. This will usually be done -+ with a build step in the Xcode project; see the iOS documentation for -+ details on how to construct this build step. -+ -+ .. versionadded:: 3.13 -+ -+ .. availability:: iOS. -+ -+ .. attribute:: name -+ -+ Name of the module the loader supports. -+ -+ .. attribute:: path -+ -+ Path to the ``.fwork`` file for the extension module. -+ -+ - :mod:`importlib.util` -- Utility code for importers - --------------------------------------------------- - -diff --git a/Doc/library/intro.rst b/Doc/library/intro.rst -index 5a4c9b8b16a..ffc8939d211 100644 ---- a/Doc/library/intro.rst -+++ b/Doc/library/intro.rst -@@ -58,7 +58,7 @@ - operating system. - - * If not separately noted, all functions that claim "Availability: Unix" are -- supported on macOS, which builds on a Unix core. -+ supported on macOS and iOS, both of which build on a Unix core. - - * If an availability note contains both a minimum Kernel version and a minimum - libc version, then both conditions must hold. For example a feature with note -@@ -119,3 +119,44 @@ - .. _wasmtime: https://wasmtime.dev/ - .. _Pyodide: https://pyodide.org/ - .. _PyScript: https://pyscript.net/ -+ -+.. _iOS-availability: -+ -+iOS -+--- -+ -+iOS is, in most respects, a POSIX operating system. File I/O, socket handling, -+and threading all behave as they would on any POSIX operating system. However, -+there are several major differences between iOS and other POSIX systems. -+ -+* iOS can only use Python in "embedded" mode. There is no Python REPL, and no -+ ability to execute binaries that are part of the normal Python developer -+ experience, such as :program:`pip`. To add Python code to your iOS app, you must use -+ the :ref:`Python embedding API ` to add a Python interpreter to an -+ iOS app created with Xcode. See the :ref:`iOS usage guide ` for -+ more details. -+ -+* An iOS app cannot use any form of subprocessing, background processing, or -+ inter-process communication. If an iOS app attempts to create a subprocess, -+ the process creating the subprocess will either lock up, or crash. An iOS app -+ has no visibility of other applications that are running, nor any ability to -+ communicate with other running applications, outside of the iOS-specific APIs -+ that exist for this purpose. -+ -+* iOS apps have limited access to modify system resources (such as the system -+ clock). These resources will often be *readable*, but attempts to modify -+ those resources will usually fail. -+ -+* iOS apps have a limited concept of console input and output. ``stdout`` and -+ ``stderr`` *exist*, and content written to ``stdout`` and ``stderr`` will be -+ visible in logs when running in Xcode, but this content *won't* be recorded -+ in the system log. If a user who has installed your app provides their app -+ logs as a diagnostic aid, they will not include any detail written to -+ ``stdout`` or ``stderr``. -+ -+ iOS apps have no concept of ``stdin`` at all. While iOS apps can have a -+ keyboard, this is a software feature, not something that is attached to -+ ``stdin``. -+ -+ As a result, Python library that involve console manipulation (such as -+ :mod:`curses` and :mod:`readline`) are not available on iOS. -diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst -index 3fec86080c5..0a8e21510c4 100644 ---- a/Doc/library/multiprocessing.rst -+++ b/Doc/library/multiprocessing.rst -@@ -8,7 +8,7 @@ - - -------------- - --.. include:: ../includes/wasm-notavail.rst -+.. include:: ../includes/wasm-ios-notavail.rst - - Introduction - ------------ -diff --git a/Doc/library/os.rst b/Doc/library/os.rst -index 6537ae298b9..0e904ef8615 100644 ---- a/Doc/library/os.rst -+++ b/Doc/library/os.rst -@@ -34,12 +34,13 @@ - - * On VxWorks, os.popen, os.fork, os.execv and os.spawn*p* are not supported. - --* On WebAssembly platforms ``wasm32-emscripten`` and ``wasm32-wasi``, large -- parts of the :mod:`os` module are not available or behave differently. API -- related to processes (e.g. :func:`~os.fork`, :func:`~os.execve`), signals -- (e.g. :func:`~os.kill`, :func:`~os.wait`), and resources -- (e.g. :func:`~os.nice`) are not available. Others like :func:`~os.getuid` -- and :func:`~os.getpid` are emulated or stubs. -+* On WebAssembly platforms ``wasm32-emscripten`` and ``wasm32-wasi``, and on -+ iOS, large parts of the :mod:`os` module are not available or behave -+ differently. API related to processes (e.g. :func:`~os.fork`, -+ :func:`~os.execve`) and resources (e.g. :func:`~os.nice`) are not available. -+ Others like :func:`~os.getuid` and :func:`~os.getpid` are emulated or stubs. -+ WebAssembly platforms also lack support for signals (e.g. :func:`~os.kill`, -+ :func:`~os.wait`). - - - .. note:: -@@ -785,6 +786,11 @@ - :func:`socket.gethostname` or even - ``socket.gethostbyaddr(socket.gethostname())``. - -+ On macOS, iOS and Android, this returns the *kernel* name and version (i.e., -+ ``'Darwin'`` on macOS and iOS; ``'Linux'`` on Android). :func:`platform.uname()` -+ can be used to get the user-facing operating system name and version on iOS and -+ Android. -+ - .. availability:: Unix. - - .. versionchanged:: 3.3 -@@ -3999,7 +4005,7 @@ - - .. audit-event:: os.exec path,args,env os.execl - -- .. availability:: Unix, Windows, not Emscripten, not WASI. -+ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. - - .. versionchanged:: 3.3 - Added support for specifying *path* as an open file descriptor -@@ -4202,7 +4208,7 @@ - for technical details of why we're surfacing this longstanding - platform compatibility problem to developers. - -- .. availability:: POSIX, not Emscripten, not WASI. -+ .. availability:: POSIX, not Emscripten, not WASI, not iOS. - - - .. function:: forkpty() -@@ -4229,7 +4235,7 @@ - threads, this now raises a :exc:`DeprecationWarning`. See the - longer explanation on :func:`os.fork`. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. function:: kill(pid, sig, /) -@@ -4252,7 +4258,7 @@ - - .. audit-event:: os.kill pid,sig os.kill - -- .. availability:: Unix, Windows, not Emscripten, not WASI. -+ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. - - .. versionchanged:: 3.2 - Added Windows support. -@@ -4268,7 +4274,7 @@ - - .. audit-event:: os.killpg pgid,sig os.killpg - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. function:: nice(increment, /) -@@ -4305,7 +4311,7 @@ - Lock program segments into memory. The value of *op* (defined in - ````) determines which segments are locked. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. function:: popen(cmd, mode='r', buffering=-1) -@@ -4337,7 +4343,7 @@ - documentation for more powerful ways to manage and communicate with - subprocesses. - -- .. availability:: not Emscripten, not WASI. -+ .. availability:: not Emscripten, not WASI, not iOS. - - .. note:: - The :ref:`Python UTF-8 Mode ` affects encodings used -@@ -4432,7 +4438,7 @@ - - .. versionadded:: 3.8 - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - .. function:: posix_spawnp(path, argv, env, *, file_actions=None, \ - setpgroup=None, resetids=False, setsid=False, setsigmask=(), \ -@@ -4448,7 +4454,7 @@ - - .. versionadded:: 3.8 - -- .. availability:: POSIX, not Emscripten, not WASI. -+ .. availability:: POSIX, not Emscripten, not WASI, not iOS. - - See :func:`posix_spawn` documentation. - -@@ -4481,7 +4487,7 @@ - - There is no way to unregister a function. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - .. versionadded:: 3.7 - -@@ -4550,7 +4556,7 @@ - - .. audit-event:: os.spawn mode,path,args,env os.spawnl - -- .. availability:: Unix, Windows, not Emscripten, not WASI. -+ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. - - :func:`spawnlp`, :func:`spawnlpe`, :func:`spawnvp` - and :func:`spawnvpe` are not available on Windows. :func:`spawnle` and -@@ -4674,7 +4680,7 @@ - - .. audit-event:: os.system command os.system - -- .. availability:: Unix, Windows, not Emscripten, not WASI. -+ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. - - - .. function:: times() -@@ -4718,7 +4724,7 @@ - :func:`waitstatus_to_exitcode` can be used to convert the exit status into an - exit code. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - .. seealso:: - -@@ -4752,7 +4758,10 @@ - Otherwise, if there are no matching children - that could be waited for, :exc:`ChildProcessError` is raised. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. -+ -+ .. note:: -+ This function is not available on macOS. - - .. note:: - This function is not available on macOS. -@@ -4793,7 +4802,7 @@ - :func:`waitstatus_to_exitcode` can be used to convert the exit status into an - exit code. - -- .. availability:: Unix, Windows, not Emscripten, not WASI. -+ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. - - .. versionchanged:: 3.5 - If the system call is interrupted and the signal handler does not raise an -@@ -4813,7 +4822,7 @@ - :func:`waitstatus_to_exitcode` can be used to convert the exit status into an - exitcode. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. function:: wait4(pid, options) -@@ -4827,7 +4836,7 @@ - :func:`waitstatus_to_exitcode` can be used to convert the exit status into an - exitcode. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. data:: P_PID -@@ -4844,7 +4853,7 @@ - * :data:`!P_PIDFD` - wait for the child identified by the file descriptor - *id* (a process file descriptor created with :func:`pidfd_open`). - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - .. note:: :data:`!P_PIDFD` is only available on Linux >= 5.4. - -@@ -4859,7 +4868,7 @@ - :func:`waitid` causes child processes to be reported if they have been - continued from a job control stop since they were last reported. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. data:: WEXITED -@@ -4870,7 +4879,7 @@ - The other ``wait*`` functions always report children that have terminated, - so this option is not available for them. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - .. versionadded:: 3.3 - -@@ -4882,7 +4891,7 @@ - - This option is not available for the other ``wait*`` functions. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - .. versionadded:: 3.3 - -@@ -4895,7 +4904,7 @@ - - This option is not available for :func:`waitid`. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. data:: WNOHANG -@@ -4904,7 +4913,7 @@ - :func:`waitid` to return right away if no child process status is available - immediately. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. data:: WNOWAIT -@@ -4914,7 +4923,7 @@ - - This option is not available for the other ``wait*`` functions. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. data:: CLD_EXITED -@@ -4927,7 +4936,7 @@ - These are the possible values for :attr:`!si_code` in the result returned by - :func:`waitid`. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - .. versionadded:: 3.3 - -@@ -4962,7 +4971,7 @@ - :func:`WIFEXITED`, :func:`WEXITSTATUS`, :func:`WIFSIGNALED`, - :func:`WTERMSIG`, :func:`WIFSTOPPED`, :func:`WSTOPSIG` functions. - -- .. availability:: Unix, Windows, not Emscripten, not WASI. -+ .. availability:: Unix, Windows, not Emscripten, not WASI, not iOS. - - .. versionadded:: 3.9 - -@@ -4978,7 +4987,7 @@ - - This function should be employed only if :func:`WIFSIGNALED` is true. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. function:: WIFCONTINUED(status) -@@ -4989,7 +4998,7 @@ - - See :data:`WCONTINUED` option. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. function:: WIFSTOPPED(status) -@@ -5001,14 +5010,14 @@ - done using :data:`WUNTRACED` option or when the process is being traced (see - :manpage:`ptrace(2)`). - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - .. function:: WIFSIGNALED(status) - - Return ``True`` if the process was terminated by a signal, otherwise return - ``False``. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. function:: WIFEXITED(status) -@@ -5017,7 +5026,7 @@ - by calling ``exit()`` or ``_exit()``, or by returning from ``main()``; - otherwise return ``False``. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. function:: WEXITSTATUS(status) -@@ -5026,7 +5035,7 @@ - - This function should be employed only if :func:`WIFEXITED` is true. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. function:: WSTOPSIG(status) -@@ -5035,7 +5044,7 @@ - - This function should be employed only if :func:`WIFSTOPPED` is true. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - .. function:: WTERMSIG(status) -@@ -5044,7 +5053,7 @@ - - This function should be employed only if :func:`WIFSIGNALED` is true. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not Emscripten, not WASI, not iOS. - - - Interface to the scheduler -diff --git a/Doc/library/platform.rst b/Doc/library/platform.rst -index 2f5bf53bc5c..0cc5e532711 100644 ---- a/Doc/library/platform.rst -+++ b/Doc/library/platform.rst -@@ -148,6 +148,9 @@ - Returns the system/OS name, such as ``'Linux'``, ``'Darwin'``, ``'Java'``, - ``'Windows'``. An empty string is returned if the value cannot be determined. - -+ On iOS and Android, this returns the user-facing OS name (i.e, ``'iOS``, -+ ``'iPadOS'`` or ``'Android'``). To obtain the kernel name (``'Darwin'`` or -+ ``'Linux'``), use :func:`os.uname()`. - - .. function:: system_alias(system, release, version) - -@@ -161,6 +164,8 @@ - Returns the system's release version, e.g. ``'#3 on degas'``. An empty string is - returned if the value cannot be determined. - -+ On iOS and Android, this is the user-facing OS version. To obtain the -+ Darwin or Linux kernel version, use :func:`os.uname()`. - - .. function:: uname() - -@@ -234,7 +239,6 @@ - macOS Platform - -------------- - -- - .. function:: mac_ver(release='', versioninfo=('','',''), machine='') - - Get macOS version information and return it as tuple ``(release, versioninfo, -@@ -244,6 +248,24 @@ - Entries which cannot be determined are set to ``''``. All tuple entries are - strings. - -+iOS Platform -+------------ -+ -+.. function:: ios_ver(system='', release='', model='', is_simulator=False) -+ -+ Get iOS version information and return it as a -+ :func:`~collections.namedtuple` with the following attributes: -+ -+ * ``system`` is the OS name; either ``'iOS'`` or ``'iPadOS'``. -+ * ``release`` is the iOS version number as a string (e.g., ``'17.2'``). -+ * ``model`` is the device model identifier; this will be a string like -+ ``'iPhone13,2'`` for a physical device, or ``'iPhone'`` on a simulator. -+ * ``is_simulator`` is a boolean describing if the app is running on a -+ simulator or a physical device. -+ -+ Entries which cannot be determined are set to the defaults given as -+ parameters. -+ - - Unix Platforms - -------------- -diff --git a/Doc/library/pwd.rst b/Doc/library/pwd.rst -index 98ca174d9e3..d71d7212cfd 100644 ---- a/Doc/library/pwd.rst -+++ b/Doc/library/pwd.rst -@@ -10,7 +10,7 @@ - This module provides access to the Unix user account and password database. It - is available on all Unix versions. - --.. availability:: Unix, not Emscripten, not WASI. -+.. availability:: Unix, not WASI, not iOS. - - Password database entries are reported as a tuple-like object, whose attributes - correspond to the members of the ``passwd`` structure (Attribute field below, -diff --git a/Doc/library/readline.rst b/Doc/library/readline.rst -index 5479ccf7f62..069dba24c29 100644 ---- a/Doc/library/readline.rst -+++ b/Doc/library/readline.rst -@@ -24,6 +24,8 @@ - allowable constructs of that file, and the capabilities of the - Readline library in general. - -+.. include:: ../includes/wasm-ios-notavail.rst -+ - .. note:: - - The underlying Readline library API may be implemented by -diff --git a/Doc/library/resource.rst b/Doc/library/resource.rst -index 02009d82104..0515d205bbc 100644 ---- a/Doc/library/resource.rst -+++ b/Doc/library/resource.rst -@@ -13,7 +13,7 @@ - This module provides basic mechanisms for measuring and controlling system - resources utilized by a program. - --.. availability:: Unix, not Emscripten, not WASI. -+.. availability:: Unix, not WASI. - - Symbolic constants are used to specify particular system resources and to - request usage information about either the current process or its children. -diff --git a/Doc/library/signal.rst b/Doc/library/signal.rst -index 641a6c021c1..79c4948e99e 100644 ---- a/Doc/library/signal.rst -+++ b/Doc/library/signal.rst -@@ -26,9 +26,9 @@ - underlying implementation), with the exception of the handler for - :const:`SIGCHLD`, which follows the underlying implementation. - --On WebAssembly platforms ``wasm32-emscripten`` and ``wasm32-wasi``, signals --are emulated and therefore behave differently. Several functions and signals --are not available on these platforms. -+On WebAssembly platforms, signals are emulated and therefore behave -+differently. Several functions and signals are not available on these -+platforms. - - Execution of Python signal handlers - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst -index d29e3c20e10..4a627e3f3f9 100644 ---- a/Doc/library/socket.rst -+++ b/Doc/library/socket.rst -@@ -1244,7 +1244,7 @@ - buffer. Raises :exc:`OverflowError` if *length* is outside the - permissible range of values. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not WASI. - - Most Unix platforms. - -@@ -1267,7 +1267,7 @@ - amount of ancillary data that can be received, since additional - data may be able to fit into the padding area. - -- .. availability:: Unix, not Emscripten, not WASI. -+ .. availability:: Unix, not WASI. - - most Unix platforms. - -@@ -1307,7 +1307,7 @@ - (index int, name string) tuples. - :exc:`OSError` if the system call fails. - -- .. availability:: Unix, Windows, not Emscripten, not WASI. -+ .. availability:: Unix, Windows, not WASI. - - .. versionadded:: 3.3 - -@@ -1334,7 +1334,7 @@ - interface name. - :exc:`OSError` if no interface with the given name exists. - -- .. availability:: Unix, Windows, not Emscripten, not WASI. -+ .. availability:: Unix, Windows, not WASI. - - .. versionadded:: 3.3 - -@@ -1351,7 +1351,7 @@ - interface index number. - :exc:`OSError` if no interface with the given index exists. - -- .. availability:: Unix, Windows, not Emscripten, not WASI. -+ .. availability:: Unix, Windows, not WASI. - - .. versionadded:: 3.3 - -@@ -1368,7 +1368,7 @@ - The *fds* parameter is a sequence of file descriptors. - Consult :meth:`~socket.sendmsg` for the documentation of these parameters. - -- .. availability:: Unix, Windows, not Emscripten, not WASI. -+ .. availability:: Unix, Windows, not WASI. - - Unix platforms supporting :meth:`~socket.sendmsg` - and :const:`SCM_RIGHTS` mechanism. -@@ -1382,7 +1382,7 @@ - Return ``(msg, list(fds), flags, addr)``. - Consult :meth:`~socket.recvmsg` for the documentation of these parameters. - -- .. availability:: Unix, Windows, not Emscripten, not WASI. -+ .. availability:: Unix, Windows, not WASI. - - Unix platforms supporting :meth:`~socket.sendmsg` - and :const:`SCM_RIGHTS` mechanism. -diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst -index 552af2ac9d3..8d8782e6a76 100644 ---- a/Doc/library/subprocess.rst -+++ b/Doc/library/subprocess.rst -@@ -25,7 +25,7 @@ - - :pep:`324` -- PEP proposing the subprocess module - --.. include:: ../includes/wasm-notavail.rst -+.. include:: ../includes/wasm-ios-notavail.rst - - Using the :mod:`subprocess` Module - ---------------------------------- -diff --git a/Doc/library/syslog.rst b/Doc/library/syslog.rst -index 79b808ab63c..332b58413d3 100644 ---- a/Doc/library/syslog.rst -+++ b/Doc/library/syslog.rst -@@ -11,7 +11,7 @@ - Refer to the Unix manual pages for a detailed description of the ``syslog`` - facility. - --.. availability:: Unix, not Emscripten, not WASI. -+.. availability:: Unix, not WASI, not iOS. - - This module wraps the system ``syslog`` family of routines. A pure Python - library that can speak to a syslog server is available in the -diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst -index b32b4af1aa8..69daa381013 100644 ---- a/Doc/library/urllib.parse.rst -+++ b/Doc/library/urllib.parse.rst -@@ -22,11 +22,19 @@ - - The module has been designed to match the internet RFC on Relative Uniform - Resource Locators. It supports the following URL schemes: ``file``, ``ftp``, --``gopher``, ``hdl``, ``http``, ``https``, ``imap``, ``mailto``, ``mms``, -+``gopher``, ``hdl``, ``http``, ``https``, ``imap``, ``itms-services``, ``mailto``, ``mms``, - ``news``, ``nntp``, ``prospero``, ``rsync``, ``rtsp``, ``rtsps``, ``rtspu``, - ``sftp``, ``shttp``, ``sip``, ``sips``, ``snews``, ``svn``, ``svn+ssh``, - ``telnet``, ``wais``, ``ws``, ``wss``. - -+.. impl-detail:: -+ -+ The inclusion of the ``itms-services`` URL scheme can prevent an app from -+ passing Apple's App Store review process for the macOS and iOS App Stores. -+ Handling for the ``itms-services`` scheme is always removed on iOS; on -+ macOS, it *may* be removed if CPython has been built with the -+ :option:`--with-app-store-compliance` option. -+ - The :mod:`urllib.parse` module defines functions that fall into two broad - categories: URL parsing and URL quoting. These are covered in detail in - the following sections. -diff --git a/Doc/library/venv.rst b/Doc/library/venv.rst -index 9eb0e9d191c..cc403f1bb7b 100644 ---- a/Doc/library/venv.rst -+++ b/Doc/library/venv.rst -@@ -56,7 +56,7 @@ - `Python Packaging User Guide: Creating and using virtual environments - `__ - --.. include:: ../includes/wasm-notavail.rst -+.. include:: ../includes/wasm-ios-notavail.rst - - Creating virtual environments - ----------------------------- -diff --git a/Doc/library/webbrowser.rst b/Doc/library/webbrowser.rst -index 9c3ff5efa2e..b99b28b2053 100644 ---- a/Doc/library/webbrowser.rst -+++ b/Doc/library/webbrowser.rst -@@ -33,6 +33,13 @@ - browsers are not available on Unix, the controlling process will launch a new - browser and wait. - -+On iOS, the :envvar:`BROWSER` environment variable, as well as any arguments -+controlling autoraise, browser preference, and new tab/window creation will be -+ignored. Web pages will *always* be opened in the user's preferred browser, in -+a new tab, with the browser being brought to the foreground. The use of the -+:mod:`webbrowser` module on iOS requires the :mod:`ctypes` module. If -+:mod:`ctypes` isn't available, calls to :func:`.open` will fail. -+ - .. program:: webbrowser - - The script :program:`webbrowser` can be used as a command-line interface for the -@@ -166,6 +173,8 @@ - +------------------------+-----------------------------------------+-------+ - | ``'chromium-browser'`` | :class:`Chromium('chromium-browser')` | | - +------------------------+-----------------------------------------+-------+ -+| ``'iosbrowser'`` | ``IOSBrowser`` | \(4) | -++------------------------+-----------------------------------------+-------+ - - Notes: - -@@ -180,7 +189,11 @@ - Only on Windows platforms. - - (3) -- Only on macOS platform. -+ Only on macOS. -+ -+(4) -+ Only on iOS. -+ - - .. versionadded:: 3.3 - Support for Chrome/Chromium has been added. -@@ -193,6 +206,9 @@ - .. deprecated-removed:: 3.11 3.13 - :class:`MacOSX` is deprecated, use :class:`MacOSXOSAScript` instead. - -+.. versionchanged:: 3.13 -+ Support for iOS has been added. -+ - Here are some simple examples:: - - url = 'https://docs.python.org/' -diff --git a/Doc/tools/extensions/availability.py b/Doc/tools/extensions/availability.py -index 225b3438b94..3164d735a3a 100644 ---- a/Doc/tools/extensions/availability.py -+++ b/Doc/tools/extensions/availability.py -@@ -24,6 +24,7 @@ - "DragonFlyBSD", - "Emscripten", - "FreeBSD", -+ "iOS", - "Linux", - "macOS", - "NetBSD", -diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst -index 6df75ac9e0b..8883a7af9c0 100644 ---- a/Doc/using/configure.rst -+++ b/Doc/using/configure.rst -@@ -638,7 +638,7 @@ - macOS Options - ------------- - --See ``Mac/README.rst``. -+See :source:`Mac/README.rst`. - - .. option:: --enable-universalsdk - .. option:: --enable-universalsdk=SDKDIR -@@ -679,6 +679,31 @@ - Specify the name for the python framework on macOS only valid when - :option:`--enable-framework` is set (default: ``Python``). - -+.. option:: --with-app-store-compliance -+.. option:: --with-app-store-compliance=PATCH-FILE -+ -+ The Python standard library contains strings that are known to trigger -+ automated inspection tool errors when submitted for distribution by -+ the macOS and iOS App Stores. If enabled, this option will apply the list of -+ patches that are known to correct app store compliance. A custom patch -+ file can also be specified. This option is disabled by default. -+ -+ .. versionadded:: 3.13 -+ -+iOS Options -+----------- -+ -+See :source:`iOS/README.rst`. -+ -+.. option:: --enable-framework=INSTALLDIR -+ -+ Create a Python.framework. Unlike macOS, the *INSTALLDIR* argument -+ specifying the installation path is mandatory. -+ -+.. option:: --with-framework-name=FRAMEWORK -+ -+ Specify the name for the framework (default: ``Python``). -+ - - Cross Compiling Options - ----------------------- -diff --git a/Doc/using/index.rst b/Doc/using/index.rst -index e1a3111f36a..f55a12f1ab8 100644 ---- a/Doc/using/index.rst -+++ b/Doc/using/index.rst -@@ -18,4 +18,5 @@ - configure.rst - windows.rst - mac.rst -+ ios.rst - editors.rst -diff --git a/Doc/using/ios.rst b/Doc/using/ios.rst -new file mode 100644 -index 00000000000..0f29de64e41 ---- /dev/null -+++ b/Doc/using/ios.rst -@@ -0,0 +1,359 @@ -+.. _using-ios: -+ -+=================== -+Using Python on iOS -+=================== -+ -+:Authors: -+ Russell Keith-Magee (2024-03) -+ -+Python on iOS is unlike Python on desktop platforms. On a desktop platform, -+Python is generally installed as a system resource that can be used by any user -+of that computer. Users then interact with Python by running a :program:`python` -+executable and entering commands at an interactive prompt, or by running a -+Python script. -+ -+On iOS, there is no concept of installing as a system resource. The only unit -+of software distribution is an "app". There is also no console where you could -+run a :program:`python` executable, or interact with a Python REPL. -+ -+As a result, the only way you can use Python on iOS is in embedded mode - that -+is, by writing a native iOS application, and embedding a Python interpreter -+using ``libPython``, and invoking Python code using the :ref:`Python embedding -+API `. The full Python interpreter, the standard library, and all -+your Python code is then packaged as a standalone bundle that can be -+distributed via the iOS App Store. -+ -+If you're looking to experiment for the first time with writing an iOS app in -+Python, projects such as `BeeWare `__ and `Kivy -+`__ will provide a much more approachable user experience. -+These projects manage the complexities associated with getting an iOS project -+running, so you only need to deal with the Python code itself. -+ -+Python at runtime on iOS -+======================== -+ -+iOS version compatibility -+------------------------- -+ -+The minimum supported iOS version is specified at compile time, using the -+:option:`--host` option to ``configure``. By default, when compiled for iOS, -+Python will be compiled with a minimum supported iOS version of 13.0. To use a -+different miniumum iOS version, provide the version number as part of the -+:option:`!--host` argument - for example, -+``--host=arm64-apple-ios15.4-simulator`` would compile an ARM64 simulator build -+with a deployment target of 15.4. -+ -+Platform identification -+----------------------- -+ -+When executing on iOS, ``sys.platform`` will report as ``ios``. This value will -+be returned on an iPhone or iPad, regardless of whether the app is running on -+the simulator or a physical device. -+ -+Information about the specific runtime environment, including the iOS version, -+device model, and whether the device is a simulator, can be obtained using -+:func:`platform.ios_ver`. :func:`platform.system` will report ``iOS`` or -+``iPadOS``, depending on the device. -+ -+:func:`os.uname` reports kernel-level details; it will report a name of -+``Darwin``. -+ -+Standard library availability -+----------------------------- -+ -+The Python standard library has some notable omissions and restrictions on -+iOS. See the :ref:`API availability guide for iOS ` for -+details. -+ -+Binary extension modules -+------------------------ -+ -+One notable difference about iOS as a platform is that App Store distribution -+imposes hard requirements on the packaging of an application. One of these -+requirements governs how binary extension modules are distributed. -+ -+The iOS App Store requires that *all* binary modules in an iOS app must be -+dynamic libraries, contained in a framework with appropriate metadata, stored -+in the ``Frameworks`` folder of the packaged app. There can be only a single -+binary per framework, and there can be no executable binary material outside -+the ``Frameworks`` folder. -+ -+This conflicts with the usual Python approach for distributing binaries, which -+allows a binary extension module to be loaded from any location on -+``sys.path``. To ensure compliance with App Store policies, an iOS project must -+post-process any Python packages, converting ``.so`` binary modules into -+individual standalone frameworks with appropriate metadata and signing. For -+details on how to perform this post-processing, see the guide for :ref:`adding -+Python to your project `. -+ -+To help Python discover binaries in their new location, the original ``.so`` -+file on ``sys.path`` is replaced with a ``.fwork`` file. This file is a text -+file containing the location of the framework binary, relative to the app -+bundle. To allow the framework to resolve back to the original location, the -+framework must contain a ``.origin`` file that contains the location of the -+``.fwork`` file, relative to the app bundle. -+ -+For example, consider the case of an import ``from foo.bar import _whiz``, -+where ``_whiz`` is implemented with the binary module -+``sources/foo/bar/_whiz.abi3.so``, with ``sources`` being the location -+registered on ``sys.path``, relative to the application bundle. This module -+*must* be distributed as ``Frameworks/foo.bar._whiz.framework/foo.bar._whiz`` -+(creating the framework name from the full import path of the module), with an -+``Info.plist`` file in the ``.framework`` directory identifying the binary as a -+framework. The ``foo.bar._whiz`` module would be represented in the original -+location with a ``sources/foo/bar/_whiz.abi3.fwork`` marker file, containing -+the path ``Frameworks/foo.bar._whiz/foo.bar._whiz``. The framework would also -+contain ``Frameworks/foo.bar._whiz.framework/foo.bar._whiz.origin``, containing -+the path to the ``.fwork`` file. -+ -+When running on iOS, the Python interpreter will install an -+:class:`~importlib.machinery.AppleFrameworkLoader` that is able to read and -+import ``.fwork`` files. Once imported, the ``__file__`` attribute of the -+binary module will report as the location of the ``.fwork`` file. However, the -+:class:`~importlib.machinery.ModuleSpec` for the loaded module will report the -+``origin`` as the location of the binary in the framework folder. -+ -+Compiler stub binaries -+---------------------- -+ -+Xcode doesn't expose explicit compilers for iOS; instead, it uses an ``xcrun`` -+script that resolves to a full compiler path (e.g., ``xcrun --sdk iphoneos -+clang`` to get the ``clang`` for an iPhone device). However, using this script -+poses two problems: -+ -+* The output of ``xcrun`` includes paths that are machine specific, resulting -+ in a sysconfig module that cannot be shared between users; and -+ -+* It results in ``CC``/``CPP``/``LD``/``AR`` definitions that include spaces. -+ There is a lot of C ecosystem tooling that assumes that you can split a -+ command line at the first space to get the path to the compiler executable; -+ this isn't the case when using ``xcrun``. -+ -+To avoid these problems, Python provided stubs for these tools. These stubs are -+shell script wrappers around the underingly ``xcrun`` tools, distributed in a -+``bin`` folder distributed alongside the compiled iOS framework. These scripts -+are relocatable, and will always resolve to the appropriate local system paths. -+By including these scripts in the bin folder that accompanies a framework, the -+contents of the ``sysconfig`` module becomes useful for end-users to compile -+their own modules. When compiling third-party Python modules for iOS, you -+should ensure these stub binaries are on your path. -+ -+Installing Python on iOS -+======================== -+ -+Tools for building iOS apps -+--------------------------- -+ -+Building for iOS requires the use of Apple's Xcode tooling. It is strongly -+recommended that you use the most recent stable release of Xcode. This will -+require the use of the most (or second-most) recently released macOS version, -+as Apple does not maintain Xcode for older macOS versions. The Xcode Command -+Line Tools are not sufficient for iOS development; you need a *full* Xcode -+install. -+ -+If you want to run your code on the iOS simulator, you'll also need to install -+an iOS Simulator Platform. You should be prompted to select an iOS Simulator -+Platform when you first run Xcode. Alternatively, you can add an iOS Simulator -+Platform by selecting from the Platforms tab of the Xcode Settings panel. -+ -+.. _adding-ios: -+ -+Adding Python to an iOS project -+------------------------------- -+ -+Python can be added to any iOS project, using either Swift or Objective C. The -+following examples will use Objective C; if you are using Swift, you may find a -+library like `PythonKit `__ to be -+helpful. -+ -+To add Python to an iOS Xcode project: -+ -+1. Build or obtain a Python ``XCFramework``. See the instructions in -+ :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``. -+ -+2. Drag the ``XCframework`` into your iOS project. In the following -+ instructions, we'll assume you've dropped the ``XCframework`` into the root -+ of your project; however, you can use any other location that you want by -+ adjusting paths as needed. -+ -+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. -+ -+4. Select the app target by selecting the root node of your Xcode project, then -+ the target name in the sidebar that appears. -+ -+5. In the "General" settings, under "Frameworks, Libraries and Embedded -+ Content", add ``Python.xcframework``, with "Embed & Sign" selected. -+ -+6. In the "Build Settings" tab, modify the following: -+ -+ - Build Options -+ -+ * User Script Sandboxing: No -+ * Enable Testability: Yes -+ -+ - Search Paths -+ -+ * Framework Search Paths: ``$(PROJECT_DIR)`` -+ * Header Search Paths: ``"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers"`` -+ -+ - Apple Clang - Warnings - All languages -+ -+ * Quoted Include In Framework Header: No -+ -+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 -+ source $PROJECT_DIR/Python.xcframework/build/build_utils.sh -+ install_python Python.xcframework app -+ -+ If you have placed your XCframework somewhere other than the root of your -+ project, modify the path to the first argument. -+ -+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*; -+ * Writing bytecode (:c:member:`PyConfig.write_bytecode`) is *disabled*; -+ * Signal handlers (:c:member:`PyConfig.install_signal_handlers`) are *enabled*; -+ * :envvar:`PYTHONHOME` for the interpreter is configured to point at the -+ ``python`` subfolder of your app's bundle; and -+ * The :envvar:`PYTHONPATH` for the interpreter includes: -+ -+ - the ``python/lib/python3.X`` subfolder of your app's bundle, -+ - the ``python/lib/python3.X/lib-dynload`` subfolder of your app's bundle, and -+ - the ``app`` subfolder of your app's bundle -+ -+ Your app's bundle location can be determined using ``[[NSBundle mainBundle] -+ resourcePath]``. -+ -+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 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 targetting (i.e., delete any device binaries -+ if you're building app 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 -+ :meth:`site.addsitedir`), rather than adding to :envvar:`PYTHONPATH` or -+ :attr:`sys.path` directly. -+ -+Testing a Python package -+------------------------ -+ -+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:`Apple/iOS/README.md` -+for details), create a clone of the Python iOS testbed project. If you used the -+``Apple`` build script to build the XCframework, you can run: -+ -+.. code-block:: bash -+ -+ $ python cross-build/iOS/testbed clone --app --app app-testbed -+ -+Or, if you've sourced your own XCframework, by running: -+ -+.. code-block:: bash -+ -+ $ python Apple/testbed clone --platform iOS --framework --app --app app-testbed -+ -+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 -+could run: -+ -+.. code-block:: bash -+ -+ $ python app-testbed run -- module1.tests -+ -+This is the equivalent of running ``python -m module1.tests`` on a desktop -+Python build. Any arguments after the ``--`` will be passed to the testbed as -+if they were arguments to ``python -m`` on a desktop machine. -+ -+You can also open the testbed project in Xcode by running: -+ -+.. code-block:: bash -+ -+ $ open app-testbed/iOSTestbed.xcodeproj -+ -+This will allow you to use the full Xcode suite of tools for debugging. -+ -+The arguments used to run the test suite are defined as part of the test plan. -+To modify the test plan, select the test plan node of the project tree (it -+should be the first child of the root node), and select the "Configurations" -+tab. Modify the "Arguments Passed On Launch" value to change the testing -+arguments. -+ -+The test plan also disables parallel testing, and specifies the use of the -+``Testbed.lldbinit`` file for providing configuration of the debugger. The -+default debugger configuration disables automatic breakpoints on the -+``SIGINT``, ``SIGUSR1``, ``SIGUSR2``, and ``SIGXFSZ`` signals. -+ -+App Store Compliance -+==================== -+ -+The only mechanism for distributing apps to third-party iOS devices is to -+submit the app to the iOS App Store; apps submitted for distribution must pass -+Apple's app review process. This process includes a set of automated validation -+rules that inspect the submitted application bundle for problematic code. There -+are some steps that must be taken to ensure that your app will be able to pass -+these validation steps. -+ -+Incompatible code in the standard library -+----------------------------------------- -+ -+The Python standard library contains some code that is known to violate these -+automated rules. While these violations appear to be false positives, Apple's -+review rules cannot be challenged; so, it is necessary to modify the Python -+standard library for an app to pass App Store review. -+ -+The Python source tree contains -+:source:`a patch file ` that will remove -+all code that is known to cause issues with the App Store review process. This -+patch is applied automatically when building for iOS. -+ -+Privacy manifests -+----------------- -+ -+In April 2025, Apple introduced a requirement for `certain third-party -+libraries to provide a Privacy Manifest -+`__. -+As a result, if you have a binary module that uses one of the affected -+libraries, you must provide an ``.xcprivacy`` file for that library. -+OpenSSL is one library affected by this requirement, but there are others. -+ -+If you produce a binary module named ``mymodule.so``, and use you the Xcode -+build script described in step 7 above, you can place a ``mymodule.xcprivacy`` -+file next to ``mymodule.so``, and the privacy manifest will be installed into -+the required location when the binary module is converted into a framework. -diff --git a/Doc/using/mac.rst b/Doc/using/mac.rst -index 8b67652d1df..2dfac075843 100644 ---- a/Doc/using/mac.rst -+++ b/Doc/using/mac.rst -@@ -188,6 +188,28 @@ - * `PyInstaller `__: A cross-platform packaging tool that creates - a single file or folder as a distributable artifact. - -+App Store Compliance -+-------------------- -+ -+Apps submitted for distribution through the macOS App Store must pass Apple's -+app review process. This process includes a set of automated validation rules -+that inspect the submitted application bundle for problematic code. -+ -+The Python standard library contains some code that is known to violate these -+automated rules. While these violations appear to be false positives, Apple's -+review rules cannot be challenged. Therefore, it is necessary to modify the -+Python standard library for an app to pass App Store review. -+ -+The Python source tree contains -+:source:`a patch file ` that will remove -+all code that is known to cause issues with the App Store review process. This -+patch is applied automatically when CPython is configured with the -+:option:`--with-app-store-compliance` option. -+ -+This patch is not normally required to use CPython on a Mac; nor is it required -+if you are distributing an app *outside* the macOS App Store. It is *only* -+required if you are using the macOS App Store as a distribution channel. -+ - Other Resources - =============== - -diff --git a/Lib/_apple_support.py b/Lib/_apple_support.py -new file mode 100644 -index 00000000000..92febdcf587 ---- /dev/null -+++ b/Lib/_apple_support.py -@@ -0,0 +1,66 @@ -+import io -+import sys -+ -+ -+def init_streams(log_write, stdout_level, stderr_level): -+ # Redirect stdout and stderr to the Apple system log. This method is -+ # invoked by init_apple_streams() (initconfig.c) if config->use_system_logger -+ # is enabled. -+ sys.stdout = SystemLog(log_write, stdout_level, errors=sys.stderr.errors) -+ sys.stderr = SystemLog(log_write, stderr_level, errors=sys.stderr.errors) -+ -+ -+class SystemLog(io.TextIOWrapper): -+ def __init__(self, log_write, level, **kwargs): -+ kwargs.setdefault("encoding", "UTF-8") -+ kwargs.setdefault("line_buffering", True) -+ super().__init__(LogStream(log_write, level), **kwargs) -+ -+ def __repr__(self): -+ return f"" -+ -+ def write(self, s): -+ if not isinstance(s, str): -+ raise TypeError( -+ f"write() argument must be str, not {type(s).__name__}") -+ -+ # In case `s` is a str subclass that writes itself to stdout or stderr -+ # when we call its methods, convert it to an actual str. -+ s = str.__str__(s) -+ -+ # We want to emit one log message per line, so split -+ # the string before sending it to the superclass. -+ for line in s.splitlines(keepends=True): -+ super().write(line) -+ -+ return len(s) -+ -+ -+class LogStream(io.RawIOBase): -+ def __init__(self, log_write, level): -+ self.log_write = log_write -+ self.level = level -+ -+ def __repr__(self): -+ return f"" -+ -+ def writable(self): -+ return True -+ -+ def write(self, b): -+ if type(b) is not bytes: -+ try: -+ b = bytes(memoryview(b)) -+ except TypeError: -+ raise TypeError( -+ f"write() argument must be bytes-like, not {type(b).__name__}" -+ ) from None -+ -+ # Writing an empty string to the stream should have no effect. -+ if b: -+ # Encode null bytes using "modified UTF-8" to avoid truncating the -+ # message. This should not affect the return value, as the caller -+ # may be expecting it to match the length of the input. -+ self.log_write(self.level, b.replace(b"\x00", b"\xc0\x80")) -+ -+ return len(b) -diff --git a/Lib/_ios_support.py b/Lib/_ios_support.py -new file mode 100644 -index 00000000000..542f5a14910 ---- /dev/null -+++ b/Lib/_ios_support.py -@@ -0,0 +1,74 @@ -+import sys -+try: -+ from ctypes import cdll, c_void_p, c_char_p, util -+except ImportError: -+ # ctypes is an optional module. If it's not present, we're limited in what -+ # we can tell about the system, but we don't want to prevent the module -+ # from working. -+ print("ctypes isn't available; iOS system calls will not be available") -+ objc = None -+else: -+ # ctypes is available. Load the ObjC library, and wrap the objc_getClass, -+ # sel_registerName methods -+ # find_library() resolves a system library through the dyld shared cache, -+ # which needs _dyld_shared_cache_contains_path(); that is unavailable before -+ # iOS 14, so fall back to the bare name, which dyld resolves by itself. -+ lib = util.find_library("objc") or "libobjc.dylib" -+ -+ try: -+ objc = cdll.LoadLibrary(lib) -+ except OSError: -+ raise ImportError("ObjC runtime library couldn't be loaded") -+ objc.objc_getClass.restype = c_void_p -+ objc.objc_getClass.argtypes = [c_char_p] -+ objc.sel_registerName.restype = c_void_p -+ objc.sel_registerName.argtypes = [c_char_p] -+ -+ -+def get_platform_ios(): -+ # Determine if this is a simulator using the multiarch value -+ is_simulator = sys.implementation._multiarch.endswith("simulator") -+ -+ # We can't use ctypes; abort -+ if not objc: -+ return None -+ -+ # Most of the methods return ObjC objects -+ objc.objc_msgSend.restype = c_void_p -+ # All the methods used have no arguments. -+ objc.objc_msgSend.argtypes = [c_void_p, c_void_p] -+ -+ # Equivalent of: -+ # device = [UIDevice currentDevice] -+ UIDevice = objc.objc_getClass(b"UIDevice") -+ SEL_currentDevice = objc.sel_registerName(b"currentDevice") -+ device = objc.objc_msgSend(UIDevice, SEL_currentDevice) -+ -+ # Equivalent of: -+ # device_systemVersion = [device systemVersion] -+ SEL_systemVersion = objc.sel_registerName(b"systemVersion") -+ device_systemVersion = objc.objc_msgSend(device, SEL_systemVersion) -+ -+ # Equivalent of: -+ # device_systemName = [device systemName] -+ SEL_systemName = objc.sel_registerName(b"systemName") -+ device_systemName = objc.objc_msgSend(device, SEL_systemName) -+ -+ # Equivalent of: -+ # device_model = [device model] -+ SEL_model = objc.sel_registerName(b"model") -+ device_model = objc.objc_msgSend(device, SEL_model) -+ -+ # UTF8String returns a const char*; -+ SEL_UTF8String = objc.sel_registerName(b"UTF8String") -+ objc.objc_msgSend.restype = c_char_p -+ -+ # Equivalent of: -+ # system = [device_systemName UTF8String] -+ # release = [device_systemVersion UTF8String] -+ # model = [device_model UTF8String] -+ system = objc.objc_msgSend(device_systemName, SEL_UTF8String).decode() -+ release = objc.objc_msgSend(device_systemVersion, SEL_UTF8String).decode() -+ model = objc.objc_msgSend(device_model, SEL_UTF8String).decode() -+ -+ return system, release, model, is_simulator -diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py -index 6cedee74236..0e88cffc74f 100644 ---- a/Lib/ctypes/__init__.py -+++ b/Lib/ctypes/__init__.py -@@ -346,6 +346,17 @@ - winmode=None): - if name: - name = _os.fspath(name) -+ -+ # If the filename that has been provided is an iOS/tvOS/watchOS -+ # .fwork file, dereference the location to the true origin of the -+ # binary. -+ if name.endswith(".fwork"): -+ with open(name) as f: -+ name = _os.path.join( -+ _os.path.dirname(_sys.executable), -+ f.read().strip() -+ ) -+ - self._name = name - flags = self._func_flags_ - if use_errno: -diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py -index c550883e7c7..12d7428fe9a 100644 ---- a/Lib/ctypes/util.py -+++ b/Lib/ctypes/util.py -@@ -67,7 +67,7 @@ - return fname - return None - --elif os.name == "posix" and sys.platform == "darwin": -+elif os.name == "posix" and sys.platform in {"darwin", "ios", "tvos", "watchos"}: - from ctypes.macholib.dyld import dyld_find as _dyld_find - def find_library(name): - possible = ['lib%s.dylib' % name, -diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py -index 6e4a087a10c..f0cd49b6773 100644 ---- a/Lib/importlib/_bootstrap_external.py -+++ b/Lib/importlib/_bootstrap_external.py -@@ -52,7 +52,7 @@ - - # Bootstrap-related code ###################################################### - _CASE_INSENSITIVE_PLATFORMS_STR_KEY = 'win', --_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY = 'cygwin', 'darwin' -+_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY = 'cygwin', 'darwin', 'ios', 'tvos', 'watchos' - _CASE_INSENSITIVE_PLATFORMS = (_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY - + _CASE_INSENSITIVE_PLATFORMS_STR_KEY) - -@@ -1698,6 +1698,46 @@ - return f'FileFinder({self.path!r})' - - -+class AppleFrameworkLoader(ExtensionFileLoader): -+ """A loader for modules that have been packaged as frameworks for -+ compatibility with Apple's iOS App Store policies. -+ """ -+ def create_module(self, spec): -+ # If the ModuleSpec has been created by the FileFinder, it will have -+ # been created with an origin pointing to the .fwork file. We need to -+ # redirect this to the location in the Frameworks folder, using the -+ # content of the .fwork file. -+ if spec.origin.endswith(".fwork"): -+ with _io.FileIO(spec.origin, 'r') as file: -+ framework_binary = file.read().decode().strip() -+ bundle_path = _path_split(sys.executable)[0] -+ spec.origin = _path_join(bundle_path, framework_binary) -+ -+ # If the loader is created based on the spec for a loaded module, the -+ # path will be pointing at the Framework location. If this occurs, -+ # get the original .fwork location to use as the module's __file__. -+ if self.path.endswith(".fwork"): -+ path = self.path -+ else: -+ with _io.FileIO(self.path + ".origin", 'r') as file: -+ origin = file.read().decode().strip() -+ bundle_path = _path_split(sys.executable)[0] -+ path = _path_join(bundle_path, origin) -+ -+ module = _bootstrap._call_with_frames_removed(_imp.create_dynamic, spec) -+ -+ _bootstrap._verbose_message( -+ "Apple framework extension module {!r} loaded from {!r} (path {!r})", -+ spec.name, -+ spec.origin, -+ path, -+ ) -+ -+ # Ensure that the __file__ points at the .fwork location -+ module.__file__ = path -+ -+ return module -+ - # Import setup ############################################################### - - def _fix_up_module(ns, name, pathname, cpathname=None): -@@ -1730,10 +1770,17 @@ - - Each item is a tuple (loader, suffixes). - """ -- extensions = ExtensionFileLoader, _imp.extension_suffixes() -+ if sys.platform in {"ios", "tvos", "watchos"}: -+ extension_loaders = [(AppleFrameworkLoader, [ -+ suffix.replace(".so", ".fwork") -+ for suffix in _imp.extension_suffixes() -+ ])] -+ else: -+ extension_loaders = [] -+ extension_loaders.append((ExtensionFileLoader, _imp.extension_suffixes())) - source = SourceFileLoader, SOURCE_SUFFIXES - bytecode = SourcelessFileLoader, BYTECODE_SUFFIXES -- return [extensions, source, bytecode] -+ return extension_loaders + [source, bytecode] - - - def _set_bootstrap_module(_bootstrap_module): -diff --git a/Lib/importlib/abc.py b/Lib/importlib/abc.py -index b56fa94eb9c..37fef357fe2 100644 ---- a/Lib/importlib/abc.py -+++ b/Lib/importlib/abc.py -@@ -180,7 +180,11 @@ - else: - return self.source_to_code(source, path) - --_register(ExecutionLoader, machinery.ExtensionFileLoader) -+_register( -+ ExecutionLoader, -+ machinery.ExtensionFileLoader, -+ machinery.AppleFrameworkLoader, -+) - - - class FileLoader(_bootstrap_external.FileLoader, ResourceLoader, ExecutionLoader): -diff --git a/Lib/importlib/machinery.py b/Lib/importlib/machinery.py -index d9a19a13f7b..fbd30b159fb 100644 ---- a/Lib/importlib/machinery.py -+++ b/Lib/importlib/machinery.py -@@ -12,6 +12,7 @@ - from ._bootstrap_external import SourceFileLoader - from ._bootstrap_external import SourcelessFileLoader - from ._bootstrap_external import ExtensionFileLoader -+from ._bootstrap_external import AppleFrameworkLoader - from ._bootstrap_external import NamespaceLoader - - -diff --git a/Lib/inspect.py b/Lib/inspect.py -index 2a8d9b053cc..4d0bbcb3835 100644 ---- a/Lib/inspect.py -+++ b/Lib/inspect.py -@@ -961,6 +961,10 @@ - elif any(filename.endswith(s) for s in - importlib.machinery.EXTENSION_SUFFIXES): - return None -+ elif filename.endswith(".fwork"): -+ # Apple mobile framework markers are another type of non-source file -+ return None -+ - # return a filename found in the linecache even if it doesn't exist on disk - if filename in linecache.cache: - return filename -@@ -991,6 +995,7 @@ - return object - if hasattr(object, '__module__'): - return sys.modules.get(object.__module__) -+ - # Try the filename to modulename cache - if _filename is not None and _filename in modulesbyfile: - return sys.modules.get(modulesbyfile[_filename]) -@@ -1084,7 +1089,7 @@ - # Allow filenames in form of "" to pass through. - # `doctest` monkeypatches `linecache` module to enable - # inspection, so let `linecache.getlines` to be called. -- if not (file.startswith('<') and file.endswith('>')): -+ if (not (file.startswith('<') and file.endswith('>'))) or file.endswith('.fwork'): - raise OSError('source code not available') - - module = getmodule(object, file) -diff --git a/Lib/modulefinder.py b/Lib/modulefinder.py -index a0a020f9eeb..ac478ee7f51 100644 ---- a/Lib/modulefinder.py -+++ b/Lib/modulefinder.py -@@ -72,7 +72,12 @@ - if isinstance(spec.loader, importlib.machinery.SourceFileLoader): - kind = _PY_SOURCE - -- elif isinstance(spec.loader, importlib.machinery.ExtensionFileLoader): -+ elif isinstance( -+ spec.loader, ( -+ importlib.machinery.ExtensionFileLoader, -+ importlib.machinery.AppleFrameworkLoader, -+ ) -+ ): - kind = _C_EXTENSION - - elif isinstance(spec.loader, importlib.machinery.SourcelessFileLoader): -diff --git a/Lib/platform.py b/Lib/platform.py -index b86e6834911..115c2f6d21c 100755 ---- a/Lib/platform.py -+++ b/Lib/platform.py -@@ -498,6 +498,78 @@ - # If that also doesn't work return the default values - return release, versioninfo, machine - -+ -+# A namedtuple for iOS version information. -+IOSVersionInfo = collections.namedtuple( -+ "IOSVersionInfo", -+ ["system", "release", "model", "is_simulator"] -+) -+ -+ -+def ios_ver(system="", release="", model="", is_simulator=False): -+ """Get iOS version information, and return it as a namedtuple: -+ (system, release, model, is_simulator). -+ -+ If values can't be determined, they are set to values provided as -+ parameters. -+ """ -+ if sys.platform == "ios": -+ import _ios_support -+ result = _ios_support.get_platform_ios() -+ if result is not None: -+ return IOSVersionInfo(*result) -+ -+ return IOSVersionInfo(system, release, model, is_simulator) -+ -+ -+# A namedtuple for tvOS version information. -+TVOSVersionInfo = collections.namedtuple( -+ "TVOSVersionInfo", -+ ["system", "release", "model", "is_simulator"] -+) -+ -+ -+def tvos_ver(system="", release="", model="", is_simulator=False): -+ """Get tvOS version information, and return it as a namedtuple: -+ (system, release, model, is_simulator). -+ -+ If values can't be determined, they are set to values provided as -+ parameters. -+ """ -+ if sys.platform == "tvos": -+ # TODO: Can the iOS implementation be used here? -+ import _ios_support -+ result = _ios_support.get_platform_ios() -+ if result is not None: -+ return TVOSVersionInfo(*result) -+ -+ return TVOSVersionInfo(system, release, model, is_simulator) -+ -+ -+# A namedtuple for watchOS version information. -+WatchOSVersionInfo = collections.namedtuple( -+ "WatchOSVersionInfo", -+ ["system", "release", "model", "is_simulator"] -+) -+ -+ -+def watchos_ver(system="", release="", model="", is_simulator=False): -+ """Get watchOS version information, and return it as a namedtuple: -+ (system, release, model, is_simulator). -+ -+ If values can't be determined, they are set to values provided as -+ parameters. -+ """ -+ if sys.platform == "watchos": -+ # TODO: Can the iOS implementation be used here? -+ import _ios_support -+ result = _ios_support.get_platform_ios() -+ if result is not None: -+ return WatchOSVersionInfo(*result) -+ -+ return WatchOSVersionInfo(system, release, model, is_simulator) -+ -+ - def _java_getprop(name, default): - - from java.lang import System -@@ -613,7 +685,7 @@ - if cleaned == platform: - break - platform = cleaned -- while platform[-1] == '-': -+ while platform and platform[-1] == '-': - platform = platform[:-1] - - return platform -@@ -654,7 +726,7 @@ - default in case the command should fail. - - """ -- if sys.platform in ('dos', 'win32', 'win16'): -+ if sys.platform in {'dos', 'win32', 'win16', 'ios', 'tvos', 'watchos'}: - # XXX Others too ? - return default - -@@ -816,6 +888,25 @@ - csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0) - return 'Alpha' if cpu_number >= 128 else 'VAX' - -+ # On the iOS/tvOS/watchOS simulator, os.uname returns the architecture as -+ # uname.machine. On device it returns the model name for some reason; but -+ # there's only one CPU architecture for devices, so we know the right -+ # answer. -+ def get_ios(): -+ if sys.implementation._multiarch.endswith("simulator"): -+ return os.uname().machine -+ return 'arm64' -+ -+ def get_tvos(): -+ if sys.implementation._multiarch.endswith("simulator"): -+ return os.uname().machine -+ return 'arm64' -+ -+ def get_watchos(): -+ if sys.implementation._multiarch.endswith("simulator"): -+ return os.uname().machine -+ return 'arm64_32' -+ - def from_subprocess(): - """ - Fall back to `uname -p` -@@ -970,6 +1061,14 @@ - system = 'Windows' - release = 'Vista' - -+ # Normalize responses on Apple mobile platforms -+ if sys.platform == 'ios': -+ system, release, _, _ = ios_ver() -+ if sys.platform == 'tvos': -+ system, release, _, _ = tvos_ver() -+ if sys.platform == 'watchos': -+ system, release, _, _ = watchos_ver() -+ - vals = system, node, release, version, machine - # Replace 'unknown' values with the more portable '' - _uname_cache = uname_result(*map(_unknown_as_blank, vals)) -@@ -1249,11 +1348,18 @@ - system, release, version = system_alias(system, release, version) - - if system == 'Darwin': -- # macOS (darwin kernel) -- macos_release = mac_ver()[0] -- if macos_release: -- system = 'macOS' -- release = macos_release -+ # macOS and iOS both report as a "Darwin" kernel -+ if sys.platform == "ios": -+ system, release, _, _ = ios_ver() -+ elif sys.platform == "tvos": -+ system, release, _, _ = tvos_ver() -+ elif sys.platform == "watchos": -+ system, release, _, _ = watchos_ver() -+ else: -+ macos_release = mac_ver()[0] -+ if macos_release: -+ system = 'macOS' -+ release = macos_release - - if system == 'Windows': - # MS platforms -diff --git a/Lib/site.py b/Lib/site.py -index aed254ad504..ecbb2f57b3c 100644 ---- a/Lib/site.py -+++ b/Lib/site.py -@@ -287,8 +287,8 @@ - if env_base: - return env_base - -- # Emscripten, VxWorks, and WASI have no home directories -- if sys.platform in {"emscripten", "vxworks", "wasi"}: -+ # Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories -+ if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}: - return None - - def joinuser(*args): -diff --git a/Lib/subprocess.py b/Lib/subprocess.py -index 881a9ce800a..e209f51013a 100644 ---- a/Lib/subprocess.py -+++ b/Lib/subprocess.py -@@ -74,8 +74,8 @@ - else: - _mswindows = True - --# wasm32-emscripten and wasm32-wasi do not support processes --_can_fork_exec = sys.platform not in {"emscripten", "wasi"} -+# some platforms do not support subprocesses -+_can_fork_exec = sys.platform not in {"emscripten", "wasi", "ios", "tvos", "watchos"} - - if _mswindows: - import _winapi -@@ -103,18 +103,22 @@ - if _can_fork_exec: - from _posixsubprocess import fork_exec as _fork_exec - # used in methods that are called by __del__ -- _waitpid = os.waitpid -- _waitstatus_to_exitcode = os.waitstatus_to_exitcode -- _WIFSTOPPED = os.WIFSTOPPED -- _WSTOPSIG = os.WSTOPSIG -- _WNOHANG = os.WNOHANG -+ class _del_safe: -+ waitpid = os.waitpid -+ waitstatus_to_exitcode = os.waitstatus_to_exitcode -+ WIFSTOPPED = os.WIFSTOPPED -+ WSTOPSIG = os.WSTOPSIG -+ WNOHANG = os.WNOHANG -+ ECHILD = errno.ECHILD - else: -- _fork_exec = None -- _waitpid = None -- _waitstatus_to_exitcode = None -- _WIFSTOPPED = None -- _WSTOPSIG = None -- _WNOHANG = None -+ class _del_safe: -+ waitpid = None -+ waitstatus_to_exitcode = None -+ WIFSTOPPED = None -+ WSTOPSIG = None -+ WNOHANG = None -+ ECHILD = errno.ECHILD -+ - import select - import selectors - -@@ -1958,20 +1962,16 @@ - raise child_exception_type(err_msg) - - -- def _handle_exitstatus(self, sts, -- _waitstatus_to_exitcode=_waitstatus_to_exitcode, -- _WIFSTOPPED=_WIFSTOPPED, -- _WSTOPSIG=_WSTOPSIG): -+ def _handle_exitstatus(self, sts, _del_safe=_del_safe): - """All callers to this function MUST hold self._waitpid_lock.""" - # This method is called (indirectly) by __del__, so it cannot - # refer to anything outside of its local scope. -- if _WIFSTOPPED(sts): -- self.returncode = -_WSTOPSIG(sts) -+ if _del_safe.WIFSTOPPED(sts): -+ self.returncode = -_del_safe.WSTOPSIG(sts) - else: -- self.returncode = _waitstatus_to_exitcode(sts) -+ self.returncode = _del_safe.waitstatus_to_exitcode(sts) - -- def _internal_poll(self, _deadstate=None, _waitpid=_waitpid, -- _WNOHANG=_WNOHANG, _ECHILD=errno.ECHILD): -+ def _internal_poll(self, _deadstate=None, _del_safe=_del_safe): - """Check if child process has terminated. Returns returncode - attribute. - -@@ -1987,13 +1987,13 @@ - try: - if self.returncode is not None: - return self.returncode # Another thread waited. -- pid, sts = _waitpid(self.pid, _WNOHANG) -+ pid, sts = _del_safe.waitpid(self.pid, _del_safe.WNOHANG) - if pid == self.pid: - self._handle_exitstatus(sts) - except OSError as e: - if _deadstate is not None: - self.returncode = _deadstate -- elif e.errno == _ECHILD: -+ elif e.errno == _del_safe.ECHILD: - # This happens if SIGCLD is set to be ignored or - # waiting for child processes has otherwise been - # disabled for our process. This child is dead, we -diff --git a/Lib/sysconfig.py b/Lib/sysconfig.py -index acc8d4d1826..7d4ebe3ccfa 100644 ---- a/Lib/sysconfig.py -+++ b/Lib/sysconfig.py -@@ -21,6 +21,7 @@ - - # Keys for get_config_var() that are never converted to Python integers. - _ALWAYS_STR = { -+ 'IPHONEOS_DEPLOYMENT_TARGET', - 'MACOSX_DEPLOYMENT_TARGET', - } - -@@ -57,6 +58,7 @@ - 'scripts': '{base}/Scripts', - 'data': '{base}', - }, -+ - # Downstream distributors can overwrite the default install scheme. - # This is done to support downstream modifications where distributors change - # the installation layout (eg. different site-packages directory). -@@ -112,8 +114,8 @@ - if env_base: - return env_base - -- # Emscripten, VxWorks, and WASI have no home directories -- if sys.platform in {"emscripten", "vxworks", "wasi"}: -+ # Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories -+ if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}: - return None - - def joinuser(*args): -@@ -299,6 +301,7 @@ - 'home': 'posix_home', - 'user': 'osx_framework_user', - } -+ - return { - 'prefix': 'posix_prefix', - 'home': 'posix_home', -@@ -831,10 +834,23 @@ - if m: - release = m.group() - elif osname[:6] == "darwin": -- import _osx_support -- osname, release, machine = _osx_support.get_platform_osx( -- get_config_vars(), -- osname, release, machine) -+ if sys.platform == "ios": -+ release = get_config_vars().get("IPHONEOS_DEPLOYMENT_TARGET", "13.0") -+ osname = sys.platform -+ machine = sys.implementation._multiarch -+ elif sys.platform == "tvos": -+ release = get_config_vars().get("TVOS_DEPLOYMENT_TARGET", "9.0") -+ osname = sys.platform -+ machine = sys.implementation._multiarch -+ elif sys.platform == "watchos": -+ release = get_config_vars().get("WATCHOS_DEPLOYMENT_TARGET", "4.0") -+ osname = sys.platform -+ machine = sys.implementation._multiarch -+ else: -+ import _osx_support -+ osname, release, machine = _osx_support.get_platform_osx( -+ get_config_vars(), -+ osname, release, machine) - - return f"{osname}-{release}-{machine}" - -diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py -index 5361475a14f..4a1f280566c 100644 ---- a/Lib/test/pythoninfo.py -+++ b/Lib/test/pythoninfo.py -@@ -287,6 +287,7 @@ - "HOMEDRIVE", - "HOMEPATH", - "IDLESTARTUP", -+ "IPHONEOS_DEPLOYMENT_TARGET", - "LANG", - "LDFLAGS", - "LDSHARED", -diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py -index 1ee8ffc1b7a..9ab3b337ede 100644 ---- a/Lib/test/support/__init__.py -+++ b/Lib/test/support/__init__.py -@@ -59,6 +59,7 @@ - "Py_DEBUG", "EXCEEDS_RECURSION_LIMIT", "C_RECURSION_LIMIT", - "skip_on_s390x", - "BrokenIter", -+ "reset_code", "on_github_actions" - ] - - -@@ -1238,6 +1239,8 @@ - opcode.ENABLE_SPECIALIZATION, "requires specialization")(test) - - -+on_github_actions = "GITHUB_ACTIONS" in os.environ -+ - #======================================================================= - # Check for the presence of docstrings. - -diff --git a/Lib/test/support/os_helper.py b/Lib/test/support/os_helper.py -index 9eb0af1d285..645fbbefbb9 100644 ---- a/Lib/test/support/os_helper.py -+++ b/Lib/test/support/os_helper.py -@@ -22,8 +22,8 @@ - - # TESTFN_UNICODE is a non-ascii filename - TESTFN_UNICODE = TESTFN_ASCII + "-\xe0\xf2\u0258\u0141\u011f" --if sys.platform == 'darwin': -- # In Mac OS X's VFS API file names are, by definition, canonically -+if support.is_apple: -+ # On Apple's VFS API file names are, by definition, canonically - # decomposed Unicode, encoded using UTF-8. See QA1173: - # http://developer.apple.com/mac/library/qa/qa2001/qa1173.html - import unicodedata -@@ -48,8 +48,8 @@ - 'encoding (%s). Unicode filename tests may not be effective' - % (TESTFN_UNENCODABLE, sys.getfilesystemencoding())) - TESTFN_UNENCODABLE = None --# macOS and Emscripten deny unencodable filenames (invalid utf-8) --elif sys.platform not in {'darwin', 'emscripten', 'wasi'}: -+# Apple and Emscripten deny unencodable filenames (invalid utf-8) -+elif not support.is_apple and sys.platform not in {"emscripten", "wasi"}: - try: - # ascii and utf-8 cannot encode the byte 0xff - b'\xff'.decode(sys.getfilesystemencoding()) -@@ -633,7 +633,8 @@ - if hasattr(os, 'sysconf'): - try: - MAXFD = os.sysconf("SC_OPEN_MAX") -- except OSError: -+ except (OSError, ValueError): -+ # gh-118201: ValueError is raised intermittently on iOS - pass - - old_modes = None -diff --git a/Lib/test/test_apple.py b/Lib/test/test_apple.py -new file mode 100644 -index 00000000000..f14db75e2f2 ---- /dev/null -+++ b/Lib/test/test_apple.py -@@ -0,0 +1,155 @@ -+import unittest -+from _apple_support import SystemLog -+from test.support import is_apple_mobile -+from unittest.mock import Mock, call -+ -+if not is_apple_mobile: -+ raise unittest.SkipTest("iOS-specific") -+ -+ -+# Test redirection of stdout and stderr to the Apple system log. -+class TestAppleSystemLogOutput(unittest.TestCase): -+ maxDiff = None -+ -+ def assert_writes(self, output): -+ self.assertEqual( -+ self.log_write.mock_calls, -+ [ -+ call(self.log_level, line) -+ for line in output -+ ] -+ ) -+ -+ self.log_write.reset_mock() -+ -+ def setUp(self): -+ self.log_write = Mock() -+ self.log_level = 42 -+ self.log = SystemLog(self.log_write, self.log_level, errors="replace") -+ -+ def test_repr(self): -+ self.assertEqual(repr(self.log), "") -+ self.assertEqual(repr(self.log.buffer), "") -+ -+ def test_log_config(self): -+ self.assertIs(self.log.writable(), True) -+ self.assertIs(self.log.readable(), False) -+ -+ self.assertEqual("UTF-8", self.log.encoding) -+ self.assertEqual("replace", self.log.errors) -+ -+ self.assertIs(self.log.line_buffering, True) -+ self.assertIs(self.log.write_through, False) -+ -+ def test_empty_str(self): -+ self.log.write("") -+ self.log.flush() -+ -+ self.assert_writes([]) -+ -+ def test_simple_str(self): -+ self.log.write("hello world\n") -+ -+ self.assert_writes([b"hello world\n"]) -+ -+ def test_buffered_str(self): -+ self.log.write("h") -+ self.log.write("ello") -+ self.log.write(" ") -+ self.log.write("world\n") -+ self.log.write("goodbye.") -+ self.log.flush() -+ -+ self.assert_writes([b"hello world\n", b"goodbye."]) -+ -+ def test_manual_flush(self): -+ self.log.write("Hello") -+ -+ self.assert_writes([]) -+ -+ self.log.write(" world\nHere for a while...\nGoodbye") -+ self.assert_writes([b"Hello world\n", b"Here for a while...\n"]) -+ -+ self.log.write(" world\nHello again") -+ self.assert_writes([b"Goodbye world\n"]) -+ -+ self.log.flush() -+ self.assert_writes([b"Hello again"]) -+ -+ def test_non_ascii(self): -+ # Spanish -+ self.log.write("ol\u00e9\n") -+ self.assert_writes([b"ol\xc3\xa9\n"]) -+ -+ # Chinese -+ self.log.write("\u4e2d\u6587\n") -+ self.assert_writes([b"\xe4\xb8\xad\xe6\x96\x87\n"]) -+ -+ # Printing Non-BMP emoji -+ self.log.write("\U0001f600\n") -+ self.assert_writes([b"\xf0\x9f\x98\x80\n"]) -+ -+ # Non-encodable surrogates are replaced -+ self.log.write("\ud800\udc00\n") -+ self.assert_writes([b"??\n"]) -+ -+ def test_modified_null(self): -+ # Null characters are logged using "modified UTF-8". -+ self.log.write("\u0000\n") -+ self.assert_writes([b"\xc0\x80\n"]) -+ self.log.write("a\u0000\n") -+ self.assert_writes([b"a\xc0\x80\n"]) -+ self.log.write("\u0000b\n") -+ self.assert_writes([b"\xc0\x80b\n"]) -+ self.log.write("a\u0000b\n") -+ self.assert_writes([b"a\xc0\x80b\n"]) -+ -+ def test_nonstandard_str(self): -+ # String subclasses are accepted, but they should be converted -+ # to a standard str without calling any of their methods. -+ class CustomStr(str): -+ def splitlines(self, *args, **kwargs): -+ raise AssertionError() -+ -+ def __len__(self): -+ raise AssertionError() -+ -+ def __str__(self): -+ raise AssertionError() -+ -+ self.log.write(CustomStr("custom\n")) -+ self.assert_writes([b"custom\n"]) -+ -+ def test_non_str(self): -+ # Non-string classes are not accepted. -+ for obj in [b"", b"hello", None, 42]: -+ with self.subTest(obj=obj): -+ with self.assertRaisesRegex( -+ TypeError, -+ fr"write\(\) argument must be str, not " -+ fr"{type(obj).__name__}" -+ ): -+ self.log.write(obj) -+ -+ def test_byteslike_in_buffer(self): -+ # The underlying buffer *can* accept bytes-like objects -+ self.log.buffer.write(bytearray(b"hello")) -+ self.log.flush() -+ -+ self.log.buffer.write(b"") -+ self.log.flush() -+ -+ self.log.buffer.write(b"goodbye") -+ self.log.flush() -+ -+ self.assert_writes([b"hello", b"goodbye"]) -+ -+ def test_non_byteslike_in_buffer(self): -+ for obj in ["hello", None, 42]: -+ with self.subTest(obj=obj): -+ with self.assertRaisesRegex( -+ TypeError, -+ fr"write\(\) argument must be bytes-like, not " -+ fr"{type(obj).__name__}" -+ ): -+ self.log.buffer.write(obj) -diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py -index abf425f5ef0..ffcde82b63e 100644 ---- a/Lib/test/test_asyncio/test_events.py -+++ b/Lib/test/test_asyncio/test_events.py -@@ -1894,6 +1894,7 @@ - else: - self.assertEqual(-signal.SIGKILL, returncode) - -+ @support.requires_subprocess() - def test_subprocess_exec(self): - prog = os.path.join(os.path.dirname(__file__), 'echo.py') - -@@ -1915,6 +1916,7 @@ - self.check_killed(proto.returncode) - self.assertEqual(b'Python The Winner', proto.data[1]) - -+ @support.requires_subprocess() - def test_subprocess_interactive(self): - prog = os.path.join(os.path.dirname(__file__), 'echo.py') - -@@ -1942,6 +1944,7 @@ - self.loop.run_until_complete(proto.completed) - self.check_killed(proto.returncode) - -+ @support.requires_subprocess() - def test_subprocess_shell(self): - connect = self.loop.subprocess_shell( - functools.partial(MySubprocessProtocol, self.loop), -@@ -1958,6 +1961,7 @@ - self.assertEqual(proto.data[2], b'') - transp.close() - -+ @support.requires_subprocess() - def test_subprocess_exitcode(self): - connect = self.loop.subprocess_shell( - functools.partial(MySubprocessProtocol, self.loop), -@@ -1969,6 +1973,7 @@ - self.assertEqual(7, proto.returncode) - transp.close() - -+ @support.requires_subprocess() - def test_subprocess_close_after_finish(self): - connect = self.loop.subprocess_shell( - functools.partial(MySubprocessProtocol, self.loop), -@@ -1983,6 +1988,7 @@ - self.assertEqual(7, proto.returncode) - self.assertIsNone(transp.close()) - -+ @support.requires_subprocess() - def test_subprocess_kill(self): - prog = os.path.join(os.path.dirname(__file__), 'echo.py') - -@@ -1999,6 +2005,7 @@ - self.check_killed(proto.returncode) - transp.close() - -+ @support.requires_subprocess() - def test_subprocess_terminate(self): - prog = os.path.join(os.path.dirname(__file__), 'echo.py') - -@@ -2016,6 +2023,7 @@ - transp.close() - - @unittest.skipIf(sys.platform == 'win32', "Don't have SIGHUP") -+ @support.requires_subprocess() - def test_subprocess_send_signal(self): - # bpo-31034: Make sure that we get the default signal handler (killing - # the process). The parent process may have decided to ignore SIGHUP, -@@ -2040,6 +2048,7 @@ - finally: - signal.signal(signal.SIGHUP, old_handler) - -+ @support.requires_subprocess() - def test_subprocess_stderr(self): - prog = os.path.join(os.path.dirname(__file__), 'echo2.py') - -@@ -2061,6 +2070,7 @@ - self.assertTrue(proto.data[2].startswith(b'ERR:test'), proto.data[2]) - self.assertEqual(0, proto.returncode) - -+ @support.requires_subprocess() - def test_subprocess_stderr_redirect_to_stdout(self): - prog = os.path.join(os.path.dirname(__file__), 'echo2.py') - -@@ -2086,6 +2096,7 @@ - transp.close() - self.assertEqual(0, proto.returncode) - -+ @support.requires_subprocess() - def test_subprocess_close_client_stream(self): - prog = os.path.join(os.path.dirname(__file__), 'echo3.py') - -@@ -2120,6 +2131,7 @@ - self.loop.run_until_complete(proto.completed) - self.check_killed(proto.returncode) - -+ @support.requires_subprocess() - def test_subprocess_wait_no_same_group(self): - # start the new process in a new session - connect = self.loop.subprocess_shell( -@@ -2132,6 +2144,7 @@ - self.assertEqual(7, proto.returncode) - transp.close() - -+ @support.requires_subprocess() - def test_subprocess_exec_invalid_args(self): - async def connect(**kwds): - await self.loop.subprocess_exec( -@@ -2145,6 +2158,7 @@ - with self.assertRaises(ValueError): - self.loop.run_until_complete(connect(shell=True)) - -+ @support.requires_subprocess() - def test_subprocess_shell_invalid_args(self): - - async def connect(cmd=None, **kwds): -diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py -index 686fef8377f..4f59c31dfe4 100644 ---- a/Lib/test/test_asyncio/test_streams.py -+++ b/Lib/test/test_asyncio/test_streams.py -@@ -10,7 +10,6 @@ - import unittest - from unittest import mock - import warnings --from test.support import socket_helper - try: - import ssl - except ImportError: -@@ -18,6 +17,7 @@ - - import asyncio - from test.test_asyncio import utils as test_utils -+from test.support import requires_subprocess, socket_helper - - - def tearDownModule(): -@@ -770,6 +770,7 @@ - self.assertEqual(msg2, b"hello world 2!\n") - - @unittest.skipIf(sys.platform == 'win32', "Don't have pipes") -+ @requires_subprocess() - def test_read_all_from_pipe_reader(self): - # See asyncio issue 168. This test is derived from the example - # subprocess_attach_read_pipe.py, but we configure the -diff --git a/Lib/test/test_asyncio/test_subprocess.py b/Lib/test/test_asyncio/test_subprocess.py -index 859d2932c33..ed43895fd68 100644 ---- a/Lib/test/test_asyncio/test_subprocess.py -+++ b/Lib/test/test_asyncio/test_subprocess.py -@@ -47,6 +47,7 @@ - self._proc.pid = -1 - - -+@support.requires_subprocess() - class SubprocessTransportTests(test_utils.TestCase): - def setUp(self): - super().setUp() -@@ -110,6 +111,7 @@ - transport.close() - - -+@support.requires_subprocess() - class SubprocessMixin: - - def test_stdin_stdout(self): -diff --git a/Lib/test/test_asyncio/test_unix_events.py b/Lib/test/test_asyncio/test_unix_events.py -index 35c924a0cd6..9452213c685 100644 ---- a/Lib/test/test_asyncio/test_unix_events.py -+++ b/Lib/test/test_asyncio/test_unix_events.py -@@ -1873,7 +1873,7 @@ - wsock.close() - - --@unittest.skipUnless(hasattr(os, 'fork'), 'requires os.fork()') -+@support.requires_fork() - class TestFork(unittest.IsolatedAsyncioTestCase): - - async def test_fork_not_share_event_loop(self): -diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py -index f284e665d6f..90d9cdd45c0 100644 ---- a/Lib/test/test_capi/test_misc.py -+++ b/Lib/test/test_capi/test_misc.py -@@ -1949,6 +1949,13 @@ - self.addCleanup(os.close, r) - self.addCleanup(os.close, w) - -+ # Apple extensions must be distributed as frameworks. This requires -+ # a specialist loader. -+ if support.is_apple_mobile: -+ loader = "AppleFrameworkLoader" -+ else: -+ loader = "ExtensionFileLoader" -+ - script = textwrap.dedent(f""" - import importlib.machinery - import importlib.util -@@ -1956,7 +1963,7 @@ - - fullname = '_test_module_state_shared' - origin = importlib.util.find_spec('_testmultiphase').origin -- loader = importlib.machinery.ExtensionFileLoader(fullname, origin) -+ loader = importlib.machinery.{loader}(fullname, origin) - spec = importlib.util.spec_from_loader(fullname, loader) - module = importlib.util.module_from_spec(spec) - attr_id = str(id(module.Error)).encode() -@@ -2130,7 +2137,12 @@ - def setUp(self): - fullname = '_testmultiphase_meth_state_access' # XXX - origin = importlib.util.find_spec('_testmultiphase').origin -- loader = importlib.machinery.ExtensionFileLoader(fullname, origin) -+ # Apple extensions must be distributed as frameworks. This requires -+ # a specialist loader. -+ if support.is_apple_mobile: -+ loader = importlib.machinery.AppleFrameworkLoader(fullname, origin) -+ else: -+ loader = importlib.machinery.ExtensionFileLoader(fullname, origin) - spec = importlib.util.spec_from_loader(fullname, loader) - module = importlib.util.module_from_spec(spec) - loader.exec_module(module) -diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py -index 7109e3d164e..6f7e102c2ff 100644 ---- a/Lib/test/test_cmd_line_script.py -+++ b/Lib/test/test_cmd_line_script.py -@@ -14,8 +14,7 @@ - - import textwrap - from test import support --from test.support import import_helper --from test.support import os_helper -+from test.support import import_helper, is_apple, os_helper - from test.support.script_helper import ( - make_pkg, make_script, make_zip_pkg, make_zip_script, - assert_python_ok, assert_python_failure, spawn_python, kill_python) -@@ -555,12 +554,17 @@ - self.assertTrue(text[3].startswith('NameError')) - - def test_non_ascii(self): -- # Mac OS X denies the creation of a file with an invalid UTF-8 name. -+ # Apple platforms deny the creation of a file with an invalid UTF-8 name. - # Windows allows creating a name with an arbitrary bytes name, but - # Python cannot a undecodable bytes argument to a subprocess. -- # WASI does not permit invalid UTF-8 names. -- if (os_helper.TESTFN_UNDECODABLE -- and sys.platform not in ('win32', 'darwin', 'emscripten', 'wasi')): -+ # Emscripten/WASI does not permit invalid UTF-8 names. -+ if ( -+ os_helper.TESTFN_UNDECODABLE -+ and sys.platform not in { -+ "win32", "emscripten", "wasi" -+ } -+ and not is_apple -+ ): - name = os.fsdecode(os_helper.TESTFN_UNDECODABLE) - elif os_helper.TESTFN_NONASCII: - name = os_helper.TESTFN_NONASCII -diff --git a/Lib/test/test_concurrent_futures/test_thread_pool.py b/Lib/test/test_concurrent_futures/test_thread_pool.py -index 6e4a4b7caff..d1357ea7fcd 100644 ---- a/Lib/test/test_concurrent_futures/test_thread_pool.py -+++ b/Lib/test/test_concurrent_futures/test_thread_pool.py -@@ -49,6 +49,7 @@ - self.assertEqual(len(executor._threads), 1) - executor.shutdown(wait=True) - -+ @support.requires_fork() - @unittest.skipUnless(hasattr(os, 'register_at_fork'), 'need os.register_at_fork') - @support.requires_resource('cpu') - def test_hang_global_shutdown_lock(self): -diff --git a/Lib/test/test_fcntl.py b/Lib/test/test_fcntl.py -index 203dd6fe57d..6d734d05245 100644 ---- a/Lib/test/test_fcntl.py -+++ b/Lib/test/test_fcntl.py -@@ -6,7 +6,9 @@ - import struct - import sys - import unittest --from test.support import verbose, cpython_only, get_pagesize -+from test.support import ( -+ cpython_only, get_pagesize, is_apple, requires_subprocess, verbose -+) - from test.support.import_helper import import_module - from test.support.os_helper import TESTFN, unlink - -@@ -56,8 +58,10 @@ - else: - start_len = "qq" - -- if (sys.platform.startswith(('netbsd', 'freebsd', 'openbsd')) -- or sys.platform == 'darwin'): -+ if ( -+ sys.platform.startswith(('netbsd', 'freebsd', 'openbsd')) -+ or is_apple -+ ): - if struct.calcsize('l') == 8: - off_t = 'l' - pid_t = 'i' -@@ -157,6 +161,7 @@ - self.assertRaises(TypeError, fcntl.flock, 'spam', fcntl.LOCK_SH) - - @unittest.skipIf(platform.system() == "AIX", "AIX returns PermissionError") -+ @requires_subprocess() - def test_lockf_exclusive(self): - self.f = open(TESTFN, 'wb+') - cmd = fcntl.LOCK_EX | fcntl.LOCK_NB -@@ -169,6 +174,7 @@ - self.assertEqual(p.exitcode, 0) - - @unittest.skipIf(platform.system() == "AIX", "AIX returns PermissionError") -+ @requires_subprocess() - def test_lockf_share(self): - self.f = open(TESTFN, 'wb+') - cmd = fcntl.LOCK_SH | fcntl.LOCK_NB -diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py -index 7542f015f78..f1eff9430f7 100644 ---- a/Lib/test/test_ftplib.py -+++ b/Lib/test/test_ftplib.py -@@ -18,6 +18,7 @@ - - from unittest import mock, TestCase, skipUnless - from test import support -+from test.support import requires_subprocess - from test.support import threading_helper - from test.support import socket_helper - from test.support import warnings_helper -@@ -903,6 +904,7 @@ - - - @skipUnless(ssl, "SSL not available") -+@requires_subprocess() - class TestTLS_FTPClassMixin(TestFTPClass): - """Repeat TestFTPClass tests starting the TLS layer for both control - and data connections first. -@@ -919,6 +921,7 @@ - - - @skipUnless(ssl, "SSL not available") -+@requires_subprocess() - class TestTLS_FTPClass(TestCase): - """Specific TLS_FTP class tests.""" - -diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py -index 81bb5bb288e..dddf5e8cd93 100644 ---- a/Lib/test/test_gc.py -+++ b/Lib/test/test_gc.py -@@ -1188,6 +1188,7 @@ - self.assertEqual(len(gc.garbage), 0) - - -+ @requires_subprocess() - @unittest.skipIf(BUILD_WITH_NDEBUG, - 'built with -NDEBUG') - def test_refcount_errors(self): -diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py -index 1cec587e875..e18e84d08cf 100644 ---- a/Lib/test/test_genericpath.py -+++ b/Lib/test/test_genericpath.py -@@ -8,9 +8,9 @@ - import unittest - import warnings - from test import support --from test.support import is_emscripten --from test.support import os_helper --from test.support import warnings_helper -+from test.support import ( -+ is_apple, is_emscripten, os_helper, warnings_helper -+) - from test.support.script_helper import assert_python_ok - from test.support.os_helper import FakePath - -@@ -506,12 +506,16 @@ - self.assertIsInstance(abspath(path), str) - - def test_nonascii_abspath(self): -- if (os_helper.TESTFN_UNDECODABLE -- # macOS and Emscripten deny the creation of a directory with an -- # invalid UTF-8 name. Windows allows creating a directory with an -- # arbitrary bytes name, but fails to enter this directory -- # (when the bytes name is used). -- and sys.platform not in ('win32', 'darwin', 'emscripten', 'wasi')): -+ if ( -+ os_helper.TESTFN_UNDECODABLE -+ # Apple platforms and Emscripten/WASI deny the creation of a -+ # directory with an invalid UTF-8 name. Windows allows creating a -+ # directory with an arbitrary bytes name, but fails to enter this -+ # directory (when the bytes name is used). -+ and sys.platform not in { -+ "win32", "emscripten", "wasi" -+ } and not is_apple -+ ): - name = os_helper.TESTFN_UNDECODABLE - elif os_helper.TESTFN_NONASCII: - name = os_helper.TESTFN_NONASCII -diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py -index 96fc9ca574e..30da4bd4723 100644 ---- a/Lib/test/test_httpservers.py -+++ b/Lib/test/test_httpservers.py -@@ -31,8 +31,9 @@ - - import unittest - from test import support --from test.support import os_helper --from test.support import threading_helper -+from test.support import ( -+ is_apple, os_helper, requires_subprocess, threading_helper -+) - - support.requires_working_socket(module=True) - -@@ -411,8 +412,8 @@ - reader.close() - return body - -- @unittest.skipIf(sys.platform == 'darwin', -- 'undecodable name cannot always be decoded on macOS') -+ @unittest.skipIf(is_apple, -+ 'undecodable name cannot always be decoded on Apple platforms') - @unittest.skipIf(sys.platform == 'win32', - 'undecodable name cannot be decoded on win32') - @unittest.skipUnless(os_helper.TESTFN_UNDECODABLE, -@@ -423,11 +424,11 @@ - with open(os.path.join(self.tempdir, filename), 'wb') as f: - f.write(os_helper.TESTFN_UNDECODABLE) - response = self.request(self.base_url + '/') -- if sys.platform == 'darwin': -- # On Mac OS the HFS+ filesystem replaces bytes that aren't valid -- # UTF-8 into a percent-encoded value. -+ if is_apple: -+ # On Apple platforms the HFS+ filesystem replaces bytes that -+ # aren't valid UTF-8 into a percent-encoded value. - for name in os.listdir(self.tempdir): -- if name != 'test': # Ignore a filename created in setUp(). -+ if name != 'test': # Ignore a filename created in setUp(). - filename = name - break - body = self.check_status_and_reason(response, HTTPStatus.OK) -@@ -712,6 +713,7 @@ - - @unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0, - "This test can't be run reliably as root (issue #13308).") -+@requires_subprocess() - class CGIHTTPServerTestCase(BaseTestCase): - class request_handler(NoLogRequestHandler, CGIHTTPRequestHandler): - def run_cgi(self): -diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py -index 1ac756f1b15..a0cb51afcba 100644 ---- a/Lib/test/test_import/__init__.py -+++ b/Lib/test/test_import/__init__.py -@@ -5,7 +5,11 @@ - import importlib.util - from importlib._bootstrap_external import _get_sourcefile - from importlib.machinery import ( -- BuiltinImporter, ExtensionFileLoader, FrozenImporter, SourceFileLoader, -+ AppleFrameworkLoader, -+ BuiltinImporter, -+ ExtensionFileLoader, -+ FrozenImporter, -+ SourceFileLoader, - ) - import marshal - import os -@@ -26,7 +30,7 @@ - - from test.support import os_helper - from test.support import ( -- STDLIB_DIR, swap_attr, swap_item, cpython_only, is_emscripten, -+ STDLIB_DIR, swap_attr, swap_item, cpython_only, is_apple_mobile, is_emscripten, - is_wasi, run_in_subinterp, run_in_subinterp_with_config) - from test.support.import_helper import ( - forget, make_legacy_pyc, unlink, unload, ready_to_import, -@@ -63,6 +67,7 @@ - MODULE_KINDS = { - BuiltinImporter: 'built-in', - ExtensionFileLoader: 'extension', -+ AppleFrameworkLoader: 'framework extension', - FrozenImporter: 'frozen', - SourceFileLoader: 'pure Python', - } -@@ -88,7 +93,12 @@ - assert module.__spec__.origin == 'built-in', module.__spec__ - - def require_extension(module, *, skip=False): -- _require_loader(module, ExtensionFileLoader, skip) -+ # Apple extensions must be distributed as frameworks. This requires -+ # a specialist loader. -+ if is_apple_mobile: -+ _require_loader(module, AppleFrameworkLoader, skip) -+ else: -+ _require_loader(module, ExtensionFileLoader, skip) - - def require_frozen(module, *, skip=True): - module = _require_loader(module, FrozenImporter, skip) -@@ -131,7 +141,8 @@ - # it to its nominal state. - sys.modules.pop('_testsinglephase', None) - _orig._clear_globals() -- _testinternalcapi.clear_extension('_testsinglephase', _orig.__file__) -+ origin = _orig.__spec__.origin -+ _testinternalcapi.clear_extension('_testsinglephase', origin) - import _testsinglephase - - -@@ -354,10 +365,14 @@ - from _testcapi import i_dont_exist - self.assertEqual(cm.exception.name, '_testcapi') - if hasattr(_testcapi, "__file__"): -- self.assertEqual(cm.exception.path, _testcapi.__file__) -+ # The path on the exception is strictly the spec origin, not the -+ # module's __file__. For most cases, these are the same; but on -+ # iOS, the Framework relocation process results in the exception -+ # being raised from the spec location. -+ self.assertEqual(cm.exception.path, _testcapi.__spec__.origin) - self.assertRegex( - str(cm.exception), -- r"cannot import name 'i_dont_exist' from '_testcapi' \(.*\.(so|pyd)\)" -+ r"cannot import name 'i_dont_exist' from '_testcapi' \(.*(\.(so|pyd))?\)" - ) - else: - self.assertEqual( -@@ -1719,6 +1734,14 @@ - os.set_blocking(r, False) - return (r, w) - -+ def create_extension_loader(self, modname, filename): -+ # Apple extensions must be distributed as frameworks. This requires -+ # a specialist loader. -+ if is_apple_mobile: -+ return AppleFrameworkLoader(modname, filename) -+ else: -+ return ExtensionFileLoader(modname, filename) -+ - def import_script(self, name, fd, filename=None, check_override=None): - override_text = '' - if check_override is not None: -@@ -1727,12 +1750,19 @@ - _imp._override_multi_interp_extensions_check({check_override}) - ''' - if filename: -+ # Apple extensions must be distributed as frameworks. This requires -+ # a specialist loader. -+ if is_apple_mobile: -+ loader = "AppleFrameworkLoader" -+ else: -+ loader = "ExtensionFileLoader" -+ - return textwrap.dedent(f''' - from importlib.util import spec_from_loader, module_from_spec -- from importlib.machinery import ExtensionFileLoader -+ from importlib.machinery import {loader} - import os, sys - {override_text} -- loader = ExtensionFileLoader({name!r}, {filename!r}) -+ loader = {loader}({name!r}, {filename!r}) - spec = spec_from_loader({name!r}, loader) - try: - module = module_from_spec(spec) -@@ -1916,7 +1946,7 @@ - # and Py_MOD_GIL_NOT_USED - modname = '_test_non_isolated' - filename = _testmultiphase.__file__ -- loader = ExtensionFileLoader(modname, filename) -+ loader = self.create_extension_loader(modname, filename) - spec = importlib.util.spec_from_loader(modname, loader) - module = importlib.util.module_from_spec(spec) - loader.exec_module(module) -@@ -1932,33 +1962,31 @@ - - @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") - def test_multi_init_extension_per_interpreter_gil_compat(self): -- # _test_shared_gil_only: -- # Explicit Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED (default) -- # and Py_MOD_GIL_NOT_USED -- # _test_no_multiple_interpreter_slot: -- # No Py_mod_multiple_interpreters slot -- # and Py_MOD_GIL_NOT_USED -- for modname in ('_test_shared_gil_only', -- '_test_no_multiple_interpreter_slot'): -- with self.subTest(modname=modname): -- -- filename = _testmultiphase.__file__ -- loader = ExtensionFileLoader(modname, filename) -- spec = importlib.util.spec_from_loader(modname, loader) -- module = importlib.util.module_from_spec(spec) -- loader.exec_module(module) -- sys.modules[modname] = module -- -- require_extension(module) -- with self.subTest(f'{modname}: isolated, strict'): -- self.check_incompatible_here(modname, filename, -- isolated=True) -- with self.subTest(f'{modname}: not isolated, strict'): -- self.check_compatible_here(modname, filename, -- strict=True, isolated=False) -- with self.subTest(f'{modname}: not isolated, not strict'): -- self.check_compatible_here( -- modname, filename, strict=False, isolated=False) -+ modname = '_test_shared_gil_only' -+ filename = _testmultiphase.__file__ -+ loader = self.create_extension_loader(modname, filename) -+ spec = importlib.util.spec_from_loader(modname, loader) -+ module = importlib.util.module_from_spec(spec) -+ loader.exec_module(module) -+ sys.modules[modname] = module -+ -+ filename = _testmultiphase.__file__ -+ loader = ExtensionFileLoader(modname, filename) -+ spec = importlib.util.spec_from_loader(modname, loader) -+ module = importlib.util.module_from_spec(spec) -+ loader.exec_module(module) -+ sys.modules[modname] = module -+ -+ require_extension(module) -+ with self.subTest(f'{modname}: isolated, strict'): -+ self.check_incompatible_here(modname, filename, -+ isolated=True) -+ with self.subTest(f'{modname}: not isolated, strict'): -+ self.check_compatible_here(modname, filename, -+ strict=True, isolated=False) -+ with self.subTest(f'{modname}: not isolated, not strict'): -+ self.check_compatible_here( -+ modname, filename, strict=False, isolated=False) - - def test_python_compat(self): - module = 'threading' -@@ -2074,10 +2102,25 @@ - @classmethod - def setUpClass(cls): - spec = importlib.util.find_spec(cls.NAME) -- from importlib.machinery import ExtensionFileLoader -- cls.FILE = spec.origin - cls.LOADER = type(spec.loader) -- assert cls.LOADER is ExtensionFileLoader -+ -+ # Apple extensions must be distributed as frameworks. This requires -+ # a specialist loader, and we need to differentiate between the -+ # spec.origin and the original file location. -+ if is_apple_mobile: -+ assert cls.LOADER is AppleFrameworkLoader -+ -+ cls.ORIGIN = spec.origin -+ with open(spec.origin + ".origin", "r") as f: -+ cls.FILE = os.path.join( -+ os.path.dirname(sys.executable), -+ f.read().strip() -+ ) -+ else: -+ assert cls.LOADER is ExtensionFileLoader -+ -+ cls.ORIGIN = spec.origin -+ cls.FILE = spec.origin - - # Start fresh. - cls.clean_up() -@@ -2093,14 +2136,15 @@ - @classmethod - def clean_up(cls): - name = cls.NAME -- filename = cls.FILE - if name in sys.modules: - if hasattr(sys.modules[name], '_clear_globals'): -- assert sys.modules[name].__file__ == filename -+ assert sys.modules[name].__file__ == cls.FILE, \ -+ f"{sys.modules[name].__file__} != {cls.FILE}" -+ - sys.modules[name]._clear_globals() - del sys.modules[name] - # Clear all internally cached data for the extension. -- _testinternalcapi.clear_extension(name, filename) -+ _testinternalcapi.clear_extension(name, cls.ORIGIN) - - ######################### - # helpers -@@ -2108,7 +2152,7 @@ - def add_module_cleanup(self, name): - def clean_up(): - # Clear all internally cached data for the extension. -- _testinternalcapi.clear_extension(name, self.FILE) -+ _testinternalcapi.clear_extension(name, self.ORIGIN) - self.addCleanup(clean_up) - - def _load_dynamic(self, name, path): -@@ -2131,7 +2175,7 @@ - except AttributeError: - already_loaded = self.already_loaded = {} - assert name not in already_loaded -- mod = self._load_dynamic(name, self.FILE) -+ mod = self._load_dynamic(name, self.ORIGIN) - self.assertNotIn(mod, already_loaded.values()) - already_loaded[name] = mod - return types.SimpleNamespace( -@@ -2143,7 +2187,7 @@ - def re_load(self, name, mod): - assert sys.modules[name] is mod - assert mod.__dict__ == mod.__dict__ -- reloaded = self._load_dynamic(name, self.FILE) -+ reloaded = self._load_dynamic(name, self.ORIGIN) - return types.SimpleNamespace( - name=name, - module=reloaded, -@@ -2163,7 +2207,7 @@ - name = {self.NAME!r} - if name in sys.modules: - sys.modules.pop(name)._clear_globals() -- _testinternalcapi.clear_extension(name, {self.FILE!r}) -+ _testinternalcapi.clear_extension(name, {self.ORIGIN!r}) - ''')) - _interpreters.destroy(interpid) - self.addCleanup(clean_up) -@@ -2180,7 +2224,7 @@ - postcleanup = f''' - {import_} - mod._clear_globals() -- _testinternalcapi.clear_extension(name, {self.FILE!r}) -+ _testinternalcapi.clear_extension(name, {self.ORIGIN!r}) - ''' - - try: -@@ -2218,7 +2262,7 @@ - # mod.__name__ might not match, but the spec will. - self.assertEqual(mod.__spec__.name, loaded.name) - self.assertEqual(mod.__file__, self.FILE) -- self.assertEqual(mod.__spec__.origin, self.FILE) -+ self.assertEqual(mod.__spec__.origin, self.ORIGIN) - if not isolated: - self.assertTrue(issubclass(mod.error, Exception)) - self.assertEqual(mod.int_const, 1969) -@@ -2612,7 +2656,7 @@ - # First, load in the main interpreter but then completely clear it. - loaded_main = self.load(self.NAME) - loaded_main.module._clear_globals() -- _testinternalcapi.clear_extension(self.NAME, self.FILE) -+ _testinternalcapi.clear_extension(self.NAME, self.ORIGIN) - - # At this point: - # * alive in 0 interpreters -diff --git a/Lib/test/test_importlib/extension/test_finder.py b/Lib/test/test_importlib/extension/test_finder.py -index 3de120958fd..cdc8884d668 100644 ---- a/Lib/test/test_importlib/extension/test_finder.py -+++ b/Lib/test/test_importlib/extension/test_finder.py -@@ -1,3 +1,4 @@ -+from test.support import is_apple_mobile - from test.test_importlib import abc, util - - machinery = util.import_importlib('importlib.machinery') -@@ -19,9 +20,27 @@ - ) - - def find_spec(self, fullname): -- importer = self.machinery.FileFinder(util.EXTENSIONS.path, -- (self.machinery.ExtensionFileLoader, -- self.machinery.EXTENSION_SUFFIXES)) -+ if is_apple_mobile: -+ # Apple mobile platforms require a specialist loader that uses -+ # .fwork files as placeholders for the true `.so` files. -+ loaders = [ -+ ( -+ self.machinery.AppleFrameworkLoader, -+ [ -+ ext.replace(".so", ".fwork") -+ for ext in self.machinery.EXTENSION_SUFFIXES -+ ] -+ ) -+ ] -+ else: -+ loaders = [ -+ ( -+ self.machinery.ExtensionFileLoader, -+ self.machinery.EXTENSION_SUFFIXES -+ ) -+ ] -+ -+ importer = self.machinery.FileFinder(util.EXTENSIONS.path, *loaders) - - return importer.find_spec(fullname) - -diff --git a/Lib/test/test_importlib/extension/test_loader.py b/Lib/test/test_importlib/extension/test_loader.py -index 12f9e43d123..7e8c9c8184a 100644 ---- a/Lib/test/test_importlib/extension/test_loader.py -+++ b/Lib/test/test_importlib/extension/test_loader.py -@@ -1,3 +1,4 @@ -+from test.support import is_apple_mobile - from warnings import catch_warnings - from test.test_importlib import abc, util - -@@ -25,8 +26,15 @@ - raise unittest.SkipTest( - f"{util.EXTENSIONS.name} is a builtin module" - ) -- self.loader = self.machinery.ExtensionFileLoader(util.EXTENSIONS.name, -- util.EXTENSIONS.file_path) -+ -+ # Apple extensions must be distributed as frameworks. This requires -+ # a specialist loader. -+ if is_apple_mobile: -+ self.LoaderClass = self.machinery.AppleFrameworkLoader -+ else: -+ self.LoaderClass = self.machinery.ExtensionFileLoader -+ -+ self.loader = self.LoaderClass(util.EXTENSIONS.name, util.EXTENSIONS.file_path) - - def load_module(self, fullname): - with warnings.catch_warnings(): -@@ -34,13 +42,11 @@ - return self.loader.load_module(fullname) - - def test_equality(self): -- other = self.machinery.ExtensionFileLoader(util.EXTENSIONS.name, -- util.EXTENSIONS.file_path) -+ other = self.LoaderClass(util.EXTENSIONS.name, util.EXTENSIONS.file_path) - self.assertEqual(self.loader, other) - - def test_inequality(self): -- other = self.machinery.ExtensionFileLoader('_' + util.EXTENSIONS.name, -- util.EXTENSIONS.file_path) -+ other = self.LoaderClass('_' + util.EXTENSIONS.name, util.EXTENSIONS.file_path) - self.assertNotEqual(self.loader, other) - - def test_load_module_API(self): -@@ -60,8 +66,7 @@ - ('__package__', '')]: - self.assertEqual(getattr(module, attr), value) - self.assertIn(util.EXTENSIONS.name, sys.modules) -- self.assertIsInstance(module.__loader__, -- self.machinery.ExtensionFileLoader) -+ self.assertIsInstance(module.__loader__, self.LoaderClass) ++# Skip tests on WASM platforms, plus iOS/tvOS/watchOS ++if is_apple_mobile or is_emscripten or is_wasi: ++ raise unittest.SkipTest(f"pty tests not required on {sys.platform}") - # No extension module as __init__ available for testing. - test_package = None -@@ -88,7 +93,7 @@ - self.assertFalse(self.loader.is_package(util.EXTENSIONS.name)) - for suffix in self.machinery.EXTENSION_SUFFIXES: - path = os.path.join('some', 'path', 'pkg', '__init__' + suffix) -- loader = self.machinery.ExtensionFileLoader('pkg', path) -+ loader = self.LoaderClass('pkg', path) - self.assertTrue(loader.is_package('pkg')) + import errno + import os +@@ -17,7 +22,6 @@ + import signal + import socket + import io # readline +-import unittest + import warnings + TEST_STRING_1 = b"I wish to buy a fish license.\n" +diff --git a/Lib/test/test_selectors.py b/Lib/test/test_selectors.py +index 31757205ca3..6b88b121580 100644 +--- a/Lib/test/test_selectors.py ++++ b/Lib/test/test_selectors.py +@@ -6,8 +6,7 @@ + import socket + import sys + from test import support +-from test.support import os_helper +-from test.support import socket_helper ++from test.support import is_apple, os_helper, socket_helper + from time import sleep + import unittest + import unittest.mock +@@ -520,7 +519,7 @@ + try: + fds = s.select() + except OSError as e: +- if e.errno == errno.EINVAL and sys.platform == 'darwin': ++ if e.errno == errno.EINVAL and is_apple: + # unexplainable errors on macOS don't need to fail the test + self.skipTest("Invalid argument error calling poll()") + raise +diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py +index b7be547de41..e3a3bbcd422 100644 +--- a/Lib/test/test_shutil.py ++++ b/Lib/test/test_shutil.py +@@ -2286,6 +2286,7 @@ + check_chown(dirname, uid, gid) -@@ -103,6 +108,14 @@ - def setUp(self): - if not self.machinery.EXTENSION_SUFFIXES or not util.EXTENSIONS: - raise unittest.SkipTest("Requires dynamic loading support.") -+ -+ # Apple extensions must be distributed as frameworks. This requires -+ # a specialist loader. -+ if is_apple_mobile: -+ self.LoaderClass = self.machinery.AppleFrameworkLoader -+ else: -+ self.LoaderClass = self.machinery.ExtensionFileLoader -+ - self.name = '_testsinglephase' - if self.name in sys.builtin_module_names: - raise unittest.SkipTest( -@@ -111,8 +124,8 @@ - finder = self.machinery.FileFinder(None) - self.spec = importlib.util.find_spec(self.name) - assert self.spec -- self.loader = self.machinery.ExtensionFileLoader( -- self.name, self.spec.origin) -+ -+ self.loader = self.LoaderClass(self.name, self.spec.origin) - def load_module(self): - with warnings.catch_warnings(): -@@ -122,7 +135,7 @@ - def load_module_by_name(self, fullname): - # Load a module from the test extension by name. - origin = self.spec.origin -- loader = self.machinery.ExtensionFileLoader(fullname, origin) -+ loader = self.LoaderClass(fullname, origin) - spec = importlib.util.spec_from_loader(fullname, loader) - module = importlib.util.module_from_spec(spec) - loader.exec_module(module) -@@ -139,8 +152,7 @@ - with self.assertRaises(AttributeError): - module.__path__ - self.assertIs(module, sys.modules[self.name]) -- self.assertIsInstance(module.__loader__, -- self.machinery.ExtensionFileLoader) -+ self.assertIsInstance(module.__loader__, self.LoaderClass) ++@support.requires_subprocess() + class TestWhich(BaseTest, unittest.TestCase): - # No extension module as __init__ available for testing. - test_package = None -@@ -184,6 +196,14 @@ def setUp(self): - if not self.machinery.EXTENSION_SUFFIXES or not util.EXTENSIONS: - raise unittest.SkipTest("Requires dynamic loading support.") -+ -+ # Apple extensions must be distributed as frameworks. This requires -+ # a specialist loader. -+ if is_apple_mobile: -+ self.LoaderClass = self.machinery.AppleFrameworkLoader -+ else: -+ self.LoaderClass = self.machinery.ExtensionFileLoader -+ - self.name = '_testmultiphase' - if self.name in sys.builtin_module_names: - raise unittest.SkipTest( -@@ -192,8 +212,7 @@ - finder = self.machinery.FileFinder(None) - self.spec = importlib.util.find_spec(self.name) - assert self.spec -- self.loader = self.machinery.ExtensionFileLoader( -- self.name, self.spec.origin) -+ self.loader = self.LoaderClass(self.name, self.spec.origin) +@@ -3381,6 +3382,7 @@ + self.assertGreaterEqual(size.lines, 0) - def load_module(self): - # Load the module from the test extension. -@@ -204,7 +223,7 @@ - def load_module_by_name(self, fullname): - # Load a module from the test extension by name. - origin = self.spec.origin -- loader = self.machinery.ExtensionFileLoader(fullname, origin) -+ loader = self.LoaderClass(fullname, origin) - spec = importlib.util.spec_from_loader(fullname, loader) - module = importlib.util.module_from_spec(spec) - loader.exec_module(module) -@@ -230,8 +249,7 @@ - with self.assertRaises(AttributeError): - module.__path__ - self.assertIs(module, sys.modules[self.name]) -- self.assertIsInstance(module.__loader__, -- self.machinery.ExtensionFileLoader) -+ self.assertIsInstance(module.__loader__, self.LoaderClass) + @unittest.skipUnless(os.isatty(sys.__stdout__.fileno()), "not on tty") ++ @support.requires_subprocess() + @unittest.skipUnless(hasattr(os, 'get_terminal_size'), + 'need os.get_terminal_size()') + def test_stty_match(self): +diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py +index 9a01ad0dd5c..e21f7dd3999 100644 +--- a/Lib/test/test_signal.py ++++ b/Lib/test/test_signal.py +@@ -13,9 +13,10 @@ + import time + import unittest + from test import support +-from test.support import os_helper ++from test.support import ( ++ is_apple, is_apple_mobile, os_helper, threading_helper ++) + from test.support.script_helper import assert_python_ok, spawn_python +-from test.support import threading_helper + try: + import _testcapi + except ImportError: +@@ -834,7 +835,7 @@ + self.assertEqual(self.hndl_called, True) - def test_functionality(self): - # Test basic functionality of stuff defined in an extension module. -diff --git a/Lib/test/test_importlib/test_util.py b/Lib/test/test_importlib/test_util.py -index 553e2087421..e23949aaa22 100644 ---- a/Lib/test/test_importlib/test_util.py -+++ b/Lib/test/test_importlib/test_util.py -@@ -703,13 +703,20 @@ + # Issue 3864, unknown if this affects earlier versions of freebsd also +- @unittest.skipIf(sys.platform in ('netbsd5',), ++ @unittest.skipIf(sys.platform in ('netbsd5',) or is_apple_mobile, + 'itimer not reliable (does not mix well with threading) on some BSDs.') + def test_itimer_virtual(self): + self.itimer = signal.ITIMER_VIRTUAL +@@ -1346,7 +1347,7 @@ + # Python handler + self.assertEqual(len(sigs), N, "Some signals were lost") - @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") - def test_incomplete_multi_phase_init_module(self): -+ # Apple extensions must be distributed as frameworks. This requires -+ # a specialist loader. -+ if support.is_apple_mobile: -+ loader = "AppleFrameworkLoader" -+ else: -+ loader = "ExtensionFileLoader" -+ - prescript = textwrap.dedent(f''' - from importlib.util import spec_from_loader, module_from_spec -- from importlib.machinery import ExtensionFileLoader -+ from importlib.machinery import {loader} +- @unittest.skipIf(sys.platform == "darwin", "crashes due to system bug (FB13453490)") ++ @unittest.skipIf(is_apple, "crashes due to system bug (FB13453490)") + @unittest.skipUnless(hasattr(signal, "SIGUSR1"), + "test needs SIGUSR1") + @threading_helper.requires_working_threading() +diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py +index cc6c2a903fa..cc1d323cd32 100644 +--- a/Lib/test/test_socket.py ++++ b/Lib/test/test_socket.py +@@ -3,6 +3,7 @@ + from test.support import os_helper + from test.support import socket_helper + from test.support import threading_helper ++from test.support import is_apple - name = '_test_shared_gil_only' - filename = {_testmultiphase.__file__!r} -- loader = ExtensionFileLoader(name, filename) -+ loader = {loader}(name, filename) - spec = spec_from_loader(name, loader) + import _thread as thread + import array +@@ -1212,8 +1213,11 @@ + # Find one service that exists, then check all the related interfaces. + # I've ordered this by protocols that have both a tcp and udp + # protocol, at least for modern Linuxes. +- if (sys.platform.startswith(('freebsd', 'netbsd', 'gnukfreebsd')) +- or sys.platform in ('linux', 'darwin')): ++ if ( ++ sys.platform.startswith(('freebsd', 'netbsd', 'gnukfreebsd')) ++ or sys.platform == 'linux' ++ or is_apple ++ ): + # avoid the 'echo' service on this platform, as there is an + # assumption breaking non-standard port/protocol entry + services = ('daytime', 'qotd', 'domain') +@@ -3724,7 +3728,7 @@ + def _testFDPassCMSG_LEN(self): + self.createAndSendFDs(1) - ''') -diff --git a/Lib/test/test_importlib/util.py b/Lib/test/test_importlib/util.py -index a900cc1dddf..89272484009 100644 ---- a/Lib/test/test_importlib/util.py -+++ b/Lib/test/test_importlib/util.py -@@ -8,6 +8,7 @@ - import os.path - from test import support - from test.support import import_helper -+from test.support import is_apple_mobile - from test.support import os_helper - import unittest - import sys -@@ -43,6 +44,11 @@ - global EXTENSIONS - for path in sys.path: - for ext in machinery.EXTENSION_SUFFIXES: -+ # Apple mobile platforms mechanically load .so files, -+ # but the findable files are labelled .fwork -+ if is_apple_mobile: -+ ext = ext.replace(".so", ".fwork") -+ - filename = EXTENSIONS.name + ext - file_path = os.path.join(path, filename) - if os.path.exists(file_path): -diff --git a/Lib/test/test_interpreters.py b/Lib/test/test_interpreters.py -index 0cd9e721b20..ffa58230425 100644 ---- a/Lib/test/test_interpreters.py -+++ b/Lib/test/test_interpreters.py -@@ -752,6 +752,7 @@ +- @unittest.skipIf(sys.platform == "darwin", "skipping, see issue #12958") ++ @unittest.skipIf(is_apple, "skipping, see issue #12958") + @unittest.skipIf(AIX, "skipping, see issue #22397") + @requireAttrs(socket, "CMSG_SPACE") + def testFDPassSeparate(self): +@@ -3735,7 +3739,7 @@ + maxcmsgs=2) - class FinalizationTests(TestBase): + @testFDPassSeparate.client_skip +- @unittest.skipIf(sys.platform == "darwin", "skipping, see issue #12958") ++ @unittest.skipIf(is_apple, "skipping, see issue #12958") + @unittest.skipIf(AIX, "skipping, see issue #22397") + def _testFDPassSeparate(self): + fd0, fd1 = self.newFDs(2) +@@ -3748,7 +3752,7 @@ + array.array("i", [fd1]))]), + len(MSG)) -+ @support.requires_subprocess() - def test_gh_109793(self): - import subprocess - argv = [sys.executable, '-c', '''if True: -diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py -index d85040a3083..9d634acfd3e 100644 ---- a/Lib/test/test_io.py -+++ b/Lib/test/test_io.py -@@ -39,11 +39,9 @@ - from test import support - from test.support.script_helper import ( - assert_python_ok, assert_python_failure, run_python_until_end) --from test.support import import_helper --from test.support import os_helper --from test.support import threading_helper --from test.support import warnings_helper --from test.support import skip_if_sanitizer -+from test.support import ( -+ import_helper, is_apple, os_helper, skip_if_sanitizer, threading_helper, warnings_helper -+) - from test.support.os_helper import FakePath +- @unittest.skipIf(sys.platform == "darwin", "skipping, see issue #12958") ++ @unittest.skipIf(is_apple, "skipping, see issue #12958") + @unittest.skipIf(AIX, "skipping, see issue #22397") + @requireAttrs(socket, "CMSG_SPACE") + def testFDPassSeparateMinSpace(self): +@@ -3762,7 +3766,7 @@ + maxcmsgs=2, ignoreflags=socket.MSG_CTRUNC) - import codecs -@@ -631,10 +629,10 @@ - self.read_ops(f, True) + @testFDPassSeparateMinSpace.client_skip +- @unittest.skipIf(sys.platform == "darwin", "skipping, see issue #12958") ++ @unittest.skipIf(is_apple, "skipping, see issue #12958") + @unittest.skipIf(AIX, "skipping, see issue #22397") + def _testFDPassSeparateMinSpace(self): + fd0, fd1 = self.newFDs(2) +@@ -3786,7 +3790,7 @@ + nbytes = self.sendmsgToServer([msg]) + self.assertEqual(nbytes, len(msg)) - def test_large_file_ops(self): -- # On Windows and Mac OSX this test consumes large resources; It takes -- # a long time to build the >2 GiB file and takes >2 GiB of disk space -- # therefore the resource must be enabled to run this test. -- if sys.platform[:3] == 'win' or sys.platform == 'darwin': -+ # On Windows and Apple platforms this test consumes large resources; It -+ # takes a long time to build the >2 GiB file and takes >2 GiB of disk -+ # space therefore the resource must be enabled to run this test. -+ if sys.platform[:3] == 'win' or is_apple: - support.requires( - 'largefile', - 'test requires %s bytes and a long time to run' % self.LARGE) -diff --git a/Lib/test/test_lib2to3/test_parser.py b/Lib/test/test_lib2to3/test_parser.py -index 2c798b181fd..e12ed1e9389 100644 ---- a/Lib/test/test_lib2to3/test_parser.py -+++ b/Lib/test/test_lib2to3/test_parser.py -@@ -62,9 +62,7 @@ - shutil.rmtree(tmpdir) +- @unittest.skipIf(sys.platform == "darwin", "see issue #24725") ++ @unittest.skipIf(is_apple, "skipping, see issue #12958") + def testFDPassEmpty(self): + # Try to pass an empty FD array. Can receive either no array + # or an empty array. +diff --git a/Lib/test/test_socketserver.py b/Lib/test/test_socketserver.py +index 0f62f9eb200..2ca356606b2 100644 +--- a/Lib/test/test_socketserver.py ++++ b/Lib/test/test_socketserver.py +@@ -218,12 +218,16 @@ + self.dgram_examine) - @unittest.skipIf(sys.executable is None, 'sys.executable required') -- @unittest.skipIf( -- sys.platform in {'emscripten', 'wasi'}, 'requires working subprocess' -- ) -+ @test.support.requires_subprocess() - def test_load_grammar_from_subprocess(self): - tmpdir = tempfile.mkdtemp() - tmpsubdir = os.path.join(tmpdir, 'subdir') -diff --git a/Lib/test/test_marshal.py b/Lib/test/test_marshal.py -index 3d9d6d5d0ac..9c759170450 100644 ---- a/Lib/test/test_marshal.py -+++ b/Lib/test/test_marshal.py -@@ -1,5 +1,5 @@ - from test import support --from test.support import os_helper, requires_debug_ranges -+from test.support import is_apple_mobile, os_helper, requires_debug_ranges - from test.support.script_helper import assert_python_ok - import array - import io -@@ -260,7 +260,7 @@ - #if os.name == 'nt' and support.Py_DEBUG: - if os.name == 'nt': - MAX_MARSHAL_STACK_DEPTH = 1000 -- elif sys.platform == 'wasi': -+ elif sys.platform == 'wasi' or is_apple_mobile: - MAX_MARSHAL_STACK_DEPTH = 1500 - else: - MAX_MARSHAL_STACK_DEPTH = 2000 -diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py -index 1867e8c957f..f75e40940e4 100644 ---- a/Lib/test/test_mmap.py -+++ b/Lib/test/test_mmap.py -@@ -1,5 +1,5 @@ - from test.support import ( -- requires, _2G, _4G, gc_collect, cpython_only, is_emscripten -+ requires, _2G, _4G, gc_collect, cpython_only, is_emscripten, is_apple, - ) - from test.support.import_helper import import_module - from test.support.os_helper import TESTFN, unlink -@@ -1009,7 +1009,7 @@ - unlink(TESTFN) + @requires_unix_sockets ++ @unittest.skipIf(test.support.is_apple_mobile and test.support.on_github_actions, ++ "gh-140702: Test fails regularly on iOS simulator on GitHub Actions") + def test_UnixDatagramServer(self): + self.run_server(socketserver.UnixDatagramServer, + socketserver.DatagramRequestHandler, + self.dgram_examine) - def _make_test_file(self, num_zeroes, tail): -- if sys.platform[:3] == 'win' or sys.platform == 'darwin': -+ if sys.platform[:3] == 'win' or is_apple: - requires('largefile', - 'test requires %s bytes and a long time to run' % str(0x180000000)) - f = open(TESTFN, 'w+b') -diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py -index 1d6d92e0235..90afbf1d14a 100644 ---- a/Lib/test/test_os.py -+++ b/Lib/test/test_os.py -@@ -2372,6 +2372,7 @@ - support.is_emscripten or support.is_wasi, - "musl libc issue on Emscripten/WASI, bpo-46390" - ) -+ @unittest.skipIf(support.is_apple_mobile, "gh-118201: Test is flaky on iOS") - def test_fpathconf(self): - self.check(os.pathconf, "PC_NAME_MAX") - self.check(os.fpathconf, "PC_NAME_MAX") -@@ -3946,6 +3947,7 @@ - self.assertGreaterEqual(size.columns, 0) - self.assertGreaterEqual(size.lines, 0) + @requires_unix_sockets ++ @unittest.skipIf(test.support.is_apple_mobile and test.support.on_github_actions, ++ "gh-140702: Test fails regularly on iOS simulator on GitHub Actions") + def test_ThreadingUnixDatagramServer(self): + self.run_server(socketserver.ThreadingUnixDatagramServer, + socketserver.DatagramRequestHandler, +diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py +index 9d3856a226d..76999792a11 100644 +--- a/Lib/test/test_sqlite3/test_dbapi.py ++++ b/Lib/test/test_sqlite3/test_dbapi.py +@@ -32,7 +32,7 @@ -+ @support.requires_subprocess() - def test_stty_match(self): - """Check if stty returns the same results + from test.support import ( + SHORT_TIMEOUT, check_disallow_instantiation, requires_subprocess, +- is_emscripten, is_wasi ++ is_apple, is_emscripten, is_wasi + ) + from test.support import threading_helper + from _testcapi import INT_MAX, ULLONG_MAX +@@ -679,7 +679,7 @@ + cx.execute(self._sql) -diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py -index 4a16ff6939c..b29731fbb42 100644 ---- a/Lib/test/test_platform.py -+++ b/Lib/test/test_platform.py -@@ -10,6 +10,14 @@ - from test import support - from test.support import os_helper + @unittest.skipIf(sys.platform == "win32", "skipped on Windows") +- @unittest.skipIf(sys.platform == "darwin", "skipped on macOS") ++ @unittest.skipIf(is_apple, "skipped on Apple platforms") + @unittest.skipIf(is_emscripten or is_wasi, "not supported on Emscripten/WASI") + @unittest.skipUnless(TESTFN_UNDECODABLE, "only works if there are undecodable paths") + def test_open_with_undecodable_path(self): +@@ -725,7 +725,7 @@ + cx.execute(self._sql) -+try: -+ # Some of the iOS tests need ctypes to operate. -+ # Confirm that the ctypes module is available -+ # is available. -+ import _ctypes -+except ImportError: -+ _ctypes = None -+ - FEDORA_OS_RELEASE = """\ - NAME=Fedora - VERSION="32 (Thirty Two)" -@@ -219,6 +227,30 @@ - self.assertEqual(res[-1], res.processor) - self.assertEqual(len(res), 6) + @unittest.skipIf(sys.platform == "win32", "skipped on Windows") +- @unittest.skipIf(sys.platform == "darwin", "skipped on macOS") ++ @unittest.skipIf(is_apple, "skipped on Apple platforms") + @unittest.skipIf(is_emscripten or is_wasi, "not supported on Emscripten/WASI") + @unittest.skipUnless(TESTFN_UNDECODABLE, "only works if there are undecodable paths") + def test_open_undecodable_uri(self): +diff --git a/Lib/test/test_stat.py b/Lib/test/test_stat.py +index c77fec3d39d..ca55d429aec 100644 +--- a/Lib/test/test_stat.py ++++ b/Lib/test/test_stat.py +@@ -2,8 +2,7 @@ + import os + import socket + import sys +-from test.support import os_helper +-from test.support import socket_helper ++from test.support import is_apple, os_helper, socket_helper + from test.support.import_helper import import_fresh_module + from test.support.os_helper import TESTFN -+ if os.name == "posix": -+ uname = os.uname() -+ self.assertEqual(res.node, uname.nodename) -+ self.assertEqual(res.version, uname.version) -+ self.assertEqual(res.machine, uname.machine) -+ -+ if sys.platform == "android": -+ self.assertEqual(res.system, "Android") -+ self.assertEqual(res.release, platform.android_ver().release) -+ elif sys.platform == "ios": -+ # Platform module needs ctypes for full operation. If ctypes -+ # isn't available, there's no ObjC module, and dummy values are -+ # returned. -+ if _ctypes: -+ self.assertIn(res.system, {"iOS", "iPadOS"}) -+ self.assertEqual(res.release, platform.ios_ver().release) -+ else: -+ self.assertEqual(res.system, "") -+ self.assertEqual(res.release, "") -+ else: -+ self.assertEqual(res.system, uname.sysname) -+ self.assertEqual(res.release, uname.release) -+ -+ - @unittest.skipUnless(sys.platform.startswith('win'), "windows only test") - def test_uname_win32_without_wmi(self): - def raises_oserror(*a): -@@ -404,6 +436,56 @@ - # parent - support.wait_process(pid, exitcode=0) +diff --git a/Lib/test/test_sys_settrace.py b/Lib/test/test_sys_settrace.py +index 35985b34a42..fb4bc7fce9a 100644 +--- a/Lib/test/test_sys_settrace.py ++++ b/Lib/test/test_sys_settrace.py +@@ -7,7 +7,7 @@ + import gc + from functools import wraps + import asyncio +-from test.support import import_helper ++from test.support import import_helper, requires_subprocess + import contextlib + import warnings -+ def test_ios_ver(self): -+ result = platform.ios_ver() -+ -+ # ios_ver is only fully available on iOS where ctypes is available. -+ if sys.platform == "ios" and _ctypes: -+ system, release, model, is_simulator = result -+ # Result is a namedtuple -+ self.assertEqual(result.system, system) -+ self.assertEqual(result.release, release) -+ self.assertEqual(result.model, model) -+ self.assertEqual(result.is_simulator, is_simulator) -+ -+ # We can't assert specific values without reproducing the logic of -+ # ios_ver(), so we check that the values are broadly what we expect. -+ -+ # System is either iOS or iPadOS, depending on the test device -+ self.assertIn(system, {"iOS", "iPadOS"}) -+ -+ # Release is a numeric version specifier with at least 2 parts -+ parts = release.split(".") -+ self.assertGreaterEqual(len(parts), 2) -+ self.assertTrue(all(part.isdigit() for part in parts)) -+ -+ # If this is a simulator, we get a high level device descriptor -+ # with no identifying model number. If this is a physical device, -+ # we get a model descriptor like "iPhone13,1" -+ if is_simulator: -+ self.assertIn(model, {"iPhone", "iPad"}) -+ else: -+ self.assertTrue( -+ (model.startswith("iPhone") or model.startswith("iPad")) -+ and "," in model -+ ) -+ -+ self.assertEqual(type(is_simulator), bool) -+ else: -+ # On non-iOS platforms, calling ios_ver doesn't fail; you get -+ # default values -+ self.assertEqual(result.system, "") -+ self.assertEqual(result.release, "") -+ self.assertEqual(result.model, "") -+ self.assertFalse(result.is_simulator) -+ -+ # Check the fallback values can be overridden by arguments -+ override = platform.ios_ver("Foo", "Bar", "Whiz", True) -+ self.assertEqual(override.system, "Foo") -+ self.assertEqual(override.release, "Bar") -+ self.assertEqual(override.model, "Whiz") -+ self.assertTrue(override.is_simulator) -+ - @unittest.skipIf(support.is_emscripten, "Does not apply to Emscripten") - def test_libc_ver(self): - # check that libc_ver(executable) doesn't raise an exception -@@ -499,7 +581,8 @@ - 'root:xnu-4570.71.2~1/RELEASE_X86_64'), - 'x86_64', 'i386') - arch = ('64bit', '') -- with mock.patch.object(platform, 'uname', return_value=uname), \ -+ with mock.patch.object(sys, "platform", "darwin"), \ -+ mock.patch.object(platform, 'uname', return_value=uname), \ - mock.patch.object(platform, 'architecture', return_value=arch): - for mac_ver, expected_terse, expected in [ - # darwin: mac_ver() returns empty strings -diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py -index e225b8919d1..5d8920ddd60 100644 ---- a/Lib/test/test_posix.py -+++ b/Lib/test/test_posix.py -@@ -1,7 +1,7 @@ - "Test posix functions" +diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py +index 67647e1b787..4e4ccd12f69 100644 +--- a/Lib/test/test_sysconfig.py ++++ b/Lib/test/test_sysconfig.py +@@ -8,7 +8,11 @@ + from copy import copy + + from test.support import ( +- captured_stdout, PythonSymlink, requires_subprocess, is_wasi ++ captured_stdout, ++ is_apple_mobile, ++ is_wasi, ++ PythonSymlink, ++ requires_subprocess, + ) + from test.support.import_helper import import_module + from test.support.os_helper import (TESTFN, unlink, skip_unless_symlink, +@@ -348,6 +352,8 @@ + # XXX more platforms to tests here + + @unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds") ++ @unittest.skipIf(is_apple_mobile, ++ f"{sys.platform} doesn't distribute header files in the runtime environment") + def test_get_config_h_filename(self): + config_h = sysconfig.get_config_h_filename() + self.assertTrue(os.path.isfile(config_h), config_h) +@@ -457,6 +463,8 @@ + self.assertEqual(my_platform, test_platform) + @unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds") ++ @unittest.skipIf(is_apple_mobile, ++ f"{sys.platform} doesn't include config folder at runtime") + def test_srcdir(self): + # See Issues #15322, #15364. + srcdir = sysconfig.get_config_var('srcdir') +@@ -591,6 +599,8 @@ + @unittest.skipIf(sys.platform.startswith('win'), + 'Test is not Windows compatible') + @unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds") ++ @unittest.skipIf(is_apple_mobile, ++ f"{sys.platform} doesn't include config folder at runtime") + def test_get_makefile_filename(self): + makefile = sysconfig.get_makefile_filename() + self.assertTrue(os.path.isfile(makefile), makefile) +diff --git a/Lib/test/test_unicode_file_functions.py b/Lib/test/test_unicode_file_functions.py +index 47619c8807b..25c16e3a0b7 100644 +--- a/Lib/test/test_unicode_file_functions.py ++++ b/Lib/test/test_unicode_file_functions.py +@@ -5,7 +5,7 @@ + import unittest + import warnings + from unicodedata import normalize +-from test.support import os_helper ++from test.support import is_apple, os_helper from test import support --from test.support import import_helper -+from test.support import is_apple - from test.support import os_helper - from test.support import warnings_helper - from test.support.script_helper import assert_python_ok -@@ -568,6 +568,7 @@ - @unittest.skipUnless(hasattr(posix, 'confstr'), - 'test needs posix.confstr()') -+ @unittest.skipIf(support.is_apple_mobile, "gh-118201: Test is flaky on iOS") - def test_confstr(self): - self.assertRaises(ValueError, posix.confstr, "CS_garbage") - self.assertEqual(len(posix.confstr("CS_PATH")) > 0, True) -@@ -796,9 +797,10 @@ - check_stat(uid, gid) - self.assertRaises(OSError, chown_func, first_param, 0, -1) - check_stat(uid, gid) -- if 0 not in os.getgroups(): -- self.assertRaises(OSError, chown_func, first_param, -1, 0) -- check_stat(uid, gid) -+ if hasattr(os, 'getgroups'): -+ if 0 not in os.getgroups(): -+ self.assertRaises(OSError, chown_func, first_param, -1, 0) -+ check_stat(uid, gid) - # test illegal types - for t in str, float: - self.assertRaises(TypeError, chown_func, first_param, t(uid), gid) -@@ -1264,8 +1266,8 @@ - self.assertIsInstance(lo, int) - self.assertIsInstance(hi, int) - self.assertGreaterEqual(hi, lo) -- # OSX evidently just returns 15 without checking the argument. -- if sys.platform != "darwin": -+ # Apple plaforms return 15 without checking the argument. -+ if not is_apple: - self.assertRaises(OSError, posix.sched_get_priority_min, -23) - self.assertRaises(OSError, posix.sched_get_priority_max, -23) -@@ -2058,11 +2060,13 @@ +@@ -23,13 +23,13 @@ + '10_\u1fee\u1ffd', + ] +-# Mac OS X decomposes Unicode names, using Normal Form D. ++# Apple platforms decompose Unicode names, using Normal Form D. + # http://developer.apple.com/mac/library/qa/qa2001/qa1173.html + # "However, most volume formats do not follow the exact specification for + # these normal forms. For example, HFS Plus uses a variant of Normal Form D + # in which U+2000 through U+2FFF, U+F900 through U+FAFF, and U+2F800 through + # U+2FAFF are not decomposed." +-if sys.platform != 'darwin': ++if not is_apple: + filenames.extend([ + # Specific code points: NFC(fn), NFD(fn), NFKC(fn) and NFKD(fn) all different + '11_\u0385\u03d3\u03d4', +@@ -119,11 +119,11 @@ + os.stat(name) + self._apply_failure(os.listdir, name, self._listdir_failure) - @unittest.skipUnless(hasattr(os, 'posix_spawn'), "test needs os.posix_spawn") -+@support.requires_subprocess() - class TestPosixSpawn(unittest.TestCase, _PosixSpawnMixin): - spawn_func = getattr(posix, 'posix_spawn', None) +- # Skip the test on darwin, because darwin does normalize the filename to ++ # Skip the test on Apple platforms, because they don't normalize the filename to + # NFD (a variant of Unicode NFD form). Normalize the filename to NFC, NFKC, + # NFKD in Python is useless, because darwin will normalize it later and so + # open(), os.stat(), etc. don't raise any exception. +- @unittest.skipIf(sys.platform == 'darwin', 'irrelevant test on Mac OS X') ++ @unittest.skipIf(is_apple, 'irrelevant test on Apple platforms') + @unittest.skipIf( + support.is_emscripten or support.is_wasi, + "test fails on Emscripten/WASI when host platform is macOS." +@@ -142,10 +142,10 @@ + self._apply_failure(os.remove, name) + self._apply_failure(os.listdir, name) +- # Skip the test on darwin, because darwin uses a normalization different ++ # Skip the test on Apple platforms, because they use a normalization different + # than Python NFD normalization: filenames are different even if we use + # Python NFD normalization. +- @unittest.skipIf(sys.platform == 'darwin', 'irrelevant test on Mac OS X') ++ @unittest.skipIf(is_apple, 'irrelevant test on Apple platforms') + def test_listdir(self): + sf0 = set(self.files) + with warnings.catch_warnings(): +diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py +index c62f1825d1f..5ace1c7cea5 100644 +--- a/Lib/test/test_urllib2.py ++++ b/Lib/test/test_urllib2.py +@@ -1,6 +1,7 @@ + import unittest + from test import support + from test.support import os_helper ++from test.support import requires_subprocess + from test.support import warnings_helper + from test import test_urllib + from unittest import mock +@@ -1040,6 +1041,7 @@ - @unittest.skipUnless(hasattr(os, 'posix_spawnp'), "test needs os.posix_spawnp") -+@support.requires_subprocess() - class TestPosixSpawnP(unittest.TestCase, _PosixSpawnMixin): - spawn_func = getattr(posix, 'posix_spawnp', None) + file_obj.close() -diff --git a/Lib/test/test_pty.py b/Lib/test/test_pty.py -index 51e3a46d0df..3f2bac0155f 100644 ---- a/Lib/test/test_pty.py -+++ b/Lib/test/test_pty.py -@@ -1,12 +1,17 @@ --from test.support import verbose, reap_children --from test.support.os_helper import TESTFN, unlink -+import sys -+import unittest -+from test.support import ( -+ is_apple_mobile, is_emscripten, is_wasi, reap_children, verbose -+) - from test.support.import_helper import import_module -+from test.support.os_helper import TESTFN, unlink ++ @requires_subprocess() + def test_http_body_pipe(self): + # A file reading from a pipe. + # A pipe cannot be seek'ed. There is no way to determine the +diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py +index 43c67ac751d..ba6c9cf53ad 100644 +--- a/Lib/test/test_venv.py ++++ b/Lib/test/test_venv.py +@@ -20,8 +20,8 @@ + import shlex + from test.support import (captured_stdout, captured_stderr, + skip_if_broken_multiprocessing_synchronize, verbose, +- requires_subprocess, is_emscripten, is_wasi, +- requires_venv_with_pip, TEST_HOME_DIR, ++ requires_subprocess, is_apple_mobile, is_emscripten, ++ is_wasi, requires_venv_with_pip, TEST_HOME_DIR, + requires_resource, copy_python_src_ignore) + from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree, + TESTFN, FakePath) +@@ -42,8 +42,10 @@ + or sys._base_executable != sys.executable, + 'cannot run venv.create from within a venv on this platform') --# Skip these tests if termios or fcntl are not available -+# Skip these tests if termios is not available - import_module('termios') --# fcntl is a proxy for not being one of the wasm32 platforms even though we --# don't use this module... a proper check for what crashes those is needed. --import_module("fcntl") -+ +-if is_emscripten or is_wasi: +- raise unittest.SkipTest("venv is not available on Emscripten/WASI.") +# Skip tests on WASM platforms, plus iOS/tvOS/watchOS +if is_apple_mobile or is_emscripten or is_wasi: -+ raise unittest.SkipTest(f"pty tests not required on {sys.platform}") - - import errno - import os -@@ -17,7 +22,6 @@ - import signal - import socket - import io # readline --import unittest - import warnings ++ raise unittest.SkipTest(f"venv tests not required on {sys.platform}") ++ - TEST_STRING_1 = b"I wish to buy a fish license.\n" -diff --git a/Lib/test/test_selectors.py b/Lib/test/test_selectors.py -index 31757205ca3..6b88b121580 100644 ---- a/Lib/test/test_selectors.py -+++ b/Lib/test/test_selectors.py -@@ -6,8 +6,7 @@ - import socket - import sys + @requires_subprocess() + def check_output(cmd, encoding=None): +diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py +index ae161786002..381b54c7c24 100644 +--- a/Lib/test/test_webbrowser.py ++++ b/Lib/test/test_webbrowser.py +@@ -6,11 +6,14 @@ + import subprocess + from unittest import mock from test import support --from test.support import os_helper --from test.support import socket_helper -+from test.support import is_apple, os_helper, socket_helper - from time import sleep - import unittest - import unittest.mock -@@ -520,7 +519,7 @@ - try: - fds = s.select() - except OSError as e: -- if e.errno == errno.EINVAL and sys.platform == 'darwin': -+ if e.errno == errno.EINVAL and is_apple: - # unexplainable errors on macOS don't need to fail the test - self.skipTest("Invalid argument error calling poll()") - raise -diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py -index b7be547de41..e3a3bbcd422 100644 ---- a/Lib/test/test_shutil.py -+++ b/Lib/test/test_shutil.py -@@ -2286,6 +2286,7 @@ - check_chown(dirname, uid, gid) ++from test.support import is_apple_mobile + from test.support import import_helper + from test.support import os_helper ++from test.support import requires_subprocess ++from test.support import threading_helper +-if not support.has_subprocess_support: +- raise unittest.SkipTest("test webserver requires subprocess") ++# The webbrowser module uses threading locks ++threading_helper.requires_working_threading(module=True) -+@support.requires_subprocess() - class TestWhich(BaseTest, unittest.TestCase): + URL = 'https://www.example.com' + CMD_NAME = 'test' +@@ -25,6 +28,7 @@ + return 0 - def setUp(self): -@@ -3381,6 +3382,7 @@ - self.assertGreaterEqual(size.lines, 0) - @unittest.skipUnless(os.isatty(sys.__stdout__.fileno()), "not on tty") -+ @support.requires_subprocess() - @unittest.skipUnless(hasattr(os, 'get_terminal_size'), - 'need os.get_terminal_size()') - def test_stty_match(self): -diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py -index 9a01ad0dd5c..e21f7dd3999 100644 ---- a/Lib/test/test_signal.py -+++ b/Lib/test/test_signal.py -@@ -13,9 +13,10 @@ - import time - import unittest - from test import support --from test.support import os_helper -+from test.support import ( -+ is_apple, is_apple_mobile, os_helper, threading_helper -+) - from test.support.script_helper import assert_python_ok, spawn_python --from test.support import threading_helper - try: - import _testcapi - except ImportError: -@@ -834,7 +835,7 @@ - self.assertEqual(self.hndl_called, True) ++@requires_subprocess() + class CommandTestMixin: - # Issue 3864, unknown if this affects earlier versions of freebsd also -- @unittest.skipIf(sys.platform in ('netbsd5',), -+ @unittest.skipIf(sys.platform in ('netbsd5',) or is_apple_mobile, - 'itimer not reliable (does not mix well with threading) on some BSDs.') - def test_itimer_virtual(self): - self.itimer = signal.ITIMER_VIRTUAL -@@ -1346,7 +1347,7 @@ - # Python handler - self.assertEqual(len(sigs), N, "Some signals were lost") + def _test(self, meth, *, args=[URL], kw={}, options, arguments): +@@ -302,6 +306,73 @@ + self.browser.open(f"--key=val {URL}") -- @unittest.skipIf(sys.platform == "darwin", "crashes due to system bug (FB13453490)") -+ @unittest.skipIf(is_apple, "crashes due to system bug (FB13453490)") - @unittest.skipUnless(hasattr(signal, "SIGUSR1"), - "test needs SIGUSR1") - @threading_helper.requires_working_threading() -diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py -index cc6c2a903fa..cc1d323cd32 100644 ---- a/Lib/test/test_socket.py -+++ b/Lib/test/test_socket.py -@@ -3,6 +3,7 @@ - from test.support import os_helper - from test.support import socket_helper - from test.support import threading_helper -+from test.support import is_apple - import _thread as thread - import array -@@ -1212,8 +1213,11 @@ - # Find one service that exists, then check all the related interfaces. - # I've ordered this by protocols that have both a tcp and udp - # protocol, at least for modern Linuxes. -- if (sys.platform.startswith(('freebsd', 'netbsd', 'gnukfreebsd')) -- or sys.platform in ('linux', 'darwin')): -+ if ( -+ sys.platform.startswith(('freebsd', 'netbsd', 'gnukfreebsd')) -+ or sys.platform == 'linux' -+ or is_apple -+ ): - # avoid the 'echo' service on this platform, as there is an - # assumption breaking non-standard port/protocol entry - services = ('daytime', 'qotd', 'domain') -@@ -3724,7 +3728,7 @@ - def _testFDPassCMSG_LEN(self): - self.createAndSendFDs(1) ++@unittest.skipUnless(sys.platform == "ios", "Test only applicable to iOS") ++class IOSBrowserTest(unittest.TestCase): ++ def _obj_ref(self, *args): ++ # Construct a string representation of the arguments that can be used ++ # as a proxy for object instance references ++ return "|".join(str(a) for a in args) ++ ++ @unittest.skipIf(getattr(webbrowser, "objc", None) is None, ++ "iOS Webbrowser tests require ctypes") ++ def setUp(self): ++ # Intercept the the objc library. Wrap the calls to get the ++ # references to classes and selectors to return strings, and ++ # wrap msgSend to return stringified object references ++ self.orig_objc = webbrowser.objc ++ ++ webbrowser.objc = mock.Mock() ++ webbrowser.objc.objc_getClass = lambda cls: f"C#{cls.decode()}" ++ webbrowser.objc.sel_registerName = lambda sel: f"S#{sel.decode()}" ++ webbrowser.objc.objc_msgSend.side_effect = self._obj_ref ++ ++ def tearDown(self): ++ webbrowser.objc = self.orig_objc ++ ++ def _test(self, meth, **kwargs): ++ # The browser always gets focus, there's no concept of separate browser ++ # windows, and there's no API-level control over creating a new tab. ++ # Therefore, all calls to webbrowser are effectively the same. ++ getattr(webbrowser, meth)(URL, **kwargs) ++ ++ # The ObjC String version of the URL is created with UTF-8 encoding ++ url_string_args = [ ++ "C#NSString", ++ "S#stringWithCString:encoding:", ++ b'https://www.example.com', ++ 4, ++ ] ++ # The NSURL version of the URL is created from that string ++ url_obj_args = [ ++ "C#NSURL", ++ "S#URLWithString:", ++ self._obj_ref(*url_string_args), ++ ] ++ # The openURL call is invoked on the shared application ++ shared_app_args = ["C#UIApplication", "S#sharedApplication"] ++ ++ # Verify that the last call is the one that opens the URL. ++ webbrowser.objc.objc_msgSend.assert_called_with( ++ self._obj_ref(*shared_app_args), ++ "S#openURL:options:completionHandler:", ++ self._obj_ref(*url_obj_args), ++ None, ++ None ++ ) ++ ++ def test_open(self): ++ self._test('open') ++ ++ def test_open_with_autoraise_false(self): ++ self._test('open', autoraise=False) ++ ++ def test_open_new(self): ++ self._test('open_new') ++ ++ def test_open_new_tab(self): ++ self._test('open_new_tab') ++ ++ + class BrowserRegistrationTest(unittest.TestCase): + + def setUp(self): +@@ -385,6 +456,10 @@ + webbrowser.register(name, None, webbrowser.GenericBrowser(name)) + webbrowser.get(sys.executable) + ++ @unittest.skipIf( ++ is_apple_mobile, ++ "Apple mobile doesn't allow modifying browser with environment" ++ ) + def test_environment(self): + webbrowser = import_helper.import_fresh_module('webbrowser') + try: +@@ -396,6 +471,10 @@ + webbrowser = import_helper.import_fresh_module('webbrowser') + webbrowser.get() + ++ @unittest.skipIf( ++ is_apple_mobile, ++ "Apple mobile doesn't allow modifying browser with environment" ++ ) + def test_environment_preferred(self): + webbrowser = import_helper.import_fresh_module('webbrowser') + try: +diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py +index 79e00f9f334..b37aedde348 100755 +--- a/Lib/webbrowser.py ++++ b/Lib/webbrowser.py +@@ -487,6 +487,9 @@ + # OS X can use below Unix support (but we prefer using the OS X + # specific stuff) -- @unittest.skipIf(sys.platform == "darwin", "skipping, see issue #12958") -+ @unittest.skipIf(is_apple, "skipping, see issue #12958") - @unittest.skipIf(AIX, "skipping, see issue #22397") - @requireAttrs(socket, "CMSG_SPACE") - def testFDPassSeparate(self): -@@ -3735,7 +3739,7 @@ - maxcmsgs=2) ++ if sys.platform == "ios": ++ register("iosbrowser", None, IOSBrowser(), preferred=True) ++ + if sys.platform == "serenityos": + # SerenityOS webbrowser, simply called "Browser". + register("Browser", None, BackgroundBrowser("Browser")) +@@ -669,6 +672,70 @@ + rc = osapipe.close() + return not rc - @testFDPassSeparate.client_skip -- @unittest.skipIf(sys.platform == "darwin", "skipping, see issue #12958") -+ @unittest.skipIf(is_apple, "skipping, see issue #12958") - @unittest.skipIf(AIX, "skipping, see issue #22397") - def _testFDPassSeparate(self): - fd0, fd1 = self.newFDs(2) -@@ -3748,7 +3752,7 @@ - array.array("i", [fd1]))]), - len(MSG)) ++# ++# Platform support for iOS ++# ++if sys.platform == "ios": ++ from _ios_support import objc ++ if objc: ++ # If objc exists, we know ctypes is also importable. ++ from ctypes import c_void_p, c_char_p, c_ulong ++ ++ class IOSBrowser(BaseBrowser): ++ def open(self, url, new=0, autoraise=True): ++ sys.audit("webbrowser.open", url) ++ # If ctypes isn't available, we can't open a browser ++ if objc is None: ++ return False ++ ++ # All the messages in this call return object references. ++ objc.objc_msgSend.restype = c_void_p ++ ++ # This is the equivalent of: ++ # NSString url_string = ++ # [NSString stringWithCString:url.encode("utf-8") ++ # encoding:NSUTF8StringEncoding]; ++ NSString = objc.objc_getClass(b"NSString") ++ constructor = objc.sel_registerName(b"stringWithCString:encoding:") ++ objc.objc_msgSend.argtypes = [c_void_p, c_void_p, c_char_p, c_ulong] ++ url_string = objc.objc_msgSend( ++ NSString, ++ constructor, ++ url.encode("utf-8"), ++ 4, # NSUTF8StringEncoding = 4 ++ ) ++ ++ # Create an NSURL object representing the URL ++ # This is the equivalent of: ++ # NSURL *nsurl = [NSURL URLWithString:url]; ++ NSURL = objc.objc_getClass(b"NSURL") ++ urlWithString_ = objc.sel_registerName(b"URLWithString:") ++ objc.objc_msgSend.argtypes = [c_void_p, c_void_p, c_void_p] ++ ns_url = objc.objc_msgSend(NSURL, urlWithString_, url_string) ++ ++ # Get the shared UIApplication instance ++ # This code is the equivalent of: ++ # UIApplication shared_app = [UIApplication sharedApplication] ++ UIApplication = objc.objc_getClass(b"UIApplication") ++ sharedApplication = objc.sel_registerName(b"sharedApplication") ++ objc.objc_msgSend.argtypes = [c_void_p, c_void_p] ++ shared_app = objc.objc_msgSend(UIApplication, sharedApplication) ++ ++ # Open the URL on the shared application ++ # This code is the equivalent of: ++ # [shared_app openURL:ns_url ++ # options:NIL ++ # completionHandler:NIL]; ++ openURL_ = objc.sel_registerName(b"openURL:options:completionHandler:") ++ objc.objc_msgSend.argtypes = [ ++ c_void_p, c_void_p, c_void_p, c_void_p, c_void_p ++ ] ++ # Method returns void ++ objc.objc_msgSend.restype = None ++ objc.objc_msgSend(shared_app, openURL_, ns_url, None, None) ++ ++ return True ++ -- @unittest.skipIf(sys.platform == "darwin", "skipping, see issue #12958") -+ @unittest.skipIf(is_apple, "skipping, see issue #12958") - @unittest.skipIf(AIX, "skipping, see issue #22397") - @requireAttrs(socket, "CMSG_SPACE") - def testFDPassSeparateMinSpace(self): -@@ -3762,7 +3766,7 @@ - maxcmsgs=2, ignoreflags=socket.MSG_CTRUNC) + def main(): + import getopt +diff --git a/Mac/BuildScript/resources/install_certificates.command b/Mac/BuildScript/resources/install_certificates.command +index b10e18e11a9..ce430a4cc8d 100755 +--- a/Mac/BuildScript/resources/install_certificates.command ++++ b/Mac/BuildScript/resources/install_certificates.command +@@ -1,4 +1,8 @@ + #!/bin/sh ++# Installing into the global environment is the point; ensure that the user ++# hasn't configured pip to always require a virtual environment. ++unset PIP_REQUIRE_VENV ++unset PIP_REQUIRE_VIRTUALENV + + /Library/Frameworks/Python.framework/Versions/@PYVER@/bin/python@PYVER@ << "EOF" - @testFDPassSeparateMinSpace.client_skip -- @unittest.skipIf(sys.platform == "darwin", "skipping, see issue #12958") -+ @unittest.skipIf(is_apple, "skipping, see issue #12958") - @unittest.skipIf(AIX, "skipping, see issue #22397") - def _testFDPassSeparateMinSpace(self): - fd0, fd1 = self.newFDs(2) -@@ -3786,7 +3790,7 @@ - nbytes = self.sendmsgToServer([msg]) - self.assertEqual(nbytes, len(msg)) +diff --git a/Mac/Resources/app-store-compliance.patch b/Mac/Resources/app-store-compliance.patch +new file mode 100644 +index 00000000000..f4b7decc01c +--- /dev/null ++++ b/Mac/Resources/app-store-compliance.patch +@@ -0,0 +1,29 @@ ++diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py ++index d6c83a75c1c..19ed4e01091 100644 ++--- a/Lib/test/test_urlparse.py +++++ b/Lib/test/test_urlparse.py ++@@ -237,11 +237,6 @@ def test_roundtrips(self): ++ '','',''), ++ ('git+ssh', 'git@github.com','/user/project.git', ++ '', '')), ++- ('itms-services://?action=download-manifest&url=https://example.com/app', ++- ('itms-services', '', '', '', ++- 'action=download-manifest&url=https://example.com/app', ''), ++- ('itms-services', '', '', ++- 'action=download-manifest&url=https://example.com/app', '')), ++ ('+scheme:path/to/file', ++ ('', '', '+scheme:path/to/file', '', '', ''), ++ ('', '', '+scheme:path/to/file', '', '')), ++diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py ++index 8f724f907d4..148caf742c9 100644 ++--- a/Lib/urllib/parse.py +++++ b/Lib/urllib/parse.py ++@@ -59,7 +59,7 @@ ++ 'imap', 'wais', 'file', 'mms', 'https', 'shttp', ++ 'snews', 'prospero', 'rtsp', 'rtsps', 'rtspu', 'rsync', ++ 'svn', 'svn+ssh', 'sftp', 'nfs', 'git', 'git+ssh', ++- 'ws', 'wss', 'itms-services'] +++ 'ws', 'wss'] ++ ++ uses_params = ['', 'ftp', 'hdl', 'prospero', 'http', 'imap', ++ 'https', 'shttp', 'rtsp', 'rtsps', 'rtspu', 'sip', +diff --git a/Makefile.pre.in b/Makefile.pre.in +index 3342296baa0..0eb390da8c8 100644 +--- a/Makefile.pre.in ++++ b/Makefile.pre.in +@@ -178,18 +178,29 @@ + EXE= @EXEEXT@ + BUILDEXE= @BUILDEXEEXT@ -- @unittest.skipIf(sys.platform == "darwin", "see issue #24725") -+ @unittest.skipIf(is_apple, "skipping, see issue #12958") - def testFDPassEmpty(self): - # Try to pass an empty FD array. Can receive either no array - # or an empty array. -diff --git a/Lib/test/test_socketserver.py b/Lib/test/test_socketserver.py -index 0f62f9eb200..2ca356606b2 100644 ---- a/Lib/test/test_socketserver.py -+++ b/Lib/test/test_socketserver.py -@@ -218,12 +218,16 @@ - self.dgram_examine) ++# Name of the patch file to apply for app store compliance ++APP_STORE_COMPLIANCE_PATCH=@APP_STORE_COMPLIANCE_PATCH@ ++ + # Short name and location for Mac OS X Python framework + UNIVERSALSDK=@UNIVERSALSDK@ + PYTHONFRAMEWORK= @PYTHONFRAMEWORK@ + PYTHONFRAMEWORKDIR= @PYTHONFRAMEWORKDIR@ + PYTHONFRAMEWORKPREFIX= @PYTHONFRAMEWORKPREFIX@ + PYTHONFRAMEWORKINSTALLDIR= @PYTHONFRAMEWORKINSTALLDIR@ +-# Deployment target selected during configure, to be checked ++PYTHONFRAMEWORKINSTALLNAMEPREFIX= @PYTHONFRAMEWORKINSTALLNAMEPREFIX@ ++RESSRCDIR= @RESSRCDIR@ ++# macOS deployment target selected during configure, to be checked + # by distutils. The export statement is needed to ensure that the + # deployment target is active during build. + MACOSX_DEPLOYMENT_TARGET=@CONFIGURE_MACOSX_DEPLOYMENT_TARGET@ + @EXPORT_MACOSX_DEPLOYMENT_TARGET@export MACOSX_DEPLOYMENT_TARGET - @requires_unix_sockets -+ @unittest.skipIf(test.support.is_apple_mobile and test.support.on_github_actions, -+ "gh-140702: Test fails regularly on iOS simulator on GitHub Actions") - def test_UnixDatagramServer(self): - self.run_server(socketserver.UnixDatagramServer, - socketserver.DatagramRequestHandler, - self.dgram_examine) ++# iOS Deployment target selected during configure. Unlike macOS, the iOS ++# deployment target is controlled using `-mios-version-min` arguments added to ++# CFLAGS and LDFLAGS by the configure script. This variable is not used during ++# the build, and is only listed here so it will be included in sysconfigdata. ++IPHONEOS_DEPLOYMENT_TARGET=@IPHONEOS_DEPLOYMENT_TARGET@ ++ + # Option to install to strip binaries + STRIPFLAG=-s - @requires_unix_sockets -+ @unittest.skipIf(test.support.is_apple_mobile and test.support.on_github_actions, -+ "gh-140702: Test fails regularly on iOS simulator on GitHub Actions") - def test_ThreadingUnixDatagramServer(self): - self.run_server(socketserver.ThreadingUnixDatagramServer, - socketserver.DatagramRequestHandler, -diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py -index 9d3856a226d..76999792a11 100644 ---- a/Lib/test/test_sqlite3/test_dbapi.py -+++ b/Lib/test/test_sqlite3/test_dbapi.py -@@ -32,7 +32,7 @@ +@@ -618,7 +629,7 @@ + .PHONY: all - from test.support import ( - SHORT_TIMEOUT, check_disallow_instantiation, requires_subprocess, -- is_emscripten, is_wasi -+ is_apple, is_emscripten, is_wasi - ) - from test.support import threading_helper - from _testcapi import INT_MAX, ULLONG_MAX -@@ -679,7 +679,7 @@ - cx.execute(self._sql) + .PHONY: build_all +-build_all: check-clean-src $(BUILDPYTHON) platform sharedmods \ ++build_all: check-clean-src check-app-store-compliance $(BUILDPYTHON) platform sharedmods \ + gdbhooks Programs/_testembed scripts checksharedmods rundsymutil - @unittest.skipIf(sys.platform == "win32", "skipped on Windows") -- @unittest.skipIf(sys.platform == "darwin", "skipped on macOS") -+ @unittest.skipIf(is_apple, "skipped on Apple platforms") - @unittest.skipIf(is_emscripten or is_wasi, "not supported on Emscripten/WASI") - @unittest.skipUnless(TESTFN_UNDECODABLE, "only works if there are undecodable paths") - def test_open_with_undecodable_path(self): -@@ -725,7 +725,7 @@ - cx.execute(self._sql) + .PHONY: build_wasm +@@ -641,6 +652,16 @@ + exit 1; \ + fi - @unittest.skipIf(sys.platform == "win32", "skipped on Windows") -- @unittest.skipIf(sys.platform == "darwin", "skipped on macOS") -+ @unittest.skipIf(is_apple, "skipped on Apple platforms") - @unittest.skipIf(is_emscripten or is_wasi, "not supported on Emscripten/WASI") - @unittest.skipUnless(TESTFN_UNDECODABLE, "only works if there are undecodable paths") - def test_open_undecodable_uri(self): -diff --git a/Lib/test/test_stat.py b/Lib/test/test_stat.py -index c77fec3d39d..ca55d429aec 100644 ---- a/Lib/test/test_stat.py -+++ b/Lib/test/test_stat.py -@@ -2,8 +2,7 @@ - import os - import socket - import sys --from test.support import os_helper --from test.support import socket_helper -+from test.support import is_apple, os_helper, socket_helper - from test.support.import_helper import import_fresh_module - from test.support.os_helper import TESTFN ++# Check that the app store compliance patch can be applied (if configured). ++# This is checked as a dry-run against the original library sources; ++# the patch will be actually applied during the install phase. ++.PHONY: check-app-store-compliance ++check-app-store-compliance: ++ @if [ "$(APP_STORE_COMPLIANCE_PATCH)" != "" ]; then \ ++ patch --dry-run --quiet --force --strip 1 --directory "$(abs_srcdir)" --input "$(abs_srcdir)/$(APP_STORE_COMPLIANCE_PATCH)"; \ ++ echo "App store compliance patch can be applied."; \ ++ fi ++ + # Profile generation build must start from a clean tree. + profile-clean-stamp: + $(MAKE) clean +@@ -830,7 +851,7 @@ + $(BLDSHARED) $(NO_AS_NEEDED) -o $@ -Wl,-h$@ $^ -diff --git a/Lib/test/test_sys_settrace.py b/Lib/test/test_sys_settrace.py -index 35985b34a42..fb4bc7fce9a 100644 ---- a/Lib/test/test_sys_settrace.py -+++ b/Lib/test/test_sys_settrace.py -@@ -7,7 +7,7 @@ - import gc - from functools import wraps - import asyncio --from test.support import import_helper -+from test.support import import_helper, requires_subprocess - import contextlib - import warnings + libpython$(LDVERSION).dylib: $(LIBRARY_OBJS) +- $(CC) -dynamiclib -Wl,-single_module $(PY_CORE_LDFLAGS) -undefined dynamic_lookup -Wl,-install_name,$(prefix)/lib/libpython$(LDVERSION).dylib -Wl,-compatibility_version,$(VERSION) -Wl,-current_version,$(VERSION) -o $@ $(LIBRARY_OBJS) $(DTRACE_OBJS) $(SHLIBS) $(LIBC) $(LIBM); \ ++ $(CC) -dynamiclib $(PY_CORE_LDFLAGS) -undefined dynamic_lookup -Wl,-install_name,$(prefix)/lib/libpython$(LDVERSION).dylib -Wl,-compatibility_version,$(VERSION) -Wl,-current_version,$(VERSION) -o $@ $(LIBRARY_OBJS) $(DTRACE_OBJS) $(SHLIBS) $(LIBC) $(LIBM); \ -diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py -index 67647e1b787..4e4ccd12f69 100644 ---- a/Lib/test/test_sysconfig.py -+++ b/Lib/test/test_sysconfig.py -@@ -8,7 +8,11 @@ - from copy import copy - from test.support import ( -- captured_stdout, PythonSymlink, requires_subprocess, is_wasi -+ captured_stdout, -+ is_apple_mobile, -+ is_wasi, -+ PythonSymlink, -+ requires_subprocess, - ) - from test.support.import_helper import import_module - from test.support.os_helper import (TESTFN, unlink, skip_unless_symlink, -@@ -348,6 +352,8 @@ - # XXX more platforms to tests here + libpython$(VERSION).sl: $(LIBRARY_OBJS) +@@ -855,14 +876,13 @@ + # This rule is here for OPENSTEP/Rhapsody/MacOSX. It builds a temporary + # minimal framework (not including the Lib directory and such) in the current + # directory. +-RESSRCDIR=Mac/Resources/framework + $(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK): \ + $(LIBRARY) \ + $(RESSRCDIR)/Info.plist + $(INSTALL) -d -m $(DIRMODE) $(PYTHONFRAMEWORKDIR)/Versions/$(VERSION) + $(CC) -o $(LDLIBRARY) $(PY_CORE_LDFLAGS) -dynamiclib \ +- -all_load $(LIBRARY) -Wl,-single_module \ +- -install_name $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK) \ ++ -all_load $(LIBRARY) \ ++ -install_name $(DESTDIR)$(PYTHONFRAMEWORKINSTALLNAMEPREFIX)/$(PYTHONFRAMEWORK) \ + -compatibility_version $(VERSION) \ + -current_version $(VERSION) \ + -framework CoreFoundation $(LIBS); +@@ -874,6 +894,21 @@ + $(LN) -fsn Versions/Current/$(PYTHONFRAMEWORK) $(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK) + $(LN) -fsn Versions/Current/Resources $(PYTHONFRAMEWORKDIR)/Resources - @unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds") -+ @unittest.skipIf(is_apple_mobile, -+ f"{sys.platform} doesn't distribute header files in the runtime environment") - def test_get_config_h_filename(self): - config_h = sysconfig.get_config_h_filename() - self.assertTrue(os.path.isfile(config_h), config_h) -@@ -457,6 +463,8 @@ - self.assertEqual(my_platform, test_platform) ++# This rule is for iOS, which requires an annoyingly just slighly different ++# format for frameworks to macOS. It *doesn't* use a versioned framework, and ++# the Info.plist must be in the root of the framework. ++$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK): \ ++ $(LIBRARY) \ ++ $(RESSRCDIR)/Info.plist ++ $(INSTALL) -d -m $(DIRMODE) $(PYTHONFRAMEWORKDIR) ++ $(CC) -o $(LDLIBRARY) $(PY_CORE_LDFLAGS) -dynamiclib \ ++ -all_load $(LIBRARY) \ ++ -install_name $(PYTHONFRAMEWORKINSTALLNAMEPREFIX)/$(PYTHONFRAMEWORK) \ ++ -compatibility_version $(VERSION) \ ++ -current_version $(VERSION) \ ++ -framework CoreFoundation $(LIBS); ++ $(INSTALL_DATA) $(RESSRCDIR)/Info.plist $(PYTHONFRAMEWORKDIR)/Info.plist ++ + # This rule builds the Cygwin Python DLL and import library if configured + # for a shared core library; otherwise, this rule is a noop. + $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS) +@@ -1863,6 +1898,36 @@ + $(RUNSHARED) /usr/libexec/oah/translate \ + ./$(BUILDPYTHON) -E -m test -j 0 -u all $(TESTOPTS) - @unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds") -+ @unittest.skipIf(is_apple_mobile, -+ f"{sys.platform} doesn't include config folder at runtime") - def test_srcdir(self): - # See Issues #15322, #15364. - srcdir = sysconfig.get_config_var('srcdir') -@@ -591,6 +599,8 @@ - @unittest.skipIf(sys.platform.startswith('win'), - 'Test is not Windows compatible') - @unittest.skipIf(is_wasi, "Incompatible with WASI mapdir and OOT builds") -+ @unittest.skipIf(is_apple_mobile, -+ f"{sys.platform} doesn't include config folder at runtime") - def test_get_makefile_filename(self): - makefile = sysconfig.get_makefile_filename() - self.assertTrue(os.path.isfile(makefile), makefile) -diff --git a/Lib/test/test_unicode_file_functions.py b/Lib/test/test_unicode_file_functions.py -index 47619c8807b..25c16e3a0b7 100644 ---- a/Lib/test/test_unicode_file_functions.py -+++ b/Lib/test/test_unicode_file_functions.py -@@ -5,7 +5,7 @@ - import unittest - import warnings - from unicodedata import normalize --from test.support import os_helper -+from test.support import is_apple, os_helper - from test import support ++# Run the test suite on the iOS simulator. Must be run on a macOS machine with ++# a full Xcode install that has an iPhone SE (3rd edition) simulator available. ++# This must be run *after* a `make install` has completed the build. The ++# `--with-framework-name` argument *cannot* be used when configuring the build. ++XCFOLDER:=iOSTestbed.$(MULTIARCH).$(shell date +%s) ++.PHONY: testios ++testios: ++ @if test "$(MACHDEP)" != "ios"; then \ ++ echo "Cannot run the iOS testbed for a non-iOS build."; \ ++ exit 1;\ ++ fi ++ @if test "$(findstring -iphonesimulator,$(MULTIARCH))" != "-iphonesimulator"; then \ ++ echo "Cannot run the iOS testbed for non-simulator builds."; \ ++ exit 1;\ ++ fi ++ @if test $(PYTHONFRAMEWORK) != "Python"; then \ ++ echo "Cannot run the iOS testbed with a non-default framework name."; \ ++ exit 1;\ ++ fi ++ @if ! test -d $(PYTHONFRAMEWORKPREFIX); then \ ++ echo "Cannot find a finalized iOS Python.framework. Have you run 'make install' to finalize the framework build?"; \ ++ exit 1;\ ++ fi ++ ++ # Clone the testbed project into the XCFOLDER ++ $(PYTHON_FOR_BUILD) $(srcdir)/Platforms/Apple/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)" ++ ++ # Run the testbed project ++ $(PYTHON_FOR_BUILD) "$(XCFOLDER)" run --verbose -- test -uall --single-process --rerun -W ++ + # Like testall, but with only one pass and without multiple processes. + # Run an optional script to include information about the build environment. + .PHONY: buildbottest +@@ -1906,7 +1971,7 @@ + # which can lead to two parallel `./python setup.py build` processes that + # step on each others toes. + .PHONY: install +-install: @FRAMEWORKINSTALLFIRST@ commoninstall bininstall maninstall @FRAMEWORKINSTALLLAST@ ++install: @FRAMEWORKINSTALLFIRST@ @INSTALLTARGETS@ @FRAMEWORKINSTALLLAST@ + if test "x$(ENSUREPIP)" != "xno" ; then \ + case $(ENSUREPIP) in \ + upgrade) ensurepip="--upgrade" ;; \ +@@ -2342,6 +2407,14 @@ + $(INSTALL_DATA) `cat pybuilddir.txt`/_sysconfigdata_$(ABIFLAGS)_$(MACHDEP)_$(MULTIARCH).py \ + $(DESTDIR)$(LIBDEST); \ + $(INSTALL_DATA) $(srcdir)/LICENSE $(DESTDIR)$(LIBDEST)/LICENSE.txt ++ @ # If app store compliance has been configured, apply the patch to the ++ @ # installed library code. The patch has been previously validated against ++ @ # the original source tree, so we can ignore any errors that are raised ++ @ # due to files that are missing because of --disable-test-modules etc. ++ @if [ "$(APP_STORE_COMPLIANCE_PATCH)" != "" ]; then \ ++ echo "Applying app store compliance patch"; \ ++ patch --force --reject-file "$(abs_builddir)/app-store-compliance.rej" --strip 2 --directory "$(DESTDIR)$(LIBDEST)" --input "$(abs_srcdir)/$(APP_STORE_COMPLIANCE_PATCH)" || true ; \ ++ fi + @ # Build PYC files for the 3 optimization levels (0, 1, 2) + -PYTHONPATH=$(DESTDIR)$(LIBDEST) $(RUNSHARED) \ + $(PYTHON_FOR_BUILD) -Wi $(DESTDIR)$(LIBDEST)/compileall.py \ +@@ -2518,10 +2591,11 @@ + # only have to cater for the structural bits of the framework. + .PHONY: frameworkinstallframework +-frameworkinstallframework: frameworkinstallstructure install frameworkinstallmaclib ++frameworkinstallframework: @FRAMEWORKINSTALLFIRST@ install frameworkinstallmaclib -@@ -23,13 +23,13 @@ - '10_\u1fee\u1ffd', - ] +-.PHONY: frameworkinstallstructure +-frameworkinstallstructure: $(LDLIBRARY) ++# macOS uses a versioned frameworks structure that includes a full install ++.PHONY: frameworkinstallversionedstructure ++frameworkinstallversionedstructure: $(LDLIBRARY) + @if test "$(PYTHONFRAMEWORKDIR)" = no-framework; then \ + echo Not configured with --enable-framework; \ + exit 1; \ +@@ -2542,6 +2616,30 @@ + $(LN) -fsn Versions/Current/Resources $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Resources + $(INSTALL_SHARED) $(LDLIBRARY) $(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/$(LDLIBRARY) --# Mac OS X decomposes Unicode names, using Normal Form D. -+# Apple platforms decompose Unicode names, using Normal Form D. - # http://developer.apple.com/mac/library/qa/qa2001/qa1173.html - # "However, most volume formats do not follow the exact specification for - # these normal forms. For example, HFS Plus uses a variant of Normal Form D - # in which U+2000 through U+2FFF, U+F900 through U+FAFF, and U+2F800 through - # U+2FAFF are not decomposed." --if sys.platform != 'darwin': -+if not is_apple: - filenames.extend([ - # Specific code points: NFC(fn), NFD(fn), NFKC(fn) and NFKD(fn) all different - '11_\u0385\u03d3\u03d4', -@@ -119,11 +119,11 @@ - os.stat(name) - self._apply_failure(os.listdir, name, self._listdir_failure) ++# iOS/tvOS/watchOS uses a non-versioned framework with Info.plist in the ++# framework root, no .lproj data, and only stub compilation assistance binaries ++.PHONY: frameworkinstallunversionedstructure ++frameworkinstallunversionedstructure: $(LDLIBRARY) ++ @if test "$(PYTHONFRAMEWORKDIR)" = no-framework; then \ ++ echo Not configured with --enable-framework; \ ++ exit 1; \ ++ else true; \ ++ fi ++ if test -d $(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include; then \ ++ echo "Clearing stale header symlink directory"; \ ++ rm -rf $(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include; \ ++ fi ++ $(INSTALL) -d -m $(DIRMODE) $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR) ++ sed 's/%VERSION%/'"`$(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import platform; print(platform.python_version())'`"'/g' < $(RESSRCDIR)/Info.plist > $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Info.plist ++ $(INSTALL_SHARED) $(LDLIBRARY) $(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/$(LDLIBRARY) ++ $(INSTALL) -d -m $(DIRMODE) $(DESTDIR)$(LIBDIR) ++ $(LN) -fs "../$(LDLIBRARY)" "$(DESTDIR)$(prefix)/lib/libpython$(LDVERSION).dylib" ++ $(LN) -fs "../$(LDLIBRARY)" "$(DESTDIR)$(prefix)/lib/libpython$(VERSION).dylib" ++ $(INSTALL) -d -m $(DIRMODE) $(DESTDIR)$(BINDIR) ++ for file in $(srcdir)/$(RESSRCDIR)/bin/* ; do \ ++ $(INSTALL) -m $(EXEMODE) $$file $(DESTDIR)$(BINDIR); \ ++ done ++ + # This installs Mac/Lib into the framework + # Install a number of symlinks to keep software that expects a normal unix + # install (which includes python-config) happy. +@@ -2582,6 +2680,19 @@ + frameworkinstallextras: + cd Mac && $(MAKE) installextras DESTDIR="$(DESTDIR)" + ++# On iOS, bin/lib can't live inside the framework; include needs to be called ++# "Headers", but *must* be in the framework, and *not* include the `python3.X` ++# subdirectory. The install has put these folders in the same folder as ++# Python.framework; Move the headers to their final framework-compatible home. ++.PHONY: frameworkinstallmobileheaders ++frameworkinstallmobileheaders: frameworkinstallunversionedstructure inclinstall ++ if test -d $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Headers; then \ ++ echo "Removing old framework headers"; \ ++ rm -rf $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Headers; \ ++ fi ++ mv "$(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include/python$(LDVERSION)" "$(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Headers" ++ $(LN) -fs "../$(PYTHONFRAMEWORKDIR)/Headers" "$(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include/python$(LDVERSION)" ++ + # Build the toplevel Makefile + Makefile.pre: $(srcdir)/Makefile.pre.in config.status + CONFIG_FILES=Makefile.pre CONFIG_HEADERS= ./config.status +@@ -2692,6 +2803,10 @@ + -find build -type f -a ! -name '*.gc??' -exec rm -f {} ';' + -rm -f Include/pydtrace_probes.h + -rm -f profile-gen-stamp ++ -rm -rf Platforms/Apple/iOS/testbed/Python.xcframework/ios-*/bin ++ -rm -rf Platforms/Apple/iOS/testbed/Python.xcframework/ios-*/lib ++ -rm -rf Platforms/Apple/iOS/testbed/Python.xcframework/ios-*/include ++ -rm -rf Platforms/Apple/iOS/testbed/Python.xcframework/ios-*/Python.framework -- # Skip the test on darwin, because darwin does normalize the filename to -+ # Skip the test on Apple platforms, because they don't normalize the filename to - # NFD (a variant of Unicode NFD form). Normalize the filename to NFC, NFKC, - # NFKD in Python is useless, because darwin will normalize it later and so - # open(), os.stat(), etc. don't raise any exception. -- @unittest.skipIf(sys.platform == 'darwin', 'irrelevant test on Mac OS X') -+ @unittest.skipIf(is_apple, 'irrelevant test on Apple platforms') - @unittest.skipIf( - support.is_emscripten or support.is_wasi, - "test fails on Emscripten/WASI when host platform is macOS." -@@ -142,10 +142,10 @@ - self._apply_failure(os.remove, name) - self._apply_failure(os.listdir, name) + .PHONY: profile-removal + profile-removal: +@@ -2717,6 +2832,8 @@ + config.cache config.log pyconfig.h Modules/config.c + -rm -rf build platform + -rm -rf $(PYTHONFRAMEWORKDIR) ++ -rm -rf Platforms/Apple/iOS/Frameworks ++ -rm -rf iOSTestbed.* + -rm -f python-config.py python-config -- # Skip the test on darwin, because darwin uses a normalization different -+ # Skip the test on Apple platforms, because they use a normalization different - # than Python NFD normalization: filenames are different even if we use - # Python NFD normalization. -- @unittest.skipIf(sys.platform == 'darwin', 'irrelevant test on Mac OS X') -+ @unittest.skipIf(is_apple, 'irrelevant test on Apple platforms') - def test_listdir(self): - sf0 = set(self.files) - with warnings.catch_warnings(): -diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py -index c62f1825d1f..5ace1c7cea5 100644 ---- a/Lib/test/test_urllib2.py -+++ b/Lib/test/test_urllib2.py -@@ -1,6 +1,7 @@ - import unittest - from test import support - from test.support import os_helper -+from test.support import requires_subprocess - from test.support import warnings_helper - from test import test_urllib - from unittest import mock -@@ -1040,6 +1041,7 @@ + # Make things extra clean, before making a distribution: +diff --git a/Modules/getpath.c b/Modules/getpath.c +index 0a310000751..83a2bc469ae 100644 +--- a/Modules/getpath.c ++++ b/Modules/getpath.c +@@ -15,6 +15,7 @@ + #endif - file_obj.close() + #ifdef __APPLE__ ++# include "TargetConditionals.h" + # include + #endif -+ @requires_subprocess() - def test_http_body_pipe(self): - # A file reading from a pipe. - # A pipe cannot be seek'ed. There is no way to determine the -diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py -index 43c67ac751d..ba6c9cf53ad 100644 ---- a/Lib/test/test_venv.py -+++ b/Lib/test/test_venv.py -@@ -20,8 +20,8 @@ - import shlex - from test.support import (captured_stdout, captured_stderr, - skip_if_broken_multiprocessing_synchronize, verbose, -- requires_subprocess, is_emscripten, is_wasi, -- requires_venv_with_pip, TEST_HOME_DIR, -+ requires_subprocess, is_apple_mobile, is_emscripten, -+ is_wasi, requires_venv_with_pip, TEST_HOME_DIR, - requires_resource, copy_python_src_ignore) - from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree, - TESTFN, FakePath) -@@ -42,8 +42,10 @@ - or sys._base_executable != sys.executable, - 'cannot run venv.create from within a venv on this platform') +@@ -759,7 +760,7 @@ + return winmodule_to_dict(dict, key, PyWin_DLLhModule); + } + #endif +-#elif defined(WITH_NEXT_FRAMEWORK) ++#elif defined(WITH_NEXT_FRAMEWORK) && !defined(TARGET_OS_IPHONE) + static char modPath[MAXPATHLEN + 1]; + static int modPathInitialized = -1; + if (modPathInitialized < 0) { +@@ -953,4 +954,3 @@ --if is_emscripten or is_wasi: -- raise unittest.SkipTest("venv is not available on Emscripten/WASI.") -+# Skip tests on WASM platforms, plus iOS/tvOS/watchOS -+if is_apple_mobile or is_emscripten or is_wasi: -+ raise unittest.SkipTest(f"venv tests not required on {sys.platform}") + return _PyStatus_OK(); + } +- +diff --git a/Platforms/Apple/.ruff.toml b/Platforms/Apple/.ruff.toml +new file mode 100644 +index 00000000000..c06e67b7060 +--- /dev/null ++++ b/Platforms/Apple/.ruff.toml +@@ -0,0 +1,22 @@ ++extend = "../../.ruff.toml" # Inherit the project-wide settings ++ ++[format] ++preview = true ++docstring-code-format = true ++ ++[lint] ++select = [ ++ "C4", # flake8-comprehensions ++ "E", # pycodestyle ++ "F", # pyflakes ++ "I", # isort ++ "ISC", # flake8-implicit-str-concat ++ "LOG", # flake8-logging ++ "PGH", # pygrep-hooks ++ "PT", # flake8-pytest-style ++ "PYI", # flake8-pyi ++ "RUF100", # Ban unused `# noqa` comments ++ # "UP", # pyupgrade ++ "W", # pycodestyle ++ "YTT", # flake8-2020 ++] +diff --git a/Platforms/Apple/__main__.py b/Platforms/Apple/__main__.py +new file mode 100644 +index 00000000000..44d624d26a1 +--- /dev/null ++++ b/Platforms/Apple/__main__.py +@@ -0,0 +1,1143 @@ ++#!/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 supports iOS, tvOS and watchOS, but it has been ++# constructed so that it could be used on any Apple platform. ++# ++# The simplest entry point is: ++# ++# $ python Platforms/Apple ci iOS ++# ++# (replace iOS with tvOS or watchOS as required.) ++# ++# 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/tvOS/watchOS ++# architecture and ABI ++# * Combine the outputs of the builds from the previous step into a single ++# XCframework, merging binaries into a "fat" binary if necessary ++# * Clone a copy of the testbed, configured to use the XCframework ++# * Construct a tarball containing the release artefacts ++# * Run the test suite using the generated XCframework. ++# ++# This is the complete sequence that would be needed in CI to build and test ++# a candidate release artefact. ++# ++# Each individual step can be invoked individually - there are commands to ++# clean, configure-build, make-build, configure-host, make-host, package, and ++# test. ++# ++# There is also a build command that can be used to combine the configure and ++# make steps for the build Python, an individual host, all hosts, or all ++# builds. ++########################################################################## ++from __future__ import annotations ++ ++import argparse ++import os ++import platform ++import re ++import shlex ++import shutil ++import signal ++import subprocess ++import sys ++import sysconfig ++import time ++from collections.abc import Callable, Sequence ++from contextlib import contextmanager ++from datetime import datetime, timezone ++from os.path import basename, relpath ++from pathlib import Path ++from subprocess import CalledProcessError ++ ++EnvironmentT = dict[str, str] ++ArgsT = Sequence[str | Path] ++ ++SCRIPT_NAME = Path(__file__).name ++PYTHON_DIR = Path(__file__).resolve().parent.parent.parent ++ ++CROSS_BUILD_DIR = PYTHON_DIR / "cross-build" ++ ++HOSTS: dict[str, dict[str, dict[str, str]]] = { ++ # Structure of this data: ++ # * Platform identifier ++ # * an XCframework slice that must exist for that platform ++ # * a host triple: the multiarch spec for that host ++ "iOS": { ++ "ios-arm64": { ++ "arm64-apple-ios": "arm64-iphoneos", ++ }, ++ "ios-arm64_x86_64-simulator": { ++ "arm64-apple-ios-simulator": "arm64-iphonesimulator", ++ "x86_64-apple-ios-simulator": "x86_64-iphonesimulator", ++ }, ++ }, ++ "tvOS": { ++ "tvos-arm64": { ++ "arm64-apple-tvos": "arm64-appletvos", ++ }, ++ "tvos-arm64_x86_64-simulator": { ++ "arm64-apple-tvos-simulator": "arm64-appletvsimulator", ++ "x86_64-apple-tvos-simulator": "x86_64-appletvsimulator", ++ }, ++ }, ++ "watchOS": { ++ "watchos-arm64_32": { ++ "arm64_32-apple-watchos": "arm64_32-watchos", ++ }, ++ "watchos-arm64_x86_64-simulator": { ++ "arm64-apple-watchos-simulator": "arm64-watchsimulator", ++ "x86_64-apple-watchos-simulator": "x86_64-watchsimulator", ++ }, ++ }, ++} ++ ++ ++def subdir(name: str, create: bool = False) -> Path: ++ """Ensure that a cross-build directory for the given name exists.""" ++ path = CROSS_BUILD_DIR / name ++ if not path.exists(): ++ if not create: ++ sys.exit( ++ f"{path} does not exist. Create it by running the appropriate " ++ f"`configure` subcommand of {SCRIPT_NAME}." ++ ) ++ else: ++ path.mkdir(parents=True) ++ return path ++ ++ ++def run( ++ command: ArgsT, ++ *, ++ host: str | None = None, ++ env: EnvironmentT | None = None, ++ log: bool | None = True, ++ **kwargs, ++) -> subprocess.CompletedProcess: ++ """Run a command in an Apple development environment. ++ ++ Optionally logs the executed command to the console. ++ """ ++ kwargs.setdefault("check", True) ++ if env is None: ++ env = os.environ.copy() ++ ++ if host: ++ host_env = apple_env(host) ++ print_env(host_env) ++ env.update(host_env) ++ ++ if log: ++ print(">", join_command(command)) ++ return subprocess.run(command, env=env, **kwargs) ++ ++ ++def join_command(args: str | Path | ArgsT) -> str: ++ """Format a command so it can be copied into a shell. ++ ++ Similar to `shlex.join`, but also accepts arguments which are Paths, or a ++ single string/Path outside of a list. ++ """ ++ if isinstance(args, (str, Path)): ++ return str(args) ++ else: ++ return shlex.join(map(str, args)) ++ ++ ++def print_env(env: EnvironmentT) -> None: ++ """Format the environment so it can be pasted into a shell.""" ++ for key, value in sorted(env.items()): ++ print(f"export {key}={shlex.quote(value)}") ++ ++ ++def platform_for_host(host): ++ """Determine the platform for a given host triple.""" ++ for plat, slices in HOSTS.items(): ++ for _, candidates in slices.items(): ++ for candidate in candidates: ++ if candidate == host: ++ return plat ++ raise KeyError(host) ++ ++ ++def apple_env(host: str) -> EnvironmentT: ++ """Construct an Apple development environment for the given host.""" ++ env = { ++ "PATH": ":".join([ ++ str( ++ PYTHON_DIR ++ / f"Platforms/Apple/{platform_for_host(host)}/Resources/bin" ++ ), ++ str(subdir(host) / "prefix"), ++ "/usr/bin", ++ "/bin", ++ "/usr/sbin", ++ "/sbin", ++ "/Library/Apple/usr/bin", ++ ]), ++ } ++ ++ return env ++ ++ ++def delete_path(name: str) -> None: ++ """Delete the named cross-build directory, if it exists.""" ++ path = CROSS_BUILD_DIR / name ++ if path.exists(): ++ print(f"Deleting {path} ...") ++ shutil.rmtree(path) ++ ++ ++def all_host_triples(platform: str) -> list[str]: ++ """Return all host triples for the given platform. ++ ++ The host triples are the platform definitions used as input to configure ++ (e.g., "arm64-apple-ios-simulator"). ++ """ ++ triples = [] ++ for slice_name, slice_parts in HOSTS[platform].items(): ++ triples.extend(list(slice_parts)) ++ return triples ++ ++ ++def clean(context: argparse.Namespace, target: str | None = None) -> None: ++ """The implementation of the "clean" command.""" ++ if target is None: ++ target = context.host ++ ++ # If we're explicitly targeting the build, there's no platform or ++ # distribution artefacts. If we're cleaning tests, we keep all built ++ # artefacts. Otherwise, the built artefacts must be dirty, so we remove ++ # them. ++ if target not in {"build", "test"}: ++ paths = ["dist", context.platform] + list(HOSTS[context.platform]) ++ else: ++ paths = [] ++ ++ if target in {"all", "build"}: ++ paths.append("build") ++ ++ if target in {"all", "hosts"}: ++ paths.extend(all_host_triples(context.platform)) ++ elif target not in {"build", "test", "package"}: ++ paths.append(target) ++ ++ if target in {"all", "hosts", "test"}: ++ paths.extend([ ++ path.name ++ for path in CROSS_BUILD_DIR.glob(f"{context.platform}-testbed.*") ++ ]) ++ ++ for path in paths: ++ delete_path(path) ++ ++ ++def build_python_path() -> Path: ++ """The path to the build Python binary.""" ++ build_dir = subdir("build") ++ binary = build_dir / "python" ++ if not binary.is_file(): ++ binary = binary.with_suffix(".exe") ++ if not binary.is_file(): ++ raise FileNotFoundError( ++ f"Unable to find `python(.exe)` in {build_dir}" ++ ) ++ ++ return binary ++ ++ ++@contextmanager ++def group(text: str): ++ """A context manager that outputs a log marker around a section of a build. ++ ++ If running in a GitHub Actions environment, the GitHub syntax for ++ collapsible log sections is used. ++ """ ++ if "GITHUB_ACTIONS" in os.environ: ++ print(f"::group::{text}") ++ else: ++ print(f"===== {text} " + "=" * (70 - len(text))) ++ ++ yield ++ ++ if "GITHUB_ACTIONS" in os.environ: ++ print("::endgroup::") ++ else: ++ print() ++ ++ ++@contextmanager ++def cwd(subdir: Path): ++ """A context manager that sets the current working directory.""" ++ orig = os.getcwd() ++ os.chdir(subdir) ++ yield ++ os.chdir(orig) ++ ++ ++def configure_build_python(context: argparse.Namespace) -> None: ++ """The implementation of the "configure-build" command.""" ++ if context.clean: ++ clean(context, "build") ++ ++ with ( ++ group("Configuring build Python"), ++ cwd(subdir("build", create=True)), ++ ): ++ command = [relpath(PYTHON_DIR / "configure")] ++ if context.args: ++ command.extend(context.args) ++ run(command) ++ ++ ++def make_build_python(context: argparse.Namespace) -> None: ++ """The implementation of the "make-build" command.""" ++ with ( ++ group("Compiling build Python"), ++ cwd(subdir("build")), ++ ): ++ run(["make", "-j", str(os.cpu_count())]) ++ ++ ++def apple_target(host: str) -> str: ++ """Return the Apple platform identifier for a given host triple.""" ++ for _, platform_slices in HOSTS.items(): ++ for slice_name, slice_parts in platform_slices.items(): ++ for host_triple, multiarch in slice_parts.items(): ++ if host == host_triple: ++ return ".".join(multiarch.split("-")[::-1]) ++ ++ raise KeyError(host) ++ ++ ++def apple_multiarch(host: str) -> str: ++ """Return the multiarch descriptor for a given host triple.""" ++ for _, platform_slices in HOSTS.items(): ++ for slice_name, slice_parts in platform_slices.items(): ++ for host_triple, multiarch in slice_parts.items(): ++ if host == host_triple: ++ return multiarch ++ ++ raise KeyError(host) ++ ++ ++def unpack_deps( ++ platform: str, ++ host: str, ++ prefix_dir: Path, ++ cache_dir: Path, ++) -> None: ++ """Unpack binary dependencies into a provided directory. ++ ++ Downloads binaries if they aren't already present. Downloads will be stored ++ in provided cache directory. ++ ++ On non-macOS platforms, as a safety mechanism, any dynamic libraries will ++ be purged from the unpacked dependencies. ++ """ ++ # To create new builds of these dependencies, usually all that's necessary ++ # is to push a tag to the cpython-apple-source-deps repository, and GitHub ++ # Actions will do the rest. ++ # ++ # If you're a member of the Python core team, and you'd like to be able to ++ # push these tags yourself, please contact Malcolm Smith or Russell ++ # Keith-Magee. ++ 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.19-1", ++ "XZ-5.6.4-2", ++ "mpdecimal-4.0.0-2", ++ "zstd-1.5.7-1", ++ ]: ++ filename = f"{name_ver.lower()}-{apple_target(host)}.tar.gz" ++ archive_path = download( ++ f"{deps_url}/{name_ver}/{filename}", ++ target_dir=cache_dir, ++ ) ++ shutil.unpack_archive(archive_path, prefix_dir) ++ ++ # Dynamic libraries will be preferentially linked over static; On non-macOS ++ # platforms, ensure that no dylibs are available in the prefix folder. ++ if platform != "macOS": ++ for dylib in prefix_dir.glob("**/*.dylib"): ++ dylib.unlink() ++ ++ ++def download(url: str, target_dir: Path) -> Path: ++ """Download the specified URL into the given directory. ++ ++ :return: The path to the downloaded archive. ++ """ ++ target_path = Path(target_dir).resolve() ++ target_path.mkdir(exist_ok=True, parents=True) ++ ++ out_path = target_path / basename(url) ++ if not Path(out_path).is_file(): ++ run([ ++ "curl", ++ "-Lf", ++ "--retry", ++ "5", ++ "--retry-all-errors", ++ "-o", ++ out_path, ++ url, ++ ]) ++ else: ++ print(f"Using cached version of {basename(url)}") ++ return out_path ++ ++ ++def configure_host_python( ++ context: argparse.Namespace, ++ host: str | None = None, ++) -> None: ++ """The implementation of the "configure-host" command.""" ++ if host is None: ++ host = context.host ++ ++ if context.clean: ++ clean(context, host) ++ ++ host_dir = subdir(host, create=True) ++ prefix_dir = host_dir / "prefix" ++ ++ with group(f"Downloading dependencies ({host})"): ++ if not prefix_dir.exists(): ++ prefix_dir.mkdir() ++ cache_dir = ( ++ Path(context.cache_dir).resolve() ++ if context.cache_dir ++ else CROSS_BUILD_DIR / "downloads" ++ ) ++ unpack_deps(context.platform, host, prefix_dir, 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-ipv6", ++ "--enable-framework", ++ # Dependent libraries. ++ f"--with-openssl={prefix_dir}", ++ f"LIBLZMA_CFLAGS=-I{prefix_dir}/include", ++ f"LIBLZMA_LIBS=-L{prefix_dir}/lib -llzma", ++ f"LIBFFI_CFLAGS=-I{prefix_dir}/include", ++ f"LIBFFI_LIBS=-L{prefix_dir}/lib -lffi", ++ f"LIBMPDEC_CFLAGS=-I{prefix_dir}/include", ++ f"LIBMPDEC_LDFLAGS=-L{prefix_dir}/lib -lmpdec", ++ ] ++ ++ if context.args: ++ command.extend(context.args) ++ run(command, host=host) ++ ++ ++def make_host_python( ++ context: argparse.Namespace, ++ host: str | None = None, ++) -> None: ++ """The implementation of the "make-host" command.""" ++ if host is None: ++ host = context.host ++ ++ with ( ++ group(f"Compiling host Python ({host})"), ++ cwd(subdir(host)), ++ ): ++ run(["make"], host=host) ++ run(["make", "install"], host=host) ++ ++ ++def framework_path(host_triple: str, multiarch: str) -> Path: ++ """The path to a built single-architecture framework product. ++ ++ :param host_triple: The host triple (e.g., arm64-apple-ios-simulator) ++ :param multiarch: The multiarch identifier (e.g., arm64-simulator) ++ """ ++ return ( ++ CROSS_BUILD_DIR ++ / f"{host_triple}/Platforms/Apple/{platform_for_host(host_triple)}" ++ / f"Frameworks/{multiarch}" ++ ) ++ ++ ++def package_version(prefix_path: Path) -> str: ++ """Extract the Python version being built from patchlevel.h.""" ++ for path in prefix_path.glob("**/patchlevel.h"): ++ text = path.read_text(encoding="utf-8") ++ if match := re.search( ++ r'\n\s*#define\s+PY_VERSION\s+"(.+)"\s*\n', text ++ ): ++ version = match[1] ++ # If not building against a tagged commit, add a timestamp to the ++ # version. Follow the PyPA version number rules, as this will make ++ # it easier to process with other tools. The version will have a ++ # `+` suffix once any official release has been made; a freshly ++ # forked main branch will have a version of 3.X.0a0. ++ if version.endswith("a0"): ++ version += "+" ++ if version.endswith("+"): ++ version += datetime.now(timezone.utc).strftime("%Y%m%d.%H%M%S") ++ ++ return version ++ ++ sys.exit("Unable to determine Python version being packaged.") ++ ++ ++def lib_platform_files(dirname, names): ++ """A file filter that ignores platform-specific files in lib.""" ++ 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 = { ++ name ++ for name in names ++ if ( ++ name.startswith("_sysconfigdata_") ++ or name.startswith("_sysconfig_vars_") ++ or name == "build-details.json" ++ ) ++ } ++ elif path.parts[-1] == "lib": ++ ignored_names = { ++ name ++ for name in names ++ if name.startswith("libpython") and name.endswith(".dylib") ++ } ++ 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. ++ ++ :return: The version number of the Python version that was packaged. ++ """ ++ package_path = CROSS_BUILD_DIR / platform ++ try: ++ package_path.mkdir() ++ except FileExistsError: ++ raise RuntimeError( ++ f"{platform} XCframework already exists; do you need to run " ++ "with --clean?" ++ ) from None ++ ++ frameworks = [] ++ # Merge Frameworks for each component SDK. If there's only one architecture ++ # for the SDK, we can use the compiled Python.framework as-is. However, if ++ # there's more than architecture, we need to merge the individual built ++ # frameworks into a merged "fat" framework. ++ for slice_name, slice_parts in HOSTS[platform].items(): ++ # Some parts are the same across all slices, so we use can any of the ++ # host frameworks as the source for the merged version. Use the first ++ # one on the list, as it's as representative as any other. ++ first_host_triple, first_multiarch = next(iter(slice_parts.items())) ++ first_framework = ( ++ framework_path(first_host_triple, first_multiarch) ++ / "Python.framework" ++ ) ++ ++ if len(slice_parts) == 1: ++ # The first framework is the only framework, so copy it. ++ print(f"Copying framework for {slice_name}...") ++ frameworks.append(first_framework) ++ else: ++ print(f"Merging framework for {slice_name}...") ++ slice_path = CROSS_BUILD_DIR / slice_name ++ slice_framework = slice_path / "Python.framework" ++ slice_framework.mkdir(exist_ok=True, parents=True) ++ ++ # Copy the Info.plist ++ shutil.copy( ++ first_framework / "Info.plist", ++ slice_framework / "Info.plist", ++ ) ++ ++ # Copy the headers ++ shutil.copytree( ++ first_framework / "Headers", ++ slice_framework / "Headers", ++ ) ++ ++ # Create the "fat" library binary for the slice ++ run( ++ ["lipo", "-create", "-output", slice_framework / "Python"] ++ + [ ++ ( ++ framework_path(host_triple, multiarch) ++ / "Python.framework/Python" ++ ) ++ for host_triple, multiarch in slice_parts.items() ++ ] ++ ) ++ ++ # Add this merged slice to the list to be added to the XCframework ++ frameworks.append(slice_framework) ++ ++ print() ++ print("Build XCframework...") ++ cmd = [ ++ "xcodebuild", ++ "-create-xcframework", ++ "-output", ++ package_path / "Python.xcframework", ++ ] ++ for framework in frameworks: ++ cmd.extend(["-framework", framework]) ++ ++ run(cmd) ++ ++ # Extract the package version from the merged framework ++ version = package_version(package_path / "Python.xcframework") ++ version_tag = ".".join(version.split(".")[:2]) ++ ++ # On non-macOS platforms, each framework in XCframework only contains the ++ # headers, libPython, plus an Info.plist. Other resources like the standard ++ # library and binary shims aren't allowed to live in framework; they need ++ # to be copied in separately. ++ print() ++ print("Copy additional resources...") ++ has_common_stdlib = False ++ for slice_name, slice_parts in HOSTS[platform].items(): ++ # Some parts are the same across all slices, so we can any of the ++ # host frameworks as the source for the merged version. ++ first_host_triple, first_multiarch = next(iter(slice_parts.items())) ++ first_path = framework_path(first_host_triple, first_multiarch) ++ first_framework = first_path / "Python.framework" ++ ++ slice_path = package_path / f"Python.xcframework/{slice_name}" ++ slice_framework = slice_path / "Python.framework" ++ ++ # Copy the binary helpers ++ print(f" - {slice_name} binaries") ++ shutil.copytree(first_path / "bin", slice_path / "bin") ++ ++ # Copy the include path (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"Platforms/Apple/{platform}/Resources/pyconfig.h", ++ slice_framework / "Headers/pyconfig.h", ++ ) ++ ++ print(f" - {slice_name} shared library") ++ # Create a simlink for the fat library ++ shared_lib = slice_path / f"lib/libpython{version_tag}.dylib" ++ shared_lib.parent.mkdir() ++ shared_lib.symlink_to("../Python.framework/Python") ++ ++ 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, ++ symlinks=True, ++ ) ++ has_common_stdlib = True ++ ++ shutil.copytree( ++ framework_path(host_triple, multiarch) / "lib", ++ slice_path / f"lib-{arch}", ++ ignore=lib_non_platform_files, ++ symlinks=True, ++ ) ++ ++ # Copy the host's pyconfig.h to an architecture-specific name. ++ arch = multiarch.split("-")[0] ++ host_path = ( ++ CROSS_BUILD_DIR ++ / host_triple ++ / f"Platforms/Apple/{platform}/Frameworks" ++ / multiarch ++ ) ++ host_framework = host_path / "Python.framework" ++ shutil.copy( ++ host_framework / "Headers/pyconfig.h", ++ slice_framework / f"Headers/pyconfig-{arch}.h", ++ ) ++ ++ # Apple identifies certain libraries as "security risks"; if you ++ # statically link those libraries into a Framework, you become ++ # responsible for providing a privacy manifest for that framework. ++ xcprivacy_file = { ++ "OpenSSL": subdir(host_triple) ++ / "prefix/share/OpenSSL.xcprivacy" ++ } ++ print(f" - {multiarch} xcprivacy files") ++ for module, lib in [ ++ ("_hashlib", "OpenSSL"), ++ ("_ssl", "OpenSSL"), ++ ]: ++ shutil.copy( ++ xcprivacy_file[lib], ++ slice_path ++ / f"lib-{arch}/python{version_tag}" ++ / f"lib-dynload/{module}.xcprivacy", ++ ) ++ ++ print(" - build tools") ++ shutil.copytree( ++ PYTHON_DIR / "Platforms/Apple/testbed/Python.xcframework/build", ++ package_path / "Python.xcframework/build", ++ ) ++ ++ return version ++ ++ ++def package(context: argparse.Namespace) -> None: ++ """The implementation of the "package" command.""" ++ if context.clean: ++ clean(context, "package") ++ ++ with group("Building package"): ++ # Create an XCframework ++ version = create_xcframework(context.platform) ++ ++ # Clone testbed ++ print() ++ run([ ++ sys.executable, ++ "Platforms/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( ++ str(CROSS_BUILD_DIR / archive_name), ++ format="gztar", ++ root_dir=CROSS_BUILD_DIR / context.platform, ++ base_dir=".", ++ ) ++ print() ++ print(f"{archive_name.relative_to(PYTHON_DIR)}.tar.gz created.") ++ ++ ++def build(context: argparse.Namespace, host: str | None = None) -> None: ++ """The implementation of the "build" command.""" ++ if host is None: ++ host = context.host ++ ++ if context.clean: ++ clean(context, host) ++ ++ if host in {"all", "build"}: ++ for step in [ ++ configure_build_python, ++ make_build_python, ++ ]: ++ step(context) ++ ++ if host == "build": ++ hosts = [] ++ elif host in {"all", "hosts"}: ++ hosts = all_host_triples(context.platform) ++ else: ++ hosts = [host] ++ ++ for step_host in hosts: ++ for step in [ ++ configure_host_python, ++ make_host_python, ++ ]: ++ step(context, host=step_host) ++ ++ if host == "all": ++ package(context) ++ ++ ++def test(context: argparse.Namespace, host: str | None = None) -> None: # noqa: PT028 ++ """The implementation of the "test" command.""" ++ if host is None: ++ host = context.host ++ ++ if context.clean: ++ clean(context, "test") ++ ++ with group(f"Test {'XCframework' if host in {'all', 'hosts'} else host}"): ++ timestamp = str(time.time_ns())[:-6] ++ testbed_dir = ( ++ CROSS_BUILD_DIR / f"{context.platform}-testbed.{timestamp}" ++ ) ++ if host in {"all", "hosts"}: ++ framework_path = ( ++ CROSS_BUILD_DIR / context.platform / "Python.xcframework" ++ ) ++ else: ++ build_arch = platform.machine() ++ host_arch = host.split("-")[0] ++ ++ if not host.endswith("-simulator"): ++ print("Skipping test suite non-simulator build.") ++ return ++ elif build_arch != host_arch: ++ print( ++ f"Skipping test suite for an {host_arch} build " ++ f"on an {build_arch} machine." ++ ) ++ return ++ else: ++ framework_path = ( ++ CROSS_BUILD_DIR ++ / host ++ / f"Platforms/Apple/{context.platform}" ++ / f"Frameworks/{apple_multiarch(host)}" ++ ) ++ ++ run([ ++ sys.executable, ++ "Platforms/Apple/testbed", ++ "clone", ++ "--platform", ++ context.platform, ++ "--framework", ++ framework_path, ++ testbed_dir, ++ ]) ++ ++ run( ++ [ ++ sys.executable, ++ testbed_dir, ++ "run", ++ "--verbose", ++ ] ++ + ( ++ ["--simulator", str(context.simulator)] ++ if context.simulator ++ else [] ++ ) ++ + [ ++ "--", ++ "test", ++ "-uall", ++ "--rerun", ++ f"--{context.ci_mode or 'fast'}-ci", ++ "--single-process", ++ "-W", ++ # Timeout handling requires subprocesses; explicitly setting ++ # the timeout to -1 disables the faulthandler. ++ "--timeout=-1", ++ # Adding Python options requires the use of a subprocess to ++ # start a new Python interpreter. ++ "--dont-add-python-opts", ++ ] ++ ) ++ ++ ++def apple_sim_host(platform_name: str) -> str: ++ """Determine the native simulator target for this platform.""" ++ for _, slice_parts in HOSTS[platform_name].items(): ++ for host_triple in slice_parts: ++ parts = host_triple.split("-") ++ if parts[0] == platform.machine() and parts[-1] == "simulator": ++ return host_triple ++ ++ raise KeyError(platform_name) ++ ++ ++def ci(context: argparse.Namespace) -> None: ++ """The implementation of the "ci" command. ++ ++ In "Fast" mode, this compiles the build python, and the simulator for the ++ build machine's architecture; and runs the test suite with `--fast-ci` ++ configuration. ++ ++ In "Slow" mode, it compiles the build python, plus all candidate ++ architectures (both device and simulator); then runs the test suite with ++ `--slow-ci` configuration. ++ """ ++ clean(context, "all") ++ if context.ci_mode == "slow": ++ # In slow mode, build and test the full XCframework ++ build(context, host="all") ++ test(context, host="all") ++ else: ++ # In fast mode, just build the simulator platform. ++ sim_host = apple_sim_host(context.platform) ++ build(context, host="build") ++ build(context, host=sim_host) ++ test(context, host=sim_host) ++ ++ ++def parse_args() -> argparse.Namespace: ++ parser = argparse.ArgumentParser( ++ description=( ++ "A tool for managing the build, package and test process of " ++ "CPython on Apple platforms." ++ ), ++ ) ++ parser.suggest_on_error = True ++ subcommands = parser.add_subparsers(dest="subcommand", required=True) ++ ++ clean = subcommands.add_parser( ++ "clean", ++ help="Delete all build directories", ++ ) ++ ++ configure_build = subcommands.add_parser( ++ "configure-build", help="Run `configure` for the build Python" ++ ) ++ make_build = 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'" ++ ), ++ ) ++ ++ # --cross-build-dir argument ++ for cmd in [ ++ clean, ++ configure_build, ++ make_build, ++ configure_host, ++ make_host, ++ build, ++ package, ++ test, ++ ci, ++ ]: ++ cmd.add_argument( ++ "--cross-build-dir", ++ action="store", ++ default=os.environ.get("CROSS_BUILD_DIR"), ++ dest="cross_build_dir", ++ type=Path, ++ help=( ++ "Path to the cross-build directory " ++ f"(default: {CROSS_BUILD_DIR}). Can also be set " ++ "with the CROSS_BUILD_DIR environment variable." ++ ), ++ ) ++ ++ # --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=os.environ.get("CACHE_DIR"), ++ 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." ++ ), ++ ) ++ group = cmd.add_mutually_exclusive_group() ++ group.add_argument( ++ "--fast-ci", ++ action="store_const", ++ dest="ci_mode", ++ const="fast", ++ help="Add test arguments for GitHub Actions", ++ ) ++ group.add_argument( ++ "--slow-ci", ++ action="store_const", ++ dest="ci_mode", ++ const="slow", ++ help="Add test arguments for buildbots", ++ ) ++ ++ for subcommand in [configure_build, configure_host, build, ci]: ++ subcommand.add_argument( ++ "args", nargs="*", help="Extra arguments to pass to `configure`" ++ ) ++ ++ return parser.parse_args() ++ ++ ++def print_called_process_error(e: subprocess.CalledProcessError) -> None: ++ for stream_name in ["stdout", "stderr"]: ++ content = getattr(e, stream_name) ++ stream = getattr(sys, stream_name) ++ if content: ++ stream.write(content) ++ if not content.endswith("\n"): ++ stream.write("\n") ++ ++ # shlex uses single quotes, so we surround the command with double quotes. ++ print( ++ f'Command "{join_command(e.cmd)}" returned exit status {e.returncode}' ++ ) ++ ++ ++def main() -> None: ++ # Handle SIGTERM the same way as SIGINT. This ensures that if we're ++ # terminated by the buildbot worker, we'll make an attempt to clean up our ++ # subprocesses. ++ def signal_handler(*args): ++ os.kill(os.getpid(), signal.SIGINT) ++ ++ signal.signal(signal.SIGTERM, signal_handler) ++ ++ # Process command line arguments ++ context = parse_args() ++ ++ # Set the CROSS_BUILD_DIR if an argument was provided ++ if context.cross_build_dir: ++ global CROSS_BUILD_DIR ++ CROSS_BUILD_DIR = context.cross_build_dir.resolve() ++ ++ dispatch: dict[str, Callable] = { ++ "clean": clean, ++ "configure-build": configure_build_python, ++ "make-build": make_build_python, ++ "configure-host": configure_host_python, ++ "make-host": make_host_python, ++ "package": package, ++ "build": build, ++ "test": test, ++ "ci": ci, ++ } ++ ++ try: ++ dispatch[context.subcommand](context) ++ except CalledProcessError as e: ++ print() ++ print_called_process_error(e) ++ sys.exit(1) ++ except RuntimeError as e: ++ print() ++ print(e) ++ sys.exit(2) ++ ++ ++if __name__ == "__main__": ++ # Under the buildbot, stdout is not a TTY, but we must still flush after ++ # every line to make sure our output appears in the correct order relative ++ # to the output of our subprocesses. ++ for stream in [sys.stdout, sys.stderr]: ++ stream.reconfigure(line_buffering=True) ++ ++ main() +diff --git a/Platforms/Apple/iOS/README.md b/Platforms/Apple/iOS/README.md +new file mode 100644 +index 00000000000..faeeead1df0 +--- /dev/null ++++ b/Platforms/Apple/iOS/README.md +@@ -0,0 +1,339 @@ ++# Python on iOS README ++ ++**iOS support is [tier 3](https://peps.python.org/pep-0011/#tier-3).** ++ ++This document provides a quick overview of some iOS specific features in the ++Python distribution. ++ ++These instructions are only needed if you're planning to compile Python for iOS ++yourself. Most users should *not* need to do this. If you're looking to ++experiment with writing an iOS app in Python, tools such as [BeeWare's ++Briefcase](https://briefcase.readthedocs.io) and [Kivy's ++Buildozer](https://buildozer.readthedocs.io) will provide a much more ++approachable user experience. ++ ++## Compilers for building on iOS ++ ++Building for iOS requires the use of Apple's Xcode tooling. It is strongly ++recommended that you use the most recent stable release of Xcode. This will ++require the use of the most (or second-most) recently released macOS version, ++as Apple does not maintain Xcode for older macOS versions. The Xcode Command ++Line Tools are not sufficient for iOS development; you need a *full* Xcode ++install. ++ ++If you want to run your code on the iOS simulator, you'll also need to install ++an iOS Simulator Platform. You should be prompted to select an iOS Simulator ++Platform when you first run Xcode. Alternatively, you can add an iOS Simulator ++Platform by selecting an open the Platforms tab of the Xcode Settings panel. ++ ++## Building Python on iOS ++ ++### ABIs and Architectures ++ ++iOS apps can be deployed on physical devices, and on the iOS simulator. Although ++the API used on these devices is identical, the ABI is different - you need to ++link against different libraries for an iOS device build (`iphoneos`) or an ++iOS simulator build (`iphonesimulator`). ++ ++Apple uses the `XCframework` format to allow specifying a single dependency ++that supports multiple ABIs. An `XCframework` is a wrapper around multiple ++ABI-specific frameworks that share a common API. ++ ++iOS can also support different CPU architectures within each ABI. At present, ++there is only a single supported architecture on physical devices - ARM64. ++However, the *simulator* supports 2 architectures - ARM64 (for running on Apple ++Silicon machines), and x86_64 (for running on older Intel-based machines). ++ ++To support multiple CPU architectures on a single platform, Apple uses a "fat ++binary" format - a single physical file that contains support for multiple ++architectures. It is possible to compile and use a "thin" single architecture ++version of a binary for testing purposes; however, the "thin" binary will not be ++portable to machines using other architectures. ++ ++### Building a multi-architecture iOS XCframework ++ ++The `Platforms/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 Platforms/Apple build iOS ++ ++This will: ++ ++* Configure and compile a version of Python to run on the build machine ++* Download pre-compiled binary dependencies for each platform ++* Configure and build a `Python.framework` for each required architecture and ++ iOS SDK ++* Merge the multiple `Python.framework` folders into a single `Python.xcframework` ++* Produce a `.tar.gz` archive in the `cross-build/dist` folder containing ++ the `Python.xcframework`, plus a copy of the Testbed app pre-configured to ++ use the XCframework. ++ ++The `Platforms/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 `Platforms/Apple` build script, you won't need to build ++individual frameworks. However, if you do need to manually configure an iOS ++Python build for a single framework, the following options are available. ++ ++#### iOS specific arguments to configure ++ ++* `--enable-framework[=DIR]` ++ ++ This argument specifies the location where the Python.framework will be ++ installed. If `DIR` is not specified, the framework will be installed into ++ a subdirectory of the `iOS/Frameworks` folder. ++ ++ This argument *must* be provided when configuring iOS builds. iOS does not ++ support non-framework builds. ++ ++* `--with-framework-name=NAME` ++ ++ Specify the name for the Python framework; defaults to `Python`. ++ ++ > [!NOTE] ++ > Unless you know what you're doing, changing the name of the Python ++ > framework on iOS is not advised. If you use this option, you won't be able ++ > to run the `Platforms/Apple` build script without making significant manual ++ > alterations, and you won't be able to use any binary packages unless you ++ > compile them yourself using your own framework name. ++ ++#### Building Python for iOS ++ ++The Python build system will create a `Python.framework` that supports a ++*single* ABI with a *single* architecture. Unlike macOS, iOS does not allow a ++framework to contain non-library content, so the iOS build will produce a ++`bin` and `lib` folder in the same output folder as `Python.framework`. ++The `lib` folder will be needed at runtime to support the Python library. ++ ++If you want to use Python in a real iOS project, you need to produce multiple ++`Python.framework` builds, one for each ABI and architecture. iOS builds of ++Python *must* be constructed as framework builds. To support this, you must ++provide the `--enable-framework` flag when configuring the build. The build ++also requires the use of cross-compilation. The minimal commands for building ++Python for the ARM64 iOS simulator will look something like: ++``` ++export PATH="$(pwd)/Platforms/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: ++ ++* `Platforms/Apple/iOS/Resources/bin` has been added to the path, providing some shims for the ++ compilers and linkers needed by the build. Xcode requires the use of `xcrun` ++ to invoke compiler tooling. However, if `xcrun` is pre-evaluated and the ++ result passed to `configure`, these results can embed user- and ++ version-specific paths into the sysconfig data, which limits the portability ++ of the compiled Python. Alternatively, if `xcrun` is used *as* the compiler, ++ it requires that compiler variables like `CC` include spaces, which can ++ cause significant problems with many C configuration systems which assume that ++ `CC` will be a single executable. ++ ++ To work around this problem, the `Platforms/Apple/iOS/Resources/bin` folder contains some ++ wrapper scripts that present as simple compilers and linkers, but wrap ++ underlying calls to `xcrun`. This allows configure to use a `CC` ++ definition without spaces, and without user- or version-specific paths, while ++ retaining the ability to adapt to the local Xcode install. These scripts are ++ included in the `bin` directory of an iOS install. ++ ++ These scripts will, by default, use the currently active Xcode installation. ++ If you want to use a different Xcode installation, you can use ++ `xcode-select` to set a new default Xcode globally, or you can use the ++ `DEVELOPER_DIR` environment variable to specify an Xcode install. The ++ scripts will use the default `iphoneos`/`iphonesimulator` SDK version for ++ the select Xcode install; if you want to use a different SDK, you can set the ++ `IOS_SDK_VERSION` environment variable. (e.g, setting ++ `IOS_SDK_VERSION=17.1` would cause the scripts to use the `iphoneos17.1` ++ and `iphonesimulator17.1` SDKs, regardless of the Xcode default.) ++ ++ The path has also been cleared of any user customizations. A common source of ++ bugs is for tools like Homebrew to accidentally leak macOS binaries into an iOS ++ build. Resetting the path to a known "bare bones" value is the easiest way to ++ avoid these problems. ++ ++* `--host` is the architecture and ABI that you want to build, in GNU compiler ++ triple format. This will be one of: ++ ++ - `arm64-apple-ios` for ARM64 iOS devices. ++ - `arm64-apple-ios-simulator` for the iOS simulator running on Apple ++ Silicon devices. ++ - `x86_64-apple-ios-simulator` for the iOS simulator running on Intel ++ devices. ++ ++* `--build` is the GNU compiler triple for the machine that will be running ++ the compiler. This is one of: ++ ++ - `arm64-apple-darwin` for Apple Silicon devices. ++ - `x86_64-apple-darwin` for Intel devices. ++ ++* `/path/to/python.exe` is the path to a Python binary on the machine that ++ will be running the compiler. This is needed because the Python compilation ++ process involves running some Python code. On a normal desktop build of ++ Python, you can compile a python interpreter and then use that interpreter to ++ run Python code. However, the binaries produced for iOS won't run on macOS, so ++ you need to provide an external Python interpreter. This interpreter must be ++ the same version as the Python that is being compiled. To be completely safe, ++ this should be the *exact* same commit hash. However, the longer a Python ++ release has been stable, the more likely it is that this constraint can be ++ relaxed - the same micro version will often be sufficient. ++ ++* The `install` target for iOS builds is slightly different to other ++ platforms. On most platforms, `make install` will install the build into ++ the final runtime location. This won't be the case for iOS, as the final ++ runtime location will be on a physical device. ++ ++ However, you still need to run the `install` target for iOS builds, as it ++ performs some final framework assembly steps. The location specified with ++ `--enable-framework` will be the location where `make install` will ++ assemble the complete iOS framework. This completed framework can then ++ be copied and relocated as required. ++ ++For a full CPython build, you also need to specify the paths to iOS builds of ++the binary libraries that CPython depends on (such as XZ, LibFFI and OpenSSL). ++This can be done by defining library specific environment variables (such as ++`LIBLZMA_CFLAGS`, `LIBLZMA_LIBS`), and the `--with-openssl` configure ++option. Versions of these libraries pre-compiled for iOS can be found in [this ++repository](https://github.com/beeware/cpython-apple-source-deps/releases). ++LibFFI is especially important, as many parts of the standard library ++(including the `platform`, `sysconfig` and `webbrowser` modules) require ++the use of the `ctypes` module at runtime. ++ ++By default, Python will be compiled with an iOS deployment target (i.e., the ++minimum supported iOS version) of 13.0. To specify a different deployment ++target, provide the version number as part of the `--host` argument - for ++example, `--host=arm64-apple-ios15.4-simulator` would compile an ARM64 ++simulator build with a deployment target of 15.4. ++ ++## Testing Python on iOS ++ ++### Testing a multi-architecture framework ++ ++Once you have a built an XCframework, you can test that framework by running: ++ ++ $ python Platforms/Apple test iOS ++ ++This test will attempt to find an "SE-class" simulator (i.e., an iPhone SE, or ++iPhone 16e, or similar), and run the test suite on the most recent version of ++iOS that is available. You can specify a simulator using the `--simulator` ++command line argument, providing the name of the simulator (e.g., `--simulator ++'iPhone 16 Pro'`). You can also use this argument to control the OS version used ++for testing; `--simulator 'iPhone 16 Pro,OS=18.2'` would attempt to run the ++tests on an iPhone 16 Pro running iOS 18.2. ++ ++If the test runner is executed on GitHub Actions, the `GITHUB_ACTIONS` ++environment variable will be exposed to the iOS process at runtime. ++ ++### Testing a single-architecture framework ++ ++The `Platforms/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 `Platforms/Apple/iOS/Resources/bin` folder and ++exclude any non-iOS tools, then run: ++``` ++make all ++make install ++make testios ++``` ++ ++This will: ++ ++* Build an iOS framework for your chosen architecture; ++* Finalize the single-platform framework; ++* Make a clean copy of the testbed project; ++* Install the Python iOS framework into the copy of the testbed project; and ++* Run the test suite on an "entry-level device" simulator (i.e., an iPhone SE, ++ iPhone 16e, or a similar). ++ ++On success, the test suite will exit and report successful completion of the ++test suite. On a 2022 M1 MacBook Pro, the test suite takes approximately 15 ++minutes to run; a couple of extra minutes is required to compile the testbed ++project, and then boot and prepare the iOS simulator. ++ ++### Debugging test failures ++ ++Running `python Platforms/Apple test iOS` generates a standalone version of the ++`Platforms/Apple/testbed` project, and runs the full test suite. It does this using ++`Platforms/Apple/testbed` itself - the folder is an executable module that can be used ++to create and run a clone of the testbed project. The standalone version of the ++testbed will be created in a directory named ++`cross-build/iOS-testbed.`. ++ ++You can generate your own standalone testbed instance by running: ++``` ++python cross-build/iOS/testbed clone my-testbed ++``` ++ ++In this invocation, `my-testbed` is the name of the folder for the new ++testbed clone. ++ ++If you've built your own XCframework, or you only want to test a single architecture, ++you can construct a standalone testbed instance by running: ++``` ++python Platforms/Apple/testbed clone --platform iOS --framework my-testbed ++``` ++ ++The framework path can be the path path to a `Python.xcframework`, or the ++path to a folder that contains a single-platform `Python.framework`. ++ ++You can then use the `my-testbed` folder to run the Python test suite, ++passing in any command line arguments you may require. For example, if you're ++trying to diagnose a failure in the `os` module, you might run: ++``` ++python my-testbed run -- test -W test_os ++``` ++ ++This is the equivalent of running `python -m test -W test_os` on a desktop ++Python build. Any arguments after the `--` will be passed to testbed as if ++they were arguments to `python -m` on a desktop machine. ++ ++### Testing in Xcode ++ ++You can also open the testbed project in Xcode by running: ++``` ++open my-testbed/iOSTestbed.xcodeproj ++``` ++ ++This will allow you to use the full Xcode suite of tools for debugging. ++ ++The arguments used to run the test suite are defined as part of the test plan. ++To modify the test plan, select the test plan node of the project tree (it ++should be the first child of the root node), and select the "Configurations" ++tab. Modify the "Arguments Passed On Launch" value to change the testing ++arguments. ++ ++The test plan also disables parallel testing, and specifies the use of the ++`Testbed.lldbinit` file for providing configuration of the debugger. The ++default debugger configuration disables automatic breakpoints on the ++`SIGINT`, `SIGUSR1`, `SIGUSR2`, and `SIGXFSZ` signals. ++ ++### Testing on an iOS device ++ ++To test on an iOS device, the app needs to be signed with known developer ++credentials. To obtain these credentials, you must have an iOS Developer ++account, and your Xcode install will need to be logged into your account (see ++the Accounts tab of the Preferences dialog). ++ ++Once the project is open, and you're signed into your Apple Developer account, ++select the root node of the project tree (labeled "iOSTestbed"), then the ++"Signing & Capabilities" tab in the details page. Select a development team ++(this will likely be your own name), and plug in a physical device to your ++macOS machine with a USB cable. You should then be able to select your physical ++device from the list of targets in the pulldown in the Xcode titlebar. +diff --git a/Platforms/Apple/iOS/Resources/Info.plist.in b/Platforms/Apple/iOS/Resources/Info.plist.in +new file mode 100644 +index 00000000000..26ef7a95de4 +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/Info.plist.in +@@ -0,0 +1,34 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleExecutable ++ Python ++ CFBundleGetInfoString ++ Python Runtime and Library ++ CFBundleIdentifier ++ @PYTHONFRAMEWORKIDENTIFIER@ ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundleName ++ Python ++ CFBundlePackageType ++ FMWK ++ CFBundleShortVersionString ++ %VERSION% ++ CFBundleLongVersionString ++ %VERSION%, (c) 2001-2024 Python Software Foundation. ++ CFBundleSignature ++ ???? ++ CFBundleVersion ++ %VERSION% ++ CFBundleSupportedPlatforms ++ ++ iPhoneOS ++ ++ MinimumOSVersion ++ @IPHONEOS_DEPLOYMENT_TARGET@ ++ ++ +diff --git a/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-ar b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-ar +new file mode 100755 +index 00000000000..3cf3eb21874 +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-ar +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-clang b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-clang +new file mode 100755 +index 00000000000..f50d5b5142f +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-clang +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-clang++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-clang++ +new file mode 100755 +index 00000000000..0794731d7dc +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-cpp b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-cpp +new file mode 100755 +index 00000000000..24fa1506bab +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-cpp +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} -E "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar +new file mode 100755 +index 00000000000..b836b6db902 +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang +new file mode 100755 +index 00000000000..4891a00876e +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ +new file mode 100755 +index 00000000000..58b2a5f6f18 +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp +new file mode 100755 +index 00000000000..c9df94e8b7c +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip +new file mode 100755 +index 00000000000..fd59d309b73 +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch arm64 "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-strip b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-strip +new file mode 100755 +index 00000000000..75e823a3d02 +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/arm64-apple-ios-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} strip -arch arm64 "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar b/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar +new file mode 100755 +index 00000000000..b836b6db902 +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang b/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang +new file mode 100755 +index 00000000000..f4739a7b945 +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ b/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ +new file mode 100755 +index 00000000000..c348ae4c103 +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp b/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp +new file mode 100755 +index 00000000000..6d7f8084c9f +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" +diff --git a/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip b/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip +new file mode 100755 +index 00000000000..c5cfb289291 +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch x86_64 "$@" +diff --git a/Platforms/Apple/iOS/Resources/pyconfig.h b/Platforms/Apple/iOS/Resources/pyconfig.h +new file mode 100644 +index 00000000000..4acff2c6051 +--- /dev/null ++++ b/Platforms/Apple/iOS/Resources/pyconfig.h +@@ -0,0 +1,7 @@ ++#ifdef __arm64__ ++#include "pyconfig-arm64.h" ++#endif ++ ++#ifdef __x86_64__ ++#include "pyconfig-x86_64.h" ++#endif +diff --git a/Platforms/Apple/testbed/Python.xcframework/Info.plist b/Platforms/Apple/testbed/Python.xcframework/Info.plist +new file mode 100644 +index 00000000000..f90105a6d70 +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/Info.plist +@@ -0,0 +1,106 @@ ++ ++ ++ ++ ++ AvailableLibraries ++ ++ ++ BinaryPath ++ Python.framework/Python ++ LibraryIdentifier ++ ios-arm64 ++ LibraryPath ++ Python.framework ++ SupportedArchitectures ++ ++ arm64 ++ ++ SupportedPlatform ++ ios ++ ++ ++ BinaryPath ++ Python.framework/Python ++ LibraryIdentifier ++ ios-arm64_x86_64-simulator ++ LibraryPath ++ Python.framework ++ SupportedArchitectures ++ ++ arm64 ++ x86_64 ++ ++ SupportedPlatform ++ ios ++ SupportedPlatformVariant ++ simulator ++ ++ ++ BinaryPath ++ Python.framework/Python ++ LibraryIdentifier ++ tvos-arm64 ++ LibraryPath ++ Python.framework ++ SupportedArchitectures ++ ++ arm64 ++ ++ SupportedPlatform ++ tvos ++ ++ ++ BinaryPath ++ Python.framework/Python ++ LibraryIdentifier ++ tvos-arm64_x86_64-simulator ++ LibraryPath ++ Python.framework ++ SupportedArchitectures ++ ++ arm64 ++ x86_64 ++ ++ SupportedPlatform ++ tvos ++ SupportedPlatformVariant ++ simulator ++ ++ ++ BinaryPath ++ Python.framework/Python ++ LibraryIdentifier ++ watchos-arm64_x86_64-simulator ++ LibraryPath ++ Python.framework ++ SupportedArchitectures ++ ++ arm64 ++ x86_64 ++ ++ SupportedPlatform ++ watchos ++ SupportedPlatformVariant ++ simulator ++ ++ ++ BinaryPath ++ Python.framework/Python ++ LibraryIdentifier ++ watchos-arm64_32 ++ LibraryPath ++ Python.framework ++ SupportedArchitectures ++ ++ arm64_32 ++ ++ SupportedPlatform ++ watchos ++ ++ ++ CFBundlePackageType ++ XFWK ++ XCFrameworkFormatVersion ++ 1.0 ++ ++ +diff --git a/Platforms/Apple/testbed/Python.xcframework/build/iOS-dylib-Info-template.plist b/Platforms/Apple/testbed/Python.xcframework/build/iOS-dylib-Info-template.plist +new file mode 100644 +index 00000000000..d6caa01c1e4 +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/build/iOS-dylib-Info-template.plist +@@ -0,0 +1,26 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleExecutable ++ ++ CFBundleIdentifier ++ ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundlePackageType ++ APPL ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSupportedPlatforms ++ ++ iPhoneOS ++ ++ MinimumOSVersion ++ 13.0 ++ CFBundleVersion ++ 1 ++ ++ +diff --git a/Platforms/Apple/testbed/Python.xcframework/build/tvOS-dylib-Info-template.plist b/Platforms/Apple/testbed/Python.xcframework/build/tvOS-dylib-Info-template.plist +new file mode 100644 +index 00000000000..a20d476fa7b +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/build/tvOS-dylib-Info-template.plist +@@ -0,0 +1,26 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleExecutable ++ ++ CFBundleIdentifier ++ ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundlePackageType ++ APPL ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSupportedPlatforms ++ ++ tvOS ++ ++ MinimumOSVersion ++ 9.0 ++ CFBundleVersion ++ 1 ++ ++ +diff --git a/Platforms/Apple/testbed/Python.xcframework/build/utils.sh b/Platforms/Apple/testbed/Python.xcframework/build/utils.sh +new file mode 100755 +index 00000000000..2c3c8008512 +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/build/utils.sh +@@ -0,0 +1,174 @@ ++# Utility methods for use in an Xcode project. ++# ++# An iOS XCframework cannot include any content other than the library binary ++# and relevant metadata. However, Python requires a standard library at runtime. ++# Therefore, it is necessary to add a build step to an Xcode app target that ++# processes the standard library and puts the content into the final app. ++# ++# In general, these tools will be invoked after bundle resources have been ++# copied into the app, but before framework embedding (and signing). ++# ++# The following is an example script, assuming that: ++# * Python.xcframework is in the root of the project ++# * There is an `app` folder that contains the app code ++# * There is an `app_packages` folder that contains installed Python packages. ++# ----- ++# set -e ++# source $PROJECT_DIR/Python.xcframework/build/build_utils.sh ++# install_python Python.xcframework app app_packages ++# ----- ++ ++# Copy the standard library from the XCframework into the app bundle. ++# ++# Accepts one argument: ++# 1. The path, relative to the root of the Xcode project, where the Python ++# XCframework can be found. ++install_stdlib() { ++ PYTHON_XCFRAMEWORK_PATH=$1 ++ ++ mkdir -p "$CODESIGNING_FOLDER_PATH/python/lib" ++ if [ "$EFFECTIVE_PLATFORM_NAME" = "-iphonesimulator" ]; then ++ echo "Installing Python modules for iOS Simulator" ++ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/ios-arm64-simulator" ]; then ++ SLICE_FOLDER="ios-arm64-simulator" ++ else ++ SLICE_FOLDER="ios-arm64_x86_64-simulator" ++ fi ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-iphoneos" ]; then ++ echo "Installing Python modules for iOS Device" ++ SLICE_FOLDER="ios-arm64" ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-appletvsimulator" ]; then ++ echo "Installing Python modules for tvOS Simulator" ++ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/tvos-arm64-simulator" ]; then ++ SLICE_FOLDER="tvos-arm64-simulator" ++ else ++ SLICE_FOLDER="tvos-arm64_x86_64-simulator" ++ fi ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-appletvos" ]; then ++ echo "Installing Python modules for tvOS Device" ++ SLICE_FOLDER="tvos-arm64" ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-watchsimulator" ]; then ++ echo "Installing Python modules for watchOS Simulator" ++ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/watchos-arm64-simulator" ]; then ++ SLICE_FOLDER="watchos-arm64-simulator" ++ else ++ SLICE_FOLDER="watchos-arm64_x86_64-simulator" ++ fi ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-watchos" ]; then ++ echo "Installing Python modules for watchOS Device" ++ SLICE_FOLDER="watchos-arm64" ++ else ++ echo "Unsupported platform name $EFFECTIVE_PLATFORM_NAME" ++ exit 1 ++ fi ++ ++ # If the XCframework has a shared lib folder, then it's a full framework. ++ # Copy both the common and slice-specific part of the lib directory. ++ # Otherwise, it's a single-arch framework; use the "full" lib folder. ++ # Don't include any libpython symlink; that can't be included at runtime ++ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib" ]; then ++ rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" --exclude 'libpython*.dylib' ++ rsync -au "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib-$ARCHS/" "$CODESIGNING_FOLDER_PATH/python/lib/" --exclude 'libpython*.dylib' ++ else ++ rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" --exclude 'libpython*.dylib' ++ fi ++} ++ ++# Convert a single .so library into a framework that iOS can load. ++# ++# Accepts three arguments: ++# 1. The path, relative to the root of the Xcode project, where the Python ++# XCframework can be found. ++# 2. The base path, relative to the installed location in the app bundle, that ++# needs to be processed. Any .so file found in this path (or a subdirectory ++# of it) will be processed. ++# 2. The full path to a single .so file to process. This path should include ++# the base path. ++install_dylib () { ++ PYTHON_XCFRAMEWORK_PATH=$1 ++ INSTALL_BASE=$2 ++ FULL_EXT=$3 ++ ++ # The name of the extension file ++ EXT=$(basename "$FULL_EXT") ++ # The name and location of the module ++ MODULE_PATH=$(dirname "$FULL_EXT") ++ MODULE_NAME=$(echo $EXT | cut -d "." -f 1) ++ # The location of the extension file, relative to the bundle ++ RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} ++ # The path to the extension file, relative to the install base ++ PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/} ++ # The full dotted name of the extension module, constructed from the file path. ++ FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d "." -f 1 | tr "/" "."); ++ # A bundle identifier; not actually used, but required by Xcode framework packaging ++ FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr "_" "-") ++ # The name of the framework folder. ++ FRAMEWORK_FOLDER="Frameworks/$FULL_MODULE_NAME.framework" ++ ++ # If the framework folder doesn't exist, create it. ++ if [ ! -d "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ]; then ++ echo "Creating framework for $RELATIVE_EXT" ++ mkdir -p "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ++ cp "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/build/$PLATFORM_FAMILY_NAME-dylib-Info-template.plist" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" ++ plutil -replace CFBundleExecutable -string "$FULL_MODULE_NAME" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" ++ plutil -replace CFBundleIdentifier -string "$FRAMEWORK_BUNDLE_ID" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" ++ fi ++ ++ echo "Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME" ++ mv "$FULL_EXT" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" ++ # Create a placeholder .fwork file where the .so was ++ echo "$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" > ${FULL_EXT%.so}.fwork ++ # Create a back reference to the .so file location in the framework ++ echo "${RELATIVE_EXT%.so}.fwork" > "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin" ++ ++ # If the framework provides an xcprivacy file, install it. ++ if [ -e "$MODULE_PATH/$MODULE_NAME.xcprivacy" ]; then ++ echo "Installing XCPrivacy file for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME" ++ XCPRIVACY_FILE="$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/PrivacyInfo.xcprivacy" ++ if [ -e "$XCPRIVACY_FILE" ]; then ++ rm -rf "$XCPRIVACY_FILE" ++ fi ++ mv "$MODULE_PATH/$MODULE_NAME.xcprivacy" "$XCPRIVACY_FILE" ++ fi ++ ++ echo "Signing framework as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)..." ++ /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ++} ++ ++# Process all the dynamic libraries in a path into Framework format. ++# ++# Accepts two arguments: ++# 1. The path, relative to the root of the Xcode project, where the Python ++# XCframework can be found. ++# 2. The base path, relative to the installed location in the app bundle, that ++# needs to be processed. Any .so file found in this path (or a subdirectory ++# of it) will be processed. ++process_dylibs () { ++ PYTHON_XCFRAMEWORK_PATH=$1 ++ LIB_PATH=$2 ++ find "$CODESIGNING_FOLDER_PATH/$LIB_PATH" -name "*.so" | while read FULL_EXT; do ++ install_dylib $PYTHON_XCFRAMEWORK_PATH "$LIB_PATH/" "$FULL_EXT" ++ done ++} ++ ++# The entry point for post-processing a Python XCframework. ++# ++# Accepts 1 or more arguments: ++# 1. The path, relative to the root of the Xcode project, where the Python ++# XCframework can be found. If the XCframework is in the root of the project, ++# 2+. The path of a package, relative to the root of the packaged app, that contains ++# library content that should be processed for binary libraries. ++install_python() { ++ PYTHON_XCFRAMEWORK_PATH=$1 ++ shift ++ ++ install_stdlib $PYTHON_XCFRAMEWORK_PATH ++ PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib" | grep -E "^python3\.\d+$") ++ 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/Platforms/Apple/testbed/Python.xcframework/build/watchOS-dylib-Info-template.plist b/Platforms/Apple/testbed/Python.xcframework/build/watchOS-dylib-Info-template.plist +new file mode 100644 +index 00000000000..6f8c0bc2095 +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/build/watchOS-dylib-Info-template.plist +@@ -0,0 +1,26 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleExecutable ++ ++ CFBundleIdentifier ++ ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundlePackageType ++ APPL ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSupportedPlatforms ++ ++ watchOS ++ ++ MinimumOSVersion ++ 4.0 ++ CFBundleVersion ++ 1 ++ ++ +diff --git a/Platforms/Apple/testbed/Python.xcframework/build/xrOS-dylib-Info-template.plist b/Platforms/Apple/testbed/Python.xcframework/build/xrOS-dylib-Info-template.plist +new file mode 100644 +index 00000000000..e91673b4fbc +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/build/xrOS-dylib-Info-template.plist +@@ -0,0 +1,30 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundleExecutable ++ ++ CFBundleIdentifier ++ ++ CFBundlePackageType ++ FMWK ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSupportedPlatforms ++ ++ XROS ++ ++ CFBundleVersion ++ 1 ++ MinimumOSVersion ++ 2.0 ++ UIDeviceFamily ++ ++ 7 ++ ++ ++ +diff --git a/Platforms/Apple/testbed/Python.xcframework/ios-arm64/README b/Platforms/Apple/testbed/Python.xcframework/ios-arm64/README +new file mode 100644 +index 00000000000..c1b076d12cd +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/ios-arm64/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling an iOS on-device ++build for testing purposes. +diff --git a/Platforms/Apple/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README b/Platforms/Apple/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README +new file mode 100644 +index 00000000000..ae334e5d769 +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling an iOS simulator ++build for testing purposes (either x86_64 or ARM64). +diff --git a/Platforms/Apple/testbed/Python.xcframework/tvos-arm64/README b/Platforms/Apple/testbed/Python.xcframework/tvos-arm64/README +new file mode 100644 +index 00000000000..ebd648d04bd +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/tvos-arm64/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a tvOS ++on-device build for testing purposes. +diff --git a/Platforms/Apple/testbed/Python.xcframework/tvos-arm64_x86_64-simulator/README b/Platforms/Apple/testbed/Python.xcframework/tvos-arm64_x86_64-simulator/README +new file mode 100644 +index 00000000000..f8163468d8c +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/tvos-arm64_x86_64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a tvOS ++simulator build for testing purposes (either x86_64 or ARM64). +diff --git a/Platforms/Apple/testbed/Python.xcframework/watchos-arm64_32/README b/Platforms/Apple/testbed/Python.xcframework/watchos-arm64_32/README +new file mode 100644 +index 00000000000..696af231df3 +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/watchos-arm64_32/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a watchOS on-device ++build for testing purposes. +diff --git a/Platforms/Apple/testbed/Python.xcframework/watchos-arm64_x86_64-simulator/README b/Platforms/Apple/testbed/Python.xcframework/watchos-arm64_x86_64-simulator/README +new file mode 100644 +index 00000000000..d38e1e98276 +--- /dev/null ++++ b/Platforms/Apple/testbed/Python.xcframework/watchos-arm64_x86_64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a watchOS ++simulator build for testing purposes (either x86_64 or ARM64). +diff --git a/Platforms/Apple/testbed/Testbed.lldbinit b/Platforms/Apple/testbed/Testbed.lldbinit +new file mode 100644 +index 00000000000..4cf00dd0f9d +--- /dev/null ++++ b/Platforms/Apple/testbed/Testbed.lldbinit +@@ -0,0 +1,4 @@ ++process handle SIGINT -n true -p true -s false ++process handle SIGUSR1 -n true -p true -s false ++process handle SIGUSR2 -n true -p true -s false ++process handle SIGXFSZ -n true -p true -s false +diff --git a/Platforms/Apple/testbed/TestbedTests/TestbedTests.m b/Platforms/Apple/testbed/TestbedTests/TestbedTests.m +new file mode 100644 +index 00000000000..cc0d9224042 +--- /dev/null ++++ b/Platforms/Apple/testbed/TestbedTests/TestbedTests.m +@@ -0,0 +1,198 @@ ++#import ++#import ++ ++@interface TestbedTests : XCTestCase ++ ++@end ++ ++@implementation TestbedTests ++ ++ ++- (void)testPython { ++ const char **argv; ++ int exit_code; ++ int failed; ++ PyStatus status; ++ PyPreConfig preconfig; ++ PyConfig config; ++ PyObject *app_packages_path; ++ PyObject *method_args; ++ PyObject *result; ++ PyObject *site_module; ++ PyObject *site_addsitedir_attr; ++ PyObject *sys_module; ++ PyObject *sys_path_attr; ++ NSArray *test_args; ++ NSString *python_home; ++ NSString *path; ++ wchar_t *wtmp_str; ++ ++ NSString *resourcePath = [[NSBundle mainBundle] resourcePath]; ++ ++ // Set some other common environment indicators to disable color, as the ++ // Xcode log can't display color. Stdout will report that it is *not* a ++ // TTY. ++ setenv("NO_COLOR", "1", true); ++ setenv("PYTHON_COLORS", "0", true); ++ ++ if (getenv("GITHUB_ACTIONS")) { ++ NSLog(@"Running in a GitHub Actions environment"); ++ } ++ // Arguments to pass into the test suite runner. ++ // argv[0] must identify the process; any subsequent arg ++ // will be handled as if it were an argument to `python -m test` ++ // The processInfo arguments contain the binary that is running, ++ // followed by the arguments defined in the test plan. This means: ++ // run_module = test_args[1] ++ // argv = ["Testbed"] + test_args[2:] ++ test_args = [[NSProcessInfo processInfo] arguments]; ++ if (test_args == NULL) { ++ NSLog(@"Unable to identify test arguments."); ++ } ++ NSLog(@"Test arguments: %@", test_args); ++ argv = malloc(sizeof(char *) * ([test_args count] - 1)); ++ argv[0] = "Testbed"; ++ for (int i = 1; i < [test_args count] - 1; i++) { ++ argv[i] = [[test_args objectAtIndex:i+1] UTF8String]; ++ } ++ ++ // Generate an isolated Python configuration. ++ NSLog(@"Configuring isolated Python..."); ++ PyPreConfig_InitIsolatedConfig(&preconfig); ++ PyConfig_InitIsolatedConfig(&config); ++ ++ // Configure the Python interpreter: ++ // Enforce UTF-8 encoding for stderr, stdout, file-system encoding and locale. ++ // See https://docs.python.org/3/library/os.html#python-utf-8-mode. ++ preconfig.utf8_mode = 1; ++ // Don't buffer stdio. We want output to appears in the log immediately ++ config.buffered_stdio = 0; ++ // Don't write bytecode; we can't modify the app bundle ++ // after it has been signed. ++ config.write_bytecode = 0; ++ // Ensure that signal handlers are installed ++ config.install_signal_handlers = 1; ++ // Run the test module. ++ config.run_module = Py_DecodeLocale([[test_args objectAtIndex:1] UTF8String], NULL); ++ // For debugging - enable verbose mode. ++ // config.verbose = 1; ++ ++ NSLog(@"Pre-initializing Python runtime..."); ++ status = Py_PreInitialize(&preconfig); ++ if (PyStatus_Exception(status)) { ++ XCTFail(@"Unable to pre-initialize Python interpreter: %s", status.err_msg); ++ PyConfig_Clear(&config); ++ return; ++ } ++ ++ // Set the home for the Python interpreter ++ python_home = [NSString stringWithFormat:@"%@/python", resourcePath, nil]; ++ NSLog(@"PythonHome: %@", python_home); ++ wtmp_str = Py_DecodeLocale([python_home UTF8String], NULL); ++ status = PyConfig_SetString(&config, &config.home, wtmp_str); ++ if (PyStatus_Exception(status)) { ++ XCTFail(@"Unable to set PYTHONHOME: %s", status.err_msg); ++ PyConfig_Clear(&config); ++ return; ++ } ++ PyMem_RawFree(wtmp_str); ++ ++ // Read the site config ++ status = PyConfig_Read(&config); ++ if (PyStatus_Exception(status)) { ++ XCTFail(@"Unable to read site config: %s", status.err_msg); ++ PyConfig_Clear(&config); ++ return; ++ } ++ ++ NSLog(@"Configure argc/argv..."); ++ status = PyConfig_SetBytesArgv(&config, [test_args count] - 1, (char**) argv); ++ if (PyStatus_Exception(status)) { ++ XCTFail(@"Unable to configure argc/argv: %s", status.err_msg); ++ PyConfig_Clear(&config); ++ return; ++ } ++ ++ NSLog(@"Initializing Python runtime..."); ++ status = Py_InitializeFromConfig(&config); ++ if (PyStatus_Exception(status)) { ++ XCTFail(@"Unable to initialize Python interpreter: %s", status.err_msg); ++ PyConfig_Clear(&config); ++ return; ++ } ++ ++ // Add app_packages as a site directory. This both adds to sys.path, ++ // and ensures that any .pth files in that directory will be executed. ++ site_module = PyImport_ImportModule("site"); ++ if (site_module == NULL) { ++ XCTFail(@"Could not import site module"); ++ return; ++ } ++ ++ site_addsitedir_attr = PyObject_GetAttrString(site_module, "addsitedir"); ++ if (site_addsitedir_attr == NULL || !PyCallable_Check(site_addsitedir_attr)) { ++ XCTFail(@"Could not access site.addsitedir"); ++ return; ++ } ++ ++ path = [NSString stringWithFormat:@"%@/app_packages", resourcePath, nil]; ++ NSLog(@"App packages path: %@", path); ++ wtmp_str = Py_DecodeLocale([path UTF8String], NULL); ++ app_packages_path = PyUnicode_FromWideChar(wtmp_str, wcslen(wtmp_str)); ++ if (app_packages_path == NULL) { ++ XCTFail(@"Could not convert app_packages path to unicode"); ++ return; ++ } ++ PyMem_RawFree(wtmp_str); ++ ++ method_args = Py_BuildValue("(O)", app_packages_path); ++ if (method_args == NULL) { ++ XCTFail(@"Could not create arguments for site.addsitedir"); ++ return; ++ } ++ ++ result = PyObject_CallObject(site_addsitedir_attr, method_args); ++ if (result == NULL) { ++ XCTFail(@"Could not add app_packages directory using site.addsitedir"); ++ return; ++ } ++ ++ // Add test code to sys.path ++ sys_module = PyImport_ImportModule("sys"); ++ if (sys_module == NULL) { ++ XCTFail(@"Could not import sys module"); ++ return; ++ } ++ ++ sys_path_attr = PyObject_GetAttrString(sys_module, "path"); ++ if (sys_path_attr == NULL) { ++ XCTFail(@"Could not access sys.path"); ++ return; ++ } ++ ++ path = [NSString stringWithFormat:@"%@/app", resourcePath, nil]; ++ NSLog(@"App path: %@", path); ++ wtmp_str = Py_DecodeLocale([path UTF8String], NULL); ++ failed = PyList_Insert(sys_path_attr, 0, PyUnicode_FromString([path UTF8String])); ++ if (failed) { ++ XCTFail(@"Unable to add app to sys.path"); ++ return; ++ } ++ PyMem_RawFree(wtmp_str); ++ ++ // Ensure the working directory is the app folder. ++ chdir([path UTF8String]); ++ ++ // Start the test suite. Print a separator to differentiate Python startup logs from app logs ++ NSLog(@"---------------------------------------------------------------------------"); ++ ++ exit_code = Py_RunMain(); ++ XCTAssertEqual(exit_code, 0, @"Test suite did not pass"); ++ ++ NSLog(@"---------------------------------------------------------------------------"); ++ ++ Py_Finalize(); ++} ++ ++ ++@end +diff --git a/Platforms/Apple/testbed/__main__.py b/Platforms/Apple/testbed/__main__.py +new file mode 100644 +index 00000000000..b7f53a8dd78 +--- /dev/null ++++ b/Platforms/Apple/testbed/__main__.py +@@ -0,0 +1,471 @@ ++import argparse ++import json ++import os ++import re ++import shlex ++import shutil ++import subprocess ++import sys ++from pathlib import Path ++ ++TEST_SLICES = { ++ "iOS": "ios-arm64_x86_64-simulator", ++ "tvOS": "tvos-arm64_x86_64-simulator", ++ "watchOS": "watchos-arm64_x86_64-simulator", ++} ++ ++DECODE_ARGS = ("UTF-8", "backslashreplace") ++ ++# The system log prefixes each line: ++# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ... ++# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ... ++ ++LOG_PREFIX_REGEX = re.compile( ++ r"^\d{4}-\d{2}-\d{2}" # YYYY-MM-DD ++ r"\s+\d+:\d{2}:\d{2}\.\d+\+\d{4}" # HH:MM:SS.ssssss+ZZZZ ++ r"\s+.*Testbed\[\d+:\w+\] " # Process/thread ID ++) ++ ++ ++# Select a simulator device to use. ++def select_simulator_device(platform): ++ # List the testing simulators, in JSON format ++ raw_json = subprocess.check_output(["xcrun", "simctl", "list", "-j"]) ++ json_data = json.loads(raw_json) ++ ++ if platform == "iOS": ++ # Any iOS device will do; we'll look for "SE" devices - but the name ++ # isn't consistent over time. Older Xcode versions will use "iPhone SE ++ # (Nth generation)"; As of 2025, they've started using "iPhone 16e". ++ # ++ # When Xcode is updated after a new release, new devices will be ++ # available and old ones will be dropped from the set available on the ++ # latest iOS version. Select the one with the highest minimum runtime ++ # version - this is an indicator of the "newest" released device, which ++ # should always be supported on the "most recent" iOS version. ++ se_simulators = sorted( ++ (devicetype["minRuntimeVersion"], devicetype["name"]) ++ for devicetype in json_data["devicetypes"] ++ if devicetype["productFamily"] == "iPhone" ++ and ( ++ ( ++ "iPhone " in devicetype["name"] ++ and devicetype["name"].endswith("e") ++ ) ++ or "iPhone SE " in devicetype["name"] ++ ) ++ ) ++ simulator = se_simulators[-1][1] ++ elif platform == "tvOS": ++ # Find the most recent tvOS release. ++ simulators = sorted( ++ (devicetype["minRuntimeVersion"], devicetype["name"]) ++ for devicetype in json_data["devicetypes"] ++ if devicetype["productFamily"] == "Apple TV" ++ ) ++ simulator = simulators[-1][1] ++ elif platform == "watchOS": ++ # Any watchOS device will do; we'll look for "SE" devices. 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" watchOS version. ++ se_simulators = sorted( ++ (devicetype["minRuntimeVersion"], devicetype["name"]) ++ for devicetype in json_data["devicetypes"] ++ if devicetype["productFamily"] == "Apple Watch" ++ and "Apple Watch SE" in devicetype["name"] ++ ) ++ simulator = se_simulators[-1][1] ++ else: ++ raise ValueError(f"Unknown platform {platform}") ++ ++ return simulator ++ ++ ++def xcode_test(location: Path, platform: str, simulator: str, verbose: bool): ++ # Build and run the test suite on the named simulator. ++ args = [ ++ "-project", ++ str(location / f"{platform}Testbed.xcodeproj"), ++ "-scheme", ++ f"{platform}Testbed", ++ "-destination", ++ f"platform={platform} Simulator,name={simulator}", ++ "-derivedDataPath", ++ str(location / "DerivedData"), ++ ] ++ verbosity_args = [] if verbose else ["-quiet"] ++ ++ print("Building test project...") ++ subprocess.run( ++ ["xcodebuild", "build-for-testing"] + args + verbosity_args, ++ check=True, ++ ) ++ ++ # Any environment variable prefixed with TEST_RUNNER_ is exposed into the ++ # test runner environment. There are some variables (like those identifying ++ # CI platforms) that can be useful to have access to. ++ test_env = os.environ.copy() ++ if "GITHUB_ACTIONS" in os.environ: ++ test_env["TEST_RUNNER_GITHUB_ACTIONS"] = os.environ["GITHUB_ACTIONS"] ++ ++ print("Running test project...") ++ # Test execution *can't* be run -quiet; verbose mode is how we see the ++ # output of the test output. However, we don't need test diagnostics - ++ # they're large, and the context they contain generally isn't helpful for ++ # CPython tests. ++ process = subprocess.Popen( ++ [ ++ "xcodebuild", ++ "test-without-building", ++ "-collect-test-diagnostics", ++ "never", ++ ] ++ + args, ++ stdout=subprocess.PIPE, ++ stderr=subprocess.STDOUT, ++ env=test_env, ++ ) ++ while line := (process.stdout.readline()).decode(*DECODE_ARGS): ++ # Strip the timestamp/process prefix from each log line ++ line = LOG_PREFIX_REGEX.sub("", line) ++ sys.stdout.write(line) ++ sys.stdout.flush() ++ ++ status = process.wait(timeout=5) ++ exit(status) ++ ++ ++def copy(src, tgt): ++ """An all-purpose copy. ++ ++ If src is a file, it is copied. If src is a symlink, it is copied *as a ++ symlink*. If src is a directory, the full tree is duplicated, with symlinks ++ being preserved. ++ """ ++ if src.is_file() or src.is_symlink(): ++ shutil.copyfile(src, tgt, follow_symlinks=False) ++ else: ++ shutil.copytree(src, tgt, symlinks=True) ++ ++ ++def clone_testbed( ++ source: Path, ++ target: Path, ++ framework: Path, ++ platform: str, ++ apps: list[Path], ++) -> None: ++ if target.exists(): ++ print(f"{target} already exists; aborting without creating project.") ++ sys.exit(10) ++ ++ if framework is None: ++ if not ( ++ source / "Python.xcframework" / TEST_SLICES[platform] / "bin" ++ ).is_dir(): ++ print( ++ f"The testbed being cloned ({source}) does not contain " ++ "a framework with slices. Re-run with --framework" ++ ) ++ sys.exit(11) ++ else: ++ if not framework.is_dir(): ++ print(f"{framework} does not exist.") ++ sys.exit(12) ++ elif not ( ++ framework.suffix == ".xcframework" ++ or (framework / "Python.framework").is_dir() ++ ): ++ print( ++ f"{framework} is not an XCframework, " ++ f"or a simulator slice of a framework build." ++ ) ++ sys.exit(13) ++ ++ print("Cloning testbed project:") ++ print(f" Cloning {source}...", end="") ++ # Only copy the files for the platform being cloned plus the files common ++ # to all platforms. The XCframework will be copied later, if needed. ++ target.mkdir(parents=True) ++ ++ for name in [ ++ "__main__.py", ++ "TestbedTests", ++ "Testbed.lldbinit", ++ f"{platform}Testbed", ++ f"{platform}Testbed.xcodeproj", ++ f"{platform}Testbed.xctestplan", ++ ]: ++ copy(source / name, target / name) ++ ++ print(" done") ++ ++ orig_xc_framework_path = source / "Python.xcframework" ++ xc_framework_path = target / "Python.xcframework" ++ test_framework_path = xc_framework_path / TEST_SLICES[platform] ++ if framework is not None: ++ if framework.suffix == ".xcframework": ++ print(" Installing XCFramework...", end="") ++ xc_framework_path.symlink_to( ++ framework.relative_to(xc_framework_path.parent, walk_up=True) ++ ) ++ print(" done") ++ else: ++ print(" Installing simulator framework...", end="") ++ # We're only installing a slice of a framework; we need ++ # to do a full tree copy to make sure we don't damage ++ # symlinked content. ++ shutil.copytree(orig_xc_framework_path, xc_framework_path) ++ if test_framework_path.is_dir(): ++ shutil.rmtree(test_framework_path) ++ else: ++ test_framework_path.unlink(missing_ok=True) ++ test_framework_path.symlink_to( ++ framework.relative_to(test_framework_path.parent, walk_up=True) ++ ) ++ print(" done") ++ else: ++ copy(orig_xc_framework_path, xc_framework_path) ++ ++ if ( ++ xc_framework_path.is_symlink() ++ and not xc_framework_path.readlink().is_absolute() ++ ): ++ # XCFramework is a relative symlink. Rewrite the symlink relative ++ # to the new location. ++ print(" Rewriting symlink to XCframework...", end="") ++ resolved_xc_framework_path = ( ++ source / xc_framework_path.readlink() ++ ).resolve() ++ xc_framework_path.unlink() ++ xc_framework_path.symlink_to( ++ resolved_xc_framework_path.relative_to( ++ xc_framework_path.parent, walk_up=True ++ ) ++ ) ++ print(" done") ++ elif ( ++ test_framework_path.is_symlink() ++ and not test_framework_path.readlink().is_absolute() ++ ): ++ print(" Rewriting symlink to simulator framework...", end="") ++ # Simulator framework is a relative symlink. Rewrite the symlink ++ # relative to the new location. ++ orig_test_framework_path = ( ++ source / "Python.XCframework" / test_framework_path.readlink() ++ ).resolve() ++ test_framework_path.unlink() ++ test_framework_path.symlink_to( ++ orig_test_framework_path.relative_to( ++ test_framework_path.parent, walk_up=True ++ ) ++ ) ++ print(" done") ++ else: ++ print(" Using pre-existing Python framework.") ++ ++ for app_src in apps: ++ print(f" Installing app {app_src.name!r}...", end="") ++ app_target = target / f"Testbed/app/{app_src.name}" ++ if app_target.is_dir(): ++ shutil.rmtree(app_target) ++ shutil.copytree(app_src, app_target) ++ print(" done") ++ ++ print(f"Successfully cloned testbed: {target.resolve()}") ++ ++ ++def update_test_plan(testbed_path, platform, args): ++ # Modify the test plan to use the requested test arguments. ++ test_plan_path = testbed_path / f"{platform}Testbed.xctestplan" ++ with test_plan_path.open("r", encoding="utf-8") as f: ++ test_plan = json.load(f) ++ ++ test_plan["defaultOptions"]["commandLineArgumentEntries"] = [ ++ {"argument": shlex.quote(arg)} for arg in args ++ ] ++ ++ with test_plan_path.open("w", encoding="utf-8") as f: ++ json.dump(test_plan, f, indent=2) ++ ++ ++def run_testbed( ++ platform: str, ++ simulator: str | None, ++ args: list[str], ++ verbose: bool = False, ++): ++ location = Path(__file__).parent ++ print("Updating test plan...", end="") ++ update_test_plan(location, platform, args) ++ print(" done.") ++ ++ # xcodebuild doesn't guarantee that the CoreSimulatorService daemon is ++ # running prior to using a simulator, but calling `xcrun simctl list` does. ++ # Determining the default simulator that *would* be used is a cheap action; ++ # so use that as a way to ensure that the CoreSimulatorService daemon is ++ # running. ++ default_simulator = select_simulator_device(platform) ++ if simulator is None: ++ simulator = default_simulator ++ print(f"Running test on {simulator}") ++ ++ xcode_test( ++ location, ++ platform=platform, ++ simulator=simulator, ++ verbose=verbose, ++ ) ++ ++ ++def main(): ++ # Look for directories like `iOSTestbed` as an indicator of the platforms ++ # that the testbed folder supports. The original source testbed can support ++ # many platforms, but when cloned, only one platform is preserved. ++ available_platforms = [ ++ platform ++ for platform in ["iOS", "tvOS", "watchOS"] ++ if (Path(__file__).parent / f"{platform}Testbed").is_dir() ++ ] ++ ++ parser = argparse.ArgumentParser( ++ description=( ++ "Manages the process of testing an Apple Python project " ++ "through Xcode." ++ ), ++ ) ++ ++ subcommands = parser.add_subparsers(dest="subcommand") ++ clone = subcommands.add_parser( ++ "clone", ++ description=( ++ "Clone the testbed project, copying in a Python framework and" ++ "any specified application code." ++ ), ++ help="Clone a testbed project to a new location.", ++ ) ++ clone.add_argument( ++ "--framework", ++ help=( ++ "The location of the XCFramework (or simulator-only slice of an " ++ "XCFramework) to use when running the testbed" ++ ), ++ ) ++ clone.add_argument( ++ "--platform", ++ dest="platform", ++ choices=available_platforms, ++ default=available_platforms[0], ++ help=f"The platform to target (default: {available_platforms[0]})", ++ ) ++ clone.add_argument( ++ "--app", ++ dest="apps", ++ action="append", ++ default=[], ++ help="The location of any code to include in the testbed project", ++ ) ++ clone.add_argument( ++ "location", ++ help="The path where the testbed will be cloned.", ++ ) ++ ++ run = subcommands.add_parser( ++ "run", ++ usage=( ++ "%(prog)s [-h] [--simulator SIMULATOR] -- " ++ " [ ...]" ++ ), ++ description=( ++ "Run a testbed project. The arguments provided after `--` will be " ++ "passed to the running test process as if they were arguments to " ++ "`python -m`." ++ ), ++ help="Run a testbed project", ++ ) ++ run.add_argument( ++ "--platform", ++ dest="platform", ++ choices=available_platforms, ++ default=available_platforms[0], ++ help=f"The platform to target (default: {available_platforms[0]})", ++ ) ++ run.add_argument( ++ "--simulator", ++ help=( ++ "The name of the simulator to use (eg: 'iPhone 16e'). Defaults to " ++ "the most recently released 'entry level' iPhone device. Device " ++ "architecture and OS version can also be specified; e.g., " ++ "`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would run on " ++ "an ARM64 iPhone 16 Pro simulator running iOS 26.0." ++ ), ++ ) ++ run.add_argument( ++ "-v", ++ "--verbose", ++ action="store_true", ++ help="Enable verbose output", ++ ) ++ ++ try: ++ pos = sys.argv.index("--") ++ testbed_args = sys.argv[1:pos] ++ test_args = sys.argv[pos + 1 :] ++ except ValueError: ++ testbed_args = sys.argv[1:] ++ test_args = [] ++ ++ context = parser.parse_args(testbed_args) ++ ++ if context.subcommand == "clone": ++ clone_testbed( ++ source=Path(__file__).parent.resolve(), ++ target=Path(context.location).resolve(), ++ framework=Path(context.framework).resolve() ++ if context.framework ++ else None, ++ platform=context.platform, ++ apps=[Path(app) for app in context.apps], ++ ) ++ elif context.subcommand == "run": ++ if test_args: ++ if not ( ++ Path(__file__).parent ++ / "Python.xcframework" ++ / TEST_SLICES[context.platform] ++ / "bin" ++ ).is_dir(): ++ print( ++ "Testbed does not contain a compiled Python framework. " ++ f"Use `python {sys.argv[0]} clone ...` to create a " ++ "runnable clone of this testbed." ++ ) ++ sys.exit(20) ++ ++ run_testbed( ++ platform=context.platform, ++ simulator=context.simulator, ++ verbose=context.verbose, ++ args=test_args, ++ ) ++ else: ++ print( ++ "Must specify test arguments " ++ f"(e.g., {sys.argv[0]} run -- test)" ++ ) ++ print() ++ parser.print_help(sys.stderr) ++ sys.exit(21) ++ else: ++ parser.print_help(sys.stderr) ++ sys.exit(1) ++ ++ ++if __name__ == "__main__": ++ # Under the buildbot, stdout is not a TTY, but we must still flush after ++ # every line to make sure our output appears in the correct order relative ++ # to the output of our subprocesses. ++ for stream in [sys.stdout, sys.stderr]: ++ stream.reconfigure(line_buffering=True) ++ main() +diff --git a/Platforms/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj b/Platforms/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj +new file mode 100644 +index 00000000000..a0dd6851dc1 +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj +@@ -0,0 +1,534 @@ ++// !$*UTF8*$! ++{ ++ archiveVersion = 1; ++ classes = { ++ }; ++ objectVersion = 77; ++ objects = { ++ ++/* Begin PBXBuildFile section */ ++ 607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; ++ 607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; ++ 607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; ++ 607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; ++/* End PBXBuildFile section */ ++ ++/* Begin PBXContainerItemProxy section */ ++ 607A662E2B0EFA3A0010BFC8 /* PBXContainerItemProxy */ = { ++ isa = PBXContainerItemProxy; ++ containerPortal = 607A660A2B0EFA380010BFC8 /* Project object */; ++ proxyType = 1; ++ remoteGlobalIDString = 607A66112B0EFA380010BFC8; ++ remoteInfo = iOSTestbed; ++ }; ++/* End PBXContainerItemProxy section */ ++ ++/* Begin PBXCopyFilesBuildPhase section */ ++ 607A664E2B0EFC080010BFC8 /* Embed Frameworks */ = { ++ isa = PBXCopyFilesBuildPhase; ++ buildActionMask = 2147483647; ++ dstPath = ""; ++ dstSubfolderSpec = 10; ++ files = ( ++ 607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */, ++ ); ++ name = "Embed Frameworks"; ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */ = { ++ isa = PBXCopyFilesBuildPhase; ++ buildActionMask = 2147483647; ++ dstPath = ""; ++ dstSubfolderSpec = 10; ++ files = ( ++ 607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */, ++ ); ++ name = "Embed Frameworks"; ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXCopyFilesBuildPhase section */ ++ ++/* Begin PBXFileReference section */ ++ 607A66122B0EFA380010BFC8 /* iOSTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; }; ++ 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSTestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; ++ 607A664A2B0EFB310010BFC8 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; ++ 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = iOSTestbed.xctestplan; sourceTree = ""; }; ++/* End PBXFileReference section */ ++ ++/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ ++ 691FECCB8963969DDBED592E /* Exceptions for "iOSTestbed" folder in "iOSTestbed" target */ = { ++ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; ++ membershipExceptions = ( ++ "iOSTestbed-Info.plist", ++ ); ++ target = 607A66112B0EFA380010BFC8 /* iOSTestbed */; ++ }; ++/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ ++ ++/* Begin PBXFileSystemSynchronizedRootGroup section */ ++ 607A66142B0EFA380010BFC8 /* iOSTestbed */ = { ++ isa = PBXFileSystemSynchronizedRootGroup; ++ exceptions = ( ++ 691FECCB8963969DDBED592E /* Exceptions for "iOSTestbed" folder in "iOSTestbed" target */, ++ ); ++ explicitFolders = ( ++ app, ++ app_packages, ++ ); ++ path = iOSTestbed; ++ sourceTree = ""; ++ }; ++ 607A66302B0EFA3A0010BFC8 /* TestbedTests */ = { ++ isa = PBXFileSystemSynchronizedRootGroup; ++ path = TestbedTests; ++ sourceTree = ""; ++ }; ++/* End PBXFileSystemSynchronizedRootGroup section */ ++ ++/* Begin PBXFrameworksBuildPhase section */ ++ 607A660F2B0EFA380010BFC8 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A662A2B0EFA3A0010BFC8 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXFrameworksBuildPhase section */ ++ ++/* Begin PBXGroup section */ ++ 607A66092B0EFA380010BFC8 = { ++ isa = PBXGroup; ++ children = ( ++ 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */, ++ 607A664A2B0EFB310010BFC8 /* Python.xcframework */, ++ 607A66142B0EFA380010BFC8 /* iOSTestbed */, ++ 607A66302B0EFA3A0010BFC8 /* TestbedTests */, ++ 607A66132B0EFA380010BFC8 /* Products */, ++ ); ++ sourceTree = ""; ++ }; ++ 607A66132B0EFA380010BFC8 /* Products */ = { ++ isa = PBXGroup; ++ children = ( ++ 607A66122B0EFA380010BFC8 /* iOSTestbed.app */, ++ 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */, ++ ); ++ name = Products; ++ sourceTree = ""; ++ }; ++/* End PBXGroup section */ ++ ++/* Begin PBXNativeTarget section */ ++ 607A66112B0EFA380010BFC8 /* iOSTestbed */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbed" */; ++ buildPhases = ( ++ 607A660E2B0EFA380010BFC8 /* Sources */, ++ 607A660F2B0EFA380010BFC8 /* Frameworks */, ++ 607A66102B0EFA380010BFC8 /* Resources */, ++ 607A66552B0F061D0010BFC8 /* Process Python libraries */, ++ 607A664E2B0EFC080010BFC8 /* Embed Frameworks */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ ); ++ fileSystemSynchronizedGroups = ( ++ 607A66142B0EFA380010BFC8 /* iOSTestbed */, ++ ); ++ name = iOSTestbed; ++ packageProductDependencies = ( ++ ); ++ productName = iOSTestbed; ++ productReference = 607A66122B0EFA380010BFC8 /* iOSTestbed.app */; ++ productType = "com.apple.product-type.application"; ++ }; ++ 607A662C2B0EFA3A0010BFC8 /* iOSTestbedTests */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbedTests" */; ++ buildPhases = ( ++ 607A66292B0EFA3A0010BFC8 /* Sources */, ++ 607A662A2B0EFA3A0010BFC8 /* Frameworks */, ++ 607A662B2B0EFA3A0010BFC8 /* Resources */, ++ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */, ++ ); ++ fileSystemSynchronizedGroups = ( ++ 607A66302B0EFA3A0010BFC8 /* TestbedTests */, ++ ); ++ name = iOSTestbedTests; ++ packageProductDependencies = ( ++ ); ++ productName = iOSTestbedTests; ++ productReference = 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */; ++ productType = "com.apple.product-type.bundle.unit-test"; ++ }; ++/* End PBXNativeTarget section */ ++ ++/* Begin PBXProject section */ ++ 607A660A2B0EFA380010BFC8 /* Project object */ = { ++ isa = PBXProject; ++ attributes = { ++ BuildIndependentTargetsInParallel = 1; ++ LastUpgradeCheck = 1500; ++ TargetAttributes = { ++ 607A66112B0EFA380010BFC8 = { ++ CreatedOnToolsVersion = 15.0.1; ++ }; ++ 607A662C2B0EFA3A0010BFC8 = { ++ CreatedOnToolsVersion = 15.0.1; ++ TestTargetID = 607A66112B0EFA380010BFC8; ++ }; ++ }; ++ }; ++ buildConfigurationList = 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "iOSTestbed" */; ++ compatibilityVersion = "Xcode 14.0"; ++ developmentRegion = en; ++ hasScannedForEncodings = 0; ++ knownRegions = ( ++ en, ++ Base, ++ ); ++ mainGroup = 607A66092B0EFA380010BFC8; ++ minimizedProjectReferenceProxies = 1; ++ preferredProjectObjectVersion = 77; ++ productRefGroup = 607A66132B0EFA380010BFC8 /* Products */; ++ projectDirPath = ""; ++ projectRoot = ""; ++ targets = ( ++ 607A66112B0EFA380010BFC8 /* iOSTestbed */, ++ 607A662C2B0EFA3A0010BFC8 /* iOSTestbedTests */, ++ ); ++ }; ++/* End PBXProject section */ ++ ++/* Begin PBXResourcesBuildPhase section */ ++ 607A66102B0EFA380010BFC8 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A662B2B0EFA3A0010BFC8 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXResourcesBuildPhase section */ ++ ++/* Begin PBXShellScriptBuildPhase section */ ++ 607A66552B0F061D0010BFC8 /* Process Python libraries */ = { ++ isa = PBXShellScriptBuildPhase; ++ alwaysOutOfDate = 1; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ inputFileListPaths = ( ++ ); ++ inputPaths = ( ++ ); ++ name = "Process Python libraries"; ++ outputFileListPaths = ( ++ ); ++ outputPaths = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ shellPath = /bin/sh; ++ shellScript = "set -e\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\ninstall_python Python.xcframework app app_packages\n"; ++ showEnvVarsInLog = 0; ++ }; ++/* End PBXShellScriptBuildPhase section */ ++ ++/* Begin PBXSourcesBuildPhase section */ ++ 607A660E2B0EFA380010BFC8 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A66292B0EFA3A0010BFC8 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXSourcesBuildPhase section */ ++ ++/* Begin PBXTargetDependency section */ ++ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */ = { ++ isa = PBXTargetDependency; ++ target = 607A66112B0EFA380010BFC8 /* iOSTestbed */; ++ targetProxy = 607A662E2B0EFA3A0010BFC8 /* PBXContainerItemProxy */; ++ }; ++/* End PBXTargetDependency section */ ++ ++/* Begin XCBuildConfiguration section */ ++ 607A663F2B0EFA3A0010BFC8 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ALWAYS_SEARCH_USER_PATHS = NO; ++ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ++ CLANG_ANALYZER_NONNULL = YES; ++ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; ++ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; ++ CLANG_ENABLE_MODULES = YES; ++ CLANG_ENABLE_OBJC_ARC = YES; ++ CLANG_ENABLE_OBJC_WEAK = YES; ++ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; ++ CLANG_WARN_BOOL_CONVERSION = YES; ++ CLANG_WARN_COMMA = YES; ++ CLANG_WARN_CONSTANT_CONVERSION = YES; ++ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; ++ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; ++ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; ++ CLANG_WARN_EMPTY_BODY = YES; ++ CLANG_WARN_ENUM_CONVERSION = YES; ++ CLANG_WARN_INFINITE_RECURSION = YES; ++ CLANG_WARN_INT_CONVERSION = YES; ++ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; ++ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; ++ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; ++ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; ++ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; ++ CLANG_WARN_STRICT_PROTOTYPES = YES; ++ CLANG_WARN_SUSPICIOUS_MOVE = YES; ++ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; ++ CLANG_WARN_UNREACHABLE_CODE = YES; ++ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; ++ COPY_PHASE_STRIP = NO; ++ DEBUG_INFORMATION_FORMAT = dwarf; ++ ENABLE_STRICT_OBJC_MSGSEND = YES; ++ ENABLE_TESTABILITY = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = YES; ++ GCC_C_LANGUAGE_STANDARD = gnu17; ++ GCC_DYNAMIC_NO_PIC = NO; ++ GCC_NO_COMMON_BLOCKS = YES; ++ GCC_OPTIMIZATION_LEVEL = 0; ++ GCC_PREPROCESSOR_DEFINITIONS = ( ++ "DEBUG=1", ++ "$(inherited)", ++ ); ++ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; ++ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; ++ GCC_WARN_UNDECLARED_SELECTOR = YES; ++ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; ++ GCC_WARN_UNUSED_FUNCTION = YES; ++ GCC_WARN_UNUSED_VARIABLE = YES; ++ IPHONEOS_DEPLOYMENT_TARGET = 15.0; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; ++ MTL_FAST_MATH = YES; ++ ONLY_ACTIVE_ARCH = YES; ++ SDKROOT = iphoneos; ++ }; ++ name = Debug; ++ }; ++ 607A66402B0EFA3A0010BFC8 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ALWAYS_SEARCH_USER_PATHS = NO; ++ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ++ CLANG_ANALYZER_NONNULL = YES; ++ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; ++ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; ++ CLANG_ENABLE_MODULES = YES; ++ CLANG_ENABLE_OBJC_ARC = YES; ++ CLANG_ENABLE_OBJC_WEAK = YES; ++ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; ++ CLANG_WARN_BOOL_CONVERSION = YES; ++ CLANG_WARN_COMMA = YES; ++ CLANG_WARN_CONSTANT_CONVERSION = YES; ++ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; ++ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; ++ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; ++ CLANG_WARN_EMPTY_BODY = YES; ++ CLANG_WARN_ENUM_CONVERSION = YES; ++ CLANG_WARN_INFINITE_RECURSION = YES; ++ CLANG_WARN_INT_CONVERSION = YES; ++ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; ++ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; ++ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; ++ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; ++ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; ++ CLANG_WARN_STRICT_PROTOTYPES = YES; ++ CLANG_WARN_SUSPICIOUS_MOVE = YES; ++ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; ++ CLANG_WARN_UNREACHABLE_CODE = YES; ++ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; ++ COPY_PHASE_STRIP = NO; ++ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ++ ENABLE_NS_ASSERTIONS = NO; ++ ENABLE_STRICT_OBJC_MSGSEND = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = YES; ++ GCC_C_LANGUAGE_STANDARD = gnu17; ++ GCC_NO_COMMON_BLOCKS = YES; ++ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; ++ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; ++ GCC_WARN_UNDECLARED_SELECTOR = YES; ++ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; ++ GCC_WARN_UNUSED_FUNCTION = YES; ++ GCC_WARN_UNUSED_VARIABLE = YES; ++ IPHONEOS_DEPLOYMENT_TARGET = 15.0; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = NO; ++ MTL_FAST_MATH = YES; ++ SDKROOT = iphoneos; ++ VALIDATE_PRODUCT = YES; ++ }; ++ name = Release; ++ }; ++ 607A66422B0EFA3A0010BFC8 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ++ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = ""; ++ ENABLE_USER_SCRIPT_SANDBOXING = NO; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist"; ++ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; ++ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; ++ INFOPLIST_KEY_UIMainStoryboardFile = Main; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ IPHONEOS_DEPLOYMENT_TARGET = 15.0; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 3.13.0a1; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = YES; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ }; ++ name = Debug; ++ }; ++ 607A66432B0EFA3A0010BFC8 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ++ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = ""; ++ ENABLE_TESTABILITY = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = NO; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist"; ++ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; ++ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; ++ INFOPLIST_KEY_UIMainStoryboardFile = Main; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ IPHONEOS_DEPLOYMENT_TARGET = 15.0; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 3.13.0a1; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = YES; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ }; ++ name = Release; ++ }; ++ 607A66452B0EFA3A0010BFC8 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ BUNDLE_LOADER = "$(TEST_HOST)"; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = 3HEZE76D99; ++ GENERATE_INFOPLIST_FILE = YES; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ IPHONEOS_DEPLOYMENT_TARGET = 15.0; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = NO; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSTestbed"; ++ }; ++ name = Debug; ++ }; ++ 607A66462B0EFA3A0010BFC8 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ BUNDLE_LOADER = "$(TEST_HOST)"; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = 3HEZE76D99; ++ GENERATE_INFOPLIST_FILE = YES; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ IPHONEOS_DEPLOYMENT_TARGET = 15.0; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = NO; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSTestbed"; ++ }; ++ name = Release; ++ }; ++/* End XCBuildConfiguration section */ ++ ++/* Begin XCConfigurationList section */ ++ 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "iOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A663F2B0EFA3A0010BFC8 /* Debug */, ++ 607A66402B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A66422B0EFA3A0010BFC8 /* Debug */, ++ 607A66432B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbedTests" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A66452B0EFA3A0010BFC8 /* Debug */, ++ 607A66462B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++/* End XCConfigurationList section */ ++ }; ++ rootObject = 607A660A2B0EFA380010BFC8 /* Project object */; ++} +diff --git a/Platforms/Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme b/Platforms/Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme +new file mode 100644 +index 00000000000..3c330a4152b +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme +@@ -0,0 +1,97 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/Platforms/Apple/testbed/iOSTestbed.xctestplan b/Platforms/Apple/testbed/iOSTestbed.xctestplan +new file mode 100644 +index 00000000000..0c4ab9eb2ba +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed.xctestplan +@@ -0,0 +1,46 @@ ++{ ++ "configurations" : [ ++ { ++ "id" : "F5A95CE4-1ADE-4A6E-A0E1-CDBAE26DF0C5", ++ "name" : "Test Scheme Action", ++ "options" : { ++ ++ } ++ } ++ ], ++ "defaultOptions" : { ++ "commandLineArgumentEntries" : [ ++ { ++ "argument" : "test" ++ }, ++ { ++ "argument" : "-uall" ++ }, ++ { ++ "argument" : "--single-process" ++ }, ++ { ++ "argument" : "--rerun" ++ }, ++ { ++ "argument" : "-W" ++ } ++ ], ++ "targetForVariableExpansion" : { ++ "containerPath" : "container:iOSTestbed.xcodeproj", ++ "identifier" : "607A66112B0EFA380010BFC8", ++ "name" : "iOSTestbed" ++ } ++ }, ++ "testTargets" : [ ++ { ++ "parallelizable" : false, ++ "target" : { ++ "containerPath" : "container:iOSTestbed.xcodeproj", ++ "identifier" : "607A662C2B0EFA3A0010BFC8", ++ "name" : "iOSTestbedTests" ++ } ++ } ++ ], ++ "version" : 1 ++} +diff --git a/Platforms/Apple/testbed/iOSTestbed/AppDelegate.h b/Platforms/Apple/testbed/iOSTestbed/AppDelegate.h +new file mode 100644 +index 00000000000..f695b3b5efc +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/AppDelegate.h +@@ -0,0 +1,11 @@ ++// ++// AppDelegate.h ++// iOSTestbed ++// ++ ++#import ++ ++@interface AppDelegate : UIResponder ++ ++ ++@end +diff --git a/Platforms/Apple/testbed/iOSTestbed/AppDelegate.m b/Platforms/Apple/testbed/iOSTestbed/AppDelegate.m +new file mode 100644 +index 00000000000..fbe71bae23b +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/AppDelegate.m +@@ -0,0 +1,24 @@ ++// ++// AppDelegate.m ++// iOSTestbed ++// ++ ++#import "AppDelegate.h" ++#import "SceneDelegate.h" ++ ++@interface AppDelegate () ++ ++@end ++ ++@implementation AppDelegate ++ ++- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { ++ UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; ++ configuration.delegateClass = [SceneDelegate class]; ++ return configuration; ++} ++ ++- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { ++} ++ ++@end +diff --git a/Platforms/Apple/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json b/Platforms/Apple/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json +new file mode 100644 +index 00000000000..eb878970081 +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json +@@ -0,0 +1,11 @@ ++{ ++ "colors" : [ ++ { ++ "idiom" : "universal" ++ } ++ ], ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +diff --git a/Platforms/Apple/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json b/Platforms/Apple/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json +new file mode 100644 +index 00000000000..13613e3ee1a +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json +@@ -0,0 +1,13 @@ ++{ ++ "images" : [ ++ { ++ "idiom" : "universal", ++ "platform" : "ios", ++ "size" : "1024x1024" ++ } ++ ], ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +diff --git a/Platforms/Apple/testbed/iOSTestbed/Assets.xcassets/Contents.json b/Platforms/Apple/testbed/iOSTestbed/Assets.xcassets/Contents.json +new file mode 100644 +index 00000000000..73c00596a7f +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/Assets.xcassets/Contents.json +@@ -0,0 +1,6 @@ ++{ ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +diff --git a/Platforms/Apple/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard b/Platforms/Apple/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard +new file mode 100644 +index 00000000000..5daafe73a86 +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard +@@ -0,0 +1,9 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/Platforms/Apple/testbed/iOSTestbed/SceneDelegate.h b/Platforms/Apple/testbed/iOSTestbed/SceneDelegate.h +new file mode 100644 +index 00000000000..264aacadca1 +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/SceneDelegate.h +@@ -0,0 +1,12 @@ ++// ++// SceneDelegate.h ++// iOSTestbed ++// ++ ++#import ++ ++@interface SceneDelegate : UIResponder ++ ++@property (strong, nonatomic) UIWindow *window; ++ ++@end +diff --git a/Platforms/Apple/testbed/iOSTestbed/SceneDelegate.m b/Platforms/Apple/testbed/iOSTestbed/SceneDelegate.m +new file mode 100644 +index 00000000000..e82e447985d +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/SceneDelegate.m +@@ -0,0 +1,21 @@ ++// ++// SceneDelegate.m ++// iOSTestbed ++// ++ ++#import "SceneDelegate.h" ++ ++@interface SceneDelegate () ++ ++@end ++ ++@implementation SceneDelegate ++ ++- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { ++ UIWindowScene *windowScene = (UIWindowScene *)scene; ++ self.window = [[UIWindow alloc] initWithWindowScene:windowScene]; ++ self.window.rootViewController = [[UIViewController alloc] init]; ++ [self.window makeKeyAndVisible]; ++} ++ ++@end +diff --git a/Platforms/Apple/testbed/iOSTestbed/app/README b/Platforms/Apple/testbed/iOSTestbed/app/README +new file mode 100644 +index 00000000000..46c0e8e2a29 +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/app/README +@@ -0,0 +1,7 @@ ++This folder can contain any Python application code. ++ ++During the build, any binary modules found in this folder will be processed into ++Framework form. ++ ++When the test suite runs, this folder will be on the PYTHONPATH, and will be the ++working directory for the test suite. +diff --git a/Platforms/Apple/testbed/iOSTestbed/app_packages/README b/Platforms/Apple/testbed/iOSTestbed/app_packages/README +new file mode 100644 +index 00000000000..02c2beccfbd +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/app_packages/README +@@ -0,0 +1,7 @@ ++This folder can be a target for installing any Python dependencies needed by the ++test suite. ++ ++During the build, any binary modules found in this folder will be processed into ++Framework form. ++ ++When the test suite runs, this folder will be on the PYTHONPATH. +diff --git a/Platforms/Apple/testbed/iOSTestbed/iOSTestbed-Info.plist b/Platforms/Apple/testbed/iOSTestbed/iOSTestbed-Info.plist +new file mode 100644 +index 00000000000..3a560656972 +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/iOSTestbed-Info.plist +@@ -0,0 +1,62 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleDisplayName ++ ${PRODUCT_NAME} ++ CFBundleExecutable ++ ${EXECUTABLE_NAME} ++ CFBundleIdentifier ++ org.python.iOSTestbed ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundleName ++ ${PRODUCT_NAME} ++ CFBundlePackageType ++ APPL ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSignature ++ ???? ++ CFBundleVersion ++ 1 ++ LSRequiresIPhoneOS ++ ++ UIRequiresFullScreen ++ ++ UILaunchStoryboardName ++ Launch Screen ++ UISupportedInterfaceOrientations ++ ++ UIInterfaceOrientationPortrait ++ UIInterfaceOrientationLandscapeLeft ++ UIInterfaceOrientationLandscapeRight ++ ++ UISupportedInterfaceOrientations~ipad ++ ++ UIInterfaceOrientationPortrait ++ UIInterfaceOrientationPortraitUpsideDown ++ UIInterfaceOrientationLandscapeLeft ++ UIInterfaceOrientationLandscapeRight ++ ++ UIApplicationSceneManifest ++ ++ UIApplicationSupportsMultipleScenes ++ ++ UISceneConfigurations ++ ++ UIWindowSceneSessionRoleApplication ++ ++ ++ UISceneConfigurationName ++ Default Configuration ++ UISceneDelegateClassName ++ SceneDelegate ++ ++ ++ ++ ++ ++ +diff --git a/Platforms/Apple/testbed/iOSTestbed/main.m b/Platforms/Apple/testbed/iOSTestbed/main.m +new file mode 100644 +index 00000000000..e32bd78c9b4 +--- /dev/null ++++ b/Platforms/Apple/testbed/iOSTestbed/main.m +@@ -0,0 +1,16 @@ ++// ++// main.m ++// iOSTestbed ++// ++ ++#import ++#import "AppDelegate.h" ++ ++int main(int argc, char * argv[]) { ++ NSString * appDelegateClassName; ++ @autoreleasepool { ++ appDelegateClassName = NSStringFromClass([AppDelegate class]); ++ ++ return UIApplicationMain(argc, argv, nil, appDelegateClassName); ++ } ++} +diff --git a/Platforms/Apple/testbed/tvOSTestbed.xcodeproj/project.pbxproj b/Platforms/Apple/testbed/tvOSTestbed.xcodeproj/project.pbxproj +new file mode 100644 +index 00000000000..5ddfba882f4 +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed.xcodeproj/project.pbxproj +@@ -0,0 +1,505 @@ ++// !$*UTF8*$! ++{ ++ archiveVersion = 1; ++ classes = { ++ }; ++ objectVersion = 77; ++ objects = { ++ ++/* Begin PBXBuildFile section */ ++ EE7C8A1E2DCD6FF3003206DB /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE7C8A1C2DCD6FF3003206DB /* Python.xcframework */; }; ++ EE7C8A1F2DCD70CD003206DB /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE7C8A1C2DCD6FF3003206DB /* Python.xcframework */; }; ++ EE7C8A202DCD70CD003206DB /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EE7C8A1C2DCD6FF3003206DB /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; ++/* End PBXBuildFile section */ ++ ++/* Begin PBXContainerItemProxy section */ ++ EE989E662DCD6E7A0036B268 /* PBXContainerItemProxy */ = { ++ isa = PBXContainerItemProxy; ++ containerPortal = EE989E462DCD6E780036B268 /* Project object */; ++ proxyType = 1; ++ remoteGlobalIDString = EE989E4D2DCD6E780036B268; ++ remoteInfo = tvOSTestbed; ++ }; ++/* End PBXContainerItemProxy section */ ++ ++/* Begin PBXCopyFilesBuildPhase section */ ++ EE7C8A212DCD70CD003206DB /* Embed Frameworks */ = { ++ isa = PBXCopyFilesBuildPhase; ++ buildActionMask = 2147483647; ++ dstPath = ""; ++ dstSubfolderSpec = 10; ++ files = ( ++ EE7C8A202DCD70CD003206DB /* Python.xcframework in Embed Frameworks */, ++ ); ++ name = "Embed Frameworks"; ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXCopyFilesBuildPhase section */ ++ ++/* Begin PBXFileReference section */ ++ 6077B3802E82A4BE00E3D6A3 /* tvOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = tvOSTestbed.xctestplan; sourceTree = ""; }; ++ EE7C8A1C2DCD6FF3003206DB /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; ++ EE989E4E2DCD6E780036B268 /* tvOSTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = tvOSTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; }; ++ EE989E652DCD6E7A0036B268 /* TestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; ++/* End PBXFileReference section */ ++ ++/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ ++ 6077B37F2E81892A00E3D6A3 /* Exceptions for "tvOSTestbed" folder in "tvOSTestbed" target */ = { ++ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; ++ membershipExceptions = ( ++ "tvOSTestbed-Info.plist", ++ ); ++ target = EE989E4D2DCD6E780036B268 /* tvOSTestbed */; ++ }; ++/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ ++ ++/* Begin PBXFileSystemSynchronizedRootGroup section */ ++ EE989E502DCD6E780036B268 /* tvOSTestbed */ = { ++ isa = PBXFileSystemSynchronizedRootGroup; ++ exceptions = ( ++ 6077B37F2E81892A00E3D6A3 /* Exceptions for "tvOSTestbed" folder in "tvOSTestbed" target */, ++ ); ++ explicitFolders = ( ++ app, ++ app_packages, ++ ); ++ path = tvOSTestbed; ++ sourceTree = ""; ++ }; ++ EE989E682DCD6E7A0036B268 /* TestbedTests */ = { ++ isa = PBXFileSystemSynchronizedRootGroup; ++ path = TestbedTests; ++ sourceTree = ""; ++ }; ++/* End PBXFileSystemSynchronizedRootGroup section */ ++ ++/* Begin PBXFrameworksBuildPhase section */ ++ EE989E4B2DCD6E780036B268 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ EE7C8A1F2DCD70CD003206DB /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ EE989E622DCD6E7A0036B268 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ EE7C8A1E2DCD6FF3003206DB /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXFrameworksBuildPhase section */ ++ ++/* Begin PBXGroup section */ ++ EE989E452DCD6E780036B268 = { ++ isa = PBXGroup; ++ children = ( ++ 6077B3802E82A4BE00E3D6A3 /* tvOSTestbed.xctestplan */, ++ EE7C8A1C2DCD6FF3003206DB /* Python.xcframework */, ++ EE989E502DCD6E780036B268 /* tvOSTestbed */, ++ EE989E682DCD6E7A0036B268 /* TestbedTests */, ++ EE989E4F2DCD6E780036B268 /* Products */, ++ ); ++ sourceTree = ""; ++ }; ++ EE989E4F2DCD6E780036B268 /* Products */ = { ++ isa = PBXGroup; ++ children = ( ++ EE989E4E2DCD6E780036B268 /* tvOSTestbed.app */, ++ EE989E652DCD6E7A0036B268 /* TestbedTests.xctest */, ++ ); ++ name = Products; ++ sourceTree = ""; ++ }; ++/* End PBXGroup section */ ++ ++/* Begin PBXNativeTarget section */ ++ EE989E4D2DCD6E780036B268 /* tvOSTestbed */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = EE989E792DCD6E7A0036B268 /* Build configuration list for PBXNativeTarget "tvOSTestbed" */; ++ buildPhases = ( ++ EE989E4A2DCD6E780036B268 /* Sources */, ++ EE989E4B2DCD6E780036B268 /* Frameworks */, ++ EE989E4C2DCD6E780036B268 /* Resources */, ++ EE7C8A222DCD70F4003206DB /* Process Python libraries */, ++ EE7C8A212DCD70CD003206DB /* Embed Frameworks */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ ); ++ fileSystemSynchronizedGroups = ( ++ EE989E502DCD6E780036B268 /* tvOSTestbed */, ++ ); ++ name = tvOSTestbed; ++ packageProductDependencies = ( ++ ); ++ productName = tvOSTestbed; ++ productReference = EE989E4E2DCD6E780036B268 /* tvOSTestbed.app */; ++ productType = "com.apple.product-type.application"; ++ }; ++ EE989E642DCD6E7A0036B268 /* TestbedTests */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = EE989E7C2DCD6E7A0036B268 /* Build configuration list for PBXNativeTarget "TestbedTests" */; ++ buildPhases = ( ++ EE989E612DCD6E7A0036B268 /* Sources */, ++ EE989E622DCD6E7A0036B268 /* Frameworks */, ++ EE989E632DCD6E7A0036B268 /* Resources */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ EE989E672DCD6E7A0036B268 /* PBXTargetDependency */, ++ ); ++ fileSystemSynchronizedGroups = ( ++ EE989E682DCD6E7A0036B268 /* TestbedTests */, ++ ); ++ name = TestbedTests; ++ packageProductDependencies = ( ++ ); ++ productName = TestbedTests; ++ productReference = EE989E652DCD6E7A0036B268 /* TestbedTests.xctest */; ++ productType = "com.apple.product-type.bundle.unit-test"; ++ }; ++/* End PBXNativeTarget section */ ++ ++/* Begin PBXProject section */ ++ EE989E462DCD6E780036B268 /* Project object */ = { ++ isa = PBXProject; ++ attributes = { ++ BuildIndependentTargetsInParallel = 1; ++ LastUpgradeCheck = 1620; ++ TargetAttributes = { ++ EE989E4D2DCD6E780036B268 = { ++ CreatedOnToolsVersion = 16.2; ++ }; ++ EE989E642DCD6E7A0036B268 = { ++ CreatedOnToolsVersion = 16.2; ++ TestTargetID = EE989E4D2DCD6E780036B268; ++ }; ++ }; ++ }; ++ buildConfigurationList = EE989E492DCD6E780036B268 /* Build configuration list for PBXProject "tvOSTestbed" */; ++ developmentRegion = en; ++ hasScannedForEncodings = 0; ++ knownRegions = ( ++ en, ++ Base, ++ ); ++ mainGroup = EE989E452DCD6E780036B268; ++ minimizedProjectReferenceProxies = 1; ++ preferredProjectObjectVersion = 77; ++ productRefGroup = EE989E4F2DCD6E780036B268 /* Products */; ++ projectDirPath = ""; ++ projectRoot = ""; ++ targets = ( ++ EE989E4D2DCD6E780036B268 /* tvOSTestbed */, ++ EE989E642DCD6E7A0036B268 /* TestbedTests */, ++ ); ++ }; ++/* End PBXProject section */ ++ ++/* Begin PBXResourcesBuildPhase section */ ++ EE989E4C2DCD6E780036B268 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ EE989E632DCD6E7A0036B268 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXResourcesBuildPhase section */ ++ ++/* Begin PBXShellScriptBuildPhase section */ ++ EE7C8A222DCD70F4003206DB /* Process Python libraries */ = { ++ isa = PBXShellScriptBuildPhase; ++ alwaysOutOfDate = 1; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ inputFileListPaths = ( ++ ); ++ inputPaths = ( ++ ); ++ name = "Process Python libraries"; ++ outputFileListPaths = ( ++ ); ++ outputPaths = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ shellPath = /bin/sh; ++ shellScript = "set -e\n\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\n\ninstall_python Python.xcframework app app_packages\n"; ++ }; ++/* End PBXShellScriptBuildPhase section */ ++ ++/* Begin PBXSourcesBuildPhase section */ ++ EE989E4A2DCD6E780036B268 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ EE989E612DCD6E7A0036B268 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXSourcesBuildPhase section */ ++ ++/* Begin PBXTargetDependency section */ ++ EE989E672DCD6E7A0036B268 /* PBXTargetDependency */ = { ++ isa = PBXTargetDependency; ++ target = EE989E4D2DCD6E780036B268 /* tvOSTestbed */; ++ targetProxy = EE989E662DCD6E7A0036B268 /* PBXContainerItemProxy */; ++ }; ++/* End PBXTargetDependency section */ ++ ++/* Begin XCBuildConfiguration section */ ++ EE989E772DCD6E7A0036B268 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ALWAYS_SEARCH_USER_PATHS = NO; ++ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ++ CLANG_ANALYZER_NONNULL = YES; ++ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; ++ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; ++ CLANG_ENABLE_MODULES = YES; ++ CLANG_ENABLE_OBJC_ARC = YES; ++ CLANG_ENABLE_OBJC_WEAK = YES; ++ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; ++ CLANG_WARN_BOOL_CONVERSION = YES; ++ CLANG_WARN_COMMA = YES; ++ CLANG_WARN_CONSTANT_CONVERSION = YES; ++ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; ++ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; ++ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; ++ CLANG_WARN_EMPTY_BODY = YES; ++ CLANG_WARN_ENUM_CONVERSION = YES; ++ CLANG_WARN_INFINITE_RECURSION = YES; ++ CLANG_WARN_INT_CONVERSION = YES; ++ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; ++ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; ++ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; ++ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; ++ CLANG_WARN_STRICT_PROTOTYPES = YES; ++ CLANG_WARN_SUSPICIOUS_MOVE = YES; ++ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; ++ CLANG_WARN_UNREACHABLE_CODE = YES; ++ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; ++ COPY_PHASE_STRIP = NO; ++ DEBUG_INFORMATION_FORMAT = dwarf; ++ ENABLE_STRICT_OBJC_MSGSEND = YES; ++ ENABLE_TESTABILITY = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = NO; ++ FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)"; ++ GCC_C_LANGUAGE_STANDARD = gnu17; ++ GCC_DYNAMIC_NO_PIC = NO; ++ GCC_NO_COMMON_BLOCKS = YES; ++ GCC_OPTIMIZATION_LEVEL = 0; ++ GCC_PREPROCESSOR_DEFINITIONS = ( ++ "DEBUG=1", ++ "$(inherited)", ++ ); ++ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; ++ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; ++ GCC_WARN_UNDECLARED_SELECTOR = YES; ++ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; ++ GCC_WARN_UNUSED_FUNCTION = YES; ++ GCC_WARN_UNUSED_VARIABLE = YES; ++ HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/Python.framework/Headers"; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; ++ MTL_FAST_MATH = YES; ++ ONLY_ACTIVE_ARCH = YES; ++ SDKROOT = appletvos; ++ TVOS_DEPLOYMENT_TARGET = 18.2; ++ }; ++ name = Debug; ++ }; ++ EE989E782DCD6E7A0036B268 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ALWAYS_SEARCH_USER_PATHS = NO; ++ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ++ CLANG_ANALYZER_NONNULL = YES; ++ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; ++ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; ++ CLANG_ENABLE_MODULES = YES; ++ CLANG_ENABLE_OBJC_ARC = YES; ++ CLANG_ENABLE_OBJC_WEAK = YES; ++ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; ++ CLANG_WARN_BOOL_CONVERSION = YES; ++ CLANG_WARN_COMMA = YES; ++ CLANG_WARN_CONSTANT_CONVERSION = YES; ++ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; ++ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; ++ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; ++ CLANG_WARN_EMPTY_BODY = YES; ++ CLANG_WARN_ENUM_CONVERSION = YES; ++ CLANG_WARN_INFINITE_RECURSION = YES; ++ CLANG_WARN_INT_CONVERSION = YES; ++ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; ++ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; ++ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; ++ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; ++ CLANG_WARN_STRICT_PROTOTYPES = YES; ++ CLANG_WARN_SUSPICIOUS_MOVE = YES; ++ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; ++ CLANG_WARN_UNREACHABLE_CODE = YES; ++ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; ++ COPY_PHASE_STRIP = NO; ++ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ++ ENABLE_NS_ASSERTIONS = NO; ++ ENABLE_STRICT_OBJC_MSGSEND = YES; ++ ENABLE_TESTABILITY = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = NO; ++ FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)"; ++ GCC_C_LANGUAGE_STANDARD = gnu17; ++ GCC_NO_COMMON_BLOCKS = YES; ++ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; ++ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; ++ GCC_WARN_UNDECLARED_SELECTOR = YES; ++ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; ++ GCC_WARN_UNUSED_FUNCTION = YES; ++ GCC_WARN_UNUSED_VARIABLE = YES; ++ HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/Python.framework/Headers"; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = NO; ++ MTL_FAST_MATH = YES; ++ SDKROOT = appletvos; ++ TVOS_DEPLOYMENT_TARGET = 18.2; ++ VALIDATE_PRODUCT = YES; ++ }; ++ name = Release; ++ }; ++ EE989E7A2DCD6E7A0036B268 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ++ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ GENERATE_INFOPLIST_FILE = NO; ++ INFOPLIST_FILE = "tvOSTestbed/tvOSTestbed-Info.plist"; ++ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; ++ INFOPLIST_KEY_UIMainStoryboardFile = Main; ++ INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.tvOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = YES; ++ TARGETED_DEVICE_FAMILY = 3; ++ }; ++ name = Debug; ++ }; ++ EE989E7B2DCD6E7A0036B268 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ++ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ GENERATE_INFOPLIST_FILE = NO; ++ INFOPLIST_FILE = "tvOSTestbed/tvOSTestbed-Info.plist"; ++ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; ++ INFOPLIST_KEY_UIMainStoryboardFile = Main; ++ INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.tvOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = YES; ++ TARGETED_DEVICE_FAMILY = 3; ++ }; ++ name = Release; ++ }; ++ EE989E7D2DCD6E7A0036B268 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ BUNDLE_LOADER = "$(TEST_HOST)"; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ GENERATE_INFOPLIST_FILE = YES; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.TestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = NO; ++ TARGETED_DEVICE_FAMILY = 3; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tvOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/tvOSTestbed"; ++ TVOS_DEPLOYMENT_TARGET = 18.2; ++ }; ++ name = Debug; ++ }; ++ EE989E7E2DCD6E7A0036B268 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ BUNDLE_LOADER = "$(TEST_HOST)"; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ GENERATE_INFOPLIST_FILE = YES; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.TestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = NO; ++ TARGETED_DEVICE_FAMILY = 3; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tvOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/tvOSTestbed"; ++ TVOS_DEPLOYMENT_TARGET = 18.2; ++ }; ++ name = Release; ++ }; ++/* End XCBuildConfiguration section */ ++ ++/* Begin XCConfigurationList section */ ++ EE989E492DCD6E780036B268 /* Build configuration list for PBXProject "tvOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ EE989E772DCD6E7A0036B268 /* Debug */, ++ EE989E782DCD6E7A0036B268 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ EE989E792DCD6E7A0036B268 /* Build configuration list for PBXNativeTarget "tvOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ EE989E7A2DCD6E7A0036B268 /* Debug */, ++ EE989E7B2DCD6E7A0036B268 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ EE989E7C2DCD6E7A0036B268 /* Build configuration list for PBXNativeTarget "TestbedTests" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ EE989E7D2DCD6E7A0036B268 /* Debug */, ++ EE989E7E2DCD6E7A0036B268 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++/* End XCConfigurationList section */ ++ }; ++ rootObject = EE989E462DCD6E780036B268 /* Project object */; ++} +diff --git a/Platforms/Apple/testbed/tvOSTestbed.xcodeproj/xcshareddata/xcschemes/tvOSTestbed.xcscheme b/Platforms/Apple/testbed/tvOSTestbed.xcodeproj/xcshareddata/xcschemes/tvOSTestbed.xcscheme +new file mode 100644 +index 00000000000..c3f3f894a1f +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed.xcodeproj/xcshareddata/xcschemes/tvOSTestbed.xcscheme +@@ -0,0 +1,97 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/Platforms/Apple/testbed/tvOSTestbed.xctestplan b/Platforms/Apple/testbed/tvOSTestbed.xctestplan +new file mode 100644 +index 00000000000..f996facc178 +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed.xctestplan +@@ -0,0 +1,46 @@ ++{ ++ "configurations" : [ ++ { ++ "id" : "F5A95CE4-1ADE-4A6E-A0E1-CDBAE26DF0C5", ++ "name" : "Test Scheme Action", ++ "options" : { ++ ++ } ++ } ++ ], ++ "defaultOptions" : { ++ "commandLineArgumentEntries" : [ ++ { ++ "argument" : "test" ++ }, ++ { ++ "argument" : "-uall" ++ }, ++ { ++ "argument" : "--single-process" ++ }, ++ { ++ "argument" : "--rerun" ++ }, ++ { ++ "argument" : "-W" ++ } ++ ], ++ "targetForVariableExpansion" : { ++ "containerPath" : "container:tvOSTestbed.xcodeproj", ++ "identifier" : "607A66112B0EFA380010BFC8", ++ "name" : "tvOSTestbed" ++ } ++ }, ++ "testTargets" : [ ++ { ++ "parallelizable" : false, ++ "target" : { ++ "containerPath" : "container:tvOSTestbed.xcodeproj", ++ "identifier" : "EE989E642DCD6E7A0036B268", ++ "name" : "TestbedTests" ++ } ++ } ++ ], ++ "version" : 1 ++} +\ No newline at end of file +diff --git a/Platforms/Apple/testbed/tvOSTestbed/AppDelegate.h b/Platforms/Apple/testbed/tvOSTestbed/AppDelegate.h +new file mode 100644 +index 00000000000..112c9ed64b8 +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed/AppDelegate.h +@@ -0,0 +1,11 @@ ++// ++// AppDelegate.h ++// tvOSTestbed ++// ++ ++#import ++ ++@interface AppDelegate : UIResponder ++ ++ ++@end +diff --git a/Platforms/Apple/testbed/tvOSTestbed/AppDelegate.m b/Platforms/Apple/testbed/tvOSTestbed/AppDelegate.m +new file mode 100644 +index 00000000000..7501ec7b6ef +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed/AppDelegate.m +@@ -0,0 +1,24 @@ ++// ++// AppDelegate.m ++// tvOSTestbed ++// ++ ++#import "AppDelegate.h" ++#import "SceneDelegate.h" ++ ++@interface AppDelegate () ++ ++@end ++ ++@implementation AppDelegate ++ ++- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { ++ UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; ++ configuration.delegateClass = [SceneDelegate class]; ++ return configuration; ++} ++ ++- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { ++} ++ ++@end +diff --git a/Platforms/Apple/testbed/tvOSTestbed/Base.lproj/LaunchScreen.storyboard b/Platforms/Apple/testbed/tvOSTestbed/Base.lproj/LaunchScreen.storyboard +new file mode 100644 +index 00000000000..660ba53de4f +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed/Base.lproj/LaunchScreen.storyboard +@@ -0,0 +1,24 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/Platforms/Apple/testbed/tvOSTestbed/SceneDelegate.h b/Platforms/Apple/testbed/tvOSTestbed/SceneDelegate.h +new file mode 100644 +index 00000000000..264aacadca1 +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed/SceneDelegate.h +@@ -0,0 +1,12 @@ ++// ++// SceneDelegate.h ++// iOSTestbed ++// ++ ++#import ++ ++@interface SceneDelegate : UIResponder ++ ++@property (strong, nonatomic) UIWindow *window; ++ ++@end +diff --git a/Platforms/Apple/testbed/tvOSTestbed/SceneDelegate.m b/Platforms/Apple/testbed/tvOSTestbed/SceneDelegate.m +new file mode 100644 +index 00000000000..e82e447985d +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed/SceneDelegate.m +@@ -0,0 +1,21 @@ ++// ++// SceneDelegate.m ++// iOSTestbed ++// ++ ++#import "SceneDelegate.h" ++ ++@interface SceneDelegate () ++ ++@end ++ ++@implementation SceneDelegate ++ ++- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { ++ UIWindowScene *windowScene = (UIWindowScene *)scene; ++ self.window = [[UIWindow alloc] initWithWindowScene:windowScene]; ++ self.window.rootViewController = [[UIViewController alloc] init]; ++ [self.window makeKeyAndVisible]; ++} ++ ++@end +diff --git a/Platforms/Apple/testbed/tvOSTestbed/app/README b/Platforms/Apple/testbed/tvOSTestbed/app/README +new file mode 100644 +index 00000000000..46c0e8e2a29 +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed/app/README +@@ -0,0 +1,7 @@ ++This folder can contain any Python application code. ++ ++During the build, any binary modules found in this folder will be processed into ++Framework form. ++ ++When the test suite runs, this folder will be on the PYTHONPATH, and will be the ++working directory for the test suite. +diff --git a/Platforms/Apple/testbed/tvOSTestbed/app_packages/README b/Platforms/Apple/testbed/tvOSTestbed/app_packages/README +new file mode 100644 +index 00000000000..02c2beccfbd +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed/app_packages/README +@@ -0,0 +1,7 @@ ++This folder can be a target for installing any Python dependencies needed by the ++test suite. ++ ++During the build, any binary modules found in this folder will be processed into ++Framework form. ++ ++When the test suite runs, this folder will be on the PYTHONPATH. +diff --git a/Platforms/Apple/testbed/tvOSTestbed/main.m b/Platforms/Apple/testbed/tvOSTestbed/main.m +new file mode 100644 +index 00000000000..d5808fbb933 +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed/main.m +@@ -0,0 +1,16 @@ ++// ++// main.m ++// tvOSTestbed ++// ++ ++#import ++#import "AppDelegate.h" ++ ++int main(int argc, char * argv[]) { ++ NSString * appDelegateClassName; ++ @autoreleasepool { ++ appDelegateClassName = NSStringFromClass([AppDelegate class]); ++ ++ return UIApplicationMain(argc, argv, nil, appDelegateClassName); ++ } ++} +diff --git a/Platforms/Apple/testbed/tvOSTestbed/tvOSTestbed-Info.plist b/Platforms/Apple/testbed/tvOSTestbed/tvOSTestbed-Info.plist +new file mode 100644 +index 00000000000..6d84447c031 +--- /dev/null ++++ b/Platforms/Apple/testbed/tvOSTestbed/tvOSTestbed-Info.plist +@@ -0,0 +1,62 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleDisplayName ++ ${PRODUCT_NAME} ++ CFBundleExecutable ++ ${EXECUTABLE_NAME} ++ CFBundleIdentifier ++ org.python.tvOSTestbed ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundleName ++ ${PRODUCT_NAME} ++ CFBundlePackageType ++ APPL ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSignature ++ ???? ++ CFBundleVersion ++ 1 ++ LSRequiresIPhoneOS ++ ++ UIRequiresFullScreen ++ ++ UILaunchStoryboardName ++ Launch Screen ++ UISupportedInterfaceOrientations ++ ++ UIInterfaceOrientationPortrait ++ UIInterfaceOrientationLandscapeLeft ++ UIInterfaceOrientationLandscapeRight ++ ++ UISupportedInterfaceOrientations~ipad ++ ++ UIInterfaceOrientationPortrait ++ UIInterfaceOrientationPortraitUpsideDown ++ UIInterfaceOrientationLandscapeLeft ++ UIInterfaceOrientationLandscapeRight ++ ++ UIApplicationSceneManifest ++ ++ UIApplicationSupportsMultipleScenes ++ ++ UISceneConfigurations ++ ++ UIWindowSceneSessionRoleApplication ++ ++ ++ UISceneDelegateClassName ++ SceneDelegate ++ UISceneConfigurationName ++ Default Configuration ++ ++ ++ ++ ++ ++ +diff --git a/Platforms/Apple/testbed/watchOSTestbed.xcodeproj/project.pbxproj b/Platforms/Apple/testbed/watchOSTestbed.xcodeproj/project.pbxproj +new file mode 100644 +index 00000000000..a4c77addc35 +--- /dev/null ++++ b/Platforms/Apple/testbed/watchOSTestbed.xcodeproj/project.pbxproj +@@ -0,0 +1,490 @@ ++// !$*UTF8*$! ++{ ++ archiveVersion = 1; ++ classes = { ++ }; ++ objectVersion = 77; ++ objects = { ++ ++/* Begin PBXBuildFile section */ ++ 60F7C0012EB0000000A1B2C3 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 60F7C0072EB0000000A1B2C3 /* Python.xcframework */; }; ++ 60F7C0022EB0000000A1B2C3 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 60F7C0072EB0000000A1B2C3 /* Python.xcframework */; }; ++ 60F7C0032EB0000000A1B2C3 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 60F7C0072EB0000000A1B2C3 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; ++/* End PBXBuildFile section */ ++ ++/* Begin PBXContainerItemProxy section */ ++ 60F7C0042EB0000000A1B2C3 /* PBXContainerItemProxy */ = { ++ isa = PBXContainerItemProxy; ++ containerPortal = 60F7C0132EB0000000A1B2C3 /* Project object */; ++ proxyType = 1; ++ remoteGlobalIDString = 60F7C0112EB0000000A1B2C3; ++ remoteInfo = watchOSTestbed; ++ }; ++/* End PBXContainerItemProxy section */ ++ ++/* Begin PBXCopyFilesBuildPhase section */ ++ 60F7C0052EB0000000A1B2C3 /* Embed Frameworks */ = { ++ isa = PBXCopyFilesBuildPhase; ++ buildActionMask = 2147483647; ++ dstPath = ""; ++ dstSubfolderSpec = 10; ++ files = ( ++ 60F7C0032EB0000000A1B2C3 /* Python.xcframework in Embed Frameworks */, ++ ); ++ name = "Embed Frameworks"; ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXCopyFilesBuildPhase section */ ++ ++/* Begin PBXFileReference section */ ++ 60F7C0062EB0000000A1B2C3 /* watchOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = watchOSTestbed.xctestplan; sourceTree = ""; }; ++ 60F7C0072EB0000000A1B2C3 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; ++ 60F7C0082EB0000000A1B2C3 /* watchOSTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = watchOSTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; }; ++ 60F7C0092EB0000000A1B2C3 /* TestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; ++/* End PBXFileReference section */ ++ ++/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ ++ 60F7C00A2EB0000000A1B2C3 /* Exceptions for "watchOSTestbed" folder in "watchOSTestbed" target */ = { ++ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; ++ membershipExceptions = ( ++ "watchOSTestbed-Info.plist", ++ ); ++ target = 60F7C0112EB0000000A1B2C3 /* watchOSTestbed */; ++ }; ++/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ ++ ++/* Begin PBXFileSystemSynchronizedRootGroup section */ ++ 60F7C00B2EB0000000A1B2C3 /* watchOSTestbed */ = { ++ isa = PBXFileSystemSynchronizedRootGroup; ++ exceptions = ( ++ 60F7C00A2EB0000000A1B2C3 /* Exceptions for "watchOSTestbed" folder in "watchOSTestbed" target */, ++ ); ++ explicitFolders = ( ++ app, ++ app_packages, ++ ); ++ path = watchOSTestbed; ++ sourceTree = ""; ++ }; ++ 60F7C00C2EB0000000A1B2C3 /* TestbedTests */ = { ++ isa = PBXFileSystemSynchronizedRootGroup; ++ path = TestbedTests; ++ sourceTree = ""; ++ }; ++/* End PBXFileSystemSynchronizedRootGroup section */ ++ ++/* Begin PBXFrameworksBuildPhase section */ ++ 60F7C00D2EB0000000A1B2C3 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 60F7C0022EB0000000A1B2C3 /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 60F7C00E2EB0000000A1B2C3 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 60F7C0012EB0000000A1B2C3 /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXFrameworksBuildPhase section */ ++ ++/* Begin PBXGroup section */ ++ 60F7C00F2EB0000000A1B2C3 = { ++ isa = PBXGroup; ++ children = ( ++ 60F7C0062EB0000000A1B2C3 /* watchOSTestbed.xctestplan */, ++ 60F7C0072EB0000000A1B2C3 /* Python.xcframework */, ++ 60F7C00B2EB0000000A1B2C3 /* watchOSTestbed */, ++ 60F7C00C2EB0000000A1B2C3 /* TestbedTests */, ++ 60F7C0102EB0000000A1B2C3 /* Products */, ++ ); ++ sourceTree = ""; ++ }; ++ 60F7C0102EB0000000A1B2C3 /* Products */ = { ++ isa = PBXGroup; ++ children = ( ++ 60F7C0082EB0000000A1B2C3 /* watchOSTestbed.app */, ++ 60F7C0092EB0000000A1B2C3 /* TestbedTests.xctest */, ++ ); ++ name = Products; ++ sourceTree = ""; ++ }; ++/* End PBXGroup section */ ++ ++/* Begin PBXNativeTarget section */ ++ 60F7C0112EB0000000A1B2C3 /* watchOSTestbed */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = 60F7C0212EB0000000A1B2C3 /* Build configuration list for PBXNativeTarget "watchOSTestbed" */; ++ buildPhases = ( ++ 60F7C0172EB0000000A1B2C3 /* Sources */, ++ 60F7C00D2EB0000000A1B2C3 /* Frameworks */, ++ 60F7C0142EB0000000A1B2C3 /* Resources */, ++ 60F7C0162EB0000000A1B2C3 /* Process Python libraries */, ++ 60F7C0052EB0000000A1B2C3 /* Embed Frameworks */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ ); ++ fileSystemSynchronizedGroups = ( ++ 60F7C00B2EB0000000A1B2C3 /* watchOSTestbed */, ++ ); ++ name = watchOSTestbed; ++ packageProductDependencies = ( ++ ); ++ productName = watchOSTestbed; ++ productReference = 60F7C0082EB0000000A1B2C3 /* watchOSTestbed.app */; ++ productType = "com.apple.product-type.application"; ++ }; ++ 60F7C0122EB0000000A1B2C3 /* TestbedTests */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = 60F7C0222EB0000000A1B2C3 /* Build configuration list for PBXNativeTarget "TestbedTests" */; ++ buildPhases = ( ++ 60F7C0182EB0000000A1B2C3 /* Sources */, ++ 60F7C00E2EB0000000A1B2C3 /* Frameworks */, ++ 60F7C0152EB0000000A1B2C3 /* Resources */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ 60F7C0192EB0000000A1B2C3 /* PBXTargetDependency */, ++ ); ++ fileSystemSynchronizedGroups = ( ++ 60F7C00C2EB0000000A1B2C3 /* TestbedTests */, ++ ); ++ name = TestbedTests; ++ packageProductDependencies = ( ++ ); ++ productName = TestbedTests; ++ productReference = 60F7C0092EB0000000A1B2C3 /* TestbedTests.xctest */; ++ productType = "com.apple.product-type.bundle.unit-test"; ++ }; ++/* End PBXNativeTarget section */ ++ ++/* Begin PBXProject section */ ++ 60F7C0132EB0000000A1B2C3 /* Project object */ = { ++ isa = PBXProject; ++ attributes = { ++ BuildIndependentTargetsInParallel = 1; ++ LastUpgradeCheck = 1620; ++ TargetAttributes = { ++ 60F7C0112EB0000000A1B2C3 = { ++ CreatedOnToolsVersion = 16.2; ++ }; ++ 60F7C0122EB0000000A1B2C3 = { ++ CreatedOnToolsVersion = 16.2; ++ TestTargetID = 60F7C0112EB0000000A1B2C3; ++ }; ++ }; ++ }; ++ buildConfigurationList = 60F7C0202EB0000000A1B2C3 /* Build configuration list for PBXProject "watchOSTestbed" */; ++ developmentRegion = en; ++ hasScannedForEncodings = 0; ++ knownRegions = ( ++ en, ++ Base, ++ ); ++ mainGroup = 60F7C00F2EB0000000A1B2C3; ++ minimizedProjectReferenceProxies = 1; ++ preferredProjectObjectVersion = 77; ++ productRefGroup = 60F7C0102EB0000000A1B2C3 /* Products */; ++ projectDirPath = ""; ++ projectRoot = ""; ++ targets = ( ++ 60F7C0112EB0000000A1B2C3 /* watchOSTestbed */, ++ 60F7C0122EB0000000A1B2C3 /* TestbedTests */, ++ ); ++ }; ++/* End PBXProject section */ ++ ++/* Begin PBXResourcesBuildPhase section */ ++ 60F7C0142EB0000000A1B2C3 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 60F7C0152EB0000000A1B2C3 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXResourcesBuildPhase section */ ++ ++/* Begin PBXShellScriptBuildPhase section */ ++ 60F7C0162EB0000000A1B2C3 /* Process Python libraries */ = { ++ isa = PBXShellScriptBuildPhase; ++ alwaysOutOfDate = 1; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ inputFileListPaths = ( ++ ); ++ inputPaths = ( ++ ); ++ name = "Process Python libraries"; ++ outputFileListPaths = ( ++ ); ++ outputPaths = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ shellPath = /bin/sh; ++ shellScript = "set -e\n\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\n\ninstall_python Python.xcframework app app_packages\n"; ++ showEnvVarsInLog = 0; ++ }; ++/* End PBXShellScriptBuildPhase section */ ++ ++/* Begin PBXSourcesBuildPhase section */ ++ 60F7C0172EB0000000A1B2C3 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 60F7C0182EB0000000A1B2C3 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXSourcesBuildPhase section */ ++ ++/* Begin PBXTargetDependency section */ ++ 60F7C0192EB0000000A1B2C3 /* PBXTargetDependency */ = { ++ isa = PBXTargetDependency; ++ target = 60F7C0112EB0000000A1B2C3 /* watchOSTestbed */; ++ targetProxy = 60F7C0042EB0000000A1B2C3 /* PBXContainerItemProxy */; ++ }; ++/* End PBXTargetDependency section */ ++ ++/* Begin XCBuildConfiguration section */ ++ 60F7C01A2EB0000000A1B2C3 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ALWAYS_SEARCH_USER_PATHS = NO; ++ CLANG_ANALYZER_NONNULL = YES; ++ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; ++ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; ++ CLANG_ENABLE_MODULES = YES; ++ CLANG_ENABLE_OBJC_ARC = YES; ++ CLANG_ENABLE_OBJC_WEAK = YES; ++ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; ++ CLANG_WARN_BOOL_CONVERSION = YES; ++ CLANG_WARN_COMMA = YES; ++ CLANG_WARN_CONSTANT_CONVERSION = YES; ++ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; ++ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; ++ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; ++ CLANG_WARN_EMPTY_BODY = YES; ++ CLANG_WARN_ENUM_CONVERSION = YES; ++ CLANG_WARN_INFINITE_RECURSION = YES; ++ CLANG_WARN_INT_CONVERSION = YES; ++ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; ++ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; ++ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; ++ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; ++ CLANG_WARN_STRICT_PROTOTYPES = YES; ++ CLANG_WARN_SUSPICIOUS_MOVE = YES; ++ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; ++ CLANG_WARN_UNREACHABLE_CODE = YES; ++ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; ++ COPY_PHASE_STRIP = NO; ++ DEBUG_INFORMATION_FORMAT = dwarf; ++ ENABLE_STRICT_OBJC_MSGSEND = YES; ++ ENABLE_TESTABILITY = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = NO; ++ FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)"; ++ GCC_C_LANGUAGE_STANDARD = gnu17; ++ GCC_DYNAMIC_NO_PIC = NO; ++ GCC_NO_COMMON_BLOCKS = YES; ++ GCC_OPTIMIZATION_LEVEL = 0; ++ GCC_PREPROCESSOR_DEFINITIONS = ( ++ "DEBUG=1", ++ "$(inherited)", ++ ); ++ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; ++ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; ++ GCC_WARN_UNDECLARED_SELECTOR = YES; ++ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; ++ GCC_WARN_UNUSED_FUNCTION = YES; ++ GCC_WARN_UNUSED_VARIABLE = YES; ++ HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/Python.framework/Headers"; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; ++ MTL_FAST_MATH = YES; ++ ONLY_ACTIVE_ARCH = YES; ++ SDKROOT = watchos; ++ WATCHOS_DEPLOYMENT_TARGET = 9.0; ++ }; ++ name = Debug; ++ }; ++ 60F7C01B2EB0000000A1B2C3 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ALWAYS_SEARCH_USER_PATHS = NO; ++ CLANG_ANALYZER_NONNULL = YES; ++ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; ++ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; ++ CLANG_ENABLE_MODULES = YES; ++ CLANG_ENABLE_OBJC_ARC = YES; ++ CLANG_ENABLE_OBJC_WEAK = YES; ++ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; ++ CLANG_WARN_BOOL_CONVERSION = YES; ++ CLANG_WARN_COMMA = YES; ++ CLANG_WARN_CONSTANT_CONVERSION = YES; ++ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; ++ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; ++ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; ++ CLANG_WARN_EMPTY_BODY = YES; ++ CLANG_WARN_ENUM_CONVERSION = YES; ++ CLANG_WARN_INFINITE_RECURSION = YES; ++ CLANG_WARN_INT_CONVERSION = YES; ++ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; ++ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; ++ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; ++ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; ++ CLANG_WARN_STRICT_PROTOTYPES = YES; ++ CLANG_WARN_SUSPICIOUS_MOVE = YES; ++ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; ++ CLANG_WARN_UNREACHABLE_CODE = YES; ++ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; ++ COPY_PHASE_STRIP = NO; ++ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ++ ENABLE_NS_ASSERTIONS = NO; ++ ENABLE_STRICT_OBJC_MSGSEND = YES; ++ ENABLE_TESTABILITY = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = NO; ++ FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)"; ++ GCC_C_LANGUAGE_STANDARD = gnu17; ++ GCC_NO_COMMON_BLOCKS = YES; ++ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; ++ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; ++ GCC_WARN_UNDECLARED_SELECTOR = YES; ++ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; ++ GCC_WARN_UNUSED_FUNCTION = YES; ++ GCC_WARN_UNUSED_VARIABLE = YES; ++ HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/Python.framework/Headers"; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = NO; ++ MTL_FAST_MATH = YES; ++ SDKROOT = watchos; ++ VALIDATE_PRODUCT = YES; ++ WATCHOS_DEPLOYMENT_TARGET = 9.0; ++ }; ++ name = Release; ++ }; ++ 60F7C01C2EB0000000A1B2C3 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ GENERATE_INFOPLIST_FILE = NO; ++ INFOPLIST_FILE = "watchOSTestbed/watchOSTestbed-Info.plist"; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.watchOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ TARGETED_DEVICE_FAMILY = 4; ++ }; ++ name = Debug; ++ }; ++ 60F7C01D2EB0000000A1B2C3 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ GENERATE_INFOPLIST_FILE = NO; ++ INFOPLIST_FILE = "watchOSTestbed/watchOSTestbed-Info.plist"; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.watchOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ TARGETED_DEVICE_FAMILY = 4; ++ }; ++ name = Release; ++ }; ++ 60F7C01E2EB0000000A1B2C3 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ BUNDLE_LOADER = "$(TEST_HOST)"; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ GENERATE_INFOPLIST_FILE = YES; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.TestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ TARGETED_DEVICE_FAMILY = 4; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/watchOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/watchOSTestbed"; ++ WATCHOS_DEPLOYMENT_TARGET = 9.0; ++ }; ++ name = Debug; ++ }; ++ 60F7C01F2EB0000000A1B2C3 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ BUNDLE_LOADER = "$(TEST_HOST)"; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ GENERATE_INFOPLIST_FILE = YES; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.TestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ TARGETED_DEVICE_FAMILY = 4; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/watchOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/watchOSTestbed"; ++ WATCHOS_DEPLOYMENT_TARGET = 9.0; ++ }; ++ name = Release; ++ }; ++/* End XCBuildConfiguration section */ ++ ++/* Begin XCConfigurationList section */ ++ 60F7C0202EB0000000A1B2C3 /* Build configuration list for PBXProject "watchOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 60F7C01A2EB0000000A1B2C3 /* Debug */, ++ 60F7C01B2EB0000000A1B2C3 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ 60F7C0212EB0000000A1B2C3 /* Build configuration list for PBXNativeTarget "watchOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 60F7C01C2EB0000000A1B2C3 /* Debug */, ++ 60F7C01D2EB0000000A1B2C3 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ 60F7C0222EB0000000A1B2C3 /* Build configuration list for PBXNativeTarget "TestbedTests" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 60F7C01E2EB0000000A1B2C3 /* Debug */, ++ 60F7C01F2EB0000000A1B2C3 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++/* End XCConfigurationList section */ ++ }; ++ rootObject = 60F7C0132EB0000000A1B2C3 /* Project object */; ++} +diff --git a/Platforms/Apple/testbed/watchOSTestbed.xcodeproj/xcshareddata/xcschemes/watchOSTestbed.xcscheme b/Platforms/Apple/testbed/watchOSTestbed.xcodeproj/xcshareddata/xcschemes/watchOSTestbed.xcscheme +new file mode 100644 +index 00000000000..96689416946 +--- /dev/null ++++ b/Platforms/Apple/testbed/watchOSTestbed.xcodeproj/xcshareddata/xcschemes/watchOSTestbed.xcscheme +@@ -0,0 +1,97 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/Platforms/Apple/testbed/watchOSTestbed.xctestplan b/Platforms/Apple/testbed/watchOSTestbed.xctestplan +new file mode 100644 +index 00000000000..f83ff3a5595 +--- /dev/null ++++ b/Platforms/Apple/testbed/watchOSTestbed.xctestplan +@@ -0,0 +1,46 @@ ++{ ++ "configurations" : [ ++ { ++ "id" : "8E3B1C52-4F0A-4D7E-9B61-2C5A7E0D9F14", ++ "name" : "Test Scheme Action", ++ "options" : { + - - @requires_subprocess() - def check_output(cmd, encoding=None): -diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py -index ae161786002..381b54c7c24 100644 ---- a/Lib/test/test_webbrowser.py -+++ b/Lib/test/test_webbrowser.py -@@ -6,11 +6,14 @@ - import subprocess - from unittest import mock - from test import support -+from test.support import is_apple_mobile - from test.support import import_helper - from test.support import os_helper -+from test.support import requires_subprocess -+from test.support import threading_helper - --if not support.has_subprocess_support: -- raise unittest.SkipTest("test webserver requires subprocess") -+# The webbrowser module uses threading locks -+threading_helper.requires_working_threading(module=True) - - URL = 'https://www.example.com' - CMD_NAME = 'test' -@@ -25,6 +28,7 @@ - return 0 - - -+@requires_subprocess() - class CommandTestMixin: - - def _test(self, meth, *, args=[URL], kw={}, options, arguments): -@@ -302,6 +306,73 @@ - self.browser.open(f"--key=val {URL}") - - -+@unittest.skipUnless(sys.platform == "ios", "Test only applicable to iOS") -+class IOSBrowserTest(unittest.TestCase): -+ def _obj_ref(self, *args): -+ # Construct a string representation of the arguments that can be used -+ # as a proxy for object instance references -+ return "|".join(str(a) for a in args) ++ } ++ } ++ ], ++ "defaultOptions" : { ++ "commandLineArgumentEntries" : [ ++ { ++ "argument" : "test" ++ }, ++ { ++ "argument" : "-uall" ++ }, ++ { ++ "argument" : "--single-process" ++ }, ++ { ++ "argument" : "--rerun" ++ }, ++ { ++ "argument" : "-W" ++ } ++ ], ++ "targetForVariableExpansion" : { ++ "containerPath" : "container:watchOSTestbed.xcodeproj", ++ "identifier" : "60F7C0112EB0000000A1B2C3", ++ "name" : "watchOSTestbed" ++ } ++ }, ++ "testTargets" : [ ++ { ++ "parallelizable" : false, ++ "target" : { ++ "containerPath" : "container:watchOSTestbed.xcodeproj", ++ "identifier" : "60F7C0122EB0000000A1B2C3", ++ "name" : "TestbedTests" ++ } ++ } ++ ], ++ "version" : 1 ++} +diff --git a/Platforms/Apple/testbed/watchOSTestbed/AppDelegate.h b/Platforms/Apple/testbed/watchOSTestbed/AppDelegate.h +new file mode 100644 +index 00000000000..cc7359048ae +--- /dev/null ++++ b/Platforms/Apple/testbed/watchOSTestbed/AppDelegate.h +@@ -0,0 +1,11 @@ ++// ++// AppDelegate.h ++// watchOSTestbed ++// + -+ @unittest.skipIf(getattr(webbrowser, "objc", None) is None, -+ "iOS Webbrowser tests require ctypes") -+ def setUp(self): -+ # Intercept the the objc library. Wrap the calls to get the -+ # references to classes and selectors to return strings, and -+ # wrap msgSend to return stringified object references -+ self.orig_objc = webbrowser.objc ++#import + -+ webbrowser.objc = mock.Mock() -+ webbrowser.objc.objc_getClass = lambda cls: f"C#{cls.decode()}" -+ webbrowser.objc.sel_registerName = lambda sel: f"S#{sel.decode()}" -+ webbrowser.objc.objc_msgSend.side_effect = self._obj_ref ++@interface AppDelegate : NSObject + -+ def tearDown(self): -+ webbrowser.objc = self.orig_objc + -+ def _test(self, meth, **kwargs): -+ # The browser always gets focus, there's no concept of separate browser -+ # windows, and there's no API-level control over creating a new tab. -+ # Therefore, all calls to webbrowser are effectively the same. -+ getattr(webbrowser, meth)(URL, **kwargs) ++@end +diff --git a/Platforms/Apple/testbed/watchOSTestbed/AppDelegate.m b/Platforms/Apple/testbed/watchOSTestbed/AppDelegate.m +new file mode 100644 +index 00000000000..3390d2efc87 +--- /dev/null ++++ b/Platforms/Apple/testbed/watchOSTestbed/AppDelegate.m +@@ -0,0 +1,21 @@ ++// ++// AppDelegate.m ++// watchOSTestbed ++// + -+ # The ObjC String version of the URL is created with UTF-8 encoding -+ url_string_args = [ -+ "C#NSString", -+ "S#stringWithCString:encoding:", -+ b'https://www.example.com', -+ 4, -+ ] -+ # The NSURL version of the URL is created from that string -+ url_obj_args = [ -+ "C#NSURL", -+ "S#URLWithString:", -+ self._obj_ref(*url_string_args), -+ ] -+ # The openURL call is invoked on the shared application -+ shared_app_args = ["C#UIApplication", "S#sharedApplication"] ++#import "AppDelegate.h" + -+ # Verify that the last call is the one that opens the URL. -+ webbrowser.objc.objc_msgSend.assert_called_with( -+ self._obj_ref(*shared_app_args), -+ "S#openURL:options:completionHandler:", -+ self._obj_ref(*url_obj_args), -+ None, -+ None -+ ) ++@interface AppDelegate () + -+ def test_open(self): -+ self._test('open') ++@end + -+ def test_open_with_autoraise_false(self): -+ self._test('open', autoraise=False) ++@implementation AppDelegate + -+ def test_open_new(self): -+ self._test('open_new') ++// The testbed has no storyboard (Interface.plist), so WatchKit requires the ++// delegate to provide a root interface controller class, or the app crashes ++// on launch. ++- (Class)applicationRootInterfaceControllerClass { ++ return [WKInterfaceController class]; ++} + -+ def test_open_new_tab(self): -+ self._test('open_new_tab') ++@end +diff --git a/Platforms/Apple/testbed/watchOSTestbed/app/README b/Platforms/Apple/testbed/watchOSTestbed/app/README +new file mode 100644 +index 00000000000..46c0e8e2a29 +--- /dev/null ++++ b/Platforms/Apple/testbed/watchOSTestbed/app/README +@@ -0,0 +1,7 @@ ++This folder can contain any Python application code. + ++During the build, any binary modules found in this folder will be processed into ++Framework form. + - class BrowserRegistrationTest(unittest.TestCase): - - def setUp(self): -@@ -385,6 +456,10 @@ - webbrowser.register(name, None, webbrowser.GenericBrowser(name)) - webbrowser.get(sys.executable) - -+ @unittest.skipIf( -+ is_apple_mobile, -+ "Apple mobile doesn't allow modifying browser with environment" -+ ) - def test_environment(self): - webbrowser = import_helper.import_fresh_module('webbrowser') - try: -@@ -396,6 +471,10 @@ - webbrowser = import_helper.import_fresh_module('webbrowser') - webbrowser.get() - -+ @unittest.skipIf( -+ is_apple_mobile, -+ "Apple mobile doesn't allow modifying browser with environment" -+ ) - def test_environment_preferred(self): - webbrowser = import_helper.import_fresh_module('webbrowser') - try: -diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py -index 79e00f9f334..b37aedde348 100755 ---- a/Lib/webbrowser.py -+++ b/Lib/webbrowser.py -@@ -487,6 +487,9 @@ - # OS X can use below Unix support (but we prefer using the OS X - # specific stuff) - -+ if sys.platform == "ios": -+ register("iosbrowser", None, IOSBrowser(), preferred=True) ++When the test suite runs, this folder will be on the PYTHONPATH, and will be the ++working directory for the test suite. +diff --git a/Platforms/Apple/testbed/watchOSTestbed/app_packages/README b/Platforms/Apple/testbed/watchOSTestbed/app_packages/README +new file mode 100644 +index 00000000000..02c2beccfbd +--- /dev/null ++++ b/Platforms/Apple/testbed/watchOSTestbed/app_packages/README +@@ -0,0 +1,7 @@ ++This folder can be a target for installing any Python dependencies needed by the ++test suite. + - if sys.platform == "serenityos": - # SerenityOS webbrowser, simply called "Browser". - register("Browser", None, BackgroundBrowser("Browser")) -@@ -669,6 +672,70 @@ - rc = osapipe.close() - return not rc - -+# -+# Platform support for iOS -+# -+if sys.platform == "ios": -+ from _ios_support import objc -+ if objc: -+ # If objc exists, we know ctypes is also importable. -+ from ctypes import c_void_p, c_char_p, c_ulong ++During the build, any binary modules found in this folder will be processed into ++Framework form. ++ ++When the test suite runs, this folder will be on the PYTHONPATH. +diff --git a/Platforms/Apple/testbed/watchOSTestbed/main.m b/Platforms/Apple/testbed/watchOSTestbed/main.m +new file mode 100644 +index 00000000000..b0e43f1a06f +--- /dev/null ++++ b/Platforms/Apple/testbed/watchOSTestbed/main.m +@@ -0,0 +1,16 @@ ++// ++// main.m ++// watchOSTestbed ++// ++ ++#import ++#import "AppDelegate.h" ++ ++int main(int argc, char * argv[]) { ++ NSString * appDelegateClassName; ++ @autoreleasepool { ++ appDelegateClassName = NSStringFromClass([AppDelegate class]); ++ ++ return WKApplicationMain(argc, argv, appDelegateClassName); ++ } ++} +diff --git a/Platforms/Apple/testbed/watchOSTestbed/watchOSTestbed-Info.plist b/Platforms/Apple/testbed/watchOSTestbed/watchOSTestbed-Info.plist +new file mode 100644 +index 00000000000..4f25642a8e2 +--- /dev/null ++++ b/Platforms/Apple/testbed/watchOSTestbed/watchOSTestbed-Info.plist +@@ -0,0 +1,30 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleDisplayName ++ ${PRODUCT_NAME} ++ CFBundleExecutable ++ ${EXECUTABLE_NAME} ++ CFBundleIdentifier ++ org.python.watchOSTestbed ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundleName ++ ${PRODUCT_NAME} ++ CFBundlePackageType ++ APPL ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSignature ++ ???? ++ CFBundleVersion ++ 1 ++ WKApplication ++ ++ WKWatchOnly ++ ++ ++ +diff --git a/Platforms/Apple/tvOS/README.md b/Platforms/Apple/tvOS/README.md +new file mode 100644 +index 00000000000..24e1cb239ba +--- /dev/null ++++ b/Platforms/Apple/tvOS/README.md +@@ -0,0 +1,28 @@ ++# Python on tvOS README + -+ class IOSBrowser(BaseBrowser): -+ def open(self, url, new=0, autoraise=True): -+ sys.audit("webbrowser.open", url) -+ # If ctypes isn't available, we can't open a browser -+ if objc is None: -+ return False ++This document provides a quick overview of some tvOS specific features in the ++Python distribution. + -+ # All the messages in this call return object references. -+ objc.objc_msgSend.restype = c_void_p ++Python on tvOS is largely the same as Python on iOS; most of the instructions in ++the [iOS README](../iOS/README.md) can be used for tvOS builds, substituting: + -+ # This is the equivalent of: -+ # NSString url_string = -+ # [NSString stringWithCString:url.encode("utf-8") -+ # encoding:NSUTF8StringEncoding]; -+ NSString = objc.objc_getClass(b"NSString") -+ constructor = objc.sel_registerName(b"stringWithCString:encoding:") -+ objc.objc_msgSend.argtypes = [c_void_p, c_void_p, c_char_p, c_ulong] -+ url_string = objc.objc_msgSend( -+ NSString, -+ constructor, -+ url.encode("utf-8"), -+ 4, # NSUTF8StringEncoding = 4 -+ ) ++- `tvOS` as the identifier for `Platforms/Apple` invocations; e.g., ++ - `python Platforms/Apple build tvOS` ++ - `python Platforms/Apple ci tvOS` ++ - `python Platforms/Apple test tvOS` + -+ # Create an NSURL object representing the URL -+ # This is the equivalent of: -+ # NSURL *nsurl = [NSURL URLWithString:url]; -+ NSURL = objc.objc_getClass(b"NSURL") -+ urlWithString_ = objc.sel_registerName(b"URLWithString:") -+ objc.objc_msgSend.argtypes = [c_void_p, c_void_p, c_void_p] -+ ns_url = objc.objc_msgSend(NSURL, urlWithString_, url_string) ++- tvOS resources are stored in the `Platforms/Apple/tvOS/Resources` directory. + -+ # Get the shared UIApplication instance -+ # This code is the equivalent of: -+ # UIApplication shared_app = [UIApplication sharedApplication] -+ UIApplication = objc.objc_getClass(b"UIApplication") -+ sharedApplication = objc.sel_registerName(b"sharedApplication") -+ objc.objc_msgSend.argtypes = [c_void_p, c_void_p] -+ shared_app = objc.objc_msgSend(UIApplication, sharedApplication) ++- The tvOS SDK version is controlled with the `TVOS_SDK_VERSION` environment ++ variable. + -+ # Open the URL on the shared application -+ # This code is the equivalent of: -+ # [shared_app openURL:ns_url -+ # options:NIL -+ # completionHandler:NIL]; -+ openURL_ = objc.sel_registerName(b"openURL:options:completionHandler:") -+ objc.objc_msgSend.argtypes = [ -+ c_void_p, c_void_p, c_void_p, c_void_p, c_void_p -+ ] -+ # Method returns void -+ objc.objc_msgSend.restype = None -+ objc.objc_msgSend(shared_app, openURL_, ns_url, None, None) ++- The minimum supported tvOS SDK version is controlled with the ++ `TVOS_DEPLOYMENT_TARGET` environment variable. + -+ return True ++- When referencing platforms, the tvOS compiler triples are: ++ - `arm64-apple-tvos` for AppleTV devices ++ - `arm64-apple-tvos-simulator` for AppleTV simulators on ARM64 hardware ++ - `x86_64-apple-tvos-simulator` for AppleTV simulators on x86-64 hardware + - - def main(): - import getopt -diff --git a/Mac/Resources/app-store-compliance.patch b/Mac/Resources/app-store-compliance.patch ++- The tvOS testbed Xcode template project is stored in ++ `Platforms/Apple/testbed/tvOSTestbed`. +diff --git a/Platforms/Apple/tvOS/Resources/Info.plist.in b/Platforms/Apple/tvOS/Resources/Info.plist.in new file mode 100644 -index 00000000000..f4b7decc01c +index 00000000000..b913504d69e --- /dev/null -+++ b/Mac/Resources/app-store-compliance.patch -@@ -0,0 +1,29 @@ -+diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py -+index d6c83a75c1c..19ed4e01091 100644 -+--- a/Lib/test/test_urlparse.py -++++ b/Lib/test/test_urlparse.py -+@@ -237,11 +237,6 @@ def test_roundtrips(self): -+ '','',''), -+ ('git+ssh', 'git@github.com','/user/project.git', -+ '', '')), -+- ('itms-services://?action=download-manifest&url=https://example.com/app', -+- ('itms-services', '', '', '', -+- 'action=download-manifest&url=https://example.com/app', ''), -+- ('itms-services', '', '', -+- 'action=download-manifest&url=https://example.com/app', '')), -+ ('+scheme:path/to/file', -+ ('', '', '+scheme:path/to/file', '', '', ''), -+ ('', '', '+scheme:path/to/file', '', '')), -+diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py -+index 8f724f907d4..148caf742c9 100644 -+--- a/Lib/urllib/parse.py -++++ b/Lib/urllib/parse.py -+@@ -59,7 +59,7 @@ -+ 'imap', 'wais', 'file', 'mms', 'https', 'shttp', -+ 'snews', 'prospero', 'rtsp', 'rtsps', 'rtspu', 'rsync', -+ 'svn', 'svn+ssh', 'sftp', 'nfs', 'git', 'git+ssh', -+- 'ws', 'wss', 'itms-services'] -++ 'ws', 'wss'] -+ -+ uses_params = ['', 'ftp', 'hdl', 'prospero', 'http', 'imap', -+ 'https', 'shttp', 'rtsp', 'rtsps', 'rtspu', 'sip', -diff --git a/Makefile.pre.in b/Makefile.pre.in -index 3342296baa0..d5485bdf93d 100644 ---- a/Makefile.pre.in -+++ b/Makefile.pre.in -@@ -178,18 +178,29 @@ - EXE= @EXEEXT@ - BUILDEXE= @BUILDEXEEXT@ - -+# Name of the patch file to apply for app store compliance -+APP_STORE_COMPLIANCE_PATCH=@APP_STORE_COMPLIANCE_PATCH@ ++++ b/Platforms/Apple/tvOS/Resources/Info.plist.in +@@ -0,0 +1,34 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleExecutable ++ @PYTHONFRAMEWORK@ ++ CFBundleGetInfoString ++ Python Runtime and Library ++ CFBundleIdentifier ++ @PYTHONFRAMEWORKIDENTIFIER@ ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundleName ++ @PYTHONFRAMEWORK@ ++ CFBundlePackageType ++ FMWK ++ CFBundleShortVersionString ++ %VERSION% ++ CFBundleLongVersionString ++ %VERSION%, (c) 2001-2024 Python Software Foundation. ++ CFBundleSignature ++ ???? ++ CFBundleVersion ++ 1 ++ CFBundleSupportedPlatforms ++ ++ tvOS ++ ++ MinimumOSVersion ++ @TVOS_DEPLOYMENT_TARGET@ ++ ++ +diff --git a/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-ar b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-ar +new file mode 100755 +index 00000000000..e302748a13c +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-ar +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvos${TVOS_SDK_VERSION} ar "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang +new file mode 100755 +index 00000000000..7fb6d3d901c +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvos${TVOS_SDK_VERSION} clang -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET} "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang++ +new file mode 100755 +index 00000000000..33bfb1367c3 +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvos${TVOS_SDK_VERSION} clang++ -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET} "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-cpp b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-cpp +new file mode 100755 +index 00000000000..641c1bc8d18 +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-cpp +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvos${TVOS_SDK_VERSION} clang -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET} -E "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-ar b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-ar +new file mode 100755 +index 00000000000..87ef5015aae +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} ar "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang +new file mode 100755 +index 00000000000..c8719cb0318 +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang++ +new file mode 100755 +index 00000000000..e3f0e720f7f +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang++ -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-cpp b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-cpp +new file mode 100755 +index 00000000000..f9a37b72a61 +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-cpp +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang -target arm64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator -E "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-strip b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-strip +new file mode 100755 +index 00000000000..a8cce95233e +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-simulator-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} strip -arch arm64 "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-strip b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-strip +new file mode 100755 +index 00000000000..ee1d2b95ff1 +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/arm64-apple-tvos-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${TVOS_SDK_VERSION} strip -arch arm64 "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-ar b/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-ar +new file mode 100755 +index 00000000000..87ef5015aae +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} ar "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang b/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang +new file mode 100755 +index 00000000000..ea0cc26cbd9 +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang -target x86_64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang++ b/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang++ +new file mode 100755 +index 00000000000..f18f3603169 +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang++ -target x86_64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-cpp b/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-cpp +new file mode 100755 +index 00000000000..b98054d1ce2 +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-cpp +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang -target x86_64-apple-tvos${TVOS_DEPLOYMENT_TARGET}-simulator -E "$@" +diff --git a/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-strip b/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-strip +new file mode 100755 +index 00000000000..f6a884b4aef +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/bin/x86_64-apple-tvos-simulator-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} strip -arch x86_64 "$@" +diff --git a/Platforms/Apple/tvOS/Resources/pyconfig.h b/Platforms/Apple/tvOS/Resources/pyconfig.h +new file mode 100644 +index 00000000000..4acff2c6051 +--- /dev/null ++++ b/Platforms/Apple/tvOS/Resources/pyconfig.h +@@ -0,0 +1,7 @@ ++#ifdef __arm64__ ++#include "pyconfig-arm64.h" ++#endif + - # Short name and location for Mac OS X Python framework - UNIVERSALSDK=@UNIVERSALSDK@ - PYTHONFRAMEWORK= @PYTHONFRAMEWORK@ - PYTHONFRAMEWORKDIR= @PYTHONFRAMEWORKDIR@ - PYTHONFRAMEWORKPREFIX= @PYTHONFRAMEWORKPREFIX@ - PYTHONFRAMEWORKINSTALLDIR= @PYTHONFRAMEWORKINSTALLDIR@ --# Deployment target selected during configure, to be checked -+PYTHONFRAMEWORKINSTALLNAMEPREFIX= @PYTHONFRAMEWORKINSTALLNAMEPREFIX@ -+RESSRCDIR= @RESSRCDIR@ -+# macOS deployment target selected during configure, to be checked - # by distutils. The export statement is needed to ensure that the - # deployment target is active during build. - MACOSX_DEPLOYMENT_TARGET=@CONFIGURE_MACOSX_DEPLOYMENT_TARGET@ - @EXPORT_MACOSX_DEPLOYMENT_TARGET@export MACOSX_DEPLOYMENT_TARGET - -+# iOS Deployment target selected during configure. Unlike macOS, the iOS -+# deployment target is controlled using `-mios-version-min` arguments added to -+# CFLAGS and LDFLAGS by the configure script. This variable is not used during -+# the build, and is only listed here so it will be included in sysconfigdata. -+IPHONEOS_DEPLOYMENT_TARGET=@IPHONEOS_DEPLOYMENT_TARGET@ ++#ifdef __x86_64__ ++#include "pyconfig-x86_64.h" ++#endif +diff --git a/Platforms/Apple/watchOS/README.md b/Platforms/Apple/watchOS/README.md +new file mode 100644 +index 00000000000..44f23e327cb +--- /dev/null ++++ b/Platforms/Apple/watchOS/README.md +@@ -0,0 +1,29 @@ ++# Python on watchOS README + - # Option to install to strip binaries - STRIPFLAG=-s - -@@ -618,7 +629,7 @@ - .PHONY: all - - .PHONY: build_all --build_all: check-clean-src $(BUILDPYTHON) platform sharedmods \ -+build_all: check-clean-src check-app-store-compliance $(BUILDPYTHON) platform sharedmods \ - gdbhooks Programs/_testembed scripts checksharedmods rundsymutil - - .PHONY: build_wasm -@@ -641,6 +652,16 @@ - exit 1; \ - fi - -+# Check that the app store compliance patch can be applied (if configured). -+# This is checked as a dry-run against the original library sources; -+# the patch will be actually applied during the install phase. -+.PHONY: check-app-store-compliance -+check-app-store-compliance: -+ @if [ "$(APP_STORE_COMPLIANCE_PATCH)" != "" ]; then \ -+ patch --dry-run --quiet --force --strip 1 --directory "$(abs_srcdir)" --input "$(abs_srcdir)/$(APP_STORE_COMPLIANCE_PATCH)"; \ -+ echo "App store compliance patch can be applied."; \ -+ fi ++This document provides a quick overview of some watchOS specific features in the ++Python distribution. + - # Profile generation build must start from a clean tree. - profile-clean-stamp: - $(MAKE) clean -@@ -830,7 +851,7 @@ - $(BLDSHARED) $(NO_AS_NEEDED) -o $@ -Wl,-h$@ $^ - - libpython$(LDVERSION).dylib: $(LIBRARY_OBJS) -- $(CC) -dynamiclib -Wl,-single_module $(PY_CORE_LDFLAGS) -undefined dynamic_lookup -Wl,-install_name,$(prefix)/lib/libpython$(LDVERSION).dylib -Wl,-compatibility_version,$(VERSION) -Wl,-current_version,$(VERSION) -o $@ $(LIBRARY_OBJS) $(DTRACE_OBJS) $(SHLIBS) $(LIBC) $(LIBM); \ -+ $(CC) -dynamiclib $(PY_CORE_LDFLAGS) -undefined dynamic_lookup -Wl,-install_name,$(prefix)/lib/libpython$(LDVERSION).dylib -Wl,-compatibility_version,$(VERSION) -Wl,-current_version,$(VERSION) -o $@ $(LIBRARY_OBJS) $(DTRACE_OBJS) $(SHLIBS) $(LIBC) $(LIBM); \ - - - libpython$(VERSION).sl: $(LIBRARY_OBJS) -@@ -855,14 +876,13 @@ - # This rule is here for OPENSTEP/Rhapsody/MacOSX. It builds a temporary - # minimal framework (not including the Lib directory and such) in the current - # directory. --RESSRCDIR=Mac/Resources/framework - $(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK): \ - $(LIBRARY) \ - $(RESSRCDIR)/Info.plist - $(INSTALL) -d -m $(DIRMODE) $(PYTHONFRAMEWORKDIR)/Versions/$(VERSION) - $(CC) -o $(LDLIBRARY) $(PY_CORE_LDFLAGS) -dynamiclib \ -- -all_load $(LIBRARY) -Wl,-single_module \ -- -install_name $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK) \ -+ -all_load $(LIBRARY) \ -+ -install_name $(DESTDIR)$(PYTHONFRAMEWORKINSTALLNAMEPREFIX)/$(PYTHONFRAMEWORK) \ - -compatibility_version $(VERSION) \ - -current_version $(VERSION) \ - -framework CoreFoundation $(LIBS); -@@ -874,6 +894,21 @@ - $(LN) -fsn Versions/Current/$(PYTHONFRAMEWORK) $(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK) - $(LN) -fsn Versions/Current/Resources $(PYTHONFRAMEWORKDIR)/Resources - -+# This rule is for iOS, which requires an annoyingly just slighly different -+# format for frameworks to macOS. It *doesn't* use a versioned framework, and -+# the Info.plist must be in the root of the framework. -+$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK): \ -+ $(LIBRARY) \ -+ $(RESSRCDIR)/Info.plist -+ $(INSTALL) -d -m $(DIRMODE) $(PYTHONFRAMEWORKDIR) -+ $(CC) -o $(LDLIBRARY) $(PY_CORE_LDFLAGS) -dynamiclib \ -+ -all_load $(LIBRARY) \ -+ -install_name $(PYTHONFRAMEWORKINSTALLNAMEPREFIX)/$(PYTHONFRAMEWORK) \ -+ -compatibility_version $(VERSION) \ -+ -current_version $(VERSION) \ -+ -framework CoreFoundation $(LIBS); -+ $(INSTALL_DATA) $(RESSRCDIR)/Info.plist $(PYTHONFRAMEWORKDIR)/Info.plist ++Python on watchOS is largely the same as Python on iOS; most of the instructions in ++the [iOS README](../iOS/README.md) can be used for watchOS builds, substituting: + - # This rule builds the Cygwin Python DLL and import library if configured - # for a shared core library; otherwise, this rule is a noop. - $(DLLLIBRARY) libpython$(LDVERSION).dll.a: $(LIBRARY_OBJS) -@@ -1863,6 +1898,36 @@ - $(RUNSHARED) /usr/libexec/oah/translate \ - ./$(BUILDPYTHON) -E -m test -j 0 -u all $(TESTOPTS) - -+# Run the test suite on the iOS simulator. Must be run on a macOS machine with -+# a full Xcode install that has an iPhone SE (3rd edition) simulator available. -+# This must be run *after* a `make install` has completed the build. The -+# `--with-framework-name` argument *cannot* be used when configuring the build. -+XCFOLDER:=iOSTestbed.$(MULTIARCH).$(shell date +%s) -+.PHONY: testios -+testios: -+ @if test "$(MACHDEP)" != "ios"; then \ -+ echo "Cannot run the iOS testbed for a non-iOS build."; \ -+ exit 1;\ -+ fi -+ @if test "$(findstring -iphonesimulator,$(MULTIARCH))" != "-iphonesimulator"; then \ -+ echo "Cannot run the iOS testbed for non-simulator builds."; \ -+ exit 1;\ -+ fi -+ @if test $(PYTHONFRAMEWORK) != "Python"; then \ -+ echo "Cannot run the iOS testbed with a non-default framework name."; \ -+ exit 1;\ -+ fi -+ @if ! test -d $(PYTHONFRAMEWORKPREFIX); then \ -+ echo "Cannot find a finalized iOS Python.framework. Have you run 'make install' to finalize the framework build?"; \ -+ exit 1;\ -+ fi ++- `watchOS` as the identifier for `Platforms/Apple` invocations; e.g., ++ - `python Platforms/Apple build watchOS` ++ - `python Platforms/Apple ci watchOS` ++ - `python Platforms/Apple test watchOS` + -+ # Clone the testbed project into the XCFOLDER -+ $(PYTHON_FOR_BUILD) $(srcdir)/Apple/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)" ++- watchOS resources are stored in the `Platforms/Apple/watchOS/Resources` ++ directory + -+ # Run the testbed project -+ $(PYTHON_FOR_BUILD) "$(XCFOLDER)" run --verbose -- test -uall --single-process --rerun -W ++- The watchOS SDK version is controlled with the `WATCHOS_SDK_VERSION` environment ++ variable. + - # Like testall, but with only one pass and without multiple processes. - # Run an optional script to include information about the build environment. - .PHONY: buildbottest -@@ -1906,7 +1971,7 @@ - # which can lead to two parallel `./python setup.py build` processes that - # step on each others toes. - .PHONY: install --install: @FRAMEWORKINSTALLFIRST@ commoninstall bininstall maninstall @FRAMEWORKINSTALLLAST@ -+install: @FRAMEWORKINSTALLFIRST@ @INSTALLTARGETS@ @FRAMEWORKINSTALLLAST@ - if test "x$(ENSUREPIP)" != "xno" ; then \ - case $(ENSUREPIP) in \ - upgrade) ensurepip="--upgrade" ;; \ -@@ -2342,6 +2407,14 @@ - $(INSTALL_DATA) `cat pybuilddir.txt`/_sysconfigdata_$(ABIFLAGS)_$(MACHDEP)_$(MULTIARCH).py \ - $(DESTDIR)$(LIBDEST); \ - $(INSTALL_DATA) $(srcdir)/LICENSE $(DESTDIR)$(LIBDEST)/LICENSE.txt -+ @ # If app store compliance has been configured, apply the patch to the -+ @ # installed library code. The patch has been previously validated against -+ @ # the original source tree, so we can ignore any errors that are raised -+ @ # due to files that are missing because of --disable-test-modules etc. -+ @if [ "$(APP_STORE_COMPLIANCE_PATCH)" != "" ]; then \ -+ echo "Applying app store compliance patch"; \ -+ patch --force --reject-file "$(abs_builddir)/app-store-compliance.rej" --strip 2 --directory "$(DESTDIR)$(LIBDEST)" --input "$(abs_srcdir)/$(APP_STORE_COMPLIANCE_PATCH)" || true ; \ -+ fi - @ # Build PYC files for the 3 optimization levels (0, 1, 2) - -PYTHONPATH=$(DESTDIR)$(LIBDEST) $(RUNSHARED) \ - $(PYTHON_FOR_BUILD) -Wi $(DESTDIR)$(LIBDEST)/compileall.py \ -@@ -2518,10 +2591,11 @@ - # only have to cater for the structural bits of the framework. - - .PHONY: frameworkinstallframework --frameworkinstallframework: frameworkinstallstructure install frameworkinstallmaclib -+frameworkinstallframework: @FRAMEWORKINSTALLFIRST@ install frameworkinstallmaclib - --.PHONY: frameworkinstallstructure --frameworkinstallstructure: $(LDLIBRARY) -+# macOS uses a versioned frameworks structure that includes a full install -+.PHONY: frameworkinstallversionedstructure -+frameworkinstallversionedstructure: $(LDLIBRARY) - @if test "$(PYTHONFRAMEWORKDIR)" = no-framework; then \ - echo Not configured with --enable-framework; \ - exit 1; \ -@@ -2542,6 +2616,30 @@ - $(LN) -fsn Versions/Current/Resources $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Resources - $(INSTALL_SHARED) $(LDLIBRARY) $(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/$(LDLIBRARY) - -+# iOS/tvOS/watchOS uses a non-versioned framework with Info.plist in the -+# framework root, no .lproj data, and only stub compilation assistance binaries -+.PHONY: frameworkinstallunversionedstructure -+frameworkinstallunversionedstructure: $(LDLIBRARY) -+ @if test "$(PYTHONFRAMEWORKDIR)" = no-framework; then \ -+ echo Not configured with --enable-framework; \ -+ exit 1; \ -+ else true; \ -+ fi -+ if test -d $(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include; then \ -+ echo "Clearing stale header symlink directory"; \ -+ rm -rf $(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include; \ -+ fi -+ $(INSTALL) -d -m $(DIRMODE) $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR) -+ sed 's/%VERSION%/'"`$(RUNSHARED) $(PYTHON_FOR_BUILD) -c 'import platform; print(platform.python_version())'`"'/g' < $(RESSRCDIR)/Info.plist > $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Info.plist -+ $(INSTALL_SHARED) $(LDLIBRARY) $(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/$(LDLIBRARY) -+ $(INSTALL) -d -m $(DIRMODE) $(DESTDIR)$(LIBDIR) -+ $(LN) -fs "../$(LDLIBRARY)" "$(DESTDIR)$(prefix)/lib/libpython$(LDVERSION).dylib" -+ $(LN) -fs "../$(LDLIBRARY)" "$(DESTDIR)$(prefix)/lib/libpython$(VERSION).dylib" -+ $(INSTALL) -d -m $(DIRMODE) $(DESTDIR)$(BINDIR) -+ for file in $(srcdir)/$(RESSRCDIR)/bin/* ; do \ -+ $(INSTALL) -m $(EXEMODE) $$file $(DESTDIR)$(BINDIR); \ -+ done ++- The minimum supported watchOS SDK version is controlled with the ++ `WATCHOS_DEPLOYMENT_TARGET` environment variable. + - # This installs Mac/Lib into the framework - # Install a number of symlinks to keep software that expects a normal unix - # install (which includes python-config) happy. -@@ -2582,6 +2680,19 @@ - frameworkinstallextras: - cd Mac && $(MAKE) installextras DESTDIR="$(DESTDIR)" - -+# On iOS, bin/lib can't live inside the framework; include needs to be called -+# "Headers", but *must* be in the framework, and *not* include the `python3.X` -+# subdirectory. The install has put these folders in the same folder as -+# Python.framework; Move the headers to their final framework-compatible home. -+.PHONY: frameworkinstallmobileheaders -+frameworkinstallmobileheaders: frameworkinstallunversionedstructure inclinstall -+ if test -d $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Headers; then \ -+ echo "Removing old framework headers"; \ -+ rm -rf $(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Headers; \ -+ fi -+ mv "$(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include/python$(LDVERSION)" "$(DESTDIR)$(PYTHONFRAMEWORKINSTALLDIR)/Headers" -+ $(LN) -fs "../$(PYTHONFRAMEWORKDIR)/Headers" "$(DESTDIR)$(PYTHONFRAMEWORKPREFIX)/include/python$(LDVERSION)" ++- When referencing platforms, the watchOS compiler triples are: ++ - `arm64_32-apple-watchos` for Apple Watch devices ++ - `arm64-apple-watchos-simulator` for Apple Watch simulators on ARM64 hardware ++ - `x86_64-apple-watchos-simulator` for Apple Watch simulators on x86_64 hardware ++ ++- The watchOS testbed Xcode template project is stored in ++ `Platforms/Apple/testbed/watchOSTestbed`. +diff --git a/Platforms/Apple/watchOS/Resources/Info.plist.in b/Platforms/Apple/watchOS/Resources/Info.plist.in +new file mode 100644 +index 00000000000..b2f64710d70 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/Info.plist.in +@@ -0,0 +1,34 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleExecutable ++ @PYTHONFRAMEWORK@ ++ CFBundleGetInfoString ++ Python Runtime and Library ++ CFBundleIdentifier ++ @PYTHONFRAMEWORKIDENTIFIER@ ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundleName ++ @PYTHONFRAMEWORK@ ++ CFBundlePackageType ++ FMWK ++ CFBundleShortVersionString ++ %VERSION% ++ CFBundleLongVersionString ++ %VERSION%, (c) 2001-2023 Python Software Foundation. ++ CFBundleSignature ++ ???? ++ CFBundleVersion ++ %VERSION% ++ CFBundleSupportedPlatforms ++ ++ watchOS ++ ++ MinimumOSVersion ++ @WATCHOS_DEPLOYMENT_TARGET@ ++ ++ +diff --git a/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-ar b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-ar +new file mode 100755 +index 00000000000..dda2b211bd5 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} ar "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang +new file mode 100755 +index 00000000000..fe834d3efe4 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang -target arm64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang++ b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang++ +new file mode 100755 +index 00000000000..757f3a26d8f +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang++ -target arm64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-cpp b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-cpp +new file mode 100755 +index 00000000000..fdb57d9e010 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-cpp +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchsimulator clang -target arm64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator -E "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-strip b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-strip +new file mode 100755 +index 00000000000..e28e3f7597a +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-simulator-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} strip -arch arm64 "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-strip b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-strip +new file mode 100755 +index 00000000000..efe5a1260ad +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/arm64-apple-watchos-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk watchos${WATCHOS_SDK_VERSION} strip -arch arm64 "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-ar b/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-ar +new file mode 100755 +index 00000000000..029f9a32073 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-ar +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchos${WATCHOS_SDK_VERSION} ar "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang b/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang +new file mode 100755 +index 00000000000..285036d4010 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchos${WATCHOS_SDK_VERSION} clang -target arm64_32-apple-watchos${WATCHOS_DEPLOYMENT_TARGET} "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang++ b/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang++ +new file mode 100755 +index 00000000000..c8f60ebec51 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchos${WATCHOS_SDK_VERSION} clang++ -target arm64_32-apple-watchos${WATCHOS_DEPLOYMENT_TARGET} "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-cpp b/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-cpp +new file mode 100755 +index 00000000000..b411fc25aa4 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/arm64_32-apple-watchos-cpp +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchos${WATCHOS_SDK_VERSION} clang -target arm64_32-apple-watchos${WATCHOS_DEPLOYMENT_TARGET} -E "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-ar b/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-ar +new file mode 100755 +index 00000000000..dda2b211bd5 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} ar "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang b/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang +new file mode 100755 +index 00000000000..4776b9b5348 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang -target x86_64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang++ b/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang++ +new file mode 100755 +index 00000000000..e9b0c5f4b87 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang++ -target x86_64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-cpp b/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-cpp +new file mode 100755 +index 00000000000..d3b821c5f7f +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-cpp +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang -target x86_64-apple-watchos${WATCHOS_DEPLOYMENT_TARGET}-simulator -E "$@" +diff --git a/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-strip b/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-strip +new file mode 100755 +index 00000000000..105c78281f9 +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/bin/x86_64-apple-watchos-simulator-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} strip -arch x86_64 "$@" +diff --git a/Platforms/Apple/watchOS/Resources/pyconfig.h b/Platforms/Apple/watchOS/Resources/pyconfig.h +new file mode 100644 +index 00000000000..f842b987b2e +--- /dev/null ++++ b/Platforms/Apple/watchOS/Resources/pyconfig.h +@@ -0,0 +1,11 @@ ++#ifdef __arm64__ ++# ifdef __LP64__ ++#include "pyconfig-arm64.h" ++# else ++#include "pyconfig-arm64_32.h" ++# endif ++#endif + - # Build the toplevel Makefile - Makefile.pre: $(srcdir)/Makefile.pre.in config.status - CONFIG_FILES=Makefile.pre CONFIG_HEADERS= ./config.status -@@ -2692,6 +2803,10 @@ - -find build -type f -a ! -name '*.gc??' -exec rm -f {} ';' - -rm -f Include/pydtrace_probes.h - -rm -f profile-gen-stamp -+ -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: -@@ -2717,6 +2832,8 @@ - config.cache config.log pyconfig.h Modules/config.c - -rm -rf build platform - -rm -rf $(PYTHONFRAMEWORKDIR) -+ -rm -rf Apple/iOS/Frameworks -+ -rm -rf iOSTestbed.* - -rm -f python-config.py python-config - - # Make things extra clean, before making a distribution: -diff --git a/Modules/getpath.c b/Modules/getpath.c -index 0a310000751..83a2bc469ae 100644 ---- a/Modules/getpath.c -+++ b/Modules/getpath.c -@@ -15,6 +15,7 @@ - #endif - - #ifdef __APPLE__ -+# include "TargetConditionals.h" - # include - #endif - -@@ -759,7 +760,7 @@ - return winmodule_to_dict(dict, key, PyWin_DLLhModule); - } - #endif --#elif defined(WITH_NEXT_FRAMEWORK) -+#elif defined(WITH_NEXT_FRAMEWORK) && !defined(TARGET_OS_IPHONE) - static char modPath[MAXPATHLEN + 1]; - static int modPathInitialized = -1; - if (modPathInitialized < 0) { -@@ -953,4 +954,3 @@ - - return _PyStatus_OK(); - } -- ++#ifdef __x86_64__ ++#include "pyconfig-x86_64.h" ++#endif diff --git a/Python/marshal.c b/Python/marshal.c index 3fc3f890422..892debe38dc 100644 --- a/Python/marshal.c @@ -10491,7 +11182,7 @@ index d74fb6deac9..1bb6a05dc11 100755 # Local variables: diff --git a/configure b/configure -index 20a58907149..ab045f31d2a 100755 +index 20a58907149..bbf83268b81 100755 --- a/configure +++ b/configure @@ -976,10 +976,14 @@ @@ -10718,9 +11409,9 @@ index 20a58907149..ab045f31d2a 100755 - enableval=/Library/Frameworks + case $ac_sys_system in + Darwin) enableval=/Library/Frameworks ;; -+ iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; -+ tvOS) enableval=Apple/tvOS/Frameworks/\$\(MULTIARCH\) ;; -+ watchOS) enableval=Apple/watchOS/Frameworks/\$\(MULTIARCH\) ;; ++ iOS) enableval=Platforms/Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; ++ tvOS) enableval=Platforms/Apple/tvOS/Frameworks/\$\(MULTIARCH\) ;; ++ watchOS) enableval=Platforms/Apple/watchOS/Frameworks/\$\(MULTIARCH\) ;; + *) as_fn_error $? "Unknown platform for framework build" "$LINENO" 5 + esac esac @@ -10780,19 +11471,10 @@ index 20a58907149..ab045f31d2a 100755 - - if test "x${prefix}" = "xNONE" ; then - FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" -- + - else - FRAMEWORKUNIXTOOLSPREFIX="${prefix}" - fi - -- case "${enableval}" in -- /System*) -- FRAMEWORKINSTALLAPPSPREFIX="/Applications" -- if test "${prefix}" = "NONE" ; then -- # See below -- FRAMEWORKUNIXTOOLSPREFIX="/usr" -- fi -- ;; + case $ac_sys_system in #( + Darwin) : + FRAMEWORKINSTALLFIRST="frameworkinstallversionedstructure" @@ -10810,8 +11492,13 @@ index 20a58907149..ab045f31d2a 100755 + FRAMEWORKUNIXTOOLSPREFIX="${prefix}" + fi -- /Library*) +- case "${enableval}" in +- /System*) - FRAMEWORKINSTALLAPPSPREFIX="/Applications" +- if test "${prefix}" = "NONE" ; then +- # See below +- FRAMEWORKUNIXTOOLSPREFIX="/usr" +- fi - ;; + case "${enableval}" in + /System*) @@ -10842,6 +11529,10 @@ index 20a58907149..ab045f31d2a 100755 + fi + ;; +- /Library*) +- FRAMEWORKINSTALLAPPSPREFIX="/Applications" +- ;; +- - */Library/Frameworks) - MDIR="`dirname "${enableval}"`" - MDIR="`dirname "${MDIR}"`" @@ -10890,9 +11581,9 @@ index 20a58907149..ab045f31d2a 100755 + + prefix=$PYTHONFRAMEWORKPREFIX + PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" -+ RESSRCDIR=Apple/iOS/Resources ++ RESSRCDIR=Platforms/Apple/iOS/Resources + -+ ac_config_files="$ac_config_files Apple/iOS/Resources/Info.plist" ++ ac_config_files="$ac_config_files Platforms/Apple/iOS/Resources/Info.plist" + + ;; + tvOS) : @@ -10905,9 +11596,9 @@ index 20a58907149..ab045f31d2a 100755 + + prefix=$PYTHONFRAMEWORKPREFIX + PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" -+ RESSRCDIR=Apple/tvOS/Resources ++ RESSRCDIR=Platforms/Apple/tvOS/Resources + -+ ac_config_files="$ac_config_files Apple/tvOS/Resources/Info.plist" ++ ac_config_files="$ac_config_files Platforms/Apple/tvOS/Resources/Info.plist" + + ;; + watchOS) : @@ -10920,9 +11611,9 @@ index 20a58907149..ab045f31d2a 100755 + + prefix=$PYTHONFRAMEWORKPREFIX + PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" -+ RESSRCDIR=Apple/watchOS/Resources ++ RESSRCDIR=Platforms/Apple/watchOS/Resources + -+ ac_config_files="$ac_config_files Apple/watchOS/Resources/Info.plist" ++ ac_config_files="$ac_config_files Platforms/Apple/watchOS/Resources/Info.plist" + + ;; + *) @@ -11741,7 +12432,7 @@ index 20a58907149..ab045f31d2a 100755 fi ac_fn_c_check_func "$LINENO" "umask" "ac_cv_func_umask" -@@ -18670,6 +19033,85 @@ +@@ -18670,6 +19033,91 @@ fi @@ -11750,13 +12441,7 @@ index 20a58907149..ab045f31d2a 100755 +# header definition prevents usage - autoconf doesn't use the headers), or +# raise an error if used at runtime. Force these symbols off. +if test "$ac_sys_system" != "iOS" -a "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "watchOS" ; then -+ ac_fn_c_check_func "$LINENO" "dupe3" "ac_cv_func_dupe3" -+if test "x$ac_cv_func_dupe3" = xyes -+then : -+ printf "%s\n" "#define HAVE_DUPE3 1" >>confdefs.h -+ -+fi -+ac_fn_c_check_func "$LINENO" "getentropy" "ac_cv_func_getentropy" ++ ac_fn_c_check_func "$LINENO" "getentropy" "ac_cv_func_getentropy" +if test "x$ac_cv_func_getentropy" = xyes +then : + printf "%s\n" "#define HAVE_GETENTROPY 1" >>confdefs.h @@ -11768,12 +12453,6 @@ index 20a58907149..ab045f31d2a 100755 + printf "%s\n" "#define HAVE_GETGROUPS 1" >>confdefs.h + +fi -+ac_fn_c_check_func "$LINENO" "pipe2" "ac_cv_func_pipe2" -+if test "x$ac_cv_func_pipe2" = xyes -+then : -+ printf "%s\n" "#define HAVE_PIPE2 1" >>confdefs.h -+ -+fi +ac_fn_c_check_func "$LINENO" "system" "ac_cv_func_system" +if test "x$ac_cv_func_system" = xyes +then : @@ -11782,6 +12461,24 @@ index 20a58907149..ab045f31d2a 100755 +fi + +fi ++# In addition, macOS introduced dup3 and pipe2 in macOS 27, late in the ++# lifetime of this version of Python. Rather than adding weaklinking ++# support for them, just continue to ignore them. ++if test "$ac_sys_system" != "iOS" -a "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "visionOS" -a "$ac_sys_system" != "watchOS" -a "$ac_sys_system" != "Darwin"; then ++ ac_fn_c_check_func "$LINENO" "dup3" "ac_cv_func_dup3" ++if test "x$ac_cv_func_dup3" = xyes ++then : ++ printf "%s\n" "#define HAVE_DUP3 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "pipe2" "ac_cv_func_pipe2" ++if test "x$ac_cv_func_pipe2" = xyes ++then : ++ printf "%s\n" "#define HAVE_PIPE2 1" >>confdefs.h ++ ++fi ++ ++fi + +# tvOS/watchOS have some additional methods that can be found, but not used. +if test "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "watchOS" ; then @@ -11827,7 +12524,7 @@ index 20a58907149..ab045f31d2a 100755 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC options needed to detect all undeclared functions" >&5 printf %s "checking for $CC options needed to detect all undeclared functions... " >&6; } if test ${ac_cv_c_undeclared_builtin_options+y} -@@ -21422,7 +21864,8 @@ +@@ -21422,7 +21870,8 @@ # check for openpty, login_tty, and forkpty @@ -11837,7 +12534,7 @@ index 20a58907149..ab045f31d2a 100755 for ac_func in openpty do : -@@ -21518,7 +21961,7 @@ +@@ -21518,7 +21967,7 @@ fi done @@ -11846,7 +12543,7 @@ index 20a58907149..ab045f31d2a 100755 printf %s "checking for library containing login_tty... " >&6; } if test ${ac_cv_search_login_tty+y} then : -@@ -21675,6 +22118,7 @@ +@@ -21675,6 +22124,7 @@ fi done @@ -11854,7 +12551,7 @@ index 20a58907149..ab045f31d2a 100755 # check for long file support functions ac_fn_c_check_func "$LINENO" "fseek64" "ac_cv_func_fseek64" -@@ -22222,6 +22666,11 @@ +@@ -22222,6 +22672,11 @@ done @@ -11866,7 +12563,7 @@ index 20a58907149..ab045f31d2a 100755 for ac_func in clock_settime do : -@@ -22232,7 +22681,7 @@ +@@ -22232,7 +22687,7 @@ else $as_nop @@ -11875,7 +12572,7 @@ index 20a58907149..ab045f31d2a 100755 printf %s "checking for clock_settime in -lrt... " >&6; } if test ${ac_cv_lib_rt_clock_settime+y} then : -@@ -22270,7 +22719,7 @@ +@@ -22270,7 +22725,7 @@ if test "x$ac_cv_lib_rt_clock_settime" = xyes then : @@ -11884,7 +12581,7 @@ index 20a58907149..ab045f31d2a 100755 fi -@@ -22279,6 +22728,7 @@ +@@ -22279,6 +22734,7 @@ fi done @@ -11892,7 +12589,7 @@ index 20a58907149..ab045f31d2a 100755 for ac_func in clock_nanosleep -@@ -22500,7 +22950,9 @@ +@@ -22500,7 +22956,9 @@ if test "$cross_compiling" = yes then : @@ -11903,7 +12600,7 @@ index 20a58907149..ab045f31d2a 100755 ac_cv_buggy_getaddrinfo="no -- configured with --(en|dis)able-ipv6" else ac_cv_buggy_getaddrinfo=yes -@@ -24404,7 +24856,7 @@ +@@ -24404,7 +24862,7 @@ printf "%s\n" "$ABIFLAGS" >&6; } { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking SOABI" >&5 printf %s "checking SOABI... " >&6; } @@ -11912,7 +12609,7 @@ index 20a58907149..ab045f31d2a 100755 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $SOABI" >&5 printf "%s\n" "$SOABI" >&6; } -@@ -24412,7 +24864,7 @@ +@@ -24412,7 +24870,7 @@ if test "$Py_DEBUG" = 'true' -a "$with_trace_refs" != "yes"; then # Similar to SOABI but remove "d" flag from ABIFLAGS @@ -11921,7 +12618,7 @@ index 20a58907149..ab045f31d2a 100755 printf "%s\n" "#define ALT_SOABI \"${ALT_SOABI}\"" >>confdefs.h -@@ -27163,24 +27615,28 @@ +@@ -27163,24 +27621,28 @@ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for device files" >&5 printf "%s\n" "$as_me: checking for device files" >&6;} @@ -11962,7 +12659,7 @@ index 20a58907149..ab045f31d2a 100755 printf %s "checking for /dev/ptmx... " >&6; } if test ${ac_cv_file__dev_ptmx+y} then : -@@ -27201,12 +27657,12 @@ +@@ -27201,12 +27663,12 @@ fi @@ -11978,7 +12675,7 @@ index 20a58907149..ab045f31d2a 100755 printf %s "checking for /dev/ptc... " >&6; } if test ${ac_cv_file__dev_ptc+y} then : -@@ -27227,10 +27683,11 @@ +@@ -27227,10 +27689,11 @@ fi @@ -11991,7 +12688,7 @@ index 20a58907149..ab045f31d2a 100755 fi if test $ac_sys_system = Darwin -@@ -27672,6 +28129,8 @@ +@@ -27672,6 +28135,8 @@ with_ensurepip=no ;; #( WASI) : with_ensurepip=no ;; #( @@ -12000,7 +12697,7 @@ index 20a58907149..ab045f31d2a 100755 *) : with_ensurepip=upgrade ;; -@@ -28613,6 +29072,27 @@ +@@ -28613,6 +29078,27 @@ py_cv_module_ossaudiodev=n/a py_cv_module_spwd=n/a ;; #( @@ -12028,18 +12725,18 @@ index 20a58907149..ab045f31d2a 100755 CYGWIN*) : -@@ -32359,6 +32839,9 @@ +@@ -32359,6 +32845,9 @@ "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" ;; -+ "Apple/iOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/iOS/Resources/Info.plist" ;; -+ "Apple/tvOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/tvOS/Resources/Info.plist" ;; -+ "Apple/watchOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/watchOS/Resources/Info.plist" ;; ++ "Platforms/Apple/iOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Platforms/Apple/iOS/Resources/Info.plist" ;; ++ "Platforms/Apple/tvOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Platforms/Apple/tvOS/Resources/Info.plist" ;; ++ "Platforms/Apple/watchOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Platforms/Apple/watchOS/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 4cbd0ae16a5..683cf2f1dea 100644 +index 4cbd0ae16a5..3ba00b44b30 100644 --- a/configure.ac +++ b/configure.ac @@ -307,6 +307,161 @@ @@ -12211,9 +12908,9 @@ index 4cbd0ae16a5..683cf2f1dea 100644 - enableval=/Library/Frameworks + case $ac_sys_system in + Darwin) enableval=/Library/Frameworks ;; -+ iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; -+ tvOS) enableval=Apple/tvOS/Frameworks/\$\(MULTIARCH\) ;; -+ watchOS) enableval=Apple/watchOS/Frameworks/\$\(MULTIARCH\) ;; ++ iOS) enableval=Platforms/Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; ++ tvOS) enableval=Platforms/Apple/tvOS/Frameworks/\$\(MULTIARCH\) ;; ++ watchOS) enableval=Platforms/Apple/watchOS/Frameworks/\$\(MULTIARCH\) ;; + *) AC_MSG_ERROR([Unknown platform for framework build]) + esac esac @@ -12379,9 +13076,9 @@ index 4cbd0ae16a5..683cf2f1dea 100644 + + prefix=$PYTHONFRAMEWORKPREFIX + PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" -+ RESSRCDIR=Apple/iOS/Resources ++ RESSRCDIR=Platforms/Apple/iOS/Resources + -+ AC_CONFIG_FILES([Apple/iOS/Resources/Info.plist]) ++ AC_CONFIG_FILES([Platforms/Apple/iOS/Resources/Info.plist]) + ;; + tvOS) : + FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" @@ -12393,9 +13090,9 @@ index 4cbd0ae16a5..683cf2f1dea 100644 + + prefix=$PYTHONFRAMEWORKPREFIX + PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" -+ RESSRCDIR=Apple/tvOS/Resources ++ RESSRCDIR=Platforms/Apple/tvOS/Resources + -+ AC_CONFIG_FILES([Apple/tvOS/Resources/Info.plist]) ++ AC_CONFIG_FILES([Platforms/Apple/tvOS/Resources/Info.plist]) + ;; + watchOS) : + FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" @@ -12407,9 +13104,9 @@ index 4cbd0ae16a5..683cf2f1dea 100644 + + prefix=$PYTHONFRAMEWORKPREFIX + PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" -+ RESSRCDIR=Apple/watchOS/Resources ++ RESSRCDIR=Platforms/Apple/watchOS/Resources + -+ AC_CONFIG_FILES([Apple/watchOS/Resources/Info.plist]) ++ AC_CONFIG_FILES([Platforms/Apple/watchOS/Resources/Info.plist]) + ;; + *) + AC_MSG_ERROR([Unknown platform for framework build]) @@ -13108,7 +13805,7 @@ index 4cbd0ae16a5..683cf2f1dea 100644 wait wait3 wait4 waitid waitpid wcscoll wcsftime wcsxfrm wmemcmp writev \ ]) -@@ -4952,6 +5342,22 @@ +@@ -4952,6 +5342,28 @@ AC_CHECK_FUNCS([lchmod]) fi @@ -13117,7 +13814,13 @@ index 4cbd0ae16a5..683cf2f1dea 100644 +# header definition prevents usage - autoconf doesn't use the headers), or +# raise an error if used at runtime. Force these symbols off. +if test "$ac_sys_system" != "iOS" -a "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "watchOS" ; then -+ AC_CHECK_FUNCS([dupe3 getentropy getgroups pipe2 system]) ++ AC_CHECK_FUNCS([getentropy getgroups system]) ++fi ++# In addition, macOS introduced dup3 and pipe2 in macOS 27, late in the ++# lifetime of this version of Python. Rather than adding weaklinking ++# support for them, just continue to ignore them. ++if test "$ac_sys_system" != "iOS" -a "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "visionOS" -a "$ac_sys_system" != "watchOS" -a "$ac_sys_system" != "Darwin"; then ++ AC_CHECK_FUNCS([dup3 pipe2]) +fi + +# tvOS/watchOS have some additional methods that can be found, but not used. @@ -13131,7 +13834,7 @@ index 4cbd0ae16a5..683cf2f1dea 100644 AC_CHECK_DECL([dirfd], [AC_DEFINE([HAVE_DIRFD], [1], [Define if you have the 'dirfd' function or macro.])], -@@ -5193,20 +5599,22 @@ +@@ -5193,20 +5605,22 @@ ]) # check for openpty, login_tty, and forkpty @@ -13168,7 +13871,7 @@ index 4cbd0ae16a5..683cf2f1dea 100644 # check for long file support functions AC_CHECK_FUNCS([fseek64 fseeko fstatvfs ftell64 ftello statvfs]) -@@ -5289,11 +5697,17 @@ +@@ -5289,11 +5703,17 @@ ]) ]) @@ -13191,7 +13894,7 @@ index 4cbd0ae16a5..683cf2f1dea 100644 AC_CHECK_FUNCS([clock_nanosleep], [], [ AC_CHECK_LIB([rt], [clock_nanosleep], [ -@@ -5439,7 +5853,9 @@ +@@ -5439,7 +5859,9 @@ [ac_cv_buggy_getaddrinfo=no], [ac_cv_buggy_getaddrinfo=yes], [ @@ -13202,7 +13905,7 @@ index 4cbd0ae16a5..683cf2f1dea 100644 ac_cv_buggy_getaddrinfo="no -- configured with --(en|dis)able-ipv6" else ac_cv_buggy_getaddrinfo=yes -@@ -5992,14 +6408,14 @@ +@@ -5992,14 +6414,14 @@ AC_MSG_CHECKING([ABIFLAGS]) AC_MSG_RESULT([$ABIFLAGS]) AC_MSG_CHECKING([SOABI]) @@ -13219,7 +13922,7 @@ index 4cbd0ae16a5..683cf2f1dea 100644 AC_DEFINE_UNQUOTED([ALT_SOABI], ["${ALT_SOABI}"], [Alternative SOABI used in debug build to load C extensions built in release mode]) fi -@@ -6648,28 +7064,35 @@ +@@ -6648,28 +7070,35 @@ AC_MSG_NOTICE([checking for device files]) dnl NOTE: Inform user how to proceed with files when cross compiling. @@ -13275,7 +13978,7 @@ index 4cbd0ae16a5..683cf2f1dea 100644 fi if test $ac_sys_system = Darwin -@@ -6941,6 +7364,7 @@ +@@ -6941,6 +7370,7 @@ AS_CASE([$ac_sys_system], [Emscripten], [with_ensurepip=no], [WASI], [with_ensurepip=no], @@ -13283,7 +13986,7 @@ index 4cbd0ae16a5..683cf2f1dea 100644 [with_ensurepip=upgrade] ) ]) -@@ -7283,6 +7707,28 @@ +@@ -7283,6 +7713,28 @@ [AIX], [PY_STDLIB_MOD_SET_NA([_scproxy], [spwd])], [VxWorks*], [PY_STDLIB_MOD_SET_NA([_scproxy], [_crypt], [termios], [grp])], [Darwin], [PY_STDLIB_MOD_SET_NA([ossaudiodev], [spwd])], @@ -13312,123 +14015,3 @@ index 4cbd0ae16a5..683cf2f1dea 100644 [CYGWIN*], [PY_STDLIB_MOD_SET_NA([_scproxy], [nis])], [QNX*], [PY_STDLIB_MOD_SET_NA([_scproxy], [nis])], [FreeBSD*], [PY_STDLIB_MOD_SET_NA([_scproxy], [spwd])], -diff --git a/iOS/Resources/bin/arm64-apple-ios-ar b/iOS/Resources/bin/arm64-apple-ios-ar -new file mode 100755 -index 00000000000..3cf3eb21874 ---- /dev/null -+++ b/iOS/Resources/bin/arm64-apple-ios-ar -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@" -diff --git a/iOS/Resources/bin/arm64-apple-ios-clang b/iOS/Resources/bin/arm64-apple-ios-clang -new file mode 100755 -index 00000000000..f50d5b5142f ---- /dev/null -+++ b/iOS/Resources/bin/arm64-apple-ios-clang -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" -diff --git a/iOS/Resources/bin/arm64-apple-ios-clang++ b/iOS/Resources/bin/arm64-apple-ios-clang++ -new file mode 100755 -index 00000000000..0794731d7dc ---- /dev/null -+++ b/iOS/Resources/bin/arm64-apple-ios-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" -diff --git a/iOS/Resources/bin/arm64-apple-ios-cpp b/iOS/Resources/bin/arm64-apple-ios-cpp -new file mode 100755 -index 00000000000..24fa1506bab ---- /dev/null -+++ b/iOS/Resources/bin/arm64-apple-ios-cpp -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} -E "$@" -diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-ar b/iOS/Resources/bin/arm64-apple-ios-simulator-ar -new file mode 100755 -index 00000000000..b836b6db902 ---- /dev/null -+++ b/iOS/Resources/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/arm64-apple-ios-simulator-clang b/iOS/Resources/bin/arm64-apple-ios-simulator-clang -new file mode 100755 -index 00000000000..4891a00876e ---- /dev/null -+++ b/iOS/Resources/bin/arm64-apple-ios-simulator-clang -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ b/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ -new file mode 100755 -index 00000000000..58b2a5f6f18 ---- /dev/null -+++ b/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-cpp b/iOS/Resources/bin/arm64-apple-ios-simulator-cpp -new file mode 100755 -index 00000000000..c9df94e8b7c ---- /dev/null -+++ b/iOS/Resources/bin/arm64-apple-ios-simulator-cpp -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" -diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-strip b/iOS/Resources/bin/arm64-apple-ios-simulator-strip -new file mode 100755 -index 00000000000..fd59d309b73 ---- /dev/null -+++ b/iOS/Resources/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/arm64-apple-ios-strip b/iOS/Resources/bin/arm64-apple-ios-strip -new file mode 100755 -index 00000000000..75e823a3d02 ---- /dev/null -+++ b/iOS/Resources/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/x86_64-apple-ios-simulator-ar b/iOS/Resources/bin/x86_64-apple-ios-simulator-ar -new file mode 100755 -index 00000000000..b836b6db902 ---- /dev/null -+++ b/iOS/Resources/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/x86_64-apple-ios-simulator-clang b/iOS/Resources/bin/x86_64-apple-ios-simulator-clang -new file mode 100755 -index 00000000000..f4739a7b945 ---- /dev/null -+++ b/iOS/Resources/bin/x86_64-apple-ios-simulator-clang -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ b/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ -new file mode 100755 -index 00000000000..c348ae4c103 ---- /dev/null -+++ b/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" -diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp b/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp -new file mode 100755 -index 00000000000..6d7f8084c9f ---- /dev/null -+++ b/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" -diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-strip b/iOS/Resources/bin/x86_64-apple-ios-simulator-strip -new file mode 100755 -index 00000000000..c5cfb289291 ---- /dev/null -+++ b/iOS/Resources/bin/x86_64-apple-ios-simulator-strip -@@ -0,0 +1,2 @@ -+#!/bin/sh -+xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch x86_64 "$@"