diff --git a/.github/workflows/build-python-version.yml b/.github/workflows/build-python-version.yml index 977f4dd..a643693 100644 --- a/.github/workflows/build-python-version.yml +++ b/.github/workflows/build-python-version.yml @@ -14,13 +14,16 @@ on: required: true type: choice options: - - 3.12.12 - - 3.13.12 - - 3.14.3 + - 3.12.13 + - 3.13.13 + - 3.14.5 env: PYTHON_VERSION: ${{ inputs.python_version || github.event.inputs.python_version }} - PYTHON_DIST_RELEASE: 20260203 # https://github.com/astral-sh/python-build-standalone/releases + # Optional override: pin a specific python-build-standalone release date + # (https://github.com/astral-sh/python-build-standalone/releases). When empty, + # linux/package-for-linux.sh auto-resolves the newest release for PYTHON_VERSION. + PYTHON_DIST_RELEASE: "" permissions: contents: read @@ -28,7 +31,7 @@ permissions: jobs: build-darwin: name: Build Python for iOS and macOS - runs-on: macos-15 + runs-on: macos-26 steps: - name: Checkout uses: actions/checkout@v4 @@ -47,17 +50,21 @@ jobs: working-directory: darwin shell: bash run: | - git clone --branch="$PYTHON_VERSION_SHORT" https://github.com/beeware/Python-Apple-support.git mkdir -p dist - pushd Python-Apple-support - make iOS - tar -czf ../dist/python-ios-mobile-forge-$PYTHON_VERSION_SHORT.tar.gz install support -C . - make macOS - popd + # iOS: 3.14+ uses CPython's in-tree Apple tooling, 3.13 builds from source per + # iOS/README.rst, 3.12 uses beeware. Emits normalized ./install + ./support. + python build_ios.py "$PYTHON_VERSION" - bash ./package-ios-for-dart.sh Python-Apple-support "$PYTHON_VERSION_SHORT" - bash ./package-macos-for-dart.sh Python-Apple-support "$PYTHON_VERSION_SHORT" + # mobile-forge artifact: iOS-only install+support tree (same structure as before). + # Captured before the macOS build also writes into ./support / ./install. + tar -czf dist/python-ios-mobile-forge-$PYTHON_VERSION_SHORT.tar.gz install support + + # macOS: universal2 framework built from source (all versions). + python build_macos.py "$PYTHON_VERSION" + + bash ./package-ios-for-dart.sh . "$PYTHON_VERSION_SHORT" + bash ./package-macos-for-dart.sh . "$PYTHON_VERSION_SHORT" - name: Upload Darwin build artifacts uses: actions/upload-artifact@v4 @@ -138,6 +145,9 @@ jobs: - working-directory: linux shell: bash + env: + # Lets resolve_pbs.py authenticate to the GitHub API and avoid rate limits. + GITHUB_TOKEN: ${{ github.token }} run: | bash ./package-for-linux.sh x86_64 "_v2" bash ./package-for-linux.sh aarch64 "" @@ -183,12 +193,14 @@ jobs: publish-release: name: Publish Release Assets runs-on: ubuntu-latest + # Only publish GitHub release assets from the main branch; other branches still + # build (and upload per-job artifacts) but don't touch releases. + if: github.ref == 'refs/heads/main' needs: - build-darwin - build-android - build-linux - build-windows - if: github.ref == 'refs/heads/main' permissions: contents: write steps: diff --git a/.github/workflows/build-python.yml b/.github/workflows/build-python.yml index 89e6454..1a4f015 100644 --- a/.github/workflows/build-python.yml +++ b/.github/workflows/build-python.yml @@ -17,9 +17,9 @@ jobs: fail-fast: false matrix: python_version: - - 3.12.12 - - 3.13.12 - - 3.14.3 + - 3.12.13 + - 3.13.13 + - 3.14.5 uses: ./.github/workflows/build-python-version.yml with: python_version: ${{ matrix.python_version }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/android/build.sh b/android/build.sh index a6c1518..ef6d2ff 100755 --- a/android/build.sh +++ b/android/build.sh @@ -187,6 +187,18 @@ else Android/android.py configure-host "$HOST" Android/android.py make-host "$HOST" cp -a "cross-build/$HOST/prefix/"* "$PREFIX" + + # CPython's official Android tooling builds OpenSSL/bzip2/libffi/xz/sqlite + # alongside Python and dumps them intermixed into $PREFIX/{include,lib}/. + # mobile-forge's make_dep_wheels.py expects each as a sibling per-lib + # install dir with its own {include,lib}/ so it can produce + # ---py3-none-android_*.whl wheels that satisfy recipe host + # requirements (e.g. cryptography's `openssl>=3.0.12`). Reorganize so the + # 3.13+ install layout matches the 3.12 contract. + python3 "$script_dir/extract_mobile_forge_deps.py" "$PREFIX" \ + --abi "$abi" \ + --version-short "$version_short" \ + --support-root "$script_dir" fi if [ -z "${toolchain:-}" ] && [ -n "${NDK_HOME:-}" ]; then diff --git a/android/extract_mobile_forge_deps.py b/android/extract_mobile_forge_deps.py new file mode 100644 index 0000000..6cd0c01 --- /dev/null +++ b/android/extract_mobile_forge_deps.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +"""Extract bundled native libs from CPython 3.13+'s Android install into the +sibling per-lib layout that mobile-forge ``make_dep_wheels.py`` expects. + +CPython's official Android tooling (used by python-build for 3.13+) builds +OpenSSL, bzip2, libffi, xz/lzma, and SQLite as part of the host build, but +installs the headers/libs intermixed inside the Python install. mobile-forge +expects each dep as a sibling ``install/android//--/`` +directory with its own ``include/`` and ``lib/`` so it can repackage each as +an ``---py3-none-android_*.whl``. + +This script reorganizes after the fact: for each known dep, detect its version +from headers/pkgconfig, materialize the sibling directory, copy the right +files, and append a matching line to ``support//android/VERSIONS``. + +Idempotent: re-running on an already-organized tree replaces the sibling +directories in place and de-duplicates the VERSIONS entries. +""" + +from __future__ import annotations + +import argparse +import re +import shutil +import sys +from pathlib import Path +from typing import Callable + + +# Default build number for python-build-emitted dep wheels for 3.13+. +# Starts fresh at 1, independent of the 3.12 counter (where openssl had reached +# `-4`). Bump per-dep in the future if a rebuild changes the contents. +DEFAULT_BUILD_NUMBER = 1 + + +def _version_from_header(prefix: Path, header: str, pattern: str) -> str | None: + path = prefix / header + if not path.is_file(): + return None + match = re.search(pattern, path.read_text()) + return match.group(1) if match else None + + +def _openssl_version(prefix: Path) -> str | None: + return _version_from_header( + prefix, "include/openssl/opensslv.h", + r'OPENSSL_VERSION_STR\s+"([0-9]+\.[0-9]+\.[0-9]+)"', + ) + + +def _libffi_version(prefix: Path) -> str | None: + pc = prefix / "lib/pkgconfig/libffi.pc" + if not pc.is_file(): + return None + match = re.search(r"^Version:\s*([0-9.]+)", pc.read_text(), re.MULTILINE) + return match.group(1) if match else None + + +def _lzma_version(prefix: Path) -> str | None: + header = prefix / "include/lzma/version.h" + if not header.is_file(): + return None + text = header.read_text() + parts = [] + for component in ("MAJOR", "MINOR", "PATCH"): + m = re.search(rf"^#define\s+LZMA_VERSION_{component}\s+(\d+)", text, re.MULTILINE) + if not m: + return None + parts.append(m.group(1)) + return ".".join(parts) + + +def _sqlite_version(prefix: Path) -> str | None: + return _version_from_header( + prefix, "include/sqlite3.h", + r'SQLITE_VERSION\s+"([0-9.]+)"', + ) + + +def _bzip2_version(prefix: Path) -> str | None: + # bzlib.h carries no version macro. CPython 3.13/3.14's official Android + # tooling pins bzip2 1.0.8 (unchanged upstream since 2019). Return the + # pinned version if the lib is present. + if (prefix / "lib/libbz2.a").is_file() and (prefix / "include/bzlib.h").is_file(): + return "1.0.8" + return None + + +# (lib_name, version_resolver, [(src_rel_to_prefix, dst_rel_to_dep_dir), ...]). +# Missing sources are silently skipped — that's how a tree built without one of +# the optional deps still produces an internally consistent VERSIONS file. +DEPS: list[tuple[str, Callable[[Path], str | None], list[tuple[str, str]]]] = [ + ( + "openssl", + _openssl_version, + [ + ("include/openssl", "include/openssl"), + ("lib/libcrypto.a", "lib/libcrypto.a"), + ("lib/libssl.a", "lib/libssl.a"), + ], + ), + ( + "bzip2", + _bzip2_version, + [ + ("include/bzlib.h", "include/bzlib.h"), + ("lib/libbz2.a", "lib/libbz2.a"), + ], + ), + ( + "libffi", + _libffi_version, + [ + ("include/ffi.h", "include/ffi.h"), + ("include/ffitarget.h", "include/ffitarget.h"), + ("lib/libffi.a", "lib/libffi.a"), + ("lib/pkgconfig/libffi.pc", "lib/pkgconfig/libffi.pc"), + ], + ), + ( + "xz", + _lzma_version, + [ + ("include/lzma.h", "include/lzma.h"), + ("include/lzma", "include/lzma"), + ("lib/liblzma.a", "lib/liblzma.a"), + ], + ), + ( + "sqlite", + _sqlite_version, + [ + ("include/sqlite3.h", "include/sqlite3.h"), + ("include/sqlite3ext.h", "include/sqlite3ext.h"), + ("lib/libsqlite3.so", "lib/libsqlite3.so"), + ], + ), +] + + +def _copy(src: Path, dst: Path) -> None: + dst.parent.mkdir(parents=True, exist_ok=True) + if dst.exists() or dst.is_symlink(): + if dst.is_dir() and not dst.is_symlink(): + shutil.rmtree(dst) + else: + dst.unlink() + if src.is_dir(): + shutil.copytree(src, dst) + else: + shutil.copy2(src, dst) + + +def extract(prefix: Path, abi: str, version_short: str, support_versions: Path, + build_number: int = DEFAULT_BUILD_NUMBER) -> list[str]: + sibling_root = prefix.parent + written: list[str] = [] + + for lib_name, version_resolver, files in DEPS: + version = version_resolver(prefix) + if not version: + continue + + dep_dir = sibling_root / f"{lib_name}-{version}-{build_number}" + if dep_dir.exists(): + shutil.rmtree(dep_dir) + + any_copied = False + for src_rel, dst_rel in files: + src = prefix / src_rel + if not src.exists(): + continue + _copy(src, dep_dir / dst_rel) + any_copied = True + + if not any_copied: + continue + + entry = f"{lib_name}: {version}-{build_number}" + written.append(entry) + print(f" wrote {dep_dir.relative_to(sibling_root.parent.parent)} ({entry})") + + _update_versions_file(support_versions, written) + return written + + +def _update_versions_file(path: Path, new_entries: list[str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + existing = path.read_text() if path.is_file() else "" + + keep: list[str] = [] + seen_keys: set[str] = set() + for line in existing.splitlines(): + stripped = line.strip() + if not stripped: + keep.append(line) + continue + key = stripped.split(":", 1)[0].strip().lower() + if any(key == entry.split(":", 1)[0].strip().lower() for entry in new_entries): + # Skip any pre-existing line for a dep we're about to re-emit. + continue + keep.append(line) + seen_keys.add(key) + + # Ensure the file ends with a newline before appending. + body = "\n".join(keep) + if body and not body.endswith("\n"): + body += "\n" + + for entry in new_entries: + body += entry + "\n" + + path.write_text(body) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("prefix", type=Path, + help="Path to the Python install root, e.g. " + "install/android//python-") + parser.add_argument("--abi", required=True, + help="Android ABI (arm64-v8a, x86_64, etc.) — used in log output.") + parser.add_argument("--version-short", required=True, + help="Python short version, e.g. 3.13 — used to locate the " + "support//android/VERSIONS file.") + parser.add_argument("--support-root", type=Path, + help="Root that contains support//android/VERSIONS. " + "Defaults to two levels above the prefix's grandparent " + "(i.e. python-build/android).") + parser.add_argument("--build-number", type=int, default=DEFAULT_BUILD_NUMBER, + help=f"Build number suffix (default: {DEFAULT_BUILD_NUMBER}).") + args = parser.parse_args() + + prefix = args.prefix.resolve() + if not prefix.is_dir(): + print(f"error: prefix is not a directory: {prefix}", file=sys.stderr) + return 2 + + # prefix = .../android/install/android//python- + # support root = .../android (3 levels up from ) + support_root = args.support_root.resolve() if args.support_root else prefix.parents[3] + support_versions = support_root / "support" / args.version_short / "android" / "VERSIONS" + + print(f"extracting bundled native deps from {prefix.name} ({args.abi}) ...") + written = extract(prefix, args.abi, args.version_short, support_versions, + build_number=args.build_number) + if not written: + print(" no deps found") + print(f" VERSIONS file: {support_versions}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/android/normalize_mobile_forge_install.py b/android/normalize_mobile_forge_install.py index 60f6cda..9399afe 100644 --- a/android/normalize_mobile_forge_install.py +++ b/android/normalize_mobile_forge_install.py @@ -25,6 +25,7 @@ from __future__ import annotations import argparse +import json import os from pathlib import Path @@ -238,6 +239,52 @@ def _local_toolchain(): path.write_text(text + block) +def rewrite_build_details_json(prefix: Path) -> None: + """Re-anchor absolute paths in ``build-details.json`` (Python 3.14+). + + CPython's official Android tooling (used on the 3.13+ build path) emits + ``lib/python/build-details.json`` alongside the per-version + sysconfigdata. Consumers like ``maturin`` read this JSON for + cross-compilation — most notably the ``libpython.dynamic`` / + ``libpython.dynamic_stableabi`` paths, which become the ``-L`` argument + to the consumer's linker. Every absolute path in the file points at + python-build CI's build-time install root (currently ``/usr/local``, + but read from the JSON so we don't bake an assumption); on every + consumer machine that path is empty, so the linker fails with + ``unable to find library -lpython3`` and the build dies. + + Re-anchor each absolute path field by replacing the build-time prefix + (read from ``base_prefix`` in the JSON itself) with the on-disk install + prefix. Reading the substitution source out of the file rather than + hard-coding ``/usr/local`` matches the narrow-prefix discipline the + sysconfig relocator follows: an upstream CPython change that moves the + Android tooling's install root won't silently miss the rewrite, and + nothing else under the install prefix's namespace can be accidentally + rewritten (e.g. a future ``c_api.headers`` value that happens to live + under a path sharing a prefix substring with an unrelated tree). + + Idempotent: once ``base_prefix`` has been re-anchored at ``str(prefix)``, + the build-time pattern no longer appears in the JSON and re-running the + substitution is a no-op. + """ + candidates = sorted(prefix.glob("lib/python*/build-details.json")) + if not candidates: + return + prefix_str = str(prefix) + for path in candidates: + data = json.loads(path.read_text()) + build_time_prefix = data.get("base_prefix") + if not build_time_prefix or build_time_prefix == prefix_str: + # Either the file has no `base_prefix` to anchor on (unexpected + # for a CPython-emitted build-details.json) or we already + # rewrote this file in a prior pass — either way, nothing to do. + continue + text = path.read_text() + new_text = text.replace(build_time_prefix, prefix_str) + if new_text != text: + path.write_text(new_text) + + def main() -> None: """CLI entry point: normalize one install prefix end-to-end. @@ -263,6 +310,7 @@ def main() -> None: prefix = args.prefix.resolve() for sysconfigdata in find_sysconfigdata(prefix): append_relocation_block(sysconfigdata, prefix, args.ndk_toolchain) + rewrite_build_details_json(prefix) replace_libpython_stub(prefix) diff --git a/android/python-android-dart.exclude b/android/python-android-dart.exclude index 42e6b31..cee3d2d 100644 --- a/android/python-android-dart.exclude +++ b/android/python-android-dart.exclude @@ -5,15 +5,29 @@ lib/*.la lib/python*/lib-dynload/_test*.so lib/python*/lib-dynload/_ctypes_test*.so lib/python*/lib-dynload/xxlimited*.so +lib/python*/lib-dynload/xxsubtype*.so lib/python*/lib-dynload/_xxtestfuzz*.so +# curses package is excluded below, so its C extensions are dead weight +lib/python*/lib-dynload/_curses*.so +lib/python*/lib-dynload/_curses_panel*.so lib/python*/curses lib/python*/ensurepip lib/python*/idlelib +lib/python*/lib2to3 lib/python*/pydoc_data lib/python*/test lib/python*/tkinter lib/python*/turtle* lib/python*/wsgiref +# Interactive-REPL / dev-only modules + easter eggs / frozen demos (never imported when +# running a script in embedded mode). +lib/python*/_pyrepl +lib/python*/rlcompleter* +lib/python*/tabnanny* +lib/python*/antigravity* +lib/python*/this* +lib/python*/__hello__* +lib/python*/__phello__ man share */__pycache__ diff --git a/android/tests/test_built_build_details_json.py b/android/tests/test_built_build_details_json.py new file mode 100644 index 0000000..4d31497 --- /dev/null +++ b/android/tests/test_built_build_details_json.py @@ -0,0 +1,197 @@ +"""Post-build tests against the actual shipped `build-details.json` files. + +CPython 3.14+'s official Android tooling emits +`lib/python/build-details.json` alongside the per-version +sysconfigdata. Downstream cross-build tooling — most importantly +`maturin` — reads it to drive cross-compilation, including the +`libpython.dynamic_stableabi` value that the consumer's linker uses +as `-L`. python-build's CI bakes build-time paths into the file (currently +under `/usr/local`); `normalize_mobile_forge_install.rewrite_build_details_json` +re-anchors those at install time so the shipped artifact reflects the +consumer's actual filesystem. + +This module asserts on the shipped artifact itself: every path field is +anchored at the install prefix, no `/usr/local` strings leaked through, +and `libpython.dynamic_stableabi` points at a file that actually exists. + +3.13 and 3.12 don't emit a `build-details.json` (it's a 3.14 addition), +so the relevant `@post_build` class is gated on `PYTHON_VERSION_SHORT` +via `@requires_python("3.14")` and degrades to a skip on the older +matrix shards. +""" + +import json +import unittest +from pathlib import Path + +from _testlib import ( + post_build, + requires_python, + INSTALL_TREE, + PYTHON_VERSION_SHORT, +) + + +# Every absolute-path string field documented for build-details.json +# schema v1 that the rewrite is responsible for re-anchoring. Listed +# explicitly rather than walked dynamically so a future CPython schema +# addition that ships a new path field shows up as a test failure (the +# field's anchored-at-install-prefix assertion must be added intentionally). +_PATH_FIELDS: tuple[tuple[str, ...], ...] = ( + ("base_prefix",), + ("base_interpreter",), + ("libpython", "dynamic"), + ("libpython", "dynamic_stableabi"), + ("c_api", "headers"), + ("c_api", "pkgconfig_path"), +) + + +def _build_details_files(install_root: Path): + """Yield (abi, build_details path) for every ABI in the install tree. + + Mirrors `_sysconfigdata_files()` in test_built_install_tree.py: glob + `python-X.Y.Z` ABI dirs under the install root, then locate the + build-details.json inside each version's lib/pythonX.Y/ subdir. + """ + android_root = install_root / "android" + if not android_root.is_dir(): + return + for abi_dir in sorted(android_root.iterdir()): + if not abi_dir.is_dir(): + continue + for py_dir in abi_dir.glob(f"python-{PYTHON_VERSION_SHORT}.*"): + details = py_dir / "lib" / f"python{PYTHON_VERSION_SHORT}" / "build-details.json" + if details.is_file(): + yield abi_dir.name, details + + +def _resolve(data: dict, path: tuple[str, ...]): + """Walk a nested dict by key path, returning None if any segment is missing.""" + node = data + for key in path: + if not isinstance(node, dict) or key not in node: + return None + node = node[key] + return node + + +@post_build +@requires_python("3.14") +class BuiltBuildDetailsJsonShape(unittest.TestCase): + """The shipped `build-details.json` must carry re-anchored paths. + + Gated on 3.14: CPython didn't ship this file pre-3.14, so on 3.12/3.13 + shards there's nothing to inspect and the class skips wholesale. + """ + + @classmethod + def setUpClass(cls): + if not INSTALL_TREE: + raise unittest.SkipTest( + "MOBILE_FORGE_INSTALL_TREE not set — the workflow's post-build " + "test step is responsible for populating it." + ) + cls.install_root = Path(INSTALL_TREE) + cls.found = list(_build_details_files(cls.install_root)) + + def test_at_least_one_build_details_json_present(self): + """Sanity: 3.14 matrix shards must ship a build-details.json per ABI. + + If absent, CPython's Android tooling silently regressed (or a recipe + change in `android/build.sh` is dropping the file). Surface that + before the downstream linker fails opaquely. + """ + self.assertGreater( + len(self.found), + 0, + f"No build-details.json files found under {self.install_root}/android/" + f"/python-{PYTHON_VERSION_SHORT}.*/lib/python{PYTHON_VERSION_SHORT}/. " + f"Either build-all.sh produced nothing, CPython's Android tooling stopped " + f"emitting build-details.json, or MOBILE_FORGE_INSTALL_TREE points at the " + f"wrong dir.", + ) + + def test_no_build_time_prefix_leaked(self): + """Regression guard for `rewrite_build_details_json` getting skipped. + + Asserts no `/usr/local` substring survives anywhere in the JSON. + Build-details.json is CPython-generated with /usr/local as the build-time + install root; if any /usr/local path leaks through (rewrite call dropped + from `main()`, function broke on a path field, etc.), the consumer linker + gets `-L/usr/local/lib` and fails with `unable to find library -lpython3` + — silently, so this is the only test that catches it before the artifact + escapes. + """ + for abi, details in self.found: + with self.subTest(abi=abi): + text = details.read_text() + self.assertNotIn( + "/usr/local", + text, + msg=( + f"/usr/local survived in {details} — " + f"did `rewrite_build_details_json` get dropped from `main()`?" + ), + ) + + def test_path_fields_anchored_at_install_prefix(self): + """Every absolute-path field must live under the install prefix. + + Stronger than `test_no_build_time_prefix_leaked` for `/usr/local` — this + catches a build-time prefix change to anything else as well. If CPython's + Android tooling ever switches its install root from `/usr/local` to + somewhere else, this fails immediately rather than letting half-rewritten + files ship. + """ + for abi, details in self.found: + with self.subTest(abi=abi): + data = json.loads(details.read_text()) + install_prefix = str(details.parents[2]) + for field in _PATH_FIELDS: + value = _resolve(data, field) + if value is None: + # Optional field absent on this ABI (e.g. some shards + # could legitimately omit pkgconfig). Don't fail just + # because the schema is sparser than the maximal set. + continue + with self.subTest(field=".".join(field)): + self.assertTrue( + value.startswith(install_prefix), + msg=( + f"build-details.json field {'.'.join(field)} = " + f"{value!r} is not anchored at install prefix " + f"{install_prefix!r} for {abi}." + ), + ) + + def test_libpython_dynamic_stableabi_resolves(self): + """`libpython.dynamic_stableabi` must point at a file that exists. + + This is the path the linker is told to follow for `-lpython3`. If the + rewrite produces a syntactically-anchored path but the file isn't there + (e.g. python-build's tarball dropped `libpython3.so`), the consumer link + fails just as opaquely as the unrewritten-path case. Verify the file the + JSON points at actually exists. + """ + for abi, details in self.found: + with self.subTest(abi=abi): + data = json.loads(details.read_text()) + stableabi = _resolve(data, ("libpython", "dynamic_stableabi")) + if stableabi is None: + self.skipTest( + f"libpython.dynamic_stableabi absent from {details}" + ) + self.assertTrue( + Path(stableabi).is_file(), + msg=( + f"libpython.dynamic_stableabi = {stableabi!r} in {details} " + f"does not exist on disk. python-build shipped a tree where " + f"the path-rewrite anchored correctly but the target file " + f"is missing." + ), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/darwin/.gitignore b/darwin/.gitignore index 21778b2..f85bd68 100644 --- a/darwin/.gitignore +++ b/darwin/.gitignore @@ -2,4 +2,9 @@ /Python-Apple-support /dist /build +/install +/support +/downloads +/deps +__pycache__/ .DS_Store \ No newline at end of file diff --git a/darwin/README.rst b/darwin/README.rst index 7fd495f..0cf5c35 100644 --- a/darwin/README.rst +++ b/darwin/README.rst @@ -1,4 +1,72 @@ -Python Apple Support -==================== +Darwin (iOS / macOS) Python builds +================================== -Building iOS and macOS "support" builds out of https://github.com/beeware/Python-Apple-support to use with Flet. \ No newline at end of file +Builds embeddable iOS and macOS Python runtimes for Flet, packaged for the Dart/Flutter +side and for `mobile-forge `__. + +Both ``build_ios.py`` and ``build_macos.py`` emit the same normalized layout, which the +``package-*-for-dart.sh`` scripts (and the mobile-forge tarball) consume:: + + install/iOS//python-/... # per-arch installs incl. lib-dynload + install/macOS/macosx/python-/Python.framework + support//iOS/Python.xcframework # device + fat-simulator slices + support//macOS/Python.xcframework # universal2 (macos-arm64_x86_64) + +where ```` is ``iphoneos.arm64``, ``iphonesimulator.arm64`` or +``iphonesimulator.x86_64``. + +iOS — ``build_ios.py `` +------------------------------------- + +Every version is built with the **same** mechanism — CPython's in-tree ``Apple`` build +tool (``python Apple build iOS``) — then ``cross-build/`` is reshaped into the layout above: + +* **3.14+** — the ``Apple/`` tooling is native; build straight from the source tarball. +* **3.13 / 3.12** — the tooling (and, for 3.12, the PEP 730 iOS runtime) isn't upstream + yet, so a vendored back-port patch (``ios_patches//Python.patch``, see that + directory's README) is applied first to add it. No dependency on beeware's + Python-Apple-support repo. + +The Apple tool downloads its own pre-compiled C deps (from +`cpython-apple-source-deps `__ — the +URL CPython itself hard-codes, for every version), builds all slices, and creates the +xcframework. Output is tee'd to ``build/iOS/build-ios-.log``. + +The reshape also rebuilds the ``install``/``support`` tree that +`mobile-forge `__ consumes (the +``python-ios-mobile-forge-.tar.gz`` artifact): per-arch ``install/iOS//-/`` +dirs (re-extracted from the deps the Apple tool downloaded) + a ``support//iOS/VERSIONS`` +listing those versions, and in each xcframework slice a stub ``bin/python`` plus the +``_sysconfigdata`` under ``platform-config/-/`` — the spots mobile-forge's +``crossenv`` setup looks for. (The dart packager excludes all of these from its bundle.) + +macOS — ``build_macos.py `` +----------------------------------------- + +Builds a universal2 ``Python.framework`` **from source** for all versions, then makes it +relocatable (``@rpath``), codesigns it, and wraps it in an xcframework. Building from +source (rather than re-bundling python.org's official ``.pkg``) means we get the exact +micro version even when no macOS installer was published for it. + +The macOS SDK supplies headers for bz2/sqlite/zlib/libffi and CPython bundles libmpdec, +but it ships **no OpenSSL** and **no lzma.h** (only ``liblzma.dylib``), so **OpenSSL** and +**xz/liblzma** are built universal2 from source here (``--openssl-version`` / ``--xz-version``). +To match the official/beeware layout, OpenSSL is built **shared** and bundled into the +framework (``Versions//lib/lib{ssl,crypto}.3.dylib``) with ``@rpath`` install names, +which ``_ssl``/``_hashlib`` reference (the embedding host — e.g. serious_python — provides +the rpath, exactly as the released artifact does); xz is linked **statically** into +``_lzma``. On 3.14+, libzstd is also built from source (``--zstd-version``) and linked +statically into ``_zstd`` (new in 3.14). All binaries are stripped (``strip -x``) before +codesigning. Pass +``--app-store-compliance`` to apply ``macos_support/app-store-compliance.patch`` (off by +default; only for Mac App Store). Full build output is tee'd to +``build/macOS/build-macos-.log``. + +Packaging +--------- + +``package-ios-for-dart.sh . `` and ``package-macos-for-dart.sh . `` turn the +normalized layout into the Dart-consumable ``dist/python-{ios,macos}-dart-.tar.gz`` +archives, reusing ``xcframework_utils.sh``, ``Modules/`` and the ``*.exclude`` lists. + +This whole flow is driven in CI by ``.github/workflows/build-python-version.yml``. diff --git a/darwin/build_ios.py b/darwin/build_ios.py new file mode 100644 index 0000000..6abea55 --- /dev/null +++ b/darwin/build_ios.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +"""Build iOS ``Python.xcframework`` + per-arch installs using CPython's standard Apple tool. + +Every supported version is built with the **same** mechanism — CPython's in-tree +``Apple`` build package (``python Apple build iOS``): + +* **3.14+** — the ``Apple/`` tooling is native; build straight from the source tarball. +* **3.13 / 3.12** — the tooling (and, for 3.12, the PEP 730 iOS runtime) isn't upstream yet, + so we first apply a vendored back-port patch (``ios_patches//Python.patch``) that + adds it, then run the identical ``python Apple build iOS``. No dependency on beeware's + Python-Apple-support repo — only the vendored patch (which we maintain). + +The Apple tool downloads its own pre-compiled C deps (from cpython-apple-source-deps, the +URL CPython itself hard-codes), builds every slice, and produces the xcframework. We then +reshape its ``cross-build/`` output into the normalized layout the dart packager + the +mobile-forge tarball consume: + + /install/iOS//python-/... # per-arch installs incl. lib-dynload + /support//iOS/Python.xcframework # device + fat-simulator slices + +where is iphoneos.arm64, iphonesimulator.arm64 or iphonesimulator.x86_64. + +Run from the ``darwin/`` directory: + python build_ios.py 3.13.13 +""" +from __future__ import annotations + +import argparse +import os +import platform +import re +import shutil +import subprocess +import sys +import urllib.request +from pathlib import Path + +_LOG = None # open file handle; set via set_log() + + +def set_log(path: Path) -> None: + """Tee all subsequent run()/_emit output to a build log file.""" + global _LOG + path.parent.mkdir(parents=True, exist_ok=True) + _LOG = open(path, "w", buffering=1) + _emit(f">>> Build log: {path}") + + +def _emit(line: str) -> None: + out = line if line.endswith("\n") else line + "\n" + sys.stdout.write(out) + sys.stdout.flush() + if _LOG: + _LOG.write(out) + + +def run(cmd: list, cwd: Path | None = None, env: dict | None = None) -> None: + _emit(f">>> {' '.join(str(c) for c in cmd)}" + (f" (cwd={cwd})" if cwd else "")) + proc = subprocess.Popen( + [str(c) for c in cmd], cwd=cwd, env=env, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, + ) + assert proc.stdout is not None + for line in proc.stdout: + _emit(line.rstrip("\n")) + if proc.wait() != 0: + raise subprocess.CalledProcessError(proc.returncode, cmd) + + +def download(url: str, dest: Path) -> None: + if dest.exists(): + _emit(f">>> Using cached {dest.name}") + return + _emit(f">>> Download {url}") + dest.parent.mkdir(parents=True, exist_ok=True) + tmp = dest.with_suffix(dest.suffix + ".part") + with urllib.request.urlopen(url, timeout=120) as resp, open(tmp, "wb") as fh: + shutil.copyfileobj(resp, fh) + tmp.replace(dest) + + +def build(version: str, short: str, minor: int, root: Path, downloads: Path, + build_dir: Path, patches_dir: Path) -> None: + tarball = downloads / f"Python-{version}.tgz" + download(f"https://www.python.org/ftp/python/{version}/Python-{version}.tgz", tarball) + + src = build_dir / f"Python-{version}" + if src.exists(): + shutil.rmtree(src) + src.mkdir(parents=True) + run(["tar", "xzf", tarball, "--strip-components", "1", "-C", src]) + + # < 3.14: back-port the Apple build tooling (+ runtime for 3.12) via our vendored patch. + if minor < 14: + patch = patches_dir / short / "Python.patch" + if not patch.exists(): + raise SystemExit(f"No vendored iOS patch for {short} at {patch}") + run(["patch", "-p1", "--force", "-i", patch], cwd=src) + # `patch` doesn't preserve the executable bit, so the compiler/linker shim scripts + # the patch adds come out non-executable. Restore +x (beeware's Makefile does the + # same) or configure fails with "C compiler cannot create executables". + for bindir in list(src.glob("Apple/*/Resources/bin")) + [src / "iOS" / "Resources" / "bin"]: + for shim in bindir.glob("*") if bindir.is_dir() else []: + shim.chmod(0o755) + + # iOS's SDK declares pipe2/dup3 in headers but doesn't provide them, and on recent + # SDKs configure mis-detects them (esp. the simulator), then the build fails with + # "call to undeclared function 'pipe2'". The Apple tool doesn't expose configure + # flags, but configure honors CONFIG_SITE, and the tool copies os.environ into its + # subprocesses — so feed the same ac_cv overrides beeware passes via a site file. + config_site = build_dir / "ios-config.site" + config_site.write_text("ac_cv_func_pipe2=no\nac_cv_func_dup3=no\n") + env = {**os.environ, "CONFIG_SITE": str(config_site)} + + # The standard Apple builder: builds the build-python, every iOS slice, downloads its + # own C deps, and emits cross-build/iOS/Python.xcframework. `build` does not run the + # (device/simulator) testbed. + host_python = shutil.which("python3") or sys.executable + run([host_python, "Apple", "build", "iOS"], cwd=src, env=env) + + reshape(version, short, src, root) + + +def _strip(paths: list) -> None: + """`strip -x` the given Mach-O files (removes local symbols, keeps exported ones).""" + files = [str(p) for p in paths if p.is_file() and not p.is_symlink()] + if files: + run(["strip", "-x", *files]) + + +def _embed_ios_version_in_host_triple(sysconfig_path: Path) -> None: + """Rewrite HOST_GNU_TYPE in a _sysconfigdata file to embed IPHONEOS_DEPLOYMENT_TARGET + (e.g. ``aarch64-apple-ios`` -> ``aarch64-apple-ios13.0``, + ``...-ios-simulator`` -> ``...-ios13.0-simulator``).""" + text = sysconfig_path.read_text() + m = re.search(r"'IPHONEOS_DEPLOYMENT_TARGET':\s*'([^']+)'", text) + if not m: + return + deploy = m.group(1) + + def repl(mo: "re.Match") -> str: + triple = mo.group(1) + if "-apple-ios" not in triple or re.search(r"-apple-ios\d", triple): + return mo.group(0) # not iOS, or already versioned + return "'HOST_GNU_TYPE': '" + triple.replace("-apple-ios", f"-apple-ios{deploy}", 1) + "'" + + new = re.sub(r"'HOST_GNU_TYPE':\s*'([^']*)'", repl, text) + if new != text: + sysconfig_path.write_text(new) + + +# xcframework slice + per-arch lib dir for each of our three install targets. +XCF_SLICE_FOR_TARGET = { + "iphoneos.arm64": ("ios-arm64", "lib-arm64"), + "iphonesimulator.arm64": ("ios-arm64_x86_64-simulator", "lib-arm64"), + "iphonesimulator.x86_64": ("ios-arm64_x86_64-simulator", "lib-x86_64"), +} + + +def reshape(version: str, short: str, src: Path, root: Path) -> None: + """Map the Apple tool's cross-build output into our normalized layout. + + The Apple xcframework keeps the pure stdlib once (``lib/python``) and the + arch-specific bits (``lib-dynload`` + ``_sysconfigdata``) per slice in + ``/lib-/python``. The dart packager instead wants, per arch, a full + ``install/iOS//python-/lib/python`` (pure stdlib + that arch's + lib-dynload + sysconfig), so we stitch those together here. + """ + cross = src / "cross-build" + pyver = f"python{short}" + + dst_xcf = root / "support" / short / "iOS" / "Python.xcframework" + if dst_xcf.exists(): + shutil.rmtree(dst_xcf) + dst_xcf.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(cross / "iOS" / "Python.xcframework", dst_xcf, symlinks=True) + + # The Apple tool configures --host=arm64-apple-ios (no version), so HOST_GNU_TYPE in + # the _sysconfigdata lacks the iOS deployment version. mobile-forge's crossenv recovers + # the iOS release by splitting HOST_GNU_TYPE (when _PYTHON_HOST_PLATFORM isn't set), so a + # version-less triple yields an empty release and pip's iOS-tag code crashes. Embed the + # version (e.g. aarch64-apple-ios -> aarch64-apple-ios13.0), matching beeware. Fixing the + # xcframework copies here means the dart-install + platform-config copies inherit it. + for sc in dst_xcf.glob(f"*/lib-*/{pyver}/_sysconfigdata__ios_*.py"): + _embed_ios_version_in_host_triple(sc) + + shared_stdlib = dst_xcf / "lib" / pyver + if not (shared_stdlib / "os.py").exists(): + raise SystemExit(f"Expected shared stdlib at {shared_stdlib}") + + for target, (slice_name, arch_lib) in XCF_SLICE_FOR_TARGET.items(): + arch_dir = dst_xcf / slice_name / arch_lib / pyver + if not (arch_dir / "lib-dynload").is_dir(): + raise SystemExit(f"Expected per-arch lib-dynload at {arch_dir}") + dst = root / "install" / "iOS" / target / f"python-{version}" / "lib" / pyver + if dst.parent.parent.exists(): + shutil.rmtree(dst.parent.parent) + dst.parent.mkdir(parents=True, exist_ok=True) + # Pure stdlib (its lib-dynload is empty), then overlay this arch's real lib-dynload + # + _sysconfigdata. + shutil.copytree(shared_stdlib, dst, symlinks=True) + if (dst / "lib-dynload").exists(): + shutil.rmtree(dst / "lib-dynload") + shutil.copytree(arch_dir / "lib-dynload", dst / "lib-dynload", symlinks=True) + for sysconfig in arch_dir.glob("_sysconfigdata*.py"): + shutil.copyfile(sysconfig, dst / sysconfig.name) + # The Apple tool leaves the iOS binaries unstripped; strip local symbols (keeps the + # exported PyInit_/C-API symbols needed for dynamic linking). These .so become the + # per-module python-xcframeworks; Xcode re-signs them when embedding into the app. + _strip([*(dst / "lib-dynload").glob("*.so")]) + + # Strip the per-slice framework binary too (the libpython that ships in the xcframework). + _strip([dst_xcf / sl / "Python.framework" / "Python" + for sl in {s for s, _ in XCF_SLICE_FOR_TARGET.values()}]) + + deps = reshape_mobile_forge(version, short, cross, dst_xcf, root) + + support = root / "support" / short / "iOS" + support.mkdir(parents=True, exist_ok=True) + lines = [f"Python version: {version}", "Build: custom", "Min iOS version: 13.0"] + if deps: + lines.append("---------------------") + lines += [f"{name}: {ver}" for name, ver in sorted(deps.items())] + (support / "VERSIONS").write_text("\n".join(lines) + "\n") + + +# Map cpython-apple-source-deps lib name -> the VERSIONS label mobile-forge greps for +# (case-insensitive, so exact casing is cosmetic; match beeware's labels). +DEP_LABELS = { + "openssl": "OpenSSL", "libffi": "libFFI", "xz": "XZ", + "bzip2": "BZip2", "mpdecimal": "mpdecimal", "zstd": "zstd", +} + + +def reshape_mobile_forge(version: str, short: str, cross: Path, dst_xcf: Path, + root: Path) -> dict: + """Add what flet-dev/mobile-forge needs (beyond the dart inputs): + + * per-arch C-dependency dirs ``install/iOS//-/`` (re-extracted from the + tarballs the Apple tool already downloaded), so mobile-forge can build dep wheels; + * in each xcframework slice, a stub ``bin/python`` and the ``_sysconfigdata`` at + ``platform-config/-/`` — the spots mobile-forge's crossenv setup looks for + (the Apple tool only leaves them under ``lib-/``). + + Returns {dep-label: version-build} for the VERSIONS file. + """ + pyver = f"python{short}" + targets = ("iphoneos.arm64", "iphonesimulator.arm64", "iphonesimulator.x86_64") + + # 1. Per-arch dependency dirs from the downloaded tarballs. + deps: dict[str, str] = {} + downloads = cross / "downloads" + pattern = re.compile(r"^(?P[a-z0-9]+)-(?P\d[^-]*-\d+)-(?P.+)\.tar\.gz$") + for tarball in sorted(downloads.glob("*.tar.gz")) if downloads.is_dir() else []: + m = pattern.match(tarball.name) + if not m or m["target"] not in targets: + continue + dep, ver, target = m["dep"], m["ver"], m["target"] + dest = root / "install" / "iOS" / target / f"{dep}-{ver}" + if dest.exists(): + shutil.rmtree(dest) + dest.mkdir(parents=True) + run(["tar", "xzf", tarball, "-C", dest]) + deps[DEP_LABELS.get(dep, dep)] = ver + + # 2. Per-slice stub python + platform-config sysconfigdata. + for slice_dir in {sl for sl, _ in XCF_SLICE_FOR_TARGET.values()}: + slice_path = dst_xcf / slice_dir + stub = slice_path / "bin" / f"python{short}" + stub.parent.mkdir(parents=True, exist_ok=True) + if not stub.exists(): + stub.write_text(f"#!/bin/bash\necho \"Can't run {slice_dir} binary\"\nexit 1\n") + stub.chmod(0o755) + # Copy each arch's _sysconfigdata into platform-config/-/ where + # CrossVEnv.create() looks for it. + for sysconfig in slice_path.glob(f"lib-*/{pyver}/_sysconfigdata__ios_*.py"): + m = re.match(r"_sysconfigdata__ios_(?P.+)-(?Piphoneos|iphonesimulator)\.py", + sysconfig.name) + if not m: + continue + pc = slice_path / "platform-config" / f"{m['arch']}-{m['sdk']}" + pc.mkdir(parents=True, exist_ok=True) + shutil.copyfile(sysconfig, pc / sysconfig.name) + + return deps + + +def main() -> None: + if platform.system() != "Darwin": + raise SystemExit("build_ios.py must run on macOS.") + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("version", help="full CPython version, e.g. 3.13.13") + parser.add_argument("--root", type=Path, default=Path.cwd(), + help="output root (default: cwd; emits install/ and support/ here)") + args = parser.parse_args() + + version = args.version + parts = version.split(".") + short = f"{parts[0]}.{parts[1]}" + minor = int(parts[1]) + + root = args.root.resolve() + script_dir = Path(__file__).resolve().parent + downloads = root / "downloads" + build_dir = root / "build" / "iOS" + build_dir.mkdir(parents=True, exist_ok=True) + set_log(build_dir / f"build-ios-{version}.log") + + if minor >= 14: + _emit(f">>> iOS {version}: native CPython Apple tooling") + else: + _emit(f">>> iOS {version}: Apple tooling back-ported via vendored patch") + + build(version, short, minor, root, downloads, build_dir, script_dir / "ios_patches") + + _emit(f"\n>>> iOS build complete for {version}") + _emit(f" xcframework: {root / 'support' / short / 'iOS' / 'Python.xcframework'}") + _emit(f" installs: {root / 'install' / 'iOS'}") + + +if __name__ == "__main__": + main() diff --git a/darwin/build_macos.py b/darwin/build_macos.py new file mode 100644 index 0000000..504e677 --- /dev/null +++ b/darwin/build_macos.py @@ -0,0 +1,593 @@ +#!/usr/bin/env python3 +"""Build a macOS universal2 ``Python.framework`` from source and wrap it in an XCframework. + +This replaces beeware's Make-based macOS path (which re-bundled python.org's official +``.pkg``) for *all* versions (3.12 / 3.13 / 3.14). Building from source means we get the +exact micro version even when python.org never published a macOS installer for it. + +macOS provides bz2/lzma/sqlite/libffi via the SDK and CPython bundles libmpdec, so the +only third-party dependency we must supply is **OpenSSL** — and since no prebuilt +universal2 macOS OpenSSL exists (cpython-apple-source-deps only ships iOS-family slices, +Homebrew is single-arch), we build it universal2 from source here. + +Output (normalized layout consumed by ``package-macos-for-dart.sh``): + /install/macOS/macosx/python-/Python.framework # installed framework + /support//macOS/Python.xcframework # framework wrapped as xcframework + +Run from the ``darwin/`` directory: + python build_macos.py 3.13.13 +""" +from __future__ import annotations + +import argparse +import os +import platform +import shutil +import subprocess +import sys +import urllib.request +from pathlib import Path + +_LOG = None # open file handle; set via set_log() + +# Third-party libs built from source. macOS ships bz2/sqlite/zlib/libffi *headers* in the +# SDK, but NOT OpenSSL (none at all) nor liblzma (dylib only, no lzma.h), so we build those +# two universal2 from source. Pinned for reproducibility, overridable via CLI. +DEFAULT_OPENSSL_VERSION = "3.6.2" +DEFAULT_XZ_VERSION = "5.8.3" +# zstd backs the _zstd module added in CPython 3.14 (PEP 784); built only for 3.14+. +DEFAULT_ZSTD_VERSION = "1.5.7" +DEFAULT_DEPLOYMENT_TARGET = "11.0" +ARCHES = ("x86_64", "arm64") + +# A minimal PATH keeps Homebrew / user Python installs from leaking into the build. +CLEAN_PATH = "/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" + + +def set_log(path: Path) -> None: + """Tee all subsequent run()/_emit output to a build log file.""" + global _LOG + path.parent.mkdir(parents=True, exist_ok=True) + _LOG = open(path, "w", buffering=1) + _emit(f">>> Build log: {path}") + + +def _emit(line: str) -> None: + sys.stdout.write(line if line.endswith("\n") else line + "\n") + sys.stdout.flush() + if _LOG: + _LOG.write(line if line.endswith("\n") else line + "\n") + + +def run(cmd: list[str], cwd: Path | None = None, env: dict | None = None) -> None: + printable = " ".join(str(c) for c in cmd) + _emit(f">>> {printable}" + (f" (cwd={cwd})" if cwd else "")) + # Merge stderr into stdout and tee line-by-line to console + log file. + proc = subprocess.Popen( + [str(c) for c in cmd], cwd=cwd, env=env, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, + ) + assert proc.stdout is not None + for line in proc.stdout: + _emit(line.rstrip("\n")) + if proc.wait() != 0: + raise subprocess.CalledProcessError(proc.returncode, cmd) + + +def download(url: str, dest: Path) -> None: + if dest.exists(): + print(f">>> Using cached {dest.name}") + return + print(f">>> Download {url}") + dest.parent.mkdir(parents=True, exist_ok=True) + tmp = dest.with_suffix(dest.suffix + ".part") + with urllib.request.urlopen(url, timeout=120) as resp, open(tmp, "wb") as fh: + shutil.copyfileobj(resp, fh) + tmp.replace(dest) + + +def base_env(deployment_target: str) -> dict: + env = dict(os.environ) + env["PATH"] = CLEAN_PATH + env["MACOSX_DEPLOYMENT_TARGET"] = deployment_target + return env + + +# --------------------------------------------------------------------------- +# OpenSSL (universal2, built from source) +# --------------------------------------------------------------------------- + +def build_openssl_universal( + openssl_version: str, + downloads: Path, + build_dir: Path, + deployment_target: str, + jobs: int, +) -> Path: + """Build OpenSSL for each arch as a SHARED lib, fuse into a universal2 install. + + Returns the prefix containing ``include/`` and ``lib/{libssl,libcrypto}.3.dylib``. + The dylib install-names are left as the (absolute) build prefix here; they are + rewritten to @rpath when bundled into the framework (see relocate_openssl). All paths + are keyed by the OpenSSL version so switching versions never reuses stale artifacts. + """ + out_prefix = build_dir / f"openssl-{openssl_version}-universal" + if (out_prefix / "lib" / "libssl.3.dylib").exists(): + _emit(f">>> Using cached universal OpenSSL at {out_prefix}") + return out_prefix + + tarball = downloads / f"openssl-{openssl_version}.tar.gz" + download( + f"https://github.com/openssl/openssl/releases/download/" + f"openssl-{openssl_version}/openssl-{openssl_version}.tar.gz", + tarball, + ) + + env = base_env(deployment_target) + # Configure every arch with the SAME --prefix so the baked install-names match, + # then redirect the actual install per arch with DESTDIR. That keeps the fat + # dylibs' load commands consistent so install_name_tool can rewrite them once. + arch_includes: dict[str, Path] = {} + arch_libdirs: dict[str, Path] = {} + for arch in ARCHES: + src = build_dir / f"openssl-{openssl_version}-{arch}" + dest = build_dir / f"openssl-{openssl_version}-dest-{arch}" + if src.exists(): + shutil.rmtree(src) + if dest.exists(): + shutil.rmtree(dest) + src.mkdir(parents=True) + run(["tar", "xzf", tarball, "--strip-components", "1", "-C", src]) + # Default (no "no-shared") builds the shared dylibs; darwin64--cc sets -arch. + run( + [ + "./Configure", + f"darwin64-{arch}-cc", + "no-tests", + f"--prefix={out_prefix}", + f"--openssldir={out_prefix}/ssl", + f"-mmacosx-version-min={deployment_target}", + ], + cwd=src, + env=env, + ) + run(["make", f"-j{jobs}"], cwd=src, env=env) + run(["make", "install_sw", f"DESTDIR={dest}"], cwd=src, env=env) + # DESTDIR + absolute prefix -> files land at . + inner = Path(f"{dest}{out_prefix}") + arch_includes[arch] = inner / "include" + arch_libdirs[arch] = inner / "lib" + + if out_prefix.exists(): + shutil.rmtree(out_prefix) + (out_prefix / "lib").mkdir(parents=True) + for lib in ("libssl.3.dylib", "libcrypto.3.dylib"): + run( + ["lipo", "-create", "-output", out_prefix / "lib" / lib] + + [arch_libdirs[a] / lib for a in ARCHES] + ) + # Unversioned symlinks so `-lssl`/`-lcrypto` resolve at link time. + (out_prefix / "lib" / "libssl.dylib").symlink_to("libssl.3.dylib") + (out_prefix / "lib" / "libcrypto.dylib").symlink_to("libcrypto.3.dylib") + # Base the include tree on one arch, then dispatch the arch-dependent headers. + shutil.copytree(arch_includes[ARCHES[0]], out_prefix / "include") + _dispatch_openssl_headers({a: arch_includes[a].parent for a in ARCHES}, + out_prefix / "include") + return out_prefix + + +def _dispatch_openssl_headers(arch_prefixes: dict[str, Path], include_dir: Path) -> None: + """Replace arch-dependent OpenSSL headers with a per-arch #include dispatcher. + + ``opensslconf.h`` / ``configuration.h`` are generated per arch; in a universal2 + compile both arches are processed in one pass, so a single copy would be wrong for + one of them. We keep both arch variants and pick at preprocessor time. + """ + for header in ("opensslconf.h", "configuration.h"): + rel = Path("openssl") / header + variants: dict[str, Path] = {} + for arch in ARCHES: + src = arch_prefixes[arch] / "include" / rel + if src.exists(): + variants[arch] = src + if not variants: + continue + for arch, src in variants.items(): + shutil.copyfile(src, include_dir / "openssl" / f"{header[:-2]}-{arch}.h") + guard = { + "x86_64": "defined(__x86_64__)", + "arm64": "defined(__arm64__) || defined(__aarch64__)", + } + lines = ["/* Auto-generated universal2 dispatcher (python-build) */"] + first = True + for arch in ARCHES: + if arch not in variants: + continue + lines.append(f"#{'if' if first else 'elif'} {guard[arch]}") + lines.append(f'# include "{header[:-2]}-{arch}.h"') + first = False + lines.append("#else") + lines.append('# error "Unsupported architecture for universal2 OpenSSL headers"') + lines.append("#endif") + (include_dir / "openssl" / header).write_text("\n".join(lines) + "\n") + + +# --------------------------------------------------------------------------- +# liblzma / xz (universal2, built from source — macOS ships no lzma.h) +# --------------------------------------------------------------------------- + +def build_xz_universal( + xz_version: str, + downloads: Path, + build_dir: Path, + deployment_target: str, + jobs: int, +) -> Path: + """Build a universal2 static liblzma. Returns a prefix with include/ + lib/liblzma.a. + + Unlike OpenSSL, xz's autotools build can emit a fat binary in a single pass (its + headers are arch-independent), so we compile with both -arch flags rather than + per-arch + lipo. + """ + out_prefix = build_dir / f"xz-{xz_version}-universal" + if (out_prefix / "lib" / "liblzma.a").exists(): + _emit(f">>> Using cached universal liblzma at {out_prefix}") + return out_prefix + + tarball = downloads / f"xz-{xz_version}.tar.gz" + download( + f"https://github.com/tukaani-project/xz/releases/download/" + f"v{xz_version}/xz-{xz_version}.tar.gz", + tarball, + ) + src = build_dir / f"xz-{xz_version}" + if src.exists(): + shutil.rmtree(src) + src.mkdir(parents=True) + run(["tar", "xzf", tarball, "--strip-components", "1", "-C", src]) + + env = base_env(deployment_target) + env["CFLAGS"] = f"-arch arm64 -arch x86_64 -mmacosx-version-min={deployment_target}" + if out_prefix.exists(): + shutil.rmtree(out_prefix) + run( + [ + "./configure", + "--disable-shared", "--enable-static", + "--disable-doc", "--disable-nls", "--disable-scripts", + # We only need liblzma, not the CLI tools. + "--disable-xz", "--disable-xzdec", "--disable-lzmadec", + "--disable-lzmainfo", "--disable-lzma-links", + f"--prefix={out_prefix}", + ], + cwd=src, env=env, + ) + run(["make", f"-j{jobs}"], cwd=src, env=env) + run(["make", "install"], cwd=src, env=env) + return out_prefix + + +# --------------------------------------------------------------------------- +# libzstd (universal2 static — backs the _zstd module new in CPython 3.14) +# --------------------------------------------------------------------------- + +def build_zstd_universal( + zstd_version: str, + downloads: Path, + build_dir: Path, + deployment_target: str, + jobs: int, +) -> Path: + """Build a universal2 static libzstd. Returns a prefix with include/ + lib/libzstd.a.""" + out_prefix = build_dir / f"zstd-{zstd_version}-universal" + if (out_prefix / "lib" / "libzstd.a").exists(): + _emit(f">>> Using cached universal libzstd at {out_prefix}") + return out_prefix + + tarball = downloads / f"zstd-{zstd_version}.tar.gz" + download( + f"https://github.com/facebook/zstd/releases/download/" + f"v{zstd_version}/zstd-{zstd_version}.tar.gz", + tarball, + ) + src = build_dir / f"zstd-{zstd_version}" + if src.exists(): + shutil.rmtree(src) + src.mkdir(parents=True) + run(["tar", "xzf", tarball, "--strip-components", "1", "-C", src]) + + env = base_env(deployment_target) + env["CFLAGS"] = f"-arch arm64 -arch x86_64 -mmacosx-version-min={deployment_target}" + # zstd's lib/ Makefile builds the static lib directly (single-pass universal). + run(["make", "-C", "lib", "libzstd.a", f"-j{jobs}"], cwd=src, env=env) + + if out_prefix.exists(): + shutil.rmtree(out_prefix) + (out_prefix / "lib").mkdir(parents=True) + (out_prefix / "include").mkdir(parents=True) + shutil.copyfile(src / "lib" / "libzstd.a", out_prefix / "lib" / "libzstd.a") + for header in ("zstd.h", "zdict.h", "zstd_errors.h"): + shutil.copyfile(src / "lib" / header, out_prefix / "include" / header) + return out_prefix + + +# --------------------------------------------------------------------------- +# CPython framework build +# --------------------------------------------------------------------------- + +def build_python_framework( + version: str, + short: str, + downloads: Path, + build_dir: Path, + install_dir: Path, + openssl_prefix: Path, + xz_prefix: Path, + zstd_prefix: Path | None, + deployment_target: str, + jobs: int, +) -> Path: + """Configure + build + install a universal2 framework. Returns the Python.framework path.""" + tarball = downloads / f"Python-{version}.tgz" + download(f"https://www.python.org/ftp/python/{version}/Python-{version}.tgz", tarball) + + src = build_dir / f"Python-{version}" + if src.exists(): + shutil.rmtree(src) + src.mkdir(parents=True) + run(["tar", "xzf", tarball, "--strip-components", "1", "-C", src]) + + if install_dir.exists(): + shutil.rmtree(install_dir) + install_dir.mkdir(parents=True) + + env = base_env(deployment_target) + configure = [ + "./configure", + "--enable-framework=" + str(install_dir), + "--enable-universalsdk=/", + "--with-universal-archs=universal2", + "--with-openssl=" + str(openssl_prefix), + # liblzma has no pkg-config on macOS; feed the from-source build directly so + # the _lzma extension builds (the SDK provides no lzma.h). + f"LIBLZMA_CFLAGS=-I{xz_prefix}/include", + f"LIBLZMA_LIBS=-L{xz_prefix}/lib -llzma", + ] + if zstd_prefix is not None: + # _zstd (CPython 3.14+) — SDK provides no zstd.h, so feed the from-source build. + configure += [ + f"LIBZSTD_CFLAGS=-I{zstd_prefix}/include", + f"LIBZSTD_LIBS=-L{zstd_prefix}/lib -lzstd", + ] + # NOTE: --enable-optimizations (PGO+LTO) intentionally omitted for first bring-up — + # it is slow and the profile task is finicky for universal2. Re-enable once green. + configure.append("--without-ensurepip") + run(configure, cwd=src, env=env) + run(["make", f"-j{jobs}"], cwd=src, env=env) + # `make install` on a --enable-framework build also runs sub-targets that write + # OUTSIDE our prefix: the IDLE/Python Launcher .app bundles -> /Applications, and + # console-script symlinks -> /usr/local/bin (which needs root and fails). Redirect + # both to throwaway dirs under build/ so only the framework+stdlib land in our + # prefix and nothing touches the system. + junk_apps = build_dir / "_appsdir" + junk_bin = build_dir / "_unixtools" + run( + ["make", "install", + f"PYTHONAPPSDIR={junk_apps}", + f"FRAMEWORKUNIXTOOLSPREFIX={junk_bin}"], + cwd=src, env=env, + ) + + framework = install_dir / "Python.framework" + if not framework.exists(): + raise SystemExit(f"Expected framework at {framework}, but it was not produced.") + return framework + + +# --------------------------------------------------------------------------- +# Bundle OpenSSL + relocate + strip + codesign + xcframework +# --------------------------------------------------------------------------- + +def relocate_openssl(framework: Path, short: str, openssl_prefix: Path) -> None: + """Bundle the shared OpenSSL dylibs into the framework and point everything at + @rpath, matching the official/beeware layout (libssl/libcrypto under Versions//lib). + + _ssl/_hashlib were linked against ``/lib/lib*.3.dylib`` (absolute), so + we copy those dylibs in, rewrite their ids + interdependency to @rpath, and rewrite the + modules' references to match. + """ + lib_dir = framework / "Versions" / short / "lib" + lib_dir.mkdir(parents=True, exist_ok=True) + rpath = f"@rpath/Python.framework/Versions/{short}/lib" + + names = ("libcrypto.3.dylib", "libssl.3.dylib") + for name in names: + shutil.copyfile(openssl_prefix / "lib" / name, lib_dir / name) + for link, target in (("libcrypto.dylib", "libcrypto.3.dylib"), + ("libssl.dylib", "libssl.3.dylib")): + dst = lib_dir / link + if not dst.exists(): + dst.symlink_to(target) + + # Fix the bundled dylibs' own ids and libssl's dependency on libcrypto. + for name in names: + run(["install_name_tool", "-id", f"{rpath}/{name}", lib_dir / name]) + run(["install_name_tool", "-change", + f"{openssl_prefix}/lib/libcrypto.3.dylib", f"{rpath}/libcrypto.3.dylib", + lib_dir / "libssl.3.dylib"]) + + # Rewrite every module that linked the build-prefix OpenSSL to the bundled @rpath one. + for module in framework.rglob("*"): + if module.suffix not in (".so", ".dylib") or not module.is_file() or module.is_symlink(): + continue + try: + otool = subprocess.run( + ["otool", "-L", str(module)], capture_output=True, text=True, check=True + ).stdout + except subprocess.CalledProcessError: + continue + for name in names: + old = f"{openssl_prefix}/lib/{name}" + if old in otool: + run(["install_name_tool", "-change", old, f"{rpath}/{name}", module]) + + +def strip_framework(framework: Path, short: str) -> None: + """Strip local/debug symbols from all binaries (keeps exported symbols). Matches the + stripped binaries the official build ships; must run before codesigning.""" + targets = [framework / "Versions" / short / "Python"] + for p in framework.rglob("*"): + if p.suffix in (".so", ".dylib") and p.is_file() and not p.is_symlink(): + targets.append(p) + # `strip -x` removes local symbols but keeps externals needed for dynamic linking. + for t in targets: + run(["strip", "-x", t]) + + +def make_relocatable(framework: Path, short: str) -> None: + """Rewrite absolute /Library/... install names to @rpath (ported from beeware's + patch/make-relocatable.sh).""" + versions = framework / "Versions" / short + python_lib = versions / "Python" + run(["install_name_tool", "-id", + f"@rpath/Python.framework/Versions/{short}/Python", python_lib]) + + lib_dir = versions / "lib" + versioned_dylibs = list(lib_dir.glob("*.*.dylib")) if lib_dir.is_dir() else [] + for dylib in versioned_dylibs: + if dylib.name == f"libpython{short}.dylib": + continue + run(["install_name_tool", "-id", + f"@rpath/Python.framework/Versions/{short}/lib/{dylib.name}", dylib]) + + old_prefix = f"/Library/Frameworks/Python.framework/Versions/{short}" + for module in framework.rglob("*"): + if module.suffix not in (".dylib", ".so") or not module.is_file(): + continue + try: + otool = subprocess.run( + ["otool", "-L", str(module)], capture_output=True, text=True, check=True + ).stdout + except subprocess.CalledProcessError: + continue + for dylib in versioned_dylibs: + old = f"{old_prefix}/lib/{dylib.name}" + if old in otool: + run(["install_name_tool", "-change", old, + f"@rpath/Python.framework/Versions/{short}/lib/{dylib.name}", module]) + if f"{old_prefix}/Python" in otool: + run(["install_name_tool", "-change", f"{old_prefix}/Python", + f"@rpath/Python.framework/Versions/{short}/Python", module]) + + +def codesign_framework(framework: Path, short: str) -> None: + args = ["codesign", "-s", "-", "--preserve-metadata=identifier,entitlements,flags,runtime", "-f"] + run(args + [framework / "Versions" / short / "Python"]) + for pattern in ("*.dylib", "*.so"): + for binary in framework.rglob(pattern): + if binary.is_file(): + run(args + [binary]) + run(args + [framework]) + + +def create_xcframework(framework: Path, output: Path) -> None: + if output.exists(): + shutil.rmtree(output) + output.parent.mkdir(parents=True, exist_ok=True) + run(["xcodebuild", "-create-xcframework", "-framework", framework, "-output", output]) + + +# --------------------------------------------------------------------------- + +def write_versions_file(support_dir: Path, version: str, openssl_version: str, + xz_version: str, zstd_version: str | None, + deployment_target: str) -> None: + support_dir.mkdir(parents=True, exist_ok=True) + text = ( + f"Python version: {version}\n" + f"Build: custom\n" + f"Min macOS version: {deployment_target}\n" + f"---------------------\n" + f"OpenSSL: {openssl_version}\n" + f"XZ: {xz_version}\n" + ) + if zstd_version is not None: + text += f"zstd: {zstd_version}\n" + (support_dir / "VERSIONS").write_text(text) + + +def main() -> None: + if platform.system() != "Darwin": + raise SystemExit("build_macos.py must run on macOS.") + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("version", help="full CPython version, e.g. 3.13.13") + parser.add_argument("--openssl-version", default=DEFAULT_OPENSSL_VERSION) + parser.add_argument("--xz-version", default=DEFAULT_XZ_VERSION) + parser.add_argument("--zstd-version", default=DEFAULT_ZSTD_VERSION) + parser.add_argument("--deployment-target", default=DEFAULT_DEPLOYMENT_TARGET) + parser.add_argument("--jobs", type=int, default=os.cpu_count() or 4) + parser.add_argument("--app-store-compliance", action="store_true", + help="apply macos_support/app-store-compliance.patch to the stdlib") + parser.add_argument("--root", type=Path, default=Path.cwd(), + help="output root (default: cwd; emits install/ and support/ here)") + args = parser.parse_args() + + version = args.version + parts = version.split(".") + short = f"{parts[0]}.{parts[1]}" + minor = int(parts[1]) + + root = args.root.resolve() + script_dir = Path(__file__).resolve().parent + downloads = root / "downloads" + build_dir = root / "build" / "macOS" + install_dir = root / "install" / "macOS" / "macosx" / f"python-{version}" + xcframework_out = root / "support" / short / "macOS" / "Python.xcframework" + + set_log(build_dir / f"build-macos-{version}.log") + + openssl_prefix = build_openssl_universal( + args.openssl_version, downloads, build_dir, + args.deployment_target, args.jobs, + ) + xz_prefix = build_xz_universal( + args.xz_version, downloads, build_dir, + args.deployment_target, args.jobs, + ) + # _zstd is new in CPython 3.14; older versions have no such module. + zstd_prefix = None + zstd_version = None + if minor >= 14: + zstd_version = args.zstd_version + zstd_prefix = build_zstd_universal( + zstd_version, downloads, build_dir, + args.deployment_target, args.jobs, + ) + + framework = build_python_framework( + version, short, downloads, build_dir, install_dir, + openssl_prefix, xz_prefix, zstd_prefix, args.deployment_target, args.jobs, + ) + + if args.app_store_compliance: + patch = script_dir / "macos_support" / "app-store-compliance.patch" + stdlib = framework / "Versions" / short / "lib" / f"python{short}" + run(["patch", "--strip", "2", "--directory", stdlib, "--input", patch]) + + relocate_openssl(framework, short, openssl_prefix) + make_relocatable(framework, short) + strip_framework(framework, short) + codesign_framework(framework, short) + create_xcframework(framework, xcframework_out) + write_versions_file(root / "support" / short / "macOS", version, + args.openssl_version, args.xz_version, zstd_version, + args.deployment_target) + + _emit(f"\n>>> macOS build complete for {version}") + _emit(f" framework: {framework}") + _emit(f" xcframework: {xcframework_out}") + + +if __name__ == "__main__": + main() diff --git a/darwin/ios_patches/3.12/Python.patch b/darwin/ios_patches/3.12/Python.patch new file mode 100644 index 0000000..4ccfda8 --- /dev/null +++ b/darwin/ios_patches/3.12/Python.patch @@ -0,0 +1,12863 @@ +diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml +index ad91fe68a33..25dffd4d899 100644 +--- a/.pre-commit-config.yaml ++++ b/.pre-commit-config.yaml +@@ -2,6 +2,10 @@ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.1 + hooks: ++ - id: ruff ++ name: Run Ruff (lint) on Apple/ ++ args: [--exit-non-zero-on-fix, --config=Apple/.ruff.toml] ++ files: ^Apple/ + - id: ruff + name: Run Ruff (lint) on Doc/ + args: [--exit-non-zero-on-fix] +@@ -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 + - id: ruff-format + name: Run Ruff (format) on Doc/ + args: [--check] +--- /dev/null ++++ b/.ruff.toml +@@ -0,0 +1,12 @@ ++# Default settings for Ruff in CPython ++ ++# PYTHON_FOR_REGEN ++target-version = "py310" ++ ++# PEP 8 ++line-length = 79 ++ ++# Enable automatic fixes by default. ++# To override this, use ``fix = false`` in a subdirectory's config file ++# or ``--no-fix`` on the command line. ++fix = true +--- /dev/null ++++ b/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 ++] +--- /dev/null ++++ b/Apple/__main__.py +@@ -0,0 +1,1110 @@ ++#!/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 ++ ++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 ++ ++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", ++ }, ++ }, ++ "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", ++ }, ++ }, ++} ++ ++ ++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 host triples are the platform definitions used as input to configure ++ (e.g., "arm64-apple-ios-simulator"). ++ """ ++ triples = [] ++ for slice_name, slice_parts in HOSTS[platform].items(): ++ triples.extend(list(slice_parts)) ++ return triples ++ ++ ++def clean(context: argparse.Namespace, target: str = "all") -> None: ++ """The implementation of the "clean" command.""" ++ # If we're explicitly targeting the build, there's no platform or ++ # distribution artefacts. If we're cleaning tests, we keep all built ++ # artefacts. Otherwise, the built artefacts must be dirty, so we remove ++ # them. ++ if target not in {"build", "test"}: ++ paths = ["dist", context.platform] + list(HOSTS[context.platform]) ++ else: ++ paths = [] ++ ++ if target in {"all", "build"}: ++ paths.append("build") ++ ++ if target in {"all", "hosts"}: ++ paths.extend(all_host_triples(context.platform)) ++ elif target not in {"build", "test", "package"}: ++ paths.append(target) ++ ++ if target in {"all", "hosts", "test"}: ++ paths.extend([ ++ path.name ++ for path in CROSS_BUILD_DIR.glob(f"{context.platform}-testbed.*") ++ ]) ++ ++ for path in paths: ++ delete_path(path) ++ ++ ++def build_python_path() -> Path: ++ """The path to the build Python binary.""" ++ build_dir = subdir("build") ++ binary = build_dir / "python" ++ if not binary.is_file(): ++ binary = binary.with_suffix(".exe") ++ if not binary.is_file(): ++ raise FileNotFoundError( ++ f"Unable to find `python(.exe)` in {build_dir}" ++ ) ++ ++ return binary ++ ++ ++@contextmanager ++def group(text: str): ++ """A context manager that outputs a log marker around a section of a build. ++ ++ If running in a GitHub Actions environment, the GitHub syntax for ++ collapsible log sections is used. ++ """ ++ if "GITHUB_ACTIONS" in os.environ: ++ print(f"::group::{text}") ++ else: ++ print(f"===== {text} " + "=" * (70 - len(text))) ++ ++ yield ++ ++ if "GITHUB_ACTIONS" in os.environ: ++ print("::endgroup::") ++ else: ++ print() ++ ++ ++@contextmanager ++def cwd(subdir: Path): ++ """A context manager that sets the current working directory.""" ++ orig = os.getcwd() ++ os.chdir(subdir) ++ yield ++ os.chdir(orig) ++ ++ ++def configure_build_python(context: argparse.Namespace) -> None: ++ """The implementation of the "configure-build" command.""" ++ if context.clean: ++ clean(context, "build") ++ ++ with ( ++ group("Configuring build Python"), ++ cwd(subdir("build", create=True)), ++ ): ++ command = [relpath(PYTHON_DIR / "configure")] ++ if context.args: ++ command.extend(context.args) ++ run(command) ++ ++ ++def make_build_python(context: argparse.Namespace) -> None: ++ """The implementation of the "make-build" command.""" ++ with ( ++ group("Compiling build Python"), ++ cwd(subdir("build")), ++ ): ++ run(["make", "-j", str(os.cpu_count())]) ++ ++ ++def apple_target(host: str) -> str: ++ """Return the Apple platform identifier for a given host triple.""" ++ for _, platform_slices in HOSTS.items(): ++ for slice_name, slice_parts in platform_slices.items(): ++ for host_triple, multiarch in slice_parts.items(): ++ if host == host_triple: ++ return ".".join(multiarch.split("-")[::-1]) ++ ++ raise KeyError(host) ++ ++ ++def apple_multiarch(host: str) -> str: ++ """Return the multiarch descriptor for a given host triple.""" ++ for _, platform_slices in HOSTS.items(): ++ for slice_name, slice_parts in platform_slices.items(): ++ for host_triple, multiarch in slice_parts.items(): ++ if host == host_triple: ++ return multiarch ++ ++ raise KeyError(host) ++ ++ ++def unpack_deps( ++ platform: str, ++ host: str, ++ prefix_dir: Path, ++ cache_dir: Path, ++) -> None: ++ """Unpack binary dependencies into a provided directory. ++ ++ Downloads binaries if they aren't already present. Downloads will be stored ++ in provided cache directory. ++ ++ 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.18-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 ++ ++ ++def configure_host_python( ++ context: argparse.Namespace, ++ host: str | None = None, ++) -> None: ++ """The implementation of the "configure-host" command.""" ++ if host is None: ++ host = context.host ++ ++ if context.clean: ++ clean(context, host) ++ ++ host_dir = subdir(host, create=True) ++ prefix_dir = host_dir / "prefix" ++ ++ with group(f"Downloading dependencies ({host})"): ++ if not prefix_dir.exists(): ++ prefix_dir.mkdir() ++ unpack_deps(context.platform, host, prefix_dir, context.cache_dir) ++ else: ++ print("Dependencies already installed") ++ ++ with ( ++ group(f"Configuring host Python ({host})"), ++ cwd(host_dir), ++ ): ++ command = [ ++ # Basic cross-compiling configuration ++ relpath(PYTHON_DIR / "configure"), ++ f"--host={host}", ++ f"--build={sysconfig.get_config_var('BUILD_GNU_TYPE')}", ++ f"--with-build-python={build_python_path()}", ++ "--with-system-libmpdec", ++ "--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}/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"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"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 / "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) ++ ++ # 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", ++ ]) ++ ++ # Build the final archive ++ archive_name = ( ++ CROSS_BUILD_DIR ++ / "dist" ++ / f"python-{version}-{context.platform}-XCframework" ++ ) ++ ++ print() ++ print("Create package archive...") ++ shutil.make_archive( ++ str(CROSS_BUILD_DIR / archive_name), ++ format="gztar", ++ root_dir=CROSS_BUILD_DIR / context.platform, ++ base_dir=".", ++ ) ++ print() ++ print(f"{archive_name.relative_to(PYTHON_DIR)}.tar.gz created.") ++ ++ ++def build(context: argparse.Namespace, host: str | None = None) -> None: ++ """The implementation of the "build" command.""" ++ if host is None: ++ host = context.host ++ ++ if context.clean: ++ clean(context, host) ++ ++ if host in {"all", "build"}: ++ for step in [ ++ configure_build_python, ++ make_build_python, ++ ]: ++ step(context) ++ ++ if host == "build": ++ hosts = [] ++ elif host in {"all", "hosts"}: ++ hosts = all_host_triples(context.platform) ++ else: ++ hosts = [host] ++ ++ for step_host in hosts: ++ for step in [ ++ configure_host_python, ++ make_host_python, ++ ]: ++ step(context, host=step_host) ++ ++ if host in {"all", "hosts"}: ++ package(context) ++ ++ ++def test(context: argparse.Namespace, host: str | None = None) -> None: ++ """The implementation of the "test" command.""" ++ if host is None: ++ host = context.host ++ ++ if context.clean: ++ clean(context, "test") ++ ++ with group(f"Test {'XCframework' if host in {'all', 'hosts'} else host}"): ++ timestamp = str(time.time_ns())[:-6] ++ testbed_dir = ( ++ CROSS_BUILD_DIR / f"{context.platform}-testbed.{timestamp}" ++ ) ++ if host in {"all", "hosts"}: ++ framework_path = ( ++ CROSS_BUILD_DIR / context.platform / "Python.xcframework" ++ ) ++ else: ++ build_arch = platform.machine() ++ host_arch = host.split("-")[0] ++ ++ if not host.endswith("-simulator"): ++ print("Skipping test suite non-simulator build.") ++ return ++ elif build_arch != host_arch: ++ print( ++ f"Skipping test suite for an {host_arch} build " ++ f"on an {build_arch} machine." ++ ) ++ return ++ else: ++ framework_path = ( ++ CROSS_BUILD_DIR ++ / host ++ / f"Apple/{context.platform}" ++ / f"Frameworks/{apple_multiarch(host)}" ++ ) ++ ++ run([ ++ sys.executable, ++ "Apple/testbed", ++ "clone", ++ "--platform", ++ context.platform, ++ "--framework", ++ framework_path, ++ testbed_dir, ++ ]) ++ ++ run( ++ [ ++ sys.executable, ++ testbed_dir, ++ "run", ++ "--verbose", ++ ] ++ + ( ++ ["--simulator", str(context.simulator)] ++ if context.simulator ++ else [] ++ ) ++ + [ ++ "--", ++ "test", ++ # 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", ++ ] ++ ) ++ ++ ++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" ++ ) ++ subcommands.add_parser( ++ "make-build", help="Run `make` for the build Python" ++ ) ++ configure_host = subcommands.add_parser( ++ "configure-host", ++ help="Run `configure` for a specific platform and target", ++ ) ++ make_host = subcommands.add_parser( ++ "make-host", ++ help="Run `make` for a specific platform and target", ++ ) ++ package = subcommands.add_parser( ++ "package", ++ help="Create a release package for the platform", ++ ) ++ build = subcommands.add_parser( ++ "build", ++ help="Build all platform targets and create the XCframework", ++ ) ++ test = subcommands.add_parser( ++ "test", ++ help="Run the testbed for a specific platform", ++ ) ++ ci = subcommands.add_parser( ++ "ci", ++ help="Run build, package, and test", ++ ) ++ ++ # platform argument ++ for cmd in [clean, configure_host, make_host, package, build, test, ci]: ++ cmd.add_argument( ++ "platform", ++ choices=HOSTS.keys(), ++ help="The target platform to build", ++ ) ++ ++ # host triple argument ++ for cmd in [configure_host, make_host]: ++ cmd.add_argument( ++ "host", ++ help="The host triple to build (e.g., arm64-apple-ios-simulator)", ++ ) ++ # optional host triple argument ++ for cmd in [clean, build, test]: ++ cmd.add_argument( ++ "host", ++ nargs="?", ++ default="all", ++ help=( ++ "The host triple to build (e.g., arm64-apple-ios-simulator), " ++ "or 'build' for just the build platform, or 'hosts' for all " ++ "host platforms, or 'all' for the build platform and all " ++ "hosts. Defaults to 'all'" ++ ), ++ ) ++ ++ # --clean option ++ for cmd in [configure_build, configure_host, build, package, test, ci]: ++ cmd.add_argument( ++ "--clean", ++ action="store_true", ++ default=False, ++ dest="clean", ++ help="Delete the relevant build directories first", ++ ) ++ ++ # --cache-dir option ++ for cmd in [configure_host, build, ci]: ++ cmd.add_argument( ++ "--cache-dir", ++ default="./cross-build/downloads", ++ help="The directory to store cached downloads.", ++ ) ++ ++ # --simulator option ++ for cmd in [test, ci]: ++ cmd.add_argument( ++ "--simulator", ++ help=( ++ "The name of the simulator to use (eg: 'iPhone 16e'). " ++ "Defaults to the most recently released 'entry level' " ++ "iPhone device. Device architecture and OS version can also " ++ "be specified; e.g., " ++ "`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would " ++ "run on an ARM64 iPhone 16 Pro simulator running iOS 26.0." ++ ), ++ ) ++ 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() ++ 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() +--- /dev/null ++++ b/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 `Apple` subfolder of the Python repository acts as a build script that ++can be used to coordinate the compilation of a complete iOS XCframework. To use ++it, run:: ++ ++ python Apple build iOS ++ ++This will: ++ ++* Configure and compile a version of Python to run on the build machine ++* Download pre-compiled binary dependencies for each platform ++* Configure and build a `Python.framework` for each required architecture and ++ iOS SDK ++* Merge the multiple `Python.framework` folders into a single `Python.xcframework` ++* Produce a `.tar.gz` archive in the `cross-build/dist` folder containing ++ the `Python.xcframework`, plus a copy of the Testbed app pre-configured to ++ use the XCframework. ++ ++The `Apple` build script has other entry points that will perform the ++individual parts of the overall `build` target, plus targets to test the ++build, clean the `cross-build` folder of iOS build products, and perform a ++complete "build and test" CI run. The `--clean` flag can also be used on ++individual commands to ensure that a stale build product are removed before ++building. ++ ++### Building a single-architecture framework ++ ++If you're using the `Apple` build script, you won't need to build ++individual frameworks. However, if you do need to manually configure an iOS ++Python build for a single framework, the following options are available. ++ ++#### iOS specific arguments to configure ++ ++* `--enable-framework[=DIR]` ++ ++ This argument specifies the location where the Python.framework will be ++ installed. If `DIR` is not specified, the framework will be installed into ++ a subdirectory of the `iOS/Frameworks` folder. ++ ++ This argument *must* be provided when configuring iOS builds. iOS does not ++ support non-framework builds. ++ ++* `--with-framework-name=NAME` ++ ++ Specify the name for the Python framework; defaults to `Python`. ++ ++ > [!NOTE] ++ > Unless you know what you're doing, changing the name of the Python ++ > framework on iOS is not advised. If you use this option, you won't be able ++ > to run the `Apple` build script without making significant manual ++ > alterations, and you won't be able to use any binary packages unless you ++ > compile them yourself using your own framework name. ++ ++#### Building Python for iOS ++ ++The Python build system will create a `Python.framework` that supports a ++*single* ABI with a *single* architecture. Unlike macOS, iOS does not allow a ++framework to contain non-library content, so the iOS build will produce a ++`bin` and `lib` folder in the same output folder as `Python.framework`. ++The `lib` folder will be needed at runtime to support the Python library. ++ ++If you want to use Python in a real iOS project, you need to produce multiple ++`Python.framework` builds, one for each ABI and architecture. iOS builds of ++Python *must* be constructed as framework builds. To support this, you must ++provide the `--enable-framework` flag when configuring the build. The build ++also requires the use of cross-compilation. The minimal commands for building ++Python for the ARM64 iOS simulator will look something like: ++``` ++export PATH="$(pwd)/Apple/iOS/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" ++./configure \ ++ --enable-framework \ ++ --host=arm64-apple-ios-simulator \ ++ --build=arm64-apple-darwin \ ++ --with-build-python=/path/to/python.exe ++make ++make install ++``` ++ ++In this invocation: ++ ++* `Apple/iOS/Resources/bin` has been added to the path, providing some shims for the ++ compilers and linkers needed by the build. Xcode requires the use of `xcrun` ++ to invoke compiler tooling. However, if `xcrun` is pre-evaluated and the ++ result passed to `configure`, these results can embed user- and ++ version-specific paths into the sysconfig data, which limits the portability ++ of the compiled Python. Alternatively, if `xcrun` is used *as* the compiler, ++ it requires that compiler variables like `CC` include spaces, which can ++ cause significant problems with many C configuration systems which assume that ++ `CC` will be a single executable. ++ ++ To work around this problem, the `Apple/iOS/Resources/bin` folder contains some ++ wrapper scripts that present as simple compilers and linkers, but wrap ++ underlying calls to `xcrun`. This allows configure to use a `CC` ++ definition without spaces, and without user- or version-specific paths, while ++ retaining the ability to adapt to the local Xcode install. These scripts are ++ included in the `bin` directory of an iOS install. ++ ++ These scripts will, by default, use the currently active Xcode installation. ++ If you want to use a different Xcode installation, you can use ++ `xcode-select` to set a new default Xcode globally, or you can use the ++ `DEVELOPER_DIR` environment variable to specify an Xcode install. The ++ scripts will use the default `iphoneos`/`iphonesimulator` SDK version for ++ the select Xcode install; if you want to use a different SDK, you can set the ++ `IOS_SDK_VERSION` environment variable. (e.g, setting ++ `IOS_SDK_VERSION=17.1` would cause the scripts to use the `iphoneos17.1` ++ and `iphonesimulator17.1` SDKs, regardless of the Xcode default.) ++ ++ The path has also been cleared of any user customizations. A common source of ++ bugs is for tools like Homebrew to accidentally leak macOS binaries into an iOS ++ build. Resetting the path to a known "bare bones" value is the easiest way to ++ avoid these problems. ++ ++* `--host` is the architecture and ABI that you want to build, in GNU compiler ++ triple format. This will be one of: ++ ++ - `arm64-apple-ios` for ARM64 iOS devices. ++ - `arm64-apple-ios-simulator` for the iOS simulator running on Apple ++ Silicon devices. ++ - `x86_64-apple-ios-simulator` for the iOS simulator running on Intel ++ devices. ++ ++* `--build` is the GNU compiler triple for the machine that will be running ++ the compiler. This is one of: ++ ++ - `arm64-apple-darwin` for Apple Silicon devices. ++ - `x86_64-apple-darwin` for Intel devices. ++ ++* `/path/to/python.exe` is the path to a Python binary on the machine that ++ will be running the compiler. This is needed because the Python compilation ++ process involves running some Python code. On a normal desktop build of ++ Python, you can compile a python interpreter and then use that interpreter to ++ run Python code. However, the binaries produced for iOS won't run on macOS, so ++ you need to provide an external Python interpreter. This interpreter must be ++ the same version as the Python that is being compiled. To be completely safe, ++ this should be the *exact* same commit hash. However, the longer a Python ++ release has been stable, the more likely it is that this constraint can be ++ relaxed - the same micro version will often be sufficient. ++ ++* The `install` target for iOS builds is slightly different to other ++ platforms. On most platforms, `make install` will install the build into ++ the final runtime location. This won't be the case for iOS, as the final ++ runtime location will be on a physical device. ++ ++ However, you still need to run the `install` target for iOS builds, as it ++ performs some final framework assembly steps. The location specified with ++ `--enable-framework` will be the location where `make install` will ++ assemble the complete iOS framework. This completed framework can then ++ be copied and relocated as required. ++ ++For a full CPython build, you also need to specify the paths to iOS builds of ++the binary libraries that CPython depends on (such as XZ, LibFFI and OpenSSL). ++This can be done by defining library specific environment variables (such as ++`LIBLZMA_CFLAGS`, `LIBLZMA_LIBS`), and the `--with-openssl` configure ++option. Versions of these libraries pre-compiled for iOS can be found in [this ++repository](https://github.com/beeware/cpython-apple-source-deps/releases). ++LibFFI is especially important, as many parts of the standard library ++(including the `platform`, `sysconfig` and `webbrowser` modules) require ++the use of the `ctypes` module at runtime. ++ ++By default, Python will be compiled with an iOS deployment target (i.e., the ++minimum supported iOS version) of 13.0. To specify a different deployment ++target, provide the version number as part of the `--host` argument - for ++example, `--host=arm64-apple-ios15.4-simulator` would compile an ARM64 ++simulator build with a deployment target of 15.4. ++ ++## Testing Python on iOS ++ ++### Testing a multi-architecture framework ++ ++Once you have a built an XCframework, you can test that framework by running: ++ ++ $ python Apple test iOS ++ ++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 `Apple/testbed` folder that contains an Xcode project that is able to run ++the Python test suite on Apple platforms. This project converts the Python test ++suite into a single test case in Xcode's XCTest framework. The single XCTest ++passes if the test suite passes. ++ ++To run the test suite, configure a Python build for an iOS simulator (i.e., ++`--host=arm64-apple-ios-simulator` or `--host=x86_64-apple-ios-simulator` ++), specifying a framework build (i.e. `--enable-framework`). Ensure that your ++`PATH` has been configured to include the `Apple/iOS/Resources/bin` folder and ++exclude any non-iOS tools, then run: ++``` ++make all ++make install ++make testios ++``` ++ ++This will: ++ ++* Build an iOS framework for your chosen architecture; ++* Finalize the single-platform framework; ++* Make a clean copy of the testbed project; ++* Install the Python iOS framework into the copy of the testbed project; and ++* Run the test suite on an "entry-level device" simulator (i.e., an iPhone SE, ++ iPhone 16e, or a similar). ++ ++On success, the test suite will exit and report successful completion of the ++test suite. On a 2022 M1 MacBook Pro, the test suite takes approximately 15 ++minutes to run; a couple of extra minutes is required to compile the testbed ++project, and then boot and prepare the iOS simulator. ++ ++### Debugging test failures ++ ++Running `python Apple test iOS` generates a standalone version of the ++`Apple/testbed` project, and runs the full test suite. It does this using ++`Apple/testbed` itself - the folder is an executable module that can be used ++to create and run a clone of the testbed project. The standalone version of the ++testbed will be created in a directory named ++`cross-build/iOS-testbed.`. ++ ++You can generate your own standalone testbed instance by running: ++``` ++python cross-build/iOS/testbed clone my-testbed ++``` ++ ++In this invocation, `my-testbed` is the name of the folder for the new ++testbed clone. ++ ++If you've built your own XCframework, or you only want to test a single architecture, ++you can construct a standalone testbed instance by running: ++``` ++python Apple/testbed clone --platform iOS --framework my-testbed ++``` ++ ++The framework path can be the path path to a `Python.xcframework`, or the ++path to a folder that contains a single-platform `Python.framework`. ++ ++You can then use the `my-testbed` folder to run the Python test suite, ++passing in any command line arguments you may require. For example, if you're ++trying to diagnose a failure in the `os` module, you might run: ++``` ++python my-testbed run -- test -W test_os ++``` ++ ++This is the equivalent of running `python -m test -W test_os` on a desktop ++Python build. Any arguments after the `--` will be passed to testbed as if ++they were arguments to `python -m` on a desktop machine. ++ ++### Testing in Xcode ++ ++You can also open the testbed project in Xcode by running: ++``` ++open my-testbed/iOSTestbed.xcodeproj ++``` ++ ++This will allow you to use the full Xcode suite of tools for debugging. ++ ++The arguments used to run the test suite are defined as part of the test plan. ++To modify the test plan, select the test plan node of the project tree (it ++should be the first child of the root node), and select the "Configurations" ++tab. Modify the "Arguments Passed On Launch" value to change the testing ++arguments. ++ ++The test plan also disables parallel testing, and specifies the use of the ++`Testbed.lldbinit` file for providing configuration of the debugger. The ++default debugger configuration disables automatic breakpoints on the ++`SIGINT`, `SIGUSR1`, `SIGUSR2`, and `SIGXFSZ` signals. ++ ++### Testing on an iOS device ++ ++To test on an iOS device, the app needs to be signed with known developer ++credentials. To obtain these credentials, you must have an iOS Developer ++account, and your Xcode install will need to be logged into your account (see ++the Accounts tab of the Preferences dialog). ++ ++Once the project is open, and you're signed into your Apple Developer account, ++select the root node of the project tree (labeled "iOSTestbed"), then the ++"Signing & Capabilities" tab in the details page. Select a development team ++(this will likely be your own name), and plug in a physical device to your ++macOS machine with a USB cable. You should then be able to select your physical ++device from the list of targets in the pulldown in the Xcode titlebar. +--- /dev/null ++++ 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@ ++ ++ +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-ar +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-clang +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-cpp +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} -E "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch arm64 "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} strip -arch arm64 "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch x86_64 "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/pyconfig.h +@@ -0,0 +1,7 @@ ++#ifdef __arm64__ ++#include "pyconfig-arm64.h" ++#endif ++ ++#ifdef __x86_64__ ++#include "pyconfig-x86_64.h" ++#endif +--- /dev/null ++++ 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 ++ ++ +--- /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 ++ ++ +--- /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 ++ ++ +--- /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 ++# ----- ++ ++# 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. ++ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib" ]; then ++ rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" ++ rsync -au "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib-$ARCHS/" "$CODESIGNING_FOLDER_PATH/python/lib/" ++ else ++ # A single-arch framework will have a libpython symlink; that can't be included at runtime ++ 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") ++ echo "Install Python $PYTHON_VER standard library extension modules..." ++ process_dylibs $PYTHON_XCFRAMEWORK_PATH python/lib/$PYTHON_VER/lib-dynload ++ ++ for package_path in $@; do ++ echo "Installing $package_path extension modules ..." ++ process_dylibs $PYTHON_XCFRAMEWORK_PATH $package_path ++ done ++} +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/build/watchOS-dylib-Info-template.plist +@@ -0,0 +1,26 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleExecutable ++ ++ CFBundleIdentifier ++ ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundlePackageType ++ APPL ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSupportedPlatforms ++ ++ watchOS ++ ++ MinimumOSVersion ++ 4.0 ++ CFBundleVersion ++ 1 ++ ++ +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/ios-arm64/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling an iOS on-device ++build for testing purposes. +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling an iOS simulator ++build for testing purposes (either x86_64 or ARM64). +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/tvos-arm64/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a tvOS ++on-device build for testing purposes. +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/tvos-arm64_x86_64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a tvOS ++simulator build for testing purposes (either x86_64 or ARM64). +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/watchos-arm64_32/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a watchOS on-device ++build for testing purposes. +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/watchos-arm64_x86_64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a watchOS ++simulator build for testing purposes (either x86_64 or ARM64). +--- /dev/null ++++ b/Apple/testbed/Testbed.lldbinit +@@ -0,0 +1,4 @@ ++process handle SIGINT -n true -p true -s false ++process handle SIGUSR1 -n true -p true -s false ++process handle SIGUSR2 -n true -p true -s false ++process handle SIGXFSZ -n true -p true -s false +--- /dev/null ++++ b/Apple/testbed/TestbedTests/TestbedTests.m +@@ -0,0 +1,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 +--- /dev/null ++++ b/Apple/testbed/__main__.py +@@ -0,0 +1,456 @@ ++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", ++ "visionOS": "xros-arm64-simulator", ++ "watchOS": "watchos-arm64_x86_64-simulator", ++} ++ ++DECODE_ARGS = ("UTF-8", "backslashreplace") ++ ++# The system log prefixes each line: ++# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ... ++# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ... ++ ++LOG_PREFIX_REGEX = re.compile( ++ r"^\d{4}-\d{2}-\d{2}" # YYYY-MM-DD ++ r"\s+\d+:\d{2}:\d{2}\.\d+\+\d{4}" # HH:MM:SS.ssssss+ZZZZ ++ r"\s+.*Testbed\[\d+:\w+\]" # Process/thread ID ++) ++ ++ ++# Select a simulator device to use. ++def select_simulator_device(platform): ++ # List the testing simulators, in JSON format ++ raw_json = subprocess.check_output(["xcrun", "simctl", "list", "-j"]) ++ json_data = json.loads(raw_json) ++ ++ if platform == "iOS": ++ # Any iOS device will do; we'll look for "SE" devices - but the name ++ # isn't consistent over time. Older Xcode versions will use "iPhone SE ++ # (Nth generation)"; As of 2025, they've started using "iPhone 16e". ++ # ++ # When Xcode is updated after a new release, new devices will be ++ # available and old ones will be dropped from the set available on the ++ # latest iOS version. Select the one with the highest minimum runtime ++ # version - this is an indicator of the "newest" released device, which ++ # should always be supported on the "most recent" iOS version. ++ se_simulators = sorted( ++ (devicetype["minRuntimeVersion"], devicetype["name"]) ++ for devicetype in json_data["devicetypes"] ++ if devicetype["productFamily"] == "iPhone" ++ and ( ++ ( ++ "iPhone " in devicetype["name"] ++ and devicetype["name"].endswith("e") ++ ) ++ or "iPhone SE " in devicetype["name"] ++ ) ++ ) ++ simulator = se_simulators[-1][1] ++ elif platform == "tvOS": ++ # Find the most recent tvOS release. ++ simulators = sorted( ++ (devicetype["minRuntimeVersion"], devicetype["name"]) ++ for devicetype in json_data["devicetypes"] ++ if devicetype["productFamily"] == "Apple TV" ++ ) ++ simulator = simulators[-1][1] ++ elif platform == "visionOS": ++ # Find the most recent visionOS release. ++ simulators = sorted( ++ (devicetype["minRuntimeVersion"], devicetype["name"]) ++ for devicetype in json_data["devicetypes"] ++ if devicetype["productFamily"] == "Apple Vision" ++ ) ++ simulator = simulators[-1][1] ++ elif platform == "watchOS": ++ raise NotImplementedError("Don't know how to launch watchOS (yet)") ++ else: ++ raise ValueError(f"Unknown platform {platform}") ++ ++ return simulator ++ ++ ++def xcode_test(location: Path, platform: str, simulator: str, verbose: bool): ++ # Build and run the test suite on the named simulator. ++ args = [ ++ "-project", ++ str(location / f"{platform}Testbed.xcodeproj"), ++ "-scheme", ++ f"{platform}Testbed", ++ "-destination", ++ f"platform={platform} Simulator,name={simulator}", ++ "-derivedDataPath", ++ str(location / "DerivedData"), ++ ] ++ verbosity_args = [] if verbose else ["-quiet"] ++ ++ print("Building test project...") ++ subprocess.run( ++ ["xcodebuild", "build-for-testing"] + args + verbosity_args, ++ check=True, ++ ) ++ ++ # 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 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.") ++ ++ if simulator is None: ++ simulator = select_simulator_device(platform) ++ print(f"Running test on {simulator}") ++ ++ xcode_test( ++ location, ++ platform=platform, ++ simulator=simulator, ++ verbose=verbose, ++ ) ++ ++ ++def main(): ++ # Look for directories like `iOSTestbed` as an indicator of the platforms ++ # that the testbed folder supports. The original source testbed can support ++ # many platforms, but when cloned, only one platform is preserved. ++ available_platforms = [ ++ platform ++ for platform in ["iOS", "tvOS", "visionOS", "watchOS"] ++ if (Path(__file__).parent / f"{platform}Testbed").is_dir() ++ ] ++ ++ parser = argparse.ArgumentParser( ++ description=( ++ "Manages the process of testing an Apple Python project " ++ "through Xcode." ++ ), ++ ) ++ ++ subcommands = parser.add_subparsers(dest="subcommand") ++ clone = subcommands.add_parser( ++ "clone", ++ description=( ++ "Clone the testbed project, copying in a Python framework and" ++ "any specified application code." ++ ), ++ help="Clone a testbed project to a new location.", ++ ) ++ clone.add_argument( ++ "--framework", ++ help=( ++ "The location of the XCFramework (or simulator-only slice of an " ++ "XCFramework) to use when running the testbed" ++ ), ++ ) ++ clone.add_argument( ++ "--platform", ++ dest="platform", ++ choices=available_platforms, ++ default=available_platforms[0], ++ help=f"The platform to target (default: {available_platforms[0]})", ++ ) ++ clone.add_argument( ++ "--app", ++ dest="apps", ++ action="append", ++ default=[], ++ help="The location of any code to include in the testbed project", ++ ) ++ clone.add_argument( ++ "location", ++ help="The path where the testbed will be cloned.", ++ ) ++ ++ run = subcommands.add_parser( ++ "run", ++ usage=( ++ "%(prog)s [-h] [--simulator SIMULATOR] -- " ++ " [ ...]" ++ ), ++ description=( ++ "Run a testbed project. The arguments provided after `--` will be " ++ "passed to the running test process as if they were arguments to " ++ "`python -m`." ++ ), ++ help="Run a testbed project", ++ ) ++ run.add_argument( ++ "--platform", ++ dest="platform", ++ choices=available_platforms, ++ default=available_platforms[0], ++ help=f"The platform to target (default: {available_platforms[0]})", ++ ) ++ run.add_argument( ++ "--simulator", ++ help=( ++ "The name of the simulator to use (eg: 'iPhone 16e'). Defaults to " ++ "the most recently released 'entry level' iPhone device. Device " ++ "architecture and OS version can also be specified; e.g., " ++ "`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would run on " ++ "an ARM64 iPhone 16 Pro simulator running iOS 26.0." ++ ), ++ ) ++ run.add_argument( ++ "-v", ++ "--verbose", ++ action="store_true", ++ help="Enable verbose output", ++ ) ++ ++ try: ++ pos = sys.argv.index("--") ++ testbed_args = sys.argv[1:pos] ++ test_args = sys.argv[pos + 1 :] ++ except ValueError: ++ testbed_args = sys.argv[1:] ++ test_args = [] ++ ++ context = parser.parse_args(testbed_args) ++ ++ if context.subcommand == "clone": ++ clone_testbed( ++ source=Path(__file__).parent.resolve(), ++ target=Path(context.location).resolve(), ++ framework=Path(context.framework).resolve() ++ if context.framework ++ else None, ++ platform=context.platform, ++ apps=[Path(app) for app in context.apps], ++ ) ++ elif context.subcommand == "run": ++ if test_args: ++ if not ( ++ Path(__file__).parent ++ / "Python.xcframework" ++ / TEST_SLICES[context.platform] ++ / "bin" ++ ).is_dir(): ++ print( ++ "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() +--- /dev/null ++++ b/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj +@@ -0,0 +1,557 @@ ++// !$*UTF8*$! ++{ ++ archiveVersion = 1; ++ classes = { ++ }; ++ objectVersion = 56; ++ objects = { ++ ++/* Begin PBXBuildFile section */ ++ 607A66172B0EFA380010BFC8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66162B0EFA380010BFC8 /* AppDelegate.m */; }; ++ 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607A66212B0EFA390010BFC8 /* Assets.xcassets */; }; ++ 607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */; }; ++ 607A66282B0EFA390010BFC8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66272B0EFA390010BFC8 /* main.m */; }; ++ 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */; }; ++ 607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; ++ 607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; ++ 607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; ++ 607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; ++ 608619542CB77BA900F46182 /* app_packages in Resources */ = {isa = PBXBuildFile; fileRef = 608619532CB77BA900F46182 /* app_packages */; }; ++ 608619562CB7819B00F46182 /* app in Resources */ = {isa = PBXBuildFile; fileRef = 608619552CB7819B00F46182 /* app */; }; ++/* End PBXBuildFile section */ ++ ++/* Begin PBXContainerItemProxy section */ ++ 607A662E2B0EFA3A0010BFC8 /* PBXContainerItemProxy */ = { ++ isa = PBXContainerItemProxy; ++ containerPortal = 607A660A2B0EFA380010BFC8 /* Project object */; ++ proxyType = 1; ++ remoteGlobalIDString = 607A66112B0EFA380010BFC8; ++ remoteInfo = iOSTestbed; ++ }; ++/* End PBXContainerItemProxy section */ ++ ++/* Begin PBXCopyFilesBuildPhase section */ ++ 607A664E2B0EFC080010BFC8 /* Embed Frameworks */ = { ++ isa = PBXCopyFilesBuildPhase; ++ buildActionMask = 2147483647; ++ dstPath = ""; ++ dstSubfolderSpec = 10; ++ files = ( ++ 607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */, ++ ); ++ name = "Embed Frameworks"; ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */ = { ++ isa = PBXCopyFilesBuildPhase; ++ buildActionMask = 2147483647; ++ dstPath = ""; ++ dstSubfolderSpec = 10; ++ files = ( ++ 607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */, ++ ); ++ name = "Embed Frameworks"; ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXCopyFilesBuildPhase section */ ++ ++/* Begin PBXFileReference section */ ++ 607A66122B0EFA380010BFC8 /* iOSTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; }; ++ 607A66152B0EFA380010BFC8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; ++ 607A66162B0EFA380010BFC8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; ++ 607A66212B0EFA390010BFC8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; ++ 607A66242B0EFA390010BFC8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; ++ 607A66272B0EFA390010BFC8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; ++ 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSTestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; ++ 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestbedTests.m; sourceTree = ""; }; ++ 607A664A2B0EFB310010BFC8 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; ++ 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = ""; }; ++ 608619532CB77BA900F46182 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = ""; }; ++ 608619552CB7819B00F46182 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = ""; }; ++ 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = iOSTestbed.xctestplan; sourceTree = ""; }; ++/* End PBXFileReference section */ ++ ++/* Begin PBXFrameworksBuildPhase section */ ++ 607A660F2B0EFA380010BFC8 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A662A2B0EFA3A0010BFC8 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXFrameworksBuildPhase section */ ++ ++/* Begin PBXGroup section */ ++ 607A66092B0EFA380010BFC8 = { ++ isa = PBXGroup; ++ children = ( ++ 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */, ++ 607A664A2B0EFB310010BFC8 /* Python.xcframework */, ++ 607A66142B0EFA380010BFC8 /* iOSTestbed */, ++ 607A66302B0EFA3A0010BFC8 /* TestbedTests */, ++ 607A66132B0EFA380010BFC8 /* Products */, ++ 607A664F2B0EFFE00010BFC8 /* Frameworks */, ++ ); ++ sourceTree = ""; ++ }; ++ 607A66132B0EFA380010BFC8 /* Products */ = { ++ isa = PBXGroup; ++ children = ( ++ 607A66122B0EFA380010BFC8 /* iOSTestbed.app */, ++ 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */, ++ ); ++ name = Products; ++ sourceTree = ""; ++ }; ++ 607A66142B0EFA380010BFC8 /* iOSTestbed */ = { ++ isa = PBXGroup; ++ children = ( ++ 608619552CB7819B00F46182 /* app */, ++ 608619532CB77BA900F46182 /* app_packages */, ++ 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */, ++ 607A66152B0EFA380010BFC8 /* AppDelegate.h */, ++ 607A66162B0EFA380010BFC8 /* AppDelegate.m */, ++ 607A66212B0EFA390010BFC8 /* Assets.xcassets */, ++ 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */, ++ 607A66272B0EFA390010BFC8 /* main.m */, ++ ); ++ path = iOSTestbed; ++ sourceTree = ""; ++ }; ++ 607A66302B0EFA3A0010BFC8 /* TestbedTests */ = { ++ isa = PBXGroup; ++ children = ( ++ 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */, ++ ); ++ path = TestbedTests; ++ sourceTree = ""; ++ }; ++ 607A664F2B0EFFE00010BFC8 /* Frameworks */ = { ++ isa = PBXGroup; ++ children = ( ++ ); ++ name = Frameworks; ++ sourceTree = ""; ++ }; ++/* End PBXGroup section */ ++ ++/* Begin PBXNativeTarget section */ ++ 607A66112B0EFA380010BFC8 /* iOSTestbed */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbed" */; ++ buildPhases = ( ++ 607A660E2B0EFA380010BFC8 /* Sources */, ++ 607A660F2B0EFA380010BFC8 /* Frameworks */, ++ 607A66102B0EFA380010BFC8 /* Resources */, ++ 607A66552B0F061D0010BFC8 /* Process Python libraries */, ++ 607A664E2B0EFC080010BFC8 /* Embed Frameworks */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ ); ++ name = iOSTestbed; ++ productName = iOSTestbed; ++ productReference = 607A66122B0EFA380010BFC8 /* iOSTestbed.app */; ++ productType = "com.apple.product-type.application"; ++ }; ++ 607A662C2B0EFA3A0010BFC8 /* iOSTestbedTests */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbedTests" */; ++ buildPhases = ( ++ 607A66292B0EFA3A0010BFC8 /* Sources */, ++ 607A662A2B0EFA3A0010BFC8 /* Frameworks */, ++ 607A662B2B0EFA3A0010BFC8 /* Resources */, ++ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */, ++ ); ++ name = iOSTestbedTests; ++ productName = iOSTestbedTests; ++ productReference = 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */; ++ productType = "com.apple.product-type.bundle.unit-test"; ++ }; ++/* End PBXNativeTarget section */ ++ ++/* Begin PBXProject section */ ++ 607A660A2B0EFA380010BFC8 /* Project object */ = { ++ isa = PBXProject; ++ attributes = { ++ BuildIndependentTargetsInParallel = 1; ++ LastUpgradeCheck = 1500; ++ TargetAttributes = { ++ 607A66112B0EFA380010BFC8 = { ++ CreatedOnToolsVersion = 15.0.1; ++ }; ++ 607A662C2B0EFA3A0010BFC8 = { ++ CreatedOnToolsVersion = 15.0.1; ++ TestTargetID = 607A66112B0EFA380010BFC8; ++ }; ++ }; ++ }; ++ buildConfigurationList = 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "iOSTestbed" */; ++ compatibilityVersion = "Xcode 14.0"; ++ developmentRegion = en; ++ hasScannedForEncodings = 0; ++ knownRegions = ( ++ en, ++ Base, ++ ); ++ mainGroup = 607A66092B0EFA380010BFC8; ++ productRefGroup = 607A66132B0EFA380010BFC8 /* Products */; ++ projectDirPath = ""; ++ projectRoot = ""; ++ targets = ( ++ 607A66112B0EFA380010BFC8 /* iOSTestbed */, ++ 607A662C2B0EFA3A0010BFC8 /* iOSTestbedTests */, ++ ); ++ }; ++/* End PBXProject section */ ++ ++/* Begin PBXResourcesBuildPhase section */ ++ 607A66102B0EFA380010BFC8 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */, ++ 608619562CB7819B00F46182 /* app in Resources */, ++ 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */, ++ 608619542CB77BA900F46182 /* app_packages in Resources */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A662B2B0EFA3A0010BFC8 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXResourcesBuildPhase section */ ++ ++/* Begin PBXShellScriptBuildPhase section */ ++ 607A66552B0F061D0010BFC8 /* Process Python libraries */ = { ++ isa = PBXShellScriptBuildPhase; ++ alwaysOutOfDate = 1; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ inputFileListPaths = ( ++ ); ++ inputPaths = ( ++ ); ++ name = "Process Python libraries"; ++ outputFileListPaths = ( ++ ); ++ outputPaths = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ shellPath = /bin/sh; ++ shellScript = "set -e\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\ninstall_python Python.xcframework app app_packages\n"; ++ showEnvVarsInLog = 0; ++ }; ++/* End PBXShellScriptBuildPhase section */ ++ ++/* Begin PBXSourcesBuildPhase section */ ++ 607A660E2B0EFA380010BFC8 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A66172B0EFA380010BFC8 /* AppDelegate.m in Sources */, ++ 607A66282B0EFA390010BFC8 /* main.m in Sources */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A66292B0EFA3A0010BFC8 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXSourcesBuildPhase section */ ++ ++/* Begin PBXTargetDependency section */ ++ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */ = { ++ isa = PBXTargetDependency; ++ target = 607A66112B0EFA380010BFC8 /* iOSTestbed */; ++ targetProxy = 607A662E2B0EFA3A0010BFC8 /* PBXContainerItemProxy */; ++ }; ++/* End PBXTargetDependency section */ ++ ++/* Begin PBXVariantGroup section */ ++ 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */ = { ++ isa = PBXVariantGroup; ++ children = ( ++ 607A66242B0EFA390010BFC8 /* Base */, ++ ); ++ name = LaunchScreen.storyboard; ++ sourceTree = ""; ++ }; ++/* End PBXVariantGroup section */ ++ ++/* Begin XCBuildConfiguration section */ ++ 607A663F2B0EFA3A0010BFC8 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ALWAYS_SEARCH_USER_PATHS = NO; ++ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ++ CLANG_ANALYZER_NONNULL = YES; ++ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; ++ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; ++ CLANG_ENABLE_MODULES = YES; ++ CLANG_ENABLE_OBJC_ARC = YES; ++ CLANG_ENABLE_OBJC_WEAK = YES; ++ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; ++ CLANG_WARN_BOOL_CONVERSION = YES; ++ CLANG_WARN_COMMA = YES; ++ CLANG_WARN_CONSTANT_CONVERSION = YES; ++ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; ++ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; ++ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; ++ CLANG_WARN_EMPTY_BODY = YES; ++ CLANG_WARN_ENUM_CONVERSION = YES; ++ CLANG_WARN_INFINITE_RECURSION = YES; ++ CLANG_WARN_INT_CONVERSION = YES; ++ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; ++ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; ++ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; ++ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; ++ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; ++ CLANG_WARN_STRICT_PROTOTYPES = YES; ++ CLANG_WARN_SUSPICIOUS_MOVE = YES; ++ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; ++ CLANG_WARN_UNREACHABLE_CODE = YES; ++ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; ++ COPY_PHASE_STRIP = NO; ++ DEBUG_INFORMATION_FORMAT = dwarf; ++ ENABLE_STRICT_OBJC_MSGSEND = YES; ++ ENABLE_TESTABILITY = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = YES; ++ GCC_C_LANGUAGE_STANDARD = gnu17; ++ GCC_DYNAMIC_NO_PIC = NO; ++ GCC_NO_COMMON_BLOCKS = YES; ++ GCC_OPTIMIZATION_LEVEL = 0; ++ GCC_PREPROCESSOR_DEFINITIONS = ( ++ "DEBUG=1", ++ "$(inherited)", ++ ); ++ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; ++ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; ++ GCC_WARN_UNDECLARED_SELECTOR = YES; ++ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; ++ GCC_WARN_UNUSED_FUNCTION = YES; ++ GCC_WARN_UNUSED_VARIABLE = YES; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; ++ MTL_FAST_MATH = YES; ++ ONLY_ACTIVE_ARCH = YES; ++ SDKROOT = iphoneos; ++ }; ++ name = Debug; ++ }; ++ 607A66402B0EFA3A0010BFC8 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ALWAYS_SEARCH_USER_PATHS = NO; ++ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ++ CLANG_ANALYZER_NONNULL = YES; ++ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; ++ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; ++ CLANG_ENABLE_MODULES = YES; ++ CLANG_ENABLE_OBJC_ARC = YES; ++ CLANG_ENABLE_OBJC_WEAK = YES; ++ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; ++ CLANG_WARN_BOOL_CONVERSION = YES; ++ CLANG_WARN_COMMA = YES; ++ CLANG_WARN_CONSTANT_CONVERSION = YES; ++ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; ++ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; ++ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; ++ CLANG_WARN_EMPTY_BODY = YES; ++ CLANG_WARN_ENUM_CONVERSION = YES; ++ CLANG_WARN_INFINITE_RECURSION = YES; ++ CLANG_WARN_INT_CONVERSION = YES; ++ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; ++ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; ++ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; ++ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; ++ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; ++ CLANG_WARN_STRICT_PROTOTYPES = YES; ++ CLANG_WARN_SUSPICIOUS_MOVE = YES; ++ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; ++ CLANG_WARN_UNREACHABLE_CODE = YES; ++ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; ++ COPY_PHASE_STRIP = NO; ++ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ++ ENABLE_NS_ASSERTIONS = NO; ++ ENABLE_STRICT_OBJC_MSGSEND = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = YES; ++ GCC_C_LANGUAGE_STANDARD = gnu17; ++ GCC_NO_COMMON_BLOCKS = YES; ++ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; ++ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; ++ GCC_WARN_UNDECLARED_SELECTOR = YES; ++ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; ++ GCC_WARN_UNUSED_FUNCTION = YES; ++ GCC_WARN_UNUSED_VARIABLE = YES; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = NO; ++ MTL_FAST_MATH = YES; ++ SDKROOT = iphoneos; ++ VALIDATE_PRODUCT = YES; ++ }; ++ name = Release; ++ }; ++ 607A66422B0EFA3A0010BFC8 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ++ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = ""; ++ ENABLE_USER_SCRIPT_SANDBOXING = NO; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist"; ++ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; ++ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; ++ INFOPLIST_KEY_UIMainStoryboardFile = Main; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 3.13.0a1; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = YES; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ }; ++ name = Debug; ++ }; ++ 607A66432B0EFA3A0010BFC8 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ++ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = ""; ++ ENABLE_TESTABILITY = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = NO; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist"; ++ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; ++ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; ++ INFOPLIST_KEY_UIMainStoryboardFile = Main; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 3.13.0a1; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = YES; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ }; ++ name = Release; ++ }; ++ 607A66452B0EFA3A0010BFC8 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ BUNDLE_LOADER = "$(TEST_HOST)"; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = 3HEZE76D99; ++ GENERATE_INFOPLIST_FILE = YES; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = NO; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSTestbed"; ++ }; ++ name = Debug; ++ }; ++ 607A66462B0EFA3A0010BFC8 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ BUNDLE_LOADER = "$(TEST_HOST)"; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = 3HEZE76D99; ++ GENERATE_INFOPLIST_FILE = YES; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = NO; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSTestbed"; ++ }; ++ name = Release; ++ }; ++/* End XCBuildConfiguration section */ ++ ++/* Begin XCConfigurationList section */ ++ 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "iOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A663F2B0EFA3A0010BFC8 /* Debug */, ++ 607A66402B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A66422B0EFA3A0010BFC8 /* Debug */, ++ 607A66432B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbedTests" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A66452B0EFA3A0010BFC8 /* Debug */, ++ 607A66462B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++/* End XCConfigurationList section */ ++ }; ++ rootObject = 607A660A2B0EFA380010BFC8 /* Project object */; ++} +--- /dev/null ++++ b/Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme +@@ -0,0 +1,97 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +--- /dev/null ++++ b/Apple/testbed/iOSTestbed.xctestplan +@@ -0,0 +1,46 @@ ++{ ++ "configurations" : [ ++ { ++ "id" : "F5A95CE4-1ADE-4A6E-A0E1-CDBAE26DF0C5", ++ "name" : "Test Scheme Action", ++ "options" : { ++ ++ } ++ } ++ ], ++ "defaultOptions" : { ++ "commandLineArgumentEntries" : [ ++ { ++ "argument" : "test" ++ }, ++ { ++ "argument" : "-uall" ++ }, ++ { ++ "argument" : "--single-process" ++ }, ++ { ++ "argument" : "--rerun" ++ }, ++ { ++ "argument" : "-W" ++ } ++ ], ++ "targetForVariableExpansion" : { ++ "containerPath" : "container:iOSTestbed.xcodeproj", ++ "identifier" : "607A66112B0EFA380010BFC8", ++ "name" : "iOSTestbed" ++ } ++ }, ++ "testTargets" : [ ++ { ++ "parallelizable" : false, ++ "target" : { ++ "containerPath" : "container:iOSTestbed.xcodeproj", ++ "identifier" : "607A662C2B0EFA3A0010BFC8", ++ "name" : "iOSTestbedTests" ++ } ++ } ++ ], ++ "version" : 1 ++} +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/AppDelegate.h +@@ -0,0 +1,11 @@ ++// ++// AppDelegate.h ++// iOSTestbed ++// ++ ++#import ++ ++@interface AppDelegate : UIResponder ++ ++ ++@end +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/AppDelegate.m +@@ -0,0 +1,19 @@ ++// ++// AppDelegate.m ++// iOSTestbed ++// ++ ++#import "AppDelegate.h" ++ ++@interface AppDelegate () ++ ++@end ++ ++@implementation AppDelegate ++ ++ ++- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ++ return YES; ++} ++ ++@end +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json +@@ -0,0 +1,11 @@ ++{ ++ "colors" : [ ++ { ++ "idiom" : "universal" ++ } ++ ], ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json +@@ -0,0 +1,13 @@ ++{ ++ "images" : [ ++ { ++ "idiom" : "universal", ++ "platform" : "ios", ++ "size" : "1024x1024" ++ } ++ ], ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/Assets.xcassets/Contents.json +@@ -0,0 +1,6 @@ ++{ ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard +@@ -0,0 +1,9 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/app/README +@@ -0,0 +1,7 @@ ++This folder can contain any Python application code. ++ ++During the build, any binary modules found in this folder will be processed into ++Framework form. ++ ++When the test suite runs, this folder will be on the PYTHONPATH, and will be the ++working directory for the test suite. +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/app_packages/README +@@ -0,0 +1,7 @@ ++This folder can be a target for installing any Python dependencies needed by the ++test suite. ++ ++During the build, any binary modules found in this folder will be processed into ++Framework form. ++ ++When the test suite runs, this folder will be on the PYTHONPATH. +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/iOSTestbed-Info.plist +@@ -0,0 +1,52 @@ ++ ++ ++ ++ ++ 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 ++ ++ ++ ++ +--- /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); ++ } ++} +--- /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 */; ++} +--- /dev/null ++++ b/Apple/testbed/tvOSTestbed.xcodeproj/xcshareddata/xcschemes/tvOSTestbed.xcscheme +@@ -0,0 +1,97 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +--- /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 +--- /dev/null ++++ b/Apple/testbed/tvOSTestbed/AppDelegate.h +@@ -0,0 +1,11 @@ ++// ++// AppDelegate.h ++// tvOSTestbed ++// ++ ++#import ++ ++@interface AppDelegate : UIResponder ++ ++ ++@end +--- /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 +--- /dev/null ++++ b/Apple/testbed/tvOSTestbed/Base.lproj/LaunchScreen.storyboard +@@ -0,0 +1,24 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +--- /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. +--- /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. +--- /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); ++ } ++} +--- /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 ++ ++ ++ ++ +--- /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 ++====================================== +--- /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@ ++ ++ +--- /dev/null ++++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-ar +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvos${TVOS_SDK_VERSION} ar "$@" +--- /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} "$@" +--- /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} "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 +--- /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 ++====================================== +--- /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@ ++ ++ +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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} "$@" +--- /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} "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 +--- /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 755ff4c6f0f..b03db8f3e0a 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 +--- /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 + =============== + +--- /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) +--- /dev/null ++++ b/Lib/_ios_support.py +@@ -0,0 +1,71 @@ ++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 ++ lib = util.find_library("objc") ++ if lib is None: ++ # Failed to load the objc library ++ raise RuntimeError("ObjC runtime library couldn't be loaded") ++ ++ objc = cdll.LoadLibrary(lib) ++ 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 9b8a8dfc5aa..7a4fad2f746 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 6efeaad8126..e9b0df085d0 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 4c22f131e31..a80b86e4dfe 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 de7ca79dc81..e7058ba5233 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()) +@@ -615,7 +615,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 +--- /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 204a77d14f0..c864d401f9e 100644 +--- a/Lib/test/test_ftplib.py ++++ b/Lib/test/test_ftplib.py +@@ -18,6 +18,7 @@ + + from unittest import 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_httpservers.py b/Lib/test/test_httpservers.py +index 88d06fe04fb..1dc38dba3d3 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) +@@ -698,6 +699,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) + + # 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.") ++ ++ # 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) + + # 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) + + 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" ++ + 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") ++ + 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 ++ + 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) ++ ++ 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) + ++ 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" + + 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") ++ ++# 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 + + 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 669f6a28781..77490b3dd9c 100644 +--- a/Lib/test/test_shutil.py ++++ b/Lib/test/test_shutil.py +@@ -2223,6 +2223,7 @@ + check_chown(dirname, uid, gid) + + ++@support.requires_subprocess() + class TestWhich(BaseTest, unittest.TestCase): + + def setUp(self): +@@ -3318,6 +3319,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) + + # 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(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 f200fc97927..613be5a5117 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 +@@ -1184,8 +1185,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') +@@ -3696,7 +3700,7 @@ + def _testFDPassCMSG_LEN(self): + self.createAndSendFDs(1) + +- @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): +@@ -3707,7 +3711,7 @@ + maxcmsgs=2) + + @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) +@@ -3720,7 +3724,7 @@ + array.array("i", [fd1]))]), + len(MSG)) + +- @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): +@@ -3734,7 +3738,7 @@ + maxcmsgs=2, ignoreflags=socket.MSG_CTRUNC) + + @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) +@@ -3758,7 +3762,7 @@ + nbytes = self.sendmsgToServer([msg]) + self.assertEqual(nbytes, len(msg)) + +- @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) + + @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) + + @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 @@ + + 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) + + @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) + + @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 + +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 + +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 + + +@@ -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) + +- # 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 50d06edd93b..146bb67ce0a 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 +@@ -996,6 +997,7 @@ + + file_obj.close() + ++ @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') + +-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}") ++ + + @requires_subprocess() + def check_output(cmd, encoding=None): +diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py +index 2d695bc8831..4a6586fb1dd 100644 +--- a/Lib/test/test_webbrowser.py ++++ b/Lib/test/test_webbrowser.py +@@ -5,11 +5,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' +@@ -24,6 +27,7 @@ + return 0 + + ++@requires_subprocess() + class CommandTestMixin: + + def _test(self, meth, *, args=[URL], kw={}, options, arguments): +@@ -219,6 +223,73 @@ + arguments=['openURL({},new-tab)'.format(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) ++ ++ @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): +@@ -302,6 +373,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: +@@ -313,6 +388,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 13b9e85f9e1..a6792fa8d56 100755 +--- a/Lib/webbrowser.py ++++ b/Lib/webbrowser.py +@@ -476,6 +476,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) ++ + if sys.platform == "serenityos": + # SerenityOS webbrowser, simply called "Browser". + register("Browser", None, BackgroundBrowser("Browser")) +@@ -656,6 +659,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 ++ ++ 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 ++ + + def main(): + import getopt +--- /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 7ca3dc62c01..a1e28beff42 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@ ++ + # 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@ ++ + # Option to install to strip binaries + STRIPFLAG=-s + +@@ -614,7 +625,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 +@@ -637,6 +648,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 ++ + # Profile generation build must start from a clean tree. + profile-clean-stamp: + $(MAKE) clean +@@ -826,7 +847,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) +@@ -851,14 +872,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); +@@ -870,6 +890,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 ++ + # 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) +@@ -1857,6 +1892,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 ++ ++ # Clone the testbed project into the XCFOLDER ++ $(PYTHON_FOR_BUILD) $(srcdir)/Apple/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)" ++ ++ # Run the testbed project ++ $(PYTHON_FOR_BUILD) "$(XCFOLDER)" run --verbose -- test -uall --single-process --rerun -W ++ + # Like testall, but with only one pass and without multiple processes. + # Run an optional script to include information about the build environment. + .PHONY: buildbottest +@@ -1900,7 +1965,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" ;; \ +@@ -2336,6 +2401,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 \ +@@ -2512,10 +2585,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; \ +@@ -2536,6 +2610,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 ++ + # 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. +@@ -2576,6 +2674,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 +@@ -2686,6 +2797,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: +@@ -2711,6 +2826,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(); + } +- +diff --git a/Python/marshal.c b/Python/marshal.c +index 3fc3f890422..892debe38dc 100644 +--- a/Python/marshal.c ++++ b/Python/marshal.c +@@ -16,6 +16,10 @@ + #include "marshal.h" // Py_MARSHAL_VERSION + #include "pycore_pystate.h" // _PyInterpreterState_GET() + ++#ifdef __APPLE__ ++# include "TargetConditionals.h" ++#endif /* __APPLE__ */ ++ + /*[clinic input] + module marshal + [clinic start generated code]*/ +@@ -35,11 +39,14 @@ + * #if defined(MS_WINDOWS) && defined(_DEBUG) + */ + #if defined(MS_WINDOWS) +-#define MAX_MARSHAL_STACK_DEPTH 1000 ++# define MAX_MARSHAL_STACK_DEPTH 1000 + #elif defined(__wasi__) +-#define MAX_MARSHAL_STACK_DEPTH 1500 ++# define MAX_MARSHAL_STACK_DEPTH 1500 ++// TARGET_OS_IPHONE covers any non-macOS Apple platform. ++#elif defined(__APPLE__) && TARGET_OS_IPHONE ++# define MAX_MARSHAL_STACK_DEPTH 1500 + #else +-#define MAX_MARSHAL_STACK_DEPTH 2000 ++# define MAX_MARSHAL_STACK_DEPTH 2000 + #endif + + #define TYPE_NULL '0' +diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c +index f5bea14b16b..14e44893e53 100644 +--- a/Python/pylifecycle.c ++++ b/Python/pylifecycle.c +@@ -34,7 +34,21 @@ + #include // getenv() + + #if defined(__APPLE__) +-#include ++# include ++# include ++# include ++// The os_log unified logging APIs were introduced in macOS 10.12, iOS 10.0, ++// tvOS 10.0, and watchOS 3.0; we enable the use of the system logger ++// automatically on non-macOS platforms. ++# if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE ++# define USE_APPLE_SYSTEM_LOG 1 ++# else ++# define USE_APPLE_SYSTEM_LOG 0 ++# endif ++ ++# if USE_APPLE_SYSTEM_LOG ++# include ++# endif + #endif + + #ifdef HAVE_SIGNAL_H +@@ -66,6 +80,9 @@ + static PyStatus init_import_site(void); + static PyStatus init_set_builtins_open(void); + static PyStatus init_sys_streams(PyThreadState *tstate); ++#if defined(__APPLE__) && USE_APPLE_SYSTEM_LOG ++static PyStatus init_apple_streams(PyThreadState *tstate); ++#endif + static void wait_for_thread_shutdown(PyThreadState *tstate); + static void call_ll_exitfuncs(_PyRuntimeState *runtime); + +@@ -1182,6 +1199,17 @@ + return status; + } + ++#if defined(__APPLE__) && USE_APPLE_SYSTEM_LOG ++ status = init_apple_streams(tstate); ++ if (_PyStatus_EXCEPTION(status)) { ++ return status; ++ } ++#endif ++ ++#ifdef Py_DEBUG ++ run_presite(tstate); ++#endif ++ + status = add_main_module(interp); + if (_PyStatus_EXCEPTION(status)) { + return status; +@@ -2641,6 +2669,69 @@ + return res; + } + ++#if defined(__APPLE__) && USE_APPLE_SYSTEM_LOG ++ ++static PyObject * ++apple_log_write_impl(PyObject *self, PyObject *args) ++{ ++ int logtype = 0; ++ const char *text = NULL; ++ if (!PyArg_ParseTuple(args, "iy", &logtype, &text)) { ++ return NULL; ++ } ++ ++ // Pass the user-provided text through explicit %s formatting ++ // to avoid % literals being interpreted as a formatting directive. ++ os_log_with_type(OS_LOG_DEFAULT, logtype, "%s", text); ++ Py_RETURN_NONE; ++} ++ ++ ++static PyMethodDef apple_log_write_method = { ++ "apple_log_write", apple_log_write_impl, METH_VARARGS ++}; ++ ++ ++static PyStatus ++init_apple_streams(PyThreadState *tstate) ++{ ++ PyStatus status = _PyStatus_OK(); ++ PyObject *_apple_support = NULL; ++ PyObject *apple_log_write = NULL; ++ PyObject *result = NULL; ++ ++ _apple_support = PyImport_ImportModule("_apple_support"); ++ if (_apple_support == NULL) { ++ goto error; ++ } ++ ++ apple_log_write = PyCFunction_New(&apple_log_write_method, NULL); ++ if (apple_log_write == NULL) { ++ goto error; ++ } ++ ++ // Initialize the logging streams, sending stdout -> Default; stderr -> Error ++ result = PyObject_CallMethod( ++ _apple_support, "init_streams", "Oii", ++ apple_log_write, OS_LOG_TYPE_DEFAULT, OS_LOG_TYPE_ERROR); ++ if (result == NULL) { ++ goto error; ++ } ++ goto done; ++ ++error: ++ _PyErr_Print(tstate); ++ status = _PyStatus_ERR("failed to initialize Apple log streams"); ++ ++done: ++ Py_XDECREF(result); ++ Py_XDECREF(apple_log_write); ++ Py_XDECREF(_apple_support); ++ return status; ++} ++ ++#endif // __APPLE__ && USE_APPLE_SYSTEM_LOG ++ + + static void + _Py_FatalError_DumpTracebacks(int fd, PyInterpreterState *interp, +diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h +index 1b1a1bdee78..a37f531984e 100644 +--- a/Python/stdlib_module_names.h ++++ b/Python/stdlib_module_names.h +@@ -5,6 +5,7 @@ + "__future__", + "_abc", + "_aix_support", ++"_apple_support", + "_ast", + "_asyncio", + "_bisect", +@@ -39,6 +40,7 @@ + "_heapq", + "_imp", + "_io", ++"_ios_support", + "_json", + "_locale", + "_lsprof", +diff --git a/config.sub b/config.sub +index d74fb6deac9..1bb6a05dc11 100755 +--- a/config.sub ++++ b/config.sub +@@ -1,14 +1,15 @@ + #! /bin/sh + # Configuration validation subroutine script. +-# Copyright 1992-2021 Free Software Foundation, Inc. ++# Copyright 1992-2024 Free Software Foundation, Inc. + + # shellcheck disable=SC2006,SC2268 # see below for rationale + +-timestamp='2021-08-14' ++# Patched 2024-02-03 to include support for arm64_32 and iOS/tvOS/watchOS simulators ++timestamp='2024-01-01' + + # This file is free software; you can redistribute it and/or modify it + # under the terms of the GNU General Public License as published by +-# the Free Software Foundation; either version 3 of the License, or ++# the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, but +@@ -76,13 +77,13 @@ + version="\ + GNU config.sub ($timestamp) + +-Copyright 1992-2021 Free Software Foundation, Inc. ++Copyright 1992-2024 Free Software Foundation, Inc. + + This is free software; see the source for copying conditions. There is NO + warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." + + help=" +-Try \`$me --help' for more information." ++Try '$me --help' for more information." + + # Parse command line + while test $# -gt 0 ; do +@@ -130,7 +131,7 @@ + # Separate into logical components for further validation + case $1 in + *-*-*-*-*) +- echo Invalid configuration \`"$1"\': more than four components >&2 ++ echo "Invalid configuration '$1': more than four components" >&2 + exit 1 + ;; + *-*-*-*) +@@ -145,7 +146,8 @@ + nto-qnx* | linux-* | uclinux-uclibc* \ + | uclinux-gnu* | kfreebsd*-gnu* | knetbsd*-gnu* | netbsd*-gnu* \ + | netbsd*-eabi* | kopensolaris*-gnu* | cloudabi*-eabi* \ +- | storm-chaos* | os2-emx* | rtmk-nova*) ++ | storm-chaos* | os2-emx* | rtmk-nova* | managarm-* \ ++ | windows-* ) + basic_machine=$field1 + basic_os=$maybe_os + ;; +@@ -943,7 +945,7 @@ + EOF + IFS=$saved_IFS + ;; +- # We use `pc' rather than `unknown' ++ # We use 'pc' rather than 'unknown' + # because (1) that's what they normally are, and + # (2) the word "unknown" tends to confuse beginning users. + i*86 | x86_64) +@@ -1020,6 +1022,11 @@ + ;; + + # Here we normalize CPU types with a missing or matching vendor ++ armh-unknown | armh-alt) ++ cpu=armv7l ++ vendor=alt ++ basic_os=${basic_os:-linux-gnueabihf} ++ ;; + dpx20-unknown | dpx20-bull) + cpu=rs6000 + vendor=bull +@@ -1070,7 +1077,7 @@ + pentium-* | p5-* | k5-* | k6-* | nexgen-* | viac3-*) + cpu=i586 + ;; +- pentiumpro-* | p6-* | 6x86-* | athlon-* | athalon_*-*) ++ pentiumpro-* | p6-* | 6x86-* | athlon-* | athlon_*-*) + cpu=i686 + ;; + pentiumii-* | pentium2-* | pentiumiii-* | pentium3-*) +@@ -1121,7 +1128,7 @@ + xscale-* | xscalee[bl]-*) + cpu=`echo "$cpu" | sed 's/^xscale/arm/'` + ;; +- arm64-*) ++ arm64-* | aarch64le-* | arm64_32-*) + cpu=aarch64 + ;; + +@@ -1175,7 +1182,7 @@ + case $cpu in + 1750a | 580 \ + | a29k \ +- | aarch64 | aarch64_be \ ++ | aarch64 | aarch64_be | aarch64c | arm64ec \ + | abacus \ + | alpha | alphaev[4-8] | alphaev56 | alphaev6[78] \ + | alpha64 | alpha64ev[4-8] | alpha64ev56 | alpha64ev6[78] \ +@@ -1194,50 +1201,29 @@ + | d10v | d30v | dlx | dsp16xx \ + | e2k | elxsi | epiphany \ + | f30[01] | f700 | fido | fr30 | frv | ft32 | fx80 \ ++ | javascript \ + | h8300 | h8500 \ + | hppa | hppa1.[01] | hppa2.0 | hppa2.0[nw] | hppa64 \ + | hexagon \ + | i370 | i*86 | i860 | i960 | ia16 | ia64 \ + | ip2k | iq2000 \ + | k1om \ ++ | kvx \ + | le32 | le64 \ + | lm32 \ +- | loongarch32 | loongarch64 | loongarchx32 \ ++ | loongarch32 | loongarch64 \ + | m32c | m32r | m32rle \ + | m5200 | m68000 | m680[012346]0 | m68360 | m683?2 | m68k \ + | m6811 | m68hc11 | m6812 | m68hc12 | m68hcs12x \ + | m88110 | m88k | maxq | mb | mcore | mep | metag \ + | microblaze | microblazeel \ +- | mips | mipsbe | mipseb | mipsel | mipsle \ +- | mips16 \ +- | mips64 | mips64eb | mips64el \ +- | mips64octeon | mips64octeonel \ +- | mips64orion | mips64orionel \ +- | mips64r5900 | mips64r5900el \ +- | mips64vr | mips64vrel \ +- | mips64vr4100 | mips64vr4100el \ +- | mips64vr4300 | mips64vr4300el \ +- | mips64vr5000 | mips64vr5000el \ +- | mips64vr5900 | mips64vr5900el \ +- | mipsisa32 | mipsisa32el \ +- | mipsisa32r2 | mipsisa32r2el \ +- | mipsisa32r3 | mipsisa32r3el \ +- | mipsisa32r5 | mipsisa32r5el \ +- | mipsisa32r6 | mipsisa32r6el \ +- | mipsisa64 | mipsisa64el \ +- | mipsisa64r2 | mipsisa64r2el \ +- | mipsisa64r3 | mipsisa64r3el \ +- | mipsisa64r5 | mipsisa64r5el \ +- | mipsisa64r6 | mipsisa64r6el \ +- | mipsisa64sb1 | mipsisa64sb1el \ +- | mipsisa64sr71k | mipsisa64sr71kel \ +- | mipsr5900 | mipsr5900el \ +- | mipstx39 | mipstx39el \ ++ | mips* \ + | mmix \ + | mn10200 | mn10300 \ + | moxie \ + | mt \ + | msp430 \ ++ | nanomips* \ + | nds32 | nds32le | nds32be \ + | nfp \ + | nios | nios2 | nios2eb | nios2el \ +@@ -1269,6 +1255,7 @@ + | ubicom32 \ + | v70 | v850 | v850e | v850e1 | v850es | v850e2 | v850e2v3 \ + | vax \ ++ | vc4 \ + | visium \ + | w65 \ + | wasm32 | wasm64 \ +@@ -1280,7 +1267,7 @@ + ;; + + *) +- echo Invalid configuration \`"$1"\': machine \`"$cpu-$vendor"\' not recognized 1>&2 ++ echo "Invalid configuration '$1': machine '$cpu-$vendor' not recognized" 1>&2 + exit 1 + ;; + esac +@@ -1301,11 +1288,12 @@ + + # Decode manufacturer-specific aliases for certain operating systems. + +-if test x$basic_os != x ++if test x"$basic_os" != x + then + +-# First recognize some ad-hoc caes, or perhaps split kernel-os, or else just ++# First recognize some ad-hoc cases, or perhaps split kernel-os, or else just + # set os. ++obj= + case $basic_os in + gnu/linux*) + kernel=linux +@@ -1336,6 +1324,10 @@ + kernel=linux + os=`echo "$basic_os" | sed -e 's|linux|gnu|'` + ;; ++ managarm*) ++ kernel=managarm ++ os=`echo "$basic_os" | sed -e 's|managarm|mlibc|'` ++ ;; + *) + kernel= + os=$basic_os +@@ -1501,10 +1493,16 @@ + os=eabi + ;; + *) +- os=elf ++ os= ++ obj=elf + ;; + esac + ;; ++ aout* | coff* | elf* | pe*) ++ # These are machine code file formats, not OSes ++ obj=$os ++ os= ++ ;; + *) + # No normalization, but not necessarily accepted, that comes below. + ;; +@@ -1523,12 +1521,15 @@ + # system, and we'll never get to this point. + + kernel= ++obj= + case $cpu-$vendor in + score-*) +- os=elf ++ os= ++ obj=elf + ;; + spu-*) +- os=elf ++ os= ++ obj=elf + ;; + *-acorn) + os=riscix1.2 +@@ -1538,28 +1539,35 @@ + os=gnu + ;; + arm*-semi) +- os=aout ++ os= ++ obj=aout + ;; + c4x-* | tic4x-*) +- os=coff ++ os= ++ obj=coff + ;; + c8051-*) +- os=elf ++ os= ++ obj=elf + ;; + clipper-intergraph) + os=clix + ;; + hexagon-*) +- os=elf ++ os= ++ obj=elf + ;; + tic54x-*) +- os=coff ++ os= ++ obj=coff + ;; + tic55x-*) +- os=coff ++ os= ++ obj=coff + ;; + tic6x-*) +- os=coff ++ os= ++ obj=coff + ;; + # This must come before the *-dec entry. + pdp10-*) +@@ -1581,19 +1589,24 @@ + os=sunos3 + ;; + m68*-cisco) +- os=aout ++ os= ++ obj=aout + ;; + mep-*) +- os=elf ++ os= ++ obj=elf + ;; + mips*-cisco) +- os=elf ++ os= ++ obj=elf + ;; +- mips*-*) +- os=elf ++ mips*-*|nanomips*-*) ++ os= ++ obj=elf + ;; + or32-*) +- os=coff ++ os= ++ obj=coff + ;; + *-tti) # must be before sparc entry or we get the wrong os. + os=sysv3 +@@ -1602,7 +1615,8 @@ + os=sunos4.1.1 + ;; + pru-*) +- os=elf ++ os= ++ obj=elf + ;; + *-be) + os=beos +@@ -1683,10 +1697,12 @@ + os=uxpv + ;; + *-rom68k) +- os=coff ++ os= ++ obj=coff + ;; + *-*bug) +- os=coff ++ os= ++ obj=coff + ;; + *-apple) + os=macos +@@ -1704,10 +1720,11 @@ + + fi + +-# Now, validate our (potentially fixed-up) OS. ++# Now, validate our (potentially fixed-up) individual pieces (OS, OBJ). ++ + case $os in + # Sometimes we do "kernel-libc", so those need to count as OSes. +- musl* | newlib* | relibc* | uclibc*) ++ llvm* | musl* | newlib* | relibc* | uclibc*) + ;; + # Likewise for "kernel-abi" + eabi* | gnueabi*) +@@ -1715,6 +1732,9 @@ + # VxWorks passes extra cpu info in the 4th filed. + simlinux | simwindows | spe) + ;; ++ # See `case $cpu-$os` validation below ++ ghcjs) ++ ;; + # Now accept the basic system types. + # The portable systems comes first. + # Each alternative MUST end in a * to match a version number. +@@ -1723,7 +1743,7 @@ + | hpux* | unos* | osf* | luna* | dgux* | auroraux* | solaris* \ + | sym* | plan9* | psp* | sim* | xray* | os68k* | v88r* \ + | hiux* | abug | nacl* | netware* | windows* \ +- | os9* | macos* | osx* | ios* \ ++ | os9* | macos* | osx* | ios* | tvos* | watchos* \ + | mpw* | magic* | mmixware* | mon960* | lnews* \ + | amigaos* | amigados* | msdos* | newsos* | unicos* | aof* \ + | aos* | aros* | cloudabi* | sortix* | twizzler* \ +@@ -1732,11 +1752,11 @@ + | mirbsd* | netbsd* | dicos* | openedition* | ose* \ + | bitrig* | openbsd* | secbsd* | solidbsd* | libertybsd* | os108* \ + | ekkobsd* | freebsd* | riscix* | lynxos* | os400* \ +- | bosx* | nextstep* | cxux* | aout* | elf* | oabi* \ +- | ptx* | coff* | ecoff* | winnt* | domain* | vsta* \ ++ | bosx* | nextstep* | cxux* | oabi* \ ++ | ptx* | ecoff* | winnt* | domain* | vsta* \ + | udi* | lites* | ieee* | go32* | aux* | hcos* \ + | chorusrdb* | cegcc* | glidix* | serenity* \ +- | cygwin* | msys* | pe* | moss* | proelf* | rtems* \ ++ | cygwin* | msys* | moss* | proelf* | rtems* \ + | midipix* | mingw32* | mingw64* | mint* \ + | uxpv* | beos* | mpeix* | udk* | moxiebox* \ + | interix* | uwin* | mks* | rhapsody* | darwin* \ +@@ -1748,49 +1768,119 @@ + | skyos* | haiku* | rdos* | toppers* | drops* | es* \ + | onefs* | tirtos* | phoenix* | fuchsia* | redox* | bme* \ + | midnightbsd* | amdhsa* | unleashed* | emscripten* | wasi* \ +- | nsk* | powerunix* | genode* | zvmoe* | qnx* | emx* | zephyr*) ++ | nsk* | powerunix* | genode* | zvmoe* | qnx* | emx* | zephyr* \ ++ | fiwix* | mlibc* | cos* | mbr* | ironclad* ) + ;; + # This one is extra strict with allowed versions + sco3.2v2 | sco3.2v[4-9]* | sco5v6*) + # Don't forget version if it is 3.2v4 or newer. + ;; ++ # This refers to builds using the UEFI calling convention ++ # (which depends on the architecture) and PE file format. ++ # Note that this is both a different calling convention and ++ # different file format than that of GNU-EFI ++ # (x86_64-w64-mingw32). ++ uefi) ++ ;; + none) + ;; ++ kernel* | msvc* ) ++ # Restricted further below ++ ;; ++ '') ++ if test x"$obj" = x ++ then ++ echo "Invalid configuration '$1': Blank OS only allowed with explicit machine code file format" 1>&2 ++ fi ++ ;; + *) +- echo Invalid configuration \`"$1"\': OS \`"$os"\' not recognized 1>&2 ++ echo "Invalid configuration '$1': OS '$os' not recognized" 1>&2 ++ exit 1 ++ ;; ++esac ++ ++case $obj in ++ aout* | coff* | elf* | pe*) ++ ;; ++ '') ++ # empty is fine ++ ;; ++ *) ++ echo "Invalid configuration '$1': Machine code format '$obj' not recognized" 1>&2 ++ exit 1 ++ ;; ++esac ++ ++# Here we handle the constraint that a (synthetic) cpu and os are ++# valid only in combination with each other and nowhere else. ++case $cpu-$os in ++ # The "javascript-unknown-ghcjs" triple is used by GHC; we ++ # accept it here in order to tolerate that, but reject any ++ # variations. ++ javascript-ghcjs) ++ ;; ++ javascript-* | *-ghcjs) ++ echo "Invalid configuration '$1': cpu '$cpu' is not valid with os '$os$obj'" 1>&2 + exit 1 + ;; + esac + + # As a final step for OS-related things, validate the OS-kernel combination + # (given a valid OS), if there is a kernel. +-case $kernel-$os in +- linux-gnu* | linux-dietlibc* | linux-android* | linux-newlib* \ +- | linux-musl* | linux-relibc* | linux-uclibc* ) ++case $kernel-$os-$obj in ++ linux-gnu*- | linux-android*- | linux-dietlibc*- | linux-llvm*- \ ++ | linux-mlibc*- | linux-musl*- | linux-newlib*- \ ++ | linux-relibc*- | linux-uclibc*- ) ++ ;; ++ uclinux-uclibc*- ) + ;; +- uclinux-uclibc* ) ++ managarm-mlibc*- | managarm-kernel*- ) + ;; +- -dietlibc* | -newlib* | -musl* | -relibc* | -uclibc* ) ++ windows*-msvc*-) ++ ;; ++ -dietlibc*- | -llvm*- | -mlibc*- | -musl*- | -newlib*- | -relibc*- \ ++ | -uclibc*- ) + # These are just libc implementations, not actual OSes, and thus + # require a kernel. +- echo "Invalid configuration \`$1': libc \`$os' needs explicit kernel." 1>&2 ++ echo "Invalid configuration '$1': libc '$os' needs explicit kernel." 1>&2 + exit 1 + ;; +- kfreebsd*-gnu* | kopensolaris*-gnu*) ++ -kernel*- ) ++ echo "Invalid configuration '$1': '$os' needs explicit kernel." 1>&2 ++ exit 1 + ;; +- vxworks-simlinux | vxworks-simwindows | vxworks-spe) ++ *-kernel*- ) ++ echo "Invalid configuration '$1': '$kernel' does not support '$os'." 1>&2 ++ exit 1 + ;; +- nto-qnx*) ++ *-msvc*- ) ++ echo "Invalid configuration '$1': '$os' needs 'windows'." 1>&2 ++ exit 1 + ;; +- os2-emx) ++ kfreebsd*-gnu*- | kopensolaris*-gnu*-) + ;; +- *-eabi* | *-gnueabi*) ++ vxworks-simlinux- | vxworks-simwindows- | vxworks-spe-) + ;; +- -*) ++ nto-qnx*-) ++ ;; ++ os2-emx-) ++ ;; ++ *-eabi*- | *-gnueabi*-) ++ ;; ++ ios*-simulator- | tvos*-simulator- | watchos*-simulator- ) ++ ;; ++ none--*) ++ # None (no kernel, i.e. freestanding / bare metal), ++ # can be paired with an machine code file format ++ ;; ++ -*-) + # Blank kernel with real OS is always fine. + ;; +- *-*) +- echo "Invalid configuration \`$1': Kernel \`$kernel' not known to work with OS \`$os'." 1>&2 ++ --*) ++ # Blank kernel and OS with real machine code file format is always fine. ++ ;; ++ *-*-*) ++ echo "Invalid configuration '$1': Kernel '$kernel' not known to work with OS '$os'." 1>&2 + exit 1 + ;; + esac +@@ -1873,7 +1963,7 @@ + ;; + esac + +-echo "$cpu-$vendor-${kernel:+$kernel-}$os" ++echo "$cpu-$vendor${kernel:+-$kernel}${os:+-$os}${obj:+-$obj}" + exit + + # Local variables: +diff --git a/configure b/configure +index 89edc42f45c..707a5f011da 100755 +--- a/configure ++++ b/configure +@@ -976,10 +976,14 @@ + CFLAGS + CC + HAS_XCRUN ++WATCHOS_DEPLOYMENT_TARGET ++TVOS_DEPLOYMENT_TARGET ++IPHONEOS_DEPLOYMENT_TARGET + EXPORT_MACOSX_DEPLOYMENT_TARGET + CONFIGURE_MACOSX_DEPLOYMENT_TARGET + _PYTHON_HOST_PLATFORM +-MACHDEP ++APP_STORE_COMPLIANCE_PATCH ++INSTALLTARGETS + FRAMEWORKINSTALLAPPSPREFIX + FRAMEWORKUNIXTOOLSPREFIX + FRAMEWORKPYTHONW +@@ -987,6 +991,8 @@ + FRAMEWORKALTINSTALLFIRST + FRAMEWORKINSTALLLAST + FRAMEWORKINSTALLFIRST ++RESSRCDIR ++PYTHONFRAMEWORKINSTALLNAMEPREFIX + PYTHONFRAMEWORKINSTALLDIR + PYTHONFRAMEWORKPREFIX + PYTHONFRAMEWORKDIR +@@ -996,6 +1002,7 @@ + LIPO_32BIT_FLAGS + ARCH_RUN_32BIT + UNIVERSALSDK ++MACHDEP + PKG_CONFIG_LIBDIR + PKG_CONFIG_PATH + PKG_CONFIG +@@ -1071,6 +1078,7 @@ + with_universal_archs + with_framework_name + enable_framework ++with_app_store_compliance + with_emscripten_target + enable_wasm_dynamic_linking + enable_wasm_pthreads +@@ -1845,6 +1853,10 @@ + specify the name for the python framework on macOS + only valid when --enable-framework is set. see + Mac/README.rst (default is 'Python') ++ --with-app-store-compliance=[PATCH-FILE] ++ Enable any patches required for compiliance with app ++ stores. Optional PATCH-FILE specifies the custom ++ patch to apply. + --with-emscripten-target=[browser|node] + Emscripten platform + --with-suffix=SUFFIX set executable suffix to SUFFIX (default is empty, +@@ -4015,6 +4027,164 @@ + as_fn_error $? "pkg-config is required" "$LINENO" 5] + fi + ++# Set name for machine-dependent library files ++ ++{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking MACHDEP" >&5 ++printf %s "checking MACHDEP... " >&6; } ++if test -z "$MACHDEP" ++then ++ # avoid using uname for cross builds ++ if test "$cross_compiling" = yes; then ++ # ac_sys_system and ac_sys_release are used for setting ++ # a lot of different things including 'define_xopen_source' ++ # in the case statement below. ++ case "$host" in ++ *-*-linux-android*) ++ ac_sys_system=Linux-android ++ ;; ++ *-*-linux*) ++ ac_sys_system=Linux ++ ;; ++ *-*-cygwin*) ++ ac_sys_system=Cygwin ++ ;; ++ *-apple-ios*) ++ ac_sys_system=iOS ++ ;; ++ *-apple-tvos*) ++ ac_sys_system=tvOS ++ ;; ++ *-apple-watchos*) ++ ac_sys_system=watchOS ++ ;; ++ *-*-vxworks*) ++ ac_sys_system=VxWorks ++ ;; ++ *-*-emscripten) ++ ac_sys_system=Emscripten ++ ;; ++ *-*-wasi) ++ ac_sys_system=WASI ++ ;; ++ *) ++ # for now, limit cross builds to known configurations ++ MACHDEP="unknown" ++ as_fn_error $? "cross build not supported for $host" "$LINENO" 5 ++ esac ++ ac_sys_release= ++ else ++ ac_sys_system=`uname -s` ++ if test "$ac_sys_system" = "AIX" \ ++ -o "$ac_sys_system" = "UnixWare" -o "$ac_sys_system" = "OpenUNIX"; then ++ ac_sys_release=`uname -v` ++ else ++ ac_sys_release=`uname -r` ++ fi ++ fi ++ ac_md_system=`echo $ac_sys_system | ++ tr -d '/ ' | tr '[A-Z]' '[a-z]'` ++ ac_md_release=`echo $ac_sys_release | ++ tr -d '/ ' | sed 's/^[A-Z]\.//' | sed 's/\..*//'` ++ MACHDEP="$ac_md_system$ac_md_release" ++ ++ case $MACHDEP in ++ aix*) MACHDEP="aix";; ++ linux*) MACHDEP="linux";; ++ cygwin*) MACHDEP="cygwin";; ++ darwin*) MACHDEP="darwin";; ++ '') MACHDEP="unknown";; ++ esac ++ ++ if test "$ac_sys_system" = "SunOS"; then ++ # For Solaris, there isn't an OS version specific macro defined ++ # in most compilers, so we define one here. ++ SUNOS_VERSION=`echo $ac_sys_release | sed -e 's!\.\(0-9\)$!.0\1!g' | tr -d '.'` ++ ++printf "%s\n" "#define Py_SUNOS_VERSION $SUNOS_VERSION" >>confdefs.h ++ ++ fi ++fi ++{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: \"$MACHDEP\"" >&5 ++printf "%s\n" "\"$MACHDEP\"" >&6; } ++ ++# On cross-compile builds, configure will look for a host-specific compiler by ++# prepending the user-provided host triple to the required binary name. ++# ++# On iOS/tvOS/watchOS, this results in binaries like "arm64-apple-ios13.0-simulator-gcc", ++# which isn't a binary that exists, and isn't very convenient, as it contains the ++# iOS version. As the default cross-compiler name won't exist, configure falls ++# back to gcc, which *definitely* won't work. We're providing wrapper scripts for ++# these tools; the binary names of these scripts are better defaults than "gcc". ++# This only requires that the user put the platform scripts folder (e.g., ++# "iOS/Resources/bin") in their path, rather than defining platform-specific ++# names/paths for AR, CC, CPP, and CXX explicitly; and if the user forgets to ++# either put the platform scripts folder in the path, or specify CC etc, ++# configure will fail. ++if test -z "$AR"; then ++ case "$host" in ++ aarch64-apple-ios*-simulator) AR=arm64-apple-ios-simulator-ar ;; ++ aarch64-apple-ios*) AR=arm64-apple-ios-ar ;; ++ x86_64-apple-ios*-simulator) AR=x86_64-apple-ios-simulator-ar ;; ++ ++ aarch64-apple-tvos*-simulator) AR=arm64-apple-tvos-simulator-ar ;; ++ aarch64-apple-tvos*) AR=arm64-apple-tvos-ar ;; ++ x86_64-apple-tvos*-simulator) AR=x86_64-apple-tvos-simulator-ar ;; ++ ++ aarch64-apple-watchos*-simulator) AR=arm64-apple-watchos-simulator-ar ;; ++ aarch64-apple-watchos*) AR=arm64_32-apple-watchos-ar ;; ++ x86_64-apple-watchos*-simulator) AR=x86_64-apple-watchos-simulator-ar ;; ++ *) ++ esac ++fi ++if test -z "$CC"; then ++ case "$host" in ++ aarch64-apple-ios*-simulator) CC=arm64-apple-ios-simulator-clang ;; ++ aarch64-apple-ios*) CC=arm64-apple-ios-clang ;; ++ x86_64-apple-ios*-simulator) CC=x86_64-apple-ios-simulator-clang ;; ++ ++ aarch64-apple-tvos*-simulator) CC=arm64-apple-tvos-simulator-clang ;; ++ aarch64-apple-tvos*) CC=arm64-apple-tvos-clang ;; ++ x86_64-apple-tvos*-simulator) CC=x86_64-apple-tvos-simulator-clang ;; ++ ++ aarch64-apple-watchos*-simulator) CC=arm64-apple-watchos-simulator-clang ;; ++ aarch64-apple-watchos*) CC=arm64_32-apple-watchos-clang ;; ++ x86_64-apple-watchos*-simulator) CC=x86_64-apple-watchos-simulator-clang ;; ++ *) ++ esac ++fi ++if test -z "$CPP"; then ++ case "$host" in ++ aarch64-apple-ios*-simulator) CPP=arm64-apple-ios-simulator-cpp ;; ++ aarch64-apple-ios*) CPP=arm64-apple-ios-cpp ;; ++ x86_64-apple-ios*-simulator) CPP=x86_64-apple-ios-simulator-cpp ;; ++ ++ aarch64-apple-tvos*-simulator) CPP=arm64-apple-tvos-simulator-cpp ;; ++ aarch64-apple-tvos*) CPP=arm64-apple-tvos-cpp ;; ++ x86_64-apple-tvos*-simulator) CPP=x86_64-apple-tvos-simulator-cpp ;; ++ ++ aarch64-apple-watchos*-simulator) CPP=arm64-apple-watchos-simulator-cpp ;; ++ aarch64-apple-watchos*) CPP=arm64_32-apple-watchos-cpp ;; ++ x86_64-apple-watchos*-simulator) CPP=x86_64-apple-watchos-simulator-cpp ;; ++ *) ++ esac ++fi ++if test -z "$CXX"; then ++ case "$host" in ++ aarch64-apple-ios*-simulator) CXX=arm64-apple-ios-simulator-clang++ ;; ++ aarch64-apple-ios*) CXX=arm64-apple-ios-clang++ ;; ++ x86_64-apple-ios*-simulator) CXX=x86_64-apple-ios-simulator-clang++ ;; ++ ++ aarch64-apple-tvos*-simulator) CXX=arm64-apple-tvos-simulator-clang++ ;; ++ aarch64-apple-tvos*) CXX=arm64-apple-tvos-clang++ ;; ++ x86_64-apple-tvos*-simulator) CXX=x86_64-apple-tvos-simulator-clang++ ;; ++ ++ aarch64-apple-watchos*-simulator) CXX=arm64-apple-watchos-simulator-clang++ ;; ++ aarch64-apple-watchos*) CXX=arm64_32-apple-watchos-clang++ ;; ++ x86_64-apple-watchos*-simulator) CXX=x86_64-apple-watchos-simulator-clang++ ;; ++ *) ++ esac ++fi ++ + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for --enable-universalsdk" >&5 + printf %s "checking for --enable-universalsdk... " >&6; } + # Check whether --enable-universalsdk was given. +@@ -4130,111 +4300,195 @@ + enableval=$enable_framework; + case $enableval in + yes) +- 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\) ;; ++ *) as_fn_error $? "Unknown platform for framework build" "$LINENO" 5 ++ esac + esac ++ + case $enableval in + no) +- PYTHONFRAMEWORK= +- PYTHONFRAMEWORKDIR=no-framework +- PYTHONFRAMEWORKPREFIX= +- PYTHONFRAMEWORKINSTALLDIR= +- FRAMEWORKINSTALLFIRST= +- FRAMEWORKINSTALLLAST= +- FRAMEWORKALTINSTALLFIRST= +- FRAMEWORKALTINSTALLLAST= +- FRAMEWORKPYTHONW= +- if test "x${prefix}" = "xNONE"; then +- FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" +- else +- FRAMEWORKUNIXTOOLSPREFIX="${prefix}" +- fi +- enable_framework= ++ case $ac_sys_system in ++ iOS) as_fn_error $? "iOS builds must use --enable-framework" "$LINENO" 5 ;; ++ tvOS) as_fn_error $? "tvOS builds must use --enable-framework" "$LINENO" 5 ;; ++ watchOS) as_fn_error $? "watchOS builds must use --enable-framework" "$LINENO" 5 ;; ++ *) ++ PYTHONFRAMEWORK= ++ PYTHONFRAMEWORKDIR=no-framework ++ PYTHONFRAMEWORKPREFIX= ++ PYTHONFRAMEWORKINSTALLDIR= ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX= ++ RESSRCDIR= ++ FRAMEWORKINSTALLFIRST= ++ FRAMEWORKINSTALLLAST= ++ FRAMEWORKALTINSTALLFIRST= ++ FRAMEWORKALTINSTALLLAST= ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="commoninstall bininstall maninstall" ++ ++ if test "x${prefix}" = "xNONE"; then ++ FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" ++ else ++ FRAMEWORKUNIXTOOLSPREFIX="${prefix}" ++ fi ++ enable_framework= ++ esac + ;; + *) + PYTHONFRAMEWORKPREFIX="${enableval}" + PYTHONFRAMEWORKINSTALLDIR=$PYTHONFRAMEWORKPREFIX/$PYTHONFRAMEWORKDIR +- FRAMEWORKINSTALLFIRST="frameworkinstallstructure" +- FRAMEWORKALTINSTALLFIRST="frameworkinstallstructure " +- FRAMEWORKINSTALLLAST="frameworkinstallmaclib frameworkinstallapps frameworkinstallunixtools" +- FRAMEWORKALTINSTALLLAST="frameworkinstallmaclib frameworkinstallapps frameworkaltinstallunixtools" +- FRAMEWORKPYTHONW="frameworkpythonw" +- FRAMEWORKINSTALLAPPSPREFIX="/Applications" +- +- 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" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmaclib frameworkinstallapps frameworkinstallunixtools" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmaclib frameworkinstallapps frameworkaltinstallunixtools" ++ FRAMEWORKPYTHONW="frameworkpythonw" ++ FRAMEWORKINSTALLAPPSPREFIX="/Applications" ++ INSTALLTARGETS="commoninstall bininstall maninstall" ++ ++ if test "x${prefix}" = "xNONE" ; then ++ FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" ++ ++ else ++ FRAMEWORKUNIXTOOLSPREFIX="${prefix}" ++ fi + +- /Library*) +- FRAMEWORKINSTALLAPPSPREFIX="/Applications" +- ;; ++ case "${enableval}" in ++ /System*) ++ FRAMEWORKINSTALLAPPSPREFIX="/Applications" ++ if test "${prefix}" = "NONE" ; then ++ # See below ++ FRAMEWORKUNIXTOOLSPREFIX="/usr" ++ fi ++ ;; ++ ++ /Library*) ++ FRAMEWORKINSTALLAPPSPREFIX="/Applications" ++ ;; ++ ++ */Library/Frameworks) ++ MDIR="`dirname "${enableval}"`" ++ MDIR="`dirname "${MDIR}"`" ++ FRAMEWORKINSTALLAPPSPREFIX="${MDIR}/Applications" ++ ++ if test "${prefix}" = "NONE"; then ++ # User hasn't specified the ++ # --prefix option, but wants to install ++ # the framework in a non-default location, ++ # ensure that the compatibility links get ++ # installed relative to that prefix as well ++ # instead of in /usr/local. ++ FRAMEWORKUNIXTOOLSPREFIX="${MDIR}" ++ fi ++ ;; + +- */Library/Frameworks) +- MDIR="`dirname "${enableval}"`" +- MDIR="`dirname "${MDIR}"`" +- FRAMEWORKINSTALLAPPSPREFIX="${MDIR}/Applications" +- +- if test "${prefix}" = "NONE"; then +- # User hasn't specified the +- # --prefix option, but wants to install +- # the framework in a non-default location, +- # ensure that the compatibility links get +- # installed relative to that prefix as well +- # instead of in /usr/local. +- FRAMEWORKUNIXTOOLSPREFIX="${MDIR}" +- fi +- ;; ++ *) ++ FRAMEWORKINSTALLAPPSPREFIX="/Applications" ++ ;; ++ esac + +- *) +- FRAMEWORKINSTALLAPPSPREFIX="/Applications" +- ;; ++ prefix=$PYTHONFRAMEWORKINSTALLDIR/Versions/$VERSION ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX=${prefix} ++ RESSRCDIR=Mac/Resources/framework ++ ++ # Add files for Mac specific code to the list of output ++ # files: ++ ac_config_files="$ac_config_files Mac/Makefile" ++ ++ ac_config_files="$ac_config_files Mac/PythonLauncher/Makefile" ++ ++ ac_config_files="$ac_config_files Mac/Resources/framework/Info.plist" ++ ++ ac_config_files="$ac_config_files Mac/Resources/app/Info.plist" ++ ++ ;; ++ iOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/iOS/Resources ++ ++ ac_config_files="$ac_config_files Apple/iOS/Resources/Info.plist" ++ ++ ;; ++ tvOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/tvOS/Resources ++ ++ ac_config_files="$ac_config_files Apple/tvOS/Resources/Info.plist" ++ ++ ;; ++ watchOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/watchOS/Resources ++ ++ ac_config_files="$ac_config_files Apple/watchOS/Resources/Info.plist" ++ ++ ;; ++ *) ++ as_fn_error $? "Unknown platform for framework build" "$LINENO" 5 ++ ;; ++ esac + esac + +- prefix=$PYTHONFRAMEWORKINSTALLDIR/Versions/$VERSION +- +- # Add files for Mac specific code to the list of output +- # files: +- ac_config_files="$ac_config_files Mac/Makefile" +- +- ac_config_files="$ac_config_files Mac/PythonLauncher/Makefile" +- +- ac_config_files="$ac_config_files Mac/Resources/framework/Info.plist" +- +- ac_config_files="$ac_config_files Mac/Resources/app/Info.plist" ++else $as_nop + ++ case $ac_sys_system in ++ iOS) as_fn_error $? "iOS builds must use --enable-framework" "$LINENO" 5 ;; ++ tvOS) as_fn_error $? "tvOS builds must use --enable-framework" "$LINENO" 5 ;; ++ watchOS) as_fn_error $? "watchOS builds must use --enable-framework" "$LINENO" 5 ;; ++ *) ++ PYTHONFRAMEWORK= ++ PYTHONFRAMEWORKDIR=no-framework ++ PYTHONFRAMEWORKPREFIX= ++ PYTHONFRAMEWORKINSTALLDIR= ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX= ++ RESSRCDIR= ++ FRAMEWORKINSTALLFIRST= ++ FRAMEWORKINSTALLLAST= ++ FRAMEWORKALTINSTALLFIRST= ++ FRAMEWORKALTINSTALLLAST= ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="commoninstall bininstall maninstall" ++ if test "x${prefix}" = "xNONE" ; then ++ FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" ++ else ++ FRAMEWORKUNIXTOOLSPREFIX="${prefix}" ++ fi ++ enable_framework= + esac + +-else $as_nop ++fi + +- PYTHONFRAMEWORK= +- PYTHONFRAMEWORKDIR=no-framework +- PYTHONFRAMEWORKPREFIX= +- PYTHONFRAMEWORKINSTALLDIR= +- FRAMEWORKINSTALLFIRST= +- FRAMEWORKINSTALLLAST= +- FRAMEWORKALTINSTALLFIRST= +- FRAMEWORKALTINSTALLLAST= +- FRAMEWORKPYTHONW= +- if test "x${prefix}" = "xNONE" ; then +- FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" +- else +- FRAMEWORKUNIXTOOLSPREFIX="${prefix}" +- fi +- enable_framework= + + +-fi + + + +@@ -4253,76 +4507,52 @@ + printf "%s\n" "#define _PYTHONFRAMEWORK \"${PYTHONFRAMEWORK}\"" >>confdefs.h + + +-# Set name for machine-dependent library files +- +-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking MACHDEP" >&5 +-printf %s "checking MACHDEP... " >&6; } +-if test -z "$MACHDEP" +-then +- # avoid using uname for cross builds +- if test "$cross_compiling" = yes; then +- # ac_sys_system and ac_sys_release are used for setting +- # a lot of different things including 'define_xopen_source' +- # in the case statement below. +- case "$host" in +- *-*-linux-android*) +- ac_sys_system=Linux-android +- ;; +- *-*-linux*) +- ac_sys_system=Linux +- ;; +- *-*-cygwin*) +- ac_sys_system=Cygwin +- ;; +- *-*-vxworks*) +- ac_sys_system=VxWorks +- ;; +- *-*-emscripten) +- ac_sys_system=Emscripten +- ;; +- *-*-wasi) +- ac_sys_system=WASI +- ;; +- *) +- # for now, limit cross builds to known configurations +- MACHDEP="unknown" +- as_fn_error $? "cross build not supported for $host" "$LINENO" 5 +- esac +- ac_sys_release= +- else +- ac_sys_system=`uname -s` +- if test "$ac_sys_system" = "AIX" \ +- -o "$ac_sys_system" = "UnixWare" -o "$ac_sys_system" = "OpenUNIX"; then +- ac_sys_release=`uname -v` +- else +- ac_sys_release=`uname -r` +- fi +- fi +- ac_md_system=`echo $ac_sys_system | +- tr -d '/ ' | tr '[A-Z]' '[a-z]'` +- ac_md_release=`echo $ac_sys_release | +- tr -d '/ ' | sed 's/^[A-Z]\.//' | sed 's/\..*//'` +- MACHDEP="$ac_md_system$ac_md_release" ++{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for --with-app-store-compliance" >&5 ++printf %s "checking for --with-app-store-compliance... " >&6; } + +- case $MACHDEP in +- aix*) MACHDEP="aix";; +- linux*) MACHDEP="linux";; +- cygwin*) MACHDEP="cygwin";; +- darwin*) MACHDEP="darwin";; +- '') MACHDEP="unknown";; ++# Check whether --with-app_store_compliance was given. ++if test ${with_app_store_compliance+y} ++then : ++ withval=$with_app_store_compliance; ++ case "$withval" in ++ yes) ++ case $ac_sys_system in ++ Darwin|iOS|tvOS|watchOS) ++ # iOS/tvOS/watchOS is able to share the macOS patch ++ APP_STORE_COMPLIANCE_PATCH="Mac/Resources/app-store-compliance.patch" ++ ;; ++ *) as_fn_error $? "no default app store compliance patch available for $ac_sys_system" "$LINENO" 5 ;; ++ esac ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: applying default app store compliance patch" >&5 ++printf "%s\n" "applying default app store compliance patch" >&6; } ++ ;; ++ *) ++ APP_STORE_COMPLIANCE_PATCH="${withval}" ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: applying custom app store compliance patch" >&5 ++printf "%s\n" "applying custom app store compliance patch" >&6; } ++ ;; + esac + +- if test "$ac_sys_system" = "SunOS"; then +- # For Solaris, there isn't an OS version specific macro defined +- # in most compilers, so we define one here. +- SUNOS_VERSION=`echo $ac_sys_release | sed -e 's!\.\(0-9\)$!.0\1!g' | tr -d '.'` ++else $as_nop + +-printf "%s\n" "#define Py_SUNOS_VERSION $SUNOS_VERSION" >>confdefs.h ++ case $ac_sys_system in ++ iOS|tvOS|watchOS) ++ # Always apply the compliance patch on iOS/tvOS/watchOS; we can use the macOS patch ++ APP_STORE_COMPLIANCE_PATCH="Mac/Resources/app-store-compliance.patch" ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: applying default app store compliance patch" >&5 ++printf "%s\n" "applying default app store compliance patch" >&6; } ++ ;; ++ *) ++ # No default app compliance patching on any other platform ++ APP_STORE_COMPLIANCE_PATCH= ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: not patching for app store compliance" >&5 ++printf "%s\n" "not patching for app store compliance" >&6; } ++ ;; ++ esac + +- fi + fi +-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: \"$MACHDEP\"" >&5 +-printf "%s\n" "\"$MACHDEP\"" >&6; } ++ ++ + + + if test "$cross_compiling" = yes; then +@@ -4330,27 +4560,93 @@ + *-*-linux*) + case "$host_cpu" in + arm*) +- _host_cpu=arm ++ _host_ident=arm + ;; + *) +- _host_cpu=$host_cpu ++ _host_ident=$host_cpu + esac + ;; + *-*-cygwin*) +- _host_cpu= ++ _host_ident= ++ ;; ++ *-apple-ios*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # IPHONEOS_DEPLOYMENT_TARGET is the minimum supported iOS version ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking iOS deployment target" >&5 ++printf %s "checking iOS deployment target... " >&6; } ++ IPHONEOS_DEPLOYMENT_TARGET=${_host_os:3} ++ IPHONEOS_DEPLOYMENT_TARGET=${IPHONEOS_DEPLOYMENT_TARGET:=13.0} ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $IPHONEOS_DEPLOYMENT_TARGET" >&5 ++printf "%s\n" "$IPHONEOS_DEPLOYMENT_TARGET" >&6; } ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-arm64-iphone${_host_device} ++ ;; ++ *) ++ _host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-$host_cpu-iphone${_host_device} ++ ;; ++ esac ++ ;; ++ *-apple-tvos*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # TVOS_DEPLOYMENT_TARGET is the minimum supported tvOS version ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking tvOS deployment target" >&5 ++printf %s "checking tvOS deployment target... " >&6; } ++ TVOS_DEPLOYMENT_TARGET=${_host_os:4} ++ TVOS_DEPLOYMENT_TARGET=${TVOS_DEPLOYMENT_TARGET:=12.0} ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $TVOS_DEPLOYMENT_TARGET" >&5 ++printf "%s\n" "$TVOS_DEPLOYMENT_TARGET" >&6; } ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${TVOS_DEPLOYMENT_TARGET}-arm64-appletv${_host_device} ++ ;; ++ *) ++ _host_ident=${TVOS_DEPLOYMENT_TARGET}-$host_cpu-appletv${_host_device} ++ ;; ++ esac ++ ;; ++ *-apple-watchos*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # WATCHOS_DEPLOYMENT_TARGET is the minimum supported watchOS version ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking watchOS deployment target" >&5 ++printf %s "checking watchOS deployment target... " >&6; } ++ WATCHOS_DEPLOYMENT_TARGET=${_host_os:7} ++ WATCHOS_DEPLOYMENT_TARGET=${WATCHOS_DEPLOYMENT_TARGET:=4.0} ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $WATCHOS_DEPLOYMENT_TARGET" >&5 ++printf "%s\n" "$WATCHOS_DEPLOYMENT_TARGET" >&6; } ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${WATCHOS_DEPLOYMENT_TARGET}-arm64-watch${_host_device} ++ ;; ++ *) ++ _host_ident=${WATCHOS_DEPLOYMENT_TARGET}-$host_cpu-watch${_host_device} ++ ;; ++ esac + ;; + *-*-vxworks*) +- _host_cpu=$host_cpu ++ _host_ident=$host_cpu + ;; + wasm32-*-* | wasm64-*-*) +- _host_cpu=$host_cpu ++ _host_ident=$host_cpu + ;; + *) + # for now, limit cross builds to known configurations + MACHDEP="unknown" + as_fn_error $? "cross build not supported for $host" "$LINENO" 5 + esac +- _PYTHON_HOST_PLATFORM="$MACHDEP${_host_cpu:+-$_host_cpu}" ++ _PYTHON_HOST_PLATFORM="$MACHDEP${_host_ident:+-$_host_ident}" + fi + + # Some systems cannot stand _XOPEN_SOURCE being defined at all; they +@@ -4417,6 +4713,13 @@ + define_xopen_source=no;; + Darwin/[12][0-9].*) + define_xopen_source=no;; ++ # On iOS/tvOS/watchOS, defining _POSIX_C_SOURCE also disables platform specific features. ++ iOS/*) ++ define_xopen_source=no;; ++ tvOS/*) ++ define_xopen_source=no;; ++ watchOS/*) ++ define_xopen_source=no;; + # On QNX 6.3.2, defining _XOPEN_SOURCE prevents netdb.h from + # defining NI_NUMERICHOST. + QNX/6.3.2) +@@ -4479,6 +4782,12 @@ + CONFIGURE_MACOSX_DEPLOYMENT_TARGET= + EXPORT_MACOSX_DEPLOYMENT_TARGET='#' + ++# Record the value of IPHONEOS_DEPLOYMENT_TARGET / TVOS_DEPLOYMENT_TARGET / ++# WATCHOS_DEPLOYMENT_TARGET enforced by the selected host triple. ++ ++ ++ ++ + # checks for alternative programs + + # compiler flags are generated in two sets, BASECFLAGS and OPT. OPT is just +@@ -4511,6 +4820,26 @@ + ;; + esac + ++case $ac_sys_system in #( ++ iOS) : ++ ++ as_fn_append CFLAGS " -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}" ++ as_fn_append LDFLAGS " -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}" ++ ;; #( ++ tvOS) : ++ ++ as_fn_append CFLAGS " -mtvos-version-min=${TVOS_DEPLOYMENT_TARGET}" ++ as_fn_append LDFLAGS " -mtvos-version-min=${TVOS_DEPLOYMENT_TARGET}" ++ ;; #( ++ watchOS) : ++ ++ as_fn_append CFLAGS " -mwatchos-version-min=${WATCHOS_DEPLOYMENT_TARGET}" ++ as_fn_append LDFLAGS " -mwatchos-version-min=${WATCHOS_DEPLOYMENT_TARGET}" ++ ;; #( ++ *) : ++ ;; ++esac ++ + if test "$ac_sys_system" = "Darwin" + then + # Extract the first word of "xcrun", so it can be a program name with args. +@@ -6908,7 +7237,42 @@ + #elif defined(__gnu_hurd__) + i386-gnu + #elif defined(__APPLE__) ++# include "TargetConditionals.h" ++# if TARGET_OS_IOS ++# if TARGET_OS_SIMULATOR ++# if __x86_64__ ++ x86_64-iphonesimulator ++# else ++ arm64-iphonesimulator ++# endif ++# else ++ arm64-iphoneos ++# endif ++# elif TARGET_OS_TV ++# if TARGET_OS_SIMULATOR ++# if __x86_64__ ++ x86_64-appletvsimulator ++# else ++ arm64-appletvsimulator ++# endif ++# else ++ arm64-appletvos ++# endif ++# elif TARGET_OS_WATCH ++# if TARGET_OS_SIMULATOR ++# if __x86_64__ ++ x86_64-watchsimulator ++# else ++ arm64-watchsimulator ++# endif ++# else ++ arm64_32-watchos ++# endif ++# elif TARGET_OS_OSX + darwin ++# else ++# error unknown Apple platform ++# endif + #elif defined(__VXWORKS__) + vxworks + #elif defined(__wasm32__) +@@ -6957,6 +7321,12 @@ + case $ac_sys_system in #( + Darwin*) : + MULTIARCH="" ;; #( ++ iOS) : ++ MULTIARCH="" ;; #( ++ tvOS) : ++ MULTIARCH="" ;; #( ++ watchOS) : ++ MULTIARCH="" ;; #( + FreeBSD*) : + MULTIARCH="" ;; #( + *) : +@@ -6964,8 +7334,6 @@ + ;; + esac + +-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $MULTIARCH" >&5 +-printf "%s\n" "$MULTIARCH" >&6; } + + if test x$PLATFORM_TRIPLET != x && test x$MULTIARCH != x; then + if test x$PLATFORM_TRIPLET != x$MULTIARCH; then +@@ -6975,6 +7343,16 @@ + MULTIARCH=$PLATFORM_TRIPLET + fi + ++{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $MULTIARCH" >&5 ++printf "%s\n" "$MULTIARCH" >&6; } ++ ++case $ac_sys_system in #( ++ iOS|tvOS|watchOS) : ++ SOABI_PLATFORM=`echo "$PLATFORM_TRIPLET" | cut -d '-' -f2` ;; #( ++ *) : ++ SOABI_PLATFORM=$PLATFORM_TRIPLET ++ ;; ++esac + + if test x$MULTIARCH != x; then + MULTIARCH_CPPFLAGS="-DMULTIARCH=\\\"$MULTIARCH\\\"" +@@ -7018,6 +7396,18 @@ + PY_SUPPORT_TIER=3 ;; #( + x86_64-*-freebsd*/clang) : + PY_SUPPORT_TIER=3 ;; #( ++ aarch64-apple-ios*-simulator/clang) : ++ PY_SUPPORT_TIER=3 ;; #( ++ aarch64-apple-ios*/clang) : ++ PY_SUPPORT_TIER=3 ;; #( ++ aarch64-apple-tvos*-simulator/clang) : ++ PY_SUPPORT_TIER=3 ;; #( ++ aarch64-apple-tvos*/clang) : ++ PY_SUPPORT_TIER=3 ;; #( ++ aarch64-apple-watchos*-simulator/clang) : ++ PY_SUPPORT_TIER=3 ;; #( ++ arm64_32-apple-watchos*/clang) : ++ PY_SUPPORT_TIER=3 ;; #( + *) : + PY_SUPPORT_TIER=0 + ;; +@@ -7471,17 +7861,25 @@ + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking LDLIBRARY" >&5 + printf %s "checking LDLIBRARY... " >&6; } + +-# MacOSX framework builds need more magic. LDLIBRARY is the dynamic ++# Apple framework builds need more magic. LDLIBRARY is the dynamic + # library that we build, but we do not want to link against it (we + # will find it with a -framework option). For this reason there is an + # extra variable BLDLIBRARY against which Python and the extension + # modules are linked, BLDLIBRARY. This is normally the same as +-# LDLIBRARY, but empty for MacOSX framework builds. ++# LDLIBRARY, but empty for MacOSX framework builds. iOS does the same, ++# but uses a non-versioned framework layout. + if test "$enable_framework" + then +- LDLIBRARY='$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)' +- RUNSHARED=DYLD_FRAMEWORK_PATH=`pwd`${DYLD_FRAMEWORK_PATH:+:${DYLD_FRAMEWORK_PATH}} ++ case $ac_sys_system in ++ Darwin) ++ LDLIBRARY='$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)';; ++ iOS|tvOS|watchOS) ++ LDLIBRARY='$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)';; ++ *) ++ as_fn_error $? "Unknown platform for framework build" "$LINENO" 5;; ++ esac + BLDLIBRARY='' ++ RUNSHARED=DYLD_FRAMEWORK_PATH=`pwd`${DYLD_FRAMEWORK_PATH:+:${DYLD_FRAMEWORK_PATH}} + else + BLDLIBRARY='$(LDLIBRARY)' + fi +@@ -7494,64 +7892,70 @@ + + case $ac_sys_system in + CYGWIN*) +- LDLIBRARY='libpython$(LDVERSION).dll.a' +- DLLLIBRARY='libpython$(LDVERSION).dll' +- ;; ++ LDLIBRARY='libpython$(LDVERSION).dll.a' ++ DLLLIBRARY='libpython$(LDVERSION).dll' ++ ;; + SunOS*) +- LDLIBRARY='libpython$(LDVERSION).so' +- BLDLIBRARY='-Wl,-R,$(LIBDIR) -L. -lpython$(LDVERSION)' +- RUNSHARED=LD_LIBRARY_PATH=`pwd`${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} +- INSTSONAME="$LDLIBRARY".$SOVERSION +- if test "$with_pydebug" != yes +- then +- PY3LIBRARY=libpython3.so +- fi +- ;; ++ LDLIBRARY='libpython$(LDVERSION).so' ++ BLDLIBRARY='-Wl,-R,$(LIBDIR) -L. -lpython$(LDVERSION)' ++ RUNSHARED=LD_LIBRARY_PATH=`pwd`${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} ++ INSTSONAME="$LDLIBRARY".$SOVERSION ++ if test "$with_pydebug" != yes ++ then ++ PY3LIBRARY=libpython3.so ++ fi ++ ;; + Linux*|GNU*|NetBSD*|FreeBSD*|DragonFly*|OpenBSD*|VxWorks*) +- LDLIBRARY='libpython$(LDVERSION).so' +- BLDLIBRARY='-L. -lpython$(LDVERSION)' +- RUNSHARED=LD_LIBRARY_PATH=`pwd`${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} +- INSTSONAME="$LDLIBRARY".$SOVERSION +- if test "$with_pydebug" != yes +- then +- PY3LIBRARY=libpython3.so +- fi +- ;; ++ LDLIBRARY='libpython$(LDVERSION).so' ++ BLDLIBRARY='-L. -lpython$(LDVERSION)' ++ RUNSHARED=LD_LIBRARY_PATH=`pwd`${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} ++ INSTSONAME="$LDLIBRARY".$SOVERSION ++ if test "$with_pydebug" != yes ++ then ++ PY3LIBRARY=libpython3.so ++ fi ++ ;; + hp*|HP*) +- case `uname -m` in +- ia64) +- LDLIBRARY='libpython$(LDVERSION).so' +- ;; +- *) +- LDLIBRARY='libpython$(LDVERSION).sl' +- ;; +- esac +- BLDLIBRARY='-Wl,+b,$(LIBDIR) -L. -lpython$(LDVERSION)' +- RUNSHARED=SHLIB_PATH=`pwd`${SHLIB_PATH:+:${SHLIB_PATH}} +- ;; ++ case `uname -m` in ++ ia64) ++ LDLIBRARY='libpython$(LDVERSION).so' ++ ;; ++ *) ++ LDLIBRARY='libpython$(LDVERSION).sl' ++ ;; ++ esac ++ BLDLIBRARY='-Wl,+b,$(LIBDIR) -L. -lpython$(LDVERSION)' ++ RUNSHARED=SHLIB_PATH=`pwd`${SHLIB_PATH:+:${SHLIB_PATH}} ++ ;; + Darwin*) +- LDLIBRARY='libpython$(LDVERSION).dylib' +- BLDLIBRARY='-L. -lpython$(LDVERSION)' +- RUNSHARED=DYLD_LIBRARY_PATH=`pwd`${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}} +- ;; ++ LDLIBRARY='libpython$(LDVERSION).dylib' ++ BLDLIBRARY='-L. -lpython$(LDVERSION)' ++ RUNSHARED=DYLD_LIBRARY_PATH=`pwd`${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}} ++ ;; ++ iOS|tvOS|watchOS) ++ LDLIBRARY='libpython$(LDVERSION).dylib' ++ ;; + AIX*) +- LDLIBRARY='libpython$(LDVERSION).so' +- RUNSHARED=LIBPATH=`pwd`${LIBPATH:+:${LIBPATH}} +- ;; ++ LDLIBRARY='libpython$(LDVERSION).so' ++ RUNSHARED=LIBPATH=`pwd`${LIBPATH:+:${LIBPATH}} ++ ;; + + esac + else # shared is disabled + PY_ENABLE_SHARED=0 + case $ac_sys_system in + CYGWIN*) +- BLDLIBRARY='$(LIBRARY)' +- LDLIBRARY='libpython$(LDVERSION).dll.a' +- ;; ++ BLDLIBRARY='$(LIBRARY)' ++ LDLIBRARY='libpython$(LDVERSION).dll.a' ++ ;; + esac + fi + ++{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $LDLIBRARY" >&5 ++printf "%s\n" "$LDLIBRARY" >&6; } ++ + if test "$cross_compiling" = yes; then +- RUNSHARED= ++ RUNSHARED= + fi + + +@@ -7746,9 +8150,6 @@ + PYTHON_FOR_BUILD="_PYTHON_HOSTRUNNER='$HOSTRUNNER' $PYTHON_FOR_BUILD" + fi + +-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $LDLIBRARY" >&5 +-printf "%s\n" "$LDLIBRARY" >&6; } +- + # LIBRARY_DEPS, LINK_PYTHON_OBJS and LINK_PYTHON_DEPS variable + case $ac_sys_system/$ac_sys_emscripten_target in #( + Emscripten/browser*) : +@@ -12812,6 +13213,11 @@ + BLDSHARED="$LDSHARED" + fi + ;; ++ iOS/*|tvOS/*|watchOS/*) ++ LDSHARED='$(CC) -dynamiclib -F . -framework $(PYTHONFRAMEWORK)' ++ LDCXXSHARED='$(CXX) -dynamiclib -F . -framework $(PYTHONFRAMEWORK)' ++ BLDSHARED="$LDSHARED" ++ ;; + Emscripten*|WASI*) + LDSHARED='$(CC) -shared' + LDCXXSHARED='$(CXX) -shared';; +@@ -12941,30 +13347,34 @@ + Linux-android*) LINKFORSHARED="-pie -Xlinker -export-dynamic";; + Linux*|GNU*) LINKFORSHARED="-Xlinker -export-dynamic";; + # -u libsys_s pulls in all symbols in libsys +- Darwin/*) ++ Darwin/*|iOS/*|tvOS/*|watchOS/*) + LINKFORSHARED="$extra_undefs -framework CoreFoundation" + + # Issue #18075: the default maximum stack size (8MBytes) is too + # small for the default recursion limit. Increase the stack size + # to ensure that tests don't crash +- stack_size="1000000" # 16 MB +- if test "$with_ubsan" = "yes" +- then +- # Undefined behavior sanitizer requires an even deeper stack +- stack_size="4000000" # 64 MB +- fi +- +- LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED" ++ stack_size="1000000" # 16 MB ++ if test "$with_ubsan" = "yes" ++ then ++ # Undefined behavior sanitizer requires an even deeper stack ++ stack_size="4000000" # 64 MB ++ fi + + + printf "%s\n" "#define THREAD_STACK_SIZE 0x$stack_size" >>confdefs.h + + +- if test "$enable_framework" +- then +- LINKFORSHARED="$LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)' ++ if test $ac_sys_system = "Darwin"; then ++ LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED" ++ ++ if test "$enable_framework"; then ++ LINKFORSHARED="$LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)' ++ fi ++ LINKFORSHARED="$LINKFORSHARED" ++ elif test "$ac_sys_system" = "iOS" -o "$ac_sys_system" = "tvOS" -o "$ac_sys_system" = "watchOS"; then ++ LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + fi +- LINKFORSHARED="$LINKFORSHARED";; ++ ;; + OpenUNIX*|UnixWare*) LINKFORSHARED="-Wl,-Bexport";; + SCO_SV*) LINKFORSHARED="-Wl,-Bexport";; + ReliantUNIX*) LINKFORSHARED="-W1 -Blargedynsym";; +@@ -14353,6 +14763,10 @@ + + ctypes_malloc_closure=yes + ;; #( ++ iOS|tvOS|watchOS) : ++ ++ ctypes_malloc_closure=yes ++ ;; #( + sunos5) : + as_fn_append LIBFFI_LIBS " -mimpure-text" + ;; #( +@@ -17622,12 +18036,6 @@ + then : + printf "%s\n" "#define HAVE_DUP3 1" >>confdefs.h + +-fi +-ac_fn_c_check_func "$LINENO" "execv" "ac_cv_func_execv" +-if test "x$ac_cv_func_execv" = xyes +-then : +- printf "%s\n" "#define HAVE_EXECV 1" >>confdefs.h +- + fi + ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero" + if test "x$ac_cv_func_explicit_bzero" = xyes +@@ -17688,18 +18096,6 @@ + then : + printf "%s\n" "#define HAVE_FEXECVE 1" >>confdefs.h + +-fi +-ac_fn_c_check_func "$LINENO" "fork" "ac_cv_func_fork" +-if test "x$ac_cv_func_fork" = xyes +-then : +- printf "%s\n" "#define HAVE_FORK 1" >>confdefs.h +- +-fi +-ac_fn_c_check_func "$LINENO" "fork1" "ac_cv_func_fork1" +-if test "x$ac_cv_func_fork1" = xyes +-then : +- printf "%s\n" "#define HAVE_FORK1 1" >>confdefs.h +- + fi + ac_fn_c_check_func "$LINENO" "fpathconf" "ac_cv_func_fpathconf" + if test "x$ac_cv_func_fpathconf" = xyes +@@ -17754,12 +18150,6 @@ + then : + printf "%s\n" "#define HAVE_GETEGID 1" >>confdefs.h + +-fi +-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 +- + fi + ac_fn_c_check_func "$LINENO" "geteuid" "ac_cv_func_geteuid" + if test "x$ac_cv_func_geteuid" = xyes +@@ -17796,12 +18186,6 @@ + then : + printf "%s\n" "#define HAVE_GETGROUPLIST 1" >>confdefs.h + +-fi +-ac_fn_c_check_func "$LINENO" "getgroups" "ac_cv_func_getgroups" +-if test "x$ac_cv_func_getgroups" = xyes +-then : +- printf "%s\n" "#define HAVE_GETGROUPS 1" >>confdefs.h +- + fi + ac_fn_c_check_func "$LINENO" "gethostname" "ac_cv_func_gethostname" + if test "x$ac_cv_func_gethostname" = xyes +@@ -18120,18 +18504,6 @@ + then : + printf "%s\n" "#define HAVE_POSIX_FALLOCATE 1" >>confdefs.h + +-fi +-ac_fn_c_check_func "$LINENO" "posix_spawn" "ac_cv_func_posix_spawn" +-if test "x$ac_cv_func_posix_spawn" = xyes +-then : +- printf "%s\n" "#define HAVE_POSIX_SPAWN 1" >>confdefs.h +- +-fi +-ac_fn_c_check_func "$LINENO" "posix_spawnp" "ac_cv_func_posix_spawnp" +-if test "x$ac_cv_func_posix_spawnp" = xyes +-then : +- printf "%s\n" "#define HAVE_POSIX_SPAWNP 1" >>confdefs.h +- + fi + ac_fn_c_check_func "$LINENO" "pread" "ac_cv_func_pread" + if test "x$ac_cv_func_pread" = xyes +@@ -18396,12 +18768,6 @@ + then : + printf "%s\n" "#define HAVE_SIGACTION 1" >>confdefs.h + +-fi +-ac_fn_c_check_func "$LINENO" "sigaltstack" "ac_cv_func_sigaltstack" +-if test "x$ac_cv_func_sigaltstack" = xyes +-then : +- printf "%s\n" "#define HAVE_SIGALTSTACK 1" >>confdefs.h +- + fi + ac_fn_c_check_func "$LINENO" "sigfillset" "ac_cv_func_sigfillset" + if test "x$ac_cv_func_sigfillset" = xyes +@@ -18492,12 +18858,6 @@ + then : + printf "%s\n" "#define HAVE_SYSCONF 1" >>confdefs.h + +-fi +-ac_fn_c_check_func "$LINENO" "system" "ac_cv_func_system" +-if test "x$ac_cv_func_system" = xyes +-then : +- printf "%s\n" "#define HAVE_SYSTEM 1" >>confdefs.h +- + fi + ac_fn_c_check_func "$LINENO" "tcgetpgrp" "ac_cv_func_tcgetpgrp" + if test "x$ac_cv_func_tcgetpgrp" = xyes +@@ -18553,10 +18913,10 @@ + printf "%s\n" "#define HAVE_TRUNCATE 1" >>confdefs.h + + fi +-ac_fn_c_check_func "$LINENO" "ttyname_r" "ac_cv_func_ttyname_r" +-if test "x$ac_cv_func_ttyname_r" = xyes ++ac_fn_c_check_func "$LINENO" "ttyname" "ac_cv_func_ttyname" ++if test "x$ac_cv_func_ttyname" = xyes + then : +- printf "%s\n" "#define HAVE_TTYNAME_R 1" >>confdefs.h ++ printf "%s\n" "#define HAVE_TTYNAME 1" >>confdefs.h + + fi + ac_fn_c_check_func "$LINENO" "umask" "ac_cv_func_umask" +@@ -18670,6 +19030,73 @@ + + fi + ++# iOS/tvOS/watchOS define some system methods that can be linked (so they are ++# found by configure), but either raise a compilation error (because the ++# 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" "getentropy" "ac_cv_func_getentropy" ++if test "x$ac_cv_func_getentropy" = xyes ++then : ++ printf "%s\n" "#define HAVE_GETENTROPY 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "getgroups" "ac_cv_func_getgroups" ++if test "x$ac_cv_func_getgroups" = xyes ++then : ++ printf "%s\n" "#define HAVE_GETGROUPS 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "system" "ac_cv_func_system" ++if test "x$ac_cv_func_system" = xyes ++then : ++ printf "%s\n" "#define HAVE_SYSTEM 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 ++ ac_fn_c_check_func "$LINENO" "execv" "ac_cv_func_execv" ++if test "x$ac_cv_func_execv" = xyes ++then : ++ printf "%s\n" "#define HAVE_EXECV 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "fork" "ac_cv_func_fork" ++if test "x$ac_cv_func_fork" = xyes ++then : ++ printf "%s\n" "#define HAVE_FORK 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "fork1" "ac_cv_func_fork1" ++if test "x$ac_cv_func_fork1" = xyes ++then : ++ printf "%s\n" "#define HAVE_FORK1 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "posix_spawn" "ac_cv_func_posix_spawn" ++if test "x$ac_cv_func_posix_spawn" = xyes ++then : ++ printf "%s\n" "#define HAVE_POSIX_SPAWN 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "posix_spawnp" "ac_cv_func_posix_spawnp" ++if test "x$ac_cv_func_posix_spawnp" = xyes ++then : ++ printf "%s\n" "#define HAVE_POSIX_SPAWNP 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "sigaltstack" "ac_cv_func_sigaltstack" ++if test "x$ac_cv_func_sigaltstack" = xyes ++then : ++ printf "%s\n" "#define HAVE_SIGALTSTACK 1" >>confdefs.h ++ ++fi ++ ++fi ++ + { 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 +21849,8 @@ + + + # check for openpty, login_tty, and forkpty +- ++# tvOS/watchOS have functions for tty, but can't use them ++if test "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "watchOS" ; then + + for ac_func in openpty + do : +@@ -21518,7 +21946,7 @@ + fi + + done +-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing login_tty" >&5 ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing login_tty" >&5 + printf %s "checking for library containing login_tty... " >&6; } + if test ${ac_cv_search_login_tty+y} + then : +@@ -21675,6 +22103,7 @@ + fi + + done ++fi + + # check for long file support functions + ac_fn_c_check_func "$LINENO" "fseek64" "ac_cv_func_fseek64" +@@ -22222,6 +22651,11 @@ + + done + ++# On iOS, tvOS and watchOS, clock_settime can be linked (so it is found by ++# configure), but when used in an unprivileged process, it crashes rather than ++# returning an error. Force the symbol off. ++if test "$ac_sys_system" != "iOS" -a "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "watchOS" ++then + + for ac_func in clock_settime + do : +@@ -22232,7 +22666,7 @@ + + else $as_nop + +- { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for clock_settime in -lrt" >&5 ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for clock_settime in -lrt" >&5 + printf %s "checking for clock_settime in -lrt... " >&6; } + if test ${ac_cv_lib_rt_clock_settime+y} + then : +@@ -22270,7 +22704,7 @@ + if test "x$ac_cv_lib_rt_clock_settime" = xyes + then : + +- printf "%s\n" "#define HAVE_CLOCK_SETTIME 1" >>confdefs.h ++ printf "%s\n" "#define HAVE_CLOCK_SETTIME 1" >>confdefs.h + + + fi +@@ -22279,6 +22713,7 @@ + fi + + done ++fi + + + for ac_func in clock_nanosleep +@@ -22500,7 +22935,9 @@ + if test "$cross_compiling" = yes + then : + +-if test "${enable_ipv6+set}" = set; then ++if test "$ac_sys_system" = "Linux-android" -o "$ac_sys_system" = "iOS" -o "$ac_sys_system" = "tvOS" -o "$ac_sys_system" = "watchOS"; then ++ ac_cv_buggy_getaddrinfo="no" ++elif test "${enable_ipv6+set}" = set; then + ac_cv_buggy_getaddrinfo="no -- configured with --(en|dis)able-ipv6" + else + ac_cv_buggy_getaddrinfo=yes +@@ -24404,7 +24841,7 @@ + printf "%s\n" "$ABIFLAGS" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking SOABI" >&5 + printf %s "checking SOABI... " >&6; } +-SOABI='cpython-'`echo $VERSION | tr -d .`${ABIFLAGS}${PLATFORM_TRIPLET:+-$PLATFORM_TRIPLET} ++SOABI='cpython-'`echo $VERSION | tr -d .`${ABIFLAGS}${SOABI_PLATFORM:+-$SOABI_PLATFORM} + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $SOABI" >&5 + printf "%s\n" "$SOABI" >&6; } + +@@ -24412,7 +24849,7 @@ + if test "$Py_DEBUG" = 'true' -a "$with_trace_refs" != "yes"; then + # Similar to SOABI but remove "d" flag from ABIFLAGS + +- ALT_SOABI='cpython-'`echo $VERSION | tr -d .``echo $ABIFLAGS | tr -d d`${PLATFORM_TRIPLET:+-$PLATFORM_TRIPLET} ++ ALT_SOABI='cpython-'`echo $VERSION | tr -d .``echo $ABIFLAGS | tr -d d`${SOABI_PLATFORM:+-$SOABI_PLATFORM} + + printf "%s\n" "#define ALT_SOABI \"${ALT_SOABI}\"" >>confdefs.h + +@@ -27163,24 +27600,28 @@ + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for device files" >&5 + printf "%s\n" "$as_me: checking for device files" >&6;} + +-if test "x$cross_compiling" = xyes; then +- if test "${ac_cv_file__dev_ptmx+set}" != set; then +- { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for /dev/ptmx" >&5 ++if test "$ac_sys_system" = "iOS" -o "$ac_sys_system" = "tvOS" -o "$ac_sys_system" = "watchOS" ; then ++ ac_cv_file__dev_ptmx=no ++ ac_cv_file__dev_ptc=no ++else ++ if test "x$cross_compiling" = xyes; then ++ if test "${ac_cv_file__dev_ptmx+set}" != set; then ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for /dev/ptmx" >&5 + printf %s "checking for /dev/ptmx... " >&6; } +- { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: not set" >&5 ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: not set" >&5 + printf "%s\n" "not set" >&6; } +- as_fn_error $? "set ac_cv_file__dev_ptmx to yes/no in your CONFIG_SITE file when cross compiling" "$LINENO" 5 +- fi +- if test "${ac_cv_file__dev_ptc+set}" != set; then +- { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for /dev/ptc" >&5 ++ as_fn_error $? "set ac_cv_file__dev_ptmx to yes/no in your CONFIG_SITE file when cross compiling" "$LINENO" 5 ++ fi ++ if test "${ac_cv_file__dev_ptc+set}" != set; then ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for /dev/ptc" >&5 + printf %s "checking for /dev/ptc... " >&6; } +- { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: not set" >&5 ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: not set" >&5 + printf "%s\n" "not set" >&6; } +- as_fn_error $? "set ac_cv_file__dev_ptc to yes/no in your CONFIG_SITE file when cross compiling" "$LINENO" 5 ++ as_fn_error $? "set ac_cv_file__dev_ptc to yes/no in your CONFIG_SITE file when cross compiling" "$LINENO" 5 ++ fi + fi +-fi + +-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for /dev/ptmx" >&5 ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for /dev/ptmx" >&5 + printf %s "checking for /dev/ptmx... " >&6; } + if test ${ac_cv_file__dev_ptmx+y} + then : +@@ -27201,12 +27642,12 @@ + + fi + +-if test "x$ac_cv_file__dev_ptmx" = xyes; then ++ if test "x$ac_cv_file__dev_ptmx" = xyes; then + + printf "%s\n" "#define HAVE_DEV_PTMX 1" >>confdefs.h + +-fi +-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for /dev/ptc" >&5 ++ fi ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for /dev/ptc" >&5 + printf %s "checking for /dev/ptc... " >&6; } + if test ${ac_cv_file__dev_ptc+y} + then : +@@ -27227,10 +27668,11 @@ + + fi + +-if test "x$ac_cv_file__dev_ptc" = xyes; then ++ if test "x$ac_cv_file__dev_ptc" = xyes; then + + printf "%s\n" "#define HAVE_DEV_PTC 1" >>confdefs.h + ++ fi + fi + + if test $ac_sys_system = Darwin +@@ -27672,6 +28114,8 @@ + with_ensurepip=no ;; #( + WASI) : + with_ensurepip=no ;; #( ++ iOS|tvOS|watchOS) : ++ with_ensurepip=no ;; #( + *) : + with_ensurepip=upgrade + ;; +@@ -28613,6 +29057,27 @@ + py_cv_module_ossaudiodev=n/a + py_cv_module_spwd=n/a + ;; #( ++ iOS|tvOS|watchOS) : ++ ++ ++ ++ py_cv_module__curses=n/a ++ py_cv_module__curses_panel=n/a ++ py_cv_module__gdbm=n/a ++ py_cv_module__multiprocessing=n/a ++ py_cv_module__posixshmem=n/a ++ py_cv_module__posixsubprocess=n/a ++ py_cv_module__scproxy=n/a ++ py_cv_module__tkinter=n/a ++ py_cv_module_grp=n/a ++ py_cv_module_nis=n/a ++ py_cv_module_readline=n/a ++ py_cv_module_pwd=n/a ++ py_cv_module_spwd=n/a ++ py_cv_module_syslog=n/a ++ py_cv_module_=n/a ++ ++ ;; #( + CYGWIN*) : + + +@@ -32359,6 +32824,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" ;; + "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 1a02d19f1b2..7881beb6d24 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -307,6 +307,161 @@ + AC_MSG_ERROR([pkg-config is required])] + fi + ++# Set name for machine-dependent library files ++AC_ARG_VAR([MACHDEP], [name for machine-dependent library files]) ++AC_MSG_CHECKING([MACHDEP]) ++if test -z "$MACHDEP" ++then ++ # avoid using uname for cross builds ++ if test "$cross_compiling" = yes; then ++ # ac_sys_system and ac_sys_release are used for setting ++ # a lot of different things including 'define_xopen_source' ++ # in the case statement below. ++ case "$host" in ++ *-*-linux-android*) ++ ac_sys_system=Linux-android ++ ;; ++ *-*-linux*) ++ ac_sys_system=Linux ++ ;; ++ *-*-cygwin*) ++ ac_sys_system=Cygwin ++ ;; ++ *-apple-ios*) ++ ac_sys_system=iOS ++ ;; ++ *-apple-tvos*) ++ ac_sys_system=tvOS ++ ;; ++ *-apple-watchos*) ++ ac_sys_system=watchOS ++ ;; ++ *-*-vxworks*) ++ ac_sys_system=VxWorks ++ ;; ++ *-*-emscripten) ++ ac_sys_system=Emscripten ++ ;; ++ *-*-wasi) ++ ac_sys_system=WASI ++ ;; ++ *) ++ # for now, limit cross builds to known configurations ++ MACHDEP="unknown" ++ AC_MSG_ERROR([cross build not supported for $host]) ++ esac ++ ac_sys_release= ++ else ++ ac_sys_system=`uname -s` ++ if test "$ac_sys_system" = "AIX" \ ++ -o "$ac_sys_system" = "UnixWare" -o "$ac_sys_system" = "OpenUNIX"; then ++ ac_sys_release=`uname -v` ++ else ++ ac_sys_release=`uname -r` ++ fi ++ fi ++ ac_md_system=`echo $ac_sys_system | ++ tr -d '[/ ]' | tr '[[A-Z]]' '[[a-z]]'` ++ ac_md_release=`echo $ac_sys_release | ++ tr -d '[/ ]' | sed 's/^[[A-Z]]\.//' | sed 's/\..*//'` ++ MACHDEP="$ac_md_system$ac_md_release" ++ ++ case $MACHDEP in ++ aix*) MACHDEP="aix";; ++ linux*) MACHDEP="linux";; ++ cygwin*) MACHDEP="cygwin";; ++ darwin*) MACHDEP="darwin";; ++ '') MACHDEP="unknown";; ++ esac ++ ++ if test "$ac_sys_system" = "SunOS"; then ++ # For Solaris, there isn't an OS version specific macro defined ++ # in most compilers, so we define one here. ++ SUNOS_VERSION=`echo $ac_sys_release | sed -e 's!\.\([0-9]\)$!.0\1!g' | tr -d '.'` ++ AC_DEFINE_UNQUOTED([Py_SUNOS_VERSION], [$SUNOS_VERSION], ++ [The version of SunOS/Solaris as reported by `uname -r' without the dot.]) ++ fi ++fi ++AC_MSG_RESULT(["$MACHDEP"]) ++ ++# On cross-compile builds, configure will look for a host-specific compiler by ++# prepending the user-provided host triple to the required binary name. ++# ++# On iOS/tvOS/watchOS, this results in binaries like "arm64-apple-ios13.0-simulator-gcc", ++# which isn't a binary that exists, and isn't very convenient, as it contains the ++# iOS version. As the default cross-compiler name won't exist, configure falls ++# back to gcc, which *definitely* won't work. We're providing wrapper scripts for ++# these tools; the binary names of these scripts are better defaults than "gcc". ++# This only requires that the user put the platform scripts folder (e.g., ++# "iOS/Resources/bin") in their path, rather than defining platform-specific ++# names/paths for AR, CC, CPP, and CXX explicitly; and if the user forgets to ++# either put the platform scripts folder in the path, or specify CC etc, ++# configure will fail. ++if test -z "$AR"; then ++ case "$host" in ++ aarch64-apple-ios*-simulator) AR=arm64-apple-ios-simulator-ar ;; ++ aarch64-apple-ios*) AR=arm64-apple-ios-ar ;; ++ x86_64-apple-ios*-simulator) AR=x86_64-apple-ios-simulator-ar ;; ++ ++ aarch64-apple-tvos*-simulator) AR=arm64-apple-tvos-simulator-ar ;; ++ aarch64-apple-tvos*) AR=arm64-apple-tvos-ar ;; ++ x86_64-apple-tvos*-simulator) AR=x86_64-apple-tvos-simulator-ar ;; ++ ++ aarch64-apple-watchos*-simulator) AR=arm64-apple-watchos-simulator-ar ;; ++ aarch64-apple-watchos*) AR=arm64_32-apple-watchos-ar ;; ++ x86_64-apple-watchos*-simulator) AR=x86_64-apple-watchos-simulator-ar ;; ++ *) ++ esac ++fi ++if test -z "$CC"; then ++ case "$host" in ++ aarch64-apple-ios*-simulator) CC=arm64-apple-ios-simulator-clang ;; ++ aarch64-apple-ios*) CC=arm64-apple-ios-clang ;; ++ x86_64-apple-ios*-simulator) CC=x86_64-apple-ios-simulator-clang ;; ++ ++ aarch64-apple-tvos*-simulator) CC=arm64-apple-tvos-simulator-clang ;; ++ aarch64-apple-tvos*) CC=arm64-apple-tvos-clang ;; ++ x86_64-apple-tvos*-simulator) CC=x86_64-apple-tvos-simulator-clang ;; ++ ++ aarch64-apple-watchos*-simulator) CC=arm64-apple-watchos-simulator-clang ;; ++ aarch64-apple-watchos*) CC=arm64_32-apple-watchos-clang ;; ++ x86_64-apple-watchos*-simulator) CC=x86_64-apple-watchos-simulator-clang ;; ++ *) ++ esac ++fi ++if test -z "$CPP"; then ++ case "$host" in ++ aarch64-apple-ios*-simulator) CPP=arm64-apple-ios-simulator-cpp ;; ++ aarch64-apple-ios*) CPP=arm64-apple-ios-cpp ;; ++ x86_64-apple-ios*-simulator) CPP=x86_64-apple-ios-simulator-cpp ;; ++ ++ aarch64-apple-tvos*-simulator) CPP=arm64-apple-tvos-simulator-cpp ;; ++ aarch64-apple-tvos*) CPP=arm64-apple-tvos-cpp ;; ++ x86_64-apple-tvos*-simulator) CPP=x86_64-apple-tvos-simulator-cpp ;; ++ ++ aarch64-apple-watchos*-simulator) CPP=arm64-apple-watchos-simulator-cpp ;; ++ aarch64-apple-watchos*) CPP=arm64_32-apple-watchos-cpp ;; ++ x86_64-apple-watchos*-simulator) CPP=x86_64-apple-watchos-simulator-cpp ;; ++ *) ++ esac ++fi ++if test -z "$CXX"; then ++ case "$host" in ++ aarch64-apple-ios*-simulator) CXX=arm64-apple-ios-simulator-clang++ ;; ++ aarch64-apple-ios*) CXX=arm64-apple-ios-clang++ ;; ++ x86_64-apple-ios*-simulator) CXX=x86_64-apple-ios-simulator-clang++ ;; ++ ++ aarch64-apple-tvos*-simulator) CXX=arm64-apple-tvos-simulator-clang++ ;; ++ aarch64-apple-tvos*) CXX=arm64-apple-tvos-clang++ ;; ++ x86_64-apple-tvos*-simulator) CXX=x86_64-apple-tvos-simulator-clang++ ;; ++ ++ aarch64-apple-watchos*-simulator) CXX=arm64-apple-watchos-simulator-clang++ ;; ++ aarch64-apple-watchos*) CXX=arm64_32-apple-watchos-clang++ ;; ++ x86_64-apple-watchos*-simulator) CXX=x86_64-apple-watchos-simulator-clang++ ;; ++ *) ++ esac ++fi ++ + AC_MSG_CHECKING([for --enable-universalsdk]) + AC_ARG_ENABLE([universalsdk], + AS_HELP_STRING([--enable-universalsdk@<:@=SDKDIR@:>@], +@@ -416,109 +571,189 @@ + [ + case $enableval in + yes) +- 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\) ;; ++ *) AC_MSG_ERROR([Unknown platform for framework build]) ++ esac + esac ++ + case $enableval in + no) +- PYTHONFRAMEWORK= +- PYTHONFRAMEWORKDIR=no-framework +- PYTHONFRAMEWORKPREFIX= +- PYTHONFRAMEWORKINSTALLDIR= +- FRAMEWORKINSTALLFIRST= +- FRAMEWORKINSTALLLAST= +- FRAMEWORKALTINSTALLFIRST= +- FRAMEWORKALTINSTALLLAST= +- FRAMEWORKPYTHONW= +- if test "x${prefix}" = "xNONE"; then +- FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" +- else +- FRAMEWORKUNIXTOOLSPREFIX="${prefix}" +- fi +- enable_framework= ++ case $ac_sys_system in ++ iOS) AC_MSG_ERROR([iOS builds must use --enable-framework]) ;; ++ tvOS) AC_MSG_ERROR([tvOS builds must use --enable-framework]) ;; ++ watchOS) AC_MSG_ERROR([watchOS builds must use --enable-framework]) ;; ++ *) ++ PYTHONFRAMEWORK= ++ PYTHONFRAMEWORKDIR=no-framework ++ PYTHONFRAMEWORKPREFIX= ++ PYTHONFRAMEWORKINSTALLDIR= ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX= ++ RESSRCDIR= ++ FRAMEWORKINSTALLFIRST= ++ FRAMEWORKINSTALLLAST= ++ FRAMEWORKALTINSTALLFIRST= ++ FRAMEWORKALTINSTALLLAST= ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="commoninstall bininstall maninstall" ++ ++ if test "x${prefix}" = "xNONE"; then ++ FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" ++ else ++ FRAMEWORKUNIXTOOLSPREFIX="${prefix}" ++ fi ++ enable_framework= ++ esac + ;; + *) + PYTHONFRAMEWORKPREFIX="${enableval}" + PYTHONFRAMEWORKINSTALLDIR=$PYTHONFRAMEWORKPREFIX/$PYTHONFRAMEWORKDIR +- FRAMEWORKINSTALLFIRST="frameworkinstallstructure" +- FRAMEWORKALTINSTALLFIRST="frameworkinstallstructure " +- FRAMEWORKINSTALLLAST="frameworkinstallmaclib frameworkinstallapps frameworkinstallunixtools" +- FRAMEWORKALTINSTALLLAST="frameworkinstallmaclib frameworkinstallapps frameworkaltinstallunixtools" +- FRAMEWORKPYTHONW="frameworkpythonw" +- FRAMEWORKINSTALLAPPSPREFIX="/Applications" +- +- 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" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmaclib frameworkinstallapps frameworkinstallunixtools" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmaclib frameworkinstallapps frameworkaltinstallunixtools" ++ FRAMEWORKPYTHONW="frameworkpythonw" ++ FRAMEWORKINSTALLAPPSPREFIX="/Applications" ++ INSTALLTARGETS="commoninstall bininstall maninstall" ++ ++ if test "x${prefix}" = "xNONE" ; then ++ FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" ++ ++ else ++ FRAMEWORKUNIXTOOLSPREFIX="${prefix}" ++ fi + +- /Library*) +- FRAMEWORKINSTALLAPPSPREFIX="/Applications" +- ;; ++ case "${enableval}" in ++ /System*) ++ FRAMEWORKINSTALLAPPSPREFIX="/Applications" ++ if test "${prefix}" = "NONE" ; then ++ # See below ++ FRAMEWORKUNIXTOOLSPREFIX="/usr" ++ fi ++ ;; ++ ++ /Library*) ++ FRAMEWORKINSTALLAPPSPREFIX="/Applications" ++ ;; ++ ++ */Library/Frameworks) ++ MDIR="`dirname "${enableval}"`" ++ MDIR="`dirname "${MDIR}"`" ++ FRAMEWORKINSTALLAPPSPREFIX="${MDIR}/Applications" ++ ++ if test "${prefix}" = "NONE"; then ++ # User hasn't specified the ++ # --prefix option, but wants to install ++ # the framework in a non-default location, ++ # ensure that the compatibility links get ++ # installed relative to that prefix as well ++ # instead of in /usr/local. ++ FRAMEWORKUNIXTOOLSPREFIX="${MDIR}" ++ fi ++ ;; + +- */Library/Frameworks) +- MDIR="`dirname "${enableval}"`" +- MDIR="`dirname "${MDIR}"`" +- FRAMEWORKINSTALLAPPSPREFIX="${MDIR}/Applications" +- +- if test "${prefix}" = "NONE"; then +- # User hasn't specified the +- # --prefix option, but wants to install +- # the framework in a non-default location, +- # ensure that the compatibility links get +- # installed relative to that prefix as well +- # instead of in /usr/local. +- FRAMEWORKUNIXTOOLSPREFIX="${MDIR}" +- fi +- ;; ++ *) ++ FRAMEWORKINSTALLAPPSPREFIX="/Applications" ++ ;; ++ esac + +- *) +- FRAMEWORKINSTALLAPPSPREFIX="/Applications" +- ;; ++ prefix=$PYTHONFRAMEWORKINSTALLDIR/Versions/$VERSION ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX=${prefix} ++ RESSRCDIR=Mac/Resources/framework ++ ++ # Add files for Mac specific code to the list of output ++ # files: ++ AC_CONFIG_FILES([Mac/Makefile]) ++ AC_CONFIG_FILES([Mac/PythonLauncher/Makefile]) ++ AC_CONFIG_FILES([Mac/Resources/framework/Info.plist]) ++ AC_CONFIG_FILES([Mac/Resources/app/Info.plist]) ++ ;; ++ iOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/iOS/Resources ++ ++ AC_CONFIG_FILES([Apple/iOS/Resources/Info.plist]) ++ ;; ++ tvOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/tvOS/Resources ++ ++ AC_CONFIG_FILES([Apple/tvOS/Resources/Info.plist]) ++ ;; ++ watchOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/watchOS/Resources ++ ++ AC_CONFIG_FILES([Apple/watchOS/Resources/Info.plist]) ++ ;; ++ *) ++ AC_MSG_ERROR([Unknown platform for framework build]) ++ ;; ++ esac + esac +- +- prefix=$PYTHONFRAMEWORKINSTALLDIR/Versions/$VERSION +- +- # Add files for Mac specific code to the list of output +- # files: +- AC_CONFIG_FILES([Mac/Makefile]) +- AC_CONFIG_FILES([Mac/PythonLauncher/Makefile]) +- AC_CONFIG_FILES([Mac/Resources/framework/Info.plist]) +- AC_CONFIG_FILES([Mac/Resources/app/Info.plist]) +- esac + ],[ +- PYTHONFRAMEWORK= +- PYTHONFRAMEWORKDIR=no-framework +- PYTHONFRAMEWORKPREFIX= +- PYTHONFRAMEWORKINSTALLDIR= +- FRAMEWORKINSTALLFIRST= +- FRAMEWORKINSTALLLAST= +- FRAMEWORKALTINSTALLFIRST= +- FRAMEWORKALTINSTALLLAST= +- FRAMEWORKPYTHONW= +- if test "x${prefix}" = "xNONE" ; then +- FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" +- else +- FRAMEWORKUNIXTOOLSPREFIX="${prefix}" +- fi +- enable_framework= +- ++ case $ac_sys_system in ++ iOS) AC_MSG_ERROR([iOS builds must use --enable-framework]) ;; ++ tvOS) AC_MSG_ERROR([tvOS builds must use --enable-framework]) ;; ++ watchOS) AC_MSG_ERROR([watchOS builds must use --enable-framework]) ;; ++ *) ++ PYTHONFRAMEWORK= ++ PYTHONFRAMEWORKDIR=no-framework ++ PYTHONFRAMEWORKPREFIX= ++ PYTHONFRAMEWORKINSTALLDIR= ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX= ++ RESSRCDIR= ++ FRAMEWORKINSTALLFIRST= ++ FRAMEWORKINSTALLLAST= ++ FRAMEWORKALTINSTALLFIRST= ++ FRAMEWORKALTINSTALLLAST= ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="commoninstall bininstall maninstall" ++ if test "x${prefix}" = "xNONE" ; then ++ FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" ++ else ++ FRAMEWORKUNIXTOOLSPREFIX="${prefix}" ++ fi ++ enable_framework= ++ esac + ]) + AC_SUBST([PYTHONFRAMEWORK]) + AC_SUBST([PYTHONFRAMEWORKIDENTIFIER]) + AC_SUBST([PYTHONFRAMEWORKDIR]) + AC_SUBST([PYTHONFRAMEWORKPREFIX]) + AC_SUBST([PYTHONFRAMEWORKINSTALLDIR]) ++AC_SUBST([PYTHONFRAMEWORKINSTALLNAMEPREFIX]) ++AC_SUBST([RESSRCDIR]) + AC_SUBST([FRAMEWORKINSTALLFIRST]) + AC_SUBST([FRAMEWORKINSTALLLAST]) + AC_SUBST([FRAMEWORKALTINSTALLFIRST]) +@@ -526,77 +761,51 @@ + AC_SUBST([FRAMEWORKPYTHONW]) + AC_SUBST([FRAMEWORKUNIXTOOLSPREFIX]) + AC_SUBST([FRAMEWORKINSTALLAPPSPREFIX]) ++AC_SUBST([INSTALLTARGETS]) + + AC_DEFINE_UNQUOTED([_PYTHONFRAMEWORK], ["${PYTHONFRAMEWORK}"], + [framework name]) + +-# Set name for machine-dependent library files +-AC_ARG_VAR([MACHDEP], [name for machine-dependent library files]) +-AC_MSG_CHECKING([MACHDEP]) +-if test -z "$MACHDEP" +-then +- # avoid using uname for cross builds +- if test "$cross_compiling" = yes; then +- # ac_sys_system and ac_sys_release are used for setting +- # a lot of different things including 'define_xopen_source' +- # in the case statement below. +- case "$host" in +- *-*-linux-android*) +- ac_sys_system=Linux-android +- ;; +- *-*-linux*) +- ac_sys_system=Linux +- ;; +- *-*-cygwin*) +- ac_sys_system=Cygwin +- ;; +- *-*-vxworks*) +- ac_sys_system=VxWorks +- ;; +- *-*-emscripten) +- ac_sys_system=Emscripten +- ;; +- *-*-wasi) +- ac_sys_system=WASI +- ;; +- *) +- # for now, limit cross builds to known configurations +- MACHDEP="unknown" +- AC_MSG_ERROR([cross build not supported for $host]) +- esac +- ac_sys_release= +- else +- ac_sys_system=`uname -s` +- if test "$ac_sys_system" = "AIX" \ +- -o "$ac_sys_system" = "UnixWare" -o "$ac_sys_system" = "OpenUNIX"; then +- ac_sys_release=`uname -v` +- else +- ac_sys_release=`uname -r` +- fi +- fi +- ac_md_system=`echo $ac_sys_system | +- tr -d '[/ ]' | tr '[[A-Z]]' '[[a-z]]'` +- ac_md_release=`echo $ac_sys_release | +- tr -d '[/ ]' | sed 's/^[[A-Z]]\.//' | sed 's/\..*//'` +- MACHDEP="$ac_md_system$ac_md_release" +- +- case $MACHDEP in +- aix*) MACHDEP="aix";; +- linux*) MACHDEP="linux";; +- cygwin*) MACHDEP="cygwin";; +- darwin*) MACHDEP="darwin";; +- '') MACHDEP="unknown";; ++dnl quadrigraphs "@<:@" and "@:>@" produce "[" and "]" in the output ++AC_MSG_CHECKING([for --with-app-store-compliance]) ++AC_ARG_WITH( ++ [app_store_compliance], ++ [AS_HELP_STRING( ++ [--with-app-store-compliance=@<:@PATCH-FILE@:>@], ++ [Enable any patches required for compiliance with app stores. ++ Optional PATCH-FILE specifies the custom patch to apply.] ++ )],[ ++ case "$withval" in ++ yes) ++ case $ac_sys_system in ++ Darwin|iOS|tvOS|watchOS) ++ # iOS/tvOS/watchOS is able to share the macOS patch ++ APP_STORE_COMPLIANCE_PATCH="Mac/Resources/app-store-compliance.patch" ++ ;; ++ *) AC_MSG_ERROR([no default app store compliance patch available for $ac_sys_system]) ;; ++ esac ++ AC_MSG_RESULT([applying default app store compliance patch]) ++ ;; ++ *) ++ APP_STORE_COMPLIANCE_PATCH="${withval}" ++ AC_MSG_RESULT([applying custom app store compliance patch]) ++ ;; + esac +- +- if test "$ac_sys_system" = "SunOS"; then +- # For Solaris, there isn't an OS version specific macro defined +- # in most compilers, so we define one here. +- SUNOS_VERSION=`echo $ac_sys_release | sed -e 's!\.\([0-9]\)$!.0\1!g' | tr -d '.'` +- AC_DEFINE_UNQUOTED([Py_SUNOS_VERSION], [$SUNOS_VERSION], +- [The version of SunOS/Solaris as reported by `uname -r' without the dot.]) +- fi +-fi +-AC_MSG_RESULT(["$MACHDEP"]) ++ ],[ ++ case $ac_sys_system in ++ iOS|tvOS|watchOS) ++ # Always apply the compliance patch on iOS/tvOS/watchOS; we can use the macOS patch ++ APP_STORE_COMPLIANCE_PATCH="Mac/Resources/app-store-compliance.patch" ++ AC_MSG_RESULT([applying default app store compliance patch]) ++ ;; ++ *) ++ # No default app compliance patching on any other platform ++ APP_STORE_COMPLIANCE_PATCH= ++ AC_MSG_RESULT([not patching for app store compliance]) ++ ;; ++ esac ++]) ++AC_SUBST([APP_STORE_COMPLIANCE_PATCH]) + + AC_SUBST([_PYTHON_HOST_PLATFORM]) + if test "$cross_compiling" = yes; then +@@ -604,27 +813,87 @@ + *-*-linux*) + case "$host_cpu" in + arm*) +- _host_cpu=arm ++ _host_ident=arm + ;; + *) +- _host_cpu=$host_cpu ++ _host_ident=$host_cpu + esac + ;; + *-*-cygwin*) +- _host_cpu= ++ _host_ident= ++ ;; ++ *-apple-ios*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # IPHONEOS_DEPLOYMENT_TARGET is the minimum supported iOS version ++ AC_MSG_CHECKING([iOS deployment target]) ++ IPHONEOS_DEPLOYMENT_TARGET=${_host_os:3} ++ IPHONEOS_DEPLOYMENT_TARGET=${IPHONEOS_DEPLOYMENT_TARGET:=13.0} ++ AC_MSG_RESULT([$IPHONEOS_DEPLOYMENT_TARGET]) ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-arm64-iphone${_host_device} ++ ;; ++ *) ++ _host_ident=${IPHONEOS_DEPLOYMENT_TARGET}-$host_cpu-iphone${_host_device} ++ ;; ++ esac ++ ;; ++ *-apple-tvos*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # TVOS_DEPLOYMENT_TARGET is the minimum supported tvOS version ++ AC_MSG_CHECKING([tvOS deployment target]) ++ TVOS_DEPLOYMENT_TARGET=${_host_os:4} ++ TVOS_DEPLOYMENT_TARGET=${TVOS_DEPLOYMENT_TARGET:=12.0} ++ AC_MSG_RESULT([$TVOS_DEPLOYMENT_TARGET]) ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${TVOS_DEPLOYMENT_TARGET}-arm64-appletv${_host_device} ++ ;; ++ *) ++ _host_ident=${TVOS_DEPLOYMENT_TARGET}-$host_cpu-appletv${_host_device} ++ ;; ++ esac ++ ;; ++ *-apple-watchos*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # WATCHOS_DEPLOYMENT_TARGET is the minimum supported watchOS version ++ AC_MSG_CHECKING([watchOS deployment target]) ++ WATCHOS_DEPLOYMENT_TARGET=${_host_os:7} ++ WATCHOS_DEPLOYMENT_TARGET=${WATCHOS_DEPLOYMENT_TARGET:=4.0} ++ AC_MSG_RESULT([$WATCHOS_DEPLOYMENT_TARGET]) ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${WATCHOS_DEPLOYMENT_TARGET}-arm64-watch${_host_device} ++ ;; ++ *) ++ _host_ident=${WATCHOS_DEPLOYMENT_TARGET}-$host_cpu-watch${_host_device} ++ ;; ++ esac + ;; + *-*-vxworks*) +- _host_cpu=$host_cpu ++ _host_ident=$host_cpu + ;; + wasm32-*-* | wasm64-*-*) +- _host_cpu=$host_cpu ++ _host_ident=$host_cpu + ;; + *) + # for now, limit cross builds to known configurations + MACHDEP="unknown" + AC_MSG_ERROR([cross build not supported for $host]) + esac +- _PYTHON_HOST_PLATFORM="$MACHDEP${_host_cpu:+-$_host_cpu}" ++ _PYTHON_HOST_PLATFORM="$MACHDEP${_host_ident:+-$_host_ident}" + fi + + # Some systems cannot stand _XOPEN_SOURCE being defined at all; they +@@ -690,6 +959,13 @@ + define_xopen_source=no;; + Darwin/@<:@[12]@:>@@<:@0-9@:>@.*) + define_xopen_source=no;; ++ # On iOS/tvOS/watchOS, defining _POSIX_C_SOURCE also disables platform specific features. ++ iOS/*) ++ define_xopen_source=no;; ++ tvOS/*) ++ define_xopen_source=no;; ++ watchOS/*) ++ define_xopen_source=no;; + # On QNX 6.3.2, defining _XOPEN_SOURCE prevents netdb.h from + # defining NI_NUMERICHOST. + QNX/6.3.2) +@@ -748,6 +1024,12 @@ + CONFIGURE_MACOSX_DEPLOYMENT_TARGET= + EXPORT_MACOSX_DEPLOYMENT_TARGET='#' + ++# Record the value of IPHONEOS_DEPLOYMENT_TARGET / TVOS_DEPLOYMENT_TARGET / ++# WATCHOS_DEPLOYMENT_TARGET enforced by the selected host triple. ++AC_SUBST([IPHONEOS_DEPLOYMENT_TARGET]) ++AC_SUBST([TVOS_DEPLOYMENT_TARGET]) ++AC_SUBST([WATCHOS_DEPLOYMENT_TARGET]) ++ + # checks for alternative programs + + # compiler flags are generated in two sets, BASECFLAGS and OPT. OPT is just +@@ -780,6 +1062,20 @@ + ], + ) + ++dnl Add the compiler flag for the iOS/tvOS/watchOS minimum supported OS version. ++AS_CASE([$ac_sys_system], ++ [iOS], [ ++ AS_VAR_APPEND([CFLAGS], [" -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}"]) ++ AS_VAR_APPEND([LDFLAGS], [" -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}"]) ++ ],[tvOS], [ ++ AS_VAR_APPEND([CFLAGS], [" -mtvos-version-min=${TVOS_DEPLOYMENT_TARGET}"]) ++ AS_VAR_APPEND([LDFLAGS], [" -mtvos-version-min=${TVOS_DEPLOYMENT_TARGET}"]) ++ ],[watchOS], [ ++ AS_VAR_APPEND([CFLAGS], [" -mwatchos-version-min=${WATCHOS_DEPLOYMENT_TARGET}"]) ++ AS_VAR_APPEND([LDFLAGS], [" -mwatchos-version-min=${WATCHOS_DEPLOYMENT_TARGET}"]) ++ ], ++) ++ + if test "$ac_sys_system" = "Darwin" + then + dnl look for SDKROOT +@@ -1077,7 +1373,42 @@ + #elif defined(__gnu_hurd__) + i386-gnu + #elif defined(__APPLE__) ++# include "TargetConditionals.h" ++# if TARGET_OS_IOS ++# if TARGET_OS_SIMULATOR ++# if __x86_64__ ++ x86_64-iphonesimulator ++# else ++ arm64-iphonesimulator ++# endif ++# else ++ arm64-iphoneos ++# endif ++# elif TARGET_OS_TV ++# if TARGET_OS_SIMULATOR ++# if __x86_64__ ++ x86_64-appletvsimulator ++# else ++ arm64-appletvsimulator ++# endif ++# else ++ arm64-appletvos ++# endif ++# elif TARGET_OS_WATCH ++# if TARGET_OS_SIMULATOR ++# if __x86_64__ ++ x86_64-watchsimulator ++# else ++ arm64-watchsimulator ++# endif ++# else ++ arm64_32-watchos ++# endif ++# elif TARGET_OS_OSX + darwin ++# else ++# error unknown Apple platform ++# endif + #elif defined(__VXWORKS__) + vxworks + #elif defined(__wasm32__) +@@ -1119,14 +1450,24 @@ + fi + rm -f conftest.c conftest.out + ++dnl On some platforms, using a true "triplet" for MULTIARCH would be redundant. ++dnl For example, `arm64-apple-darwin` is redundant, because there isn't a ++dnl non-Apple Darwin. Including the CPU architecture can also be potentially ++dnl redundant - on macOS, for example, it's possible to do a single compile ++dnl pass that includes multiple architectures, so it would be misleading for ++dnl MULTIARCH (and thus the sysconfigdata module name) to include a single CPU ++dnl architecture. PLATFORM_TRIPLET will be a pair or single value for these ++dnl platforms. + AC_MSG_CHECKING([for multiarch]) + AS_CASE([$ac_sys_system], + [Darwin*], [MULTIARCH=""], ++ [iOS], [MULTIARCH=""], ++ [tvOS], [MULTIARCH=""], ++ [watchOS], [MULTIARCH=""], + [FreeBSD*], [MULTIARCH=""], + [MULTIARCH=$($CC --print-multiarch 2>/dev/null)] + ) + AC_SUBST([MULTIARCH]) +-AC_MSG_RESULT([$MULTIARCH]) + + if test x$PLATFORM_TRIPLET != x && test x$MULTIARCH != x; then + if test x$PLATFORM_TRIPLET != x$MULTIARCH; then +@@ -1136,6 +1477,17 @@ + MULTIARCH=$PLATFORM_TRIPLET + fi + AC_SUBST([PLATFORM_TRIPLET]) ++AC_MSG_RESULT([$MULTIARCH]) ++ ++dnl Even if we *do* include the CPU architecture in the MULTIARCH value, some ++dnl platforms don't need the CPU architecture in the SOABI tag. These platforms ++dnl will have multiple sysconfig modules (one for each CPU architecture), but ++dnl use a single "fat" binary at runtime. SOABI_PLATFORM is the component of ++dnl the PLATFORM_TRIPLET that will be used in binary module extensions. ++AS_CASE([$ac_sys_system], ++ [iOS|tvOS|watchOS], [SOABI_PLATFORM=`echo "$PLATFORM_TRIPLET" | cut -d '-' -f2`], ++ [SOABI_PLATFORM=$PLATFORM_TRIPLET] ++) + + if test x$MULTIARCH != x; then + MULTIARCH_CPPFLAGS="-DMULTIARCH=\\\"$MULTIARCH\\\"" +@@ -1166,6 +1518,12 @@ + [wasm32-unknown-emscripten/clang], [PY_SUPPORT_TIER=3], dnl WebAssembly Emscripten + [wasm32-unknown-wasi/clang], [PY_SUPPORT_TIER=3], dnl WebAssembly System Interface + [x86_64-*-freebsd*/clang], [PY_SUPPORT_TIER=3], dnl FreeBSD on AMD64 ++ [aarch64-apple-ios*-simulator/clang], [PY_SUPPORT_TIER=3], dnl iOS Simulator on arm64 ++ [aarch64-apple-ios*/clang], [PY_SUPPORT_TIER=3], dnl iOS on ARM64 ++ [aarch64-apple-tvos*-simulator/clang], [PY_SUPPORT_TIER=3], dnl tvOS Simulator on arm64 ++ [aarch64-apple-tvos*/clang], [PY_SUPPORT_TIER=3], dnl tvOS on ARM64 ++ [aarch64-apple-watchos*-simulator/clang], [PY_SUPPORT_TIER=3], dnl watchOS Simulator on arm64 ++ [arm64_32-apple-watchos*/clang], [PY_SUPPORT_TIER=3], dnl watchOS on ARM64 + [PY_SUPPORT_TIER=0] + ) + +@@ -1482,17 +1840,25 @@ + + AC_MSG_CHECKING([LDLIBRARY]) + +-# MacOSX framework builds need more magic. LDLIBRARY is the dynamic ++# Apple framework builds need more magic. LDLIBRARY is the dynamic + # library that we build, but we do not want to link against it (we + # will find it with a -framework option). For this reason there is an + # extra variable BLDLIBRARY against which Python and the extension + # modules are linked, BLDLIBRARY. This is normally the same as +-# LDLIBRARY, but empty for MacOSX framework builds. ++# LDLIBRARY, but empty for MacOSX framework builds. iOS does the same, ++# but uses a non-versioned framework layout. + if test "$enable_framework" + then +- LDLIBRARY='$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)' +- RUNSHARED=DYLD_FRAMEWORK_PATH=`pwd`${DYLD_FRAMEWORK_PATH:+:${DYLD_FRAMEWORK_PATH}} ++ case $ac_sys_system in ++ Darwin) ++ LDLIBRARY='$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)';; ++ iOS|tvOS|watchOS) ++ LDLIBRARY='$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)';; ++ *) ++ AC_MSG_ERROR([Unknown platform for framework build]);; ++ esac + BLDLIBRARY='' ++ RUNSHARED=DYLD_FRAMEWORK_PATH=`pwd`${DYLD_FRAMEWORK_PATH:+:${DYLD_FRAMEWORK_PATH}} + else + BLDLIBRARY='$(LDLIBRARY)' + fi +@@ -1504,64 +1870,69 @@ + [Defined if Python is built as a shared library.]) + case $ac_sys_system in + CYGWIN*) +- LDLIBRARY='libpython$(LDVERSION).dll.a' +- DLLLIBRARY='libpython$(LDVERSION).dll' +- ;; ++ LDLIBRARY='libpython$(LDVERSION).dll.a' ++ DLLLIBRARY='libpython$(LDVERSION).dll' ++ ;; + SunOS*) +- LDLIBRARY='libpython$(LDVERSION).so' +- BLDLIBRARY='-Wl,-R,$(LIBDIR) -L. -lpython$(LDVERSION)' +- RUNSHARED=LD_LIBRARY_PATH=`pwd`${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} +- INSTSONAME="$LDLIBRARY".$SOVERSION +- if test "$with_pydebug" != yes +- then +- PY3LIBRARY=libpython3.so +- fi +- ;; ++ LDLIBRARY='libpython$(LDVERSION).so' ++ BLDLIBRARY='-Wl,-R,$(LIBDIR) -L. -lpython$(LDVERSION)' ++ RUNSHARED=LD_LIBRARY_PATH=`pwd`${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} ++ INSTSONAME="$LDLIBRARY".$SOVERSION ++ if test "$with_pydebug" != yes ++ then ++ PY3LIBRARY=libpython3.so ++ fi ++ ;; + Linux*|GNU*|NetBSD*|FreeBSD*|DragonFly*|OpenBSD*|VxWorks*) +- LDLIBRARY='libpython$(LDVERSION).so' +- BLDLIBRARY='-L. -lpython$(LDVERSION)' +- RUNSHARED=LD_LIBRARY_PATH=`pwd`${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} +- INSTSONAME="$LDLIBRARY".$SOVERSION +- if test "$with_pydebug" != yes +- then +- PY3LIBRARY=libpython3.so +- fi +- ;; ++ LDLIBRARY='libpython$(LDVERSION).so' ++ BLDLIBRARY='-L. -lpython$(LDVERSION)' ++ RUNSHARED=LD_LIBRARY_PATH=`pwd`${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} ++ INSTSONAME="$LDLIBRARY".$SOVERSION ++ if test "$with_pydebug" != yes ++ then ++ PY3LIBRARY=libpython3.so ++ fi ++ ;; + hp*|HP*) +- case `uname -m` in +- ia64) +- LDLIBRARY='libpython$(LDVERSION).so' +- ;; +- *) +- LDLIBRARY='libpython$(LDVERSION).sl' +- ;; +- esac +- BLDLIBRARY='-Wl,+b,$(LIBDIR) -L. -lpython$(LDVERSION)' +- RUNSHARED=SHLIB_PATH=`pwd`${SHLIB_PATH:+:${SHLIB_PATH}} +- ;; ++ case `uname -m` in ++ ia64) ++ LDLIBRARY='libpython$(LDVERSION).so' ++ ;; ++ *) ++ LDLIBRARY='libpython$(LDVERSION).sl' ++ ;; ++ esac ++ BLDLIBRARY='-Wl,+b,$(LIBDIR) -L. -lpython$(LDVERSION)' ++ RUNSHARED=SHLIB_PATH=`pwd`${SHLIB_PATH:+:${SHLIB_PATH}} ++ ;; + Darwin*) +- LDLIBRARY='libpython$(LDVERSION).dylib' +- BLDLIBRARY='-L. -lpython$(LDVERSION)' +- RUNSHARED=DYLD_LIBRARY_PATH=`pwd`${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}} +- ;; ++ LDLIBRARY='libpython$(LDVERSION).dylib' ++ BLDLIBRARY='-L. -lpython$(LDVERSION)' ++ RUNSHARED=DYLD_LIBRARY_PATH=`pwd`${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}} ++ ;; ++ iOS|tvOS|watchOS) ++ LDLIBRARY='libpython$(LDVERSION).dylib' ++ ;; + AIX*) +- LDLIBRARY='libpython$(LDVERSION).so' +- RUNSHARED=LIBPATH=`pwd`${LIBPATH:+:${LIBPATH}} +- ;; ++ LDLIBRARY='libpython$(LDVERSION).so' ++ RUNSHARED=LIBPATH=`pwd`${LIBPATH:+:${LIBPATH}} ++ ;; + + esac + else # shared is disabled + PY_ENABLE_SHARED=0 + case $ac_sys_system in + CYGWIN*) +- BLDLIBRARY='$(LIBRARY)' +- LDLIBRARY='libpython$(LDVERSION).dll.a' +- ;; ++ BLDLIBRARY='$(LIBRARY)' ++ LDLIBRARY='libpython$(LDVERSION).dll.a' ++ ;; + esac + fi + ++AC_MSG_RESULT([$LDLIBRARY]) ++ + if test "$cross_compiling" = yes; then +- RUNSHARED= ++ RUNSHARED= + fi + + AC_ARG_VAR([HOSTRUNNER], [Program to run CPython for the host platform]) +@@ -1617,8 +1988,6 @@ + PYTHON_FOR_BUILD="_PYTHON_HOSTRUNNER='$HOSTRUNNER' $PYTHON_FOR_BUILD" + fi + +-AC_MSG_RESULT([$LDLIBRARY]) +- + # LIBRARY_DEPS, LINK_PYTHON_OBJS and LINK_PYTHON_DEPS variable + AS_CASE([$ac_sys_system/$ac_sys_emscripten_target], + [Emscripten/browser*], [LIBRARY_DEPS='$(PY3LIBRARY) $(WASM_STDLIB) python.html python.worker.js'], +@@ -3380,6 +3749,11 @@ + BLDSHARED="$LDSHARED" + fi + ;; ++ iOS/*|tvOS/*|watchOS/*) ++ LDSHARED='$(CC) -dynamiclib -F . -framework $(PYTHONFRAMEWORK)' ++ LDCXXSHARED='$(CXX) -dynamiclib -F . -framework $(PYTHONFRAMEWORK)' ++ BLDSHARED="$LDSHARED" ++ ;; + Emscripten*|WASI*) + LDSHARED='$(CC) -shared' + LDCXXSHARED='$(CXX) -shared';; +@@ -3500,30 +3874,34 @@ + Linux-android*) LINKFORSHARED="-pie -Xlinker -export-dynamic";; + Linux*|GNU*) LINKFORSHARED="-Xlinker -export-dynamic";; + # -u libsys_s pulls in all symbols in libsys +- Darwin/*) ++ Darwin/*|iOS/*|tvOS/*|watchOS/*) + LINKFORSHARED="$extra_undefs -framework CoreFoundation" + + # Issue #18075: the default maximum stack size (8MBytes) is too + # small for the default recursion limit. Increase the stack size + # to ensure that tests don't crash +- stack_size="1000000" # 16 MB +- if test "$with_ubsan" = "yes" +- then +- # Undefined behavior sanitizer requires an even deeper stack +- stack_size="4000000" # 64 MB +- fi ++ stack_size="1000000" # 16 MB ++ if test "$with_ubsan" = "yes" ++ then ++ # Undefined behavior sanitizer requires an even deeper stack ++ stack_size="4000000" # 64 MB ++ fi + +- LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED" ++ AC_DEFINE_UNQUOTED([THREAD_STACK_SIZE], ++ [0x$stack_size], ++ [Custom thread stack size depending on chosen sanitizer runtimes.]) + +- AC_DEFINE_UNQUOTED([THREAD_STACK_SIZE], +- [0x$stack_size], +- [Custom thread stack size depending on chosen sanitizer runtimes.]) ++ if test $ac_sys_system = "Darwin"; then ++ LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED" + +- if test "$enable_framework" +- then +- LINKFORSHARED="$LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)' ++ if test "$enable_framework"; then ++ LINKFORSHARED="$LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)' ++ fi ++ LINKFORSHARED="$LINKFORSHARED" ++ elif test "$ac_sys_system" = "iOS" -o "$ac_sys_system" = "tvOS" -o "$ac_sys_system" = "watchOS"; then ++ LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + fi +- LINKFORSHARED="$LINKFORSHARED";; ++ ;; + OpenUNIX*|UnixWare*) LINKFORSHARED="-Wl,-Bexport";; + SCO_SV*) LINKFORSHARED="-Wl,-Bexport";; + ReliantUNIX*) LINKFORSHARED="-W1 -Blargedynsym";; +@@ -3897,6 +4275,9 @@ + dnl when do we need USING_APPLE_OS_LIBFFI? + ctypes_malloc_closure=yes + ], ++ [iOS|tvOS|watchOS], [ ++ ctypes_malloc_closure=yes ++ ], + [sunos5], [AS_VAR_APPEND([LIBFFI_LIBS], [" -mimpure-text"])] + ) + AS_VAR_IF([ctypes_malloc_closure], [yes], [ +@@ -4920,28 +5301,28 @@ + # checks for library functions + AC_CHECK_FUNCS([ \ + accept4 alarm bind_textdomain_codeset chmod chown clock close_range confstr \ +- copy_file_range ctermid dup dup3 execv explicit_bzero explicit_memset \ ++ copy_file_range ctermid dup dup3 explicit_bzero explicit_memset \ + faccessat fchmod fchmodat fchown fchownat fdopendir fdwalk fexecve \ +- fork fork1 fpathconf fstatat ftime ftruncate futimens futimes futimesat \ +- gai_strerror getegid getentropy geteuid getgid getgrgid getgrgid_r \ +- getgrnam_r getgrouplist getgroups gethostname getitimer getloadavg getlogin \ ++ fpathconf fstatat ftime ftruncate futimens futimes futimesat \ ++ gai_strerror getegid geteuid getgid getgrgid getgrgid_r \ ++ getgrnam_r getgrouplist gethostname getitimer getloadavg getlogin \ + getpeername getpgid getpid getppid getpriority _getpty \ + getpwent getpwnam_r getpwuid getpwuid_r getresgid getresuid getrusage getsid getspent \ + getspnam getuid getwd if_nameindex initgroups kill killpg lchown linkat \ + lockf lstat lutimes madvise mbrtowc memrchr mkdirat mkfifo mkfifoat \ + mknod mknodat mktime mmap mremap nice openat opendir pathconf pause pipe \ +- pipe2 plock poll posix_fadvise posix_fallocate posix_spawn posix_spawnp \ ++ pipe2 plock poll posix_fadvise posix_fallocate \ + pread preadv preadv2 pthread_condattr_setclock pthread_init pthread_kill \ + pwrite pwritev pwritev2 readlink readlinkat readv realpath renameat \ + rtpSpawn sched_get_priority_max sched_rr_get_interval sched_setaffinity \ + sched_setparam sched_setscheduler sem_clockwait sem_getvalue sem_open \ + sem_timedwait sem_unlink sendfile setegid seteuid setgid sethostname \ + setitimer setlocale setpgid setpgrp setpriority setregid setresgid \ +- setresuid setreuid setsid setuid setvbuf shutdown sigaction sigaltstack \ ++ setresuid setreuid setsid setuid setvbuf shutdown sigaction \ + sigfillset siginterrupt sigpending sigrelse sigtimedwait sigwait \ + sigwaitinfo snprintf splice strftime strlcpy strsignal symlinkat sync \ +- sysconf system tcgetpgrp tcsetpgrp tempnam timegm times tmpfile \ +- tmpnam tmpnam_r truncate ttyname_r umask uname unlinkat utimensat utimes vfork \ ++ sysconf tcgetpgrp tcsetpgrp tempnam timegm times tmpfile \ ++ tmpnam tmpnam_r truncate ttyname umask uname unlinkat utimensat utimes vfork \ + wait wait3 wait4 waitid waitpid wcscoll wcsftime wcsxfrm wmemcmp writev \ + ]) + +@@ -4952,6 +5333,22 @@ + AC_CHECK_FUNCS([lchmod]) + fi + ++# iOS/tvOS/watchOS define some system methods that can be linked (so they are ++# found by configure), but either raise a compilation error (because the ++# 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([ getentropy getgroups system ]) ++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 ++ AC_CHECK_FUNCS([ \ ++ execv fork fork1 posix_spawn posix_spawnp \ ++ sigaltstack \ ++ ]) ++fi ++ + AC_CHECK_DECL([dirfd], + [AC_DEFINE([HAVE_DIRFD], [1], + [Define if you have the 'dirfd' function or macro.])], +@@ -5193,20 +5590,22 @@ + ]) + + # check for openpty, login_tty, and forkpty +- +-AC_CHECK_FUNCS([openpty], [], +- [AC_CHECK_LIB([util], [openpty], +- [AC_DEFINE([HAVE_OPENPTY]) LIBS="$LIBS -lutil"], +- [AC_CHECK_LIB([bsd], [openpty], +- [AC_DEFINE([HAVE_OPENPTY]) LIBS="$LIBS -lbsd"])])]) +-AC_SEARCH_LIBS([login_tty], [util], +- [AC_DEFINE([HAVE_LOGIN_TTY], [1], [Define to 1 if you have the `login_tty' function.])] +-) +-AC_CHECK_FUNCS([forkpty], [], +- [AC_CHECK_LIB([util], [forkpty], +- [AC_DEFINE([HAVE_FORKPTY]) LIBS="$LIBS -lutil"], +- [AC_CHECK_LIB([bsd], [forkpty], +- [AC_DEFINE([HAVE_FORKPTY]) LIBS="$LIBS -lbsd"])])]) ++# tvOS/watchOS have functions for tty, but can't use them ++if test "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "watchOS" ; then ++ AC_CHECK_FUNCS([openpty], [], ++ [AC_CHECK_LIB([util], [openpty], ++ [AC_DEFINE([HAVE_OPENPTY]) LIBS="$LIBS -lutil"], ++ [AC_CHECK_LIB([bsd], [openpty], ++ [AC_DEFINE([HAVE_OPENPTY]) LIBS="$LIBS -lbsd"])])]) ++ AC_SEARCH_LIBS([login_tty], [util], ++ [AC_DEFINE([HAVE_LOGIN_TTY], [1], [Define to 1 if you have the `login_tty' function.])] ++ ) ++ AC_CHECK_FUNCS([forkpty], [], ++ [AC_CHECK_LIB([util], [forkpty], ++ [AC_DEFINE([HAVE_FORKPTY]) LIBS="$LIBS -lutil"], ++ [AC_CHECK_LIB([bsd], [forkpty], ++ [AC_DEFINE([HAVE_FORKPTY]) LIBS="$LIBS -lbsd"])])]) ++fi + + # check for long file support functions + AC_CHECK_FUNCS([fseek64 fseeko fstatvfs ftell64 ftello statvfs]) +@@ -5289,11 +5688,17 @@ + ]) + ]) + +-AC_CHECK_FUNCS([clock_settime], [], [ +- AC_CHECK_LIB([rt], [clock_settime], [ +- AC_DEFINE([HAVE_CLOCK_SETTIME], [1]) +- ]) +-]) ++# On iOS, tvOS and watchOS, clock_settime can be linked (so it is found by ++# configure), but when used in an unprivileged process, it crashes rather than ++# returning an error. Force the symbol off. ++if test "$ac_sys_system" != "iOS" -a "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "watchOS" ++then ++ AC_CHECK_FUNCS([clock_settime], [], [ ++ AC_CHECK_LIB([rt], [clock_settime], [ ++ AC_DEFINE([HAVE_CLOCK_SETTIME], [1]) ++ ]) ++ ]) ++fi + + AC_CHECK_FUNCS([clock_nanosleep], [], [ + AC_CHECK_LIB([rt], [clock_nanosleep], [ +@@ -5439,7 +5844,9 @@ + [ac_cv_buggy_getaddrinfo=no], + [ac_cv_buggy_getaddrinfo=yes], + [ +-if test "${enable_ipv6+set}" = set; then ++if test "$ac_sys_system" = "Linux-android" -o "$ac_sys_system" = "iOS" -o "$ac_sys_system" = "tvOS" -o "$ac_sys_system" = "watchOS"; then ++ ac_cv_buggy_getaddrinfo="no" ++elif test "${enable_ipv6+set}" = set; then + ac_cv_buggy_getaddrinfo="no -- configured with --(en|dis)able-ipv6" + else + ac_cv_buggy_getaddrinfo=yes +@@ -5992,14 +6399,14 @@ + AC_MSG_CHECKING([ABIFLAGS]) + AC_MSG_RESULT([$ABIFLAGS]) + AC_MSG_CHECKING([SOABI]) +-SOABI='cpython-'`echo $VERSION | tr -d .`${ABIFLAGS}${PLATFORM_TRIPLET:+-$PLATFORM_TRIPLET} ++SOABI='cpython-'`echo $VERSION | tr -d .`${ABIFLAGS}${SOABI_PLATFORM:+-$SOABI_PLATFORM} + AC_MSG_RESULT([$SOABI]) + + # Release and debug (Py_DEBUG) ABI are compatible, but not Py_TRACE_REFS ABI + if test "$Py_DEBUG" = 'true' -a "$with_trace_refs" != "yes"; then + # Similar to SOABI but remove "d" flag from ABIFLAGS + AC_SUBST([ALT_SOABI]) +- ALT_SOABI='cpython-'`echo $VERSION | tr -d .``echo $ABIFLAGS | tr -d d`${PLATFORM_TRIPLET:+-$PLATFORM_TRIPLET} ++ ALT_SOABI='cpython-'`echo $VERSION | tr -d .``echo $ABIFLAGS | tr -d d`${SOABI_PLATFORM:+-$SOABI_PLATFORM} + AC_DEFINE_UNQUOTED([ALT_SOABI], ["${ALT_SOABI}"], + [Alternative SOABI used in debug build to load C extensions built in release mode]) + fi +@@ -6648,28 +7055,35 @@ + AC_MSG_NOTICE([checking for device files]) + + dnl NOTE: Inform user how to proceed with files when cross compiling. +-if test "x$cross_compiling" = xyes; then +- if test "${ac_cv_file__dev_ptmx+set}" != set; then +- AC_MSG_CHECKING([for /dev/ptmx]) +- AC_MSG_RESULT([not set]) +- AC_MSG_ERROR([set ac_cv_file__dev_ptmx to yes/no in your CONFIG_SITE file when cross compiling]) +- fi +- if test "${ac_cv_file__dev_ptc+set}" != set; then +- AC_MSG_CHECKING([for /dev/ptc]) +- AC_MSG_RESULT([not set]) +- AC_MSG_ERROR([set ac_cv_file__dev_ptc to yes/no in your CONFIG_SITE file when cross compiling]) ++dnl iOS cross-compile builds are predictable; they won't ever ++dnl have /dev/ptmx or /dev/ptc, so we can set them explicitly. ++if test "$ac_sys_system" = "iOS" -o "$ac_sys_system" = "tvOS" -o "$ac_sys_system" = "watchOS" ; then ++ ac_cv_file__dev_ptmx=no ++ ac_cv_file__dev_ptc=no ++else ++ if test "x$cross_compiling" = xyes; then ++ if test "${ac_cv_file__dev_ptmx+set}" != set; then ++ AC_MSG_CHECKING([for /dev/ptmx]) ++ AC_MSG_RESULT([not set]) ++ AC_MSG_ERROR([set ac_cv_file__dev_ptmx to yes/no in your CONFIG_SITE file when cross compiling]) ++ fi ++ if test "${ac_cv_file__dev_ptc+set}" != set; then ++ AC_MSG_CHECKING([for /dev/ptc]) ++ AC_MSG_RESULT([not set]) ++ AC_MSG_ERROR([set ac_cv_file__dev_ptc to yes/no in your CONFIG_SITE file when cross compiling]) ++ fi + fi +-fi + +-AC_CHECK_FILE([/dev/ptmx], [], []) +-if test "x$ac_cv_file__dev_ptmx" = xyes; then +- AC_DEFINE([HAVE_DEV_PTMX], [1], +- [Define to 1 if you have the /dev/ptmx device file.]) +-fi +-AC_CHECK_FILE([/dev/ptc], [], []) +-if test "x$ac_cv_file__dev_ptc" = xyes; then +- AC_DEFINE([HAVE_DEV_PTC], [1], +- [Define to 1 if you have the /dev/ptc device file.]) ++ AC_CHECK_FILE([/dev/ptmx], [], []) ++ if test "x$ac_cv_file__dev_ptmx" = xyes; then ++ AC_DEFINE([HAVE_DEV_PTMX], [1], ++ [Define to 1 if you have the /dev/ptmx device file.]) ++ fi ++ AC_CHECK_FILE([/dev/ptc], [], []) ++ if test "x$ac_cv_file__dev_ptc" = xyes; then ++ AC_DEFINE([HAVE_DEV_PTC], [1], ++ [Define to 1 if you have the /dev/ptc device file.]) ++ fi + fi + + if test $ac_sys_system = Darwin +@@ -6941,6 +7355,7 @@ + AS_CASE([$ac_sys_system], + [Emscripten], [with_ensurepip=no], + [WASI], [with_ensurepip=no], ++ [iOS|tvOS|watchOS], [with_ensurepip=no], + [with_ensurepip=upgrade] + ) + ]) +@@ -7283,6 +7698,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])], ++ [iOS|tvOS|watchOS], [ ++ dnl subprocess and multiprocessing are not supported (no fork syscall). ++ dnl curses and tkinter user interface are not available. ++ dnl gdbm and nis aren't available ++ dnl Stub implementations are provided for pwd, grp etc APIs ++ PY_STDLIB_MOD_SET_NA( ++ [_curses], ++ [_curses_panel], ++ [_gdbm], ++ [_multiprocessing], ++ [_posixshmem], ++ [_posixsubprocess], ++ [_scproxy], ++ [_tkinter], ++ [grp], ++ [nis], ++ [readline], ++ [pwd], ++ [spwd], ++ [syslog], ++ ) ++ ], + [CYGWIN*], [PY_STDLIB_MOD_SET_NA([_scproxy], [nis])], + [QNX*], [PY_STDLIB_MOD_SET_NA([_scproxy], [nis])], + [FreeBSD*], [PY_STDLIB_MOD_SET_NA([_scproxy], [spwd])], +--- /dev/null ++++ b/iOS/Resources/bin/arm64-apple-ios-ar +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@" +--- /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} "$@" +--- /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} "$@" +--- /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 "$@" +--- /dev/null ++++ b/iOS/Resources/bin/arm64-apple-ios-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" diff --git a/darwin/ios_patches/3.13/Python.patch b/darwin/ios_patches/3.13/Python.patch new file mode 100644 index 0000000..dc94847 --- /dev/null +++ b/darwin/ios_patches/3.13/Python.patch @@ -0,0 +1,8054 @@ +diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml +index 6347ffb3e11..19259c069d7 100644 +--- a/.pre-commit-config.yaml ++++ b/.pre-commit-config.yaml +@@ -2,6 +2,10 @@ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.13.2 + hooks: ++ - id: ruff-check ++ name: Run Ruff (lint) on Apple/ ++ args: [--exit-non-zero-on-fix, --config=Apple/.ruff.toml] ++ files: ^Apple/ + - id: ruff-check + name: Run Ruff (lint) on Doc/ + args: [--exit-non-zero-on-fix] +@@ -26,6 +30,10 @@ + name: Run Ruff (lint) on Tools/wasm/ + args: [--exit-non-zero-on-fix, --config=Tools/wasm/.ruff.toml] + files: ^Tools/wasm/ ++ - id: ruff-format ++ name: Run Ruff (format) on Apple/ ++ args: [--exit-non-zero-on-fix, --config=Apple/.ruff.toml] ++ files: ^Apple + - id: ruff-format + name: Run Ruff (format) on Doc/ + args: [--check] +--- /dev/null ++++ b/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 ++] +--- /dev/null ++++ b/Apple/__main__.py +@@ -0,0 +1,1111 @@ ++#!/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 ++ ++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 ++ ++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", ++ }, ++ }, ++ "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", ++ }, ++ }, ++} ++ ++ ++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 host triples are the platform definitions used as input to configure ++ (e.g., "arm64-apple-ios-simulator"). ++ """ ++ triples = [] ++ for slice_name, slice_parts in HOSTS[platform].items(): ++ triples.extend(list(slice_parts)) ++ return triples ++ ++ ++def clean(context: argparse.Namespace, target: str = "all") -> None: ++ """The implementation of the "clean" command.""" ++ # If we're explicitly targeting the build, there's no platform or ++ # distribution artefacts. If we're cleaning tests, we keep all built ++ # artefacts. Otherwise, the built artefacts must be dirty, so we remove ++ # them. ++ if target not in {"build", "test"}: ++ paths = ["dist", context.platform] + list(HOSTS[context.platform]) ++ else: ++ paths = [] ++ ++ if target in {"all", "build"}: ++ paths.append("build") ++ ++ if target in {"all", "hosts"}: ++ paths.extend(all_host_triples(context.platform)) ++ elif target not in {"build", "test", "package"}: ++ paths.append(target) ++ ++ if target in {"all", "hosts", "test"}: ++ paths.extend([ ++ path.name ++ for path in CROSS_BUILD_DIR.glob(f"{context.platform}-testbed.*") ++ ]) ++ ++ for path in paths: ++ delete_path(path) ++ ++ ++def build_python_path() -> Path: ++ """The path to the build Python binary.""" ++ build_dir = subdir("build") ++ binary = build_dir / "python" ++ if not binary.is_file(): ++ binary = binary.with_suffix(".exe") ++ if not binary.is_file(): ++ raise FileNotFoundError( ++ f"Unable to find `python(.exe)` in {build_dir}" ++ ) ++ ++ return binary ++ ++ ++@contextmanager ++def group(text: str): ++ """A context manager that outputs a log marker around a section of a build. ++ ++ If running in a GitHub Actions environment, the GitHub syntax for ++ collapsible log sections is used. ++ """ ++ if "GITHUB_ACTIONS" in os.environ: ++ print(f"::group::{text}") ++ else: ++ print(f"===== {text} " + "=" * (70 - len(text))) ++ ++ yield ++ ++ if "GITHUB_ACTIONS" in os.environ: ++ print("::endgroup::") ++ else: ++ print() ++ ++ ++@contextmanager ++def cwd(subdir: Path): ++ """A context manager that sets the current working directory.""" ++ orig = os.getcwd() ++ os.chdir(subdir) ++ yield ++ os.chdir(orig) ++ ++ ++def configure_build_python(context: argparse.Namespace) -> None: ++ """The implementation of the "configure-build" command.""" ++ if context.clean: ++ clean(context, "build") ++ ++ with ( ++ group("Configuring build Python"), ++ cwd(subdir("build", create=True)), ++ ): ++ command = [relpath(PYTHON_DIR / "configure")] ++ if context.args: ++ command.extend(context.args) ++ run(command) ++ ++ ++def make_build_python(context: argparse.Namespace) -> None: ++ """The implementation of the "make-build" command.""" ++ with ( ++ group("Compiling build Python"), ++ cwd(subdir("build")), ++ ): ++ run(["make", "-j", str(os.cpu_count())]) ++ ++ ++def apple_target(host: str) -> str: ++ """Return the Apple platform identifier for a given host triple.""" ++ for _, platform_slices in HOSTS.items(): ++ for slice_name, slice_parts in platform_slices.items(): ++ for host_triple, multiarch in slice_parts.items(): ++ if host == host_triple: ++ return ".".join(multiarch.split("-")[::-1]) ++ ++ raise KeyError(host) ++ ++ ++def apple_multiarch(host: str) -> str: ++ """Return the multiarch descriptor for a given host triple.""" ++ for _, platform_slices in HOSTS.items(): ++ for slice_name, slice_parts in platform_slices.items(): ++ for host_triple, multiarch in slice_parts.items(): ++ if host == host_triple: ++ return multiarch ++ ++ raise KeyError(host) ++ ++ ++def unpack_deps( ++ platform: str, ++ host: str, ++ prefix_dir: Path, ++ cache_dir: Path, ++) -> None: ++ """Unpack binary dependencies into a provided directory. ++ ++ Downloads binaries if they aren't already present. Downloads will be stored ++ in provided cache directory. ++ ++ 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.18-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() ++ unpack_deps(context.platform, host, prefix_dir, context.cache_dir) ++ else: ++ print("Dependencies already installed") ++ ++ with ( ++ group(f"Configuring host Python ({host})"), ++ cwd(host_dir), ++ ): ++ command = [ ++ # Basic cross-compiling configuration ++ relpath(PYTHON_DIR / "configure"), ++ f"--host={host}", ++ f"--build={sysconfig.get_config_var('BUILD_GNU_TYPE')}", ++ f"--with-build-python={build_python_path()}", ++ "--with-system-libmpdec", ++ "--enable-ipv6", ++ "--enable-framework", ++ # Dependent libraries. ++ f"--with-openssl={prefix_dir}", ++ f"LIBLZMA_CFLAGS=-I{prefix_dir}/include", ++ f"LIBLZMA_LIBS=-L{prefix_dir}/lib -llzma", ++ f"LIBFFI_CFLAGS=-I{prefix_dir}/include", ++ f"LIBFFI_LIBS=-L{prefix_dir}/lib -lffi", ++ f"LIBMPDEC_CFLAGS=-I{prefix_dir}/include", ++ f"LIBMPDEC_LIBS=-L{prefix_dir}/lib -lmpdec", ++ f"LIBZSTD_CFLAGS=-I{prefix_dir}/include", ++ f"LIBZSTD_LIBS=-L{prefix_dir}/lib -lzstd", ++ ] ++ ++ if context.args: ++ command.extend(context.args) ++ run(command, host=host) ++ ++ ++def make_host_python( ++ context: argparse.Namespace, ++ host: str | None = None, ++) -> None: ++ """The implementation of the "make-host" command.""" ++ if host is None: ++ host = context.host ++ ++ with ( ++ group(f"Compiling host Python ({host})"), ++ cwd(subdir(host)), ++ ): ++ run(["make", "-j", str(os.cpu_count())], host=host) ++ run(["make", "install"], host=host) ++ ++ ++def framework_path(host_triple: str, multiarch: str) -> Path: ++ """The path to a built single-architecture framework product. ++ ++ :param host_triple: The host triple (e.g., arm64-apple-ios-simulator) ++ :param multiarch: The multiarch identifier (e.g., arm64-simulator) ++ """ ++ return ( ++ CROSS_BUILD_DIR ++ / f"{host_triple}/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"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"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 / "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) ++ ++ # 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", ++ ]) ++ ++ # Build the final archive ++ archive_name = ( ++ CROSS_BUILD_DIR ++ / "dist" ++ / f"python-{version}-{context.platform}-XCframework" ++ ) ++ ++ print() ++ print("Create package archive...") ++ shutil.make_archive( ++ str(CROSS_BUILD_DIR / archive_name), ++ format="gztar", ++ root_dir=CROSS_BUILD_DIR / context.platform, ++ base_dir=".", ++ ) ++ print() ++ print(f"{archive_name.relative_to(PYTHON_DIR)}.tar.gz created.") ++ ++ ++def build(context: argparse.Namespace, host: str | None = None) -> None: ++ """The implementation of the "build" command.""" ++ if host is None: ++ host = context.host ++ ++ if context.clean: ++ clean(context, host) ++ ++ if host in {"all", "build"}: ++ for step in [ ++ configure_build_python, ++ make_build_python, ++ ]: ++ step(context) ++ ++ if host == "build": ++ hosts = [] ++ elif host in {"all", "hosts"}: ++ hosts = all_host_triples(context.platform) ++ else: ++ hosts = [host] ++ ++ for step_host in hosts: ++ for step in [ ++ configure_host_python, ++ make_host_python, ++ ]: ++ step(context, host=step_host) ++ ++ if host in {"all", "hosts"}: ++ package(context) ++ ++ ++def test(context: argparse.Namespace, host: str | None = None) -> None: # 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"Apple/{context.platform}" ++ / f"Frameworks/{apple_multiarch(host)}" ++ ) ++ ++ run([ ++ sys.executable, ++ "Apple/testbed", ++ "clone", ++ "--platform", ++ context.platform, ++ "--framework", ++ framework_path, ++ testbed_dir, ++ ]) ++ ++ run( ++ [ ++ sys.executable, ++ testbed_dir, ++ "run", ++ "--verbose", ++ ] ++ + ( ++ ["--simulator", str(context.simulator)] ++ if context.simulator ++ else [] ++ ) ++ + [ ++ "--", ++ "test", ++ f"--{context.ci_mode}-ci", ++ "--single-process", ++ "--no-randomize", ++ # Timeout handling requires subprocesses; explicitly setting ++ # the timeout to -1 disables the faulthandler. ++ "--timeout=-1", ++ # Adding Python options requires the use of a subprocess to ++ # start a new Python interpreter. ++ "--dont-add-python-opts", ++ ] ++ ) ++ ++ ++def 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" ++ ) ++ subcommands.add_parser( ++ "make-build", help="Run `make` for the build Python" ++ ) ++ configure_host = subcommands.add_parser( ++ "configure-host", ++ help="Run `configure` for a specific platform and target", ++ ) ++ make_host = subcommands.add_parser( ++ "make-host", ++ help="Run `make` for a specific platform and target", ++ ) ++ package = subcommands.add_parser( ++ "package", ++ help="Create a release package for the platform", ++ ) ++ build = subcommands.add_parser( ++ "build", ++ help="Build all platform targets and create the XCframework", ++ ) ++ test = subcommands.add_parser( ++ "test", ++ help="Run the testbed for a specific platform", ++ ) ++ ci = subcommands.add_parser( ++ "ci", ++ help="Run build, package, and test", ++ ) ++ ++ # platform argument ++ for cmd in [clean, configure_host, make_host, package, build, test, ci]: ++ cmd.add_argument( ++ "platform", ++ choices=HOSTS.keys(), ++ help="The target platform to build", ++ ) ++ ++ # host triple argument ++ for cmd in [configure_host, make_host]: ++ cmd.add_argument( ++ "host", ++ help="The host triple to build (e.g., arm64-apple-ios-simulator)", ++ ) ++ # optional host triple argument ++ for cmd in [clean, build, test]: ++ cmd.add_argument( ++ "host", ++ nargs="?", ++ default="all", ++ help=( ++ "The host triple to build (e.g., arm64-apple-ios-simulator), " ++ "or 'build' for just the build platform, or 'hosts' for all " ++ "host platforms, or 'all' for the build platform and all " ++ "hosts. Defaults to 'all'" ++ ), ++ ) ++ ++ # --clean option ++ for cmd in [configure_build, configure_host, build, package, test, ci]: ++ cmd.add_argument( ++ "--clean", ++ action="store_true", ++ default=False, ++ dest="clean", ++ help="Delete the relevant build directories first", ++ ) ++ ++ # --cache-dir option ++ for cmd in [configure_host, build, ci]: ++ cmd.add_argument( ++ "--cache-dir", ++ default="./cross-build/downloads", ++ help="The directory to store cached downloads.", ++ ) ++ ++ # --simulator option ++ for cmd in [test, ci]: ++ cmd.add_argument( ++ "--simulator", ++ help=( ++ "The name of the simulator to use (eg: 'iPhone 16e'). " ++ "Defaults to the most recently released 'entry level' " ++ "iPhone device. Device architecture and OS version can also " ++ "be specified; e.g., " ++ "`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would " ++ "run on an ARM64 iPhone 16 Pro simulator running iOS 26.0." ++ ), ++ ) ++ 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() ++ 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() +--- /dev/null ++++ b/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 `Apple` subfolder of the Python repository acts as a build script that ++can be used to coordinate the compilation of a complete iOS XCframework. To use ++it, run:: ++ ++ python Apple build iOS ++ ++This will: ++ ++* Configure and compile a version of Python to run on the build machine ++* Download pre-compiled binary dependencies for each platform ++* Configure and build a `Python.framework` for each required architecture and ++ iOS SDK ++* Merge the multiple `Python.framework` folders into a single `Python.xcframework` ++* Produce a `.tar.gz` archive in the `cross-build/dist` folder containing ++ the `Python.xcframework`, plus a copy of the Testbed app pre-configured to ++ use the XCframework. ++ ++The `Apple` build script has other entry points that will perform the ++individual parts of the overall `build` target, plus targets to test the ++build, clean the `cross-build` folder of iOS build products, and perform a ++complete "build and test" CI run. The `--clean` flag can also be used on ++individual commands to ensure that a stale build product are removed before ++building. ++ ++### Building a single-architecture framework ++ ++If you're using the `Apple` build script, you won't need to build ++individual frameworks. However, if you do need to manually configure an iOS ++Python build for a single framework, the following options are available. ++ ++#### iOS specific arguments to configure ++ ++* `--enable-framework[=DIR]` ++ ++ This argument specifies the location where the Python.framework will be ++ installed. If `DIR` is not specified, the framework will be installed into ++ a subdirectory of the `iOS/Frameworks` folder. ++ ++ This argument *must* be provided when configuring iOS builds. iOS does not ++ support non-framework builds. ++ ++* `--with-framework-name=NAME` ++ ++ Specify the name for the Python framework; defaults to `Python`. ++ ++ > [!NOTE] ++ > Unless you know what you're doing, changing the name of the Python ++ > framework on iOS is not advised. If you use this option, you won't be able ++ > to run the `Apple` build script without making significant manual ++ > alterations, and you won't be able to use any binary packages unless you ++ > compile them yourself using your own framework name. ++ ++#### Building Python for iOS ++ ++The Python build system will create a `Python.framework` that supports a ++*single* ABI with a *single* architecture. Unlike macOS, iOS does not allow a ++framework to contain non-library content, so the iOS build will produce a ++`bin` and `lib` folder in the same output folder as `Python.framework`. ++The `lib` folder will be needed at runtime to support the Python library. ++ ++If you want to use Python in a real iOS project, you need to produce multiple ++`Python.framework` builds, one for each ABI and architecture. iOS builds of ++Python *must* be constructed as framework builds. To support this, you must ++provide the `--enable-framework` flag when configuring the build. The build ++also requires the use of cross-compilation. The minimal commands for building ++Python for the ARM64 iOS simulator will look something like: ++``` ++export PATH="$(pwd)/Apple/iOS/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" ++./configure \ ++ --enable-framework \ ++ --host=arm64-apple-ios-simulator \ ++ --build=arm64-apple-darwin \ ++ --with-build-python=/path/to/python.exe ++make ++make install ++``` ++ ++In this invocation: ++ ++* `Apple/iOS/Resources/bin` has been added to the path, providing some shims for the ++ compilers and linkers needed by the build. Xcode requires the use of `xcrun` ++ to invoke compiler tooling. However, if `xcrun` is pre-evaluated and the ++ result passed to `configure`, these results can embed user- and ++ version-specific paths into the sysconfig data, which limits the portability ++ of the compiled Python. Alternatively, if `xcrun` is used *as* the compiler, ++ it requires that compiler variables like `CC` include spaces, which can ++ cause significant problems with many C configuration systems which assume that ++ `CC` will be a single executable. ++ ++ To work around this problem, the `Apple/iOS/Resources/bin` folder contains some ++ wrapper scripts that present as simple compilers and linkers, but wrap ++ underlying calls to `xcrun`. This allows configure to use a `CC` ++ definition without spaces, and without user- or version-specific paths, while ++ retaining the ability to adapt to the local Xcode install. These scripts are ++ included in the `bin` directory of an iOS install. ++ ++ These scripts will, by default, use the currently active Xcode installation. ++ If you want to use a different Xcode installation, you can use ++ `xcode-select` to set a new default Xcode globally, or you can use the ++ `DEVELOPER_DIR` environment variable to specify an Xcode install. The ++ scripts will use the default `iphoneos`/`iphonesimulator` SDK version for ++ the select Xcode install; if you want to use a different SDK, you can set the ++ `IOS_SDK_VERSION` environment variable. (e.g, setting ++ `IOS_SDK_VERSION=17.1` would cause the scripts to use the `iphoneos17.1` ++ and `iphonesimulator17.1` SDKs, regardless of the Xcode default.) ++ ++ The path has also been cleared of any user customizations. A common source of ++ bugs is for tools like Homebrew to accidentally leak macOS binaries into an iOS ++ build. Resetting the path to a known "bare bones" value is the easiest way to ++ avoid these problems. ++ ++* `--host` is the architecture and ABI that you want to build, in GNU compiler ++ triple format. This will be one of: ++ ++ - `arm64-apple-ios` for ARM64 iOS devices. ++ - `arm64-apple-ios-simulator` for the iOS simulator running on Apple ++ Silicon devices. ++ - `x86_64-apple-ios-simulator` for the iOS simulator running on Intel ++ devices. ++ ++* `--build` is the GNU compiler triple for the machine that will be running ++ the compiler. This is one of: ++ ++ - `arm64-apple-darwin` for Apple Silicon devices. ++ - `x86_64-apple-darwin` for Intel devices. ++ ++* `/path/to/python.exe` is the path to a Python binary on the machine that ++ will be running the compiler. This is needed because the Python compilation ++ process involves running some Python code. On a normal desktop build of ++ Python, you can compile a python interpreter and then use that interpreter to ++ run Python code. However, the binaries produced for iOS won't run on macOS, so ++ you need to provide an external Python interpreter. This interpreter must be ++ the same version as the Python that is being compiled. To be completely safe, ++ this should be the *exact* same commit hash. However, the longer a Python ++ release has been stable, the more likely it is that this constraint can be ++ relaxed - the same micro version will often be sufficient. ++ ++* The `install` target for iOS builds is slightly different to other ++ platforms. On most platforms, `make install` will install the build into ++ the final runtime location. This won't be the case for iOS, as the final ++ runtime location will be on a physical device. ++ ++ However, you still need to run the `install` target for iOS builds, as it ++ performs some final framework assembly steps. The location specified with ++ `--enable-framework` will be the location where `make install` will ++ assemble the complete iOS framework. This completed framework can then ++ be copied and relocated as required. ++ ++For a full CPython build, you also need to specify the paths to iOS builds of ++the binary libraries that CPython depends on (such as XZ, LibFFI and OpenSSL). ++This can be done by defining library specific environment variables (such as ++`LIBLZMA_CFLAGS`, `LIBLZMA_LIBS`), and the `--with-openssl` configure ++option. Versions of these libraries pre-compiled for iOS can be found in [this ++repository](https://github.com/beeware/cpython-apple-source-deps/releases). ++LibFFI is especially important, as many parts of the standard library ++(including the `platform`, `sysconfig` and `webbrowser` modules) require ++the use of the `ctypes` module at runtime. ++ ++By default, Python will be compiled with an iOS deployment target (i.e., the ++minimum supported iOS version) of 13.0. To specify a different deployment ++target, provide the version number as part of the `--host` argument - for ++example, `--host=arm64-apple-ios15.4-simulator` would compile an ARM64 ++simulator build with a deployment target of 15.4. ++ ++## Testing Python on iOS ++ ++### Testing a multi-architecture framework ++ ++Once you have a built an XCframework, you can test that framework by running: ++ ++ $ python Apple test iOS ++ ++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 `Apple/testbed` folder that contains an Xcode project that is able to run ++the Python test suite on Apple platforms. This project converts the Python test ++suite into a single test case in Xcode's XCTest framework. The single XCTest ++passes if the test suite passes. ++ ++To run the test suite, configure a Python build for an iOS simulator (i.e., ++`--host=arm64-apple-ios-simulator` or `--host=x86_64-apple-ios-simulator` ++), specifying a framework build (i.e. `--enable-framework`). Ensure that your ++`PATH` has been configured to include the `Apple/iOS/Resources/bin` folder and ++exclude any non-iOS tools, then run: ++``` ++make all ++make install ++make testios ++``` ++ ++This will: ++ ++* Build an iOS framework for your chosen architecture; ++* Finalize the single-platform framework; ++* Make a clean copy of the testbed project; ++* Install the Python iOS framework into the copy of the testbed project; and ++* Run the test suite on an "entry-level device" simulator (i.e., an iPhone SE, ++ iPhone 16e, or a similar). ++ ++On success, the test suite will exit and report successful completion of the ++test suite. On a 2022 M1 MacBook Pro, the test suite takes approximately 15 ++minutes to run; a couple of extra minutes is required to compile the testbed ++project, and then boot and prepare the iOS simulator. ++ ++### Debugging test failures ++ ++Running `python Apple test iOS` generates a standalone version of the ++`Apple/testbed` project, and runs the full test suite. It does this using ++`Apple/testbed` itself - the folder is an executable module that can be used ++to create and run a clone of the testbed project. The standalone version of the ++testbed will be created in a directory named ++`cross-build/iOS-testbed.`. ++ ++You can generate your own standalone testbed instance by running: ++``` ++python cross-build/iOS/testbed clone my-testbed ++``` ++ ++In this invocation, `my-testbed` is the name of the folder for the new ++testbed clone. ++ ++If you've built your own XCframework, or you only want to test a single architecture, ++you can construct a standalone testbed instance by running: ++``` ++python Apple/testbed clone --platform iOS --framework my-testbed ++``` ++ ++The framework path can be the path path to a `Python.xcframework`, or the ++path to a folder that contains a single-platform `Python.framework`. ++ ++You can then use the `my-testbed` folder to run the Python test suite, ++passing in any command line arguments you may require. For example, if you're ++trying to diagnose a failure in the `os` module, you might run: ++``` ++python my-testbed run -- test -W test_os ++``` ++ ++This is the equivalent of running `python -m test -W test_os` on a desktop ++Python build. Any arguments after the `--` will be passed to testbed as if ++they were arguments to `python -m` on a desktop machine. ++ ++### Testing in Xcode ++ ++You can also open the testbed project in Xcode by running: ++``` ++open my-testbed/iOSTestbed.xcodeproj ++``` ++ ++This will allow you to use the full Xcode suite of tools for debugging. ++ ++The arguments used to run the test suite are defined as part of the test plan. ++To modify the test plan, select the test plan node of the project tree (it ++should be the first child of the root node), and select the "Configurations" ++tab. Modify the "Arguments Passed On Launch" value to change the testing ++arguments. ++ ++The test plan also disables parallel testing, and specifies the use of the ++`Testbed.lldbinit` file for providing configuration of the debugger. The ++default debugger configuration disables automatic breakpoints on the ++`SIGINT`, `SIGUSR1`, `SIGUSR2`, and `SIGXFSZ` signals. ++ ++### Testing on an iOS device ++ ++To test on an iOS device, the app needs to be signed with known developer ++credentials. To obtain these credentials, you must have an iOS Developer ++account, and your Xcode install will need to be logged into your account (see ++the Accounts tab of the Preferences dialog). ++ ++Once the project is open, and you're signed into your Apple Developer account, ++select the root node of the project tree (labeled "iOSTestbed"), then the ++"Signing & Capabilities" tab in the details page. Select a development team ++(this will likely be your own name), and plug in a physical device to your ++macOS machine with a USB cable. You should then be able to select your physical ++device from the list of targets in the pulldown in the Xcode titlebar. +--- /dev/null ++++ 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@ ++ ++ +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-ar +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-clang +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-cpp +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} -E "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch arm64 "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/arm64-apple-ios-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} strip -arch arm64 "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch x86_64 "$@" +--- /dev/null ++++ b/Apple/iOS/Resources/pyconfig.h +@@ -0,0 +1,7 @@ ++#ifdef __arm64__ ++#include "pyconfig-arm64.h" ++#endif ++ ++#ifdef __x86_64__ ++#include "pyconfig-x86_64.h" ++#endif +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/Info.plist +@@ -0,0 +1,136 @@ ++ ++ ++ ++ ++ AvailableLibraries ++ ++ ++ BinaryPath ++ Python.framework/Python ++ LibraryIdentifier ++ ios-arm64 ++ LibraryPath ++ Python.framework ++ SupportedArchitectures ++ ++ arm64 ++ ++ SupportedPlatform ++ ios ++ ++ ++ BinaryPath ++ Python.framework/Python ++ LibraryIdentifier ++ ios-arm64_x86_64-simulator ++ LibraryPath ++ Python.framework ++ SupportedArchitectures ++ ++ arm64 ++ x86_64 ++ ++ SupportedPlatform ++ ios ++ SupportedPlatformVariant ++ simulator ++ ++ ++ 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 ++ xros-arm64-simulator ++ LibraryPath ++ Python.framework ++ SupportedArchitectures ++ ++ arm64 ++ ++ SupportedPlatform ++ xros ++ SupportedPlatformVariant ++ simulator ++ ++ ++ BinaryPath ++ Python.framework/Python ++ LibraryIdentifier ++ xros-arm64 ++ LibraryPath ++ Python.framework ++ SupportedArchitectures ++ ++ arm64 ++ ++ SupportedPlatform ++ xros ++ ++ ++ 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 ++ ++ +--- /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 ++ ++ +--- /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 ++ ++ +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/build/utils.sh +@@ -0,0 +1,180 @@ ++# Utility methods for use in an Xcode project. ++# ++# An iOS XCframework cannot include any content other than the library binary ++# and relevant metadata. However, Python requires a standard library at runtime. ++# Therefore, it is necessary to add a build step to an Xcode app target that ++# processes the standard library and puts the content into the final app. ++# ++# In general, these tools will be invoked after bundle resources have been ++# copied into the app, but before framework embedding (and signing). ++# ++# The following is an example script, assuming that: ++# * Python.xcframework is in the root of the project ++# * There is an `app` folder that contains the app code ++# * There is an `app_packages` folder that contains installed Python packages. ++# ----- ++# set -e ++# source $PROJECT_DIR/Python.xcframework/build/build_utils.sh ++# install_python Python.xcframework app app_packages ++# ----- ++ ++# Copy the standard library from the XCframework into the app bundle. ++# ++# Accepts one argument: ++# 1. The path, relative to the root of the Xcode project, where the Python ++# XCframework can be found. ++install_stdlib() { ++ PYTHON_XCFRAMEWORK_PATH=$1 ++ ++ mkdir -p "$CODESIGNING_FOLDER_PATH/python/lib" ++ if [ "$EFFECTIVE_PLATFORM_NAME" = "-iphonesimulator" ]; then ++ echo "Installing Python modules for iOS Simulator" ++ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/ios-arm64-simulator" ]; then ++ SLICE_FOLDER="ios-arm64-simulator" ++ else ++ SLICE_FOLDER="ios-arm64_x86_64-simulator" ++ fi ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-iphoneos" ]; then ++ echo "Installing Python modules for iOS Device" ++ SLICE_FOLDER="ios-arm64" ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-appletvsimulator" ]; then ++ echo "Installing Python modules for tvOS Simulator" ++ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/tvos-arm64-simulator" ]; then ++ SLICE_FOLDER="tvos-arm64-simulator" ++ else ++ SLICE_FOLDER="tvos-arm64_x86_64-simulator" ++ fi ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-appletvos" ]; then ++ echo "Installing Python modules for tvOS Device" ++ SLICE_FOLDER="tvos-arm64" ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-watchsimulator" ]; then ++ echo "Installing Python modules for watchOS Simulator" ++ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/watchos-arm64-simulator" ]; then ++ SLICE_FOLDER="watchos-arm64-simulator" ++ else ++ SLICE_FOLDER="watchos-arm64_x86_64-simulator" ++ fi ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-watchos" ]; then ++ echo "Installing Python modules for watchOS Device" ++ SLICE_FOLDER="watchos-arm64" ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-xrsimulator" ]; then ++ echo "Installing Python modules for visionOS Simulator" ++ SLICE_FOLDER="xros-arm64-simulator" ++ elif [ "$EFFECTIVE_PLATFORM_NAME" = "-xros" ]; then ++ echo "Installing Python modules for visionOS Device" ++ SLICE_FOLDER="xros-arm64" ++ else ++ echo "Unsupported platform name $EFFECTIVE_PLATFORM_NAME" ++ exit 1 ++ fi ++ ++ # If the XCframework has a shared lib folder, then it's a full framework. ++ # Copy both the common and slice-specific part of the lib directory. ++ # Otherwise, it's a single-arch framework; use the "full" lib folder. ++ if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib" ]; then ++ rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" ++ rsync -au "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib-$ARCHS/" "$CODESIGNING_FOLDER_PATH/python/lib/" ++ else ++ # A single-arch framework will have a libpython symlink; that can't be included at runtime ++ 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") ++ echo "Install Python $PYTHON_VER standard library extension modules..." ++ process_dylibs $PYTHON_XCFRAMEWORK_PATH python/lib/$PYTHON_VER/lib-dynload ++ ++ for package_path in $@; do ++ echo "Installing $package_path extension modules ..." ++ process_dylibs $PYTHON_XCFRAMEWORK_PATH $package_path ++ done ++} +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/build/watchOS-dylib-Info-template.plist +@@ -0,0 +1,26 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleExecutable ++ ++ CFBundleIdentifier ++ ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundlePackageType ++ APPL ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSupportedPlatforms ++ ++ watchOS ++ ++ MinimumOSVersion ++ 4.0 ++ CFBundleVersion ++ 1 ++ ++ +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/build/xrOS-dylib-Info-template.plist +@@ -0,0 +1,30 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundleExecutable ++ ++ CFBundleIdentifier ++ ++ CFBundlePackageType ++ FMWK ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSupportedPlatforms ++ ++ XROS ++ ++ CFBundleVersion ++ 1 ++ MinimumOSVersion ++ 2.0 ++ UIDeviceFamily ++ ++ 7 ++ ++ ++ +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/ios-arm64/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling an iOS on-device ++build for testing purposes. +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling an iOS simulator ++build for testing purposes (either x86_64 or ARM64). +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/tvos-arm64/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a tvOS ++on-device build for testing purposes. +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/tvos-arm64_x86_64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a tvOS ++simulator build for testing purposes (either x86_64 or ARM64). +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/watchos-arm64_32/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a watchOS on-device ++build for testing purposes. +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/watchos-arm64_x86_64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling a watchOS ++simulator build for testing purposes (either x86_64 or ARM64). +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/xros-arm64-simulator/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling an visionOS simulator ++build for testing purposes (either x86_64 or ARM64). +--- /dev/null ++++ b/Apple/testbed/Python.xcframework/xros-arm64/README +@@ -0,0 +1,4 @@ ++This directory is intentionally empty. ++ ++It should be used as a target for `--enable-framework` when compiling an visionOS on-device ++build for testing purposes. +--- /dev/null ++++ b/Apple/testbed/Testbed.lldbinit +@@ -0,0 +1,4 @@ ++process handle SIGINT -n true -p true -s false ++process handle SIGUSR1 -n true -p true -s false ++process handle SIGUSR2 -n true -p true -s false ++process handle SIGXFSZ -n true -p true -s false +--- /dev/null ++++ b/Apple/testbed/TestbedTests/TestbedTests.m +@@ -0,0 +1,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 +--- /dev/null ++++ b/Apple/testbed/__main__.py +@@ -0,0 +1,456 @@ ++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", ++ "visionOS": "xros-arm64-simulator", ++ "watchOS": "watchos-arm64_x86_64-simulator", ++} ++ ++DECODE_ARGS = ("UTF-8", "backslashreplace") ++ ++# The system log prefixes each line: ++# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ... ++# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ... ++ ++LOG_PREFIX_REGEX = re.compile( ++ r"^\d{4}-\d{2}-\d{2}" # YYYY-MM-DD ++ r"\s+\d+:\d{2}:\d{2}\.\d+\+\d{4}" # HH:MM:SS.ssssss+ZZZZ ++ r"\s+.*Testbed\[\d+:\w+\]" # Process/thread ID ++) ++ ++ ++# Select a simulator device to use. ++def select_simulator_device(platform): ++ # List the testing simulators, in JSON format ++ raw_json = subprocess.check_output(["xcrun", "simctl", "list", "-j"]) ++ json_data = json.loads(raw_json) ++ ++ if platform == "iOS": ++ # Any iOS device will do; we'll look for "SE" devices - but the name ++ # isn't consistent over time. Older Xcode versions will use "iPhone SE ++ # (Nth generation)"; As of 2025, they've started using "iPhone 16e". ++ # ++ # When Xcode is updated after a new release, new devices will be ++ # available and old ones will be dropped from the set available on the ++ # latest iOS version. Select the one with the highest minimum runtime ++ # version - this is an indicator of the "newest" released device, which ++ # should always be supported on the "most recent" iOS version. ++ se_simulators = sorted( ++ (devicetype["minRuntimeVersion"], devicetype["name"]) ++ for devicetype in json_data["devicetypes"] ++ if devicetype["productFamily"] == "iPhone" ++ and ( ++ ( ++ "iPhone " in devicetype["name"] ++ and devicetype["name"].endswith("e") ++ ) ++ or "iPhone SE " in devicetype["name"] ++ ) ++ ) ++ simulator = se_simulators[-1][1] ++ elif platform == "tvOS": ++ # Find the most recent tvOS release. ++ simulators = sorted( ++ (devicetype["minRuntimeVersion"], devicetype["name"]) ++ for devicetype in json_data["devicetypes"] ++ if devicetype["productFamily"] == "Apple TV" ++ ) ++ simulator = simulators[-1][1] ++ elif platform == "visionOS": ++ # Find the most recent visionOS release. ++ simulators = sorted( ++ (devicetype["minRuntimeVersion"], devicetype["name"]) ++ for devicetype in json_data["devicetypes"] ++ if devicetype["productFamily"] == "Apple Vision" ++ ) ++ simulator = simulators[-1][1] ++ elif platform == "watchOS": ++ raise NotImplementedError("Don't know how to launch watchOS (yet)") ++ else: ++ raise ValueError(f"Unknown platform {platform}") ++ ++ return simulator ++ ++ ++def xcode_test(location: Path, platform: str, simulator: str, verbose: bool): ++ # Build and run the test suite on the named simulator. ++ args = [ ++ "-project", ++ str(location / f"{platform}Testbed.xcodeproj"), ++ "-scheme", ++ f"{platform}Testbed", ++ "-destination", ++ f"platform={platform} Simulator,name={simulator}", ++ "-derivedDataPath", ++ str(location / "DerivedData"), ++ ] ++ verbosity_args = [] if verbose else ["-quiet"] ++ ++ print("Building test project...") ++ subprocess.run( ++ ["xcodebuild", "build-for-testing"] + args + verbosity_args, ++ check=True, ++ ) ++ ++ # 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 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.") ++ ++ if simulator is None: ++ simulator = select_simulator_device(platform) ++ print(f"Running test on {simulator}") ++ ++ xcode_test( ++ location, ++ platform=platform, ++ simulator=simulator, ++ verbose=verbose, ++ ) ++ ++ ++def main(): ++ # Look for directories like `iOSTestbed` as an indicator of the platforms ++ # that the testbed folder supports. The original source testbed can support ++ # many platforms, but when cloned, only one platform is preserved. ++ available_platforms = [ ++ platform ++ for platform in ["iOS", "tvOS", "visionOS", "watchOS"] ++ if (Path(__file__).parent / f"{platform}Testbed").is_dir() ++ ] ++ ++ parser = argparse.ArgumentParser( ++ description=( ++ "Manages the process of testing an Apple Python project " ++ "through Xcode." ++ ), ++ ) ++ ++ subcommands = parser.add_subparsers(dest="subcommand") ++ clone = subcommands.add_parser( ++ "clone", ++ description=( ++ "Clone the testbed project, copying in a Python framework and" ++ "any specified application code." ++ ), ++ help="Clone a testbed project to a new location.", ++ ) ++ clone.add_argument( ++ "--framework", ++ help=( ++ "The location of the XCFramework (or simulator-only slice of an " ++ "XCFramework) to use when running the testbed" ++ ), ++ ) ++ clone.add_argument( ++ "--platform", ++ dest="platform", ++ choices=available_platforms, ++ default=available_platforms[0], ++ help=f"The platform to target (default: {available_platforms[0]})", ++ ) ++ clone.add_argument( ++ "--app", ++ dest="apps", ++ action="append", ++ default=[], ++ help="The location of any code to include in the testbed project", ++ ) ++ clone.add_argument( ++ "location", ++ help="The path where the testbed will be cloned.", ++ ) ++ ++ run = subcommands.add_parser( ++ "run", ++ usage=( ++ "%(prog)s [-h] [--simulator SIMULATOR] -- " ++ " [ ...]" ++ ), ++ description=( ++ "Run a testbed project. The arguments provided after `--` will be " ++ "passed to the running test process as if they were arguments to " ++ "`python -m`." ++ ), ++ help="Run a testbed project", ++ ) ++ run.add_argument( ++ "--platform", ++ dest="platform", ++ choices=available_platforms, ++ default=available_platforms[0], ++ help=f"The platform to target (default: {available_platforms[0]})", ++ ) ++ run.add_argument( ++ "--simulator", ++ help=( ++ "The name of the simulator to use (eg: 'iPhone 16e'). Defaults to " ++ "the most recently released 'entry level' iPhone device. Device " ++ "architecture and OS version can also be specified; e.g., " ++ "`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would run on " ++ "an ARM64 iPhone 16 Pro simulator running iOS 26.0." ++ ), ++ ) ++ run.add_argument( ++ "-v", ++ "--verbose", ++ action="store_true", ++ help="Enable verbose output", ++ ) ++ ++ try: ++ pos = sys.argv.index("--") ++ testbed_args = sys.argv[1:pos] ++ test_args = sys.argv[pos + 1 :] ++ except ValueError: ++ testbed_args = sys.argv[1:] ++ test_args = [] ++ ++ context = parser.parse_args(testbed_args) ++ ++ if context.subcommand == "clone": ++ clone_testbed( ++ source=Path(__file__).parent.resolve(), ++ target=Path(context.location).resolve(), ++ framework=Path(context.framework).resolve() ++ if context.framework ++ else None, ++ platform=context.platform, ++ apps=[Path(app) for app in context.apps], ++ ) ++ elif context.subcommand == "run": ++ if test_args: ++ if not ( ++ Path(__file__).parent ++ / "Python.xcframework" ++ / TEST_SLICES[context.platform] ++ / "bin" ++ ).is_dir(): ++ print( ++ "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() +--- /dev/null ++++ b/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj +@@ -0,0 +1,557 @@ ++// !$*UTF8*$! ++{ ++ archiveVersion = 1; ++ classes = { ++ }; ++ objectVersion = 56; ++ objects = { ++ ++/* Begin PBXBuildFile section */ ++ 607A66172B0EFA380010BFC8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66162B0EFA380010BFC8 /* AppDelegate.m */; }; ++ 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607A66212B0EFA390010BFC8 /* Assets.xcassets */; }; ++ 607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */; }; ++ 607A66282B0EFA390010BFC8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66272B0EFA390010BFC8 /* main.m */; }; ++ 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */; }; ++ 607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; ++ 607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; ++ 607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; }; ++ 607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; ++ 608619542CB77BA900F46182 /* app_packages in Resources */ = {isa = PBXBuildFile; fileRef = 608619532CB77BA900F46182 /* app_packages */; }; ++ 608619562CB7819B00F46182 /* app in Resources */ = {isa = PBXBuildFile; fileRef = 608619552CB7819B00F46182 /* app */; }; ++/* End PBXBuildFile section */ ++ ++/* Begin PBXContainerItemProxy section */ ++ 607A662E2B0EFA3A0010BFC8 /* PBXContainerItemProxy */ = { ++ isa = PBXContainerItemProxy; ++ containerPortal = 607A660A2B0EFA380010BFC8 /* Project object */; ++ proxyType = 1; ++ remoteGlobalIDString = 607A66112B0EFA380010BFC8; ++ remoteInfo = iOSTestbed; ++ }; ++/* End PBXContainerItemProxy section */ ++ ++/* Begin PBXCopyFilesBuildPhase section */ ++ 607A664E2B0EFC080010BFC8 /* Embed Frameworks */ = { ++ isa = PBXCopyFilesBuildPhase; ++ buildActionMask = 2147483647; ++ dstPath = ""; ++ dstSubfolderSpec = 10; ++ files = ( ++ 607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */, ++ ); ++ name = "Embed Frameworks"; ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */ = { ++ isa = PBXCopyFilesBuildPhase; ++ buildActionMask = 2147483647; ++ dstPath = ""; ++ dstSubfolderSpec = 10; ++ files = ( ++ 607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */, ++ ); ++ name = "Embed Frameworks"; ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXCopyFilesBuildPhase section */ ++ ++/* Begin PBXFileReference section */ ++ 607A66122B0EFA380010BFC8 /* iOSTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; }; ++ 607A66152B0EFA380010BFC8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; ++ 607A66162B0EFA380010BFC8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; ++ 607A66212B0EFA390010BFC8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; ++ 607A66242B0EFA390010BFC8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; ++ 607A66272B0EFA390010BFC8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; ++ 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSTestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; ++ 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestbedTests.m; sourceTree = ""; }; ++ 607A664A2B0EFB310010BFC8 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; ++ 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = ""; }; ++ 608619532CB77BA900F46182 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = ""; }; ++ 608619552CB7819B00F46182 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = ""; }; ++ 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = iOSTestbed.xctestplan; sourceTree = ""; }; ++/* End PBXFileReference section */ ++ ++/* Begin PBXFrameworksBuildPhase section */ ++ 607A660F2B0EFA380010BFC8 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A662A2B0EFA3A0010BFC8 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXFrameworksBuildPhase section */ ++ ++/* Begin PBXGroup section */ ++ 607A66092B0EFA380010BFC8 = { ++ isa = PBXGroup; ++ children = ( ++ 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */, ++ 607A664A2B0EFB310010BFC8 /* Python.xcframework */, ++ 607A66142B0EFA380010BFC8 /* iOSTestbed */, ++ 607A66302B0EFA3A0010BFC8 /* TestbedTests */, ++ 607A66132B0EFA380010BFC8 /* Products */, ++ 607A664F2B0EFFE00010BFC8 /* Frameworks */, ++ ); ++ sourceTree = ""; ++ }; ++ 607A66132B0EFA380010BFC8 /* Products */ = { ++ isa = PBXGroup; ++ children = ( ++ 607A66122B0EFA380010BFC8 /* iOSTestbed.app */, ++ 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */, ++ ); ++ name = Products; ++ sourceTree = ""; ++ }; ++ 607A66142B0EFA380010BFC8 /* iOSTestbed */ = { ++ isa = PBXGroup; ++ children = ( ++ 608619552CB7819B00F46182 /* app */, ++ 608619532CB77BA900F46182 /* app_packages */, ++ 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */, ++ 607A66152B0EFA380010BFC8 /* AppDelegate.h */, ++ 607A66162B0EFA380010BFC8 /* AppDelegate.m */, ++ 607A66212B0EFA390010BFC8 /* Assets.xcassets */, ++ 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */, ++ 607A66272B0EFA390010BFC8 /* main.m */, ++ ); ++ path = iOSTestbed; ++ sourceTree = ""; ++ }; ++ 607A66302B0EFA3A0010BFC8 /* TestbedTests */ = { ++ isa = PBXGroup; ++ children = ( ++ 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */, ++ ); ++ path = TestbedTests; ++ sourceTree = ""; ++ }; ++ 607A664F2B0EFFE00010BFC8 /* Frameworks */ = { ++ isa = PBXGroup; ++ children = ( ++ ); ++ name = Frameworks; ++ sourceTree = ""; ++ }; ++/* End PBXGroup section */ ++ ++/* Begin PBXNativeTarget section */ ++ 607A66112B0EFA380010BFC8 /* iOSTestbed */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbed" */; ++ buildPhases = ( ++ 607A660E2B0EFA380010BFC8 /* Sources */, ++ 607A660F2B0EFA380010BFC8 /* Frameworks */, ++ 607A66102B0EFA380010BFC8 /* Resources */, ++ 607A66552B0F061D0010BFC8 /* Process Python libraries */, ++ 607A664E2B0EFC080010BFC8 /* Embed Frameworks */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ ); ++ name = iOSTestbed; ++ productName = iOSTestbed; ++ productReference = 607A66122B0EFA380010BFC8 /* iOSTestbed.app */; ++ productType = "com.apple.product-type.application"; ++ }; ++ 607A662C2B0EFA3A0010BFC8 /* iOSTestbedTests */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbedTests" */; ++ buildPhases = ( ++ 607A66292B0EFA3A0010BFC8 /* Sources */, ++ 607A662A2B0EFA3A0010BFC8 /* Frameworks */, ++ 607A662B2B0EFA3A0010BFC8 /* Resources */, ++ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */, ++ ); ++ name = iOSTestbedTests; ++ productName = iOSTestbedTests; ++ productReference = 607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */; ++ productType = "com.apple.product-type.bundle.unit-test"; ++ }; ++/* End PBXNativeTarget section */ ++ ++/* Begin PBXProject section */ ++ 607A660A2B0EFA380010BFC8 /* Project object */ = { ++ isa = PBXProject; ++ attributes = { ++ BuildIndependentTargetsInParallel = 1; ++ LastUpgradeCheck = 1500; ++ TargetAttributes = { ++ 607A66112B0EFA380010BFC8 = { ++ CreatedOnToolsVersion = 15.0.1; ++ }; ++ 607A662C2B0EFA3A0010BFC8 = { ++ CreatedOnToolsVersion = 15.0.1; ++ TestTargetID = 607A66112B0EFA380010BFC8; ++ }; ++ }; ++ }; ++ buildConfigurationList = 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "iOSTestbed" */; ++ compatibilityVersion = "Xcode 14.0"; ++ developmentRegion = en; ++ hasScannedForEncodings = 0; ++ knownRegions = ( ++ en, ++ Base, ++ ); ++ mainGroup = 607A66092B0EFA380010BFC8; ++ productRefGroup = 607A66132B0EFA380010BFC8 /* Products */; ++ projectDirPath = ""; ++ projectRoot = ""; ++ targets = ( ++ 607A66112B0EFA380010BFC8 /* iOSTestbed */, ++ 607A662C2B0EFA3A0010BFC8 /* iOSTestbedTests */, ++ ); ++ }; ++/* End PBXProject section */ ++ ++/* Begin PBXResourcesBuildPhase section */ ++ 607A66102B0EFA380010BFC8 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */, ++ 608619562CB7819B00F46182 /* app in Resources */, ++ 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */, ++ 608619542CB77BA900F46182 /* app_packages in Resources */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A662B2B0EFA3A0010BFC8 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXResourcesBuildPhase section */ ++ ++/* Begin PBXShellScriptBuildPhase section */ ++ 607A66552B0F061D0010BFC8 /* Process Python libraries */ = { ++ isa = PBXShellScriptBuildPhase; ++ alwaysOutOfDate = 1; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ inputFileListPaths = ( ++ ); ++ inputPaths = ( ++ ); ++ name = "Process Python libraries"; ++ outputFileListPaths = ( ++ ); ++ outputPaths = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ shellPath = /bin/sh; ++ shellScript = "set -e\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\ninstall_python Python.xcframework app app_packages\n"; ++ showEnvVarsInLog = 0; ++ }; ++/* End PBXShellScriptBuildPhase section */ ++ ++/* Begin PBXSourcesBuildPhase section */ ++ 607A660E2B0EFA380010BFC8 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A66172B0EFA380010BFC8 /* AppDelegate.m in Sources */, ++ 607A66282B0EFA390010BFC8 /* main.m in Sources */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A66292B0EFA3A0010BFC8 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXSourcesBuildPhase section */ ++ ++/* Begin PBXTargetDependency section */ ++ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */ = { ++ isa = PBXTargetDependency; ++ target = 607A66112B0EFA380010BFC8 /* iOSTestbed */; ++ targetProxy = 607A662E2B0EFA3A0010BFC8 /* PBXContainerItemProxy */; ++ }; ++/* End PBXTargetDependency section */ ++ ++/* Begin PBXVariantGroup section */ ++ 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */ = { ++ isa = PBXVariantGroup; ++ children = ( ++ 607A66242B0EFA390010BFC8 /* Base */, ++ ); ++ name = LaunchScreen.storyboard; ++ sourceTree = ""; ++ }; ++/* End PBXVariantGroup section */ ++ ++/* Begin XCBuildConfiguration section */ ++ 607A663F2B0EFA3A0010BFC8 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ALWAYS_SEARCH_USER_PATHS = NO; ++ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ++ CLANG_ANALYZER_NONNULL = YES; ++ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; ++ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; ++ CLANG_ENABLE_MODULES = YES; ++ CLANG_ENABLE_OBJC_ARC = YES; ++ CLANG_ENABLE_OBJC_WEAK = YES; ++ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; ++ CLANG_WARN_BOOL_CONVERSION = YES; ++ CLANG_WARN_COMMA = YES; ++ CLANG_WARN_CONSTANT_CONVERSION = YES; ++ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; ++ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; ++ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; ++ CLANG_WARN_EMPTY_BODY = YES; ++ CLANG_WARN_ENUM_CONVERSION = YES; ++ CLANG_WARN_INFINITE_RECURSION = YES; ++ CLANG_WARN_INT_CONVERSION = YES; ++ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; ++ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; ++ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; ++ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; ++ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; ++ CLANG_WARN_STRICT_PROTOTYPES = YES; ++ CLANG_WARN_SUSPICIOUS_MOVE = YES; ++ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; ++ CLANG_WARN_UNREACHABLE_CODE = YES; ++ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; ++ COPY_PHASE_STRIP = NO; ++ DEBUG_INFORMATION_FORMAT = dwarf; ++ ENABLE_STRICT_OBJC_MSGSEND = YES; ++ ENABLE_TESTABILITY = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = YES; ++ GCC_C_LANGUAGE_STANDARD = gnu17; ++ GCC_DYNAMIC_NO_PIC = NO; ++ GCC_NO_COMMON_BLOCKS = YES; ++ GCC_OPTIMIZATION_LEVEL = 0; ++ GCC_PREPROCESSOR_DEFINITIONS = ( ++ "DEBUG=1", ++ "$(inherited)", ++ ); ++ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; ++ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; ++ GCC_WARN_UNDECLARED_SELECTOR = YES; ++ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; ++ GCC_WARN_UNUSED_FUNCTION = YES; ++ GCC_WARN_UNUSED_VARIABLE = YES; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; ++ MTL_FAST_MATH = YES; ++ ONLY_ACTIVE_ARCH = YES; ++ SDKROOT = iphoneos; ++ }; ++ name = Debug; ++ }; ++ 607A66402B0EFA3A0010BFC8 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ALWAYS_SEARCH_USER_PATHS = NO; ++ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ++ CLANG_ANALYZER_NONNULL = YES; ++ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; ++ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; ++ CLANG_ENABLE_MODULES = YES; ++ CLANG_ENABLE_OBJC_ARC = YES; ++ CLANG_ENABLE_OBJC_WEAK = YES; ++ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; ++ CLANG_WARN_BOOL_CONVERSION = YES; ++ CLANG_WARN_COMMA = YES; ++ CLANG_WARN_CONSTANT_CONVERSION = YES; ++ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; ++ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; ++ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; ++ CLANG_WARN_EMPTY_BODY = YES; ++ CLANG_WARN_ENUM_CONVERSION = YES; ++ CLANG_WARN_INFINITE_RECURSION = YES; ++ CLANG_WARN_INT_CONVERSION = YES; ++ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; ++ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; ++ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; ++ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; ++ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; ++ CLANG_WARN_STRICT_PROTOTYPES = YES; ++ CLANG_WARN_SUSPICIOUS_MOVE = YES; ++ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; ++ CLANG_WARN_UNREACHABLE_CODE = YES; ++ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; ++ COPY_PHASE_STRIP = NO; ++ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ++ ENABLE_NS_ASSERTIONS = NO; ++ ENABLE_STRICT_OBJC_MSGSEND = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = YES; ++ GCC_C_LANGUAGE_STANDARD = gnu17; ++ GCC_NO_COMMON_BLOCKS = YES; ++ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; ++ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; ++ GCC_WARN_UNDECLARED_SELECTOR = YES; ++ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; ++ GCC_WARN_UNUSED_FUNCTION = YES; ++ GCC_WARN_UNUSED_VARIABLE = YES; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = NO; ++ MTL_FAST_MATH = YES; ++ SDKROOT = iphoneos; ++ VALIDATE_PRODUCT = YES; ++ }; ++ name = Release; ++ }; ++ 607A66422B0EFA3A0010BFC8 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ++ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = ""; ++ ENABLE_USER_SCRIPT_SANDBOXING = NO; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist"; ++ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; ++ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; ++ INFOPLIST_KEY_UIMainStoryboardFile = Main; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 3.13.0a1; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = YES; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ }; ++ name = Debug; ++ }; ++ 607A66432B0EFA3A0010BFC8 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ++ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = ""; ++ ENABLE_TESTABILITY = YES; ++ ENABLE_USER_SCRIPT_SANDBOXING = NO; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist"; ++ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; ++ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; ++ INFOPLIST_KEY_UIMainStoryboardFile = Main; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 3.13.0a1; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = YES; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ }; ++ name = Release; ++ }; ++ 607A66452B0EFA3A0010BFC8 /* Debug */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ BUNDLE_LOADER = "$(TEST_HOST)"; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = 3HEZE76D99; ++ GENERATE_INFOPLIST_FILE = YES; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = NO; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSTestbed"; ++ }; ++ name = Debug; ++ }; ++ 607A66462B0EFA3A0010BFC8 /* Release */ = { ++ isa = XCBuildConfiguration; ++ buildSettings = { ++ BUNDLE_LOADER = "$(TEST_HOST)"; ++ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; ++ CODE_SIGN_STYLE = Automatic; ++ CURRENT_PROJECT_VERSION = 1; ++ DEVELOPMENT_TEAM = 3HEZE76D99; ++ GENERATE_INFOPLIST_FILE = YES; ++ HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\""; ++ IPHONEOS_DEPLOYMENT_TARGET = 13.0; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SWIFT_EMIT_LOC_STRINGS = NO; ++ TARGETED_DEVICE_FAMILY = "1,2"; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSTestbed"; ++ }; ++ name = Release; ++ }; ++/* End XCBuildConfiguration section */ ++ ++/* Begin XCConfigurationList section */ ++ 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "iOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A663F2B0EFA3A0010BFC8 /* Debug */, ++ 607A66402B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A66422B0EFA3A0010BFC8 /* Debug */, ++ 607A66432B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbedTests" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A66452B0EFA3A0010BFC8 /* Debug */, ++ 607A66462B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++/* End XCConfigurationList section */ ++ }; ++ rootObject = 607A660A2B0EFA380010BFC8 /* Project object */; ++} +--- /dev/null ++++ b/Apple/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme +@@ -0,0 +1,97 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +--- /dev/null ++++ b/Apple/testbed/iOSTestbed.xctestplan +@@ -0,0 +1,46 @@ ++{ ++ "configurations" : [ ++ { ++ "id" : "F5A95CE4-1ADE-4A6E-A0E1-CDBAE26DF0C5", ++ "name" : "Test Scheme Action", ++ "options" : { ++ ++ } ++ } ++ ], ++ "defaultOptions" : { ++ "commandLineArgumentEntries" : [ ++ { ++ "argument" : "test" ++ }, ++ { ++ "argument" : "-uall" ++ }, ++ { ++ "argument" : "--single-process" ++ }, ++ { ++ "argument" : "--rerun" ++ }, ++ { ++ "argument" : "-W" ++ } ++ ], ++ "targetForVariableExpansion" : { ++ "containerPath" : "container:iOSTestbed.xcodeproj", ++ "identifier" : "607A66112B0EFA380010BFC8", ++ "name" : "iOSTestbed" ++ } ++ }, ++ "testTargets" : [ ++ { ++ "parallelizable" : false, ++ "target" : { ++ "containerPath" : "container:iOSTestbed.xcodeproj", ++ "identifier" : "607A662C2B0EFA3A0010BFC8", ++ "name" : "iOSTestbedTests" ++ } ++ } ++ ], ++ "version" : 1 ++} +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/AppDelegate.h +@@ -0,0 +1,11 @@ ++// ++// AppDelegate.h ++// iOSTestbed ++// ++ ++#import ++ ++@interface AppDelegate : UIResponder ++ ++ ++@end +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/AppDelegate.m +@@ -0,0 +1,19 @@ ++// ++// AppDelegate.m ++// iOSTestbed ++// ++ ++#import "AppDelegate.h" ++ ++@interface AppDelegate () ++ ++@end ++ ++@implementation AppDelegate ++ ++ ++- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ++ return YES; ++} ++ ++@end +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json +@@ -0,0 +1,11 @@ ++{ ++ "colors" : [ ++ { ++ "idiom" : "universal" ++ } ++ ], ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json +@@ -0,0 +1,13 @@ ++{ ++ "images" : [ ++ { ++ "idiom" : "universal", ++ "platform" : "ios", ++ "size" : "1024x1024" ++ } ++ ], ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/Assets.xcassets/Contents.json +@@ -0,0 +1,6 @@ ++{ ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard +@@ -0,0 +1,9 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/app/README +@@ -0,0 +1,7 @@ ++This folder can contain any Python application code. ++ ++During the build, any binary modules found in this folder will be processed into ++Framework form. ++ ++When the test suite runs, this folder will be on the PYTHONPATH, and will be the ++working directory for the test suite. +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/app_packages/README +@@ -0,0 +1,7 @@ ++This folder can be a target for installing any Python dependencies needed by the ++test suite. ++ ++During the build, any binary modules found in this folder will be processed into ++Framework form. ++ ++When the test suite runs, this folder will be on the PYTHONPATH. +--- /dev/null ++++ b/Apple/testbed/iOSTestbed/iOSTestbed-Info.plist +@@ -0,0 +1,52 @@ ++ ++ ++ ++ ++ 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 ++ ++ ++ ++ +--- /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); ++ } ++} +--- /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 */; ++} +--- /dev/null ++++ b/Apple/testbed/tvOSTestbed.xcodeproj/xcshareddata/xcschemes/tvOSTestbed.xcscheme +@@ -0,0 +1,97 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +--- /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 +--- /dev/null ++++ b/Apple/testbed/tvOSTestbed/AppDelegate.h +@@ -0,0 +1,11 @@ ++// ++// AppDelegate.h ++// tvOSTestbed ++// ++ ++#import ++ ++@interface AppDelegate : UIResponder ++ ++ ++@end +--- /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 +--- /dev/null ++++ b/Apple/testbed/tvOSTestbed/Base.lproj/LaunchScreen.storyboard +@@ -0,0 +1,24 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +--- /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. +--- /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. +--- /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); ++ } ++} +--- /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 ++ ++ ++ ++ +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed.xcodeproj/project.pbxproj +@@ -0,0 +1,558 @@ ++// !$*UTF8*$! ++{ ++ archiveVersion = 1; ++ classes = { ++ }; ++ objectVersion = 56; ++ objects = { ++ ++/* Begin PBXBuildFile section */ ++ 607A66172B0EFA380010BFC8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66162B0EFA380010BFC8 /* AppDelegate.m */; }; ++ 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607A66212B0EFA390010BFC8 /* Assets.xcassets */; }; ++ 607A66282B0EFA390010BFC8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66272B0EFA390010BFC8 /* main.m */; }; ++ 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */; }; ++ 608619542CB77BA900F46182 /* app_packages in Resources */ = {isa = PBXBuildFile; fileRef = 608619532CB77BA900F46182 /* app_packages */; }; ++ 608619562CB7819B00F46182 /* app in Resources */ = {isa = PBXBuildFile; fileRef = 608619552CB7819B00F46182 /* app */; }; ++ EEB367CE2DADF5C900B9A1D7 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EEE9C80C2DAB5ECA0056F8C6 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; ++ EEB367CF2DADF5D300B9A1D7 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EEE9C80C2DAB5ECA0056F8C6 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; ++ EEE9C80D2DAB5ECA0056F8C6 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EEE9C80C2DAB5ECA0056F8C6 /* Python.xcframework */; }; ++ EEE9C80E2DAB5ECA0056F8C6 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EEE9C80C2DAB5ECA0056F8C6 /* Python.xcframework */; }; ++/* 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 = ( ++ EEB367CF2DADF5D300B9A1D7 /* Python.xcframework in Embed Frameworks */, ++ ); ++ name = "Embed Frameworks"; ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */ = { ++ isa = PBXCopyFilesBuildPhase; ++ buildActionMask = 2147483647; ++ dstPath = ""; ++ dstSubfolderSpec = 10; ++ files = ( ++ EEB367CE2DADF5C900B9A1D7 /* Python.xcframework in Embed Frameworks */, ++ ); ++ name = "Embed Frameworks"; ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXCopyFilesBuildPhase section */ ++ ++/* Begin PBXFileReference section */ ++ 6077B3D62E82E60C00E3D6A3 /* visionOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = visionOSTestbed.xctestplan; sourceTree = ""; }; ++ 607A66122B0EFA380010BFC8 /* visionOSTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = visionOSTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; }; ++ 607A66152B0EFA380010BFC8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; ++ 607A66162B0EFA380010BFC8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; ++ 607A66212B0EFA390010BFC8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; ++ 607A66272B0EFA390010BFC8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; ++ 607A662D2B0EFA3A0010BFC8 /* TestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; ++ 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestbedTests.m; sourceTree = ""; }; ++ 607A66592B0F08600010BFC8 /* visionOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "visionOSTestbed-Info.plist"; sourceTree = ""; }; ++ 608619532CB77BA900F46182 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = ""; }; ++ 608619552CB7819B00F46182 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = ""; }; ++ EEE9C80C2DAB5ECA0056F8C6 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; ++/* End PBXFileReference section */ ++ ++/* Begin PBXFrameworksBuildPhase section */ ++ 607A660F2B0EFA380010BFC8 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ EEE9C80D2DAB5ECA0056F8C6 /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A662A2B0EFA3A0010BFC8 /* Frameworks */ = { ++ isa = PBXFrameworksBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ EEE9C80E2DAB5ECA0056F8C6 /* Python.xcframework in Frameworks */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXFrameworksBuildPhase section */ ++ ++/* Begin PBXGroup section */ ++ 607A66092B0EFA380010BFC8 = { ++ isa = PBXGroup; ++ children = ( ++ 6077B3D62E82E60C00E3D6A3 /* visionOSTestbed.xctestplan */, ++ EEE9C80C2DAB5ECA0056F8C6 /* Python.xcframework */, ++ 607A66142B0EFA380010BFC8 /* visionOSTestbed */, ++ 607A66302B0EFA3A0010BFC8 /* TestbedTests */, ++ 607A66132B0EFA380010BFC8 /* Products */, ++ 607A664F2B0EFFE00010BFC8 /* Frameworks */, ++ ); ++ sourceTree = ""; ++ }; ++ 607A66132B0EFA380010BFC8 /* Products */ = { ++ isa = PBXGroup; ++ children = ( ++ 607A66122B0EFA380010BFC8 /* visionOSTestbed.app */, ++ 607A662D2B0EFA3A0010BFC8 /* TestbedTests.xctest */, ++ ); ++ name = Products; ++ sourceTree = ""; ++ }; ++ 607A66142B0EFA380010BFC8 /* visionOSTestbed */ = { ++ isa = PBXGroup; ++ children = ( ++ 608619552CB7819B00F46182 /* app */, ++ 608619532CB77BA900F46182 /* app_packages */, ++ 607A66592B0F08600010BFC8 /* visionOSTestbed-Info.plist */, ++ 607A66152B0EFA380010BFC8 /* AppDelegate.h */, ++ 607A66162B0EFA380010BFC8 /* AppDelegate.m */, ++ 607A66212B0EFA390010BFC8 /* Assets.xcassets */, ++ 607A66272B0EFA390010BFC8 /* main.m */, ++ ); ++ path = visionOSTestbed; ++ sourceTree = ""; ++ }; ++ 607A66302B0EFA3A0010BFC8 /* TestbedTests */ = { ++ isa = PBXGroup; ++ children = ( ++ 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */, ++ ); ++ path = TestbedTests; ++ sourceTree = ""; ++ }; ++ 607A664F2B0EFFE00010BFC8 /* Frameworks */ = { ++ isa = PBXGroup; ++ children = ( ++ ); ++ name = Frameworks; ++ sourceTree = ""; ++ }; ++/* End PBXGroup section */ ++ ++/* Begin PBXNativeTarget section */ ++ 607A66112B0EFA380010BFC8 /* visionOSTestbed */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "visionOSTestbed" */; ++ buildPhases = ( ++ 607A660E2B0EFA380010BFC8 /* Sources */, ++ 607A660F2B0EFA380010BFC8 /* Frameworks */, ++ 607A66102B0EFA380010BFC8 /* Resources */, ++ 607A66552B0F061D0010BFC8 /* Process Python libraries */, ++ 607A664E2B0EFC080010BFC8 /* Embed Frameworks */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ ); ++ name = visionOSTestbed; ++ productName = iOSTestbed; ++ productReference = 607A66122B0EFA380010BFC8 /* visionOSTestbed.app */; ++ productType = "com.apple.product-type.application"; ++ }; ++ 607A662C2B0EFA3A0010BFC8 /* TestbedTests */ = { ++ isa = PBXNativeTarget; ++ buildConfigurationList = 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "TestbedTests" */; ++ buildPhases = ( ++ 607A66292B0EFA3A0010BFC8 /* Sources */, ++ 607A662A2B0EFA3A0010BFC8 /* Frameworks */, ++ 607A662B2B0EFA3A0010BFC8 /* Resources */, ++ 607A66522B0EFFE00010BFC8 /* Embed Frameworks */, ++ ); ++ buildRules = ( ++ ); ++ dependencies = ( ++ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */, ++ ); ++ name = TestbedTests; ++ productName = iOSTestbedTests; ++ productReference = 607A662D2B0EFA3A0010BFC8 /* TestbedTests.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 "visionOSTestbed" */; ++ compatibilityVersion = "Xcode 14.0"; ++ developmentRegion = en; ++ hasScannedForEncodings = 0; ++ knownRegions = ( ++ en, ++ Base, ++ ); ++ mainGroup = 607A66092B0EFA380010BFC8; ++ productRefGroup = 607A66132B0EFA380010BFC8 /* Products */; ++ projectDirPath = ""; ++ projectRoot = ""; ++ targets = ( ++ 607A66112B0EFA380010BFC8 /* visionOSTestbed */, ++ 607A662C2B0EFA3A0010BFC8 /* TestbedTests */, ++ ); ++ }; ++/* End PBXProject section */ ++ ++/* Begin PBXResourcesBuildPhase section */ ++ 607A66102B0EFA380010BFC8 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 608619562CB7819B00F46182 /* app in Resources */, ++ 607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */, ++ 608619542CB77BA900F46182 /* app_packages in Resources */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A662B2B0EFA3A0010BFC8 /* Resources */ = { ++ isa = PBXResourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXResourcesBuildPhase section */ ++ ++/* Begin PBXShellScriptBuildPhase section */ ++ 607A66552B0F061D0010BFC8 /* Process Python libraries */ = { ++ isa = PBXShellScriptBuildPhase; ++ alwaysOutOfDate = 1; ++ buildActionMask = 2147483647; ++ files = ( ++ ); ++ inputFileListPaths = ( ++ ); ++ inputPaths = ( ++ ); ++ name = "Process Python libraries"; ++ outputFileListPaths = ( ++ ); ++ outputPaths = ( ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ shellPath = /bin/sh; ++ shellScript = "set -e\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 */ ++ 607A660E2B0EFA380010BFC8 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A66172B0EFA380010BFC8 /* AppDelegate.m in Sources */, ++ 607A66282B0EFA390010BFC8 /* main.m in Sources */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++ 607A66292B0EFA3A0010BFC8 /* Sources */ = { ++ isa = PBXSourcesBuildPhase; ++ buildActionMask = 2147483647; ++ files = ( ++ 607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */, ++ ); ++ runOnlyForDeploymentPostprocessing = 0; ++ }; ++/* End PBXSourcesBuildPhase section */ ++ ++/* Begin PBXTargetDependency section */ ++ 607A662F2B0EFA3A0010BFC8 /* PBXTargetDependency */ = { ++ isa = PBXTargetDependency; ++ target = 607A66112B0EFA380010BFC8 /* visionOSTestbed */; ++ 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 = 12.0; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; ++ MTL_FAST_MATH = YES; ++ ONLY_ACTIVE_ARCH = YES; ++ SDKROOT = xros; ++ }; ++ 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 = 12.0; ++ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; ++ MTL_ENABLE_DEBUG_INFO = NO; ++ MTL_FAST_MATH = YES; ++ SDKROOT = xros; ++ 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 = "visionOSTestbed/visionOSTestbed-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"; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 3.13.0a1; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.visionOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SUPPORTED_PLATFORMS = "xros xrsimulator"; ++ SUPPORTS_MACCATALYST = NO; ++ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; ++ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; ++ SWIFT_EMIT_LOC_STRINGS = YES; ++ TARGETED_DEVICE_FAMILY = 7; ++ XROS_DEPLOYMENT_TARGET = 2.0; ++ }; ++ 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 = "visionOSTestbed/visionOSTestbed-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"; ++ LD_RUNPATH_SEARCH_PATHS = ( ++ "$(inherited)", ++ "@executable_path/Frameworks", ++ ); ++ MARKETING_VERSION = 3.13.0a1; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.visionOSTestbed; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SUPPORTED_PLATFORMS = "xros xrsimulator"; ++ SUPPORTS_MACCATALYST = NO; ++ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; ++ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; ++ SWIFT_EMIT_LOC_STRINGS = YES; ++ TARGETED_DEVICE_FAMILY = 7; ++ XROS_DEPLOYMENT_TARGET = 2.0; ++ }; ++ 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 = 12.0; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.TestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SUPPORTED_PLATFORMS = "xros xrsimulator"; ++ SUPPORTS_MACCATALYST = NO; ++ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; ++ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; ++ SWIFT_EMIT_LOC_STRINGS = NO; ++ TARGETED_DEVICE_FAMILY = 7; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/visionOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/visionOSTestbed"; ++ }; ++ 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 = 12.0; ++ MARKETING_VERSION = 1.0; ++ PRODUCT_BUNDLE_IDENTIFIER = org.python.TestbedTests; ++ PRODUCT_NAME = "$(TARGET_NAME)"; ++ SUPPORTED_PLATFORMS = "xros xrsimulator"; ++ SUPPORTS_MACCATALYST = NO; ++ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; ++ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; ++ SWIFT_EMIT_LOC_STRINGS = NO; ++ TARGETED_DEVICE_FAMILY = 7; ++ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/visionOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/visionOSTestbed"; ++ }; ++ name = Release; ++ }; ++/* End XCBuildConfiguration section */ ++ ++/* Begin XCConfigurationList section */ ++ 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "visionOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A663F2B0EFA3A0010BFC8 /* Debug */, ++ 607A66402B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "visionOSTestbed" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A66422B0EFA3A0010BFC8 /* Debug */, ++ 607A66432B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++ 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "TestbedTests" */ = { ++ isa = XCConfigurationList; ++ buildConfigurations = ( ++ 607A66452B0EFA3A0010BFC8 /* Debug */, ++ 607A66462B0EFA3A0010BFC8 /* Release */, ++ ); ++ defaultConfigurationIsVisible = 0; ++ defaultConfigurationName = Release; ++ }; ++/* End XCConfigurationList section */ ++ }; ++ rootObject = 607A660A2B0EFA380010BFC8 /* Project object */; ++} +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed.xcodeproj/xcshareddata/xcschemes/visionOSTestbed.xcscheme +@@ -0,0 +1,97 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed.xctestplan +@@ -0,0 +1,46 @@ ++{ ++ "configurations" : [ ++ { ++ "id" : "C17FA044-0B70-48CA-AFF8-BC252081002F", ++ "name" : "Test Scheme Action", ++ "options" : { ++ ++ } ++ } ++ ], ++ "defaultOptions" : { ++ "commandLineArgumentEntries" : [ ++ { ++ "argument" : "test" ++ }, ++ { ++ "argument" : "-uall" ++ }, ++ { ++ "argument" : "--single-process" ++ }, ++ { ++ "argument" : "--rerun" ++ }, ++ { ++ "argument" : "-W" ++ } ++ ], ++ "targetForVariableExpansion" : { ++ "containerPath" : "container:visionOSTestbed.xcodeproj", ++ "identifier" : "607A66112B0EFA380010BFC8", ++ "name" : "visionOSTestbed" ++ } ++ }, ++ "testTargets" : [ ++ { ++ "parallelizable" : false, ++ "target" : { ++ "containerPath" : "container:visionOSTestbed.xcodeproj", ++ "identifier" : "607A662C2B0EFA3A0010BFC8", ++ "name" : "TestbedTests" ++ } ++ } ++ ], ++ "version" : 1 ++} +\ No newline at end of file +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed/AppDelegate.h +@@ -0,0 +1,11 @@ ++// ++// AppDelegate.h ++// visionOSTestbed ++// ++ ++#import ++ ++@interface AppDelegate : UIResponder ++ ++ ++@end +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed/AppDelegate.m +@@ -0,0 +1,19 @@ ++// ++// AppDelegate.m ++// visionOSTestbed ++// ++ ++#import "AppDelegate.h" ++ ++@interface AppDelegate () ++ ++@end ++ ++@implementation AppDelegate ++ ++ ++- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ++ return YES; ++} ++ ++@end +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json +@@ -0,0 +1,11 @@ ++{ ++ "colors" : [ ++ { ++ "idiom" : "universal" ++ } ++ ], ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json +@@ -0,0 +1,13 @@ ++{ ++ "images" : [ ++ { ++ "idiom" : "universal", ++ "platform" : "ios", ++ "size" : "1024x1024" ++ } ++ ], ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed/Assets.xcassets/Contents.json +@@ -0,0 +1,6 @@ ++{ ++ "info" : { ++ "author" : "xcode", ++ "version" : 1 ++ } ++} +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed/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 ++iOS Framework form. ++ ++When the test suite runs, this folder will be on the PYTHONPATH, and will be the ++working directory for the test suite. +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed/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 ++iOS Framework form. ++ ++When the test suite runs, this folder will be on the PYTHONPATH. +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed/main.m +@@ -0,0 +1,16 @@ ++// ++// main.m ++// visionOSTestbed ++// ++ ++#import ++#import "AppDelegate.h" ++ ++int main(int argc, char * argv[]) { ++ NSString * appDelegateClassName; ++ @autoreleasepool { ++ appDelegateClassName = NSStringFromClass([AppDelegate class]); ++ ++ return UIApplicationMain(argc, argv, nil, appDelegateClassName); ++ } ++} +--- /dev/null ++++ b/Apple/testbed/visionOSTestbed/visionOSTestbed-Info.plist +@@ -0,0 +1,56 @@ ++ ++ ++ ++ ++ CFBundleDevelopmentRegion ++ en ++ CFBundleDisplayName ++ ${PRODUCT_NAME} ++ CFBundleExecutable ++ ${EXECUTABLE_NAME} ++ CFBundleIdentifier ++ org.python.visionOSTestbed ++ CFBundleInfoDictionaryVersion ++ 6.0 ++ CFBundleName ++ ${PRODUCT_NAME} ++ CFBundlePackageType ++ APPL ++ CFBundleShortVersionString ++ 1.0 ++ CFBundleSignature ++ ???? ++ CFBundleVersion ++ 1 ++ TestArgs ++ ++ test ++ -uall ++ --single-process ++ --rerun ++ -W ++ ++ UIApplicationSceneManifest ++ ++ UIApplicationSupportsMultipleScenes ++ ++ UISceneConfigurations ++ ++ ++ UIRequiresFullScreen ++ ++ UISupportedInterfaceOrientations ++ ++ UIInterfaceOrientationPortrait ++ UIInterfaceOrientationLandscapeLeft ++ UIInterfaceOrientationLandscapeRight ++ ++ UISupportedInterfaceOrientations~ipad ++ ++ UIInterfaceOrientationPortrait ++ UIInterfaceOrientationPortraitUpsideDown ++ UIInterfaceOrientationLandscapeLeft ++ UIInterfaceOrientationLandscapeRight ++ ++ ++ +--- /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 ++====================================== +--- /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@ ++ ++ +--- /dev/null ++++ b/Apple/tvOS/Resources/bin/arm64-apple-tvos-ar +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvos${TVOS_SDK_VERSION} ar "$@" +--- /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} "$@" +--- /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} "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 +--- /dev/null ++++ b/Apple/visionOS/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 ++ ++ XROS ++ ++ MinimumOSVersion ++ @XROS_DEPLOYMENT_TARGET@ ++ ++ +--- /dev/null ++++ b/Apple/visionOS/Resources/bin/arm64-apple-xros-ar +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk xros${XROS_SDK_VERSION} ar "$@" +--- /dev/null ++++ b/Apple/visionOS/Resources/bin/arm64-apple-xros-clang +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk xros${XROS_SDK_VERSION} clang -target arm64-apple-xros${XROS_DEPLOYMENT_TARGET} "$@" +--- /dev/null ++++ b/Apple/visionOS/Resources/bin/arm64-apple-xros-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk xros${XROS_SDK_VERSION} clang++ -target arm64-apple-xros${XROS_DEPLOYMENT_TARGET} "$@" +--- /dev/null ++++ b/Apple/visionOS/Resources/bin/arm64-apple-xros-cpp +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk xros${XROS_SDK_VERSION} clang -target arm64-apple-xros${XROS_DEPLOYMENT_TARGET} -E "$@" +--- /dev/null ++++ b/Apple/visionOS/Resources/bin/arm64-apple-xros-simulator-ar +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk xrsimulator${XROS_SDK_VERSION} ar "$@" +--- /dev/null ++++ b/Apple/visionOS/Resources/bin/arm64-apple-xros-simulator-clang +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk xrsimulator${XROS_SDK_VERSION} clang -target arm64-apple-xros${XROS_DEPLOYMENT_TARGET}-simulator "$@" +--- /dev/null ++++ b/Apple/visionOS/Resources/bin/arm64-apple-xros-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk xrsimulator${XROS_SDK_VERSION} clang++ -target arm64-apple-xros${XROS_DEPLOYMENT_TARGET}-simulator "$@" +--- /dev/null ++++ b/Apple/visionOS/Resources/bin/arm64-apple-xros-simulator-cpp +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk xrsimulator${XROS_SDK_VERSION} clang -target arm64-apple-xros${XROS_DEPLOYMENT_TARGET}-simulator -E "$@" +--- /dev/null ++++ b/Apple/visionOS/Resources/bin/arm64-apple-xros-simulator-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk xrsimulator${XROS_SDK_VERSION} strip -arch arm64 "$@" +--- /dev/null ++++ b/Apple/visionOS/Resources/bin/arm64-apple-xros-strip +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk xros${XROS_SDK_VERSION} strip -arch arm64 "$@" +--- /dev/null ++++ b/Apple/visionOS/Resources/pyconfig.h +@@ -0,0 +1,3 @@ ++#ifdef __arm64__ ++#include "pyconfig-arm64.h" ++#endif +--- /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 ++====================================== +--- /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@ ++ ++ +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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} "$@" +--- /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} "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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 "$@" +--- /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/using/ios.rst b/Doc/using/ios.rst +index 9921fd6114b..4ed18f4938c 100644 +--- a/Doc/using/ios.rst ++++ b/Doc/using/ios.rst +@@ -170,7 +170,7 @@ + To add Python to an iOS Xcode project: + + 1. Build or obtain a Python ``XCFramework``. See the instructions in +- :source:`iOS/README.rst` (in the CPython source distribution) for details on ++ :source:`Apple/iOS/README.md` (in the CPython source distribution) for details on + how to build a Python ``XCFramework``. At a minimum, you will need a build + that supports ``arm64-apple-ios``, plus one of either + ``arm64-apple-ios-simulator`` or ``x86_64-apple-ios-simulator``. +@@ -180,22 +180,19 @@ + of your project; however, you can use any other location that you want by + adjusting paths as needed. + +-3. Drag the ``iOS/Resources/dylib-Info-template.plist`` file into your project, +- and ensure it is associated with the app target. +- +-4. Add your application code as a folder in your Xcode project. In the ++3. Add your application code as a folder in your Xcode project. In the + following instructions, we'll assume that your user code is in a folder + named ``app`` in the root of your project; you can use any other location by + adjusting paths as needed. Ensure that this folder is associated with your + app target. + +-5. Select the app target by selecting the root node of your Xcode project, then ++4. Select the app target by selecting the root node of your Xcode project, then + the target name in the sidebar that appears. + +-6. In the "General" settings, under "Frameworks, Libraries and Embedded ++5. In the "General" settings, under "Frameworks, Libraries and Embedded + Content", add ``Python.xcframework``, with "Embed & Sign" selected. + +-7. In the "Build Settings" tab, modify the following: ++6. In the "Build Settings" tab, modify the following: + + - Build Options + +@@ -211,86 +208,24 @@ + + * Quoted Include In Framework Header: No + +-8. Add a build step that copies the Python standard library into your app. In +- the "Build Phases" tab, add a new "Run Script" build step *before* the +- "Embed Frameworks" step, but *after* the "Copy Bundle Resources" step. Name +- the step "Install Target Specific Python Standard Library", disable the +- "Based on dependency analysis" checkbox, and set the script content to: ++7. Add a build step that processes the Python standard library, and your own ++ Python binary dependencies. In the "Build Phases" tab, add a new "Run ++ Script" build step *before* the "Embed Frameworks" step, but *after* the ++ "Copy Bundle Resources" step. Name the step "Process Python libraries", ++ disable the "Based on dependency analysis" checkbox, and set the script ++ content to: + + .. code-block:: bash + +- set -e +- +- mkdir -p "$CODESIGNING_FOLDER_PATH/python/lib" +- if [ "$EFFECTIVE_PLATFORM_NAME" = "-iphonesimulator" ]; then +- echo "Installing Python modules for iOS Simulator" +- rsync -au --delete "$PROJECT_DIR/Python.xcframework/ios-arm64_x86_64-simulator/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" +- else +- echo "Installing Python modules for iOS Device" +- rsync -au --delete "$PROJECT_DIR/Python.xcframework/ios-arm64/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" +- fi ++ set -e ++ source $PROJECT_DIR/Python.xcframework/build/build_utils.sh ++ install_python Python.xcframework app + +- Note that the name of the simulator "slice" in the XCframework may be +- different, depending the CPU architectures your ``XCFramework`` supports. ++ If you have placed your XCframework somewhere other than the root of your ++ project, modify the path to the first argument. + +-9. Add a second build step that processes the binary extension modules in the +- standard library into "Framework" format. Add a "Run Script" build step +- *directly after* the one you added in step 8, named "Prepare Python Binary +- Modules". It should also have "Based on dependency analysis" unchecked, with +- the following script content: +- +- .. code-block:: bash +- +- set -e +- +- install_dylib () { +- INSTALL_BASE=$1 +- FULL_EXT=$2 +- +- # The name of the extension file +- EXT=$(basename "$FULL_EXT") +- # The location of the extension file, relative to the bundle +- RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} +- # The path to the extension file, relative to the install base +- PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/} +- # The full dotted name of the extension module, constructed from the file path. +- FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d "." -f 1 | tr "/" "."); +- # A bundle identifier; not actually used, but required by Xcode framework packaging +- FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr "_" "-") +- # The name of the framework folder. +- FRAMEWORK_FOLDER="Frameworks/$FULL_MODULE_NAME.framework" +- +- # If the framework folder doesn't exist, create it. +- if [ ! -d "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ]; then +- echo "Creating framework for $RELATIVE_EXT" +- mkdir -p "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" +- cp "$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" +- plutil -replace CFBundleExecutable -string "$FULL_MODULE_NAME" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" +- plutil -replace CFBundleIdentifier -string "$FRAMEWORK_BUNDLE_ID" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" +- fi +- +- echo "Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME" +- mv "$FULL_EXT" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" +- # Create a placeholder .fwork file where the .so was +- echo "$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" > ${FULL_EXT%.so}.fwork +- # Create a back reference to the .so file location in the framework +- echo "${RELATIVE_EXT%.so}.fwork" > "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin" +- } +- +- PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib") +- echo "Install Python $PYTHON_VER standard library extension modules..." +- find "$CODESIGNING_FOLDER_PATH/python/lib/$PYTHON_VER/lib-dynload" -name "*.so" | while read FULL_EXT; do +- install_dylib python/lib/$PYTHON_VER/lib-dynload/ "$FULL_EXT" +- done +- +- # Clean up dylib template +- rm -f "$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist" +- +- echo "Signing frameworks as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)..." +- find "$CODESIGNING_FOLDER_PATH/Frameworks" -name "*.framework" -exec /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der "{}" \; +- +-10. Add Objective C code to initialize and use a Python interpreter in embedded +- mode. You should ensure that: ++8. Add Objective C code to initialize and use a Python interpreter in embedded ++ mode. You should ensure that: + + * UTF-8 mode (:c:member:`PyPreConfig.utf8_mode`) is *enabled*; + * Buffered stdio (:c:member:`PyConfig.buffered_stdio`) is *disabled*; +@@ -307,22 +242,19 @@ + Your app's bundle location can be determined using ``[[NSBundle mainBundle] + resourcePath]``. + +-Steps 8, 9 and 10 of these instructions assume that you have a single folder of ++Steps 7 and 8 of these instructions assume that you have a single folder of + pure Python application code, named ``app``. If you have third-party binary + modules in your app, some additional steps will be required: + + * You need to ensure that any folders containing third-party binaries are +- either associated with the app target, or copied in as part of step 8. Step 8 +- should also purge any binaries that are not appropriate for the platform a +- specific build is targeting (i.e., delete any device binaries if you're +- building an app targeting the simulator). +- +-* Any folders that contain third-party binaries must be processed into +- framework form by step 9. The invocation of ``install_dylib`` that processes +- the ``lib-dynload`` folder can be copied and adapted for this purpose. ++ either associated with the app target, or are explicitly copied as part of ++ step 7. Step 7 should also purge any binaries that are not appropriate for ++ the platform a specific build is targeting (i.e., delete any device binaries ++ if you're building an app targeting the simulator). + +-* If you're using a separate folder for third-party packages, ensure that folder +- is included as part of the :envvar:`PYTHONPATH` configuration in step 10. ++* If you're using a separate folder for third-party packages, ensure that ++ folder is added to the end of the call to ``install_python`` in step 7, and ++ as part of the :envvar:`PYTHONPATH` configuration in step 8. + + * If any of the folders that contain third-party packages will contain ``.pth`` + files, you should add that folder as a *site directory* (using +@@ -332,25 +264,30 @@ + Testing a Python package + ------------------------ + +-The CPython source tree contains :source:`a testbed project ` that ++The CPython source tree contains :source:`a testbed project ` that + is used to run the CPython test suite on the iOS simulator. This testbed can also + be used as a testbed project for running your Python library's test suite on iOS. + +-After building or obtaining an iOS XCFramework (See :source:`iOS/README.rst` +-for details), create a clone of the Python iOS testbed project by running: ++After building or obtaining an iOS XCFramework (see :source:`Apple/iOS/README.md` ++for details), create a clone of the Python iOS testbed project. If you used the ++``Apple`` build script to build the XCframework, you can run: ++ ++.. code-block:: bash ++ ++ $ python cross-build/iOS/testbed clone --app --app app-testbed ++ ++Or, if you've sourced your own XCframework, by running: + + .. code-block:: bash + +- $ python iOS/testbed clone --framework --app --app app-testbed ++ $ python Apple/testbed clone --platform iOS --framework --app --app app-testbed + +-You will need to modify the ``iOS/testbed`` reference to point to that +-directory in the CPython source tree; any folders specified with the ``--app`` +-flag will be copied into the cloned testbed project. The resulting testbed will +-be created in the ``app-testbed`` folder. In this example, the ``module1`` and +-``module2`` would be importable modules at runtime. If your project has +-additional dependencies, they can be installed into the +-``app-testbed/iOSTestbed/app_packages`` folder (using ``pip install --target +-app-testbed/iOSTestbed/app_packages`` or similar). ++Any folders specified with the ``--app`` flag will be copied into the cloned ++testbed project. The resulting testbed will be created in the ``app-testbed`` ++folder. In this example, the ``module1`` and ``module2`` would be importable ++modules at runtime. If your project has additional dependencies, they can be ++installed into the ``app-testbed/Testbed/app_packages`` folder (using ``pip ++install --target app-testbed/Testbed/app_packages`` or similar). + + You can then use the ``app-testbed`` folder to run the test suite for your app, + For example, if ``module1.tests`` was the entry point to your test suite, you +@@ -379,7 +316,7 @@ + arguments. + + The test plan also disables parallel testing, and specifies the use of the +-``iOSTestbed.lldbinit`` file for providing configuration of the debugger. The ++``Testbed.lldbinit`` file for providing configuration of the debugger. The + default debugger configuration disables automatic breakpoints on the + ``SIGINT``, ``SIGUSR1``, ``SIGUSR2``, and ``SIGXFSZ`` signals. + +@@ -389,7 +326,12 @@ + The only mechanism for distributing apps to third-party iOS devices is to + submit the app to the iOS App Store; apps submitted for distribution must pass + Apple's app review process. This process includes a set of automated validation +-rules that inspect the submitted application bundle for problematic code. ++rules that inspect the submitted application bundle for problematic code. There ++are some steps that must be taken to ensure that your app will be able to pass ++these validation steps. ++ ++Incompatible code in the standard library ++----------------------------------------- + + The Python standard library contains some code that is known to violate these + automated rules. While these violations appear to be false positives, Apple's +@@ -400,3 +342,18 @@ + :source:`a patch file ` that will remove + all code that is known to cause issues with the App Store review process. This + patch is applied automatically when building for iOS. ++ ++Privacy manifests ++----------------- ++ ++In April 2025, Apple introduced a requirement for `certain third-party ++libraries to provide a Privacy Manifest ++`__. ++As a result, if you have a binary module that uses one of the affected ++libraries, you must provide an ``.xcprivacy`` file for that library. ++OpenSSL is one library affected by this requirement, but there are others. ++ ++If you produce a binary module named ``mymodule.so``, and use you the Xcode ++build script described in step 7 above, you can place a ``mymodule.xcprivacy`` ++file next to ``mymodule.so``, and the privacy manifest will be installed into ++the required location when the binary module is converted into a framework. +diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py +index 80651dc64ce..8a8f3e95505 100644 +--- a/Lib/ctypes/__init__.py ++++ b/Lib/ctypes/__init__.py +@@ -380,9 +380,9 @@ + + else: + def _load_library(self, name, mode, handle, winmode): +- # 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 the filename that has been provided is an iOS, tvOS, visionOS ++ # or watchOS .fwork file, dereference the location to the true ++ # origin of the binary. + if name and name.endswith(".fwork"): + with open(name) as f: + name = _os.path.join( +diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py +index 117bf06cb01..87611a6d03f 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 in {"darwin", "ios", "tvos", "watchos"}: ++elif os.name == "posix" and sys.platform in {"darwin", "ios", "tvos", "visionos", "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 41f538acb03..02fc9b9daf3 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', 'ios', 'tvos', 'watchos' ++_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY = 'cygwin', 'darwin', 'ios', 'tvos', 'visionos', 'watchos' + _CASE_INSENSITIVE_PLATFORMS = (_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY + + _CASE_INSENSITIVE_PLATFORMS_STR_KEY) + +@@ -1802,7 +1802,7 @@ + """ + extension_loaders = [] + if hasattr(_imp, 'create_dynamic'): +- if sys.platform in {"ios", "tvos", "watchos"}: ++ if sys.platform in {"ios", "tvos", "visionos", "watchos"}: + extension_loaders = [(AppleFrameworkLoader, [ + suffix.replace(".so", ".fwork") + for suffix in _imp.extension_suffixes() +diff --git a/Lib/platform.py b/Lib/platform.py +index 8895177e326..9e1e0628671 100755 +--- a/Lib/platform.py ++++ b/Lib/platform.py +@@ -522,6 +522,78 @@ + 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) ++ ++ ++# A namedtuple for visionOS version information. ++VisionOSVersionInfo = collections.namedtuple( ++ "VisionOSVersionInfo", ++ ["system", "release", "model", "is_simulator"] ++) ++ ++ ++def visionos_ver(system="", release="", model="", is_simulator=False): ++ """Get visionOS 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 == "visionos": ++ # TODO: Can the iOS implementation be used here? ++ import _ios_support ++ result = _ios_support.get_platform_ios() ++ if result is not None: ++ return VisionOSVersionInfo(*result) ++ ++ return VisionOSVersionInfo(system, release, model, is_simulator) ++ ++ + def _java_getprop(name, default): + """This private helper is deprecated in 3.13 and will be removed in 3.15""" + from java.lang import System +@@ -721,7 +793,7 @@ + default in case the command should fail. + + """ +- if sys.platform in {'dos', 'win32', 'win16', 'ios', 'tvos', 'watchos'}: ++ if sys.platform in {'dos', 'win32', 'win16', 'ios', 'tvos', 'visionos', 'watchos'}: + # XXX Others too ? + return default + +@@ -885,14 +957,30 @@ + csid, cpu_number = vms_lib.getsyi('SYI$_CPU', 0) + return 'Alpha' if cpu_number >= 128 else 'VAX' + +- # On the iOS 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 iOS devices, so we know the right answer. ++ # On the iOS/tvOS/visionOS/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_visionos(): ++ 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` +@@ -1052,9 +1140,15 @@ + system = 'Android' + release = android_ver().release + +- # Normalize responses on iOS ++ # 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 == 'visionos': ++ system, release, _, _ = visionos_ver() ++ if sys.platform == 'watchos': ++ system, release, _, _ = watchos_ver() + + vals = system, node, release, version, machine + # Replace 'unknown' values with the more portable '' +@@ -1344,6 +1438,12 @@ + # 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 == "visionos": ++ system, release, _, _ = visionos_ver() ++ elif sys.platform == "watchos": ++ system, release, _, _ = watchos_ver() + else: + macos_release = mac_ver()[0] + if macos_release: +diff --git a/Lib/site.py b/Lib/site.py +index 041dca113a5..3dedc828524 100644 +--- a/Lib/site.py ++++ b/Lib/site.py +@@ -292,8 +292,8 @@ + if env_base: + return env_base + +- # Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories +- if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}: ++ # Emscripten, iOS, tvOS, visionOS, VxWorks, WASI, and watchOS have no home directories ++ if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "visionos", "wasi", "watchos"}: + return None + + def joinuser(*args): +diff --git a/Lib/subprocess.py b/Lib/subprocess.py +index 885f0092b53..0e04efb25ce 100644 +--- a/Lib/subprocess.py ++++ b/Lib/subprocess.py +@@ -75,7 +75,7 @@ + _mswindows = True + + # some platforms do not support subprocesses +-_can_fork_exec = sys.platform not in {"emscripten", "wasi", "ios", "tvos", "watchos"} ++_can_fork_exec = sys.platform not in {"emscripten", "wasi", "ios", "tvos", "visionos", "watchos"} + + if _mswindows: + import _winapi +diff --git a/Lib/sysconfig/__init__.py b/Lib/sysconfig/__init__.py +index f7bd675bb3b..98ee0cf234c 100644 +--- a/Lib/sysconfig/__init__.py ++++ b/Lib/sysconfig/__init__.py +@@ -23,6 +23,9 @@ + _ALWAYS_STR = { + 'IPHONEOS_DEPLOYMENT_TARGET', + 'MACOSX_DEPLOYMENT_TARGET', ++ 'TVOS_DEPLOYMENT_TARGET', ++ 'WATCHOS_DEPLOYMENT_TARGET', ++ 'XROS_DEPLOYMENT_TARGET', + } + + _INSTALL_SCHEMES = { +@@ -117,7 +120,7 @@ + return env_base + + # Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories +- if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}: ++ if sys.platform in {"emscripten", "ios", "tvos", "visionos", "vxworks", "wasi", "watchos"}: + return None + + def joinuser(*args): +@@ -680,6 +683,18 @@ + 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", "12.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 ++ elif sys.platform == "visionos": ++ release = get_config_vars().get("XROS_DEPLOYMENT_TARGET", "2.0") ++ osname = sys.platform ++ machine = sys.implementation._multiarch + else: + import _osx_support + osname, release, machine = _osx_support.get_platform_osx( +diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py +index 22ab655507a..368100eeb29 100644 +--- a/Lib/test/datetimetester.py ++++ b/Lib/test/datetimetester.py +@@ -6840,9 +6840,9 @@ + self.assertEqual(dt_orig, dt_rt) + + def test_type_check_in_subinterp(self): +- # iOS requires the use of the custom framework loader, ++ # Apple mobile platforms require the use of the custom framework loader, + # not the ExtensionFileLoader. +- if sys.platform == "ios": ++ if support.is_apple_mobile: + extension_loader = "AppleFrameworkLoader" + else: + extension_loader = "ExtensionFileLoader" +diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py +index 4605938b875..cc65b3d63dc 100644 +--- a/Lib/test/support/__init__.py ++++ b/Lib/test/support/__init__.py +@@ -64,6 +64,7 @@ + "force_not_colorized_test_class", + "make_clean_env", + "BrokenIter", ++ "reset_code", "on_github_actions" + ] + + +@@ -582,7 +583,7 @@ + + is_android = sys.platform == "android" + +-if sys.platform not in {"win32", "vxworks", "ios", "tvos", "watchos"}: ++if sys.platform not in {"win32", "vxworks", "ios", "tvos", "visionos", "watchos"}: + unix_shell = '/system/bin/sh' if is_android else '/bin/sh' + else: + unix_shell = None +@@ -592,7 +593,7 @@ + is_emscripten = sys.platform == "emscripten" + is_wasi = sys.platform == "wasi" + +-is_apple_mobile = sys.platform in {"ios", "tvos", "watchos"} ++is_apple_mobile = sys.platform in {"ios", "tvos", "visionos", "watchos"} + is_apple = is_apple_mobile or sys.platform == "darwin" + + has_fork_support = hasattr(os, "fork") and not ( +@@ -1330,6 +1331,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/test__interpreters.py b/Lib/test/test__interpreters.py +index 6fb2bc24fa3..88cb6691cd8 100644 +--- a/Lib/test/test__interpreters.py ++++ b/Lib/test/test__interpreters.py +@@ -695,6 +695,7 @@ + f'assert(obj == {obj!r})', + ) + ++ @support.requires_subprocess() + def test_os_exec(self): + expected = 'spam spam spam spam spam' + subinterp = _interpreters.create() +diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py +index ed277276b51..c846a9bc5d7 100644 +--- a/Lib/test/test_platform.py ++++ b/Lib/test/test_platform.py +@@ -236,13 +236,21 @@ + if sys.platform == "android": + self.assertEqual(res.system, "Android") + self.assertEqual(res.release, platform.android_ver().release) +- elif sys.platform == "ios": ++ elif support.is_apple_mobile: + # 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) ++ if sys.platform == "ios": ++ # iPads also identify as iOS ++ self.assertIn(res.system, {"iOS", "iPadOS"}) ++ else: ++ # All other platforms - sys.platform is the lower case ++ # form of system (e.g., visionOS->visionos) ++ self.assertEqual(res.system.lower(), sys.platform) ++ # Use the platform-specific version method ++ platform_ver = getattr(platform, f"{sys.platform}_ver") ++ self.assertEqual(res.release, platform_ver().release) + else: + self.assertEqual(res.system, "") + self.assertEqual(res.release, "") +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) + + @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) + + @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_webbrowser.py b/Lib/test/test_webbrowser.py +index 4fcbc5c2e59..851bd6e8f3c 100644 +--- a/Lib/test/test_webbrowser.py ++++ b/Lib/test/test_webbrowser.py +@@ -234,7 +234,8 @@ + arguments=[f'openURL({URL},new-tab)']) + + +-@unittest.skipUnless(sys.platform == "ios", "Test only applicable to iOS") ++@unittest.skipUnless(sys.platform in {"ios", "visionOS"}, ++ "Test only applicable to iOS and visionOS") + class IOSBrowserTest(unittest.TestCase): + def _obj_ref(self, *args): + # Construct a string representation of the arguments that can be used +diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py +index 2f9555ad60d..249756ba4de 100755 +--- a/Lib/webbrowser.py ++++ b/Lib/webbrowser.py +@@ -489,7 +489,8 @@ + # OS X can use below Unix support (but we prefer using the OS X + # specific stuff) + +- if sys.platform == "ios": ++ if sys.platform in {"ios", "visionos"}: ++ # iOS and visionOS provide a browser; tvOS and watchOS don't. + register("iosbrowser", None, IOSBrowser(), preferred=True) + + if sys.platform == "serenityos": +@@ -616,9 +617,10 @@ + return not rc + + # +-# Platform support for iOS ++# Platform support for Apple Mobile platforms that provide a browser ++# (i.e., iOS and visionOS) + # +-if sys.platform == "ios": ++if sys.platform in {"ios", "visionos"}: + from _ios_support import objc + if objc: + # If objc exists, we know ctypes is also importable. +diff --git a/Makefile.pre.in b/Makefile.pre.in +index a7dc9709d62..304a9f6fc3a 100644 +--- a/Makefile.pre.in ++++ b/Makefile.pre.in +@@ -202,6 +202,12 @@ + # the build, and is only listed here so it will be included in sysconfigdata. + IPHONEOS_DEPLOYMENT_TARGET=@IPHONEOS_DEPLOYMENT_TARGET@ + ++# visionOS Deployment target is *actually* used during the build, by the ++# compiler shims; export. ++XROS_DEPLOYMENT_TARGET=@XROS_DEPLOYMENT_TARGET@ ++@EXPORT_XROS_DEPLOYMENT_TARGET@export XROS_DEPLOYMENT_TARGET ++ ++ + # Option to install to strip binaries + STRIPFLAG=-s + +@@ -2087,7 +2093,7 @@ + fi + + # Clone the testbed project into the XCFOLDER +- $(PYTHON_FOR_BUILD) $(srcdir)/iOS/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)" ++ $(PYTHON_FOR_BUILD) $(srcdir)/Apple/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)" + + # Run the testbed project + $(PYTHON_FOR_BUILD) "$(XCFOLDER)" run --verbose -- test -uall --single-process --rerun -W +@@ -2791,6 +2797,9 @@ + $(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); \ +@@ -2976,10 +2985,10 @@ + -find build -type f -a ! -name '*.gc??' -exec rm -f {} ';' + -rm -f Include/pydtrace_probes.h + -rm -f profile-gen-stamp +- -rm -rf iOS/testbed/Python.xcframework/ios-*/bin +- -rm -rf iOS/testbed/Python.xcframework/ios-*/lib +- -rm -rf iOS/testbed/Python.xcframework/ios-*/include +- -rm -rf iOS/testbed/Python.xcframework/ios-*/Python.framework ++ -rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/bin ++ -rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/lib ++ -rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/include ++ -rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/Python.framework + + .PHONY: profile-removal + profile-removal: +@@ -3005,7 +3014,7 @@ + config.cache config.log pyconfig.h Modules/config.c + -rm -rf build platform + -rm -rf $(PYTHONFRAMEWORKDIR) +- -rm -rf iOS/Frameworks ++ -rm -rf Apple/iOS/Frameworks + -rm -rf iOSTestbed.* + -rm -f python-config.py python-config + -rm -rf cross-build +diff --git a/Misc/platform_triplet.c b/Misc/platform_triplet.c +index f5cd73bdea8..6c1863c943b 100644 +--- a/Misc/platform_triplet.c ++++ b/Misc/platform_triplet.c +@@ -257,6 +257,32 @@ + # else + PLATFORM_TRIPLET=arm64-iphoneos + # endif ++# elif defined(TARGET_OS_TV) && TARGET_OS_TV ++# if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR ++# if __x86_64__ ++PLATFORM_TRIPLET=x86_64-appletvsimulator ++# else ++PLATFORM_TRIPLET=arm64-appletvsimulator ++# endif ++# else ++PLATFORM_TRIPLET=arm64-appletvos ++# endif ++# elif defined(TARGET_OS_WATCH) && TARGET_OS_WATCH ++# if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR ++# if __x86_64__ ++PLATFORM_TRIPLET=x86_64-watchsimulator ++# else ++PLATFORM_TRIPLET=arm64-watchsimulator ++# endif ++# else ++PLATFORM_TRIPLET=arm64_32-watchos ++# endif ++# elif defined(TARGET_OS_VISION) && TARGET_OS_VISION ++# if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR ++PLATFORM_TRIPLET=arm64-xrsimulator ++# else ++PLATFORM_TRIPLET=arm64-xros ++# endif + // Older macOS SDKs do not define TARGET_OS_OSX + # elif !defined(TARGET_OS_OSX) || TARGET_OS_OSX + PLATFORM_TRIPLET=darwin +diff --git a/config.sub b/config.sub +index 1bb6a05dc11..49febd56a37 100755 +--- a/config.sub ++++ b/config.sub +@@ -1743,7 +1743,7 @@ + | hpux* | unos* | osf* | luna* | dgux* | auroraux* | solaris* \ + | sym* | plan9* | psp* | sim* | xray* | os68k* | v88r* \ + | hiux* | abug | nacl* | netware* | windows* \ +- | os9* | macos* | osx* | ios* | tvos* | watchos* \ ++ | os9* | macos* | osx* | ios* | tvos* | watchos* | xros* \ + | mpw* | magic* | mmixware* | mon960* | lnews* \ + | amigaos* | amigados* | msdos* | newsos* | unicos* | aof* \ + | aos* | aros* | cloudabi* | sortix* | twizzler* \ +@@ -1867,7 +1867,7 @@ + ;; + *-eabi*- | *-gnueabi*-) + ;; +- ios*-simulator- | tvos*-simulator- | watchos*-simulator- ) ++ ios*-simulator- | tvos*-simulator- | watchos*-simulator- | xros*-simulator-) + ;; + none--*) + # None (no kernel, i.e. freestanding / bare metal), +diff --git a/configure b/configure +index 901a26c9602..508d01f1b7e 100755 +--- a/configure ++++ b/configure +@@ -979,6 +979,10 @@ + CFLAGS + CC + HAS_XCRUN ++EXPORT_XROS_DEPLOYMENT_TARGET ++WATCHOS_DEPLOYMENT_TARGET ++XROS_DEPLOYMENT_TARGET ++TVOS_DEPLOYMENT_TARGET + IPHONEOS_DEPLOYMENT_TARGET + EXPORT_MACOSX_DEPLOYMENT_TARGET + CONFIGURE_MACOSX_DEPLOYMENT_TARGET +@@ -4058,6 +4062,15 @@ + *-apple-ios*) + ac_sys_system=iOS + ;; ++ *-apple-tvos*) ++ ac_sys_system=tvOS ++ ;; ++ *-apple-xros*) ++ ac_sys_system=visionOS ++ ;; ++ *-apple-watchos*) ++ ac_sys_system=watchOS ++ ;; + *-*-vxworks*) + ac_sys_system=VxWorks + ;; +@@ -4112,7 +4125,7 @@ + # On cross-compile builds, configure will look for a host-specific compiler by + # prepending the user-provided host triple to the required binary name. + # +-# On iOS, this results in binaries like "arm64-apple-ios13.0-simulator-gcc", ++# On iOS/tvOS/visionOS/watchOS, this results in binaries like "arm64-apple-ios13.0-simulator-gcc", + # which isn't a binary that exists, and isn't very convenient, as it contains the + # iOS version. As the default cross-compiler name won't exist, configure falls + # back to gcc, which *definitely* won't work. We're providing wrapper scripts for +@@ -4127,6 +4140,17 @@ + aarch64-apple-ios*-simulator) AR=arm64-apple-ios-simulator-ar ;; + aarch64-apple-ios*) AR=arm64-apple-ios-ar ;; + x86_64-apple-ios*-simulator) AR=x86_64-apple-ios-simulator-ar ;; ++ ++ aarch64-apple-tvos*-simulator) AR=arm64-apple-tvos-simulator-ar ;; ++ aarch64-apple-tvos*) AR=arm64-apple-tvos-ar ;; ++ x86_64-apple-tvos*-simulator) AR=x86_64-apple-tvos-simulator-ar ;; ++ ++ aarch64-apple-xros*-simulator) AR=arm64-apple-xros-simulator-ar ;; ++ aarch64-apple-xros*) AR=arm64-apple-xros-ar ;; ++ ++ aarch64-apple-watchos*-simulator) AR=arm64-apple-watchos-simulator-ar ;; ++ aarch64-apple-watchos*) AR=arm64_32-apple-watchos-ar ;; ++ x86_64-apple-watchos*-simulator) AR=x86_64-apple-watchos-simulator-ar ;; + *) + esac + fi +@@ -4135,6 +4159,17 @@ + aarch64-apple-ios*-simulator) CC=arm64-apple-ios-simulator-clang ;; + aarch64-apple-ios*) CC=arm64-apple-ios-clang ;; + x86_64-apple-ios*-simulator) CC=x86_64-apple-ios-simulator-clang ;; ++ ++ aarch64-apple-tvos*-simulator) CC=arm64-apple-tvos-simulator-clang ;; ++ aarch64-apple-tvos*) CC=arm64-apple-tvos-clang ;; ++ x86_64-apple-tvos*-simulator) CC=x86_64-apple-tvos-simulator-clang ;; ++ ++ aarch64-apple-xros*-simulator) CC=arm64-apple-xros-simulator-clang ;; ++ aarch64-apple-xros*) CC=arm64-apple-xros-clang ;; ++ ++ aarch64-apple-watchos*-simulator) CC=arm64-apple-watchos-simulator-clang ;; ++ aarch64-apple-watchos*) CC=arm64_32-apple-watchos-clang ;; ++ x86_64-apple-watchos*-simulator) CC=x86_64-apple-watchos-simulator-clang ;; + *) + esac + fi +@@ -4143,6 +4178,17 @@ + aarch64-apple-ios*-simulator) CPP=arm64-apple-ios-simulator-cpp ;; + aarch64-apple-ios*) CPP=arm64-apple-ios-cpp ;; + x86_64-apple-ios*-simulator) CPP=x86_64-apple-ios-simulator-cpp ;; ++ ++ aarch64-apple-tvos*-simulator) CPP=arm64-apple-tvos-simulator-cpp ;; ++ aarch64-apple-tvos*) CPP=arm64-apple-tvos-cpp ;; ++ x86_64-apple-tvos*-simulator) CPP=x86_64-apple-tvos-simulator-cpp ;; ++ ++ aarch64-apple-xros*-simulator) CPP=arm64-apple-xros-simulator-cpp ;; ++ aarch64-apple-xros*) CPP=arm64-apple-xros-cpp ;; ++ ++ aarch64-apple-watchos*-simulator) CPP=arm64-apple-watchos-simulator-cpp ;; ++ aarch64-apple-watchos*) CPP=arm64_32-apple-watchos-cpp ;; ++ x86_64-apple-watchos*-simulator) CPP=x86_64-apple-watchos-simulator-cpp ;; + *) + esac + fi +@@ -4151,6 +4197,17 @@ + aarch64-apple-ios*-simulator) CXX=arm64-apple-ios-simulator-clang++ ;; + aarch64-apple-ios*) CXX=arm64-apple-ios-clang++ ;; + x86_64-apple-ios*-simulator) CXX=x86_64-apple-ios-simulator-clang++ ;; ++ ++ aarch64-apple-tvos*-simulator) CXX=arm64-apple-tvos-simulator-clang++ ;; ++ aarch64-apple-tvos*) CXX=arm64-apple-tvos-clang++ ;; ++ x86_64-apple-tvos*-simulator) CXX=x86_64-apple-tvos-simulator-clang++ ;; ++ ++ aarch64-apple-xros*-simulator) CXX=arm64-apple-xros-simulator-clang++ ;; ++ aarch64-apple-xros*) CXX=arm64-apple-xros-clang++ ;; ++ ++ aarch64-apple-watchos*-simulator) CXX=arm64-apple-watchos-simulator-clang++ ;; ++ aarch64-apple-watchos*) CXX=arm64_32-apple-watchos-clang++ ;; ++ x86_64-apple-watchos*-simulator) CXX=x86_64-apple-watchos-simulator-clang++ ;; + *) + esac + fi +@@ -4271,8 +4328,11 @@ + case $enableval in + yes) + case $ac_sys_system in +- Darwin) enableval=/Library/Frameworks ;; +- iOS) enableval=iOS/Frameworks/\$\(MULTIARCH\) ;; ++ Darwin) enableval=/Library/Frameworks ;; ++ iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; ++ tvOS) enableval=Apple/tvOS/Frameworks/\$\(MULTIARCH\) ;; ++ visionOS) enableval=Apple/visionOS/Frameworks/\$\(MULTIARCH\) ;; ++ watchOS) enableval=Apple/watchOS/Frameworks/\$\(MULTIARCH\) ;; + *) as_fn_error $? "Unknown platform for framework build" "$LINENO" 5 + esac + esac +@@ -4281,6 +4341,9 @@ + no) + case $ac_sys_system in + iOS) as_fn_error $? "iOS builds must use --enable-framework" "$LINENO" 5 ;; ++ tvOS) as_fn_error $? "tvOS builds must use --enable-framework" "$LINENO" 5 ;; ++ visionOS) as_fn_error $? "visionOS builds must use --enable-framework" "$LINENO" 5 ;; ++ watchOS) as_fn_error $? "watchOS builds must use --enable-framework" "$LINENO" 5 ;; + *) + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework +@@ -4383,9 +4446,54 @@ + + prefix=$PYTHONFRAMEWORKPREFIX + PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" +- RESSRCDIR=iOS/Resources ++ RESSRCDIR=Apple/iOS/Resources ++ ++ ac_config_files="$ac_config_files Apple/iOS/Resources/Info.plist" ++ ++ ;; ++ tvOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/tvOS/Resources ++ ++ ac_config_files="$ac_config_files Apple/tvOS/Resources/Info.plist" ++ ++ ;; ++ visionOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/visionOS/Resources ++ ++ ac_config_files="$ac_config_files Apple/visionOS/Resources/Info.plist" ++ ++ ;; ++ watchOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/watchOS/Resources + +- ac_config_files="$ac_config_files iOS/Resources/Info.plist" ++ ac_config_files="$ac_config_files Apple/watchOS/Resources/Info.plist" + + ;; + *) +@@ -4398,6 +4506,9 @@ + + case $ac_sys_system in + iOS) as_fn_error $? "iOS builds must use --enable-framework" "$LINENO" 5 ;; ++ tvOS) as_fn_error $? "tvOS builds must use --enable-framework" "$LINENO" 5 ;; ++ visionOS) as_fn_error $? "visionOS builds must use --enable-framework" "$LINENO" 5 ;; ++ watchOS) as_fn_error $? "watchOS builds must use --enable-framework" "$LINENO" 5 ;; + *) + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework +@@ -4451,8 +4562,8 @@ + case "$withval" in + yes) + case $ac_sys_system in +- Darwin|iOS) +- # iOS is able to share the macOS patch ++ Darwin|iOS|tvOS|visionOS|watchOS) ++ # iOS/tvOS/visionOS/watchOS is able to share the macOS patch + APP_STORE_COMPLIANCE_PATCH="Mac/Resources/app-store-compliance.patch" + ;; + *) as_fn_error $? "no default app store compliance patch available for $ac_sys_system" "$LINENO" 5 ;; +@@ -4470,8 +4581,8 @@ + else $as_nop + + case $ac_sys_system in +- iOS) +- # Always apply the compliance patch on iOS; we can use the macOS patch ++ iOS|tvOS|visionOS|watchOS) ++ # Always apply the compliance patch on iOS/tvOS/visionOS/watchOS; we can use the macOS patch + APP_STORE_COMPLIANCE_PATCH="Mac/Resources/app-store-compliance.patch" + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: applying default app store compliance patch" >&5 + printf "%s\n" "applying default app store compliance patch" >&6; } +@@ -4488,6 +4599,8 @@ + + + ++EXPORT_XROS_DEPLOYMENT_TARGET='#' ++ + + if test "$cross_compiling" = yes; then + case "$host" in +@@ -4525,6 +4638,78 @@ + ;; + esac + ;; ++ *-apple-tvos*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # TVOS_DEPLOYMENT_TARGET is the minimum supported tvOS version ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking tvOS deployment target" >&5 ++printf %s "checking tvOS deployment target... " >&6; } ++ TVOS_DEPLOYMENT_TARGET=${_host_os:4} ++ TVOS_DEPLOYMENT_TARGET=${TVOS_DEPLOYMENT_TARGET:=12.0} ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $TVOS_DEPLOYMENT_TARGET" >&5 ++printf "%s\n" "$TVOS_DEPLOYMENT_TARGET" >&6; } ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${TVOS_DEPLOYMENT_TARGET}-arm64-appletv${_host_device} ++ ;; ++ *) ++ _host_ident=${TVOS_DEPLOYMENT_TARGET}-$host_cpu-appletv${_host_device} ++ ;; ++ esac ++ ;; ++ *-apple-xros*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # XROS_DEPLOYMENT_TARGET is the minimum supported visionOS version ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking visionOS deployment target" >&5 ++printf %s "checking visionOS deployment target... " >&6; } ++ XROS_DEPLOYMENT_TARGET=${_host_os:8} ++ XROS_DEPLOYMENT_TARGET=${XROS_DEPLOYMENT_TARGET:=2.0} ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $XROS_DEPLOYMENT_TARGET" >&5 ++printf "%s\n" "$XROS_DEPLOYMENT_TARGET" >&6; } ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking exporting flag of visionOS deployment target" >&5 ++printf %s "checking exporting flag of visionOS deployment target... " >&6; } ++ export XROS_DEPLOYMENT_TARGET ++ EXPORT_XROS_DEPLOYMENT_TARGET='' ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $EXPORT_XROS_DEPLOYMENT_TARGET" >&5 ++printf "%s\n" "$EXPORT_XROS_DEPLOYMENT_TARGET" >&6; } ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${XROS_DEPLOYMENT_TARGET}-arm64-xr${_host_device} ++ ;; ++ *) ++ _host_ident=${XROS_DEPLOYMENT_TARGET}-$host_cpu-xr${_host_device} ++ ;; ++ esac ++ ;; ++ *-apple-watchos*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # WATCHOS_DEPLOYMENT_TARGET is the minimum supported watchOS version ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking watchOS deployment target" >&5 ++printf %s "checking watchOS deployment target... " >&6; } ++ WATCHOS_DEPLOYMENT_TARGET=${_host_os:7} ++ WATCHOS_DEPLOYMENT_TARGET=${WATCHOS_DEPLOYMENT_TARGET:=4.0} ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $WATCHOS_DEPLOYMENT_TARGET" >&5 ++printf "%s\n" "$WATCHOS_DEPLOYMENT_TARGET" >&6; } ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${WATCHOS_DEPLOYMENT_TARGET}-arm64-watch${_host_device} ++ ;; ++ *) ++ _host_ident=${WATCHOS_DEPLOYMENT_TARGET}-$host_cpu-watch${_host_device} ++ ;; ++ esac ++ ;; + *-*-vxworks*) + _host_ident=$host_cpu + ;; +@@ -4603,9 +4788,15 @@ + define_xopen_source=no;; + Darwin/[12][0-9].*) + define_xopen_source=no;; +- # On iOS, defining _POSIX_C_SOURCE also disables platform specific features. ++ # On iOS/tvOS/visionOS/watchOS, defining _POSIX_C_SOURCE also disables platform specific features. + iOS/*) + define_xopen_source=no;; ++ tvOS/*) ++ define_xopen_source=no;; ++ visionOS/*) ++ define_xopen_source=no;; ++ watchOS/*) ++ define_xopen_source=no;; + # On QNX 6.3.2, defining _XOPEN_SOURCE prevents netdb.h from + # defining NI_NUMERICHOST. + QNX/6.3.2) +@@ -4668,7 +4859,14 @@ + CONFIGURE_MACOSX_DEPLOYMENT_TARGET= + EXPORT_MACOSX_DEPLOYMENT_TARGET='#' + +-# Record the value of IPHONEOS_DEPLOYMENT_TARGET enforced by the selected host triple. ++# Record the value of IPHONEOS_DEPLOYMENT_TARGET / TVOS_DEPLOYMENT_TARGET / ++# XROS_DEPLOYMENT_TARGET / WATCHOS_DEPLOYMENT_TARGET enforced by the selected host triple. ++ ++ ++ ++ ++ ++# XROS_DEPLOYMENT_TARGET should get exported + + + # checks for alternative programs +@@ -4709,6 +4907,16 @@ + as_fn_append CFLAGS " -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}" + as_fn_append LDFLAGS " -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}" + ;; #( ++ tvOS) : ++ ++ as_fn_append CFLAGS " -mtvos-version-min=${TVOS_DEPLOYMENT_TARGET}" ++ as_fn_append LDFLAGS " -mtvos-version-min=${TVOS_DEPLOYMENT_TARGET}" ++ ;; #( ++ watchOS) : ++ ++ as_fn_append CFLAGS " -mwatchos-version-min=${WATCHOS_DEPLOYMENT_TARGET}" ++ as_fn_append LDFLAGS " -mwatchos-version-min=${WATCHOS_DEPLOYMENT_TARGET}" ++ ;; #( + *) : + ;; + esac +@@ -7010,6 +7218,12 @@ + MULTIARCH="" ;; #( + iOS) : + MULTIARCH="" ;; #( ++ tvOS) : ++ MULTIARCH="" ;; #( ++ visionOS) : ++ MULTIARCH="" ;; #( ++ watchOS) : ++ MULTIARCH="" ;; #( + FreeBSD*) : + MULTIARCH="" ;; #( + *) : +@@ -7030,7 +7244,7 @@ + printf "%s\n" "$MULTIARCH" >&6; } + + case $ac_sys_system in #( +- iOS) : ++ iOS|tvOS|visionOS|watchOS) : + SOABI_PLATFORM=`echo "$PLATFORM_TRIPLET" | cut -d '-' -f2` ;; #( + *) : + SOABI_PLATFORM=$PLATFORM_TRIPLET +@@ -7081,6 +7295,18 @@ + PY_SUPPORT_TIER=3 ;; #( + aarch64-apple-ios*/clang) : + PY_SUPPORT_TIER=3 ;; #( ++ aarch64-apple-tvos*-simulator/clang) : ++ PY_SUPPORT_TIER=3 ;; #( ++ aarch64-apple-tvos*/clang) : ++ PY_SUPPORT_TIER=3 ;; #( ++ aarch64-apple-xros*-simulator/clang) : ++ PY_SUPPORT_TIER=3 ;; #( ++ aarch64-apple-xros*/clang) : ++ PY_SUPPORT_TIER=3 ;; #( ++ aarch64-apple-watchos*-simulator/clang) : ++ PY_SUPPORT_TIER=3 ;; #( ++ arm64_32-apple-watchos*/clang) : ++ PY_SUPPORT_TIER=3 ;; #( + aarch64-*-linux-android/clang) : + PY_SUPPORT_TIER=3 ;; #( + x86_64-*-linux-android/clang) : +@@ -7554,7 +7780,7 @@ + case $ac_sys_system in + Darwin) + LDLIBRARY='$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)';; +- iOS) ++ iOS|tvOS|visionOS|watchOS) + LDLIBRARY='$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)';; + *) + as_fn_error $? "Unknown platform for framework build" "$LINENO" 5;; +@@ -7620,7 +7846,7 @@ + BLDLIBRARY='-L. -lpython$(LDVERSION)' + RUNSHARED=DYLD_LIBRARY_PATH=`pwd`${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}} + ;; +- iOS) ++ iOS|tvOS|visionOS|watchOS) + LDLIBRARY='libpython$(LDVERSION).dylib' + ;; + AIX*) +@@ -12975,7 +13201,7 @@ + BLDSHARED="$LDSHARED" + fi + ;; +- iOS/*) ++ iOS/*|tvOS/*|visionOS/*|watchOS/*) + LDSHARED='$(CC) -dynamiclib -F . -framework $(PYTHONFRAMEWORK)' + LDCXXSHARED='$(CXX) -dynamiclib -F . -framework $(PYTHONFRAMEWORK)' + BLDSHARED="$LDSHARED" +@@ -13108,7 +13334,7 @@ + Linux-android*) LINKFORSHARED="-pie -Xlinker -export-dynamic";; + Linux*|GNU*) LINKFORSHARED="-Xlinker -export-dynamic";; + # -u libsys_s pulls in all symbols in libsys +- Darwin/*|iOS/*) ++ Darwin/*|iOS/*|tvOS/*|visionOS/*|watchOS/*) + LINKFORSHARED="$extra_undefs -framework CoreFoundation" + + # Issue #18075: the default maximum stack size (8MBytes) is too +@@ -13132,7 +13358,7 @@ + LINKFORSHARED="$LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)' + fi + LINKFORSHARED="$LINKFORSHARED" +- elif test $ac_sys_system = "iOS"; then ++ elif test "$ac_sys_system" = "iOS" -o "$ac_sys_system" = "tvOS" -o "$ac_sys_system" = "visionOS" -o "$ac_sys_system" = "watchOS"; then + LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + fi + ;; +@@ -14728,7 +14954,7 @@ + + ctypes_malloc_closure=yes + ;; #( +- iOS) : ++ iOS|tvOS|visionOS|watchOS) : + + ctypes_malloc_closure=yes + ;; #( +@@ -18197,12 +18423,6 @@ + then : + printf "%s\n" "#define HAVE_DUP3 1" >>confdefs.h + +-fi +-ac_fn_c_check_func "$LINENO" "execv" "ac_cv_func_execv" +-if test "x$ac_cv_func_execv" = xyes +-then : +- printf "%s\n" "#define HAVE_EXECV 1" >>confdefs.h +- + fi + ac_fn_c_check_func "$LINENO" "explicit_bzero" "ac_cv_func_explicit_bzero" + if test "x$ac_cv_func_explicit_bzero" = xyes +@@ -18263,18 +18483,6 @@ + then : + printf "%s\n" "#define HAVE_FEXECVE 1" >>confdefs.h + +-fi +-ac_fn_c_check_func "$LINENO" "fork" "ac_cv_func_fork" +-if test "x$ac_cv_func_fork" = xyes +-then : +- printf "%s\n" "#define HAVE_FORK 1" >>confdefs.h +- +-fi +-ac_fn_c_check_func "$LINENO" "fork1" "ac_cv_func_fork1" +-if test "x$ac_cv_func_fork1" = xyes +-then : +- printf "%s\n" "#define HAVE_FORK1 1" >>confdefs.h +- + fi + ac_fn_c_check_func "$LINENO" "fpathconf" "ac_cv_func_fpathconf" + if test "x$ac_cv_func_fpathconf" = xyes +@@ -18707,24 +18915,6 @@ + then : + printf "%s\n" "#define HAVE_POSIX_OPENPT 1" >>confdefs.h + +-fi +-ac_fn_c_check_func "$LINENO" "posix_spawn" "ac_cv_func_posix_spawn" +-if test "x$ac_cv_func_posix_spawn" = xyes +-then : +- printf "%s\n" "#define HAVE_POSIX_SPAWN 1" >>confdefs.h +- +-fi +-ac_fn_c_check_func "$LINENO" "posix_spawnp" "ac_cv_func_posix_spawnp" +-if test "x$ac_cv_func_posix_spawnp" = xyes +-then : +- printf "%s\n" "#define HAVE_POSIX_SPAWNP 1" >>confdefs.h +- +-fi +-ac_fn_c_check_func "$LINENO" "posix_spawn_file_actions_addclosefrom_np" "ac_cv_func_posix_spawn_file_actions_addclosefrom_np" +-if test "x$ac_cv_func_posix_spawn_file_actions_addclosefrom_np" = xyes +-then : +- printf "%s\n" "#define HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP 1" >>confdefs.h +- + fi + ac_fn_c_check_func "$LINENO" "pread" "ac_cv_func_pread" + if test "x$ac_cv_func_pread" = xyes +@@ -19013,12 +19203,6 @@ + then : + printf "%s\n" "#define HAVE_SIGACTION 1" >>confdefs.h + +-fi +-ac_fn_c_check_func "$LINENO" "sigaltstack" "ac_cv_func_sigaltstack" +-if test "x$ac_cv_func_sigaltstack" = xyes +-then : +- printf "%s\n" "#define HAVE_SIGALTSTACK 1" >>confdefs.h +- + fi + ac_fn_c_check_func "$LINENO" "sigfillset" "ac_cv_func_sigfillset" + if test "x$ac_cv_func_sigfillset" = xyes +@@ -19287,11 +19471,11 @@ + + fi + +-# iOS defines some system methods that can be linked (so they are ++# iOS/tvOS/visionOS/watchOS define some system methods that can be linked (so they are + # found by configure), but either raise a compilation error (because the + # 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" ; then ++if test "$ac_sys_system" != "iOS" -a "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "visionOS" -a "$ac_sys_system" != "watchOS" ; then + ac_fn_c_check_func "$LINENO" "getentropy" "ac_cv_func_getentropy" + if test "x$ac_cv_func_getentropy" = xyes + then : +@@ -19313,6 +19497,53 @@ + + 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 ++ ac_fn_c_check_func "$LINENO" "execv" "ac_cv_func_execv" ++if test "x$ac_cv_func_execv" = xyes ++then : ++ printf "%s\n" "#define HAVE_EXECV 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "fork" "ac_cv_func_fork" ++if test "x$ac_cv_func_fork" = xyes ++then : ++ printf "%s\n" "#define HAVE_FORK 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "fork1" "ac_cv_func_fork1" ++if test "x$ac_cv_func_fork1" = xyes ++then : ++ printf "%s\n" "#define HAVE_FORK1 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "posix_spawn" "ac_cv_func_posix_spawn" ++if test "x$ac_cv_func_posix_spawn" = xyes ++then : ++ printf "%s\n" "#define HAVE_POSIX_SPAWN 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "posix_spawnp" "ac_cv_func_posix_spawnp" ++if test "x$ac_cv_func_posix_spawnp" = xyes ++then : ++ printf "%s\n" "#define HAVE_POSIX_SPAWNP 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "posix_spawn_file_actions_addclosefrom_np" "ac_cv_func_posix_spawn_file_actions_addclosefrom_np" ++if test "x$ac_cv_func_posix_spawn_file_actions_addclosefrom_np" = xyes ++then : ++ printf "%s\n" "#define HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP 1" >>confdefs.h ++ ++fi ++ac_fn_c_check_func "$LINENO" "sigaltstack" "ac_cv_func_sigaltstack" ++if test "x$ac_cv_func_sigaltstack" = xyes ++then : ++ printf "%s\n" "#define HAVE_SIGALTSTACK 1" >>confdefs.h ++ ++fi ++ ++fi ++ + { 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} +@@ -22135,7 +22366,8 @@ + + + # check for openpty, login_tty, and forkpty +- ++# tvOS/watchOS have functions for tty, but can't use them ++if test "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "watchOS" ; then + + for ac_func in openpty + do : +@@ -22231,7 +22463,7 @@ + fi + + done +-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing login_tty" >&5 ++ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing login_tty" >&5 + printf %s "checking for library containing login_tty... " >&6; } + if test ${ac_cv_search_login_tty+y} + then : +@@ -22388,6 +22620,7 @@ + fi + + done ++fi + + # check for long file support functions + ac_fn_c_check_func "$LINENO" "fseek64" "ac_cv_func_fseek64" +@@ -22634,10 +22867,10 @@ + + done + +-# On Android and iOS, clock_settime can be linked (so it is found by ++# On Android, iOS, tvOS, visionOS, and watchOS, clock_settime can be linked (so it is found by + # configure), but when used in an unprivileged process, it crashes rather than + # returning an error. Force the symbol off. +-if test "$ac_sys_system" != "Linux-android" && test "$ac_sys_system" != "iOS" ++if test "$ac_sys_system" != "Linux-android" -a "$ac_sys_system" != "iOS" -a "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "visionOS" -a "$ac_sys_system" != "watchOS" + then + + for ac_func in clock_settime +@@ -22923,7 +23156,7 @@ + if test "$cross_compiling" = yes + then : + +-if test "$ac_sys_system" = "Linux-android" || test "$ac_sys_system" = "iOS"; then ++if test "$ac_sys_system" = "Linux-android" || test "$ac_sys_system" = "iOS" || test "$ac_sys_system" = "tvOS" || test "$ac_sys_system" = "visionOS" || test "$ac_sys_system" = "watchOS"; then + ac_cv_buggy_getaddrinfo="no" + elif test "${enable_ipv6+set}" = set; then + ac_cv_buggy_getaddrinfo="no -- configured with --(en|dis)able-ipv6" +@@ -24869,8 +25102,8 @@ + LIBPYTHON="\$(BLDLIBRARY)" + fi + +-# On iOS the shared libraries must be linked with the Python framework +-if test "$ac_sys_system" = "iOS"; then ++# On iOS/tvOS/watchOS the shared libraries must be linked with the Python framework ++if test "$ac_sys_system" = "iOS" -o $ac_sys_system = "tvOS" -o $ac_sys_system = "visionOS" -o $ac_sys_system = "watchOS"; then + MODULE_DEPS_SHARED="$MODULE_DEPS_SHARED \$(PYTHONFRAMEWORKDIR)/\$(PYTHONFRAMEWORK)" + fi + +@@ -27518,7 +27751,7 @@ + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for device files" >&5 + printf "%s\n" "$as_me: checking for device files" >&6;} + +-if test "$ac_sys_system" = "Linux-android" || test "$ac_sys_system" = "iOS"; then ++if test "$ac_sys_system" = "Linux-android" -o "$ac_sys_system" = "iOS" -o "$ac_sys_system" = "tvOS" -o "$ac_sys_system" = "visionOS" -o "$ac_sys_system" = "watchOS" ; then + ac_cv_file__dev_ptmx=no + ac_cv_file__dev_ptc=no + else +@@ -27951,7 +28184,7 @@ + with_ensurepip=no ;; #( + WASI) : + with_ensurepip=no ;; #( +- iOS) : ++ iOS|tvOS|visionOS|watchOS) : + with_ensurepip=no ;; #( + *) : + with_ensurepip=upgrade +@@ -28970,7 +29203,7 @@ + ;; #( + Darwin) : + ;; #( +- iOS) : ++ iOS|tvOS|visionOS|watchOS) : + + + +@@ -32734,7 +32967,10 @@ + "Mac/PythonLauncher/Makefile") CONFIG_FILES="$CONFIG_FILES Mac/PythonLauncher/Makefile" ;; + "Mac/Resources/framework/Info.plist") CONFIG_FILES="$CONFIG_FILES Mac/Resources/framework/Info.plist" ;; + "Mac/Resources/app/Info.plist") CONFIG_FILES="$CONFIG_FILES Mac/Resources/app/Info.plist" ;; +- "iOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES iOS/Resources/Info.plist" ;; ++ "Apple/iOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/iOS/Resources/Info.plist" ;; ++ "Apple/tvOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/tvOS/Resources/Info.plist" ;; ++ "Apple/visionOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/visionOS/Resources/Info.plist" ;; ++ "Apple/watchOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/watchOS/Resources/Info.plist" ;; + "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 597a44b331a..5109db2f48f 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -330,6 +330,15 @@ + *-apple-ios*) + ac_sys_system=iOS + ;; ++ *-apple-tvos*) ++ ac_sys_system=tvOS ++ ;; ++ *-apple-xros*) ++ ac_sys_system=visionOS ++ ;; ++ *-apple-watchos*) ++ ac_sys_system=watchOS ++ ;; + *-*-vxworks*) + ac_sys_system=VxWorks + ;; +@@ -382,7 +391,7 @@ + # On cross-compile builds, configure will look for a host-specific compiler by + # prepending the user-provided host triple to the required binary name. + # +-# On iOS, this results in binaries like "arm64-apple-ios13.0-simulator-gcc", ++# On iOS/tvOS/visionOS/watchOS, this results in binaries like "arm64-apple-ios13.0-simulator-gcc", + # which isn't a binary that exists, and isn't very convenient, as it contains the + # iOS version. As the default cross-compiler name won't exist, configure falls + # back to gcc, which *definitely* won't work. We're providing wrapper scripts for +@@ -397,6 +406,17 @@ + aarch64-apple-ios*-simulator) AR=arm64-apple-ios-simulator-ar ;; + aarch64-apple-ios*) AR=arm64-apple-ios-ar ;; + x86_64-apple-ios*-simulator) AR=x86_64-apple-ios-simulator-ar ;; ++ ++ aarch64-apple-tvos*-simulator) AR=arm64-apple-tvos-simulator-ar ;; ++ aarch64-apple-tvos*) AR=arm64-apple-tvos-ar ;; ++ x86_64-apple-tvos*-simulator) AR=x86_64-apple-tvos-simulator-ar ;; ++ ++ aarch64-apple-xros*-simulator) AR=arm64-apple-xros-simulator-ar ;; ++ aarch64-apple-xros*) AR=arm64-apple-xros-ar ;; ++ ++ aarch64-apple-watchos*-simulator) AR=arm64-apple-watchos-simulator-ar ;; ++ aarch64-apple-watchos*) AR=arm64_32-apple-watchos-ar ;; ++ x86_64-apple-watchos*-simulator) AR=x86_64-apple-watchos-simulator-ar ;; + *) + esac + fi +@@ -405,6 +425,17 @@ + aarch64-apple-ios*-simulator) CC=arm64-apple-ios-simulator-clang ;; + aarch64-apple-ios*) CC=arm64-apple-ios-clang ;; + x86_64-apple-ios*-simulator) CC=x86_64-apple-ios-simulator-clang ;; ++ ++ aarch64-apple-tvos*-simulator) CC=arm64-apple-tvos-simulator-clang ;; ++ aarch64-apple-tvos*) CC=arm64-apple-tvos-clang ;; ++ x86_64-apple-tvos*-simulator) CC=x86_64-apple-tvos-simulator-clang ;; ++ ++ aarch64-apple-xros*-simulator) CC=arm64-apple-xros-simulator-clang ;; ++ aarch64-apple-xros*) CC=arm64-apple-xros-clang ;; ++ ++ aarch64-apple-watchos*-simulator) CC=arm64-apple-watchos-simulator-clang ;; ++ aarch64-apple-watchos*) CC=arm64_32-apple-watchos-clang ;; ++ x86_64-apple-watchos*-simulator) CC=x86_64-apple-watchos-simulator-clang ;; + *) + esac + fi +@@ -413,6 +444,17 @@ + aarch64-apple-ios*-simulator) CPP=arm64-apple-ios-simulator-cpp ;; + aarch64-apple-ios*) CPP=arm64-apple-ios-cpp ;; + x86_64-apple-ios*-simulator) CPP=x86_64-apple-ios-simulator-cpp ;; ++ ++ aarch64-apple-tvos*-simulator) CPP=arm64-apple-tvos-simulator-cpp ;; ++ aarch64-apple-tvos*) CPP=arm64-apple-tvos-cpp ;; ++ x86_64-apple-tvos*-simulator) CPP=x86_64-apple-tvos-simulator-cpp ;; ++ ++ aarch64-apple-xros*-simulator) CPP=arm64-apple-xros-simulator-cpp ;; ++ aarch64-apple-xros*) CPP=arm64-apple-xros-cpp ;; ++ ++ aarch64-apple-watchos*-simulator) CPP=arm64-apple-watchos-simulator-cpp ;; ++ aarch64-apple-watchos*) CPP=arm64_32-apple-watchos-cpp ;; ++ x86_64-apple-watchos*-simulator) CPP=x86_64-apple-watchos-simulator-cpp ;; + *) + esac + fi +@@ -421,6 +463,17 @@ + aarch64-apple-ios*-simulator) CXX=arm64-apple-ios-simulator-clang++ ;; + aarch64-apple-ios*) CXX=arm64-apple-ios-clang++ ;; + x86_64-apple-ios*-simulator) CXX=x86_64-apple-ios-simulator-clang++ ;; ++ ++ aarch64-apple-tvos*-simulator) CXX=arm64-apple-tvos-simulator-clang++ ;; ++ aarch64-apple-tvos*) CXX=arm64-apple-tvos-clang++ ;; ++ x86_64-apple-tvos*-simulator) CXX=x86_64-apple-tvos-simulator-clang++ ;; ++ ++ aarch64-apple-xros*-simulator) CXX=arm64-apple-xros-simulator-clang++ ;; ++ aarch64-apple-xros*) CXX=arm64-apple-xros-clang++ ;; ++ ++ aarch64-apple-watchos*-simulator) CXX=arm64-apple-watchos-simulator-clang++ ;; ++ aarch64-apple-watchos*) CXX=arm64_32-apple-watchos-clang++ ;; ++ x86_64-apple-watchos*-simulator) CXX=x86_64-apple-watchos-simulator-clang++ ;; + *) + esac + fi +@@ -535,8 +588,11 @@ + case $enableval in + yes) + case $ac_sys_system in +- Darwin) enableval=/Library/Frameworks ;; +- iOS) enableval=iOS/Frameworks/\$\(MULTIARCH\) ;; ++ Darwin) enableval=/Library/Frameworks ;; ++ iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;; ++ tvOS) enableval=Apple/tvOS/Frameworks/\$\(MULTIARCH\) ;; ++ visionOS) enableval=Apple/visionOS/Frameworks/\$\(MULTIARCH\) ;; ++ watchOS) enableval=Apple/watchOS/Frameworks/\$\(MULTIARCH\) ;; + *) AC_MSG_ERROR([Unknown platform for framework build]) + esac + esac +@@ -545,6 +601,9 @@ + no) + case $ac_sys_system in + iOS) AC_MSG_ERROR([iOS builds must use --enable-framework]) ;; ++ tvOS) AC_MSG_ERROR([tvOS builds must use --enable-framework]) ;; ++ visionOS) AC_MSG_ERROR([visionOS builds must use --enable-framework]) ;; ++ watchOS) AC_MSG_ERROR([watchOS builds must use --enable-framework]) ;; + *) + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework +@@ -643,9 +702,51 @@ + + prefix=$PYTHONFRAMEWORKPREFIX + PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" +- RESSRCDIR=iOS/Resources ++ RESSRCDIR=Apple/iOS/Resources + +- AC_CONFIG_FILES([iOS/Resources/Info.plist]) ++ AC_CONFIG_FILES([Apple/iOS/Resources/Info.plist]) ++ ;; ++ tvOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/tvOS/Resources ++ ++ AC_CONFIG_FILES([Apple/tvOS/Resources/Info.plist]) ++ ;; ++ visionOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/visionOS/Resources ++ ++ AC_CONFIG_FILES([Apple/visionOS/Resources/Info.plist]) ++ ;; ++ watchOS) : ++ FRAMEWORKINSTALLFIRST="frameworkinstallunversionedstructure" ++ FRAMEWORKALTINSTALLFIRST="frameworkinstallunversionedstructure " ++ FRAMEWORKINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKALTINSTALLLAST="frameworkinstallmobileheaders" ++ FRAMEWORKPYTHONW= ++ INSTALLTARGETS="libinstall inclinstall sharedinstall" ++ ++ prefix=$PYTHONFRAMEWORKPREFIX ++ PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR" ++ RESSRCDIR=Apple/watchOS/Resources ++ ++ AC_CONFIG_FILES([Apple/watchOS/Resources/Info.plist]) + ;; + *) + AC_MSG_ERROR([Unknown platform for framework build]) +@@ -655,6 +756,9 @@ + ],[ + case $ac_sys_system in + iOS) AC_MSG_ERROR([iOS builds must use --enable-framework]) ;; ++ tvOS) AC_MSG_ERROR([tvOS builds must use --enable-framework]) ;; ++ visionOS) AC_MSG_ERROR([visionOS builds must use --enable-framework]) ;; ++ watchOS) AC_MSG_ERROR([watchOS builds must use --enable-framework]) ;; + *) + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework +@@ -707,8 +811,8 @@ + case "$withval" in + yes) + case $ac_sys_system in +- Darwin|iOS) +- # iOS is able to share the macOS patch ++ Darwin|iOS|tvOS|visionOS|watchOS) ++ # iOS/tvOS/visionOS/watchOS is able to share the macOS patch + APP_STORE_COMPLIANCE_PATCH="Mac/Resources/app-store-compliance.patch" + ;; + *) AC_MSG_ERROR([no default app store compliance patch available for $ac_sys_system]) ;; +@@ -722,8 +826,8 @@ + esac + ],[ + case $ac_sys_system in +- iOS) +- # Always apply the compliance patch on iOS; we can use the macOS patch ++ iOS|tvOS|visionOS|watchOS) ++ # Always apply the compliance patch on iOS/tvOS/visionOS/watchOS; we can use the macOS patch + APP_STORE_COMPLIANCE_PATCH="Mac/Resources/app-store-compliance.patch" + AC_MSG_RESULT([applying default app store compliance patch]) + ;; +@@ -736,6 +840,8 @@ + ]) + AC_SUBST([APP_STORE_COMPLIANCE_PATCH]) + ++EXPORT_XROS_DEPLOYMENT_TARGET='#' ++ + AC_SUBST([_PYTHON_HOST_PLATFORM]) + if test "$cross_compiling" = yes; then + case "$host" in +@@ -771,6 +877,70 @@ + ;; + esac + ;; ++ *-apple-tvos*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # TVOS_DEPLOYMENT_TARGET is the minimum supported tvOS version ++ AC_MSG_CHECKING([tvOS deployment target]) ++ TVOS_DEPLOYMENT_TARGET=${_host_os:4} ++ TVOS_DEPLOYMENT_TARGET=${TVOS_DEPLOYMENT_TARGET:=12.0} ++ AC_MSG_RESULT([$TVOS_DEPLOYMENT_TARGET]) ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${TVOS_DEPLOYMENT_TARGET}-arm64-appletv${_host_device} ++ ;; ++ *) ++ _host_ident=${TVOS_DEPLOYMENT_TARGET}-$host_cpu-appletv${_host_device} ++ ;; ++ esac ++ ;; ++ *-apple-xros*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # XROS_DEPLOYMENT_TARGET is the minimum supported visionOS version ++ AC_MSG_CHECKING([visionOS deployment target]) ++ XROS_DEPLOYMENT_TARGET=${_host_os:8} ++ XROS_DEPLOYMENT_TARGET=${XROS_DEPLOYMENT_TARGET:=2.0} ++ AC_MSG_RESULT([$XROS_DEPLOYMENT_TARGET]) ++ AC_MSG_CHECKING([exporting flag of visionOS deployment target]) ++ export XROS_DEPLOYMENT_TARGET ++ EXPORT_XROS_DEPLOYMENT_TARGET='' ++ AC_MSG_RESULT([$EXPORT_XROS_DEPLOYMENT_TARGET]) ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${XROS_DEPLOYMENT_TARGET}-arm64-xr${_host_device} ++ ;; ++ *) ++ _host_ident=${XROS_DEPLOYMENT_TARGET}-$host_cpu-xr${_host_device} ++ ;; ++ esac ++ ;; ++ *-apple-watchos*) ++ _host_os=`echo $host | cut -d '-' -f3` ++ _host_device=`echo $host | cut -d '-' -f4` ++ _host_device=${_host_device:=os} ++ ++ # WATCHOS_DEPLOYMENT_TARGET is the minimum supported watchOS version ++ AC_MSG_CHECKING([watchOS deployment target]) ++ WATCHOS_DEPLOYMENT_TARGET=${_host_os:7} ++ WATCHOS_DEPLOYMENT_TARGET=${WATCHOS_DEPLOYMENT_TARGET:=4.0} ++ AC_MSG_RESULT([$WATCHOS_DEPLOYMENT_TARGET]) ++ ++ case "$host_cpu" in ++ aarch64) ++ _host_ident=${WATCHOS_DEPLOYMENT_TARGET}-arm64-watch${_host_device} ++ ;; ++ *) ++ _host_ident=${WATCHOS_DEPLOYMENT_TARGET}-$host_cpu-watch${_host_device} ++ ;; ++ esac ++ ;; + *-*-vxworks*) + _host_ident=$host_cpu + ;; +@@ -848,9 +1018,15 @@ + define_xopen_source=no;; + Darwin/@<:@[12]@:>@@<:@0-9@:>@.*) + define_xopen_source=no;; +- # On iOS, defining _POSIX_C_SOURCE also disables platform specific features. ++ # On iOS/tvOS/visionOS/watchOS, defining _POSIX_C_SOURCE also disables platform specific features. + iOS/*) + define_xopen_source=no;; ++ tvOS/*) ++ define_xopen_source=no;; ++ visionOS/*) ++ define_xopen_source=no;; ++ watchOS/*) ++ define_xopen_source=no;; + # On QNX 6.3.2, defining _XOPEN_SOURCE prevents netdb.h from + # defining NI_NUMERICHOST. + QNX/6.3.2) +@@ -909,8 +1085,15 @@ + CONFIGURE_MACOSX_DEPLOYMENT_TARGET= + EXPORT_MACOSX_DEPLOYMENT_TARGET='#' + +-# Record the value of IPHONEOS_DEPLOYMENT_TARGET enforced by the selected host triple. ++# Record the value of IPHONEOS_DEPLOYMENT_TARGET / TVOS_DEPLOYMENT_TARGET / ++# XROS_DEPLOYMENT_TARGET / WATCHOS_DEPLOYMENT_TARGET enforced by the selected host triple. + AC_SUBST([IPHONEOS_DEPLOYMENT_TARGET]) ++AC_SUBST([TVOS_DEPLOYMENT_TARGET]) ++AC_SUBST([XROS_DEPLOYMENT_TARGET]) ++AC_SUBST([WATCHOS_DEPLOYMENT_TARGET]) ++ ++# XROS_DEPLOYMENT_TARGET should get exported ++AC_SUBST([EXPORT_XROS_DEPLOYMENT_TARGET]) + + # checks for alternative programs + +@@ -944,11 +1127,19 @@ + ], + ) + +-dnl Add the compiler flag for the iOS minimum supported OS version. ++dnl Add the compiler flag for the iOS/tvOS/watchOS minimum supported OS ++dnl version. visionOS doesn't use an explicit -mxros-version-min option - ++dnl it encodes the min version into the target triple. + AS_CASE([$ac_sys_system], + [iOS], [ + AS_VAR_APPEND([CFLAGS], [" -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}"]) + AS_VAR_APPEND([LDFLAGS], [" -mios-version-min=${IPHONEOS_DEPLOYMENT_TARGET}"]) ++ ],[tvOS], [ ++ AS_VAR_APPEND([CFLAGS], [" -mtvos-version-min=${TVOS_DEPLOYMENT_TARGET}"]) ++ AS_VAR_APPEND([LDFLAGS], [" -mtvos-version-min=${TVOS_DEPLOYMENT_TARGET}"]) ++ ],[watchOS], [ ++ AS_VAR_APPEND([CFLAGS], [" -mwatchos-version-min=${WATCHOS_DEPLOYMENT_TARGET}"]) ++ AS_VAR_APPEND([LDFLAGS], [" -mwatchos-version-min=${WATCHOS_DEPLOYMENT_TARGET}"]) + ], + ) + +@@ -1136,6 +1327,9 @@ + AS_CASE([$ac_sys_system], + [Darwin*], [MULTIARCH=""], + [iOS], [MULTIARCH=""], ++ [tvOS], [MULTIARCH=""], ++ [visionOS], [MULTIARCH=""], ++ [watchOS], [MULTIARCH=""], + [FreeBSD*], [MULTIARCH=""], + [MULTIARCH=$($CC --print-multiarch 2>/dev/null)] + ) +@@ -1157,7 +1351,7 @@ + dnl use a single "fat" binary at runtime. SOABI_PLATFORM is the component of + dnl the PLATFORM_TRIPLET that will be used in binary module extensions. + AS_CASE([$ac_sys_system], +- [iOS], [SOABI_PLATFORM=`echo "$PLATFORM_TRIPLET" | cut -d '-' -f2`], ++ [iOS|tvOS|visionOS|watchOS], [SOABI_PLATFORM=`echo "$PLATFORM_TRIPLET" | cut -d '-' -f2`], + [SOABI_PLATFORM=$PLATFORM_TRIPLET] + ) + +@@ -1184,15 +1378,21 @@ + [wasm32-unknown-wasip1/clang], [PY_SUPPORT_TIER=2], dnl WebAssembly System Interface preview1, clang + [x86_64-*-linux-gnu/clang], [PY_SUPPORT_TIER=2], dnl Linux on AMD64, any vendor, glibc, clang + +- [aarch64-pc-windows-msvc/msvc], [PY_SUPPORT_TIER=3], dnl Windows ARM64, MSVC +- [armv7l-*-linux-gnueabihf/gcc], [PY_SUPPORT_TIER=3], dnl ARMv7 LE with hardware floats, any vendor, glibc, gcc +- [powerpc64le-*-linux-gnu/clang], [PY_SUPPORT_TIER=3], dnl Linux on PPC64 little endian, glibc, clang +- [s390x-*-linux-gnu/gcc], [PY_SUPPORT_TIER=3], dnl Linux on 64bit s390x (big endian), glibc, gcc +- [x86_64-*-freebsd*/clang], [PY_SUPPORT_TIER=3], dnl FreeBSD on AMD64 +- [aarch64-apple-ios*-simulator/clang], [PY_SUPPORT_TIER=3], dnl iOS Simulator on arm64 +- [aarch64-apple-ios*/clang], [PY_SUPPORT_TIER=3], dnl iOS on ARM64 +- [aarch64-*-linux-android/clang], [PY_SUPPORT_TIER=3], dnl Android on ARM64 +- [x86_64-*-linux-android/clang], [PY_SUPPORT_TIER=3], dnl Android on AMD64 ++ [aarch64-pc-windows-msvc/msvc], [PY_SUPPORT_TIER=3], dnl Windows ARM64, MSVC ++ [armv7l-*-linux-gnueabihf/gcc], [PY_SUPPORT_TIER=3], dnl ARMv7 LE with hardware floats, any vendor, glibc, gcc ++ [powerpc64le-*-linux-gnu/clang], [PY_SUPPORT_TIER=3], dnl Linux on PPC64 little endian, glibc, clang ++ [s390x-*-linux-gnu/gcc], [PY_SUPPORT_TIER=3], dnl Linux on 64bit s390x (big endian), glibc, gcc ++ [x86_64-*-freebsd*/clang], [PY_SUPPORT_TIER=3], dnl FreeBSD on AMD64 ++ [aarch64-apple-ios*-simulator/clang], [PY_SUPPORT_TIER=3], dnl iOS Simulator on arm64 ++ [aarch64-apple-ios*/clang], [PY_SUPPORT_TIER=3], dnl iOS on ARM64 ++ [aarch64-apple-tvos*-simulator/clang], [PY_SUPPORT_TIER=3], dnl tvOS Simulator on arm64 ++ [aarch64-apple-tvos*/clang], [PY_SUPPORT_TIER=3], dnl tvOS on ARM64 ++ [aarch64-apple-xros*-simulator/clang], [PY_SUPPORT_TIER=3], dnl visionOS Simulator on arm64 ++ [aarch64-apple-xros*/clang], [PY_SUPPORT_TIER=3], dnl visionOS on ARM64 ++ [aarch64-apple-watchos*-simulator/clang], [PY_SUPPORT_TIER=3], dnl watchOS Simulator on arm64 ++ [arm64_32-apple-watchos*/clang], [PY_SUPPORT_TIER=3], dnl watchOS on ARM64 ++ [aarch64-*-linux-android/clang], [PY_SUPPORT_TIER=3], dnl Android on ARM64 ++ [x86_64-*-linux-android/clang], [PY_SUPPORT_TIER=3], dnl Android on AMD64 + + [PY_SUPPORT_TIER=0] + ) +@@ -1525,7 +1725,7 @@ + case $ac_sys_system in + Darwin) + LDLIBRARY='$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)';; +- iOS) ++ iOS|tvOS|visionOS|watchOS) + LDLIBRARY='$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)';; + *) + AC_MSG_ERROR([Unknown platform for framework build]);; +@@ -1590,7 +1790,7 @@ + BLDLIBRARY='-L. -lpython$(LDVERSION)' + RUNSHARED=DYLD_LIBRARY_PATH=`pwd`${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}} + ;; +- iOS) ++ iOS|tvOS|visionOS|watchOS) + LDLIBRARY='libpython$(LDVERSION).dylib' + ;; + AIX*) +@@ -3486,7 +3686,7 @@ + BLDSHARED="$LDSHARED" + fi + ;; +- iOS/*) ++ iOS/*|tvOS/*|visionOS/*|watchOS/*) + LDSHARED='$(CC) -dynamiclib -F . -framework $(PYTHONFRAMEWORK)' + LDCXXSHARED='$(CXX) -dynamiclib -F . -framework $(PYTHONFRAMEWORK)' + BLDSHARED="$LDSHARED" +@@ -3610,7 +3810,7 @@ + Linux-android*) LINKFORSHARED="-pie -Xlinker -export-dynamic";; + Linux*|GNU*) LINKFORSHARED="-Xlinker -export-dynamic";; + # -u libsys_s pulls in all symbols in libsys +- Darwin/*|iOS/*) ++ Darwin/*|iOS/*|tvOS/*|visionOS/*|watchOS/*) + LINKFORSHARED="$extra_undefs -framework CoreFoundation" + + # Issue #18075: the default maximum stack size (8MBytes) is too +@@ -3634,7 +3834,7 @@ + LINKFORSHARED="$LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/Versions/$(VERSION)/$(PYTHONFRAMEWORK)' + fi + LINKFORSHARED="$LINKFORSHARED" +- elif test $ac_sys_system = "iOS"; then ++ elif test "$ac_sys_system" = "iOS" -o "$ac_sys_system" = "tvOS" -o "$ac_sys_system" = "visionOS" -o "$ac_sys_system" = "watchOS"; then + LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + fi + ;; +@@ -4106,7 +4306,7 @@ + dnl when do we need USING_APPLE_OS_LIBFFI? + ctypes_malloc_closure=yes + ], +- [iOS], [ ++ [iOS|tvOS|visionOS|watchOS], [ + ctypes_malloc_closure=yes + ], + [sunos5], [AS_VAR_APPEND([LIBFFI_LIBS], [" -mimpure-text"])] +@@ -5217,9 +5417,9 @@ + # checks for library functions + AC_CHECK_FUNCS([ \ + accept4 alarm bind_textdomain_codeset chmod chown clock closefrom close_range confstr \ +- copy_file_range ctermid dup dup3 execv explicit_bzero explicit_memset \ ++ copy_file_range ctermid dup dup3 explicit_bzero explicit_memset \ + faccessat fchmod fchmodat fchown fchownat fdopendir fdwalk fexecve \ +- fork fork1 fpathconf fstatat ftime ftruncate futimens futimes futimesat \ ++ fpathconf fstatat ftime ftruncate futimens futimes futimesat \ + gai_strerror getegid geteuid getgid getgrent getgrgid getgrgid_r \ + getgrnam_r getgrouplist gethostname getitimer getloadavg getlogin getlogin_r \ + getpeername getpgid getpid getppid getpriority _getpty \ +@@ -5227,15 +5427,14 @@ + getspnam getuid getwd grantpt if_nameindex initgroups kill killpg lchown linkat \ + lockf lstat lutimes madvise mbrtowc memrchr mkdirat mkfifo mkfifoat \ + mknod mknodat mktime mmap mremap nice openat opendir pathconf pause pipe \ +- pipe2 plock poll posix_fadvise posix_fallocate posix_openpt posix_spawn posix_spawnp \ +- posix_spawn_file_actions_addclosefrom_np \ ++ pipe2 plock poll posix_fadvise posix_fallocate posix_openpt \ + pread preadv preadv2 process_vm_readv pthread_cond_timedwait_relative_np pthread_condattr_setclock pthread_init \ + pthread_kill ptsname ptsname_r pwrite pwritev pwritev2 readlink readlinkat readv realpath renameat \ + rtpSpawn sched_get_priority_max sched_rr_get_interval sched_setaffinity \ + sched_setparam sched_setscheduler sem_clockwait sem_getvalue sem_open \ + sem_timedwait sem_unlink sendfile setegid seteuid setgid sethostname \ + setitimer setlocale setpgid setpgrp setpriority setregid setresgid \ +- setresuid setreuid setsid setuid setvbuf shutdown sigaction sigaltstack \ ++ setresuid setreuid setsid setuid setvbuf shutdown sigaction \ + sigfillset siginterrupt sigpending sigrelse sigtimedwait sigwait \ + sigwaitinfo snprintf splice strftime strlcpy strsignal symlinkat sync \ + sysconf tcgetpgrp tcsetpgrp tempnam timegm times tmpfile \ +@@ -5250,12 +5449,20 @@ + AC_CHECK_FUNCS([lchmod]) + fi + +-# iOS defines some system methods that can be linked (so they are ++# iOS/tvOS/visionOS/watchOS define some system methods that can be linked (so they are + # found by configure), but either raise a compilation error (because the + # 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" ; then +- AC_CHECK_FUNCS([getentropy getgroups system]) ++if test "$ac_sys_system" != "iOS" -a "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "visionOS" -a "$ac_sys_system" != "watchOS" ; then ++ AC_CHECK_FUNCS([ getentropy getgroups system ]) ++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 ++ AC_CHECK_FUNCS([ \ ++ execv fork fork1 posix_spawn posix_spawnp posix_spawn_file_actions_addclosefrom_np \ ++ sigaltstack \ ++ ]) + fi + + AC_CHECK_DECL([dirfd], +@@ -5518,20 +5725,22 @@ + [@%:@include ]) + + # check for openpty, login_tty, and forkpty +- +-AC_CHECK_FUNCS([openpty], [], +- [AC_CHECK_LIB([util], [openpty], +- [AC_DEFINE([HAVE_OPENPTY]) LIBS="$LIBS -lutil"], +- [AC_CHECK_LIB([bsd], [openpty], +- [AC_DEFINE([HAVE_OPENPTY]) LIBS="$LIBS -lbsd"])])]) +-AC_SEARCH_LIBS([login_tty], [util], +- [AC_DEFINE([HAVE_LOGIN_TTY], [1], [Define to 1 if you have the `login_tty' function.])] +-) +-AC_CHECK_FUNCS([forkpty], [], +- [AC_CHECK_LIB([util], [forkpty], +- [AC_DEFINE([HAVE_FORKPTY]) LIBS="$LIBS -lutil"], +- [AC_CHECK_LIB([bsd], [forkpty], +- [AC_DEFINE([HAVE_FORKPTY]) LIBS="$LIBS -lbsd"])])]) ++# tvOS/watchOS have functions for tty, but can't use them ++if test "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "watchOS" ; then ++ AC_CHECK_FUNCS([openpty], [], ++ [AC_CHECK_LIB([util], [openpty], ++ [AC_DEFINE([HAVE_OPENPTY]) LIBS="$LIBS -lutil"], ++ [AC_CHECK_LIB([bsd], [openpty], ++ [AC_DEFINE([HAVE_OPENPTY]) LIBS="$LIBS -lbsd"])])]) ++ AC_SEARCH_LIBS([login_tty], [util], ++ [AC_DEFINE([HAVE_LOGIN_TTY], [1], [Define to 1 if you have the `login_tty' function.])] ++ ) ++ AC_CHECK_FUNCS([forkpty], [], ++ [AC_CHECK_LIB([util], [forkpty], ++ [AC_DEFINE([HAVE_FORKPTY]) LIBS="$LIBS -lutil"], ++ [AC_CHECK_LIB([bsd], [forkpty], ++ [AC_DEFINE([HAVE_FORKPTY]) LIBS="$LIBS -lbsd"])])]) ++fi + + # check for long file support functions + AC_CHECK_FUNCS([fseek64 fseeko fstatvfs ftell64 ftello statvfs]) +@@ -5570,10 +5779,10 @@ + ]) + ]) + +-# On Android and iOS, clock_settime can be linked (so it is found by ++# On Android, iOS, tvOS, visionOS, and watchOS, clock_settime can be linked (so it is found by + # configure), but when used in an unprivileged process, it crashes rather than + # returning an error. Force the symbol off. +-if test "$ac_sys_system" != "Linux-android" && test "$ac_sys_system" != "iOS" ++if test "$ac_sys_system" != "Linux-android" -a "$ac_sys_system" != "iOS" -a "$ac_sys_system" != "tvOS" -a "$ac_sys_system" != "visionOS" -a "$ac_sys_system" != "watchOS" + then + AC_CHECK_FUNCS([clock_settime], [], [ + AC_CHECK_LIB([rt], [clock_settime], [ +@@ -5731,7 +5940,7 @@ + [ac_cv_buggy_getaddrinfo=no], + [ac_cv_buggy_getaddrinfo=yes], + [ +-if test "$ac_sys_system" = "Linux-android" || test "$ac_sys_system" = "iOS"; then ++if test "$ac_sys_system" = "Linux-android" || test "$ac_sys_system" = "iOS" || test "$ac_sys_system" = "tvOS" || test "$ac_sys_system" = "visionOS" || test "$ac_sys_system" = "watchOS"; then + ac_cv_buggy_getaddrinfo="no" + elif test "${enable_ipv6+set}" = set; then + ac_cv_buggy_getaddrinfo="no -- configured with --(en|dis)able-ipv6" +@@ -6322,8 +6531,8 @@ + LIBPYTHON="\$(BLDLIBRARY)" + fi + +-# On iOS the shared libraries must be linked with the Python framework +-if test "$ac_sys_system" = "iOS"; then ++# On iOS/tvOS/watchOS the shared libraries must be linked with the Python framework ++if test "$ac_sys_system" = "iOS" -o $ac_sys_system = "tvOS" -o $ac_sys_system = "visionOS" -o $ac_sys_system = "watchOS"; then + MODULE_DEPS_SHARED="$MODULE_DEPS_SHARED \$(PYTHONFRAMEWORKDIR)/\$(PYTHONFRAMEWORK)" + fi + +@@ -6931,7 +7140,7 @@ + dnl NOTE: Inform user how to proceed with files when cross compiling. + dnl Some cross-compile builds are predictable; they won't ever + dnl have /dev/ptmx or /dev/ptc, so we can set them explicitly. +-if test "$ac_sys_system" = "Linux-android" || test "$ac_sys_system" = "iOS"; then ++if test "$ac_sys_system" = "Linux-android" -o "$ac_sys_system" = "iOS" -o "$ac_sys_system" = "tvOS" -o "$ac_sys_system" = "visionOS" -o "$ac_sys_system" = "watchOS" ; then + ac_cv_file__dev_ptmx=no + ac_cv_file__dev_ptc=no + else +@@ -7188,7 +7397,7 @@ + AS_CASE([$ac_sys_system], + [Emscripten], [with_ensurepip=no], + [WASI], [with_ensurepip=no], +- [iOS], [with_ensurepip=no], ++ [iOS|tvOS|visionOS|watchOS], [with_ensurepip=no], + [with_ensurepip=upgrade] + ) + ]) +@@ -7596,7 +7805,7 @@ + [VxWorks*], [PY_STDLIB_MOD_SET_NA([_scproxy], [termios], [grp])], + dnl The _scproxy module is available on macOS + [Darwin], [], +- [iOS], [ ++ [iOS|tvOS|visionOS|watchOS], [ + dnl subprocess and multiprocessing are not supported (no fork syscall). + dnl curses and tkinter user interface are not available. + dnl gdbm and nis aren't available diff --git a/darwin/ios_patches/README.md b/darwin/ios_patches/README.md new file mode 100644 index 0000000..a6e9d3d --- /dev/null +++ b/darwin/ios_patches/README.md @@ -0,0 +1,39 @@ +# iOS source patches + +Vendored patches that back-port CPython's in-tree Apple build tooling (the `Apple/` +package — `python Apple build iOS`) onto older CPython versions, so `build_ios.py` can use +the **same** standard mechanism for every version instead of bespoke build code. + +| Patch | Back-ports | Notes | +|---|---|---| +| `3.13/Python.patch` | `Apple/` build tooling + `iOS/Resources/bin` shims | iOS *runtime* (PEP 730) is already upstream in 3.13 | +| `3.12/Python.patch` | `Apple/` build tooling **+ the PEP 730 runtime** (`_ios_support.py`, `getpath.c`, `pylifecycle.c`, …) | 3.12 has no iOS support at all upstream | + +3.14+ needs **no** patch — the `Apple/` tooling is native there. + +## Provenance + +Originally derived from [beeware/Python-Apple-support](https://github.com/beeware/Python-Apple-support) +`patch/Python/Python.patch` on the matching `3.X` branch. We vendor them here so the build +has **no runtime dependency on the Python-Apple-support repo** and so we can adjust them for +micro releases ourselves. + +(Note: the build still downloads pre-compiled C dependencies — OpenSSL/libffi/xz/… — from +`beeware/cpython-apple-source-deps`, because CPython's own `Apple/__main__.py` hard-codes +that URL for *all* versions, including native 3.14. That is the upstream-standard binary-dep +source and is out of scope here.) + +## Maintaining across releases + +The patches are applied with `patch -p1`. They generally apply across micro releases +unchanged, but a micro bump can drift a hunk (usually in `configure`, or a `Lib/test/` file). +When that happens: + +1. Extract the target source: `tar xzf Python-.tgz`. +2. `cd` in and `patch -p1 --force < /Python.patch`; inspect any `*.rej`. +3. Fix the hunk in the patch (or drop it if it only touches `Lib/test/`, which is neither + built nor shipped — that is exactly why the 3.12 patch here has had its + `Lib/test/test_genericpath.py` hunk removed for 3.12.13). + +A minor-version bump (e.g. adding 3.15) means refreshing from the corresponding upstream +back-port branch. diff --git a/darwin/macos_support/README.md b/darwin/macos_support/README.md new file mode 100644 index 0000000..7857dc6 --- /dev/null +++ b/darwin/macos_support/README.md @@ -0,0 +1,20 @@ +# macOS support assets + +Vendored helper assets for the from-source macOS framework build (`darwin/build_macos.py`). + +## `app-store-compliance.patch` + +Sourced from [beeware/Python-Apple-support](https://github.com/beeware/Python-Apple-support) +(`patch/Python/app-store-compliance.patch`). It removes the `itms-services` URL scheme from the +stdlib's `urllib.parse`, which Apple's App Store review flags. It is **optional** and applied only +when `build_macos.py --app-store-compliance` is passed (off by default — plain desktop macOS apps +don't need it). + +## Not vendored (handled elsewhere) + +- **module map** — the dart packagers overlay `darwin/Modules/module.modulemap`, so beeware's + `module.modulemap.prefix` is unnecessary here. +- **framework/stdlib pruning** — handled by `darwin/python-darwin-framework.exclude` and + `darwin/python-darwin-stdlib.exclude` in the dart packagers. +- **relocation** — beeware's `patch/make-relocatable.sh` logic is ported directly into + `build_macos.py` (`install_name_tool` driven by `otool -L`). diff --git a/darwin/macos_support/app-store-compliance.patch b/darwin/macos_support/app-store-compliance.patch new file mode 100644 index 0000000..f4b7dec --- /dev/null +++ b/darwin/macos_support/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/darwin/package-ios-for-dart.sh b/darwin/package-ios-for-dart.sh index 1921851..542d0e0 100755 --- a/darwin/package-ios-for-dart.sh +++ b/darwin/package-ios-for-dart.sh @@ -35,6 +35,12 @@ mkdir -p $stdlib_dir # copy Python.xcframework rsync -av --exclude-from=$script_dir/python-darwin-framework.exclude $python_apple_support_root/support/$python_version_short/iOS/Python.xcframework $frameworks_dir +# Drop the full, un-pruned stdlib carried at the xcframework root (incl. the CPython test +# suite, ~200 MB). serious_python loads the stdlib from python-stdlib/ and C extensions from +# python-xcframeworks/*.fwork, so this copy is dead weight in shipped apps. (Done here rather +# than via the shared rsync exclude, which is also used for macOS where a `lib` pattern would +# wrongly drop the bundled OpenSSL under Versions/*/lib.) +rm -rf $frameworks_dir/Python.xcframework/lib cp -r $script_dir/Modules $frameworks_dir/Python.xcframework/ios-arm64/Python.framework cp -r $script_dir/Modules $frameworks_dir/Python.xcframework/ios-arm64_x86_64-simulator/Python.framework diff --git a/darwin/package-macos-for-dart.sh b/darwin/package-macos-for-dart.sh index 913c872..fa04588 100755 --- a/darwin/package-macos-for-dart.sh +++ b/darwin/package-macos-for-dart.sh @@ -34,7 +34,9 @@ rsync -av --exclude-from=$script_dir/python-darwin-framework.exclude $python_app cp -r $script_dir/Modules $frameworks_dir/Python.xcframework/macos-arm64_x86_64/Python.framework mkdir -p $frameworks_dir/Python.xcframework/macos-arm64_x86_64/Python.framework/Headers cp -r $python_apple_support_root/support/$python_version_short/macOS/Python.xcframework/macos-arm64_x86_64/Python.framework/Versions/$python_version_short/include/python$python_version_short/* $frameworks_dir/Python.xcframework/macos-arm64_x86_64/Python.framework/Headers -rm $frameworks_dir/Python.xcframework/macos-arm64_x86_64/Python.framework/Headers/module.modulemap +# The built-in modulemap (if the source framework shipped one) is replaced by the +# overlaid darwin/Modules/module.modulemap above; -f tolerates builds without one. +rm -f $frameworks_dir/Python.xcframework/macos-arm64_x86_64/Python.framework/Headers/module.modulemap # copy stdlibs rsync -av --exclude-from=$script_dir/python-darwin-stdlib.exclude $python_apple_support_root/install/macOS/macosx/python-*/Python.framework/Versions/Current/lib/python$python_version_short/* $stdlib_dir diff --git a/darwin/python-darwin-framework.exclude b/darwin/python-darwin-framework.exclude index afbea50..0c1f1c6 100644 --- a/darwin/python-darwin-framework.exclude +++ b/darwin/python-darwin-framework.exclude @@ -1,16 +1,21 @@ ios-arm64/bin ios-arm64/include ios-arm64/lib +ios-arm64/lib-arm64 ios-arm64/platform-config ios-arm64/Python.framework/Headers/module.modulemap ios-arm64_x86_64-simulator/bin ios-arm64_x86_64-simulator/include ios-arm64_x86_64-simulator/lib +ios-arm64_x86_64-simulator/lib-arm64 +ios-arm64_x86_64-simulator/lib-x86_64 ios-arm64_x86_64-simulator/platform-config ios-arm64_x86_64-simulator/Python.framework/Headers/module.modulemap macos-arm64_x86_64/Python.framework/Headers macos-arm64_x86_64/Python.framework/Versions/*/_CodeSignature macos-arm64_x86_64/Python.framework/Versions/*/Headers +macos-arm64_x86_64/Python.framework/Versions/*/bin +macos-arm64_x86_64/Python.framework/Versions/*/share macos-arm64_x86_64/Python.framework/Versions/*/include macos-arm64_x86_64/Python.framework/Versions/*/lib/itcl* macos-arm64_x86_64/Python.framework/Versions/*/lib/libform* diff --git a/darwin/python-darwin-stdlib.exclude b/darwin/python-darwin-stdlib.exclude index b06a320..64725c9 100644 --- a/darwin/python-darwin-stdlib.exclude +++ b/darwin/python-darwin-stdlib.exclude @@ -1,16 +1,30 @@ lib-dynload/_test*.so lib-dynload/_ctypes_test*.so lib-dynload/xxlimited*.so +lib-dynload/xxsubtype*.so lib-dynload/_xxtestfuzz*.so +# curses package is excluded below, so its C extensions are dead weight +lib-dynload/_curses*.so +lib-dynload/_curses_panel*.so config-* ctypes/macholib/fetch_macholib* curses ensurepip idlelib +lib2to3 pydoc_data site-packages test tkinter turtle* wsgiref +# Interactive-REPL / dev-only modules — never imported when running a script (embedded +# mode); plus easter eggs and frozen demo modules. +_pyrepl +rlcompleter* +tabnanny* +antigravity* +this* +__hello__* +__phello__ */__pycache__ \ No newline at end of file diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..27bec55 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1,6 @@ +/x86_64 +/aarch64 +/cpython-*.tar.gz +/python-linux-dart-*.tar.gz +__pycache__/ +.DS_Store diff --git a/linux/package-for-linux.sh b/linux/package-for-linux.sh index 00c5b5c..416cc4d 100755 --- a/linux/package-for-linux.sh +++ b/linux/package-for-linux.sh @@ -1,22 +1,31 @@ #!/usr/bin/env bash set -euo pipefail +# $1: PBS arch (x86_64, aarch64). $2: microarch suffix (e.g. _v2 for x86_64 — a broad +# baseline covering ~all x86-64 CPUs since ~2009; aarch64 has no such variants so ""). PYTHON_ARCH=${1:?} PYTHON_ARCH_VER=${2:-""} +script_dir=$(dirname "$(realpath "$0")") + if [ -z "${PYTHON_VERSION:-}" ]; then - echo "PYTHON_VERSION is required (e.g. 3.13.12)" - exit 1 -fi -if [ -z "${PYTHON_DIST_RELEASE:-}" ]; then - echo "PYTHON_DIST_RELEASE is required" + echo "PYTHON_VERSION is required (e.g. 3.13.13)" exit 1 fi PYTHON_VERSION_SHORT=${PYTHON_VERSION_SHORT:-$(echo "$PYTHON_VERSION" | cut -d. -f1,2)} -DIST_FILE=cpython-${PYTHON_VERSION}+${PYTHON_DIST_RELEASE}-${PYTHON_ARCH}${PYTHON_ARCH_VER}-unknown-linux-gnu-install_only_stripped.tar.gz -curl -OL https://github.com/astral-sh/python-build-standalone/releases/download/${PYTHON_DIST_RELEASE}/${DIST_FILE} +# Resolve the python-build-standalone download URL. PYTHON_DIST_RELEASE is optional: +# when set it pins a specific release date, otherwise the newest release shipping this +# exact micro version + arch is auto-resolved. +DIST_URL=$(python3 "$script_dir/resolve_pbs.py" \ + --version "$PYTHON_VERSION" \ + --arch "$PYTHON_ARCH" \ + --arch-ver "$PYTHON_ARCH_VER" \ + ${PYTHON_DIST_RELEASE:+--release "$PYTHON_DIST_RELEASE"}) +DIST_FILE=$(basename "$DIST_URL") +echo "Downloading $DIST_FILE" +curl -OL "$DIST_URL" mkdir -p $PYTHON_ARCH/build tar zxvf $DIST_FILE -C $PYTHON_ARCH/build diff --git a/linux/python-linux-dart.exclude b/linux/python-linux-dart.exclude index fca773a..b9192d2 100644 --- a/linux/python-linux-dart.exclude +++ b/linux/python-linux-dart.exclude @@ -4,15 +4,29 @@ lib/python*/lib-dynload/_test*.so lib/python*/lib-dynload/_ctypes_test*.so lib/python*/lib-dynload/xxlimited*.so +lib/python*/lib-dynload/xxsubtype*.so lib/python*/lib-dynload/_xxtestfuzz*.so +# curses package is excluded below, so its C extensions are dead weight +lib/python*/lib-dynload/_curses*.so +lib/python*/lib-dynload/_curses_panel*.so lib/python*/curses lib/python*/ensurepip lib/python*/idlelib +lib/python*/lib2to3 lib/python*/pydoc_data lib/python*/test lib/python*/tkinter lib/python*/turtle* lib/python*/wsgiref +# Interactive-REPL / dev-only modules + easter eggs / frozen demos (never imported when +# running a script in embedded mode). Globs catch the .pyc too (sources go via *.py above). +lib/python*/_pyrepl +lib/python*/rlcompleter* +lib/python*/tabnanny* +lib/python*/antigravity* +lib/python*/this* +lib/python*/__hello__* +lib/python*/__phello__ lib/itcl* lib/tcl* lib/thread* diff --git a/linux/resolve_pbs.py b/linux/resolve_pbs.py new file mode 100644 index 0000000..2a17fba --- /dev/null +++ b/linux/resolve_pbs.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Resolve a python-build-standalone (PBS) download URL. + +PBS publishes one GitHub release per build date (tag = YYYYMMDD), each containing +assets for many CPython versions/arches. Rather than hand-pinning a release date, +this finds the newest release that actually ships the exact micro version + arch we +want and prints its download URL. + +Usage: + resolve_pbs.py --version 3.13.13 --arch x86_64 --arch-ver _v2 + resolve_pbs.py --version 3.13.13 --arch aarch64 # arch-ver defaults to "" + resolve_pbs.py --version 3.13.13 --arch x86_64 --arch-ver _v2 --release 20260510 + +When --release is given it is treated as an explicit override: the canonical URL for +that release is returned without hitting the API. Otherwise the GitHub releases API is +queried (authenticated with GITHUB_TOKEN/GH_TOKEN when present, to dodge rate limits). +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import urllib.error +import urllib.request + +REPO = "astral-sh/python-build-standalone" +API = f"https://api.github.com/repos/{REPO}/releases" +DL = f"https://github.com/{REPO}/releases/download" + +# PBS has a very long release history. A target micro that we'd ship is always recent, +# so bound the newest-first fallback scan rather than crawling every page (which both +# burns rate limit and risks gateway timeouts when the asset genuinely doesn't exist). +MAX_FALLBACK_PAGES = 5 # 5 * 100 = 500 most-recent releases + + +def asset_name(version: str, arch: str, arch_ver: str, release: str) -> str: + # NOTE: the "install_only_stripped" (not "freethreaded") flavour is the one we ship. + return ( + f"cpython-{version}+{release}-{arch}{arch_ver}" + f"-unknown-linux-gnu-install_only_stripped.tar.gz" + ) + + +def _get(url: str) -> bytes: + req = urllib.request.Request(url) + req.add_header("Accept", "application/vnd.github+json") + req.add_header("X-GitHub-Api-Version", "2022-11-28") + token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + if token: + req.add_header("Authorization", f"Bearer {token}") + with urllib.request.urlopen(req, timeout=60) as resp: + return resp.read() + + +def _find_in_release(release: dict, version: str, arch: str, arch_ver: str) -> str | None: + want = asset_name(version, arch, arch_ver, release["tag_name"]) + for asset in release.get("assets", []): + if asset.get("name") == want: + return asset["browser_download_url"] + return None + + +def resolve(version: str, arch: str, arch_ver: str) -> str: + # Try the latest release first (the common, fast path). + try: + latest = json.loads(_get(f"{API}/latest")) + url = _find_in_release(latest, version, arch, arch_ver) + if url: + return url + except urllib.error.HTTPError as exc: # pragma: no cover - network dependent + if exc.code not in (403, 404): + raise + + # Fall back to paging newest-first (bounded) until we find a release with the asset. + for page in range(1, MAX_FALLBACK_PAGES + 1): + releases = json.loads(_get(f"{API}?per_page=100&page={page}")) + if not releases: + break + for release in releases: # API returns newest-first + url = _find_in_release(release, version, arch, arch_ver) + if url: + return url + + raise SystemExit( + f"No python-build-standalone asset found for " + f"{asset_name(version, arch, arch_ver, '')} " + f"in the {MAX_FALLBACK_PAGES * 100} most-recent releases." + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--version", required=True, help="full CPython version, e.g. 3.13.13") + parser.add_argument("--arch", required=True, help="e.g. x86_64 or aarch64") + parser.add_argument("--arch-ver", default="", help="microarch suffix, e.g. _v2 (default: none)") + parser.add_argument( + "--release", + default="", + help="explicit PBS release tag (YYYYMMDD); overrides auto-resolution", + ) + args = parser.parse_args() + + if args.release: + name = asset_name(args.version, args.arch, args.arch_ver, args.release) + print(f"{DL}/{args.release}/{name}") + return + + print(resolve(args.version, args.arch, args.arch_ver)) + + +if __name__ == "__main__": + main() diff --git a/windows/python-windows-dart.exclude b/windows/python-windows-dart.exclude index 886d977..95657b4 100644 --- a/windows/python-windows-dart.exclude +++ b/windows/python-windows-dart.exclude @@ -12,12 +12,21 @@ Lib/lib2to3 Lib/site-packages/pip* Lib/site-packages/setuptools* Lib/site-packages/wheel* +# Interactive-REPL / dev-only modules + easter eggs / frozen demos. +Lib/_pyrepl +Lib/rlcompleter.py +Lib/tabnanny.py +Lib/antigravity.py +Lib/this.py +Lib/__hello__.py +Lib/__phello__ # Optional/test/GUI extension modules and DLLs. DLLs/_test*.pyd DLLs/_ctypes_test*.pyd DLLs/_tkinter*.pyd DLLs/xxlimited*.pyd +DLLs/xxsubtype*.pyd DLLs/tcl86t.dll DLLs/tk86t.dll DLLs/zlib1.dll