From c919018c782d2332d19683d870d36947870b720b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 21:08:53 +0000 Subject: [PATCH] Remove Android and iOS directories and documentation Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- Android/README.md | 166 ---- Android/android-env.sh | 99 --- Android/android.py | 812 ------------------ Android/testbed/.gitignore | 22 - .../inspectionProfiles/Project_Default.xml | 8 - Android/testbed/app/.gitignore | 1 - Android/testbed/app/build.gradle.kts | 268 ------ .../java/org/python/testbed/PythonSuite.kt | 35 - .../testbed/app/src/main/AndroidManifest.xml | 20 - Android/testbed/app/src/main/c/CMakeLists.txt | 14 - .../testbed/app/src/main/c/main_activity.c | 152 ---- .../java/org/python/testbed/MainActivity.kt | 92 -- .../src/main/python/android_testbed_main.py | 48 -- .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 3110 -> 0 bytes .../app/src/main/res/layout/activity_main.xml | 19 - .../app/src/main/res/values/strings.xml | 3 - Android/testbed/build.gradle.kts | 5 - Android/testbed/gradle.properties | 28 - .../gradle/wrapper/gradle-wrapper.properties | 6 - Android/testbed/settings.gradle.kts | 18 - Doc/using/android.rst | 74 -- Doc/using/index.rst | 2 - Doc/using/ios.rst | 393 --------- Lib/_android_support.py | 181 ---- Lib/_ios_support.py | 71 -- Lib/test/test_android.py | 457 ---------- iOS/README.rst | 373 -------- iOS/Resources/Info.plist.in | 34 - iOS/Resources/bin/arm64-apple-ios-ar | 2 - iOS/Resources/bin/arm64-apple-ios-clang | 2 - iOS/Resources/bin/arm64-apple-ios-clang++ | 2 - iOS/Resources/bin/arm64-apple-ios-cpp | 2 - .../bin/arm64-apple-ios-simulator-ar | 2 - .../bin/arm64-apple-ios-simulator-clang | 2 - .../bin/arm64-apple-ios-simulator-clang++ | 2 - .../bin/arm64-apple-ios-simulator-cpp | 2 - .../bin/arm64-apple-ios-simulator-strip | 2 - iOS/Resources/bin/arm64-apple-ios-strip | 2 - .../bin/x86_64-apple-ios-simulator-ar | 2 - .../bin/x86_64-apple-ios-simulator-clang | 2 - .../bin/x86_64-apple-ios-simulator-clang++ | 2 - .../bin/x86_64-apple-ios-simulator-cpp | 2 - .../bin/x86_64-apple-ios-simulator-strip | 2 - iOS/Resources/dylib-Info-template.plist | 26 - iOS/Resources/pyconfig.h | 7 - iOS/testbed/Python.xcframework/Info.plist | 44 - .../Python.xcframework/ios-arm64/README | 4 - .../ios-arm64_x86_64-simulator/README | 4 - iOS/testbed/__main__.py | 548 ------------ .../iOSTestbed.xcodeproj/project.pbxproj | 580 ------------- iOS/testbed/iOSTestbed/AppDelegate.h | 11 - iOS/testbed/iOSTestbed/AppDelegate.m | 19 - .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 13 - .../iOSTestbed/Assets.xcassets/Contents.json | 6 - .../Base.lproj/LaunchScreen.storyboard | 9 - iOS/testbed/iOSTestbed/app/README | 7 - iOS/testbed/iOSTestbed/app_packages/README | 7 - .../iOSTestbed/dylib-Info-template.plist | 26 - iOS/testbed/iOSTestbed/iOSTestbed-Info.plist | 64 -- iOS/testbed/iOSTestbed/main.m | 16 - iOS/testbed/iOSTestbedTests/iOSTestbedTests.m | 193 ----- 62 files changed, 5026 deletions(-) delete mode 100644 Android/README.md delete mode 100644 Android/android-env.sh delete mode 100644 Android/android.py delete mode 100644 Android/testbed/.gitignore delete mode 100644 Android/testbed/.idea/inspectionProfiles/Project_Default.xml delete mode 100644 Android/testbed/app/.gitignore delete mode 100644 Android/testbed/app/build.gradle.kts delete mode 100644 Android/testbed/app/src/androidTest/java/org/python/testbed/PythonSuite.kt delete mode 100644 Android/testbed/app/src/main/AndroidManifest.xml delete mode 100644 Android/testbed/app/src/main/c/CMakeLists.txt delete mode 100644 Android/testbed/app/src/main/c/main_activity.c delete mode 100644 Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt delete mode 100644 Android/testbed/app/src/main/python/android_testbed_main.py delete mode 100644 Android/testbed/app/src/main/res/drawable-xxhdpi/ic_launcher.png delete mode 100644 Android/testbed/app/src/main/res/layout/activity_main.xml delete mode 100644 Android/testbed/app/src/main/res/values/strings.xml delete mode 100644 Android/testbed/build.gradle.kts delete mode 100644 Android/testbed/gradle.properties delete mode 100644 Android/testbed/gradle/wrapper/gradle-wrapper.properties delete mode 100644 Android/testbed/settings.gradle.kts delete mode 100644 Doc/using/android.rst delete mode 100644 Doc/using/ios.rst delete mode 100644 Lib/_android_support.py delete mode 100644 Lib/_ios_support.py delete mode 100644 Lib/test/test_android.py delete mode 100644 iOS/README.rst delete mode 100644 iOS/Resources/Info.plist.in delete mode 100644 iOS/Resources/bin/arm64-apple-ios-ar delete mode 100644 iOS/Resources/bin/arm64-apple-ios-clang delete mode 100644 iOS/Resources/bin/arm64-apple-ios-clang++ delete mode 100644 iOS/Resources/bin/arm64-apple-ios-cpp delete mode 100644 iOS/Resources/bin/arm64-apple-ios-simulator-ar delete mode 100644 iOS/Resources/bin/arm64-apple-ios-simulator-clang delete mode 100644 iOS/Resources/bin/arm64-apple-ios-simulator-clang++ delete mode 100644 iOS/Resources/bin/arm64-apple-ios-simulator-cpp delete mode 100644 iOS/Resources/bin/arm64-apple-ios-simulator-strip delete mode 100644 iOS/Resources/bin/arm64-apple-ios-strip delete mode 100644 iOS/Resources/bin/x86_64-apple-ios-simulator-ar delete mode 100644 iOS/Resources/bin/x86_64-apple-ios-simulator-clang delete mode 100644 iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ delete mode 100644 iOS/Resources/bin/x86_64-apple-ios-simulator-cpp delete mode 100644 iOS/Resources/bin/x86_64-apple-ios-simulator-strip delete mode 100644 iOS/Resources/dylib-Info-template.plist delete mode 100644 iOS/Resources/pyconfig.h delete mode 100644 iOS/testbed/Python.xcframework/Info.plist delete mode 100644 iOS/testbed/Python.xcframework/ios-arm64/README delete mode 100644 iOS/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README delete mode 100644 iOS/testbed/__main__.py delete mode 100644 iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj delete mode 100644 iOS/testbed/iOSTestbed/AppDelegate.h delete mode 100644 iOS/testbed/iOSTestbed/AppDelegate.m delete mode 100644 iOS/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 iOS/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 iOS/testbed/iOSTestbed/Assets.xcassets/Contents.json delete mode 100644 iOS/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard delete mode 100644 iOS/testbed/iOSTestbed/app/README delete mode 100644 iOS/testbed/iOSTestbed/app_packages/README delete mode 100644 iOS/testbed/iOSTestbed/dylib-Info-template.plist delete mode 100644 iOS/testbed/iOSTestbed/iOSTestbed-Info.plist delete mode 100644 iOS/testbed/iOSTestbed/main.m delete mode 100644 iOS/testbed/iOSTestbedTests/iOSTestbedTests.m diff --git a/Android/README.md b/Android/README.md deleted file mode 100644 index 62685f6..0000000 --- a/Android/README.md +++ /dev/null @@ -1,166 +0,0 @@ -# Typthon for Android - -If you obtained this README as part of a release package, then the only -applicable sections are "Prerequisites", "Testing", and "Using in your own app". - -If you obtained this README as part of the CTypthon source tree, then you can -also follow the other sections to compile Typthon for Android yourself. - -However, most app developers should not need to do any of these things manually. -Instead, use one of the tools listed -[here](https://docs.python.org/3/using/android.html), which will provide a much -easier experience. - - -## Prerequisites - -If you already have an Android SDK installed, export the `ANDROID_HOME` -environment variable to point at its location. Otherwise, here's how to install -it: - -* Download the "Command line tools" from . -* Create a directory `android-sdk/cmdline-tools`, and unzip the command line - tools package into it. -* Rename `android-sdk/cmdline-tools/cmdline-tools` to - `android-sdk/cmdline-tools/latest`. -* `export ANDROID_HOME=/path/to/android-sdk` - -The `android.py` script will automatically use the SDK's `sdkmanager` to install -any packages it needs. - -The script also requires the following commands to be on the `PATH`: - -* `curl` -* `java` (or set the `JAVA_HOME` environment variable) - - -## Building - -Typthon can be built for Android on any POSIX platform supported by the Android -development tools, which currently means Linux or macOS. - -First we'll make a "build" Python (for your development machine), then use it to -help produce a "host" Typthon for Android. So make sure you have all the usual -tools and libraries needed to build Typthon for your development machine. - -The easiest way to do a build is to use the `android.py` script. You can either -have it perform the entire build process from start to finish in one step, or -you can do it in discrete steps that mirror running `configure` and `make` for -each of the two builds of Typthon you end up producing. - -The discrete steps for building via `android.py` are: - -```sh -./android.py configure-build -./android.py make-build -./android.py configure-host HOST -./android.py make-host HOST -``` - -`HOST` identifies which architecture to build. To see the possible values, run -`./android.py configure-host --help`. - -To do all steps in a single command, run: - -```sh -./android.py build HOST -``` - -In the end you should have a build Python in `cross-build/build`, and a host -Python in `cross-build/HOST`. - -You can use `--` as a separator for any of the `configure`-related commands – -including `build` itself – to pass arguments to the underlying `configure` -call. For example, if you want a pydebug build that also caches the results from -`configure`, you can do: - -```sh -./android.py build HOST -- -C --with-pydebug -``` - - -## Packaging - -After building an architecture as described in the section above, you can -package it for release with this command: - -```sh -./android.py package HOST -``` - -`HOST` is defined in the section above. - -This will generate a tarball in `cross-build/HOST/dist`, whose structure is -similar to the `Android` directory of the CTypthon source tree. - - -## Testing - -The Python test suite can be run on Linux, macOS, or Windows: - -* On Linux, the emulator needs access to the KVM virtualization interface, and - a DISPLAY environment variable pointing at an X server. Xvfb is acceptable. - -The test suite can usually be run on a device with 2 GB of RAM, but this is -borderline, so you may need to increase it to 4 GB. As of Android -Studio Koala, 2 GB is the default for all emulators, although the user interface -may indicate otherwise. Locate the emulator's directory under `~/.android/avd`, -and find `hw.ramSize` in both config.ini and hardware-qemu.ini. Either set these -manually to the same value, or use the Android Studio Device Manager, which will -update both files. - -You can run the test suite either: - -* Within the CPython repository, after doing a build as described above. On - Windows, you won't be able to do the build on the same machine, so you'll have - to copy the `cross-build/HOST/prefix` directory from somewhere else. - -* Or by taking a release package built using the `package` command, extracting - it wherever you want, and using its own copy of `android.py`. - -The test script supports the following modes: - -* In `--connected` mode, it runs on a device or emulator you have already - connected to the build machine. List the available devices with - `$ANDROID_HOME/platform-tools/adb devices -l`, then pass a device ID to the - script like this: - - ```sh - ./android.py test --connected emulator-5554 - ``` - -* In `--managed` mode, it uses a temporary headless emulator defined in the - `managedDevices` section of testbed/app/build.gradle.kts. This mode is slower, - but more reproducible. - - We currently define two devices: `minVersion` and `maxVersion`, corresponding - to our minimum and maximum supported Android versions. For example: - - ```sh - ./android.py test --managed maxVersion - ``` - -By default, the only messages the script will show are Python's own stdout and -stderr. Add the `-v` option to also show Gradle output, and non-Python logcat -messages. - -Any other arguments on the `android.py test` command line will be passed through -to `python -m test` – use `--` to separate them from android.py's own options. -See the [Python Developer's -Guide](https://devguide.python.org/testing/run-write-tests/) for common options -– most of them will work on Android, except for those that involve subprocesses, -such as `-j`. - -Every time you run `android.py test`, changes in pure-Python files in the -repository's `Lib` directory will be picked up immediately. Changes in C files, -and architecture-specific files such as sysconfigdata, will not take effect -until you re-run `android.py make-host` or `build`. - -The testbed app can also be used to test third-party packages. For more details, -run `android.py test --help`, paying attention to the options `--site-packages`, -`--cwd`, `-c` and `-m`. - - -## Using in your own app - -See https://docs.python.org/3/using/android.html. diff --git a/Android/android-env.sh b/Android/android-env.sh deleted file mode 100644 index 7b381a0..0000000 --- a/Android/android-env.sh +++ /dev/null @@ -1,99 +0,0 @@ -# This script must be sourced with the following variables already set: -: "${ANDROID_HOME:?}" # Path to Android SDK -: "${HOST:?}" # GNU target triplet - -# You may also override the following: -: "${ANDROID_API_LEVEL:=24}" # Minimum Android API level the build will run on -: "${PREFIX:-}" # Path in which to find required libraries - - -# Print all messages on stderr so they're visible when running within build-wheel. -log() { - echo "$1" >&2 -} - -fail() { - log "$1" - exit 1 -} - -# When moving to a new version of the NDK, carefully review the following: -# -# * https://developer.android.com/ndk/downloads/revision_history -# -# * https://android.googlesource.com/platform/ndk/+/ndk-rXX-release/docs/BuildSystemMaintainers.md -# where XX is the NDK version. Do a diff against the version you're upgrading from, e.g.: -# https://android.googlesource.com/platform/ndk/+/ndk-r25-release..ndk-r26-release/docs/BuildSystemMaintainers.md -ndk_version=27.2.12479018 - -ndk=$ANDROID_HOME/ndk/$ndk_version -if ! [ -e "$ndk" ]; then - log "Installing NDK - this may take several minutes" - yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;$ndk_version" -fi - -if [ "$HOST" = "arm-linux-androideabi" ]; then - clang_triplet=armv7a-linux-androideabi -else - clang_triplet="$HOST" -fi - -# These variables are based on BuildSystemMaintainers.md above, and -# $ndk/build/cmake/android.toolchain.cmake. -toolchain=$(echo "$ndk"/toolchains/llvm/prebuilt/*) -export AR="$toolchain/bin/llvm-ar" -export AS="$toolchain/bin/llvm-as" -export CC="$toolchain/bin/${clang_triplet}${ANDROID_API_LEVEL}-clang" -export CXX="${CC}++" -export LD="$toolchain/bin/ld" -export NM="$toolchain/bin/llvm-nm" -export RANLIB="$toolchain/bin/llvm-ranlib" -export READELF="$toolchain/bin/llvm-readelf" -export STRIP="$toolchain/bin/llvm-strip" - -# The quotes make sure the wildcard in the `toolchain` assignment has been expanded. -for path in "$AR" "$AS" "$CC" "$CXX" "$LD" "$NM" "$RANLIB" "$READELF" "$STRIP"; do - if ! [ -e "$path" ]; then - fail "$path does not exist" - fi -done - -export CFLAGS="-D__BIONIC_NO_PAGE_SIZE_MACRO" -export LDFLAGS="-Wl,--build-id=sha1 -Wl,--no-rosegment -Wl,-z,max-page-size=16384" - -# Unlike Linux, Android does not implicitly use a dlopened library to resolve -# relocations in subsequently-loaded libraries, even if RTLD_GLOBAL is used -# (https://github.com/android/ndk/issues/1244). So any library that fails to -# build with this flag, would also fail to load at runtime. -LDFLAGS="$LDFLAGS -Wl,--no-undefined" - -# Many packages get away with omitting -lm on Linux, but Android is stricter. -LDFLAGS="$LDFLAGS -lm" - -# -mstackrealign is included where necessary in the clang launcher scripts which are -# pointed to by $CC, so we don't need to include it here. -if [ "$HOST" = "arm-linux-androideabi" ]; then - CFLAGS="$CFLAGS -march=armv7-a -mthumb" -fi - -if [ -n "${PREFIX:-}" ]; then - abs_prefix="$(realpath "$PREFIX")" - CFLAGS="$CFLAGS -I$abs_prefix/include" - LDFLAGS="$LDFLAGS -L$abs_prefix/lib" - - export PKG_CONFIG="pkg-config --define-prefix" - export PKG_CONFIG_LIBDIR="$abs_prefix/lib/pkgconfig" -fi - -# When compiling C++, some build systems will combine CFLAGS and CXXFLAGS, and some will -# use CXXFLAGS alone. -export CXXFLAGS="$CFLAGS" - -# Use the same variable name as conda-build -if [ "$(uname)" = "Darwin" ]; then - CPU_COUNT="$(sysctl -n hw.ncpu)" - export CPU_COUNT -else - CPU_COUNT="$(nproc)" - export CPU_COUNT -fi diff --git a/Android/android.py b/Android/android.py deleted file mode 100644 index a3a48c0..0000000 --- a/Android/android.py +++ /dev/null @@ -1,812 +0,0 @@ -#!/usr/bin/env python3 - -import asyncio -import argparse -import os -import re -import shlex -import shutil -import signal -import subprocess -import sys -import sysconfig -from asyncio import wait_for -from contextlib import asynccontextmanager -from datetime import datetime, timezone -from glob import glob -from os.path import abspath, basename, relpath -from pathlib import Path -from subprocess import CalledProcessError -from tempfile import TemporaryDirectory - - -SCRIPT_NAME = Path(__file__).name -ANDROID_DIR = Path(__file__).resolve().parent -PYTHON_DIR = ANDROID_DIR.parent -in_source_tree = ( - ANDROID_DIR.name == "Android" and (PYTHON_DIR / "pyconfig.h.in").exists() -) - -TESTBED_DIR = ANDROID_DIR / "testbed" -CROSS_BUILD_DIR = PYTHON_DIR / "cross-build" - -HOSTS = ["aarch64-linux-android", "x86_64-linux-android"] -APP_ID = "org.python.testbed" -DECODE_ARGS = ("UTF-8", "backslashreplace") - - -try: - android_home = Path(os.environ['ANDROID_HOME']) -except KeyError: - sys.exit("The ANDROID_HOME environment variable is required.") - -adb = Path( - f"{android_home}/platform-tools/adb" - + (".exe" if os.name == "nt" else "") -) - -gradlew = Path( - f"{TESTBED_DIR}/gradlew" - + (".bat" if os.name == "nt" else "") -) - -logcat_started = False - - -def delete_glob(pattern): - # Path.glob doesn't accept non-relative patterns. - for path in glob(str(pattern)): - path = Path(path) - print(f"Deleting {path} ...") - if path.is_dir() and not path.is_symlink(): - shutil.rmtree(path) - else: - path.unlink() - - -def subdir(*parts, create=False): - path = CROSS_BUILD_DIR.joinpath(*parts) - if not path.exists(): - if not create: - sys.exit( - f"{path} does not exist. Create it by running the appropriate " - f"`configure` subcommand of {SCRIPT_NAME}.") - else: - path.mkdir(parents=True) - return path - - -def run(command, *, host=None, env=None, log=True, **kwargs): - kwargs.setdefault("check", True) - if env is None: - env = os.environ.copy() - - if host: - host_env = android_env(host) - print_env(host_env) - env.update(host_env) - - if log: - print(">", join_command(command)) - return subprocess.run(command, env=env, **kwargs) - - -# Format a command so it can be copied into a shell. Like shlex.join, but also -# accepts arguments which are Paths, or a single string/Path outside of a list. -def join_command(args): - if isinstance(args, (str, Path)): - return str(args) - else: - return shlex.join(map(str, args)) - - -# Format the environment so it can be pasted into a shell. -def print_env(env): - for key, value in sorted(env.items()): - print(f"export {key}={shlex.quote(value)}") - - -def android_env(host): - if host: - prefix = subdir(host) / "prefix" - else: - prefix = ANDROID_DIR / "prefix" - sysconfig_files = prefix.glob("lib/python*/_sysconfigdata__android_*.py") - sysconfig_filename = next(sysconfig_files).name - host = re.fullmatch(r"_sysconfigdata__android_(.+).py", sysconfig_filename)[1] - - env_script = ANDROID_DIR / "android-env.sh" - env_output = subprocess.run( - f"set -eu; " - f"export HOST={host}; " - f"PREFIX={prefix}; " - f". {env_script}; " - f"export", - check=True, shell=True, capture_output=True, encoding='utf-8', - ).stdout - - env = {} - for line in env_output.splitlines(): - # We don't require every line to match, as there may be some other - # output from installing the NDK. - if match := re.search( - "^(declare -x |export )?(\\w+)=['\"]?(.*?)['\"]?$", line - ): - key, value = match[2], match[3] - if os.environ.get(key) != value: - env[key] = value - - if not env: - raise ValueError(f"Found no variables in {env_script.name} output:\n" - + env_output) - return env - - -def build_python_path(): - """The path to the build Python binary.""" - build_dir = subdir("build") - binary = build_dir / "python" - if not binary.is_file(): - binary = binary.with_suffix(".exe") - if not binary.is_file(): - raise FileNotFoundError("Unable to find `python(.exe)` in " - f"{build_dir}") - - return binary - - -def configure_build_python(context): - if context.clean: - clean("build") - os.chdir(subdir("build", create=True)) - - command = [relpath(PYTHON_DIR / "configure")] - if context.args: - command.extend(context.args) - run(command) - - -def make_build_python(context): - os.chdir(subdir("build")) - run(["make", "-j", str(os.cpu_count())]) - - -def unpack_deps(host, prefix_dir): - os.chdir(prefix_dir) - deps_url = "https://github.com/beeware/cpython-android-source-deps/releases/download" - for name_ver in ["bzip2-1.0.8-3", "libffi-3.4.4-3", "openssl-3.0.15-4", - "sqlite-3.49.1-0", "xz-5.4.6-1", "zstd-1.5.7-1"]: - filename = f"{name_ver}-{host}.tar.gz" - download(f"{deps_url}/{name_ver}/{filename}") - shutil.unpack_archive(filename) - os.remove(filename) - - -def download(url, target_dir="."): - out_path = f"{target_dir}/{basename(url)}" - run(["curl", "-Lf", "--retry", "5", "--retry-all-errors", "-o", out_path, url]) - return out_path - - -def configure_host_python(context): - if context.clean: - clean(context.host) - - host_dir = subdir(context.host, create=True) - prefix_dir = host_dir / "prefix" - if not prefix_dir.exists(): - prefix_dir.mkdir() - unpack_deps(context.host, prefix_dir) - - os.chdir(host_dir) - command = [ - # Basic cross-compiling configuration - relpath(PYTHON_DIR / "configure"), - f"--host={context.host}", - f"--build={sysconfig.get_config_var('BUILD_GNU_TYPE')}", - f"--with-build-python={build_python_path()}", - "--without-ensurepip", - - # Android always uses a shared libpython. - "--enable-shared", - "--without-static-libpython", - - # Dependent libraries. The others are found using pkg-config: see - # android-env.sh. - f"--with-openssl={prefix_dir}", - ] - - if context.args: - command.extend(context.args) - run(command, host=context.host) - - -def make_host_python(context): - # The CFLAGS and LDFLAGS set in android-env include the prefix dir, so - # delete any previous Python installation to prevent it being used during - # the build. - host_dir = subdir(context.host) - prefix_dir = host_dir / "prefix" - for pattern in ("include/python*", "lib/libpython*", "lib/python*"): - delete_glob(f"{prefix_dir}/{pattern}") - - # The Android environment variables were already captured in the Makefile by - # `configure`, and passing them again when running `make` may cause some - # flags to be duplicated. So we don't use the `host` argument here. - os.chdir(host_dir) - run(["make", "-j", str(os.cpu_count())]) - run(["make", "install", f"prefix={prefix_dir}"]) - - -def build_all(context): - steps = [configure_build_python, make_build_python, configure_host_python, - make_host_python] - for step in steps: - step(context) - - -def clean(host): - delete_glob(CROSS_BUILD_DIR / host) - - -def clean_all(context): - for host in HOSTS + ["build"]: - clean(host) - - -def setup_sdk(): - sdkmanager = android_home / ( - "cmdline-tools/latest/bin/sdkmanager" - + (".bat" if os.name == "nt" else "") - ) - - # Gradle will fail if it needs to install an SDK package whose license - # hasn't been accepted, so pre-accept all licenses. - if not all((android_home / "licenses" / path).exists() for path in [ - "android-sdk-arm-dbt-license", "android-sdk-license" - ]): - run( - [sdkmanager, "--licenses"], - text=True, - capture_output=True, - input="y\n" * 100, - ) - - # Gradle may install this automatically, but we can't rely on that because - # we need to run adb within the logcat task. - if not adb.exists(): - run([sdkmanager, "platform-tools"]) - - -# To avoid distributing compiled artifacts without corresponding source code, -# the Gradle wrapper is not included in the CPython repository. Instead, we -# extract it from the Gradle GitHub repository. -def setup_testbed(): - paths = ["gradlew", "gradlew.bat", "gradle/wrapper/gradle-wrapper.jar"] - if all((TESTBED_DIR / path).exists() for path in paths): - return - - # The wrapper version isn't important, as any version of the wrapper can - # download any version of Gradle. The Gradle version actually used for the - # build is specified in testbed/gradle/wrapper/gradle-wrapper.properties. - version = "8.9.0" - - for path in paths: - out_path = TESTBED_DIR / path - out_path.parent.mkdir(exist_ok=True) - download( - f"https://raw.githubusercontent.com/gradle/gradle/v{version}/{path}", - out_path.parent, - ) - os.chmod(out_path, 0o755) - - -# run_testbed will build the app automatically, but it's useful to have this as -# a separate command to allow running the app outside of this script. -def build_testbed(context): - setup_sdk() - setup_testbed() - run( - [gradlew, "--console", "plain", "packageDebug", "packageDebugAndroidTest"], - cwd=TESTBED_DIR, - ) - - -# Work around a bug involving sys.exit and TaskGroups -# (https://github.com/python/cpython/issues/101515). -def exit(*args): - raise MySystemExit(*args) - - -class MySystemExit(Exception): - pass - - -# The `test` subcommand runs all subprocesses through this context manager so -# that no matter what happens, they can always be cancelled from another task, -# and they will always be cleaned up on exit. -@asynccontextmanager -async def async_process(*args, **kwargs): - process = await asyncio.create_subprocess_exec(*args, **kwargs) - try: - yield process - finally: - if process.returncode is None: - # Allow a reasonably long time for Gradle to clean itself up, - # because we don't want stale emulators left behind. - timeout = 10 - process.terminate() - try: - await wait_for(process.wait(), timeout) - except TimeoutError: - print( - f"Command {args} did not terminate after {timeout} seconds " - f" - sending SIGKILL" - ) - process.kill() - - # Even after killing the process we must still wait for it, - # otherwise we'll get the warning "Exception ignored in __del__". - await wait_for(process.wait(), timeout=1) - - -async def async_check_output(*args, **kwargs): - async with async_process( - *args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs - ) as process: - stdout, stderr = await process.communicate() - if process.returncode == 0: - return stdout.decode(*DECODE_ARGS) - else: - raise CalledProcessError( - process.returncode, args, - stdout.decode(*DECODE_ARGS), stderr.decode(*DECODE_ARGS) - ) - - -# Return a list of the serial numbers of connected devices. Emulators will have -# serials of the form "emulator-5678". -async def list_devices(): - serials = [] - header_found = False - - lines = (await async_check_output(adb, "devices")).splitlines() - for line in lines: - # Ignore blank lines, and all lines before the header. - line = line.strip() - if line == "List of devices attached": - header_found = True - elif header_found and line: - try: - serial, status = line.split() - except ValueError: - raise ValueError(f"failed to parse {line!r}") - if status == "device": - serials.append(serial) - - if not header_found: - raise ValueError(f"failed to parse {lines}") - return serials - - -async def find_device(context, initial_devices): - if context.managed: - print("Waiting for managed device - this may take several minutes") - while True: - new_devices = set(await list_devices()).difference(initial_devices) - if len(new_devices) == 0: - await asyncio.sleep(1) - elif len(new_devices) == 1: - serial = new_devices.pop() - print(f"Serial: {serial}") - return serial - else: - exit(f"Found more than one new device: {new_devices}") - else: - return context.connected - - -# An older version of this script in #121595 filtered the logs by UID instead. -# But logcat can't filter by UID until API level 31. If we ever switch back to -# filtering by UID, we'll also have to filter by time so we only show messages -# produced after the initial call to `stop_app`. -# -# We're more likely to miss the PID because it's shorter-lived, so there's a -# workaround in PythonSuite.kt to stop it being *too* short-lived. -async def find_pid(serial): - print("Waiting for app to start - this may take several minutes") - shown_error = False - while True: - try: - # `pidof` requires API level 24 or higher. The level 23 emulator - # includes it, but it doesn't work (it returns all processes). - pid = (await async_check_output( - adb, "-s", serial, "shell", "pidof", "-s", APP_ID - )).strip() - except CalledProcessError as e: - # If the app isn't running yet, pidof gives no output. So if there - # is output, there must have been some other error. However, this - # sometimes happens transiently, especially when running a managed - # emulator for the first time, so don't make it fatal. - if (e.stdout or e.stderr) and not shown_error: - print_called_process_error(e) - print("This may be transient, so continuing to wait") - shown_error = True - else: - # Some older devices (e.g. Nexus 4) return zero even when no process - # was found, so check whether we actually got any output. - if pid: - print(f"PID: {pid}") - return pid - - # Loop fairly rapidly to avoid missing a short-lived process. - await asyncio.sleep(0.2) - - -async def logcat_task(context, initial_devices): - # Gradle may need to do some large downloads of libraries and emulator - # images. This will happen during find_device in --managed mode, or find_pid - # in --connected mode. - startup_timeout = 600 - serial = await wait_for(find_device(context, initial_devices), startup_timeout) - pid = await wait_for(find_pid(serial), startup_timeout) - - # `--pid` requires API level 24 or higher. - args = [adb, "-s", serial, "logcat", "--pid", pid, "--format", "tag"] - hidden_output = [] - async with async_process( - *args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - ) as process: - while line := (await process.stdout.readline()).decode(*DECODE_ARGS): - if match := re.fullmatch(r"([A-Z])/(.*)", line, re.DOTALL): - level, message = match.groups() - else: - # If the regex doesn't match, this is probably the second or - # subsequent line of a multi-line message. Python won't produce - # such messages, but other components might. - level, message = None, line - - # Exclude high-volume messages which are rarely useful. - if context.verbose < 2 and "from python test_syslog" in message: - continue - - # Put high-level messages on stderr so they're highlighted in the - # buildbot logs. This will include Python's own stderr. - stream = ( - sys.stderr - if level in ["W", "E", "F"] # WARNING, ERROR, FATAL (aka ASSERT) - else sys.stdout - ) - - # To simplify automated processing of the output, e.g. a buildbot - # posting a failure notice on a GitHub PR, we strip the level and - # tag indicators from Python's stdout and stderr. - for prefix in ["python.stdout: ", "python.stderr: "]: - if message.startswith(prefix): - global logcat_started - logcat_started = True - stream.write(message.removeprefix(prefix)) - break - else: - if context.verbose: - # Non-Python messages add a lot of noise, but they may - # sometimes help explain a failure. - stream.write(line) - else: - hidden_output.append(line) - - # If the device disconnects while logcat is running, which always - # happens in --managed mode, some versions of adb return non-zero. - # Distinguish this from a logcat startup error by checking whether we've - # received a message from Python yet. - status = await wait_for(process.wait(), timeout=1) - if status != 0 and not logcat_started: - raise CalledProcessError(status, args, "".join(hidden_output)) - - -def stop_app(serial): - run([adb, "-s", serial, "shell", "am", "force-stop", APP_ID], log=False) - - -async def gradle_task(context): - env = os.environ.copy() - if context.managed: - task_prefix = context.managed - else: - task_prefix = "connected" - env["ANDROID_SERIAL"] = context.connected - - hidden_output = [] - - def log(line): - # Gradle may take several minutes to install SDK packages, so it's worth - # showing those messages even in non-verbose mode. - if context.verbose or line.startswith('Preparing "Install'): - sys.stdout.write(line) - else: - hidden_output.append(line) - - if context.command: - mode = "-c" - module = context.command - else: - mode = "-m" - module = context.module or "test" - - args = [ - gradlew, "--console", "plain", f"{task_prefix}DebugAndroidTest", - ] + [ - # Build-time properties - f"-Ppython.{name}={value}" - for name, value in [ - ("sitePackages", context.site_packages), ("cwd", context.cwd) - ] if value - ] + [ - # Runtime properties - f"-Pandroid.testInstrumentationRunnerArguments.python{name}={value}" - for name, value in [ - ("Mode", mode), ("Module", module), ("Args", join_command(context.args)) - ] if value - ] - if context.verbose >= 2: - args.append("--info") - log("> " + join_command(args)) - - try: - async with async_process( - *args, cwd=TESTBED_DIR, env=env, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - ) as process: - while line := (await process.stdout.readline()).decode(*DECODE_ARGS): - log(line) - - status = await wait_for(process.wait(), timeout=1) - if status == 0: - exit(0) - else: - raise CalledProcessError(status, args) - finally: - # If logcat never started, then something has gone badly wrong, so the - # user probably wants to see the Gradle output even in non-verbose mode. - if hidden_output and not logcat_started: - sys.stdout.write("".join(hidden_output)) - - # Gradle does not stop the tests when interrupted. - if context.connected: - stop_app(context.connected) - - -async def run_testbed(context): - setup_sdk() - setup_testbed() - - if context.managed: - # In this mode, Gradle will create a device with an unpredictable name. - # So we save a list of the running devices before starting Gradle, and - # find_device then waits for a new device to appear. - initial_devices = await list_devices() - else: - # In case the previous shutdown was unclean, make sure the app isn't - # running, otherwise we might show logs from a previous run. This is - # unnecessary in --managed mode, because Gradle creates a new emulator - # every time. - stop_app(context.connected) - initial_devices = None - - try: - async with asyncio.TaskGroup() as tg: - tg.create_task(logcat_task(context, initial_devices)) - tg.create_task(gradle_task(context)) - except* MySystemExit as e: - raise SystemExit(*e.exceptions[0].args) from None - except* CalledProcessError as e: - # Extract it from the ExceptionGroup so it can be handled by `main`. - raise e.exceptions[0] - - -def package_version(prefix_dir): - patchlevel_glob = f"{prefix_dir}/include/python*/patchlevel.h" - patchlevel_paths = glob(patchlevel_glob) - if len(patchlevel_paths) != 1: - sys.exit(f"{patchlevel_glob} matched {len(patchlevel_paths)} paths.") - - for line in open(patchlevel_paths[0]): - if match := re.fullmatch(r'\s*#define\s+PY_VERSION\s+"(.+)"\s*', line): - version = match[1] - break - else: - sys.exit(f"Failed to find Python version in {patchlevel_paths[0]}.") - - # If not building against a tagged commit, add a timestamp to the version. - # Follow the PyPA version number rules, as this will make it easier to - # process with other tools. - if version.endswith("+"): - version += datetime.now(timezone.utc).strftime("%Y%m%d.%H%M%S") - - return version - - -def package(context): - prefix_dir = subdir(context.host, "prefix") - version = package_version(prefix_dir) - - with TemporaryDirectory(prefix=SCRIPT_NAME) as temp_dir: - temp_dir = Path(temp_dir) - - # Include all tracked files from the Android directory. - for line in run( - ["git", "ls-files"], - cwd=ANDROID_DIR, capture_output=True, text=True, log=False, - ).stdout.splitlines(): - src = ANDROID_DIR / line - dst = temp_dir / line - dst.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(src, dst, follow_symlinks=False) - - # Include anything from the prefix directory which could be useful - # either for embedding Python in an app, or building third-party - # packages against it. - for rel_dir, patterns in [ - ("include", ["openssl*", "python*", "sqlite*"]), - ("lib", ["engines-3", "libcrypto*.so", "libpython*", "libsqlite*", - "libssl*.so", "ossl-modules", "python*"]), - ("lib/pkgconfig", ["*crypto*", "*ssl*", "*python*", "*sqlite*"]), - ]: - for pattern in patterns: - for src in glob(f"{prefix_dir}/{rel_dir}/{pattern}"): - dst = temp_dir / relpath(src, prefix_dir.parent) - dst.parent.mkdir(parents=True, exist_ok=True) - if Path(src).is_dir(): - shutil.copytree( - src, dst, symlinks=True, - ignore=lambda *args: ["__pycache__"] - ) - else: - shutil.copy2(src, dst, follow_symlinks=False) - - dist_dir = subdir(context.host, "dist", create=True) - package_path = shutil.make_archive( - f"{dist_dir}/python-{version}-{context.host}", "gztar", temp_dir - ) - print(f"Wrote {package_path}") - - -def env(context): - print_env(android_env(getattr(context, "host", 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 install_signal_handler(): - def signal_handler(*args): - os.kill(os.getpid(), signal.SIGINT) - - signal.signal(signal.SIGTERM, signal_handler) - - -def parse_args(): - parser = argparse.ArgumentParser() - subcommands = parser.add_subparsers(dest="subcommand", required=True) - - # Subcommands - build = subcommands.add_parser( - "build", help="Run configure-build, make-build, configure-host and " - "make-host") - 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 Android") - make_host = subcommands.add_parser( - "make-host", help="Run `make` for Android") - - subcommands.add_parser("clean", help="Delete all build directories") - subcommands.add_parser("build-testbed", help="Build the testbed app") - test = subcommands.add_parser("test", help="Run the testbed app") - package = subcommands.add_parser("package", help="Make a release package") - env = subcommands.add_parser("env", help="Print environment variables") - - # Common arguments - for subcommand in build, configure_build, configure_host: - subcommand.add_argument( - "--clean", action="store_true", default=False, dest="clean", - help="Delete the relevant build directories first") - - host_commands = [build, configure_host, make_host, package] - if in_source_tree: - host_commands.append(env) - for subcommand in host_commands: - subcommand.add_argument( - "host", metavar="HOST", choices=HOSTS, - help="Host triplet: choices=[%(choices)s]") - - for subcommand in build, configure_build, configure_host: - subcommand.add_argument("args", nargs="*", - help="Extra arguments to pass to `configure`") - - # Test arguments - test.add_argument( - "-v", "--verbose", action="count", default=0, - help="Show Gradle output, and non-Python logcat messages. " - "Use twice to include high-volume messages which are rarely useful.") - - device_group = test.add_mutually_exclusive_group(required=True) - device_group.add_argument( - "--connected", metavar="SERIAL", help="Run on a connected device. " - "Connect it yourself, then get its serial from `adb devices`.") - device_group.add_argument( - "--managed", metavar="NAME", help="Run on a Gradle-managed device. " - "These are defined in `managedDevices` in testbed/app/build.gradle.kts.") - - test.add_argument( - "--site-packages", metavar="DIR", type=abspath, - help="Directory to copy as the app's site-packages.") - test.add_argument( - "--cwd", metavar="DIR", type=abspath, - help="Directory to copy as the app's working directory.") - - mode_group = test.add_mutually_exclusive_group() - mode_group.add_argument( - "-c", dest="command", help="Execute the given Python code.") - mode_group.add_argument( - "-m", dest="module", help="Execute the module with the given name.") - test.epilog = ( - "If neither -c nor -m are passed, the default is '-m test', which will " - "run Python's own test suite.") - test.add_argument( - "args", nargs="*", help=f"Arguments to add to sys.argv. " - f"Separate them from {SCRIPT_NAME}'s own arguments with `--`.") - - return parser.parse_args() - - -def main(): - install_signal_handler() - - # 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) - - context = parse_args() - dispatch = { - "configure-build": configure_build_python, - "make-build": make_build_python, - "configure-host": configure_host_python, - "make-host": make_host_python, - "build": build_all, - "clean": clean_all, - "build-testbed": build_testbed, - "test": run_testbed, - "package": package, - "env": env, - } - - try: - result = dispatch[context.subcommand](context) - if asyncio.iscoroutine(result): - asyncio.run(result) - except CalledProcessError as e: - print_called_process_error(e) - sys.exit(1) - - -def print_called_process_error(e): - for stream_name in ["stdout", "stderr"]: - content = getattr(e, stream_name) - stream = getattr(sys, stream_name) - if content: - stream.write(content) - if not content.endswith("\n"): - stream.write("\n") - - # shlex uses single quotes, so we surround the command with double quotes. - print( - f'Command "{join_command(e.cmd)}" returned exit status {e.returncode}' - ) - - -if __name__ == "__main__": - main() diff --git a/Android/testbed/.gitignore b/Android/testbed/.gitignore deleted file mode 100644 index 7c57aee..0000000 --- a/Android/testbed/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -# The Gradle wrapper can be downloaded by running the `test` or `build-testbed` -# commands of android.py. -/gradlew -/gradlew.bat -/gradle/wrapper/gradle-wrapper.jar - -# The repository's top-level .gitignore file ignores all .idea directories, but -# we want to keep any files which can't be regenerated from the Gradle -# configuration. -!.idea/ -/.idea/* -!/.idea/inspectionProfiles - -*.iml -.gradle -/local.properties -.DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties diff --git a/Android/testbed/.idea/inspectionProfiles/Project_Default.xml b/Android/testbed/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 220d9ed..0000000 --- a/Android/testbed/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/Android/testbed/app/.gitignore b/Android/testbed/app/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/Android/testbed/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/Android/testbed/app/build.gradle.kts b/Android/testbed/app/build.gradle.kts deleted file mode 100644 index 92cffd6..0000000 --- a/Android/testbed/app/build.gradle.kts +++ /dev/null @@ -1,268 +0,0 @@ -import com.android.build.api.variant.* -import kotlin.math.max - -plugins { - id("com.android.application") - id("org.jetbrains.kotlin.android") -} - -val ANDROID_DIR = file("../..") -val PYTHON_DIR = ANDROID_DIR.parentFile!! -val PYTHON_CROSS_DIR = file("$PYTHON_DIR/cross-build") -val inSourceTree = ( - ANDROID_DIR.name == "Android" && file("$PYTHON_DIR/pyconfig.h.in").exists() -) - -val KNOWN_ABIS = mapOf( - "aarch64-linux-android" to "arm64-v8a", - "x86_64-linux-android" to "x86_64", -) - -// Discover prefixes. -val prefixes = ArrayList() -if (inSourceTree) { - for ((triplet, _) in KNOWN_ABIS.entries) { - val prefix = file("$PYTHON_CROSS_DIR/$triplet/prefix") - if (prefix.exists()) { - prefixes.add(prefix) - } - } -} else { - // Testbed is inside a release package. - val prefix = file("$ANDROID_DIR/prefix") - if (prefix.exists()) { - prefixes.add(prefix) - } -} -if (prefixes.isEmpty()) { - throw GradleException( - "No Android prefixes found: see README.md for testing instructions" - ) -} - -// Detect Python versions and ABIs. -lateinit var pythonVersion: String -var abis = HashMap() -for ((i, prefix) in prefixes.withIndex()) { - val libDir = file("$prefix/lib") - val version = run { - for (filename in libDir.list()!!) { - """python(\d+\.\d+)""".toRegex().matchEntire(filename)?.let { - return@run it.groupValues[1] - } - } - throw GradleException("Failed to find Python version in $libDir") - } - if (i == 0) { - pythonVersion = version - } else if (pythonVersion != version) { - throw GradleException( - "${prefixes[0]} is Python $pythonVersion, but $prefix is Python $version" - ) - } - - val libPythonDir = file("$libDir/python$pythonVersion") - val triplet = run { - for (filename in libPythonDir.list()!!) { - """_sysconfigdata__android_(.+).py""".toRegex().matchEntire(filename)?.let { - return@run it.groupValues[1] - } - } - throw GradleException("Failed to find Python triplet in $libPythonDir") - } - abis[prefix] = KNOWN_ABIS[triplet]!! -} - - -android { - val androidEnvFile = file("../../android-env.sh").absoluteFile - - namespace = "org.python.testbed" - compileSdk = 34 - - defaultConfig { - applicationId = "org.python.testbed" - - minSdk = androidEnvFile.useLines { - for (line in it) { - """ANDROID_API_LEVEL:=(\d+)""".toRegex().find(line)?.let { - return@useLines it.groupValues[1].toInt() - } - } - throw GradleException("Failed to find API level in $androidEnvFile") - } - targetSdk = 34 - - versionCode = 1 - versionName = "1.0" - - ndk.abiFilters.addAll(abis.values) - externalNativeBuild.cmake.arguments( - "-DPYTHON_PREFIX_DIR=" + if (inSourceTree) { - // AGP uses the ${} syntax for its own purposes, so use a Jinja style - // placeholder. - "$PYTHON_CROSS_DIR/{{triplet}}/prefix" - } else { - prefixes[0] - }, - "-DPYTHON_VERSION=$pythonVersion", - "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", - ) - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - ndkVersion = androidEnvFile.useLines { - for (line in it) { - """ndk_version=(\S+)""".toRegex().find(line)?.let { - return@useLines it.groupValues[1] - } - } - throw GradleException("Failed to find NDK version in $androidEnvFile") - } - externalNativeBuild.cmake { - path("src/main/c/CMakeLists.txt") - } - - // Set this property to something non-empty, otherwise it'll use the default - // list, which ignores asset directories beginning with an underscore. - aaptOptions.ignoreAssetsPattern = ".git" - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" - } - - testOptions { - managedDevices { - localDevices { - create("minVersion") { - device = "Small Phone" - - // Managed devices have a minimum API level of 27. - apiLevel = max(27, defaultConfig.minSdk!!) - - // ATD devices are smaller and faster, but have a minimum - // API level of 30. - systemImageSource = if (apiLevel >= 30) "aosp-atd" else "aosp" - } - - create("maxVersion") { - device = "Small Phone" - apiLevel = defaultConfig.targetSdk!! - systemImageSource = "aosp-atd" - } - } - - // If the previous test run succeeded and nothing has changed, - // Gradle thinks there's no need to run it again. Override that. - afterEvaluate { - (localDevices.names + listOf("connected")).forEach { - tasks.named("${it}DebugAndroidTest") { - outputs.upToDateWhen { false } - } - } - } - } - } -} - -dependencies { - implementation("androidx.appcompat:appcompat:1.6.1") - implementation("com.google.android.material:material:1.11.0") - implementation("androidx.constraintlayout:constraintlayout:2.1.4") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test:rules:1.5.0") -} - - -// Create some custom tasks to copy Python and its standard library from -// elsewhere in the repository. -androidComponents.onVariants { variant -> - val pyPlusVer = "python$pythonVersion" - generateTask(variant, variant.sources.assets!!) { - into("python") { - // Include files such as pyconfig.h are used by some of the tests. - into("include/$pyPlusVer") { - for (prefix in prefixes) { - from("$prefix/include/$pyPlusVer") - } - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - } - - into("lib/$pyPlusVer") { - // To aid debugging, the source directory takes priority when - // running inside a CPython source tree. - if (inSourceTree) { - from("$PYTHON_DIR/Lib") - } - for (prefix in prefixes) { - from("$prefix/lib/$pyPlusVer") - } - - into("site-packages") { - from("$projectDir/src/main/python") - - val sitePackages = findProperty("python.sitePackages") as String? - if (!sitePackages.isNullOrEmpty()) { - if (!file(sitePackages).exists()) { - throw GradleException("$sitePackages does not exist") - } - from(sitePackages) - } - } - - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - exclude("**/__pycache__") - } - - into("cwd") { - val cwd = findProperty("python.cwd") as String? - if (!cwd.isNullOrEmpty()) { - if (!file(cwd).exists()) { - throw GradleException("$cwd does not exist") - } - from(cwd) - } - } - } - } - - generateTask(variant, variant.sources.jniLibs!!) { - for ((prefix, abi) in abis.entries) { - into(abi) { - from("$prefix/lib") - include("libpython*.*.so") - include("lib*_python.so") - } - } - } -} - - -fun generateTask( - variant: ApplicationVariant, directories: SourceDirectories, - configure: GenerateTask.() -> Unit -) { - val taskName = "generate" + - listOf(variant.name, "Python", directories.name) - .map { it.replaceFirstChar(Char::uppercase) } - .joinToString("") - - directories.addGeneratedSourceDirectory( - tasks.register(taskName) { - into(outputDir) - configure() - }, - GenerateTask::outputDir) -} - - -// addGeneratedSourceDirectory requires the task to have a DirectoryProperty. -abstract class GenerateTask: Sync() { - @get:OutputDirectory - abstract val outputDir: DirectoryProperty -} diff --git a/Android/testbed/app/src/androidTest/java/org/python/testbed/PythonSuite.kt b/Android/testbed/app/src/androidTest/java/org/python/testbed/PythonSuite.kt deleted file mode 100644 index 94be52d..0000000 --- a/Android/testbed/app/src/androidTest/java/org/python/testbed/PythonSuite.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.python.testbed - -import androidx.test.annotation.UiThreadTest -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - - -@RunWith(AndroidJUnit4::class) -class PythonSuite { - @Test - @UiThreadTest - fun testPython() { - val start = System.currentTimeMillis() - try { - val status = PythonTestRunner( - InstrumentationRegistry.getInstrumentation().targetContext - ).run( - InstrumentationRegistry.getArguments() - ) - assertEquals(0, status) - } finally { - // Make sure the process lives long enough for the test script to - // detect it (see `find_pid` in android.py). - val delay = 2000 - (System.currentTimeMillis() - start) - if (delay > 0) { - Thread.sleep(delay) - } - } - } -} diff --git a/Android/testbed/app/src/main/AndroidManifest.xml b/Android/testbed/app/src/main/AndroidManifest.xml deleted file mode 100644 index 2be8a82..0000000 --- a/Android/testbed/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Android/testbed/app/src/main/c/CMakeLists.txt b/Android/testbed/app/src/main/c/CMakeLists.txt deleted file mode 100644 index 6d5ccd9..0000000 --- a/Android/testbed/app/src/main/c/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -cmake_minimum_required(VERSION 3.4.1) -project(testbed) - -# Resolve variables from the command line. -string( - REPLACE {{triplet}} ${CMAKE_LIBRARY_ARCHITECTURE} - PYTHON_PREFIX_DIR ${PYTHON_PREFIX_DIR} -) - -include_directories(${PYTHON_PREFIX_DIR}/include/python${PYTHON_VERSION}) -link_directories(${PYTHON_PREFIX_DIR}/lib) -link_libraries(log python${PYTHON_VERSION}) - -add_library(main_activity SHARED main_activity.c) diff --git a/Android/testbed/app/src/main/c/main_activity.c b/Android/testbed/app/src/main/c/main_activity.c deleted file mode 100644 index ec7f93a..0000000 --- a/Android/testbed/app/src/main/c/main_activity.c +++ /dev/null @@ -1,152 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - - -static void throw_runtime_exception(JNIEnv *env, const char *message) { - (*env)->ThrowNew( - env, - (*env)->FindClass(env, "java/lang/RuntimeException"), - message); -} - - -// --- Stdio redirection ------------------------------------------------------ - -// Most apps won't need this, because the Python-level sys.stdout and sys.stderr -// are redirected to the Android logcat by Python itself. However, in the -// testbed it's useful to redirect the native streams as well, to debug problems -// in the Python startup or redirection process. -// -// Based on -// https://github.com/beeware/briefcase-android-gradle-template/blob/v0.3.11/%7B%7B%20cookiecutter.safe_formal_name%20%7D%7D/app/src/main/cpp/native-lib.cpp - -typedef struct { - FILE *file; - int fd; - android_LogPriority priority; - char *tag; - int pipe[2]; -} StreamInfo; - -// The FILE member can't be initialized here because stdout and stderr are not -// compile-time constants. Instead, it's initialized immediately before the -// redirection. -static StreamInfo STREAMS[] = { - {NULL, STDOUT_FILENO, ANDROID_LOG_INFO, "native.stdout", {-1, -1}}, - {NULL, STDERR_FILENO, ANDROID_LOG_WARN, "native.stderr", {-1, -1}}, - {NULL, -1, ANDROID_LOG_UNKNOWN, NULL, {-1, -1}}, -}; - -// The maximum length of a log message in bytes, including the level marker and -// tag, is defined as LOGGER_ENTRY_MAX_PAYLOAD in -// platform/system/logging/liblog/include/log/log.h. As of API level 30, messages -// longer than this will be be truncated by logcat. This limit has already been -// reduced at least once in the history of Android (from 4076 to 4068 between API -// level 23 and 26), so leave some headroom. -static const int MAX_BYTES_PER_WRITE = 4000; - -static void *redirection_thread(void *arg) { - StreamInfo *si = (StreamInfo*)arg; - ssize_t read_size; - char buf[MAX_BYTES_PER_WRITE]; - while ((read_size = read(si->pipe[0], buf, sizeof buf - 1)) > 0) { - buf[read_size] = '\0'; /* add null-terminator */ - __android_log_write(si->priority, si->tag, buf); - } - return 0; -} - -static char *redirect_stream(StreamInfo *si) { - /* make the FILE unbuffered, to ensure messages are never lost */ - if (setvbuf(si->file, 0, _IONBF, 0)) { - return "setvbuf"; - } - - /* create the pipe and redirect the file descriptor */ - if (pipe(si->pipe)) { - return "pipe"; - } - if (dup2(si->pipe[1], si->fd) == -1) { - return "dup2"; - } - - /* start the logging thread */ - pthread_t thr; - if ((errno = pthread_create(&thr, 0, redirection_thread, si))) { - return "pthread_create"; - } - if ((errno = pthread_detach(thr))) { - return "pthread_detach"; - } - return 0; -} - -JNIEXPORT void JNICALL Java_org_python_testbed_PythonTestRunner_redirectStdioToLogcat( - JNIEnv *env, jobject obj -) { - STREAMS[0].file = stdout; - STREAMS[1].file = stderr; - for (StreamInfo *si = STREAMS; si->file; si++) { - char *error_prefix; - if ((error_prefix = redirect_stream(si))) { - char error_message[1024]; - snprintf(error_message, sizeof(error_message), - "%s: %s", error_prefix, strerror(errno)); - throw_runtime_exception(env, error_message); - return; - } - } -} - - -// --- Python initialization --------------------------------------------------- - -static PyStatus set_config_string( - JNIEnv *env, PyConfig *config, wchar_t **config_str, jstring value -) { - const char *value_utf8 = (*env)->GetStringUTFChars(env, value, NULL); - PyStatus status = PyConfig_SetBytesString(config, config_str, value_utf8); - (*env)->ReleaseStringUTFChars(env, value, value_utf8); - return status; -} - -static void throw_status(JNIEnv *env, PyStatus status) { - throw_runtime_exception(env, status.err_msg ? status.err_msg : ""); -} - -JNIEXPORT int JNICALL Java_org_python_testbed_PythonTestRunner_runPython( - JNIEnv *env, jobject obj, jstring home, jstring runModule -) { - PyConfig config; - PyStatus status; - PyConfig_InitIsolatedConfig(&config); - - status = set_config_string(env, &config, &config.home, home); - if (PyStatus_Exception(status)) { - throw_status(env, status); - return 1; - } - - status = set_config_string(env, &config, &config.run_module, runModule); - if (PyStatus_Exception(status)) { - throw_status(env, status); - return 1; - } - - // Some tests generate SIGPIPE and SIGXFSZ, which should be ignored. - config.install_signal_handlers = 1; - - status = Py_InitializeFromConfig(&config); - if (PyStatus_Exception(status)) { - throw_status(env, status); - return 1; - } - - return Py_RunMain(); -} diff --git a/Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt b/Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt deleted file mode 100644 index ef28948..0000000 --- a/Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt +++ /dev/null @@ -1,92 +0,0 @@ -package org.python.testbed - -import android.content.Context -import android.os.* -import android.system.Os -import android.widget.TextView -import androidx.appcompat.app.* -import java.io.* - - -// Launching the tests from an activity is OK for a quick check, but for -// anything more complicated it'll be more convenient to use `android.py test` -// to launch the tests via PythonSuite. -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - val status = PythonTestRunner(this).run("-m", "test", "-W -uall") - findViewById(R.id.tvHello).text = "Exit status $status" - } -} - - -class PythonTestRunner(val context: Context) { - fun run(instrumentationArgs: Bundle) = run( - instrumentationArgs.getString("pythonMode")!!, - instrumentationArgs.getString("pythonModule")!!, - instrumentationArgs.getString("pythonArgs") ?: "", - ) - - /** Run Python. - * - * @param mode Either "-c" or "-m". - * @param module Python statements for "-c" mode, or a module name for - * "-m" mode. - * @param args Arguments to add to sys.argv. Will be parsed by `shlex.split`. - * @return The Python exit status: zero on success, nonzero on failure. */ - fun run(mode: String, module: String, args: String) : Int { - Os.setenv("PYTHON_MODE", mode, true) - Os.setenv("PYTHON_MODULE", module, true) - Os.setenv("PYTHON_ARGS", args, true) - - // Python needs this variable to help it find the temporary directory, - // but Android only sets it on API level 33 and later. - Os.setenv("TMPDIR", context.cacheDir.toString(), false) - - val pythonHome = extractAssets() - System.loadLibrary("main_activity") - redirectStdioToLogcat() - - // The main module is in src/main/python. We don't simply call it - // "main", as that could clash with third-party test code. - return runPython(pythonHome.toString(), "android_testbed_main") - } - - private fun extractAssets() : File { - val pythonHome = File(context.filesDir, "python") - if (pythonHome.exists() && !pythonHome.deleteRecursively()) { - throw RuntimeException("Failed to delete $pythonHome") - } - extractAssetDir("python", context.filesDir) - return pythonHome - } - - private fun extractAssetDir(path: String, targetDir: File) { - val names = context.assets.list(path) - ?: throw RuntimeException("Failed to list $path") - val targetSubdir = File(targetDir, path) - if (!targetSubdir.mkdirs()) { - throw RuntimeException("Failed to create $targetSubdir") - } - - for (name in names) { - val subPath = "$path/$name" - val input: InputStream - try { - input = context.assets.open(subPath) - } catch (e: FileNotFoundException) { - extractAssetDir(subPath, targetDir) - continue - } - input.use { - File(targetSubdir, name).outputStream().use { output -> - input.copyTo(output) - } - } - } - } - - private external fun redirectStdioToLogcat() - private external fun runPython(home: String, runModule: String) : Int -} diff --git a/Android/testbed/app/src/main/python/android_testbed_main.py b/Android/testbed/app/src/main/python/android_testbed_main.py deleted file mode 100644 index 31b8e53..0000000 --- a/Android/testbed/app/src/main/python/android_testbed_main.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -import runpy -import shlex -import signal -import sys - -# Some tests use SIGUSR1, but that's blocked by default in an Android app in -# order to make it available to `sigwait` in the Signal Catcher thread. -# (https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:art/runtime/signal_catcher.cc). -# That thread's functionality is only useful for debugging the JVM, so disabling -# it should not weaken the tests. -# -# There's no safe way of stopping the thread completely (#123982), but simply -# unblocking SIGUSR1 is enough to fix most tests. -# -# However, in tests that generate multiple different signals in quick -# succession, it's possible for SIGUSR1 to arrive while the main thread is busy -# running the C-level handler for a different signal. In that case, the SIGUSR1 -# may be sent to the Signal Catcher thread instead, which will generate a log -# message containing the text "reacting to signal". -# -# Such tests may need to be changed in one of the following ways: -# * Use a signal other than SIGUSR1 (e.g. test_stress_delivery_simultaneous in -# test_signal.py). -# * Send the signal to a specific thread rather than the whole process (e.g. -# test_signals in test_threadsignals.py. -signal.pthread_sigmask(signal.SIG_UNBLOCK, [signal.SIGUSR1]) - -mode = os.environ["PYTHON_MODE"] -module = os.environ["PYTHON_MODULE"] -sys.argv[1:] = shlex.split(os.environ["PYTHON_ARGS"]) - -cwd = f"{sys.prefix}/cwd" -if not os.path.exists(cwd): - # Empty directories are lost in the asset packing/unpacking process. - os.mkdir(cwd) -os.chdir(cwd) - -if mode == "-c": - # In -c mode, sys.path starts with an empty string, which means whatever the current - # working directory is at the moment of each import. - sys.path.insert(0, "") - exec(module, {}) -elif mode == "-m": - sys.path.insert(0, os.getcwd()) - runpy.run_module(module, run_name="__main__", alter_sys=True) -else: - raise ValueError(f"unknown mode: {mode}") diff --git a/Android/testbed/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/Android/testbed/app/src/main/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index 741d6580d60e05080e71e845240863f6b273333c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3110 zcmZ`)c{J1w+y4D#82cK@5~GZz$odeb8q64!?6MDGhU{swg(16WFly|>Sei*Z_NOdK zo1&1NsK?gWm90o#?>XOh&iBuEpL5;Ux$krTcU||nQZ27y`M4#x0RZ4LH9=b+G5NpX zgdO#7#xwsM5m&H@V<-S{uKgFF{+TDsM@#Wr7>8RnLHJu?9yh&#u&}UmzJY$Bo*u#8 z=Ynn$3f7Pk0Kn5=iZ-wfpI9x54i?!KfBRCuY^3GP3{S3hMWz+6`A$n1*RZ9PpMF4q zFLTw__EXm9K#exe9dont8^m%aqd*;snDa{G;Li~ye)#EAoKIWHN|rR0<3e@DBOobK_suVh(?P{3y7vaQk`5{_18_L~&KO+PyM3^Pi3oGBV;%|BsX% zcHOeX@)Ye%OyiAnrm54$hqo;r%Gn(Tj(+h%L>{v#csbv+H|)LJD|uByc!>!Xv)HDgM28MLaR0EYg%%3X z1#ar?p&M(*!~ZGiHwi8J(C573o3fPO1Cr@PnE>ODe;vadHcj=>#Xsm4Pp+9f?RbTT0FB2 zAM=1mBp`1YCA7MSs)m9zX)tBk$T8s0jj-f?s<107y}=N8gawwk)ed`Y51j9)-V3_B zL6MXoR$6`Dco4*20Md%tx(^{JfD-`W02>Jq1R(!+__2szGRkIt2z3=&bp%BE_fYI+ zDreaL=S0MW#H6qs?}QFMp7Av@gf^CLxy&}Hf72Zq$&NDwMeNI7<0Eu2&ZyCh-`z@! zp1sA9CuryQNnUdmY?DT#!?&XQQxfOgI817Sep4J@Xzl*;HbIj#no`}FmG@T%=|IMj z9mxzQPo{=a@3sKS^z~Sean$&RG-zVKor;t>nY4F;Rz6K1P;^lv8JowhT#+C;xqG<- zGZR6LW};PmmY63oLQ|bE>WTBD?9&4i?=Sy=bthnlo4YzYA+HF2wGt}B2U1YVdmo>~ z2#`aOYalukoGD0E;|D6XWDRb`1(5uYhsVc51x<@Rd1!NwdA@}JmP!2L%V*BL=MsE< zVfTd^K=BsIpH@LJ2CAS*AWeDV_L4nz&ny((Dv1YaQuKamHK2~c@ihSC9QVCB3B8xr zm{t!emAw-00BmLP00%FH;YXj$PER-&qNBuDyi*Uh3s{&v`FDnwfo8t~HDq@zzw9Lob6+lDYC-n17119tqo=mFGzY&$P?M)PoF+g6&Dj@gWn}Q{c%vMM@6{Bs@dy=C16%;&hjE%0zMI? zXjl)b<(^KEgg@A4qOlr#ZEs1M~&AN=r?#cF* zQ3)s*a-ovZlb$pV?(uQ3h$mR`=LZ%npTs13-|qc1W8gKUdA-ZCa$lM5+qT1hZ^e!% zs1-XygoVpARHqiTWm*)rpHfDe1}Oh9txJggUhIAoz0V~c8Wn+V#iaSnqJ^Ss1=r!W zdh@BF0iyL^wIv-u}6&2`~AysKWz6z1$NCH+jr_^jqm+M)=f@KR$zqv z;=I-CiPgXM-t1O&?UH(agYu%AhF1%=bMezf{(kU&XG6|z%r2i65_R=&)trR_#>;6) z)n)8?RMdrF=>^BA??zTQ4KFsF>|fXCTd0Uhw3SQ8-5O%KaRWE?EXNE_&0Q&)>#23) z9;SAMv(L!A4svha6`jrOeS#OHs$GiqA-*d#A%1S%o1M_U<@hWvQJVYw!K0Q4MCLrC zYtVFK$RWOg$-IO!)X6#UpiNHL%i*2fGKP;YTJx*p zwH@8PE`9fWOQ&8eO(O2=IN_;eKqhj<{Dk~x{E})oCXscud^dm ziuOL0r%(R${b$q;=Nneh%S5VN^+olGo{y%r0+bak77T^e#Nz^wTg0#URA3S{E-VS7 zT9t3o0+UP)-(DK5z9APgg&=A3l*DlrReQd9me%KB=X=r+FTJw8En|1t;mVg-Haa@D zky)TOFp(bU<>}PhiSJVM&{V(i3{=zu0fQ;l>4t03+N88)Ne&@&D=0`&$sIRtkv=d0 z*t|g>+pDCxy*EACx|0B>*`;WoR?pIEGyw`{QI@u5{h_a}B}LWw$pXT-o|8IL=G*40 zY$%8GT24LKZ^Jrn)=m4M+cwe5?fVPz%FcR9gDi%I zpyF(jg?L=E(gmDz<(QA1_bTxCw}7RZ^@9(`O0~y=ZL`bUuGU^Tej9@7m{y*T2-nUF zb{S0~WV~I2sh^~^5@4NdaJ*;gjWOLthP zfo~xBvafDP^J^2e57iYz)>Eo7uvgX72w_y4gQzNER^j!0=`RSBR9F%#{_o?+d+f*m zH%>yp8F4DtP71&TQEqG|h1+?e|6k@D1(1o)s`m92S-SbN7e0JAPr0#&)~`=DgK&Uy z`MIXOT~Tg@{=Y!H?}Gz9GN9-Vyt5c@8;t@50lndsuwLy4&FiCA!|;F?PJKu7K2&RP z;6Q%tNmb5CZjy!{+F~bMsPb?6zN>RYb4Fh@3>cS=E1yL2lQ%WdA+6_k8JaE=nl5lkwUxgm58Uf2>Pa5t9>>eNrW8Rx<>9vOUl9G)*6b69mmS-kEqQhy|q9HF?2>d3Xe@bT528 zc`+&Z?G=kikGb#isCRag779=soISf_`q08OPtDs;MqgOaV16p22VyaNz$GYFBB3x= z(=u?1g|`9#(p0g*K+npPpAS=k$_5(@Rd7sfBbvq&)j_9QPAh z!BPZ2&U_`2=X}@*F;!nXyI8s%Q|Gnqbuxe*C7qJjYwonj z;k7aAwWXo)dp)9kM|fp+E<3(4{$gmRc=~HYoYsTm0)}x6IJ4cK(;zsmuF{`6AnzJ@ wobE4xi)d3rtlRePkGCZCRb>UJ6+2oVa;Pv|-I|Ho!@yCQVy>cV4Bg}Z16Zh-Q2+n{ diff --git a/Android/testbed/app/src/main/res/layout/activity_main.xml b/Android/testbed/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 2139860..0000000 --- a/Android/testbed/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Android/testbed/app/src/main/res/values/strings.xml b/Android/testbed/app/src/main/res/values/strings.xml deleted file mode 100644 index 352d2f9..0000000 --- a/Android/testbed/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Python testbed - \ No newline at end of file diff --git a/Android/testbed/build.gradle.kts b/Android/testbed/build.gradle.kts deleted file mode 100644 index 451517b..0000000 --- a/Android/testbed/build.gradle.kts +++ /dev/null @@ -1,5 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -plugins { - id("com.android.application") version "8.10.0" apply false - id("org.jetbrains.kotlin.android") version "1.9.22" apply false -} diff --git a/Android/testbed/gradle.properties b/Android/testbed/gradle.properties deleted file mode 100644 index e9f345c..0000000 --- a/Android/testbed/gradle.properties +++ /dev/null @@ -1,28 +0,0 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true - -# By default, the app will be uninstalled after the tests finish (apparently -# after 10 seconds in case of an unclean shutdown). We disable this, because -# when using android.py it can conflict with the installation of the next run. -android.injected.androidTest.leaveApksInstalledAfterRun=true diff --git a/Android/testbed/gradle/wrapper/gradle-wrapper.properties b/Android/testbed/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 5d42fba..0000000 --- a/Android/testbed/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Feb 19 20:29:06 GMT 2024 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/Android/testbed/settings.gradle.kts b/Android/testbed/settings.gradle.kts deleted file mode 100644 index 5e08773..0000000 --- a/Android/testbed/settings.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -pluginManagement { - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} - -rootProject.name = "Python testbed" -include(":app") - \ No newline at end of file diff --git a/Doc/using/android.rst b/Doc/using/android.rst deleted file mode 100644 index cb3c962..0000000 --- a/Doc/using/android.rst +++ /dev/null @@ -1,74 +0,0 @@ -.. _using-android: - -======================= -Using Python on Android -======================= - -Python on Android 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 -Typthon script. - -On Android, 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 Android is in embedded mode – that -is, by writing a native Android application, embedding a Typthon interpreter -using ``libpython``, and invoking Typthon code using the :ref:`Python embedding -API `. The full Typthon interpreter, the standard library, and all -your Typthon code is then packaged into your app for its own private use. - -The Python standard library has some notable omissions and restrictions on -Android. See the :ref:`API availability guide ` for -details. - -Adding Python to an Android app -------------------------------- - -Most app developers should use one of the following tools, which will provide a -much easier experience: - -* `Briefcase `__, from the BeeWare project -* `Buildozer `__, from the Kivy project -* `Chaquopy `__ -* `pyqtdeploy `__ -* `Termux `__ - -If you're sure you want to do all of this manually, read on. You can use the -:source:`testbed app ` as a guide; each step below contains a -link to the relevant file. - -* Build Python by following the instructions in :source:`Android/README.md`. - This will create the directory ``cross-build/HOST/prefix``. - -* Add code to your :source:`build.gradle ` - file to copy the following items into your project. All except your own Python - code can be copied from ``prefix/lib``: - - * In your JNI libraries: - - * ``libpython*.*.so`` - * ``lib*_python.so`` (external libraries such as OpenSSL) - - * In your assets: - - * ``python*.*`` (the Python standard library) - * ``python*.*/site-packages`` (your own Typthon code) - -* Add code to your app to :source:`extract the assets to the filesystem - `. - -* Add code to your app to :source:`start Python in embedded mode - `. This will need to be C code - called via JNI. - -Building a Python package for Android -------------------------------------- - -Python packages can be built for Android as wheels and released on PyPI. The -recommended tool for doing this is `cibuildwheel -`__, which automates -all the details of setting up a cross-compilation environment, building the -wheel, and testing it on an emulator. diff --git a/Doc/using/index.rst b/Doc/using/index.rst index 90fdfc0..b4937f3 100644 --- a/Doc/using/index.rst +++ b/Doc/using/index.rst @@ -19,6 +19,4 @@ interpreter and things that make working with Python easier. configure.rst windows.rst mac.rst - android.rst - ios.rst editors.rst diff --git a/Doc/using/ios.rst b/Doc/using/ios.rst deleted file mode 100644 index 9e1e2b1..0000000 --- a/Doc/using/ios.rst +++ /dev/null @@ -1,393 +0,0 @@ -.. _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 -Typthon 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 Typthon interpreter -using ``libPython``, and invoking Typthon code using the :ref:`Python embedding -API `. The full Typthon interpreter, the standard library, and all -your Typthon 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 Typthon 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 minimum 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 Typthon 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:`iOS/README.rst` (in the CPython source distribution) for details on - how to build a Python ``XCFramework``. At a minimum, you will need a build - that supports ``arm64-apple-ios``, plus one of either - ``arm64-apple-ios-simulator`` or ``x86_64-apple-ios-simulator``. - -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. 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 - 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 - the target name in the sidebar that appears. - -6. 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: - - - 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 - -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: - - .. 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 - - Note that the name of the simulator "slice" in the XCframework may be - different, depending the CPU architectures your ``XCFramework`` supports. - -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 Typthon 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*; - * System logging (:c:member:`PyConfig.use_system_logger`) is *enabled* - (optional, but strongly recommended; this is enabled by default); - * :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 8, 9 and 10 of these instructions assume that you have a single folder of -pure Typthon application code, named ``app``. If you have third-party binary -modules in your app, some additional steps will be required: - -* You need to ensure that any folders containing third-party binaries are - either associated with the app target, or copied in as part of step 8. Step 8 - should also purge any binaries that are not appropriate for the platform a - specific build is targeting (i.e., delete any device binaries if you're - building an app targeting the simulator). - -* Any folders that contain third-party binaries must be processed into - framework form by step 9. The invocation of ``install_dylib`` that processes - the ``lib-dynload`` folder can be copied and adapted for this purpose. - -* If you're using a separate folder for third-party packages, ensure that folder - is included as part of the :envvar:`PYTHONPATH` configuration in step 10. - -* 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:`iOS/README.rst` -for details), create a clone of the Python iOS testbed project by running: - -.. code-block:: bash - - $ python iOS/testbed clone --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). - -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. - -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. - -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. diff --git a/Lib/_android_support.py b/Lib/_android_support.py deleted file mode 100644 index ae506f6..0000000 --- a/Lib/_android_support.py +++ /dev/null @@ -1,181 +0,0 @@ -import io -import sys -from threading import RLock -from time import sleep, time - -# The maximum length of a log message in bytes, including the level marker and -# tag, is defined as LOGGER_ENTRY_MAX_PAYLOAD at -# https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/logging/liblog/include/log/log.h;l=71. -# Messages longer than this will be truncated by logcat. This limit has already -# been reduced at least once in the history of Android (from 4076 to 4068 between -# API level 23 and 26), so leave some headroom. -MAX_BYTES_PER_WRITE = 4000 - -# UTF-8 uses a maximum of 4 bytes per character, so limiting text writes to this -# size ensures that we can always avoid exceeding MAX_BYTES_PER_WRITE. -# However, if the actual number of bytes per character is smaller than that, -# then we may still join multiple consecutive text writes into binary -# writes containing a larger number of characters. -MAX_CHARS_PER_WRITE = MAX_BYTES_PER_WRITE // 4 - - -# When embedded in an app on current versions of Android, there's no easy way to -# monitor the C-level stdout and stderr. The testbed comes with a .c file to -# redirect them to the system log using a pipe, but that wouldn't be convenient -# or appropriate for all apps. So we redirect at the Python level instead. -def init_streams(android_log_write, stdout_prio, stderr_prio): - if sys.executable: - return # Not embedded in an app. - - global logcat - logcat = Logcat(android_log_write) - - sys.stdout = TextLogStream( - stdout_prio, "python.stdout", sys.stdout.fileno()) - sys.stderr = TextLogStream( - stderr_prio, "python.stderr", sys.stderr.fileno()) - - -class TextLogStream(io.TextIOWrapper): - def __init__(self, prio, tag, fileno=None, **kwargs): - # The default is surrogateescape for stdout and backslashreplace for - # stderr, but in the context of an Android log, readability is more - # important than reversibility. - kwargs.setdefault("encoding", "UTF-8") - kwargs.setdefault("errors", "backslashreplace") - - super().__init__(BinaryLogStream(prio, tag, fileno), **kwargs) - self._lock = RLock() - self._pending_bytes = [] - self._pending_bytes_count = 0 - - 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 wherever possible, so split - # the string into lines first. Note that "".splitlines() == [], so - # nothing will be logged for an empty string. - with self._lock: - for line in s.splitlines(keepends=True): - while line: - chunk = line[:MAX_CHARS_PER_WRITE] - line = line[MAX_CHARS_PER_WRITE:] - self._write_chunk(chunk) - - return len(s) - - # The size and behavior of TextIOWrapper's buffer is not part of its public - # API, so we handle buffering ourselves to avoid truncation. - def _write_chunk(self, s): - b = s.encode(self.encoding, self.errors) - if self._pending_bytes_count + len(b) > MAX_BYTES_PER_WRITE: - self.flush() - - self._pending_bytes.append(b) - self._pending_bytes_count += len(b) - if ( - self.write_through - or b.endswith(b"\n") - or self._pending_bytes_count > MAX_BYTES_PER_WRITE - ): - self.flush() - - def flush(self): - with self._lock: - self.buffer.write(b"".join(self._pending_bytes)) - self._pending_bytes.clear() - self._pending_bytes_count = 0 - - # Since this is a line-based logging system, line buffering cannot be turned - # off, i.e. a newline always causes a flush. - @property - def line_buffering(self): - return True - - -class BinaryLogStream(io.RawIOBase): - def __init__(self, prio, tag, fileno=None): - self.prio = prio - self.tag = tag - self._fileno = fileno - - 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: - logcat.write(self.prio, self.tag, b) - return len(b) - - # This is needed by the test suite --timeout option, which uses faulthandler. - def fileno(self): - if self._fileno is None: - raise io.UnsupportedOperation("fileno") - return self._fileno - - -# When a large volume of data is written to logcat at once, e.g. when a test -# module fails in --verbose3 mode, there's a risk of overflowing logcat's own -# buffer and losing messages. We avoid this by imposing a rate limit using the -# token bucket algorithm, based on a conservative estimate of how fast `adb -# logcat` can consume data. -MAX_BYTES_PER_SECOND = 1024 * 1024 - -# The logcat buffer size of a device can be determined by running `logcat -g`. -# We set the token bucket size to half of the buffer size of our current minimum -# API level, because other things on the system will be producing messages as -# well. -BUCKET_SIZE = 128 * 1024 - -# https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/logging/liblog/include/log/log_read.h;l=39 -PER_MESSAGE_OVERHEAD = 28 - - -class Logcat: - def __init__(self, android_log_write): - self.android_log_write = android_log_write - self._lock = RLock() - self._bucket_level = 0 - self._prev_write_time = time() - - def write(self, prio, tag, message): - # Encode null bytes using "modified UTF-8" to avoid them truncating the - # message. - message = message.replace(b"\x00", b"\xc0\x80") - - with self._lock: - now = time() - self._bucket_level += ( - (now - self._prev_write_time) * MAX_BYTES_PER_SECOND) - - # If the bucket level is still below zero, the clock must have gone - # backwards, so reset it to zero and continue. - self._bucket_level = max(0, min(self._bucket_level, BUCKET_SIZE)) - self._prev_write_time = now - - self._bucket_level -= PER_MESSAGE_OVERHEAD + len(tag) + len(message) - if self._bucket_level < 0: - sleep(-self._bucket_level / MAX_BYTES_PER_SECOND) - - self.android_log_write(prio, tag, message) diff --git a/Lib/_ios_support.py b/Lib/_ios_support.py deleted file mode 100644 index 20467a7..0000000 --- a/Lib/_ios_support.py +++ /dev/null @@ -1,71 +0,0 @@ -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", file=sys.stderr) - 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 ImportError("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/test/test_android.py b/Lib/test/test_android.py deleted file mode 100644 index de83ce0..0000000 --- a/Lib/test/test_android.py +++ /dev/null @@ -1,457 +0,0 @@ -import io -import platform -import queue -import re -import subprocess -import sys -import unittest -from _android_support import TextLogStream -from array import array -from contextlib import ExitStack, contextmanager -from threading import Thread -from test.support import LOOPBACK_TIMEOUT -from time import time -from unittest.mock import patch - - -if sys.platform != "android": - raise unittest.SkipTest("Android-specific") - -api_level = platform.android_ver().api_level - -# (name, level, fileno) -STREAM_INFO = [("stdout", "I", 1), ("stderr", "W", 2)] - - -# Test redirection of stdout and stderr to the Android log. -class TestAndroidOutput(unittest.TestCase): - maxDiff = None - - def setUp(self): - self.logcat_process = subprocess.Popen( - ["logcat", "-v", "tag"], stdout=subprocess.PIPE, - errors="backslashreplace" - ) - self.logcat_queue = queue.Queue() - - def logcat_thread(): - for line in self.logcat_process.stdout: - self.logcat_queue.put(line.rstrip("\n")) - self.logcat_process.stdout.close() - - self.logcat_thread = Thread(target=logcat_thread) - self.logcat_thread.start() - - try: - from ctypes import CDLL, c_char_p, c_int - android_log_write = getattr(CDLL("liblog.so"), "__android_log_write") - android_log_write.argtypes = (c_int, c_char_p, c_char_p) - ANDROID_LOG_INFO = 4 - - # Separate tests using a marker line with a different tag. - tag, message = "python.test", f"{self.id()} {time()}" - android_log_write( - ANDROID_LOG_INFO, tag.encode("UTF-8"), message.encode("UTF-8")) - self.assert_log("I", tag, message, skip=True) - except: - # If setUp throws an exception, tearDown is not automatically - # called. Avoid leaving a dangling thread which would keep the - # Python process alive indefinitely. - self.tearDown() - raise - - def assert_logs(self, level, tag, expected, **kwargs): - for line in expected: - self.assert_log(level, tag, line, **kwargs) - - def assert_log(self, level, tag, expected, *, skip=False): - deadline = time() + LOOPBACK_TIMEOUT - while True: - try: - line = self.logcat_queue.get(timeout=(deadline - time())) - except queue.Empty: - raise self.failureException( - f"line not found: {expected!r}" - ) from None - if match := re.fullmatch(fr"(.)/{tag}: (.*)", line): - try: - self.assertEqual(level, match[1]) - self.assertEqual(expected, match[2]) - break - except AssertionError: - if not skip: - raise - - def tearDown(self): - self.logcat_process.terminate() - self.logcat_process.wait(LOOPBACK_TIMEOUT) - self.logcat_thread.join(LOOPBACK_TIMEOUT) - - # Avoid an irrelevant warning about threading._dangling. - self.logcat_thread = None - - @contextmanager - def unbuffered(self, stream): - stream.reconfigure(write_through=True) - try: - yield - finally: - stream.reconfigure(write_through=False) - - # In --verbose3 mode, sys.stdout and sys.stderr are captured, so we can't - # test them directly. Detect this mode and use some temporary streams with - # the same properties. - def stream_context(self, stream_name, level): - # https://developer.android.com/ndk/reference/group/logging - prio = {"I": 4, "W": 5}[level] - - stack = ExitStack() - stack.enter_context(self.subTest(stream_name)) - stream = getattr(sys, stream_name) - native_stream = getattr(sys, f"__{stream_name}__") - if isinstance(stream, io.StringIO): - stack.enter_context( - patch( - f"sys.{stream_name}", - TextLogStream( - prio, f"python.{stream_name}", native_stream.fileno(), - errors="backslashreplace" - ), - ) - ) - return stack - - def test_str(self): - for stream_name, level, fileno in STREAM_INFO: - with self.stream_context(stream_name, level): - stream = getattr(sys, stream_name) - tag = f"python.{stream_name}" - self.assertEqual(f"", repr(stream)) - - self.assertIs(stream.writable(), True) - self.assertIs(stream.readable(), False) - self.assertEqual(stream.fileno(), fileno) - self.assertEqual("UTF-8", stream.encoding) - self.assertEqual("backslashreplace", stream.errors) - self.assertIs(stream.line_buffering, True) - self.assertIs(stream.write_through, False) - - def write(s, lines=None, *, write_len=None): - if write_len is None: - write_len = len(s) - self.assertEqual(write_len, stream.write(s)) - if lines is None: - lines = [s] - self.assert_logs(level, tag, lines) - - # Single-line messages, - with self.unbuffered(stream): - write("", []) - - write("a") - write("Hello") - write("Hello world") - write(" ") - write(" ") - - # Non-ASCII text - write("ol\u00e9") # Spanish - write("\u4e2d\u6587") # Chinese - - # Non-BMP emoji - write("\U0001f600") - - # Non-encodable surrogates - write("\ud800\udc00", [r"\ud800\udc00"]) - - # Code used by surrogateescape (which isn't enabled here) - write("\udc80", [r"\udc80"]) - - # Null characters are logged using "modified UTF-8". - write("\u0000", [r"\xc0\x80"]) - write("a\u0000", [r"a\xc0\x80"]) - write("\u0000b", [r"\xc0\x80b"]) - write("a\u0000b", [r"a\xc0\x80b"]) - - # Multi-line messages. Avoid identical consecutive lines, as - # they may activate "chatty" filtering and break the tests. - write("\nx", [""]) - write("\na\n", ["x", "a"]) - write("\n", [""]) - write("b\n", ["b"]) - write("c\n\n", ["c", ""]) - write("d\ne", ["d"]) - write("xx", []) - write("f\n\ng", ["exxf", ""]) - write("\n", ["g"]) - - # Since this is a line-based logging system, line buffering - # cannot be turned off, i.e. a newline always causes a flush. - stream.reconfigure(line_buffering=False) - self.assertIs(stream.line_buffering, True) - - # However, buffering can be turned off completely if you want a - # flush after every write. - with self.unbuffered(stream): - write("\nx", ["", "x"]) - write("\na\n", ["", "a"]) - write("\n", [""]) - write("b\n", ["b"]) - write("c\n\n", ["c", ""]) - write("d\ne", ["d", "e"]) - write("xx", ["xx"]) - write("f\n\ng", ["f", "", "g"]) - write("\n", [""]) - - # "\r\n" should be translated into "\n". - write("hello\r\n", ["hello"]) - write("hello\r\nworld\r\n", ["hello", "world"]) - write("\r\n", [""]) - - # Non-standard line separators should be preserved. - write("before form feed\x0cafter form feed\n", - ["before form feed\x0cafter form feed"]) - write("before line separator\u2028after line separator\n", - ["before line separator\u2028after line separator"]) - - # 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() - - write(CustomStr("custom\n"), ["custom"], write_len=7) - - # 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__}" - ): - stream.write(obj) - - # Manual flushing is supported. - write("hello", []) - stream.flush() - self.assert_log(level, tag, "hello") - write("hello", []) - write("world", []) - stream.flush() - self.assert_log(level, tag, "helloworld") - - # Long lines are split into blocks of 1000 characters - # (MAX_CHARS_PER_WRITE in _android_support.py), but - # TextIOWrapper should then join them back together as much as - # possible without exceeding 4000 UTF-8 bytes - # (MAX_BYTES_PER_WRITE). - # - # ASCII (1 byte per character) - write(("foobar" * 700) + "\n", # 4200 bytes in - [("foobar" * 666) + "foob", # 4000 bytes out - "ar" + ("foobar" * 33)]) # 200 bytes out - - # "Full-width" digits 0-9 (3 bytes per character) - s = "\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19" - write((s * 150) + "\n", # 4500 bytes in - [s * 100, # 3000 bytes out - s * 50]) # 1500 bytes out - - s = "0123456789" - write(s * 200, []) # 2000 bytes in - write(s * 150, []) # 1500 bytes in - write(s * 51, [s * 350]) # 510 bytes in, 3500 bytes out - write("\n", [s * 51]) # 0 bytes in, 510 bytes out - - def test_bytes(self): - for stream_name, level, fileno in STREAM_INFO: - with self.stream_context(stream_name, level): - stream = getattr(sys, stream_name).buffer - tag = f"python.{stream_name}" - self.assertEqual(f"", repr(stream)) - self.assertIs(stream.writable(), True) - self.assertIs(stream.readable(), False) - self.assertEqual(stream.fileno(), fileno) - - def write(b, lines=None, *, write_len=None): - if write_len is None: - write_len = len(b) - self.assertEqual(write_len, stream.write(b)) - if lines is None: - lines = [b.decode()] - self.assert_logs(level, tag, lines) - - # Single-line messages, - write(b"", []) - - write(b"a") - write(b"Hello") - write(b"Hello world") - write(b" ") - write(b" ") - - # Non-ASCII text - write(b"ol\xc3\xa9") # Spanish - write(b"\xe4\xb8\xad\xe6\x96\x87") # Chinese - - # Non-BMP emoji - write(b"\xf0\x9f\x98\x80") - - # Null bytes are logged using "modified UTF-8". - write(b"\x00", [r"\xc0\x80"]) - write(b"a\x00", [r"a\xc0\x80"]) - write(b"\x00b", [r"\xc0\x80b"]) - write(b"a\x00b", [r"a\xc0\x80b"]) - - # Invalid UTF-8 - write(b"\xff", [r"\xff"]) - write(b"a\xff", [r"a\xff"]) - write(b"\xffb", [r"\xffb"]) - write(b"a\xffb", [r"a\xffb"]) - - # Log entries containing newlines are shown differently by - # `logcat -v tag`, `logcat -v long`, and Android Studio. We - # currently use `logcat -v tag`, which shows each line as if it - # was a separate log entry, but strips a single trailing - # newline. - # - # On newer versions of Android, all three of the above tools (or - # maybe Logcat itself) will also strip any number of leading - # newlines. - write(b"\nx", ["", "x"] if api_level < 30 else ["x"]) - write(b"\na\n", ["", "a"] if api_level < 30 else ["a"]) - write(b"\n", [""]) - write(b"b\n", ["b"]) - write(b"c\n\n", ["c", ""]) - write(b"d\ne", ["d", "e"]) - write(b"xx", ["xx"]) - write(b"f\n\ng", ["f", "", "g"]) - write(b"\n", [""]) - - # "\r\n" should be translated into "\n". - write(b"hello\r\n", ["hello"]) - write(b"hello\r\nworld\r\n", ["hello", "world"]) - write(b"\r\n", [""]) - - # Other bytes-like objects are accepted. - write(bytearray(b"bytearray")) - - mv = memoryview(b"memoryview") - write(mv, ["memoryview"]) # Continuous - write(mv[::2], ["mmrve"]) # Discontinuous - - write( - # Android only supports little-endian architectures, so the - # bytes representation is as follows: - array("H", [ - 0, # 00 00 - 1, # 01 00 - 65534, # FE FF - 65535, # FF FF - ]), - - # After encoding null bytes with modified UTF-8, the only - # valid UTF-8 sequence is \x01. All other bytes are handled - # by backslashreplace. - ["\\xc0\\x80\\xc0\\x80" - "\x01\\xc0\\x80" - "\\xfe\\xff" - "\\xff\\xff"], - write_len=8, - ) - - # Non-bytes-like classes are not accepted. - 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__}" - ): - stream.write(obj) - - -class TestAndroidRateLimit(unittest.TestCase): - def test_rate_limit(self): - # https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/logging/liblog/include/log/log_read.h;l=39 - PER_MESSAGE_OVERHEAD = 28 - - # https://developer.android.com/ndk/reference/group/logging - ANDROID_LOG_DEBUG = 3 - - # To avoid flooding the test script output, use a different tag rather - # than stdout or stderr. - tag = "python.rate_limit" - stream = TextLogStream(ANDROID_LOG_DEBUG, tag) - - # Make a test message which consumes 1 KB of the logcat buffer. - message = "Line {:03d} " - message += "." * ( - 1024 - PER_MESSAGE_OVERHEAD - len(tag) - len(message.format(0)) - ) + "\n" - - # To avoid depending on the performance of the test device, we mock the - # passage of time. - mock_now = time() - - def mock_time(): - # Avoid division by zero by simulating a small delay. - mock_sleep(0.0001) - return mock_now - - def mock_sleep(duration): - nonlocal mock_now - mock_now += duration - - # See _android_support.py. The default values of these parameters work - # well across a wide range of devices, but we'll use smaller values to - # ensure a quick and reliable test that doesn't flood the log too much. - MAX_KB_PER_SECOND = 100 - BUCKET_KB = 10 - with ( - patch("_android_support.MAX_BYTES_PER_SECOND", MAX_KB_PER_SECOND * 1024), - patch("_android_support.BUCKET_SIZE", BUCKET_KB * 1024), - patch("_android_support.sleep", mock_sleep), - patch("_android_support.time", mock_time), - ): - # Make sure the token bucket is full. - stream.write("Initial message to reset _prev_write_time") - mock_sleep(BUCKET_KB / MAX_KB_PER_SECOND) - line_num = 0 - - # Write BUCKET_KB messages, and return the rate at which they were - # accepted in KB per second. - def write_bucketful(): - nonlocal line_num - start = mock_time() - max_line_num = line_num + BUCKET_KB - while line_num < max_line_num: - stream.write(message.format(line_num)) - line_num += 1 - return BUCKET_KB / (mock_time() - start) - - # The first bucketful should be written with minimal delay. The - # factor of 2 here is not arbitrary: it verifies that the system can - # write fast enough to empty the bucket within two bucketfuls, which - # the next part of the test depends on. - self.assertGreater(write_bucketful(), MAX_KB_PER_SECOND * 2) - - # Write another bucketful to empty the token bucket completely. - write_bucketful() - - # The next bucketful should be written at the rate limit. - self.assertAlmostEqual( - write_bucketful(), MAX_KB_PER_SECOND, - delta=MAX_KB_PER_SECOND * 0.1 - ) - - # Once the token bucket refills, we should go back to full speed. - mock_sleep(BUCKET_KB / MAX_KB_PER_SECOND) - self.assertGreater(write_bucketful(), MAX_KB_PER_SECOND * 2) diff --git a/iOS/README.rst b/iOS/README.rst deleted file mode 100644 index 37a7870..0000000 --- a/iOS/README.rst +++ /dev/null @@ -1,373 +0,0 @@ -Typthon on iOS README - -:Authors: - Russell Keith-Magee (2023-11) - -This document provides a quick overview of some iOS specific features in the -Typthon distribution. - -These instructions are only needed if you're planning to compile Typthon for iOS -yourself. Most users should *not* need to do this. If you're looking to -experiment with writing an iOS app in Typthon, tools such as `BeeWare's Briefcase -`__ and `Kivy's Buildozer -`__ 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. - -iOS specific arguments to configure -=================================== - -* ``--enable-framework[=DIR]`` - - This argument specifies the location where the Typthon.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 Typthon.framework; defaults to ``Python``. - - .. admonition:: Use this option with care! - - Unless you know what you're doing, changing the name of the Python - framework on iOS is not advised. If you use this option, you won't be able - to run the ``make testios`` target without making significant manual - alterations, and you won't be able to use any binary packages unless you - compile them yourself using your own framework name. - -Building Typthon 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 single-architecture framework ----------------------------------------- - -The Python build system will create a ``Typthon.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 ``Typthon.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 -``Typthon.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 -Typthon for the ARM64 iOS simulator will look something like:: - - $ export PATH="$(pwd)/iOS/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" - $ ./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: - -* ``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 ``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 (XZ, BZip2, LibFFI and OpenSSL). -This can be done by defining the ``LIBLZMA_CFLAGS``, ``LIBLZMA_LIBS``, -``BZIP2_CFLAGS``, ``BZIP2_LIBS``, ``LIBFFI_CFLAGS``, and ``LIBFFI_LIBS`` -environment variables, and the ``--with-openssl`` configure option. Versions of -these libraries pre-compiled for iOS can be found in `this repository -`__. LibFFI is -especially important, as many parts of the standard library (including the -``platform``, ``sysconfig`` and ``webbrowser`` modules) require the use of the -``ctypes`` module at runtime. - -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. - -Merge thin frameworks into fat frameworks ------------------------------------------ - -Once you've built a ``Typthon.framework`` for each ABI and architecture, you -must produce a "fat" framework for each ABI that contains all the architectures -for that ABI. - -The ``iphoneos`` build only needs to support a single architecture, so it can be -used without modification. - -If you only want to support a single simulator architecture, (e.g., only support -ARM64 simulators), you can use a single architecture ``Typthon.framework`` build. -However, if you want to create ``Python.xcframework`` that supports *all* -architectures, you'll need to merge the ``iphonesimulator`` builds for ARM64 and -x86_64 into a single "fat" framework. - -The "fat" framework can be constructed by performing a directory merge of the -content of the two "thin" ``Typthon.framework`` directories, plus the ``bin`` and -``lib`` folders for each thin framework. When performing this merge: - -* The pure Python standard library content is identical for each architecture, - except for a handful of platform-specific files (such as the ``sysconfig`` - module). Ensure that the "fat" framework has the union of all standard library - files. - -* Any binary files in the standard library, plus the main - ``libPython3.X.dylib``, can be merged using the ``lipo`` tool, provide by - Xcode:: - - $ lipo -create -output module.dylib path/to/x86_64/module.dylib path/to/arm64/module.dylib - -* The header files will be identical on both architectures, except for - ``pyconfig.h``. Copy all the headers from one platform (say, arm64), rename - ``pyconfig.h`` to ``pyconfig-arm64.h``, and copy the ``pyconfig.h`` for the - other architecture into the merged header folder as ``pyconfig-x86_64.h``. - Then copy the ``iOS/Resources/pyconfig.h`` file from the CPython sources into - the merged headers folder. This will allow the two Python architectures to - share a common ``pyconfig.h`` header file. - -At this point, you should have 2 Typthon.framework folders - one for ``iphoneos``, -and one for ``iphonesimulator`` that is a merge of x86+64 and ARM64 content. - -Merge frameworks into an XCframework ------------------------------------- - -Now that we have 2 (potentially fat) ABI-specific frameworks, we can merge those -frameworks into a single ``XCframework``. - -The initial skeleton of an ``XCframework`` is built using:: - - xcodebuild -create-xcframework -output Python.xcframework -framework path/to/iphoneos/Typthon.framework -framework path/to/iphonesimulator/Typthon.framework - -Then, copy the ``bin`` and ``lib`` folders into the architecture-specific slices of -the XCframework:: - - cp path/to/iphoneos/bin Python.xcframework/ios-arm64 - cp path/to/iphoneos/lib Python.xcframework/ios-arm64 - - cp path/to/iphonesimulator/bin Python.xcframework/ios-arm64_x86_64-simulator - cp path/to/iphonesimulator/lib Python.xcframework/ios-arm64_x86_64-simulator - -Note that the name of the architecture-specific slice for the simulator will -depend on the CPU architecture(s) that you build. - -You now have a Python.xcframework that can be used in a project. - -Testing Python on iOS -===================== - -The ``iOS/testbed`` folder that contains an Xcode project that is able to run -the iOS test suite. This project converts the Python test suite into a single -test case in Xcode's XCTest framework. The single XCTest passes if the test -suite passes. - -To run the test suite, configure a Python build for an iOS simulator (i.e., -``--host=arm64-apple-ios-simulator`` or ``--host=x86_64-apple-ios-simulator`` -), specifying a framework build (i.e. ``--enable-framework``). Ensure that your -``PATH`` has been configured to include the ``iOS/Resources/bin`` folder and -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 "iPhone SE (3rd generation)" simulator. - -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 ``make test`` generates a standalone version of the ``iOS/testbed`` -project, and runs the full test suite. It does this using ``iOS/testbed`` -itself - the folder is an executable module that can be used to create and run -a clone of the testbed project. - -You can generate your own standalone testbed instance by running:: - - $ python iOS/testbed clone --framework iOS/Frameworks/arm64-iphonesimulator my-testbed - -This invocation assumes that ``iOS/Frameworks/arm64-iphonesimulator`` is the -path to the iOS simulator framework for your platform (ARM64 in this case); -``my-testbed`` is the name of the folder for the new testbed clone. - -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. - -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. - -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. - -Running specific tests -^^^^^^^^^^^^^^^^^^^^^^ - -As the test suite is being executed on an iOS simulator, it is not possible to -pass in command line arguments to configure test suite operation. To work -around this limitation, the arguments that would normally be passed as command -line arguments are configured as part of the ``iOSTestbed-Info.plist`` file -that is used to configure the iOS testbed app. In this file, the ``TestArgs`` -key is an array containing the arguments that would be passed to ``python -m`` -on the command line (including ``test`` in position 0, the name of the test -module to be executed). - -Disabling automated breakpoints -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -By default, Xcode will inserts an automatic breakpoint whenever a signal is -raised. The Python test suite raises many of these signals as part of normal -operation; unless you are trying to diagnose an issue with signals, the -automatic breakpoints can be inconvenient. However, they can be disabled by -creating a symbolic breakpoint that is triggered at the start of the test run. - -Select "Debug > Breakpoints > Create Symbolic Breakpoint" from the Xcode menu, and -populate the new brewpoint with the following details: - -* **Name**: IgnoreSignals -* **Symbol**: UIApplicationMain -* **Action**: Add debugger commands for: - - ``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`` -* Check the "Automatically continue after evaluating" box. - -All other details can be left blank. When the process executes the -``UIApplicationMain`` entry point, the breakpoint will trigger, run the debugger -commands to disable the automatic breakpoints, and automatically resume. diff --git a/iOS/Resources/Info.plist.in b/iOS/Resources/Info.plist.in deleted file mode 100644 index c3e261e..0000000 --- a/iOS/Resources/Info.plist.in +++ /dev/null @@ -1,34 +0,0 @@ - - - - - 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 - - iPhoneOS - - MinimumOSVersion - @IPHONEOS_DEPLOYMENT_TARGET@ - - diff --git a/iOS/Resources/bin/arm64-apple-ios-ar b/iOS/Resources/bin/arm64-apple-ios-ar deleted file mode 100644 index 3cf3eb2..0000000 --- a/iOS/Resources/bin/arm64-apple-ios-ar +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@" diff --git a/iOS/Resources/bin/arm64-apple-ios-clang b/iOS/Resources/bin/arm64-apple-ios-clang deleted file mode 100644 index f50d5b5..0000000 --- a/iOS/Resources/bin/arm64-apple-ios-clang +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" diff --git a/iOS/Resources/bin/arm64-apple-ios-clang++ b/iOS/Resources/bin/arm64-apple-ios-clang++ deleted file mode 100644 index 0794731..0000000 --- a/iOS/Resources/bin/arm64-apple-ios-clang++ +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphoneos${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@" diff --git a/iOS/Resources/bin/arm64-apple-ios-cpp b/iOS/Resources/bin/arm64-apple-ios-cpp deleted file mode 100644 index 24fa150..0000000 --- a/iOS/Resources/bin/arm64-apple-ios-cpp +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} -E "$@" diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-ar b/iOS/Resources/bin/arm64-apple-ios-simulator-ar deleted file mode 100644 index b836b6d..0000000 --- a/iOS/Resources/bin/arm64-apple-ios-simulator-ar +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-clang b/iOS/Resources/bin/arm64-apple-ios-simulator-clang deleted file mode 100644 index 4891a00..0000000 --- a/iOS/Resources/bin/arm64-apple-ios-simulator-clang +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ b/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ deleted file mode 100644 index 58b2a5f..0000000 --- a/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-cpp b/iOS/Resources/bin/arm64-apple-ios-simulator-cpp deleted file mode 100644 index c9df94e..0000000 --- a/iOS/Resources/bin/arm64-apple-ios-simulator-cpp +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" diff --git a/iOS/Resources/bin/arm64-apple-ios-simulator-strip b/iOS/Resources/bin/arm64-apple-ios-simulator-strip deleted file mode 100644 index fd59d30..0000000 --- a/iOS/Resources/bin/arm64-apple-ios-simulator-strip +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch arm64 "$@" diff --git a/iOS/Resources/bin/arm64-apple-ios-strip b/iOS/Resources/bin/arm64-apple-ios-strip deleted file mode 100644 index 75e823a..0000000 --- a/iOS/Resources/bin/arm64-apple-ios-strip +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphoneos${IOS_SDK_VERSION} strip -arch arm64 "$@" diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-ar b/iOS/Resources/bin/x86_64-apple-ios-simulator-ar deleted file mode 100644 index b836b6d..0000000 --- a/iOS/Resources/bin/x86_64-apple-ios-simulator-ar +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@" diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-clang b/iOS/Resources/bin/x86_64-apple-ios-simulator-clang deleted file mode 100644 index f4739a7..0000000 --- a/iOS/Resources/bin/x86_64-apple-ios-simulator-clang +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ b/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ deleted file mode 100644 index c348ae4..0000000 --- a/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@" diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp b/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp deleted file mode 100644 index 6d7f808..0000000 --- a/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@" diff --git a/iOS/Resources/bin/x86_64-apple-ios-simulator-strip b/iOS/Resources/bin/x86_64-apple-ios-simulator-strip deleted file mode 100644 index c5cfb28..0000000 --- a/iOS/Resources/bin/x86_64-apple-ios-simulator-strip +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch x86_64 "$@" diff --git a/iOS/Resources/dylib-Info-template.plist b/iOS/Resources/dylib-Info-template.plist deleted file mode 100644 index f652e27..0000000 --- a/iOS/Resources/dylib-Info-template.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - - CFBundleIdentifier - - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSupportedPlatforms - - iPhoneOS - - MinimumOSVersion - 12.0 - CFBundleVersion - 1 - - diff --git a/iOS/Resources/pyconfig.h b/iOS/Resources/pyconfig.h deleted file mode 100644 index 4acff2c..0000000 --- a/iOS/Resources/pyconfig.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifdef __arm64__ -#include "pyconfig-arm64.h" -#endif - -#ifdef __x86_64__ -#include "pyconfig-x86_64.h" -#endif diff --git a/iOS/testbed/Python.xcframework/Info.plist b/iOS/testbed/Python.xcframework/Info.plist deleted file mode 100644 index c6418de..0000000 --- a/iOS/testbed/Python.xcframework/Info.plist +++ /dev/null @@ -1,44 +0,0 @@ - - - - - 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 - - - CFBundlePackageType - XFWK - XCFrameworkFormatVersion - 1.0 - - diff --git a/iOS/testbed/Python.xcframework/ios-arm64/README b/iOS/testbed/Python.xcframework/ios-arm64/README deleted file mode 100644 index c1b076d..0000000 --- a/iOS/testbed/Python.xcframework/ios-arm64/README +++ /dev/null @@ -1,4 +0,0 @@ -This directory is intentionally empty. - -It should be used as a target for `--enable-framework` when compiling an iOS on-device -build for testing purposes. diff --git a/iOS/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README b/iOS/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README deleted file mode 100644 index ae334e5..0000000 --- a/iOS/testbed/Python.xcframework/ios-arm64_x86_64-simulator/README +++ /dev/null @@ -1,4 +0,0 @@ -This directory is intentionally empty. - -It should be used as a target for `--enable-framework` when compiling an iOS simulator -build for testing purposes (either x86_64 or ARM64). diff --git a/iOS/testbed/__main__.py b/iOS/testbed/__main__.py deleted file mode 100644 index 1146bf3..0000000 --- a/iOS/testbed/__main__.py +++ /dev/null @@ -1,548 +0,0 @@ -import argparse -import asyncio -import fcntl -import json -import os -import plistlib -import re -import shutil -import subprocess -import sys -import tempfile -from contextlib import asynccontextmanager -from datetime import datetime -from pathlib import Path - - -DECODE_ARGS = ("UTF-8", "backslashreplace") - -# The system log prefixes each line: -# 2025-01-17 16:14:29.090 Df iOSTestbed[23987:1fd393b4] (Python) ... -# 2025-01-17 16:14:29.090 E iOSTestbed[23987:1fd393b4] (Python) ... - -LOG_PREFIX_REGEX = re.compile( - r"^\d{4}-\d{2}-\d{2}" # YYYY-MM-DD - r"\s+\d+:\d{2}:\d{2}\.\d+" # HH:MM:SS.sss - r"\s+\w+" # Df/E - r"\s+iOSTestbed\[\d+:\w+\]" # Process/thread ID - r"\s+\(Python\)\s" # Logger name -) - - -# Work around a bug involving sys.exit and TaskGroups -# (https://github.com/python/cpython/issues/101515). -def exit(*args): - raise MySystemExit(*args) - - -class MySystemExit(Exception): - pass - - -class SimulatorLock: - # An fcntl-based filesystem lock that can be used to ensure that - def __init__(self, timeout): - self.filename = Path(tempfile.gettempdir()) / "python-ios-testbed" - self.timeout = timeout - - self.fd = None - - async def acquire(self): - # Ensure the lockfile exists - self.filename.touch(exist_ok=True) - - # Try `timeout` times to acquire the lock file, with a 1 second pause - # between each attempt. Report status every 10 seconds. - for i in range(0, self.timeout): - try: - fd = os.open(self.filename, os.O_RDWR | os.O_TRUNC, 0o644) - fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except OSError: - os.close(fd) - if i % 10 == 0: - print("... waiting", flush=True) - await asyncio.sleep(1) - else: - self.fd = fd - return - - # If we reach the end of the loop, we've exceeded the allowed number of - # attempts. - raise ValueError("Unable to obtain lock on iOS simulator creation") - - def release(self): - # If a lock is held, release it. - if self.fd is not None: - # Release the lock. - fcntl.flock(self.fd, fcntl.LOCK_UN) - os.close(self.fd) - self.fd = None - - -# All subprocesses are executed through this context manager so that no matter -# what happens, they can always be cancelled from another task, and they will -# always be cleaned up on exit. -@asynccontextmanager -async def async_process(*args, **kwargs): - process = await asyncio.create_subprocess_exec(*args, **kwargs) - try: - yield process - finally: - if process.returncode is None: - # Allow a reasonably long time for Xcode to clean itself up, - # because we don't want stale emulators left behind. - timeout = 10 - process.terminate() - try: - await asyncio.wait_for(process.wait(), timeout) - except TimeoutError: - print( - f"Command {args} did not terminate after {timeout} seconds " - f" - sending SIGKILL" - ) - process.kill() - - # Even after killing the process we must still wait for it, - # otherwise we'll get the warning "Exception ignored in __del__". - await asyncio.wait_for(process.wait(), timeout=1) - - -async def async_check_output(*args, **kwargs): - async with async_process( - *args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs - ) as process: - stdout, stderr = await process.communicate() - if process.returncode == 0: - return stdout.decode(*DECODE_ARGS) - else: - raise subprocess.CalledProcessError( - process.returncode, - args, - stdout.decode(*DECODE_ARGS), - stderr.decode(*DECODE_ARGS), - ) - - -# Select a simulator device to use. -async def select_simulator_device(): - # List the testing simulators, in JSON format - raw_json = await async_check_output( - "xcrun", "simctl", "list", "-j" - ) - json_data = json.loads(raw_json) - - # Any device will do; we'll look for "SE" devices - but the name isn't - # consistent over time. Older Xcode versions will use "iPhone SE (Nth - # generation)"; As of 2025, they've started using "iPhone 16e". - # - # When Xcode is updated after a new release, new devices will be available - # and old ones will be dropped from the set available on the latest iOS - # version. Select the one with the highest minimum runtime version - this - # is an indicator of the "newest" released device, which should always be - # supported on the "most recent" iOS version. - se_simulators = sorted( - (devicetype["minRuntimeVersion"], devicetype["name"]) - for devicetype in json_data["devicetypes"] - if devicetype["productFamily"] == "iPhone" - and ( - ("iPhone " in devicetype["name"] and devicetype["name"].endswith("e")) - or "iPhone SE " in devicetype["name"] - ) - ) - - return se_simulators[-1][1] - - -# Return a list of UDIDs associated with booted simulators -async def list_devices(): - try: - # List the testing simulators, in JSON format - raw_json = await async_check_output( - "xcrun", "simctl", "--set", "testing", "list", "-j" - ) - json_data = json.loads(raw_json) - - # Filter out the booted iOS simulators - return [ - simulator["udid"] - for runtime, simulators in json_data["devices"].items() - for simulator in simulators - if runtime.split(".")[-1].startswith("iOS") and simulator["state"] == "Booted" - ] - except subprocess.CalledProcessError as e: - # If there's no ~/Library/Developer/XCTestDevices folder (which is the - # case on fresh installs, and in some CI environments), `simctl list` - # returns error code 1, rather than an empty list. Handle that case, - # but raise all other errors. - if e.returncode == 1: - return [] - else: - raise - - -async def find_device(initial_devices, lock): - while True: - new_devices = set(await list_devices()).difference(initial_devices) - if len(new_devices) == 0: - await asyncio.sleep(1) - elif len(new_devices) == 1: - udid = new_devices.pop() - print(f"{datetime.now():%Y-%m-%d %H:%M:%S}: New test simulator detected") - print(f"UDID: {udid}", flush=True) - lock.release() - return udid - else: - exit(f"Found more than one new device: {new_devices}") - - -async def log_stream_task(initial_devices, lock): - # Wait up to 5 minutes for the build to complete and the simulator to boot. - udid = await asyncio.wait_for(find_device(initial_devices, lock), 5 * 60) - - # Stream the iOS device's logs, filtering out messages that come from the - # XCTest test suite (catching NSLog messages from the test method), or - # Python itself (catching stdout/stderr content routed to the system log - # with config->use_system_logger). - args = [ - "xcrun", - "simctl", - "--set", - "testing", - "spawn", - udid, - "log", - "stream", - "--style", - "compact", - "--predicate", - ( - 'senderImagePath ENDSWITH "/iOSTestbedTests.xctest/iOSTestbedTests"' - ' OR senderImagePath ENDSWITH "/Python.framework/Python"' - ), - ] - - async with async_process( - *args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) as process: - suppress_dupes = False - while line := (await process.stdout.readline()).decode(*DECODE_ARGS): - # Strip the prefix from each log line - line = LOG_PREFIX_REGEX.sub("", line) - # The iOS log streamer can sometimes lag; when it does, it outputs - # a warning about messages being dropped... often multiple times. - # Only print the first of these duplicated warnings. - if line.startswith("=== Messages dropped "): - if not suppress_dupes: - suppress_dupes = True - sys.stdout.write(line) - else: - suppress_dupes = False - sys.stdout.write(line) - sys.stdout.flush() - - -async def xcode_test(location, simulator, verbose): - # Run the test suite on the named simulator - print("Starting xcodebuild...", flush=True) - args = [ - "xcodebuild", - "test", - "-project", - str(location / "iOSTestbed.xcodeproj"), - "-scheme", - "iOSTestbed", - "-destination", - f"platform=iOS Simulator,name={simulator}", - "-resultBundlePath", - str(location / f"{datetime.now():%Y%m%d-%H%M%S}.xcresult"), - "-derivedDataPath", - str(location / "DerivedData"), - ] - if not verbose: - args += ["-quiet"] - - async with async_process( - *args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) as process: - while line := (await process.stdout.readline()).decode(*DECODE_ARGS): - sys.stdout.write(line) - sys.stdout.flush() - - status = await asyncio.wait_for(process.wait(), timeout=1) - exit(status) - - -def clone_testbed( - source: Path, - target: Path, - framework: Path, - 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/ios-arm64_x86_64-simulator/bin" - ).is_dir(): - print( - f"The testbed being cloned ({source}) does not contain " - f"a simulator framework. 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="", flush=True) - shutil.copytree(source, target, symlinks=True) - print(" done") - - xc_framework_path = target / "Python.xcframework" - sim_framework_path = xc_framework_path / "ios-arm64_x86_64-simulator" - if framework is not None: - if framework.suffix == ".xcframework": - print(" Installing XCFramework...", end="", flush=True) - if xc_framework_path.is_dir(): - shutil.rmtree(xc_framework_path) - else: - xc_framework_path.unlink(missing_ok=True) - xc_framework_path.symlink_to( - framework.relative_to(xc_framework_path.parent, walk_up=True) - ) - print(" done") - else: - print(" Installing simulator framework...", end="", flush=True) - if sim_framework_path.is_dir(): - shutil.rmtree(sim_framework_path) - else: - sim_framework_path.unlink(missing_ok=True) - sim_framework_path.symlink_to( - framework.relative_to(sim_framework_path.parent, walk_up=True) - ) - print(" done") - else: - 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="", flush=True) - orig_xc_framework_path = ( - source - / xc_framework_path.readlink() - ).resolve() - xc_framework_path.unlink() - xc_framework_path.symlink_to( - orig_xc_framework_path.relative_to( - xc_framework_path.parent, walk_up=True - ) - ) - print(" done") - elif ( - sim_framework_path.is_symlink() - and not sim_framework_path.readlink().is_absolute() - ): - print(" Rewriting symlink to simulator framework...", end="", flush=True) - # Simulator framework is a relative symlink. Rewrite the symlink - # relative to the new location. - orig_sim_framework_path = ( - source - / "Python.XCframework" - / sim_framework_path.readlink() - ).resolve() - sim_framework_path.unlink() - sim_framework_path.symlink_to( - orig_sim_framework_path.relative_to( - sim_framework_path.parent, walk_up=True - ) - ) - print(" done") - else: - print(" Using pre-existing iOS framework.") - - for app_src in apps: - print(f" Installing app {app_src.name!r}...", end="", flush=True) - app_target = target / f"iOSTestbed/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_plist(testbed_path, args): - # Add the test runner arguments to the testbed's Info.plist file. - info_plist = testbed_path / "iOSTestbed" / "iOSTestbed-Info.plist" - with info_plist.open("rb") as f: - info = plistlib.load(f) - - info["TestArgs"] = args - - with info_plist.open("wb") as f: - plistlib.dump(info, f) - - -async def run_testbed(simulator: str | None, args: list[str], verbose: bool=False): - location = Path(__file__).parent - print("Updating plist...", end="", flush=True) - update_plist(location, args) - print(" done.", flush=True) - - if simulator is None: - simulator = await select_simulator_device() - print(f"Running test on {simulator}", flush=True) - - # We need to get an exclusive lock on simulator creation, to avoid issues - # with multiple simulators starting and being unable to tell which - # simulator is due to which testbed instance. See - # https://github.com/python/cpython/issues/130294 for details. Wait up to - # 10 minutes for a simulator to boot. - print("Obtaining lock on simulator creation...", flush=True) - simulator_lock = SimulatorLock(timeout=10*60) - await simulator_lock.acquire() - print("Simulator lock acquired.", flush=True) - - # Get the list of devices that are booted at the start of the test run. - # The simulator started by the test suite will be detected as the new - # entry that appears on the device list. - initial_devices = await list_devices() - - try: - async with asyncio.TaskGroup() as tg: - tg.create_task(log_stream_task(initial_devices, simulator_lock)) - tg.create_task(xcode_test(location, simulator=simulator, verbose=verbose)) - except* MySystemExit as e: - raise SystemExit(*e.exceptions[0].args) from None - except* subprocess.CalledProcessError as e: - # Extract it from the ExceptionGroup so it can be handled by `main`. - raise e.exceptions[0] - finally: - simulator_lock.release() - - -def main(): - parser = argparse.ArgumentParser( - description=( - "Manages the process of testing a Python project in the iOS simulator." - ), - ) - - subcommands = parser.add_subparsers(dest="subcommand") - - clone = subcommands.add_parser( - "clone", - description=( - "Clone the testbed project, copying in an iOS 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( - "--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 iOS process as if they were arguments to " - "`python -m`." - ), - help="Run a testbed project", - ) - 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." - ) - ) - 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, - apps=[Path(app) for app in context.apps], - ) - elif context.subcommand == "run": - if test_args: - if not ( - Path(__file__).parent / "Python.xcframework/ios-arm64_x86_64-simulator/bin" - ).is_dir(): - print( - f"Testbed does not contain a compiled iOS framework. Use " - f"`python {sys.argv[0]} clone ...` to create a runnable " - f"clone of this testbed." - ) - sys.exit(20) - - asyncio.run( - run_testbed( - simulator=context.simulator, - verbose=context.verbose, - args=test_args, - ) - ) - else: - print(f"Must specify test arguments (e.g., {sys.argv[0]} run -- test)") - print() - parser.print_help(sys.stderr) - sys.exit(21) - else: - parser.print_help(sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj b/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj deleted file mode 100644 index c7d6390..0000000 --- a/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj +++ /dev/null @@ -1,580 +0,0 @@ -// !$*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 /* iOSTestbedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66312B0EFA3A0010BFC8 /* iOSTestbedTests.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, ); }; }; - 607A66582B0F079F0010BFC8 /* dylib-Info-template.plist in Resources */ = {isa = PBXBuildFile; fileRef = 607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */; }; - 608619542CB77BA900F46182 /* app_packages in Resources */ = {isa = PBXBuildFile; fileRef = 608619532CB77BA900F46182 /* app_packages */; }; - 608619562CB7819B00F46182 /* app in Resources */ = {isa = PBXBuildFile; fileRef = 608619552CB7819B00F46182 /* app */; }; -/* End PBXBuildFile section */ - -/* 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 /* iOSTestbedTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iOSTestbedTests.m; sourceTree = ""; }; - 607A664A2B0EFB310010BFC8 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = ""; }; - 607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "dylib-Info-template.plist"; sourceTree = ""; }; - 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = ""; }; - 608619532CB77BA900F46182 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = ""; }; - 608619552CB7819B00F46182 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = ""; }; -/* 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 = ( - 607A664A2B0EFB310010BFC8 /* Python.xcframework */, - 607A66142B0EFA380010BFC8 /* iOSTestbed */, - 607A66302B0EFA3A0010BFC8 /* iOSTestbedTests */, - 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 */, - 607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */, - 607A66152B0EFA380010BFC8 /* AppDelegate.h */, - 607A66162B0EFA380010BFC8 /* AppDelegate.m */, - 607A66212B0EFA390010BFC8 /* Assets.xcassets */, - 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */, - 607A66272B0EFA390010BFC8 /* main.m */, - ); - path = iOSTestbed; - sourceTree = ""; - }; - 607A66302B0EFA3A0010BFC8 /* iOSTestbedTests */ = { - isa = PBXGroup; - children = ( - 607A66312B0EFA3A0010BFC8 /* iOSTestbedTests.m */, - ); - path = iOSTestbedTests; - 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 /* Install Target Specific Python Standard Library */, - 607A66562B0F06200010BFC8 /* Prepare Python Binary Modules */, - 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 */, - 607A66582B0F079F0010BFC8 /* dylib-Info-template.plist 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 /* Install Target Specific Python Standard Library */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Install Target Specific Python Standard Library"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "set -e\n\nmkdir -p \"$CODESIGNING_FOLDER_PATH/python/lib\"\nif [ \"$EFFECTIVE_PLATFORM_NAME\" = \"-iphonesimulator\" ]; then\n echo \"Installing Python modules for iOS Simulator\"\n rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64_x86_64-simulator/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\" \nelse\n echo \"Installing Python modules for iOS Device\"\n rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\" \nfi\n"; - showEnvVarsInLog = 0; - }; - 607A66562B0F06200010BFC8 /* Prepare Python Binary Modules */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Prepare Python Binary Modules"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "set -e\n\ninstall_dylib () {\n INSTALL_BASE=$1\n FULL_EXT=$2\n\n # The name of the extension file\n EXT=$(basename \"$FULL_EXT\")\n # The location of the extension file, relative to the bundle\n RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} \n # The path to the extension file, relative to the install base\n PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/}\n # The full dotted name of the extension module, constructed from the file path.\n FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d \".\" -f 1 | tr \"/\" \".\"); \n # A bundle identifier; not actually used, but required by Xcode framework packaging\n FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr \"_\" \"-\")\n # The name of the framework folder.\n FRAMEWORK_FOLDER=\"Frameworks/$FULL_MODULE_NAME.framework\"\n\n # If the framework folder doesn't exist, create it.\n if [ ! -d \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\" ]; then\n echo \"Creating framework for $RELATIVE_EXT\" \n mkdir -p \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\"\n cp \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n plutil -replace CFBundleExecutable -string \"$FULL_MODULE_NAME\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n plutil -replace CFBundleIdentifier -string \"$FRAMEWORK_BUNDLE_ID\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n fi\n \n echo \"Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" \n mv \"$FULL_EXT\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\"\n # Create a placeholder .fwork file where the .so was\n echo \"$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" > ${FULL_EXT%.so}.fwork\n # Create a back reference to the .so file location in the framework\n echo \"${RELATIVE_EXT%.so}.fwork\" > \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin\" \n}\n\nPYTHON_VER=$(ls -1 \"$CODESIGNING_FOLDER_PATH/python/lib\")\necho \"Install Python $PYTHON_VER standard library extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/python/lib/$PYTHON_VER/lib-dynload\" -name \"*.so\" | while read FULL_EXT; do\n install_dylib python/lib/$PYTHON_VER/lib-dynload/ \"$FULL_EXT\"\ndone\necho \"Install app package extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/app_packages\" -name \"*.so\" | while read FULL_EXT; do\n install_dylib app_packages/ \"$FULL_EXT\"\ndone\necho \"Install app extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/app\" -name \"*.so\" | while read FULL_EXT; do\n install_dylib app/ \"$FULL_EXT\"\ndone\n\n# Clean up dylib template \nrm -f \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\"\necho \"Signing frameworks as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)...\"\nfind \"$CODESIGNING_FOLDER_PATH/Frameworks\" -name \"*.framework\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der \"{}\" \\; \n"; - 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 /* iOSTestbedTests.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 = 12.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 = 12.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 = 12.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 = 12.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 = 12.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 = 12.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOSTestbed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSTestbed"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 607A660D2B0EFA380010BFC8 /* Build configuration list for PBXProject "iOSTestbed" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 607A663F2B0EFA3A0010BFC8 /* Debug */, - 607A66402B0EFA3A0010BFC8 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 607A66412B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbed" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 607A66422B0EFA3A0010BFC8 /* Debug */, - 607A66432B0EFA3A0010BFC8 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 607A66442B0EFA3A0010BFC8 /* Build configuration list for PBXNativeTarget "iOSTestbedTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 607A66452B0EFA3A0010BFC8 /* Debug */, - 607A66462B0EFA3A0010BFC8 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 607A660A2B0EFA380010BFC8 /* Project object */; -} diff --git a/iOS/testbed/iOSTestbed/AppDelegate.h b/iOS/testbed/iOSTestbed/AppDelegate.h deleted file mode 100644 index f695b3b..0000000 --- a/iOS/testbed/iOSTestbed/AppDelegate.h +++ /dev/null @@ -1,11 +0,0 @@ -// -// AppDelegate.h -// iOSTestbed -// - -#import - -@interface AppDelegate : UIResponder - - -@end diff --git a/iOS/testbed/iOSTestbed/AppDelegate.m b/iOS/testbed/iOSTestbed/AppDelegate.m deleted file mode 100644 index e508539..0000000 --- a/iOS/testbed/iOSTestbed/AppDelegate.m +++ /dev/null @@ -1,19 +0,0 @@ -// -// AppDelegate.m -// iOSTestbed -// - -#import "AppDelegate.h" - -@interface AppDelegate () - -@end - -@implementation AppDelegate - - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - return YES; -} - -@end diff --git a/iOS/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json b/iOS/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/iOS/testbed/iOSTestbed/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iOS/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json b/iOS/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 13613e3..0000000 --- a/iOS/testbed/iOSTestbed/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iOS/testbed/iOSTestbed/Assets.xcassets/Contents.json b/iOS/testbed/iOSTestbed/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/iOS/testbed/iOSTestbed/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iOS/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard b/iOS/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 5daafe7..0000000 --- a/iOS/testbed/iOSTestbed/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/iOS/testbed/iOSTestbed/app/README b/iOS/testbed/iOSTestbed/app/README deleted file mode 100644 index af22c68..0000000 --- a/iOS/testbed/iOSTestbed/app/README +++ /dev/null @@ -1,7 +0,0 @@ -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. diff --git a/iOS/testbed/iOSTestbed/app_packages/README b/iOS/testbed/iOSTestbed/app_packages/README deleted file mode 100644 index 42d7fde..0000000 --- a/iOS/testbed/iOSTestbed/app_packages/README +++ /dev/null @@ -1,7 +0,0 @@ -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. diff --git a/iOS/testbed/iOSTestbed/dylib-Info-template.plist b/iOS/testbed/iOSTestbed/dylib-Info-template.plist deleted file mode 100644 index f652e27..0000000 --- a/iOS/testbed/iOSTestbed/dylib-Info-template.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - - CFBundleIdentifier - - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSupportedPlatforms - - iPhoneOS - - MinimumOSVersion - 12.0 - CFBundleVersion - 1 - - diff --git a/iOS/testbed/iOSTestbed/iOSTestbed-Info.plist b/iOS/testbed/iOSTestbed/iOSTestbed-Info.plist deleted file mode 100644 index a582f42..0000000 --- a/iOS/testbed/iOSTestbed/iOSTestbed-Info.plist +++ /dev/null @@ -1,64 +0,0 @@ - - - - - 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 - - TestArgs - - test - -uall - --single-process - --rerun - -W - - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - - - diff --git a/iOS/testbed/iOSTestbed/main.m b/iOS/testbed/iOSTestbed/main.m deleted file mode 100644 index e32bd78..0000000 --- a/iOS/testbed/iOSTestbed/main.m +++ /dev/null @@ -1,16 +0,0 @@ -// -// main.m -// iOSTestbed -// - -#import -#import "AppDelegate.h" - -int main(int argc, char * argv[]) { - NSString * appDelegateClassName; - @autoreleasepool { - appDelegateClassName = NSStringFromClass([AppDelegate class]); - - return UIApplicationMain(argc, argv, nil, appDelegateClassName); - } -} diff --git a/iOS/testbed/iOSTestbedTests/iOSTestbedTests.m b/iOS/testbed/iOSTestbedTests/iOSTestbedTests.m deleted file mode 100644 index b502a6e..0000000 --- a/iOS/testbed/iOSTestbedTests/iOSTestbedTests.m +++ /dev/null @@ -1,193 +0,0 @@ -#import -#import - -@interface iOSTestbedTests : XCTestCase - -@end - -@implementation iOSTestbedTests - - -- (void)testPython { - const char **argv; - int exit_code; - int failed; - PyStatus status; - PyPreConfig preconfig; - PyConfig config; - PyObject *app_packages_path; - PyObject *method_args; - PyObject *result; - PyObject *site_module; - PyObject *site_addsitedir_attr; - PyObject *sys_module; - PyObject *sys_path_attr; - NSArray *test_args; - NSString *python_home; - NSString *path; - wchar_t *wtmp_str; - - NSString *resourcePath = [[NSBundle mainBundle] resourcePath]; - - // Set some other common environment indicators to disable color, as the - // Xcode log can't display color. Stdout will report that it is *not* a - // TTY. - setenv("NO_COLOR", "1", true); - setenv("PYTHON_COLORS", "0", true); - - // Arguments to pass into the test suite runner. - // argv[0] must identify the process; any subsequent arg - // will be handled as if it were an argument to `python -m test` - test_args = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"TestArgs"]; - if (test_args == NULL) { - NSLog(@"Unable to identify test arguments."); - } - argv = malloc(sizeof(char *) * ([test_args count] + 1)); - argv[0] = "iOSTestbed"; - for (int i = 1; i < [test_args count]; i++) { - argv[i] = [[test_args objectAtIndex:i] UTF8String]; - } - NSLog(@"Test command: %@", test_args); - - // Generate an isolated Python configuration. - NSLog(@"Configuring isolated Python..."); - PyPreConfig_InitIsolatedConfig(&preconfig); - PyConfig_InitIsolatedConfig(&config); - - // Configure the Python interpreter: - // Enforce UTF-8 encoding for stderr, stdout, file-system encoding and locale. - // See https://docs.python.org/3/library/os.html#python-utf-8-mode. - preconfig.utf8_mode = 1; - // Use the system logger for stdout/err - config.use_system_logger = 1; - // Don't buffer stdio. We want output to appears in the log immediately - config.buffered_stdio = 0; - // Don't write bytecode; we can't modify the app bundle - // after it has been signed. - config.write_bytecode = 0; - // Ensure that signal handlers are installed - config.install_signal_handlers = 1; - // Run the test module. - config.run_module = Py_DecodeLocale([[test_args objectAtIndex:0] 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], (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