mirror of
https://github.com/johndoe6345789/typthon.git
synced 2026-04-24 13:45:05 +00:00
Remove Android and iOS directories and documentation
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
@@ -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 <https://developer.android.com/studio>.
|
||||
* 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.
|
||||
@@ -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
|
||||
@@ -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()
|
||||
22
Android/testbed/.gitignore
vendored
22
Android/testbed/.gitignore
vendored
@@ -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
|
||||
@@ -1,8 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="AndroidLintGradleDependency" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
|
||||
<inspection_tool class="AndroidLintOldTargetApi" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
|
||||
<inspection_tool class="UnstableApiUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true" editorAttributes="INFO_ATTRIBUTES" />
|
||||
</profile>
|
||||
</component>
|
||||
1
Android/testbed/app/.gitignore
vendored
1
Android/testbed/app/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -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<File>()
|
||||
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<File, String>()
|
||||
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<GenerateTask>(taskName) {
|
||||
into(outputDir)
|
||||
configure()
|
||||
},
|
||||
GenerateTask::outputDir)
|
||||
}
|
||||
|
||||
|
||||
// addGeneratedSourceDirectory requires the task to have a DirectoryProperty.
|
||||
abstract class GenerateTask: Sync() {
|
||||
@get:OutputDirectory
|
||||
abstract val outputDir: DirectoryProperty
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Material3.Light.NoActionBar">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -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)
|
||||
@@ -1,152 +0,0 @@
|
||||
#include <android/log.h>
|
||||
#include <errno.h>
|
||||
#include <jni.h>
|
||||
#include <pthread.h>
|
||||
#include <Python.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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<TextView>(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
|
||||
}
|
||||
@@ -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}")
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvHello"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">Python testbed</string>
|
||||
</resources>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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 <embedding>`. 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 <mobile-availability>` 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 <https://briefcase.readthedocs.io>`__, from the BeeWare project
|
||||
* `Buildozer <https://buildozer.readthedocs.io>`__, from the Kivy project
|
||||
* `Chaquopy <https://chaquo.com/chaquopy>`__
|
||||
* `pyqtdeploy <https://www.riverbankcomputing.com/static/Docs/pyqtdeploy/>`__
|
||||
* `Termux <https://termux.dev/en/>`__
|
||||
|
||||
If you're sure you want to do all of this manually, read on. You can use the
|
||||
:source:`testbed app <Android/testbed>` 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 <Android/testbed/app/build.gradle.kts>`
|
||||
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
|
||||
<Android/testbed/app/src/main/java/org/python/testbed/MainActivity.kt>`.
|
||||
|
||||
* Add code to your app to :source:`start Python in embedded mode
|
||||
<Android/testbed/app/src/main/c/main_activity.c>`. 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
|
||||
<https://cibuildwheel.pypa.io/en/stable/platforms/#android>`__, which automates
|
||||
all the details of setting up a cross-compilation environment, building the
|
||||
wheel, and testing it on an emulator.
|
||||
@@ -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
|
||||
|
||||
@@ -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 <embedding>`. 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 <https://beeware.org>`__ and `Kivy
|
||||
<https://kivy.org>`__ 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 <mobile-availability>` 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 <adding-ios>`.
|
||||
|
||||
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 <https://github.com/pvieito/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 <iOS/testbed>` 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 <path/to/Python.xcframework> --app <path/to/module1> --app <path/to/module2> 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 <Mac/Resources/app-store-compliance.patch>` 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.
|
||||
@@ -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"<TextLogStream {self.buffer.tag!r}>"
|
||||
|
||||
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"<BinaryLogStream {self.tag!r}>"
|
||||
|
||||
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)
|
||||
@@ -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
|
||||
@@ -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"<TextLogStream '{tag}'>", 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"<BinaryLogStream '{tag}'>", 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)
|
||||
373
iOS/README.rst
373
iOS/README.rst
@@ -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
|
||||
<https://briefcase.readthedocs.io>`__ and `Kivy's Buildozer
|
||||
<https://buildozer.readthedocs.io>`__ will provide a much more approachable
|
||||
user experience.
|
||||
|
||||
Compilers for building on iOS
|
||||
=============================
|
||||
|
||||
Building for iOS requires the use of Apple's Xcode tooling. It is strongly
|
||||
recommended that you use the most recent stable release of Xcode. This will
|
||||
require the use of the most (or second-most) recently released macOS version,
|
||||
as Apple does not maintain Xcode for older macOS versions. The Xcode Command
|
||||
Line Tools are not sufficient for iOS development; you need a *full* Xcode
|
||||
install.
|
||||
|
||||
If you want to run your code on the iOS simulator, you'll also need to install
|
||||
an iOS Simulator Platform. You should be prompted to select an iOS Simulator
|
||||
Platform when you first run Xcode. Alternatively, you can add an iOS Simulator
|
||||
Platform by selecting an open the Platforms tab of the Xcode Settings panel.
|
||||
|
||||
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
|
||||
<https://github.com/beeware/cpython-apple-source-deps/releases>`__. LibFFI is
|
||||
especially important, as many parts of the standard library (including the
|
||||
``platform``, ``sysconfig`` and ``webbrowser`` modules) require the use of the
|
||||
``ctypes`` module at runtime.
|
||||
|
||||
By default, Python will be compiled with an iOS deployment target (i.e., the
|
||||
minimum supported iOS version) of 13.0. To specify a different deployment
|
||||
target, provide the version number as part of the ``--host`` argument - for
|
||||
example, ``--host=arm64-apple-ios15.4-simulator`` would compile an ARM64
|
||||
simulator build with a deployment target of 15.4.
|
||||
|
||||
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.
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
|
||||
<plist version="0.9">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Python</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>Python Runtime and Library</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>@PYTHONFRAMEWORKIDENTIFIER@</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Python</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>@VERSION@</string>
|
||||
<key>CFBundleLongVersionString</key>
|
||||
<string>%VERSION%, (c) 2001-2024 Python Software Foundation.</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>@IPHONEOS_DEPLOYMENT_TARGET@</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphoneos${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} -E "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch arm64 "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphoneos${IOS_SDK_VERSION} strip -arch arm64 "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch x86_64 "$@"
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string></string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,7 +0,0 @@
|
||||
#ifdef __arm64__
|
||||
#include "pyconfig-arm64.h"
|
||||
#endif
|
||||
|
||||
#ifdef __x86_64__
|
||||
#include "pyconfig-x86_64.h"
|
||||
#endif
|
||||
@@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AvailableLibraries</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>Python.framework/Python</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>Python.framework</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>Python.framework/Python</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64_x86_64-simulator</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>Python.framework</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
<string>x86_64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
<key>SupportedPlatformVariant</key>
|
||||
<string>simulator</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XFWK</string>
|
||||
<key>XCFrameworkFormatVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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] -- <test arg> [<test arg> ...]",
|
||||
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()
|
||||
@@ -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 = "<group>"; };
|
||||
607A66162B0EFA380010BFC8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
||||
607A66212B0EFA390010BFC8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
607A66242B0EFA390010BFC8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
607A66272B0EFA390010BFC8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
607A664A2B0EFB310010BFC8 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = "<group>"; };
|
||||
607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "dylib-Info-template.plist"; sourceTree = "<group>"; };
|
||||
607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = "<group>"; };
|
||||
608619532CB77BA900F46182 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = "<group>"; };
|
||||
608619552CB7819B00F46182 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
607A66132B0EFA380010BFC8 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607A66122B0EFA380010BFC8 /* iOSTestbed.app */,
|
||||
607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
607A66302B0EFA3A0010BFC8 /* iOSTestbedTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607A66312B0EFA3A0010BFC8 /* iOSTestbedTests.m */,
|
||||
);
|
||||
path = iOSTestbedTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607A664F2B0EFFE00010BFC8 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//
|
||||
// AppDelegate.h
|
||||
// iOSTestbed
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
|
||||
@end
|
||||
@@ -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
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
|
||||
</dependencies>
|
||||
<scenes/>
|
||||
</document>
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string></string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,64 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.python.iOSTestbed</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>Launch Screen</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>TestArgs</key>
|
||||
<array>
|
||||
<string>test</string> <!-- Invoke "python -m test" -->
|
||||
<string>-uall</string> <!-- Enable all resources -->
|
||||
<string>--single-process</string> <!-- always run all tests sequentially in a single process -->
|
||||
<string>--rerun</string> <!-- Re-run failed tests in verbose mode -->
|
||||
<string>-W</string> <!-- Display test output on failure -->
|
||||
<!-- To run a subset of tests, add the test names below; e.g.,
|
||||
<string>test_os</string>
|
||||
<string>test_sys</string>
|
||||
-->
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// main.m
|
||||
// iOSTestbed
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "AppDelegate.h"
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
NSString * appDelegateClassName;
|
||||
@autoreleasepool {
|
||||
appDelegateClassName = NSStringFromClass([AppDelegate class]);
|
||||
|
||||
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <Python/Python.h>
|
||||
|
||||
@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
|
||||
Reference in New Issue
Block a user