mirror of
https://github.com/python/cpython.git
synced 2025-10-19 07:53:46 +00:00
gh-138171: Migrate iOS testbed location and add Apple build script (#138176)
Adds tooling to generate and test an iOS XCframework, in a way that will also facilitate adding other XCframework targets for other Apple platforms (tvOS, watchOS, visionOS and even macOS, potentially). --------- Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
This commit is contained in:
parent
9243a4b933
commit
35c7e52b3e
49 changed files with 1708 additions and 612 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -150,6 +150,7 @@ Lib/test/test_android.py @mhsmith @freakboy3742
|
|||
# iOS
|
||||
Doc/using/ios.rst @freakboy3742
|
||||
Lib/_ios_support.py @freakboy3742
|
||||
Apple/ @freakboy3742
|
||||
iOS/ @freakboy3742
|
||||
|
||||
# macOS
|
||||
|
|
18
.gitignore
vendored
18
.gitignore
vendored
|
@ -71,15 +71,15 @@ Lib/test/data/*
|
|||
/Makefile
|
||||
/Makefile.pre
|
||||
/iOSTestbed.*
|
||||
iOS/Frameworks/
|
||||
iOS/Resources/Info.plist
|
||||
iOS/testbed/build
|
||||
iOS/testbed/Python.xcframework/ios-*/bin
|
||||
iOS/testbed/Python.xcframework/ios-*/include
|
||||
iOS/testbed/Python.xcframework/ios-*/lib
|
||||
iOS/testbed/Python.xcframework/ios-*/Python.framework
|
||||
iOS/testbed/iOSTestbed.xcodeproj/project.xcworkspace
|
||||
iOS/testbed/iOSTestbed.xcodeproj/xcuserdata
|
||||
Apple/iOS/Frameworks/
|
||||
Apple/iOS/Resources/Info.plist
|
||||
Apple/testbed/build
|
||||
Apple/testbed/Python.xcframework/*/bin
|
||||
Apple/testbed/Python.xcframework/*/include
|
||||
Apple/testbed/Python.xcframework/*/lib
|
||||
Apple/testbed/Python.xcframework/*/Python.framework
|
||||
Apple/testbed/*Testbed.xcodeproj/project.xcworkspace
|
||||
Apple/testbed/*Testbed.xcodeproj/xcuserdata
|
||||
Mac/Makefile
|
||||
Mac/PythonLauncher/Info.plist
|
||||
Mac/PythonLauncher/Makefile
|
||||
|
|
990
Apple/__main__.py
Normal file
990
Apple/__main__.py
Normal file
|
@ -0,0 +1,990 @@
|
|||
#!/usr/bin/env python3
|
||||
##########################################################################
|
||||
# Apple XCframework build script
|
||||
#
|
||||
# This script simplifies the process of configuring, compiling and packaging an
|
||||
# XCframework for an Apple platform.
|
||||
#
|
||||
# At present, it only supports iOS, but it has been constructed so that it
|
||||
# could be used on any Apple platform.
|
||||
#
|
||||
# The simplest entry point is:
|
||||
#
|
||||
# $ python Apple ci iOS
|
||||
#
|
||||
# which will:
|
||||
# * Clean any pre-existing build artefacts
|
||||
# * Configure and make a Python that can be used for the build
|
||||
# * Configure and make a Python for each supported iOS architecture and ABI
|
||||
# * Combine the outputs of the builds from the previous step into a single
|
||||
# XCframework, merging binaries into a "fat" binary if necessary
|
||||
# * Clone a copy of the testbed, configured to use the XCframework
|
||||
# * Construct a tarball containing the release artefacts
|
||||
# * Run the test suite using the generated XCframework.
|
||||
#
|
||||
# This is the complete sequence that would be needed in CI to build and test
|
||||
# a candidate release artefact.
|
||||
#
|
||||
# Each individual step can be invoked individually - there are commands to
|
||||
# clean, configure-build, make-build, configure-host, make-host, package, and
|
||||
# test.
|
||||
#
|
||||
# There is also a build command that can be used to combine the configure and
|
||||
# make steps for the build Python, an individual host, all hosts, or all
|
||||
# builds.
|
||||
##########################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
import time
|
||||
from collections.abc import Sequence
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
from os.path import basename, relpath
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError
|
||||
from typing import Callable
|
||||
|
||||
EnvironmentT = dict[str, str]
|
||||
ArgsT = Sequence[str | Path]
|
||||
|
||||
SCRIPT_NAME = Path(__file__).name
|
||||
PYTHON_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
CROSS_BUILD_DIR = PYTHON_DIR / "cross-build"
|
||||
|
||||
HOSTS: dict[str, dict[str, dict[str, str]]] = {
|
||||
# Structure of this data:
|
||||
# * Platform identifier
|
||||
# * an XCframework slice that must exist for that platform
|
||||
# * a host triple: the multiarch spec for that host
|
||||
"iOS": {
|
||||
"ios-arm64": {
|
||||
"arm64-apple-ios": "arm64-iphoneos",
|
||||
},
|
||||
"ios-arm64_x86_64-simulator": {
|
||||
"arm64-apple-ios-simulator": "arm64-iphonesimulator",
|
||||
"x86_64-apple-ios-simulator": "x86_64-iphonesimulator",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def subdir(name: str, create: bool = False) -> Path:
|
||||
"""Ensure that a cross-build directory for the given name exists."""
|
||||
path = CROSS_BUILD_DIR / name
|
||||
if not path.exists():
|
||||
if not create:
|
||||
sys.exit(
|
||||
f"{path} does not exist. Create it by running the appropriate "
|
||||
f"`configure` subcommand of {SCRIPT_NAME}."
|
||||
)
|
||||
else:
|
||||
path.mkdir(parents=True)
|
||||
return path
|
||||
|
||||
|
||||
def run(
|
||||
command: ArgsT,
|
||||
*,
|
||||
host: str | None = None,
|
||||
env: EnvironmentT | None = None,
|
||||
log: bool | None = True,
|
||||
**kwargs,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""Run a command in an Apple development environment.
|
||||
|
||||
Optionally logs the executed command to the console.
|
||||
"""
|
||||
kwargs.setdefault("check", True)
|
||||
if env is None:
|
||||
env = os.environ.copy()
|
||||
|
||||
if host:
|
||||
host_env = apple_env(host)
|
||||
print_env(host_env)
|
||||
env.update(host_env)
|
||||
|
||||
if log:
|
||||
print(">", join_command(command))
|
||||
return subprocess.run(command, env=env, **kwargs)
|
||||
|
||||
|
||||
def join_command(args: str | Path | ArgsT) -> str:
|
||||
"""Format a command so it can be copied into a shell.
|
||||
|
||||
Similar to `shlex.join`, but also accepts arguments which are Paths, or a
|
||||
single string/Path outside of a list.
|
||||
"""
|
||||
if isinstance(args, (str, Path)):
|
||||
return str(args)
|
||||
else:
|
||||
return shlex.join(map(str, args))
|
||||
|
||||
|
||||
def print_env(env: EnvironmentT) -> None:
|
||||
"""Format the environment so it can be pasted into a shell."""
|
||||
for key, value in sorted(env.items()):
|
||||
print(f"export {key}={shlex.quote(value)}")
|
||||
|
||||
|
||||
def apple_env(host: str) -> EnvironmentT:
|
||||
"""Construct an Apple development environment for the given host."""
|
||||
env = {
|
||||
"PATH": ":".join(
|
||||
[
|
||||
str(PYTHON_DIR / "Apple/iOS/Resources/bin"),
|
||||
str(subdir(host) / "prefix"),
|
||||
"/usr/bin",
|
||||
"/bin",
|
||||
"/usr/sbin",
|
||||
"/sbin",
|
||||
"/Library/Apple/usr/bin",
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def delete_path(name: str) -> None:
|
||||
"""Delete the named cross-build directory, if it exists."""
|
||||
path = CROSS_BUILD_DIR / name
|
||||
if path.exists():
|
||||
print(f"Deleting {path} ...")
|
||||
shutil.rmtree(path)
|
||||
|
||||
|
||||
def all_host_triples(platform: str) -> list[str]:
|
||||
"""Return all host triples for the given platform.
|
||||
|
||||
The host triples are the platform definitions used as input to configure
|
||||
(e.g., "arm64-apple-ios-simulator").
|
||||
"""
|
||||
triples = []
|
||||
for slice_name, slice_parts in HOSTS[platform].items():
|
||||
triples.extend(list(slice_parts))
|
||||
return triples
|
||||
|
||||
|
||||
def clean(context: argparse.Namespace, target: str = "all") -> None:
|
||||
"""The implementation of the "clean" command."""
|
||||
# If we're explicitly targeting the build, there's no platform or
|
||||
# distribution artefacts. If we're cleaning tests, we keep all built
|
||||
# artefacts. Otherwise, the built artefacts must be dirty, so we remove
|
||||
# them.
|
||||
if target not in {"build", "test"}:
|
||||
paths = ["dist", context.platform] + list(HOSTS[context.platform])
|
||||
else:
|
||||
paths = []
|
||||
|
||||
if target in {"all", "build"}:
|
||||
paths.append("build")
|
||||
|
||||
if target in {"all", "hosts"}:
|
||||
paths.extend(all_host_triples(context.platform))
|
||||
elif target not in {"build", "test", "package"}:
|
||||
paths.append(target)
|
||||
|
||||
if target in {"all", "hosts", "test"}:
|
||||
paths.extend(
|
||||
[
|
||||
path.name
|
||||
for path in CROSS_BUILD_DIR.glob(
|
||||
f"{context.platform}-testbed.*"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
for path in paths:
|
||||
delete_path(path)
|
||||
|
||||
|
||||
def build_python_path() -> Path:
|
||||
"""The path to the build Python binary."""
|
||||
build_dir = subdir("build")
|
||||
binary = build_dir / "python"
|
||||
if not binary.is_file():
|
||||
binary = binary.with_suffix(".exe")
|
||||
if not binary.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"Unable to find `python(.exe)` in {build_dir}"
|
||||
)
|
||||
|
||||
return binary
|
||||
|
||||
|
||||
@contextmanager
|
||||
def group(text: str):
|
||||
"""A context manager that outputs a log marker around a section of a build.
|
||||
|
||||
If running in a GitHub Actions environment, the GitHub syntax for
|
||||
collapsible log sections is used.
|
||||
"""
|
||||
if "GITHUB_ACTIONS" in os.environ:
|
||||
print(f"::group::{text}")
|
||||
else:
|
||||
print(f"===== {text} " + "=" * (70 - len(text)))
|
||||
|
||||
yield
|
||||
|
||||
if "GITHUB_ACTIONS" in os.environ:
|
||||
print("::endgroup::")
|
||||
else:
|
||||
print()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def cwd(subdir: Path):
|
||||
"""A context manager that sets the current working directory."""
|
||||
orig = os.getcwd()
|
||||
os.chdir(subdir)
|
||||
yield
|
||||
os.chdir(orig)
|
||||
|
||||
|
||||
def configure_build_python(context: argparse.Namespace) -> None:
|
||||
"""The implementation of the "configure-build" command."""
|
||||
if context.clean:
|
||||
clean(context, "build")
|
||||
|
||||
with (
|
||||
group("Configuring build Python"),
|
||||
cwd(subdir("build", create=True)),
|
||||
):
|
||||
command = [relpath(PYTHON_DIR / "configure")]
|
||||
if context.args:
|
||||
command.extend(context.args)
|
||||
run(command)
|
||||
|
||||
|
||||
def make_build_python(context: argparse.Namespace) -> None:
|
||||
"""The implementation of the "make-build" command."""
|
||||
with (
|
||||
group("Compiling build Python"),
|
||||
cwd(subdir("build")),
|
||||
):
|
||||
run(["make", "-j", str(os.cpu_count())])
|
||||
|
||||
|
||||
def apple_target(host: str) -> str:
|
||||
"""Return the Apple platform identifier for a given host triple."""
|
||||
for _, platform_slices in HOSTS.items():
|
||||
for slice_name, slice_parts in platform_slices.items():
|
||||
for host_triple, multiarch in slice_parts.items():
|
||||
if host == host_triple:
|
||||
return ".".join(multiarch.split("-")[::-1])
|
||||
|
||||
raise KeyError(host)
|
||||
|
||||
|
||||
def apple_multiarch(host: str) -> str:
|
||||
"""Return the multiarch descriptor for a given host triple."""
|
||||
for _, platform_slices in HOSTS.items():
|
||||
for slice_name, slice_parts in platform_slices.items():
|
||||
for host_triple, multiarch in slice_parts.items():
|
||||
if host == host_triple:
|
||||
return multiarch
|
||||
|
||||
raise KeyError(host)
|
||||
|
||||
|
||||
def unpack_deps(
|
||||
platform: str,
|
||||
host: str,
|
||||
prefix_dir: Path,
|
||||
cache_dir: Path,
|
||||
) -> None:
|
||||
"""Unpack binary dependencies into a provided directory.
|
||||
|
||||
Downloads binaries if they aren't already present. Downloads will be stored
|
||||
in provided cache directory.
|
||||
|
||||
On iOS, as a safety mechanism, any dynamic libraries will be purged from
|
||||
the unpacked dependencies.
|
||||
"""
|
||||
deps_url = "https://github.com/beeware/cpython-apple-source-deps/releases/download"
|
||||
for name_ver in [
|
||||
"BZip2-1.0.8-2",
|
||||
"libFFI-3.4.7-2",
|
||||
"OpenSSL-3.0.16-2",
|
||||
"XZ-5.6.4-2",
|
||||
"mpdecimal-4.0.0-2",
|
||||
"zstd-1.5.7-1",
|
||||
]:
|
||||
filename = f"{name_ver.lower()}-{apple_target(host)}.tar.gz"
|
||||
archive_path = download(
|
||||
f"{deps_url}/{name_ver}/{filename}",
|
||||
target_dir=cache_dir,
|
||||
)
|
||||
shutil.unpack_archive(archive_path, prefix_dir)
|
||||
|
||||
# Dynamic libraries will be preferentially linked over static;
|
||||
# On iOS, ensure that no dylibs are available in the prefix folder.
|
||||
if platform == "iOS":
|
||||
for dylib in prefix_dir.glob("**/*.dylib"):
|
||||
dylib.unlink()
|
||||
|
||||
|
||||
def download(url: str, target_dir: Path) -> Path:
|
||||
"""Download the specified URL into the given directory.
|
||||
|
||||
:return: The path to the downloaded archive.
|
||||
"""
|
||||
target_path = Path(target_dir).resolve()
|
||||
target_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
out_path = target_path / basename(url)
|
||||
if not Path(out_path).is_file():
|
||||
run(
|
||||
[
|
||||
"curl",
|
||||
"-Lf",
|
||||
"--retry",
|
||||
"5",
|
||||
"--retry-all-errors",
|
||||
"-o",
|
||||
out_path,
|
||||
url,
|
||||
]
|
||||
)
|
||||
else:
|
||||
print(f"Using cached version of {basename(url)}")
|
||||
return out_path
|
||||
|
||||
|
||||
def configure_host_python(
|
||||
context: argparse.Namespace,
|
||||
host: str | None = None,
|
||||
) -> None:
|
||||
"""The implementation of the "configure-host" command."""
|
||||
if host is None:
|
||||
host = context.host
|
||||
|
||||
if context.clean:
|
||||
clean(context, host)
|
||||
|
||||
host_dir = subdir(host, create=True)
|
||||
prefix_dir = host_dir / "prefix"
|
||||
|
||||
with group(f"Downloading dependencies ({host})"):
|
||||
if not prefix_dir.exists():
|
||||
prefix_dir.mkdir()
|
||||
unpack_deps(context.platform, host, prefix_dir, context.cache_dir)
|
||||
else:
|
||||
print("Dependencies already installed")
|
||||
|
||||
with (
|
||||
group(f"Configuring host Python ({host})"),
|
||||
cwd(host_dir),
|
||||
):
|
||||
command = [
|
||||
# Basic cross-compiling configuration
|
||||
relpath(PYTHON_DIR / "configure"),
|
||||
f"--host={host}",
|
||||
f"--build={sysconfig.get_config_var('BUILD_GNU_TYPE')}",
|
||||
f"--with-build-python={build_python_path()}",
|
||||
"--with-system-libmpdec",
|
||||
"--enable-framework",
|
||||
# Dependent libraries.
|
||||
f"--with-openssl={prefix_dir}",
|
||||
f"LIBLZMA_CFLAGS=-I{prefix_dir}/include",
|
||||
f"LIBLZMA_LIBS=-L{prefix_dir}/lib -llzma",
|
||||
f"LIBFFI_CFLAGS=-I{prefix_dir}/include",
|
||||
f"LIBFFI_LIBS=-L{prefix_dir}/lib -lffi",
|
||||
f"LIBMPDEC_CFLAGS=-I{prefix_dir}/include",
|
||||
f"LIBMPDEC_LIBS=-L{prefix_dir}/lib -lmpdec",
|
||||
f"LIBZSTD_CFLAGS=-I{prefix_dir}/include",
|
||||
f"LIBZSTD_LIBS=-L{prefix_dir}/lib -lzstd",
|
||||
]
|
||||
|
||||
if context.args:
|
||||
command.extend(context.args)
|
||||
run(command, host=host)
|
||||
|
||||
|
||||
def make_host_python(
|
||||
context: argparse.Namespace,
|
||||
host: str | None = None,
|
||||
) -> None:
|
||||
"""The implementation of the "make-host" command."""
|
||||
if host is None:
|
||||
host = context.host
|
||||
|
||||
with (
|
||||
group(f"Compiling host Python ({host})"),
|
||||
cwd(subdir(host)),
|
||||
):
|
||||
run(["make", "-j", str(os.cpu_count())], host=host)
|
||||
run(["make", "install"], host=host)
|
||||
|
||||
|
||||
def framework_path(host_triple: str, multiarch: str) -> Path:
|
||||
"""The path to a built single-architecture framework product.
|
||||
|
||||
:param host_triple: The host triple (e.g., arm64-apple-ios-simulator)
|
||||
:param multiarch: The multiarch identifier (e.g., arm64-simulator)
|
||||
"""
|
||||
return CROSS_BUILD_DIR / f"{host_triple}/Apple/iOS/Frameworks/{multiarch}"
|
||||
|
||||
|
||||
def package_version(prefix_path: Path) -> str:
|
||||
"""Extract the Python version being built from patchlevel.h."""
|
||||
for path in prefix_path.glob("**/patchlevel.h"):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if match := re.search(
|
||||
r'\n\s*#define\s+PY_VERSION\s+"(.+)"\s*\n', text
|
||||
):
|
||||
version = match[1]
|
||||
# If not building against a tagged commit, add a timestamp to the
|
||||
# version. Follow the PyPA version number rules, as this will make
|
||||
# it easier to process with other tools. The version will have a
|
||||
# `+` suffix once any official release has been made; a freshly
|
||||
# forked main branch will have a version of 3.X.0a0.
|
||||
if version.endswith("a0"):
|
||||
version += "+"
|
||||
if version.endswith("+"):
|
||||
version += datetime.now(timezone.utc).strftime("%Y%m%d.%H%M%S")
|
||||
|
||||
return version
|
||||
|
||||
sys.exit("Unable to determine Python version being packaged.")
|
||||
|
||||
|
||||
def lib_platform_files(dirname, names):
|
||||
"""A file filter that ignores platform-specific files in the lib directory.
|
||||
"""
|
||||
path = Path(dirname)
|
||||
if (
|
||||
path.parts[-3] == "lib"
|
||||
and path.parts[-2].startswith("python")
|
||||
and path.parts[-1] == "lib-dynload"
|
||||
):
|
||||
return names
|
||||
elif path.parts[-2] == "lib" and path.parts[-1].startswith("python"):
|
||||
ignored_names = set(
|
||||
name
|
||||
for name in names
|
||||
if (
|
||||
name.startswith("_sysconfigdata_")
|
||||
or name.startswith("_sysconfig_vars_")
|
||||
or name == "build-details.json"
|
||||
)
|
||||
)
|
||||
else:
|
||||
ignored_names = set()
|
||||
|
||||
return ignored_names
|
||||
|
||||
|
||||
def lib_non_platform_files(dirname, names):
|
||||
"""A file filter that ignores anything *except* platform-specific files
|
||||
in the lib directory.
|
||||
"""
|
||||
path = Path(dirname)
|
||||
if path.parts[-2] == "lib" and path.parts[-1].startswith("python"):
|
||||
return set(names) - lib_platform_files(dirname, names) - {"lib-dynload"}
|
||||
else:
|
||||
return set()
|
||||
|
||||
|
||||
def create_xcframework(platform: str) -> str:
|
||||
"""Build an XCframework from the component parts for the platform.
|
||||
|
||||
:return: The version number of the Python verion that was packaged.
|
||||
"""
|
||||
package_path = CROSS_BUILD_DIR / platform
|
||||
try:
|
||||
package_path.mkdir()
|
||||
except FileExistsError:
|
||||
raise RuntimeError(
|
||||
f"{platform} XCframework already exists; do you need to run with --clean?"
|
||||
) from None
|
||||
|
||||
frameworks = []
|
||||
# Merge Frameworks for each component SDK. If there's only one architecture
|
||||
# for the SDK, we can use the compiled Python.framework as-is. However, if
|
||||
# there's more than architecture, we need to merge the individual built
|
||||
# frameworks into a merged "fat" framework.
|
||||
for slice_name, slice_parts in HOSTS[platform].items():
|
||||
# Some parts are the same across all slices, so we use can any of the
|
||||
# host frameworks as the source for the merged version. Use the first
|
||||
# one on the list, as it's as representative as any other.
|
||||
first_host_triple, first_multiarch = next(iter(slice_parts.items()))
|
||||
first_framework = (
|
||||
framework_path(first_host_triple, first_multiarch)
|
||||
/ "Python.framework"
|
||||
)
|
||||
|
||||
if len(slice_parts) == 1:
|
||||
# The first framework is the only framework, so copy it.
|
||||
print(f"Copying framework for {slice_name}...")
|
||||
frameworks.append(first_framework)
|
||||
else:
|
||||
print(f"Merging framework for {slice_name}...")
|
||||
slice_path = CROSS_BUILD_DIR / slice_name
|
||||
slice_framework = slice_path / "Python.framework"
|
||||
slice_framework.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# Copy the Info.plist
|
||||
shutil.copy(
|
||||
first_framework / "Info.plist",
|
||||
slice_framework / "Info.plist",
|
||||
)
|
||||
|
||||
# Copy the headers
|
||||
shutil.copytree(
|
||||
first_framework / "Headers",
|
||||
slice_framework / "Headers",
|
||||
)
|
||||
|
||||
# Create the "fat" library binary for the slice
|
||||
run(
|
||||
["lipo", "-create", "-output", slice_framework / "Python"]
|
||||
+ [
|
||||
(
|
||||
framework_path(host_triple, multiarch)
|
||||
/ "Python.framework/Python"
|
||||
)
|
||||
for host_triple, multiarch in slice_parts.items()
|
||||
]
|
||||
)
|
||||
|
||||
# Add this merged slice to the list to be added to the XCframework
|
||||
frameworks.append(slice_framework)
|
||||
|
||||
print()
|
||||
print("Build XCframework...")
|
||||
cmd = [
|
||||
"xcodebuild",
|
||||
"-create-xcframework",
|
||||
"-output",
|
||||
package_path / "Python.xcframework",
|
||||
]
|
||||
for framework in frameworks:
|
||||
cmd.extend(["-framework", framework])
|
||||
|
||||
run(cmd)
|
||||
|
||||
# Extract the package version from the merged framework
|
||||
version = package_version(package_path / "Python.xcframework")
|
||||
|
||||
# On non-macOS platforms, each framework in XCframework only contains the
|
||||
# headers, libPython, plus an Info.plist. Other resources like the standard
|
||||
# library and binary shims aren't allowed to live in framework; they need
|
||||
# to be copied in separately.
|
||||
print()
|
||||
print("Copy additional resources...")
|
||||
has_common_stdlib = False
|
||||
for slice_name, slice_parts in HOSTS[platform].items():
|
||||
# Some parts are the same across all slices, so we can any of the
|
||||
# host frameworks as the source for the merged version.
|
||||
first_host_triple, first_multiarch = next(iter(slice_parts.items()))
|
||||
first_path = framework_path(first_host_triple, first_multiarch)
|
||||
first_framework = first_path / "Python.framework"
|
||||
|
||||
slice_path = package_path / f"Python.xcframework/{slice_name}"
|
||||
slice_framework = slice_path / "Python.framework"
|
||||
|
||||
# Copy the binary helpers
|
||||
print(f" - {slice_name} binaries")
|
||||
shutil.copytree(first_path / "bin", slice_path / "bin")
|
||||
|
||||
# Copy the include path (this will be a symlink to the framework headers)
|
||||
print(f" - {slice_name} include files")
|
||||
shutil.copytree(
|
||||
first_path / "include",
|
||||
slice_path / "include",
|
||||
symlinks=True,
|
||||
)
|
||||
|
||||
# Copy in the cross-architecture pyconfig.h
|
||||
shutil.copy(
|
||||
PYTHON_DIR / f"Apple/{platform}/Resources/pyconfig.h",
|
||||
slice_framework / "Headers/pyconfig.h",
|
||||
)
|
||||
|
||||
print(f" - {slice_name} architecture-specific files")
|
||||
for host_triple, multiarch in slice_parts.items():
|
||||
print(f" - {multiarch} standard library")
|
||||
arch, _ = multiarch.split("-", 1)
|
||||
|
||||
if not has_common_stdlib:
|
||||
print(" - using this architecture as the common stdlib")
|
||||
shutil.copytree(
|
||||
framework_path(host_triple, multiarch) / "lib",
|
||||
package_path / "Python.xcframework/lib",
|
||||
ignore=lib_platform_files,
|
||||
)
|
||||
has_common_stdlib = True
|
||||
|
||||
shutil.copytree(
|
||||
framework_path(host_triple, multiarch) / "lib",
|
||||
slice_path / f"lib-{arch}",
|
||||
ignore=lib_non_platform_files,
|
||||
)
|
||||
|
||||
# Copy the host's pyconfig.h to an architecture-specific name.
|
||||
arch = multiarch.split("-")[0]
|
||||
host_path = (
|
||||
CROSS_BUILD_DIR
|
||||
/ host_triple
|
||||
/ "Apple/iOS/Frameworks"
|
||||
/ multiarch
|
||||
)
|
||||
host_framework = host_path / "Python.framework"
|
||||
shutil.copy(
|
||||
host_framework / "Headers/pyconfig.h",
|
||||
slice_framework / f"Headers/pyconfig-{arch}.h",
|
||||
)
|
||||
|
||||
print(" - build tools")
|
||||
shutil.copytree(
|
||||
PYTHON_DIR / "Apple/testbed/Python.xcframework/build",
|
||||
package_path / "Python.xcframework/build",
|
||||
)
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def package(context: argparse.Namespace) -> None:
|
||||
"""The implementation of the "package" command."""
|
||||
if context.clean:
|
||||
clean(context, "package")
|
||||
|
||||
with group("Building package"):
|
||||
# Create an XCframework
|
||||
version = create_xcframework(context.platform)
|
||||
|
||||
# Clone testbed
|
||||
print()
|
||||
run(
|
||||
[
|
||||
sys.executable,
|
||||
"Apple/testbed",
|
||||
"clone",
|
||||
"--platform",
|
||||
context.platform,
|
||||
"--framework",
|
||||
CROSS_BUILD_DIR / context.platform / "Python.xcframework",
|
||||
CROSS_BUILD_DIR / context.platform / "testbed",
|
||||
]
|
||||
)
|
||||
|
||||
# Build the final archive
|
||||
archive_name = (
|
||||
CROSS_BUILD_DIR
|
||||
/ "dist"
|
||||
/ f"python-{version}-{context.platform}-XCframework"
|
||||
)
|
||||
|
||||
print()
|
||||
print("Create package archive...")
|
||||
shutil.make_archive(
|
||||
str(CROSS_BUILD_DIR / archive_name),
|
||||
format="gztar",
|
||||
root_dir=CROSS_BUILD_DIR / context.platform,
|
||||
base_dir=".",
|
||||
)
|
||||
print()
|
||||
print(f"{archive_name.relative_to(PYTHON_DIR)}.tar.gz created.")
|
||||
|
||||
|
||||
def build(context: argparse.Namespace, host: str | None = None) -> None:
|
||||
"""The implementation of the "build" command."""
|
||||
if host is None:
|
||||
host = context.host
|
||||
|
||||
if context.clean:
|
||||
clean(context, host)
|
||||
|
||||
if host in {"all", "build"}:
|
||||
for step in [
|
||||
configure_build_python,
|
||||
make_build_python,
|
||||
]:
|
||||
step(context)
|
||||
|
||||
if host == "build":
|
||||
hosts = []
|
||||
elif host in {"all", "hosts"}:
|
||||
hosts = all_host_triples(context.platform)
|
||||
else:
|
||||
hosts = [host]
|
||||
|
||||
for step_host in hosts:
|
||||
for step in [
|
||||
configure_host_python,
|
||||
make_host_python,
|
||||
]:
|
||||
step(context, host=step_host)
|
||||
|
||||
if host in {"all", "hosts"}:
|
||||
package(context)
|
||||
|
||||
|
||||
def test(context: argparse.Namespace, host: str | None = None) -> None:
|
||||
"""The implementation of the "test" command."""
|
||||
if host is None:
|
||||
host = context.host
|
||||
|
||||
if context.clean:
|
||||
clean(context, "test")
|
||||
|
||||
with group(f"Test {'XCframework' if host in {'all', 'hosts'} else host}"):
|
||||
timestamp = str(time.time_ns())[:-6]
|
||||
testbed_dir = (
|
||||
CROSS_BUILD_DIR / f"{context.platform}-testbed.{timestamp}"
|
||||
)
|
||||
if host in {"all", "hosts"}:
|
||||
framework_path = (
|
||||
CROSS_BUILD_DIR / context.platform / "Python.xcframework"
|
||||
)
|
||||
else:
|
||||
build_arch = platform.machine()
|
||||
host_arch = host.split("-")[0]
|
||||
|
||||
if not host.endswith("-simulator"):
|
||||
print("Skipping test suite non-simulator build.")
|
||||
return
|
||||
elif build_arch != host_arch:
|
||||
print(
|
||||
f"Skipping test suite for an {host_arch} build "
|
||||
f"on an {build_arch} machine."
|
||||
)
|
||||
return
|
||||
else:
|
||||
framework_path = (
|
||||
CROSS_BUILD_DIR
|
||||
/ host
|
||||
/ f"Apple/{context.platform}"
|
||||
/ f"Frameworks/{apple_multiarch(host)}"
|
||||
)
|
||||
|
||||
run(
|
||||
[
|
||||
sys.executable,
|
||||
"Apple/testbed",
|
||||
"clone",
|
||||
"--platform",
|
||||
context.platform,
|
||||
"--framework",
|
||||
framework_path,
|
||||
testbed_dir,
|
||||
]
|
||||
)
|
||||
|
||||
run(
|
||||
[
|
||||
sys.executable,
|
||||
testbed_dir,
|
||||
"run",
|
||||
"--verbose",
|
||||
]
|
||||
+ (
|
||||
["--simulator", str(context.simulator)]
|
||||
if context.simulator
|
||||
else []
|
||||
)
|
||||
+ [
|
||||
"--",
|
||||
"test",
|
||||
"--slow-ci" if context.slow else "--fast-ci",
|
||||
"--single-process",
|
||||
"--no-randomize",
|
||||
# Timeout handling requires subprocesses; explicitly setting
|
||||
# the timeout to -1 disables the faulthandler.
|
||||
"--timeout=-1",
|
||||
# Adding Python options requires the use of a subprocess to
|
||||
# start a new Python interpreter.
|
||||
"--dont-add-python-opts",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def ci(context: argparse.Namespace) -> None:
|
||||
"""The implementation of the "ci" command."""
|
||||
clean(context, "all")
|
||||
build(context, host="all")
|
||||
test(context, host="all")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"A tool for managing the build, package and test process of "
|
||||
"CPython on Apple platforms."
|
||||
),
|
||||
)
|
||||
parser.suggest_on_error = True
|
||||
subcommands = parser.add_subparsers(dest="subcommand", required=True)
|
||||
|
||||
clean = subcommands.add_parser(
|
||||
"clean",
|
||||
help="Delete all build directories",
|
||||
)
|
||||
|
||||
configure_build = subcommands.add_parser(
|
||||
"configure-build", help="Run `configure` for the build Python"
|
||||
)
|
||||
subcommands.add_parser(
|
||||
"make-build", help="Run `make` for the build Python"
|
||||
)
|
||||
configure_host = subcommands.add_parser(
|
||||
"configure-host",
|
||||
help="Run `configure` for a specific platform and target",
|
||||
)
|
||||
make_host = subcommands.add_parser(
|
||||
"make-host",
|
||||
help="Run `make` for a specific platform and target",
|
||||
)
|
||||
package = subcommands.add_parser(
|
||||
"package",
|
||||
help="Create a release package for the platform",
|
||||
)
|
||||
build = subcommands.add_parser(
|
||||
"build",
|
||||
help="Build all platform targets and create the XCframework",
|
||||
)
|
||||
test = subcommands.add_parser(
|
||||
"test",
|
||||
help="Run the testbed for a specific platform",
|
||||
)
|
||||
ci = subcommands.add_parser(
|
||||
"ci",
|
||||
help="Run build, package, and test",
|
||||
)
|
||||
|
||||
# platform argument
|
||||
for cmd in [clean, configure_host, make_host, package, build, test, ci]:
|
||||
cmd.add_argument(
|
||||
"platform",
|
||||
choices=HOSTS.keys(),
|
||||
help="The target platform to build",
|
||||
)
|
||||
|
||||
# host triple argument
|
||||
for cmd in [configure_host, make_host]:
|
||||
cmd.add_argument(
|
||||
"host",
|
||||
help="The host triple to build (e.g., arm64-apple-ios-simulator)",
|
||||
)
|
||||
# optional host triple argument
|
||||
for cmd in [clean, build, test]:
|
||||
cmd.add_argument(
|
||||
"host",
|
||||
nargs="?",
|
||||
default="all",
|
||||
help=(
|
||||
"The host triple to build (e.g., arm64-apple-ios-simulator), "
|
||||
"or 'build' for just the build platform, or 'hosts' for all "
|
||||
"host platforms, or 'all' for the build platform and all "
|
||||
"hosts. Defaults to 'all'"
|
||||
),
|
||||
)
|
||||
|
||||
# --clean option
|
||||
for cmd in [configure_build, configure_host, build, package, test, ci]:
|
||||
cmd.add_argument(
|
||||
"--clean",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="clean",
|
||||
help="Delete the relevant build directories first",
|
||||
)
|
||||
|
||||
# --cache-dir option
|
||||
for cmd in [configure_host, build, ci]:
|
||||
cmd.add_argument(
|
||||
"--cache-dir",
|
||||
default="./cross-build/downloads",
|
||||
help="The directory to store cached downloads.",
|
||||
)
|
||||
|
||||
# --simulator option
|
||||
for cmd in [test, ci]:
|
||||
cmd.add_argument(
|
||||
"--simulator",
|
||||
help=(
|
||||
"The name of the simulator to use (eg: 'iPhone 16e'). Defaults to "
|
||||
"the most recently released 'entry level' iPhone device. Device "
|
||||
"architecture and OS version can also be specified; e.g., "
|
||||
"`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would run on "
|
||||
"an ARM64 iPhone 16 Pro simulator running iOS 26.0."
|
||||
),
|
||||
)
|
||||
cmd.add_argument(
|
||||
"--slow",
|
||||
action="store_true",
|
||||
help="Run tests with --slow-ci options.",
|
||||
)
|
||||
|
||||
for subcommand in [configure_build, configure_host, build, ci]:
|
||||
subcommand.add_argument(
|
||||
"args", nargs="*", help="Extra arguments to pass to `configure`"
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def print_called_process_error(e: subprocess.CalledProcessError) -> None:
|
||||
for stream_name in ["stdout", "stderr"]:
|
||||
content = getattr(e, stream_name)
|
||||
stream = getattr(sys, stream_name)
|
||||
if content:
|
||||
stream.write(content)
|
||||
if not content.endswith("\n"):
|
||||
stream.write("\n")
|
||||
|
||||
# shlex uses single quotes, so we surround the command with double quotes.
|
||||
print(
|
||||
f'Command "{join_command(e.cmd)}" returned exit status {e.returncode}'
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Handle SIGTERM the same way as SIGINT. This ensures that if we're
|
||||
# terminated by the buildbot worker, we'll make an attempt to clean up our
|
||||
# subprocesses.
|
||||
def signal_handler(*args):
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# Process command line arguments
|
||||
context = parse_args()
|
||||
dispatch: dict[str, Callable] = {
|
||||
"clean": clean,
|
||||
"configure-build": configure_build_python,
|
||||
"make-build": make_build_python,
|
||||
"configure-host": configure_host_python,
|
||||
"make-host": make_host_python,
|
||||
"package": package,
|
||||
"build": build,
|
||||
"test": test,
|
||||
"ci": ci,
|
||||
}
|
||||
|
||||
try:
|
||||
dispatch[context.subcommand](context)
|
||||
except CalledProcessError as e:
|
||||
print()
|
||||
print_called_process_error(e)
|
||||
sys.exit(1)
|
||||
except RuntimeError as e:
|
||||
print()
|
||||
print(e)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
328
Apple/iOS/README.md
Normal file
328
Apple/iOS/README.md
Normal file
|
@ -0,0 +1,328 @@
|
|||
# Python on iOS README
|
||||
|
||||
**iOS support is [tier 3](https://peps.python.org/pep-0011/#tier-3).**
|
||||
|
||||
This document provides a quick overview of some iOS specific features in the
|
||||
Python distribution.
|
||||
|
||||
These instructions are only needed if you're planning to compile Python for iOS
|
||||
yourself. Most users should *not* need to do this. If you're looking to
|
||||
experiment with writing an iOS app in Python, tools such as [BeeWare's
|
||||
Briefcase](https://briefcase.readthedocs.io) and [Kivy's
|
||||
Buildozer](https://buildozer.readthedocs.io) will provide a much more
|
||||
approachable user experience.
|
||||
|
||||
## Compilers for building on iOS
|
||||
|
||||
Building for iOS requires the use of Apple's Xcode tooling. It is strongly
|
||||
recommended that you use the most recent stable release of Xcode. This will
|
||||
require the use of the most (or second-most) recently released macOS version,
|
||||
as Apple does not maintain Xcode for older macOS versions. The Xcode Command
|
||||
Line Tools are not sufficient for iOS development; you need a *full* Xcode
|
||||
install.
|
||||
|
||||
If you want to run your code on the iOS simulator, you'll also need to install
|
||||
an iOS Simulator Platform. You should be prompted to select an iOS Simulator
|
||||
Platform when you first run Xcode. Alternatively, you can add an iOS Simulator
|
||||
Platform by selecting an open the Platforms tab of the Xcode Settings panel.
|
||||
|
||||
## Building Python on iOS
|
||||
|
||||
### ABIs and Architectures
|
||||
|
||||
iOS apps can be deployed on physical devices, and on the iOS simulator. Although
|
||||
the API used on these devices is identical, the ABI is different - you need to
|
||||
link against different libraries for an iOS device build (`iphoneos`) or an
|
||||
iOS simulator build (`iphonesimulator`).
|
||||
|
||||
Apple uses the `XCframework` format to allow specifying a single dependency
|
||||
that supports multiple ABIs. An `XCframework` is a wrapper around multiple
|
||||
ABI-specific frameworks that share a common API.
|
||||
|
||||
iOS can also support different CPU architectures within each ABI. At present,
|
||||
there is only a single supported architecture on physical devices - ARM64.
|
||||
However, the *simulator* supports 2 architectures - ARM64 (for running on Apple
|
||||
Silicon machines), and x86_64 (for running on older Intel-based machines).
|
||||
|
||||
To support multiple CPU architectures on a single platform, Apple uses a "fat
|
||||
binary" format - a single physical file that contains support for multiple
|
||||
architectures. It is possible to compile and use a "thin" single architecture
|
||||
version of a binary for testing purposes; however, the "thin" binary will not be
|
||||
portable to machines using other architectures.
|
||||
|
||||
### Building a multi-architecture iOS XCframework
|
||||
|
||||
The `Apple` subfolder of the Python repository acts as a build script that
|
||||
can be used to coordinate the compilation of a complete iOS XCframework. To use
|
||||
it, run::
|
||||
|
||||
python Apple build iOS
|
||||
|
||||
This will:
|
||||
|
||||
* Configure and compile a version of Python to run on the build machine
|
||||
* Download pre-compiled binary dependencies for each platform
|
||||
* Configure and build a `Python.framework` for each required architecture and
|
||||
iOS SDK
|
||||
* Merge the multiple `Python.framework` folders into a single `Python.xcframework`
|
||||
* Produce a `.tar.gz` archive in the `cross-build/dist` folder containing
|
||||
the `Python.xcframework`, plus a copy of the Testbed app pre-configured to
|
||||
use the XCframework.
|
||||
|
||||
The `Apple` build script has other entry points that will perform the
|
||||
individual parts of the overall `build` target, plus targets to test the
|
||||
build, clean the `cross-build` folder of iOS build products, and perform a
|
||||
complete "build and test" CI run. The `--clean` flag can also be used on
|
||||
individual commands to ensure that a stale build product are removed before
|
||||
building.
|
||||
|
||||
### Building a single-architecture framework
|
||||
|
||||
If you're using the `Apple` build script, you won't need to build
|
||||
individual frameworks. However, if you do need to manually configure an iOS
|
||||
Python build for a single framework, the following options are available.
|
||||
|
||||
#### iOS specific arguments to configure
|
||||
|
||||
* `--enable-framework[=DIR]`
|
||||
|
||||
This argument specifies the location where the Python.framework will be
|
||||
installed. If `DIR` is not specified, the framework will be installed into
|
||||
a subdirectory of the `iOS/Frameworks` folder.
|
||||
|
||||
This argument *must* be provided when configuring iOS builds. iOS does not
|
||||
support non-framework builds.
|
||||
|
||||
* `--with-framework-name=NAME`
|
||||
|
||||
Specify the name for the Python framework; defaults to `Python`.
|
||||
|
||||
> [!NOTE]
|
||||
> Unless you know what you're doing, changing the name of the Python
|
||||
> framework on iOS is not advised. If you use this option, you won't be able
|
||||
> to run the `Apple` build script without making significant manual
|
||||
> alterations, and you won't be able to use any binary packages unless you
|
||||
> compile them yourself using your own framework name.
|
||||
|
||||
#### Building Python for iOS
|
||||
|
||||
The Python build system will create a `Python.framework` that supports a
|
||||
*single* ABI with a *single* architecture. Unlike macOS, iOS does not allow a
|
||||
framework to contain non-library content, so the iOS build will produce a
|
||||
`bin` and `lib` folder in the same output folder as `Python.framework`.
|
||||
The `lib` folder will be needed at runtime to support the Python library.
|
||||
|
||||
If you want to use Python in a real iOS project, you need to produce multiple
|
||||
`Python.framework` builds, one for each ABI and architecture. iOS builds of
|
||||
Python *must* be constructed as framework builds. To support this, you must
|
||||
provide the `--enable-framework` flag when configuring the build. The build
|
||||
also requires the use of cross-compilation. The minimal commands for building
|
||||
Python for the ARM64 iOS simulator will look something like:
|
||||
```
|
||||
export PATH="$(pwd)/Apple/iOS/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
|
||||
./configure \
|
||||
--enable-framework \
|
||||
--host=arm64-apple-ios-simulator \
|
||||
--build=arm64-apple-darwin \
|
||||
--with-build-python=/path/to/python.exe
|
||||
make
|
||||
make install
|
||||
```
|
||||
|
||||
In this invocation:
|
||||
|
||||
* `Apple/iOS/Resources/bin` has been added to the path, providing some shims for the
|
||||
compilers and linkers needed by the build. Xcode requires the use of `xcrun`
|
||||
to invoke compiler tooling. However, if `xcrun` is pre-evaluated and the
|
||||
result passed to `configure`, these results can embed user- and
|
||||
version-specific paths into the sysconfig data, which limits the portability
|
||||
of the compiled Python. Alternatively, if `xcrun` is used *as* the compiler,
|
||||
it requires that compiler variables like `CC` include spaces, which can
|
||||
cause significant problems with many C configuration systems which assume that
|
||||
`CC` will be a single executable.
|
||||
|
||||
To work around this problem, the `Apple/iOS/Resources/bin` folder contains some
|
||||
wrapper scripts that present as simple compilers and linkers, but wrap
|
||||
underlying calls to `xcrun`. This allows configure to use a `CC`
|
||||
definition without spaces, and without user- or version-specific paths, while
|
||||
retaining the ability to adapt to the local Xcode install. These scripts are
|
||||
included in the `bin` directory of an iOS install.
|
||||
|
||||
These scripts will, by default, use the currently active Xcode installation.
|
||||
If you want to use a different Xcode installation, you can use
|
||||
`xcode-select` to set a new default Xcode globally, or you can use the
|
||||
`DEVELOPER_DIR` environment variable to specify an Xcode install. The
|
||||
scripts will use the default `iphoneos`/`iphonesimulator` SDK version for
|
||||
the select Xcode install; if you want to use a different SDK, you can set the
|
||||
`IOS_SDK_VERSION` environment variable. (e.g, setting
|
||||
`IOS_SDK_VERSION=17.1` would cause the scripts to use the `iphoneos17.1`
|
||||
and `iphonesimulator17.1` SDKs, regardless of the Xcode default.)
|
||||
|
||||
The path has also been cleared of any user customizations. A common source of
|
||||
bugs is for tools like Homebrew to accidentally leak macOS binaries into an iOS
|
||||
build. Resetting the path to a known "bare bones" value is the easiest way to
|
||||
avoid these problems.
|
||||
|
||||
* `--host` is the architecture and ABI that you want to build, in GNU compiler
|
||||
triple format. This will be one of:
|
||||
|
||||
- `arm64-apple-ios` for ARM64 iOS devices.
|
||||
- `arm64-apple-ios-simulator` for the iOS simulator running on Apple
|
||||
Silicon devices.
|
||||
- `x86_64-apple-ios-simulator` for the iOS simulator running on Intel
|
||||
devices.
|
||||
|
||||
* `--build` is the GNU compiler triple for the machine that will be running
|
||||
the compiler. This is one of:
|
||||
|
||||
- `arm64-apple-darwin` for Apple Silicon devices.
|
||||
- `x86_64-apple-darwin` for Intel devices.
|
||||
|
||||
* `/path/to/python.exe` is the path to a Python binary on the machine that
|
||||
will be running the compiler. This is needed because the Python compilation
|
||||
process involves running some Python code. On a normal desktop build of
|
||||
Python, you can compile a python interpreter and then use that interpreter to
|
||||
run Python code. However, the binaries produced for iOS won't run on macOS, so
|
||||
you need to provide an external Python interpreter. This interpreter must be
|
||||
the same version as the Python that is being compiled. To be completely safe,
|
||||
this should be the *exact* same commit hash. However, the longer a Python
|
||||
release has been stable, the more likely it is that this constraint can be
|
||||
relaxed - the same micro version will often be sufficient.
|
||||
|
||||
* The `install` target for iOS builds is slightly different to other
|
||||
platforms. On most platforms, `make install` will install the build into
|
||||
the final runtime location. This won't be the case for iOS, as the final
|
||||
runtime location will be on a physical device.
|
||||
|
||||
However, you still need to run the `install` target for iOS builds, as it
|
||||
performs some final framework assembly steps. The location specified with
|
||||
`--enable-framework` will be the location where `make install` will
|
||||
assemble the complete iOS framework. This completed framework can then
|
||||
be copied and relocated as required.
|
||||
|
||||
For a full CPython build, you also need to specify the paths to iOS builds of
|
||||
the binary libraries that CPython depends on (such as XZ, LibFFI and OpenSSL).
|
||||
This can be done by defining library specific environment variables (such as
|
||||
`LIBLZMA_CFLAGS`, `LIBLZMA_LIBS`), and the `--with-openssl` configure
|
||||
option. Versions of these libraries pre-compiled for iOS can be found in [this
|
||||
repository](https://github.com/beeware/cpython-apple-source-deps/releases).
|
||||
LibFFI is especially important, as many parts of the standard library
|
||||
(including the `platform`, `sysconfig` and `webbrowser` modules) require
|
||||
the use of the `ctypes` module at runtime.
|
||||
|
||||
By default, Python will be compiled with an iOS deployment target (i.e., the
|
||||
minimum supported iOS version) of 13.0. To specify a different deployment
|
||||
target, provide the version number as part of the `--host` argument - for
|
||||
example, `--host=arm64-apple-ios15.4-simulator` would compile an ARM64
|
||||
simulator build with a deployment target of 15.4.
|
||||
|
||||
## Testing Python on iOS
|
||||
|
||||
### Testing a multi-architecture framework
|
||||
|
||||
Once you have a built an XCframework, you can test that framework by running:
|
||||
|
||||
$ python Apple test iOS
|
||||
|
||||
### Testing a single-architecture framework
|
||||
|
||||
The `Apple/testbed` folder that contains an Xcode project that is able to run
|
||||
the Python test suite on Apple platforms. This project converts the Python test
|
||||
suite into a single test case in Xcode's XCTest framework. The single XCTest
|
||||
passes if the test suite passes.
|
||||
|
||||
To run the test suite, configure a Python build for an iOS simulator (i.e.,
|
||||
`--host=arm64-apple-ios-simulator` or `--host=x86_64-apple-ios-simulator`
|
||||
), specifying a framework build (i.e. `--enable-framework`). Ensure that your
|
||||
`PATH` has been configured to include the `Apple/iOS/Resources/bin` folder and
|
||||
exclude any non-iOS tools, then run:
|
||||
```
|
||||
make all
|
||||
make install
|
||||
make testios
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
* Build an iOS framework for your chosen architecture;
|
||||
* Finalize the single-platform framework;
|
||||
* Make a clean copy of the testbed project;
|
||||
* Install the Python iOS framework into the copy of the testbed project; and
|
||||
* Run the test suite on an "entry-level device" simulator (i.e., an iPhone SE,
|
||||
iPhone 16e, or a similar).
|
||||
|
||||
On success, the test suite will exit and report successful completion of the
|
||||
test suite. On a 2022 M1 MacBook Pro, the test suite takes approximately 15
|
||||
minutes to run; a couple of extra minutes is required to compile the testbed
|
||||
project, and then boot and prepare the iOS simulator.
|
||||
|
||||
### Debugging test failures
|
||||
|
||||
Running `python Apple test iOS` generates a standalone version of the
|
||||
`Apple/testbed` project, and runs the full test suite. It does this using
|
||||
`Apple/testbed` itself - the folder is an executable module that can be used
|
||||
to create and run a clone of the testbed project. The standalone version of the
|
||||
testbed will be created in a directory named
|
||||
`cross-build/iOS-testbed.<timestamp>`.
|
||||
|
||||
You can generate your own standalone testbed instance by running:
|
||||
```
|
||||
python cross-build/iOS/testbed clone my-testbed
|
||||
```
|
||||
|
||||
In this invocation, `my-testbed` is the name of the folder for the new
|
||||
testbed clone.
|
||||
|
||||
If you've built your own XCframework, or you only want to test a single architecture,
|
||||
you can construct a standalone testbed instance by running:
|
||||
```
|
||||
python Apple/testbed clone --platform iOS --framework <path/to/framework> my-testbed
|
||||
```
|
||||
|
||||
The framework path can be the path path to a `Python.xcframework`, or the
|
||||
path to a folder that contains a single-platform `Python.framework`.
|
||||
|
||||
You can then use the `my-testbed` folder to run the Python test suite,
|
||||
passing in any command line arguments you may require. For example, if you're
|
||||
trying to diagnose a failure in the `os` module, you might run:
|
||||
```
|
||||
python my-testbed run -- test -W test_os
|
||||
```
|
||||
|
||||
This is the equivalent of running `python -m test -W test_os` on a desktop
|
||||
Python build. Any arguments after the `--` will be passed to testbed as if
|
||||
they were arguments to `python -m` on a desktop machine.
|
||||
|
||||
### Testing in Xcode
|
||||
|
||||
You can also open the testbed project in Xcode by running:
|
||||
```
|
||||
open my-testbed/iOSTestbed.xcodeproj
|
||||
```
|
||||
|
||||
This will allow you to use the full Xcode suite of tools for debugging.
|
||||
|
||||
The arguments used to run the test suite are defined as part of the test plan.
|
||||
To modify the test plan, select the test plan node of the project tree (it
|
||||
should be the first child of the root node), and select the "Configurations"
|
||||
tab. Modify the "Arguments Passed On Launch" value to change the testing
|
||||
arguments.
|
||||
|
||||
The test plan also disables parallel testing, and specifies the use of the
|
||||
`Testbed.lldbinit` file for providing configuration of the debugger. The
|
||||
default debugger configuration disables automatic breakpoints on the
|
||||
`SIGINT`, `SIGUSR1`, `SIGUSR2`, and `SIGXFSZ` signals.
|
||||
|
||||
### Testing on an iOS device
|
||||
|
||||
To test on an iOS device, the app needs to be signed with known developer
|
||||
credentials. To obtain these credentials, you must have an iOS Developer
|
||||
account, and your Xcode install will need to be logged into your account (see
|
||||
the Accounts tab of the Preferences dialog).
|
||||
|
||||
Once the project is open, and you're signed into your Apple Developer account,
|
||||
select the root node of the project tree (labeled "iOSTestbed"), then the
|
||||
"Signing & Capabilities" tab in the details page. Select a development team
|
||||
(this will likely be your own name), and plug in a physical device to your
|
||||
macOS machine with a USB cable. You should then be able to select your physical
|
||||
device from the list of targets in the pulldown in the Xcode titlebar.
|
2
Apple/iOS/Resources/bin/arm64-apple-ios-ar
Executable file
2
Apple/iOS/Resources/bin/arm64-apple-ios-ar
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphoneos${IOS_SDK_VERSION} ar "$@"
|
2
Apple/iOS/Resources/bin/arm64-apple-ios-clang
Executable file
2
Apple/iOS/Resources/bin/arm64-apple-ios-clang
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@"
|
2
Apple/iOS/Resources/bin/arm64-apple-ios-clang++
Executable file
2
Apple/iOS/Resources/bin/arm64-apple-ios-clang++
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphoneos${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} "$@"
|
2
Apple/iOS/Resources/bin/arm64-apple-ios-cpp
Executable file
2
Apple/iOS/Resources/bin/arm64-apple-ios-cpp
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET} -E "$@"
|
2
Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar
Executable file
2
Apple/iOS/Resources/bin/arm64-apple-ios-simulator-ar
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@"
|
2
Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang
Executable file
2
Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"
|
2
Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++
Executable file
2
Apple/iOS/Resources/bin/arm64-apple-ios-simulator-clang++
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"
|
2
Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp
Executable file
2
Apple/iOS/Resources/bin/arm64-apple-ios-simulator-cpp
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@"
|
2
Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip
Executable file
2
Apple/iOS/Resources/bin/arm64-apple-ios-simulator-strip
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch arm64 "$@"
|
2
Apple/iOS/Resources/bin/arm64-apple-ios-strip
Executable file
2
Apple/iOS/Resources/bin/arm64-apple-ios-strip
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphoneos${IOS_SDK_VERSION} strip -arch arm64 "$@"
|
2
Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar
Executable file
2
Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-ar
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} ar "$@"
|
2
Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang
Executable file
2
Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"
|
2
Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++
Executable file
2
Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator "$@"
|
2
Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp
Executable file
2
Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios${IPHONEOS_DEPLOYMENT_TARGET}-simulator -E "$@"
|
2
Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip
Executable file
2
Apple/iOS/Resources/bin/x86_64-apple-ios-simulator-strip
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
xcrun --sdk iphonesimulator${IOS_SDK_VERSION} strip -arch x86_64 "$@"
|
|
@ -19,7 +19,7 @@
|
|||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<string>13.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
137
Apple/testbed/Python.xcframework/build/utils.sh
Executable file
137
Apple/testbed/Python.xcframework/build/utils.sh
Executable file
|
@ -0,0 +1,137 @@
|
|||
# Utility methods for use in an Xcode project.
|
||||
#
|
||||
# An iOS XCframework cannot include any content other than the library binary
|
||||
# and relevant metadata. However, Python requires a standard library at runtime.
|
||||
# Therefore, it is necessary to add a build step to an Xcode app target that
|
||||
# processes the standard library and puts the content into the final app.
|
||||
#
|
||||
# In general, these tools will be invoked after bundle resources have been
|
||||
# copied into the app, but before framework embedding (and signing).
|
||||
#
|
||||
# The following is an example script, assuming that:
|
||||
# * Python.xcframework is in the root of the project
|
||||
# * There is an `app` folder that contains the app code
|
||||
# * There is an `app_packages` folder that contains installed Python packages.
|
||||
# -----
|
||||
# set -e
|
||||
# source $PROJECT_DIR/Python.xcframework/build/build_utils.sh
|
||||
# install_python Python.xcframework app app_packages
|
||||
# -----
|
||||
|
||||
# Copy the standard library from the XCframework into the app bundle.
|
||||
#
|
||||
# Accepts one argument:
|
||||
# 1. The path, relative to the root of the Xcode project, where the Python
|
||||
# XCframework can be found.
|
||||
install_stdlib() {
|
||||
PYTHON_XCFRAMEWORK_PATH=$1
|
||||
|
||||
mkdir -p "$CODESIGNING_FOLDER_PATH/python/lib"
|
||||
if [ "$EFFECTIVE_PLATFORM_NAME" = "-iphonesimulator" ]; then
|
||||
echo "Installing Python modules for iOS Simulator"
|
||||
if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/ios-arm64-simulator" ]; then
|
||||
SLICE_FOLDER="ios-arm64-simulator"
|
||||
else
|
||||
SLICE_FOLDER="ios-arm64_x86_64-simulator"
|
||||
fi
|
||||
else
|
||||
echo "Installing Python modules for iOS Device"
|
||||
SLICE_FOLDER="ios-arm64"
|
||||
fi
|
||||
|
||||
# If the XCframework has a shared lib folder, then it's a full framework.
|
||||
# Copy both the common and slice-specific part of the lib directory.
|
||||
# Otherwise, it's a single-arch framework; use the "full" lib folder.
|
||||
if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib" ]; then
|
||||
rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/"
|
||||
rsync -au "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib-$ARCHS/" "$CODESIGNING_FOLDER_PATH/python/lib/"
|
||||
else
|
||||
rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/"
|
||||
fi
|
||||
}
|
||||
|
||||
# Convert a single .so library into a framework that iOS can load.
|
||||
#
|
||||
# Accepts three arguments:
|
||||
# 1. The path, relative to the root of the Xcode project, where the Python
|
||||
# XCframework can be found.
|
||||
# 2. The base path, relative to the installed location in the app bundle, that
|
||||
# needs to be processed. Any .so file found in this path (or a subdirectory
|
||||
# of it) will be processed.
|
||||
# 2. The full path to a single .so file to process. This path should include
|
||||
# the base path.
|
||||
install_dylib () {
|
||||
PYTHON_XCFRAMEWORK_PATH=$1
|
||||
INSTALL_BASE=$2
|
||||
FULL_EXT=$3
|
||||
|
||||
# The name of the extension file
|
||||
EXT=$(basename "$FULL_EXT")
|
||||
# The location of the extension file, relative to the bundle
|
||||
RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/}
|
||||
# The path to the extension file, relative to the install base
|
||||
PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/}
|
||||
# The full dotted name of the extension module, constructed from the file path.
|
||||
FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d "." -f 1 | tr "/" ".");
|
||||
# A bundle identifier; not actually used, but required by Xcode framework packaging
|
||||
FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr "_" "-")
|
||||
# The name of the framework folder.
|
||||
FRAMEWORK_FOLDER="Frameworks/$FULL_MODULE_NAME.framework"
|
||||
|
||||
# If the framework folder doesn't exist, create it.
|
||||
if [ ! -d "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ]; then
|
||||
echo "Creating framework for $RELATIVE_EXT"
|
||||
mkdir -p "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER"
|
||||
cp "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/build/$PLATFORM_FAMILY_NAME-dylib-Info-template.plist" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
|
||||
plutil -replace CFBundleExecutable -string "$FULL_MODULE_NAME" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
|
||||
plutil -replace CFBundleIdentifier -string "$FRAMEWORK_BUNDLE_ID" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
|
||||
fi
|
||||
|
||||
echo "Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME"
|
||||
mv "$FULL_EXT" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME"
|
||||
# Create a placeholder .fwork file where the .so was
|
||||
echo "$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" > ${FULL_EXT%.so}.fwork
|
||||
# Create a back reference to the .so file location in the framework
|
||||
echo "${RELATIVE_EXT%.so}.fwork" > "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin"
|
||||
|
||||
echo "Signing framework as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)..."
|
||||
/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER"
|
||||
}
|
||||
|
||||
# Process all the dynamic libraries in a path into Framework format.
|
||||
#
|
||||
# Accepts two arguments:
|
||||
# 1. The path, relative to the root of the Xcode project, where the Python
|
||||
# XCframework can be found.
|
||||
# 2. The base path, relative to the installed location in the app bundle, that
|
||||
# needs to be processed. Any .so file found in this path (or a subdirectory
|
||||
# of it) will be processed.
|
||||
process_dylibs () {
|
||||
PYTHON_XCFRAMEWORK_PATH=$1
|
||||
LIB_PATH=$2
|
||||
find "$CODESIGNING_FOLDER_PATH/$LIB_PATH" -name "*.so" | while read FULL_EXT; do
|
||||
install_dylib $PYTHON_XCFRAMEWORK_PATH "$LIB_PATH/" "$FULL_EXT"
|
||||
done
|
||||
}
|
||||
|
||||
# The entry point for post-processing a Python XCframework.
|
||||
#
|
||||
# Accepts 1 or more arguments:
|
||||
# 1. The path, relative to the root of the Xcode project, where the Python
|
||||
# XCframework can be found. If the XCframework is in the root of the project,
|
||||
# 2+. The path of a package, relative to the root of the packaged app, that contains
|
||||
# library content that should be processed for binary libraries.
|
||||
install_python() {
|
||||
PYTHON_XCFRAMEWORK_PATH=$1
|
||||
shift
|
||||
|
||||
install_stdlib $PYTHON_XCFRAMEWORK_PATH
|
||||
PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib")
|
||||
echo "Install Python $PYTHON_VER standard library extension modules..."
|
||||
process_dylibs $PYTHON_XCFRAMEWORK_PATH python/lib/$PYTHON_VER/lib-dynload
|
||||
|
||||
for package_path in $@; do
|
||||
echo "Installing $package_path extension modules ..."
|
||||
process_dylibs $PYTHON_XCFRAMEWORK_PATH $package_path
|
||||
done
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
#import <XCTest/XCTest.h>
|
||||
#import <Python/Python.h>
|
||||
|
||||
@interface iOSTestbedTests : XCTestCase
|
||||
@interface TestbedTests : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation iOSTestbedTests
|
||||
@implementation TestbedTests
|
||||
|
||||
|
||||
- (void)testPython {
|
||||
|
@ -41,14 +41,14 @@
|
|||
// The processInfo arguments contain the binary that is running,
|
||||
// followed by the arguments defined in the test plan. This means:
|
||||
// run_module = test_args[1]
|
||||
// argv = ["iOSTestbed"] + test_args[2:]
|
||||
// argv = ["Testbed"] + test_args[2:]
|
||||
test_args = [[NSProcessInfo processInfo] arguments];
|
||||
if (test_args == NULL) {
|
||||
NSLog(@"Unable to identify test arguments.");
|
||||
}
|
||||
NSLog(@"Test arguments: %@", test_args);
|
||||
argv = malloc(sizeof(char *) * ([test_args count] - 1));
|
||||
argv[0] = "iOSTestbed";
|
||||
argv[0] = "Testbed";
|
||||
for (int i = 1; i < [test_args count] - 1; i++) {
|
||||
argv[i] = [[test_args objectAtIndex:i+1] UTF8String];
|
||||
}
|
|
@ -6,6 +6,9 @@
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TEST_SLICES = {
|
||||
"iOS": "ios-arm64_x86_64-simulator",
|
||||
}
|
||||
|
||||
DECODE_ARGS = ("UTF-8", "backslashreplace")
|
||||
|
||||
|
@ -21,45 +24,49 @@
|
|||
|
||||
|
||||
# Select a simulator device to use.
|
||||
def select_simulator_device():
|
||||
def select_simulator_device(platform):
|
||||
# List the testing simulators, in JSON format
|
||||
raw_json = subprocess.check_output(["xcrun", "simctl", "list", "-j"])
|
||||
json_data = json.loads(raw_json)
|
||||
|
||||
# Any device will do; we'll look for "SE" devices - but the name isn't
|
||||
# consistent over time. Older Xcode versions will use "iPhone SE (Nth
|
||||
# generation)"; As of 2025, they've started using "iPhone 16e".
|
||||
#
|
||||
# When Xcode is updated after a new release, new devices will be available
|
||||
# and old ones will be dropped from the set available on the latest iOS
|
||||
# version. Select the one with the highest minimum runtime version - this
|
||||
# is an indicator of the "newest" released device, which should always be
|
||||
# supported on the "most recent" iOS version.
|
||||
se_simulators = sorted(
|
||||
(devicetype["minRuntimeVersion"], devicetype["name"])
|
||||
for devicetype in json_data["devicetypes"]
|
||||
if devicetype["productFamily"] == "iPhone"
|
||||
and (
|
||||
(
|
||||
"iPhone " in devicetype["name"]
|
||||
and devicetype["name"].endswith("e")
|
||||
if platform == "iOS":
|
||||
# Any iOS device will do; we'll look for "SE" devices - but the name isn't
|
||||
# consistent over time. Older Xcode versions will use "iPhone SE (Nth
|
||||
# generation)"; As of 2025, they've started using "iPhone 16e".
|
||||
#
|
||||
# When Xcode is updated after a new release, new devices will be available
|
||||
# and old ones will be dropped from the set available on the latest iOS
|
||||
# version. Select the one with the highest minimum runtime version - this
|
||||
# is an indicator of the "newest" released device, which should always be
|
||||
# supported on the "most recent" iOS version.
|
||||
se_simulators = sorted(
|
||||
(devicetype["minRuntimeVersion"], devicetype["name"])
|
||||
for devicetype in json_data["devicetypes"]
|
||||
if devicetype["productFamily"] == "iPhone"
|
||||
and (
|
||||
(
|
||||
"iPhone " in devicetype["name"]
|
||||
and devicetype["name"].endswith("e")
|
||||
)
|
||||
or "iPhone SE " in devicetype["name"]
|
||||
)
|
||||
or "iPhone SE " in devicetype["name"]
|
||||
)
|
||||
)
|
||||
simulator = se_simulators[-1][1]
|
||||
else:
|
||||
raise ValueError(f"Unknown platform {platform}")
|
||||
|
||||
return se_simulators[-1][1]
|
||||
return simulator
|
||||
|
||||
|
||||
def xcode_test(location, simulator, verbose):
|
||||
def xcode_test(location: Path, platform: str, simulator: str, verbose: bool):
|
||||
# Build and run the test suite on the named simulator.
|
||||
args = [
|
||||
"-project",
|
||||
str(location / "iOSTestbed.xcodeproj"),
|
||||
str(location / f"{platform}Testbed.xcodeproj"),
|
||||
"-scheme",
|
||||
"iOSTestbed",
|
||||
f"{platform}Testbed",
|
||||
"-destination",
|
||||
f"platform=iOS Simulator,name={simulator}",
|
||||
f"platform={platform} Simulator,name={simulator}",
|
||||
"-derivedDataPath",
|
||||
str(location / "DerivedData"),
|
||||
]
|
||||
|
@ -89,10 +96,24 @@ def xcode_test(location, simulator, verbose):
|
|||
exit(status)
|
||||
|
||||
|
||||
def copy(src, tgt):
|
||||
"""An all-purpose copy.
|
||||
|
||||
If src is a file, it is copied. If src is a symlink, it is copied *as a
|
||||
symlink*. If src is a directory, the full tree is duplicated, with symlinks
|
||||
being preserved.
|
||||
"""
|
||||
if src.is_file() or src.is_symlink():
|
||||
shutil.copyfile(src, tgt, follow_symlinks=False)
|
||||
else:
|
||||
shutil.copytree(src, tgt, symlinks=True)
|
||||
|
||||
|
||||
def clone_testbed(
|
||||
source: Path,
|
||||
target: Path,
|
||||
framework: Path,
|
||||
platform: str,
|
||||
apps: list[Path],
|
||||
) -> None:
|
||||
if target.exists():
|
||||
|
@ -101,11 +122,11 @@ def clone_testbed(
|
|||
|
||||
if framework is None:
|
||||
if not (
|
||||
source / "Python.xcframework/ios-arm64_x86_64-simulator/bin"
|
||||
source / "Python.xcframework" / TEST_SLICES[platform] / "bin"
|
||||
).is_dir():
|
||||
print(
|
||||
f"The testbed being cloned ({source}) does not contain "
|
||||
f"a simulator framework. Re-run with --framework"
|
||||
"a framework with slices. Re-run with --framework"
|
||||
)
|
||||
sys.exit(11)
|
||||
else:
|
||||
|
@ -124,33 +145,49 @@ def clone_testbed(
|
|||
|
||||
print("Cloning testbed project:")
|
||||
print(f" Cloning {source}...", end="")
|
||||
shutil.copytree(source, target, symlinks=True)
|
||||
# Only copy the files for the platform being cloned plus the files common
|
||||
# to all platforms. The XCframework will be copied later, if needed.
|
||||
target.mkdir(parents=True)
|
||||
|
||||
for name in [
|
||||
"__main__.py",
|
||||
"TestbedTests",
|
||||
"Testbed.lldbinit",
|
||||
f"{platform}Testbed",
|
||||
f"{platform}Testbed.xcodeproj",
|
||||
f"{platform}Testbed.xctestplan",
|
||||
]:
|
||||
copy(source / name, target / name)
|
||||
|
||||
print(" done")
|
||||
|
||||
orig_xc_framework_path = source / "Python.xcframework"
|
||||
xc_framework_path = target / "Python.xcframework"
|
||||
sim_framework_path = xc_framework_path / "ios-arm64_x86_64-simulator"
|
||||
test_framework_path = xc_framework_path / TEST_SLICES[platform]
|
||||
if framework is not None:
|
||||
if framework.suffix == ".xcframework":
|
||||
print(" Installing XCFramework...", end="")
|
||||
if xc_framework_path.is_dir():
|
||||
shutil.rmtree(xc_framework_path)
|
||||
else:
|
||||
xc_framework_path.unlink(missing_ok=True)
|
||||
xc_framework_path.symlink_to(
|
||||
framework.relative_to(xc_framework_path.parent, walk_up=True)
|
||||
)
|
||||
print(" done")
|
||||
else:
|
||||
print(" Installing simulator framework...", end="")
|
||||
if sim_framework_path.is_dir():
|
||||
shutil.rmtree(sim_framework_path)
|
||||
# We're only installing a slice of a framework; we need
|
||||
# to do a full tree copy to make sure we don't damage
|
||||
# symlinked content.
|
||||
shutil.copytree(orig_xc_framework_path, xc_framework_path)
|
||||
if test_framework_path.is_dir():
|
||||
shutil.rmtree(test_framework_path)
|
||||
else:
|
||||
sim_framework_path.unlink(missing_ok=True)
|
||||
sim_framework_path.symlink_to(
|
||||
framework.relative_to(sim_framework_path.parent, walk_up=True)
|
||||
test_framework_path.unlink(missing_ok=True)
|
||||
test_framework_path.symlink_to(
|
||||
framework.relative_to(test_framework_path.parent, walk_up=True)
|
||||
)
|
||||
print(" done")
|
||||
else:
|
||||
copy(orig_xc_framework_path, xc_framework_path)
|
||||
|
||||
if (
|
||||
xc_framework_path.is_symlink()
|
||||
and not xc_framework_path.readlink().is_absolute()
|
||||
|
@ -158,39 +195,39 @@ def clone_testbed(
|
|||
# XCFramework is a relative symlink. Rewrite the symlink relative
|
||||
# to the new location.
|
||||
print(" Rewriting symlink to XCframework...", end="")
|
||||
orig_xc_framework_path = (
|
||||
resolved_xc_framework_path = (
|
||||
source / xc_framework_path.readlink()
|
||||
).resolve()
|
||||
xc_framework_path.unlink()
|
||||
xc_framework_path.symlink_to(
|
||||
orig_xc_framework_path.relative_to(
|
||||
resolved_xc_framework_path.relative_to(
|
||||
xc_framework_path.parent, walk_up=True
|
||||
)
|
||||
)
|
||||
print(" done")
|
||||
elif (
|
||||
sim_framework_path.is_symlink()
|
||||
and not sim_framework_path.readlink().is_absolute()
|
||||
test_framework_path.is_symlink()
|
||||
and not test_framework_path.readlink().is_absolute()
|
||||
):
|
||||
print(" Rewriting symlink to simulator framework...", end="")
|
||||
# Simulator framework is a relative symlink. Rewrite the symlink
|
||||
# relative to the new location.
|
||||
orig_sim_framework_path = (
|
||||
source / "Python.XCframework" / sim_framework_path.readlink()
|
||||
orig_test_framework_path = (
|
||||
source / "Python.XCframework" / test_framework_path.readlink()
|
||||
).resolve()
|
||||
sim_framework_path.unlink()
|
||||
sim_framework_path.symlink_to(
|
||||
orig_sim_framework_path.relative_to(
|
||||
sim_framework_path.parent, walk_up=True
|
||||
test_framework_path.unlink()
|
||||
test_framework_path.symlink_to(
|
||||
orig_test_framework_path.relative_to(
|
||||
test_framework_path.parent, walk_up=True
|
||||
)
|
||||
)
|
||||
print(" done")
|
||||
else:
|
||||
print(" Using pre-existing iOS framework.")
|
||||
print(" Using pre-existing Python framework.")
|
||||
|
||||
for app_src in apps:
|
||||
print(f" Installing app {app_src.name!r}...", end="")
|
||||
app_target = target / f"iOSTestbed/app/{app_src.name}"
|
||||
app_target = target / f"Testbed/app/{app_src.name}"
|
||||
if app_target.is_dir():
|
||||
shutil.rmtree(app_target)
|
||||
shutil.copytree(app_src, app_target)
|
||||
|
@ -199,9 +236,9 @@ def clone_testbed(
|
|||
print(f"Successfully cloned testbed: {target.resolve()}")
|
||||
|
||||
|
||||
def update_test_plan(testbed_path, args):
|
||||
def update_test_plan(testbed_path, platform, args):
|
||||
# Modify the test plan to use the requested test arguments.
|
||||
test_plan_path = testbed_path / "iOSTestbed.xctestplan"
|
||||
test_plan_path = testbed_path / f"{platform}Testbed.xctestplan"
|
||||
with test_plan_path.open("r", encoding="utf-8") as f:
|
||||
test_plan = json.load(f)
|
||||
|
||||
|
@ -213,32 +250,50 @@ def update_test_plan(testbed_path, args):
|
|||
json.dump(test_plan, f, indent=2)
|
||||
|
||||
|
||||
def run_testbed(simulator: str | None, args: list[str], verbose: bool = False):
|
||||
def run_testbed(
|
||||
platform: str,
|
||||
simulator: str | None,
|
||||
args: list[str],
|
||||
verbose: bool = False,
|
||||
):
|
||||
location = Path(__file__).parent
|
||||
print("Updating test plan...", end="")
|
||||
update_test_plan(location, args)
|
||||
update_test_plan(location, platform, args)
|
||||
print(" done.")
|
||||
|
||||
if simulator is None:
|
||||
simulator = select_simulator_device()
|
||||
simulator = select_simulator_device(platform)
|
||||
print(f"Running test on {simulator}")
|
||||
|
||||
xcode_test(location, simulator=simulator, verbose=verbose)
|
||||
xcode_test(
|
||||
location,
|
||||
platform=platform,
|
||||
simulator=simulator,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
# Look for directories like `iOSTestbed` as an indicator of the platforms
|
||||
# that the testbed folder supports. The original source testbed can support
|
||||
# many platforms, but when cloned, only one platform is preserved.
|
||||
available_platforms = [
|
||||
platform
|
||||
for platform in ["iOS"]
|
||||
if (Path(__file__).parent / f"{platform}Testbed").is_dir()
|
||||
]
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Manages the process of testing a Python project in the iOS simulator."
|
||||
"Manages the process of testing an Apple Python project through Xcode."
|
||||
),
|
||||
)
|
||||
|
||||
subcommands = parser.add_subparsers(dest="subcommand")
|
||||
|
||||
clone = subcommands.add_parser(
|
||||
"clone",
|
||||
description=(
|
||||
"Clone the testbed project, copying in an iOS Python framework and"
|
||||
"Clone the testbed project, copying in a Python framework and"
|
||||
"any specified application code."
|
||||
),
|
||||
help="Clone a testbed project to a new location.",
|
||||
|
@ -250,6 +305,13 @@ def main():
|
|||
"XCFramework) to use when running the testbed"
|
||||
),
|
||||
)
|
||||
clone.add_argument(
|
||||
"--platform",
|
||||
dest="platform",
|
||||
choices=available_platforms,
|
||||
default=available_platforms[0],
|
||||
help=f"The platform to target (default: {available_platforms[0]})",
|
||||
)
|
||||
clone.add_argument(
|
||||
"--app",
|
||||
dest="apps",
|
||||
|
@ -272,6 +334,13 @@ def main():
|
|||
),
|
||||
help="Run a testbed project",
|
||||
)
|
||||
run.add_argument(
|
||||
"--platform",
|
||||
dest="platform",
|
||||
choices=available_platforms,
|
||||
default=available_platforms[0],
|
||||
help=f"The platform to target (default: {available_platforms[0]})",
|
||||
)
|
||||
run.add_argument(
|
||||
"--simulator",
|
||||
help=(
|
||||
|
@ -306,22 +375,26 @@ def main():
|
|||
framework=Path(context.framework).resolve()
|
||||
if context.framework
|
||||
else None,
|
||||
platform=context.platform,
|
||||
apps=[Path(app) for app in context.apps],
|
||||
)
|
||||
elif context.subcommand == "run":
|
||||
if test_args:
|
||||
if not (
|
||||
Path(__file__).parent
|
||||
/ "Python.xcframework/ios-arm64_x86_64-simulator/bin"
|
||||
/ "Python.xcframework"
|
||||
/ TEST_SLICES[context.platform]
|
||||
/ "bin"
|
||||
).is_dir():
|
||||
print(
|
||||
f"Testbed does not contain a compiled iOS framework. Use "
|
||||
f"Testbed does not contain a compiled Python framework. Use "
|
||||
f"`python {sys.argv[0]} clone ...` to create a runnable "
|
||||
f"clone of this testbed."
|
||||
)
|
||||
sys.exit(20)
|
||||
|
||||
run_testbed(
|
||||
platform=context.platform,
|
||||
simulator=context.simulator,
|
||||
verbose=context.verbose,
|
||||
args=test_args,
|
|
@ -11,12 +11,11 @@
|
|||
607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607A66212B0EFA390010BFC8 /* Assets.xcassets */; };
|
||||
607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607A66232B0EFA390010BFC8 /* LaunchScreen.storyboard */; };
|
||||
607A66282B0EFA390010BFC8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66272B0EFA390010BFC8 /* main.m */; };
|
||||
607A66322B0EFA3A0010BFC8 /* iOSTestbedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66312B0EFA3A0010BFC8 /* iOSTestbedTests.m */; };
|
||||
607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 607A66312B0EFA3A0010BFC8 /* TestbedTests.m */; };
|
||||
607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; };
|
||||
607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; };
|
||||
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 */
|
||||
|
@ -64,9 +63,8 @@
|
|||
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>"; };
|
||||
607A66312B0EFA3A0010BFC8 /* TestbedTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestbedTests.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>"; };
|
||||
|
@ -99,7 +97,7 @@
|
|||
60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */,
|
||||
607A664A2B0EFB310010BFC8 /* Python.xcframework */,
|
||||
607A66142B0EFA380010BFC8 /* iOSTestbed */,
|
||||
607A66302B0EFA3A0010BFC8 /* iOSTestbedTests */,
|
||||
607A66302B0EFA3A0010BFC8 /* TestbedTests */,
|
||||
607A66132B0EFA380010BFC8 /* Products */,
|
||||
607A664F2B0EFFE00010BFC8 /* Frameworks */,
|
||||
);
|
||||
|
@ -120,7 +118,6 @@
|
|||
608619552CB7819B00F46182 /* app */,
|
||||
608619532CB77BA900F46182 /* app_packages */,
|
||||
607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */,
|
||||
607A66572B0F079F0010BFC8 /* dylib-Info-template.plist */,
|
||||
607A66152B0EFA380010BFC8 /* AppDelegate.h */,
|
||||
607A66162B0EFA380010BFC8 /* AppDelegate.m */,
|
||||
607A66212B0EFA390010BFC8 /* Assets.xcassets */,
|
||||
|
@ -130,12 +127,12 @@
|
|||
path = iOSTestbed;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607A66302B0EFA3A0010BFC8 /* iOSTestbedTests */ = {
|
||||
607A66302B0EFA3A0010BFC8 /* TestbedTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
607A66312B0EFA3A0010BFC8 /* iOSTestbedTests.m */,
|
||||
607A66312B0EFA3A0010BFC8 /* TestbedTests.m */,
|
||||
);
|
||||
path = iOSTestbedTests;
|
||||
path = TestbedTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
607A664F2B0EFFE00010BFC8 /* Frameworks */ = {
|
||||
|
@ -155,8 +152,7 @@
|
|||
607A660E2B0EFA380010BFC8 /* Sources */,
|
||||
607A660F2B0EFA380010BFC8 /* Frameworks */,
|
||||
607A66102B0EFA380010BFC8 /* Resources */,
|
||||
607A66552B0F061D0010BFC8 /* Install Target Specific Python Standard Library */,
|
||||
607A66562B0F06200010BFC8 /* Prepare Python Binary Modules */,
|
||||
607A66552B0F061D0010BFC8 /* Process Python libraries */,
|
||||
607A664E2B0EFC080010BFC8 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
|
@ -230,7 +226,6 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
607A66252B0EFA390010BFC8 /* LaunchScreen.storyboard in Resources */,
|
||||
607A66582B0F079F0010BFC8 /* dylib-Info-template.plist in Resources */,
|
||||
608619562CB7819B00F46182 /* app in Resources */,
|
||||
607A66222B0EFA390010BFC8 /* Assets.xcassets in Resources */,
|
||||
608619542CB77BA900F46182 /* app_packages in Resources */,
|
||||
|
@ -247,7 +242,7 @@
|
|||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
607A66552B0F061D0010BFC8 /* Install Target Specific Python Standard Library */ = {
|
||||
607A66552B0F061D0010BFC8 /* Process Python libraries */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -257,34 +252,14 @@
|
|||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Install Target Specific Python Standard Library";
|
||||
name = "Process Python libraries";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\n\nmkdir -p \"$CODESIGNING_FOLDER_PATH/python/lib\"\nif [ \"$EFFECTIVE_PLATFORM_NAME\" = \"-iphonesimulator\" ]; then\n echo \"Installing Python modules for iOS Simulator\"\n rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64_x86_64-simulator/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\" \nelse\n echo \"Installing Python modules for iOS Device\"\n rsync -au --delete \"$PROJECT_DIR/Python.xcframework/ios-arm64/lib/\" \"$CODESIGNING_FOLDER_PATH/python/lib/\" \nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
607A66562B0F06200010BFC8 /* Prepare Python Binary Modules */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Prepare Python Binary Modules";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\n\ninstall_dylib () {\n INSTALL_BASE=$1\n FULL_EXT=$2\n\n # The name of the extension file\n EXT=$(basename \"$FULL_EXT\")\n # The location of the extension file, relative to the bundle\n RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} \n # The path to the extension file, relative to the install base\n PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/}\n # The full dotted name of the extension module, constructed from the file path.\n FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d \".\" -f 1 | tr \"/\" \".\"); \n # A bundle identifier; not actually used, but required by Xcode framework packaging\n FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr \"_\" \"-\")\n # The name of the framework folder.\n FRAMEWORK_FOLDER=\"Frameworks/$FULL_MODULE_NAME.framework\"\n\n # If the framework folder doesn't exist, create it.\n if [ ! -d \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\" ]; then\n echo \"Creating framework for $RELATIVE_EXT\" \n mkdir -p \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER\"\n cp \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n plutil -replace CFBundleExecutable -string \"$FULL_MODULE_NAME\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n plutil -replace CFBundleIdentifier -string \"$FRAMEWORK_BUNDLE_ID\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist\"\n fi\n \n echo \"Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" \n mv \"$FULL_EXT\" \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\"\n # Create a placeholder .fwork file where the .so was\n echo \"$FRAMEWORK_FOLDER/$FULL_MODULE_NAME\" > ${FULL_EXT%.so}.fwork\n # Create a back reference to the .so file location in the framework\n echo \"${RELATIVE_EXT%.so}.fwork\" > \"$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin\" \n}\n\nPYTHON_VER=$(ls -1 \"$CODESIGNING_FOLDER_PATH/python/lib\")\necho \"Install Python $PYTHON_VER standard library extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/python/lib/$PYTHON_VER/lib-dynload\" -name \"*.so\" | while read FULL_EXT; do\n install_dylib python/lib/$PYTHON_VER/lib-dynload/ \"$FULL_EXT\"\ndone\necho \"Install app package extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/app_packages\" -name \"*.so\" | while read FULL_EXT; do\n install_dylib app_packages/ \"$FULL_EXT\"\ndone\necho \"Install app extension modules...\"\nfind \"$CODESIGNING_FOLDER_PATH/app\" -name \"*.so\" | while read FULL_EXT; do\n install_dylib app/ \"$FULL_EXT\"\ndone\n\n# Clean up dylib template \nrm -f \"$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist\"\necho \"Signing frameworks as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)...\"\nfind \"$CODESIGNING_FOLDER_PATH/Frameworks\" -name \"*.framework\" -exec /usr/bin/codesign --force --sign \"$EXPANDED_CODE_SIGN_IDENTITY\" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der \"{}\" \\; \n";
|
||||
shellScript = "set -e\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\ninstall_python Python.xcframework app app_packages\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
@ -303,7 +278,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
607A66322B0EFA3A0010BFC8 /* iOSTestbedTests.m in Sources */,
|
||||
607A66322B0EFA3A0010BFC8 /* TestbedTests.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
|
@ -27,7 +27,7 @@
|
|||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "/Users/rkm/projects/pyspamsum/localtest/iOSTestbed.lldbinit"
|
||||
customLLDBInitFile = "$(SOURCE_ROOT)/Testbed.lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<TestPlans>
|
||||
<TestPlanReference
|
|
@ -1,7 +1,7 @@
|
|||
This folder can contain any Python application code.
|
||||
|
||||
During the build, any binary modules found in this folder will be processed into
|
||||
iOS Framework form.
|
||||
Framework form.
|
||||
|
||||
When the test suite runs, this folder will be on the PYTHONPATH, and will be the
|
||||
working directory for the test suite.
|
|
@ -2,6 +2,6 @@ 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.
|
||||
Framework form.
|
||||
|
||||
When the test suite runs, this folder will be on the PYTHONPATH.
|
|
@ -170,7 +170,7 @@ helpful.
|
|||
To add Python to an iOS Xcode project:
|
||||
|
||||
1. Build or obtain a Python ``XCFramework``. See the instructions in
|
||||
:source:`iOS/README.rst` (in the CPython source distribution) for details on
|
||||
:source:`Apple/iOS/README.md` (in the CPython source distribution) for details on
|
||||
how to build a Python ``XCFramework``. At a minimum, you will need a build
|
||||
that supports ``arm64-apple-ios``, plus one of either
|
||||
``arm64-apple-ios-simulator`` or ``x86_64-apple-ios-simulator``.
|
||||
|
@ -180,22 +180,19 @@ To add Python to an iOS Xcode project:
|
|||
of your project; however, you can use any other location that you want by
|
||||
adjusting paths as needed.
|
||||
|
||||
3. Drag the ``iOS/Resources/dylib-Info-template.plist`` file into your project,
|
||||
and ensure it is associated with the app target.
|
||||
|
||||
4. Add your application code as a folder in your Xcode project. In the
|
||||
3. Add your application code as a folder in your Xcode project. In the
|
||||
following instructions, we'll assume that your user code is in a folder
|
||||
named ``app`` in the root of your project; you can use any other location by
|
||||
adjusting paths as needed. Ensure that this folder is associated with your
|
||||
app target.
|
||||
|
||||
5. Select the app target by selecting the root node of your Xcode project, then
|
||||
4. Select the app target by selecting the root node of your Xcode project, then
|
||||
the target name in the sidebar that appears.
|
||||
|
||||
6. In the "General" settings, under "Frameworks, Libraries and Embedded
|
||||
5. In the "General" settings, under "Frameworks, Libraries and Embedded
|
||||
Content", add ``Python.xcframework``, with "Embed & Sign" selected.
|
||||
|
||||
7. In the "Build Settings" tab, modify the following:
|
||||
6. In the "Build Settings" tab, modify the following:
|
||||
|
||||
- Build Options
|
||||
|
||||
|
@ -211,86 +208,24 @@ To add Python to an iOS Xcode project:
|
|||
|
||||
* Quoted Include In Framework Header: No
|
||||
|
||||
8. Add a build step that copies the Python standard library into your app. In
|
||||
the "Build Phases" tab, add a new "Run Script" build step *before* the
|
||||
"Embed Frameworks" step, but *after* the "Copy Bundle Resources" step. Name
|
||||
the step "Install Target Specific Python Standard Library", disable the
|
||||
"Based on dependency analysis" checkbox, and set the script content to:
|
||||
7. Add a build step that processes the Python standard library, and your own
|
||||
Python binary dependencies. In the "Build Phases" tab, add a new "Run
|
||||
Script" build step *before* the "Embed Frameworks" step, but *after* the
|
||||
"Copy Bundle Resources" step. Name the step "Process Python libraries",
|
||||
disable the "Based on dependency analysis" checkbox, and set the script
|
||||
content to:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
set -e
|
||||
set -e
|
||||
source $PROJECT_DIR/Python.xcframework/build/build_utils.sh
|
||||
install_python Python.xcframework app
|
||||
|
||||
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
|
||||
If you have placed your XCframework somewhere other than the root of your
|
||||
project, modify the path to the first argument.
|
||||
|
||||
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 Python interpreter in embedded
|
||||
mode. You should ensure that:
|
||||
8. Add Objective C code to initialize and use a Python interpreter in embedded
|
||||
mode. You should ensure that:
|
||||
|
||||
* UTF-8 mode (:c:member:`PyPreConfig.utf8_mode`) is *enabled*;
|
||||
* Buffered stdio (:c:member:`PyConfig.buffered_stdio`) is *disabled*;
|
||||
|
@ -309,22 +244,19 @@ To add Python to an iOS Xcode project:
|
|||
Your app's bundle location can be determined using ``[[NSBundle mainBundle]
|
||||
resourcePath]``.
|
||||
|
||||
Steps 8, 9 and 10 of these instructions assume that you have a single folder of
|
||||
Steps 7 and 8 of these instructions assume that you have a single folder of
|
||||
pure Python application code, named ``app``. If you have third-party binary
|
||||
modules in your app, some additional steps will be required:
|
||||
|
||||
* You need to ensure that any folders containing third-party binaries are
|
||||
either associated with the app target, or copied in as part of step 8. Step 8
|
||||
should also purge any binaries that are not appropriate for the platform a
|
||||
specific build is targeting (i.e., delete any device binaries if you're
|
||||
building an app targeting the simulator).
|
||||
either associated with the app target, or are explicitly copied as part of
|
||||
step 7. Step 7 should also purge any binaries that are not appropriate for
|
||||
the platform a specific build is targeting (i.e., delete any device binaries
|
||||
if you're building an app targeting the simulator).
|
||||
|
||||
* 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 you're using a separate folder for third-party packages, ensure that
|
||||
folder is added to the end of the call to ``install_python`` in step 7, and
|
||||
as part of the :envvar:`PYTHONPATH` configuration in step 8.
|
||||
|
||||
* If any of the folders that contain third-party packages will contain ``.pth``
|
||||
files, you should add that folder as a *site directory* (using
|
||||
|
@ -334,25 +266,30 @@ modules in your app, some additional steps will be required:
|
|||
Testing a Python package
|
||||
------------------------
|
||||
|
||||
The CPython source tree contains :source:`a testbed project <iOS/testbed>` that
|
||||
The CPython source tree contains :source:`a testbed project <Apple/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:
|
||||
After building or obtaining an iOS XCFramework (see :source:`Apple/iOS/README.md`
|
||||
for details), create a clone of the Python iOS testbed project. If you used the
|
||||
``Apple`` build script to build the XCframework, you can run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ python iOS/testbed clone --framework <path/to/Python.xcframework> --app <path/to/module1> --app <path/to/module2> app-testbed
|
||||
$ python cross-build/iOS/testbed clone --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).
|
||||
Or, if you've sourced your own XCframework, by running:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ python Apple/testbed clone --platform iOS --framework <path/to/Python.xcframework> --app <path/to/module1> --app <path/to/module2> app-testbed
|
||||
|
||||
Any folders specified with the ``--app`` flag will be copied into the cloned
|
||||
testbed project. The resulting testbed will be created in the ``app-testbed``
|
||||
folder. In this example, the ``module1`` and ``module2`` would be importable
|
||||
modules at runtime. If your project has additional dependencies, they can be
|
||||
installed into the ``app-testbed/Testbed/app_packages`` folder (using ``pip
|
||||
install --target app-testbed/Testbed/app_packages`` or similar).
|
||||
|
||||
You can then use the ``app-testbed`` folder to run the test suite for your app,
|
||||
For example, if ``module1.tests`` was the entry point to your test suite, you
|
||||
|
@ -381,7 +318,7 @@ tab. Modify the "Arguments Passed On Launch" value to change the testing
|
|||
arguments.
|
||||
|
||||
The test plan also disables parallel testing, and specifies the use of the
|
||||
``iOSTestbed.lldbinit`` file for providing configuration of the debugger. The
|
||||
``Testbed.lldbinit`` file for providing configuration of the debugger. The
|
||||
default debugger configuration disables automatic breakpoints on the
|
||||
``SIGINT``, ``SIGUSR1``, ``SIGUSR2``, and ``SIGXFSZ`` signals.
|
||||
|
||||
|
|
|
@ -2320,7 +2320,7 @@ testios:
|
|||
fi
|
||||
|
||||
# Clone the testbed project into the XCFOLDER
|
||||
$(PYTHON_FOR_BUILD) $(srcdir)/iOS/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)"
|
||||
$(PYTHON_FOR_BUILD) $(srcdir)/Apple/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)"
|
||||
|
||||
# Run the testbed project
|
||||
$(PYTHON_FOR_BUILD) "$(XCFOLDER)" run --verbose -- test -uall --single-process --rerun -W
|
||||
|
@ -3248,10 +3248,10 @@ clean-retain-profile: pycremoval
|
|||
-find build -type f -a ! -name '*.gc??' -exec rm -f {} ';'
|
||||
-rm -f Include/pydtrace_probes.h
|
||||
-rm -f profile-gen-stamp
|
||||
-rm -rf iOS/testbed/Python.xcframework/ios-*/bin
|
||||
-rm -rf iOS/testbed/Python.xcframework/ios-*/lib
|
||||
-rm -rf iOS/testbed/Python.xcframework/ios-*/include
|
||||
-rm -rf iOS/testbed/Python.xcframework/ios-*/Python.framework
|
||||
-rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/bin
|
||||
-rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/lib
|
||||
-rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/include
|
||||
-rm -rf Apple/iOS/testbed/Python.xcframework/ios-*/Python.framework
|
||||
|
||||
.PHONY: profile-removal
|
||||
profile-removal:
|
||||
|
@ -3277,7 +3277,7 @@ clobber: clean
|
|||
config.cache config.log pyconfig.h Modules/config.c
|
||||
-rm -rf build platform
|
||||
-rm -rf $(PYTHONFRAMEWORKDIR)
|
||||
-rm -rf iOS/Frameworks
|
||||
-rm -rf Apple/iOS/Frameworks
|
||||
-rm -rf iOSTestbed.*
|
||||
-rm -f python-config.py python-config
|
||||
-rm -rf cross-build
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
A script for building an iOS XCframework was added. As part of this change,
|
||||
the top level ``iOS`` folder has been moved to be a subdirectory of the
|
||||
``Apple`` folder.
|
8
configure
generated
vendored
8
configure
generated
vendored
|
@ -4358,7 +4358,7 @@ then :
|
|||
yes)
|
||||
case $ac_sys_system in
|
||||
Darwin) enableval=/Library/Frameworks ;;
|
||||
iOS) enableval=iOS/Frameworks/\$\(MULTIARCH\) ;;
|
||||
iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;;
|
||||
*) as_fn_error $? "Unknown platform for framework build" "$LINENO" 5
|
||||
esac
|
||||
esac
|
||||
|
@ -4469,9 +4469,9 @@ then :
|
|||
|
||||
prefix=$PYTHONFRAMEWORKPREFIX
|
||||
PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR"
|
||||
RESSRCDIR=iOS/Resources
|
||||
RESSRCDIR=Apple/iOS/Resources
|
||||
|
||||
ac_config_files="$ac_config_files iOS/Resources/Info.plist"
|
||||
ac_config_files="$ac_config_files Apple/iOS/Resources/Info.plist"
|
||||
|
||||
;;
|
||||
*)
|
||||
|
@ -35232,7 +35232,7 @@ do
|
|||
"Mac/PythonLauncher/Makefile") CONFIG_FILES="$CONFIG_FILES Mac/PythonLauncher/Makefile" ;;
|
||||
"Mac/Resources/framework/Info.plist") CONFIG_FILES="$CONFIG_FILES Mac/Resources/framework/Info.plist" ;;
|
||||
"Mac/Resources/app/Info.plist") CONFIG_FILES="$CONFIG_FILES Mac/Resources/app/Info.plist" ;;
|
||||
"iOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES iOS/Resources/Info.plist" ;;
|
||||
"Apple/iOS/Resources/Info.plist") CONFIG_FILES="$CONFIG_FILES Apple/iOS/Resources/Info.plist" ;;
|
||||
"Makefile.pre") CONFIG_FILES="$CONFIG_FILES Makefile.pre" ;;
|
||||
"Misc/python.pc") CONFIG_FILES="$CONFIG_FILES Misc/python.pc" ;;
|
||||
"Misc/python-embed.pc") CONFIG_FILES="$CONFIG_FILES Misc/python-embed.pc" ;;
|
||||
|
|
|
@ -559,7 +559,7 @@ AC_ARG_ENABLE([framework],
|
|||
yes)
|
||||
case $ac_sys_system in
|
||||
Darwin) enableval=/Library/Frameworks ;;
|
||||
iOS) enableval=iOS/Frameworks/\$\(MULTIARCH\) ;;
|
||||
iOS) enableval=Apple/iOS/Frameworks/\$\(MULTIARCH\) ;;
|
||||
*) AC_MSG_ERROR([Unknown platform for framework build])
|
||||
esac
|
||||
esac
|
||||
|
@ -666,9 +666,9 @@ AC_ARG_ENABLE([framework],
|
|||
|
||||
prefix=$PYTHONFRAMEWORKPREFIX
|
||||
PYTHONFRAMEWORKINSTALLNAMEPREFIX="@rpath/$PYTHONFRAMEWORKDIR"
|
||||
RESSRCDIR=iOS/Resources
|
||||
RESSRCDIR=Apple/iOS/Resources
|
||||
|
||||
AC_CONFIG_FILES([iOS/Resources/Info.plist])
|
||||
AC_CONFIG_FILES([Apple/iOS/Resources/Info.plist])
|
||||
;;
|
||||
*)
|
||||
AC_MSG_ERROR([Unknown platform for framework build])
|
||||
|
|
352
iOS/README.rst
352
iOS/README.rst
|
@ -1,352 +0,0 @@
|
|||
====================
|
||||
Python on iOS README
|
||||
====================
|
||||
|
||||
:Authors:
|
||||
Russell Keith-Magee (2023-11)
|
||||
|
||||
This document provides a quick overview of some iOS specific features in the
|
||||
Python distribution.
|
||||
|
||||
These instructions are only needed if you're planning to compile Python for iOS
|
||||
yourself. Most users should *not* need to do this. If you're looking to
|
||||
experiment with writing an iOS app in Python, tools such as `BeeWare's Briefcase
|
||||
<https://briefcase.readthedocs.io>`__ and `Kivy's Buildozer
|
||||
<https://buildozer.readthedocs.io>`__ will provide a much more approachable
|
||||
user experience.
|
||||
|
||||
Compilers for building on iOS
|
||||
=============================
|
||||
|
||||
Building for iOS requires the use of Apple's Xcode tooling. It is strongly
|
||||
recommended that you use the most recent stable release of Xcode. This will
|
||||
require the use of the most (or second-most) recently released macOS version,
|
||||
as Apple does not maintain Xcode for older macOS versions. The Xcode Command
|
||||
Line Tools are not sufficient for iOS development; you need a *full* Xcode
|
||||
install.
|
||||
|
||||
If you want to run your code on the iOS simulator, you'll also need to install
|
||||
an iOS Simulator Platform. You should be prompted to select an iOS Simulator
|
||||
Platform when you first run Xcode. Alternatively, you can add an iOS Simulator
|
||||
Platform by selecting an open the Platforms tab of the Xcode Settings panel.
|
||||
|
||||
iOS specific arguments to configure
|
||||
===================================
|
||||
|
||||
* ``--enable-framework[=DIR]``
|
||||
|
||||
This argument specifies the location where the Python.framework will be
|
||||
installed. If ``DIR`` is not specified, the framework will be installed into
|
||||
a subdirectory of the ``iOS/Frameworks`` folder.
|
||||
|
||||
This argument *must* be provided when configuring iOS builds. iOS does not
|
||||
support non-framework builds.
|
||||
|
||||
* ``--with-framework-name=NAME``
|
||||
|
||||
Specify the name for the Python framework; defaults to ``Python``.
|
||||
|
||||
.. admonition:: Use this option with care!
|
||||
|
||||
Unless you know what you're doing, changing the name of the Python
|
||||
framework on iOS is not advised. If you use this option, you won't be able
|
||||
to run the ``make testios`` target without making significant manual
|
||||
alterations, and you won't be able to use any binary packages unless you
|
||||
compile them yourself using your own framework name.
|
||||
|
||||
Building Python on iOS
|
||||
======================
|
||||
|
||||
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 ``Python.framework`` that supports a
|
||||
*single* ABI with a *single* architecture. Unlike macOS, iOS does not allow a
|
||||
framework to contain non-library content, so the iOS build will produce a
|
||||
``bin`` and ``lib`` folder in the same output folder as ``Python.framework``.
|
||||
The ``lib`` folder will be needed at runtime to support the Python library.
|
||||
|
||||
If you want to use Python in a real iOS project, you need to produce multiple
|
||||
``Python.framework`` builds, one for each ABI and architecture. iOS builds of
|
||||
Python *must* be constructed as framework builds. To support this, you must
|
||||
provide the ``--enable-framework`` flag when configuring the build. The build
|
||||
also requires the use of cross-compilation. The minimal commands for building
|
||||
Python for the ARM64 iOS simulator will look something like::
|
||||
|
||||
$ export PATH="$(pwd)/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 ``Python.framework`` for each ABI and architecture, you
|
||||
must produce a "fat" framework for each ABI that contains all the architectures
|
||||
for that ABI.
|
||||
|
||||
The ``iphoneos`` build only needs to support a single architecture, so it can be
|
||||
used without modification.
|
||||
|
||||
If you only want to support a single simulator architecture, (e.g., only support
|
||||
ARM64 simulators), you can use a single architecture ``Python.framework`` build.
|
||||
However, if you want to create ``Python.xcframework`` that supports *all*
|
||||
architectures, you'll need to merge the ``iphonesimulator`` builds for ARM64 and
|
||||
x86_64 into a single "fat" framework.
|
||||
|
||||
The "fat" framework can be constructed by performing a directory merge of the
|
||||
content of the two "thin" ``Python.framework`` directories, plus the ``bin`` and
|
||||
``lib`` folders for each thin framework. When performing this merge:
|
||||
|
||||
* The pure Python standard library content is identical for each architecture,
|
||||
except for a handful of platform-specific files (such as the ``sysconfig``
|
||||
module). Ensure that the "fat" framework has the union of all standard library
|
||||
files.
|
||||
|
||||
* Any binary files in the standard library, plus the main
|
||||
``libPython3.X.dylib``, can be merged using the ``lipo`` tool, provide by
|
||||
Xcode::
|
||||
|
||||
$ lipo -create -output module.dylib path/to/x86_64/module.dylib path/to/arm64/module.dylib
|
||||
|
||||
* The header files will be identical on both architectures, except for
|
||||
``pyconfig.h``. Copy all the headers from one platform (say, arm64), rename
|
||||
``pyconfig.h`` to ``pyconfig-arm64.h``, and copy the ``pyconfig.h`` for the
|
||||
other architecture into the merged header folder as ``pyconfig-x86_64.h``.
|
||||
Then copy the ``iOS/Resources/pyconfig.h`` file from the CPython sources into
|
||||
the merged headers folder. This will allow the two Python architectures to
|
||||
share a common ``pyconfig.h`` header file.
|
||||
|
||||
At this point, you should have 2 Python.framework folders - one for ``iphoneos``,
|
||||
and one for ``iphonesimulator`` that is a merge of x86+64 and ARM64 content.
|
||||
|
||||
Merge frameworks into an XCframework
|
||||
------------------------------------
|
||||
|
||||
Now that we have 2 (potentially fat) ABI-specific frameworks, we can merge those
|
||||
frameworks into a single ``XCframework``.
|
||||
|
||||
The initial skeleton of an ``XCframework`` is built using::
|
||||
|
||||
xcodebuild -create-xcframework -output Python.xcframework -framework path/to/iphoneos/Python.framework -framework path/to/iphonesimulator/Python.framework
|
||||
|
||||
Then, copy the ``bin`` and ``lib`` folders into the architecture-specific slices of
|
||||
the XCframework::
|
||||
|
||||
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 testios`` generates a standalone version of the ``iOS/testbed``
|
||||
project, and runs the full test suite. It does this using ``iOS/testbed``
|
||||
itself - the folder is an executable module that can be used to create and run
|
||||
a clone of the testbed project.
|
||||
|
||||
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.
|
||||
|
||||
Testing in Xcode
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
You can also open the testbed project in Xcode by running::
|
||||
|
||||
$ open my-testbed/iOSTestbed.xcodeproj
|
||||
|
||||
This will allow you to use the full Xcode suite of tools for debugging.
|
||||
|
||||
The arguments used to run the test suite are defined as part of the test plan.
|
||||
To modify the test plan, select the test plan node of the project tree (it
|
||||
should be the first child of the root node), and select the "Configurations"
|
||||
tab. Modify the "Arguments Passed On Launch" value to change the testing
|
||||
arguments.
|
||||
|
||||
The test plan also disables parallel testing, and specifies the use of the
|
||||
``iOSTestbed.lldbinit`` file for providing configuration of the debugger. The
|
||||
default debugger configuration disables automatic breakpoints on the
|
||||
``SIGINT``, ``SIGUSR1``, ``SIGUSR2``, and ``SIGXFSZ`` signals.
|
||||
|
||||
Testing on an iOS device
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To test on an iOS device, the app needs to be signed with known developer
|
||||
credentials. To obtain these credentials, you must have an iOS Developer
|
||||
account, and your Xcode install will need to be logged into your account (see
|
||||
the Accounts tab of the Preferences dialog).
|
||||
|
||||
Once the project is open, and you're signed into your Apple Developer account,
|
||||
select the root node of the project tree (labeled "iOSTestbed"), then the
|
||||
"Signing & Capabilities" tab in the details page. Select a development team
|
||||
(this will likely be your own name), and plug in a physical device to your
|
||||
macOS machine with a USB cable. You should then be able to select your physical
|
||||
device from the list of targets in the pulldown in the Xcode titlebar.
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue