mirror of
https://github.com/python/cpython.git
synced 2026-10-28 10:25:05 +08:00
gh-159385: Add support for free-threaded and debug builds for iOS (#159386)
Modify the iOS build tools to support the building free threaded, debug, and potentiall other abiflag variants of Python for iOS.
This commit is contained in:
1 parent
32fd69bdea
commit
d6f2129abd
12 files changed
+380
-92
No files matched your search
@@ -346,18 +346,28 @@ jobs:
|
||||
run: python3 Platforms/Android ci --fast-ci ${{ matrix.arch }}-linux-android
|
||||
|
||||
build-ios:
|
||||
name: iOS
|
||||
name: >-
|
||||
iOS
|
||||
${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }}
|
||||
needs: build-context
|
||||
if: needs.build-context.outputs.run-ios == 'true'
|
||||
timeout-minutes: 60
|
||||
runs-on: macos-26
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
free-threading:
|
||||
- false
|
||||
- true
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build and test
|
||||
run: python3 Platforms/Apple ci iOS --fast-ci
|
||||
run: >-
|
||||
python3 Platforms/Apple ci iOS --fast-ci
|
||||
${{ fromJSON(matrix.free-threading) && '--abiflags t' || '' }}
|
||||
|
||||
build-emscripten:
|
||||
name: 'Emscripten'
|
||||
|
||||
+4
-4
@@ -76,10 +76,10 @@ Lib/test/data/*
|
||||
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
|
||||
Platforms/Apple/testbed/Python*.xcframework/*/bin
|
||||
Platforms/Apple/testbed/Python*.xcframework/*/include
|
||||
Platforms/Apple/testbed/Python*.xcframework/*/lib
|
||||
Platforms/Apple/testbed/Python*.xcframework/*/Python*.framework
|
||||
Apple/testbed/*Testbed.xcodeproj/project.xcworkspace
|
||||
Apple/testbed/*Testbed.xcodeproj/xcuserdata
|
||||
Mac/Makefile
|
||||
|
||||
+3
-5
@@ -2286,7 +2286,9 @@ testuniversal: all
|
||||
# Run the test suite on the iOS simulator. Must be run on a macOS machine with
|
||||
# a full Xcode install that has an iPhone SE (3rd edition) simulator available.
|
||||
# This must be run *after* a `make install` has completed the build. The
|
||||
# `--with-framework-name` argument *cannot* be used when configuring the build.
|
||||
# testbed is configured for the framework name used to configure the build;
|
||||
# that name is expected to be `Python`, optionally followed by uppercased ABI
|
||||
# flags (e.g., `PythonT`).
|
||||
XCFOLDER:=iOSTestbed.$(MULTIARCH).$(shell date +%s).$$PPID
|
||||
.PHONY: testios
|
||||
testios:
|
||||
@@ -2298,10 +2300,6 @@ testios:
|
||||
echo "Cannot run the iOS testbed for non-simulator builds."; \
|
||||
exit 1;\
|
||||
fi
|
||||
@if test $(PYTHONFRAMEWORK) != "Python"; then \
|
||||
echo "Cannot run the iOS testbed with a non-default framework name."; \
|
||||
exit 1;\
|
||||
fi
|
||||
@if ! test -d $(PYTHONFRAMEWORKPREFIX); then \
|
||||
echo "Cannot find a finalized iOS Python.framework. Have you run 'make install' to finalize the framework build?"; \
|
||||
exit 1;\
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
iOS builds can now be configured to generate free-threading or debug output.
|
||||
+98
-26
@@ -77,6 +77,45 @@ HOSTS: dict[str, dict[str, dict[str, str]]] = {
|
||||
},
|
||||
}
|
||||
|
||||
# The configure arguments needed to produce each ABI flag. Flags are listed in
|
||||
# the order that configure adds them to ABIFLAGS; any `--abiflags` value must
|
||||
# use the same order.
|
||||
ABIFLAG_CONFIGURE_ARGS: dict[str, list[str]] = {
|
||||
"t": ["--disable-gil"],
|
||||
"d": ["--with-pydebug"],
|
||||
}
|
||||
|
||||
|
||||
def validate_abiflags(value: str) -> str:
|
||||
"""Validate an `--abiflags` value (for use as an argparse type)."""
|
||||
known = "".join(ABIFLAG_CONFIGURE_ARGS)
|
||||
for flag in value:
|
||||
if flag not in ABIFLAG_CONFIGURE_ARGS:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"unknown ABI flag {flag!r} (known flags: {known!r})"
|
||||
)
|
||||
positions = [known.index(flag) for flag in value]
|
||||
if positions != sorted(set(positions)):
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"ABI flags must be unique, and in the order {known!r}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def framework_name(abiflags: str) -> str:
|
||||
"""The name of the Python framework for the given ABI flags."""
|
||||
return f"Python{abiflags.upper()}"
|
||||
|
||||
|
||||
def abiflag_configure_args(abiflags: str) -> list[str]:
|
||||
"""The configure arguments needed to produce the given ABI flags."""
|
||||
return [arg for flag in abiflags for arg in ABIFLAG_CONFIGURE_ARGS[flag]]
|
||||
|
||||
|
||||
def cross_build_dir(base: Path, abiflags: str) -> Path:
|
||||
"""The cross-build directory for the given base and ABI flags."""
|
||||
return base.with_name(f"{base.name}-{abiflags}") if abiflags else base
|
||||
|
||||
|
||||
def subdir(name: str, create: bool = False) -> Path:
|
||||
"""Ensure that a cross-build directory for the given name exists."""
|
||||
@@ -258,6 +297,7 @@ def configure_build_python(context: argparse.Namespace) -> None:
|
||||
cwd(subdir("build", create=True)),
|
||||
):
|
||||
command = [relpath(PYTHON_DIR / "configure")]
|
||||
command.extend(abiflag_configure_args(context.abiflags))
|
||||
if context.args:
|
||||
command.extend(context.args)
|
||||
run(command)
|
||||
@@ -420,6 +460,11 @@ def configure_host_python(
|
||||
f"LIBZSTD_CFLAGS=-I{prefix_dir}/include",
|
||||
f"LIBZSTD_LIBS=-L{prefix_dir}/lib -lzstd",
|
||||
]
|
||||
command.extend(abiflag_configure_args(context.abiflags))
|
||||
if context.abiflags:
|
||||
command.append(
|
||||
f"--with-framework-name={framework_name(context.abiflags)}"
|
||||
)
|
||||
|
||||
if context.args:
|
||||
command.extend(context.args)
|
||||
@@ -521,11 +566,16 @@ def lib_non_platform_files(dirname, names):
|
||||
return set()
|
||||
|
||||
|
||||
def create_xcframework(platform: str) -> str:
|
||||
def create_xcframework(platform: str, abiflags: str) -> str:
|
||||
"""Build an XCframework from the component parts for the platform.
|
||||
|
||||
:param platform: The platform being packaged (e.g., iOS).
|
||||
:param abiflags: The ABI flags of the build (e.g., "t").
|
||||
:return: The version number of the Python version that was packaged.
|
||||
"""
|
||||
fw_name = framework_name(abiflags)
|
||||
# The stdlib directory only carries the threading ABI flag.
|
||||
abi_thread = "t" if "t" in abiflags else ""
|
||||
package_path = CROSS_BUILD_DIR / platform
|
||||
try:
|
||||
package_path.mkdir()
|
||||
@@ -547,7 +597,7 @@ def create_xcframework(platform: str) -> str:
|
||||
first_host_triple, first_multiarch = next(iter(slice_parts.items()))
|
||||
first_framework = (
|
||||
framework_path(first_host_triple, first_multiarch)
|
||||
/ "Python.framework"
|
||||
/ f"{fw_name}.framework"
|
||||
)
|
||||
|
||||
if len(slice_parts) == 1:
|
||||
@@ -557,7 +607,7 @@ def create_xcframework(platform: str) -> str:
|
||||
else:
|
||||
print(f"Merging framework for {slice_name}...")
|
||||
slice_path = CROSS_BUILD_DIR / slice_name
|
||||
slice_framework = slice_path / "Python.framework"
|
||||
slice_framework = slice_path / f"{fw_name}.framework"
|
||||
slice_framework.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# Copy the Info.plist
|
||||
@@ -574,11 +624,11 @@ def create_xcframework(platform: str) -> str:
|
||||
|
||||
# Create the "fat" library binary for the slice
|
||||
run(
|
||||
["lipo", "-create", "-output", slice_framework / "Python"]
|
||||
["lipo", "-create", "-output", slice_framework / fw_name]
|
||||
+ [
|
||||
(
|
||||
framework_path(host_triple, multiarch)
|
||||
/ "Python.framework/Python"
|
||||
/ f"{fw_name}.framework/{fw_name}"
|
||||
)
|
||||
for host_triple, multiarch in slice_parts.items()
|
||||
]
|
||||
@@ -593,7 +643,7 @@ def create_xcframework(platform: str) -> str:
|
||||
"xcodebuild",
|
||||
"-create-xcframework",
|
||||
"-output",
|
||||
package_path / "Python.xcframework",
|
||||
package_path / f"{fw_name}.xcframework",
|
||||
]
|
||||
for framework in frameworks:
|
||||
cmd.extend(["-framework", framework])
|
||||
@@ -601,7 +651,7 @@ def create_xcframework(platform: str) -> str:
|
||||
run(cmd)
|
||||
|
||||
# Extract the package version from the merged framework
|
||||
version = package_version(package_path / "Python.xcframework")
|
||||
version = package_version(package_path / f"{fw_name}.xcframework")
|
||||
version_tag = ".".join(version.split(".")[:2])
|
||||
|
||||
# On non-macOS platforms, each framework in XCframework only contains the
|
||||
@@ -616,10 +666,10 @@ def create_xcframework(platform: str) -> str:
|
||||
# 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"
|
||||
first_framework = first_path / f"{fw_name}.framework"
|
||||
|
||||
slice_path = package_path / f"Python.xcframework/{slice_name}"
|
||||
slice_framework = slice_path / "Python.framework"
|
||||
slice_path = package_path / f"{fw_name}.xcframework/{slice_name}"
|
||||
slice_framework = slice_path / f"{fw_name}.framework"
|
||||
|
||||
# Copy the binary helpers
|
||||
print(f" - {slice_name} binaries")
|
||||
@@ -641,9 +691,9 @@ def create_xcframework(platform: str) -> str:
|
||||
|
||||
print(f" - {slice_name} shared library")
|
||||
# Create a simlink for the fat library
|
||||
shared_lib = slice_path / f"lib/libpython{version_tag}.dylib"
|
||||
shared_lib = slice_path / f"lib/libpython{version_tag}{abiflags}.dylib"
|
||||
shared_lib.parent.mkdir()
|
||||
shared_lib.symlink_to("../Python.framework/Python")
|
||||
shared_lib.symlink_to(f"../{fw_name}.framework/{fw_name}")
|
||||
|
||||
print(f" - {slice_name} architecture-specific files")
|
||||
for host_triple, multiarch in slice_parts.items():
|
||||
@@ -654,7 +704,7 @@ def create_xcframework(platform: str) -> str:
|
||||
print(" - using this architecture as the common stdlib")
|
||||
shutil.copytree(
|
||||
framework_path(host_triple, multiarch) / "lib",
|
||||
package_path / "Python.xcframework/lib",
|
||||
package_path / f"{fw_name}.xcframework/lib",
|
||||
ignore=lib_platform_files,
|
||||
symlinks=True,
|
||||
)
|
||||
@@ -675,7 +725,7 @@ def create_xcframework(platform: str) -> str:
|
||||
/ "Platforms/Apple/iOS/Frameworks"
|
||||
/ multiarch
|
||||
)
|
||||
host_framework = host_path / "Python.framework"
|
||||
host_framework = host_path / f"{fw_name}.framework"
|
||||
shutil.copy(
|
||||
host_framework / "Headers/pyconfig.h",
|
||||
slice_framework / f"Headers/pyconfig-{arch}.h",
|
||||
@@ -696,14 +746,17 @@ def create_xcframework(platform: str) -> str:
|
||||
shutil.copy(
|
||||
xcprivacy_file[lib],
|
||||
slice_path
|
||||
/ f"lib-{arch}/python{version_tag}"
|
||||
/ f"lib-{arch}/python{version_tag}{abi_thread}"
|
||||
/ f"lib-dynload/{module}.xcprivacy",
|
||||
)
|
||||
|
||||
print(" - build tools")
|
||||
shutil.copytree(
|
||||
PYTHON_DIR / "Platforms/Apple/testbed/Python.xcframework/build",
|
||||
package_path / "Python.xcframework/build",
|
||||
PYTHON_DIR
|
||||
/ "Platforms/Apple/testbed"
|
||||
/ "Python.xcframework"
|
||||
/ "build",
|
||||
package_path / f"{fw_name}.xcframework/build",
|
||||
)
|
||||
|
||||
return version
|
||||
@@ -716,7 +769,8 @@ def package(context: argparse.Namespace) -> None:
|
||||
|
||||
with group("Building package"):
|
||||
# Create an XCframework
|
||||
version = create_xcframework(context.platform)
|
||||
version = create_xcframework(context.platform, context.abiflags)
|
||||
fw_name = framework_name(context.abiflags)
|
||||
|
||||
# Clone testbed
|
||||
print()
|
||||
@@ -727,7 +781,7 @@ def package(context: argparse.Namespace) -> None:
|
||||
"--platform",
|
||||
context.platform,
|
||||
"--framework",
|
||||
CROSS_BUILD_DIR / context.platform / "Python.xcframework",
|
||||
CROSS_BUILD_DIR / context.platform / f"{fw_name}.xcframework",
|
||||
CROSS_BUILD_DIR / context.platform / "testbed",
|
||||
])
|
||||
|
||||
@@ -735,7 +789,10 @@ def package(context: argparse.Namespace) -> None:
|
||||
archive_name = (
|
||||
CROSS_BUILD_DIR
|
||||
/ "dist"
|
||||
/ f"python-{version}-{context.platform}-XCframework"
|
||||
/ (
|
||||
f"python-{version}{context.abiflags}"
|
||||
f"-{context.platform}-XCframework"
|
||||
)
|
||||
)
|
||||
|
||||
print()
|
||||
@@ -799,7 +856,9 @@ def test(context: argparse.Namespace, host: str | None = None) -> None: # noqa:
|
||||
)
|
||||
if host in {"all", "hosts"}:
|
||||
framework_path = (
|
||||
CROSS_BUILD_DIR / context.platform / "Python.xcframework"
|
||||
CROSS_BUILD_DIR
|
||||
/ context.platform
|
||||
/ f"{framework_name(context.abiflags)}.xcframework"
|
||||
)
|
||||
else:
|
||||
build_arch = platform.machine()
|
||||
@@ -974,7 +1033,7 @@ def parse_args() -> argparse.Namespace:
|
||||
),
|
||||
)
|
||||
|
||||
# --cross-build-dir argument
|
||||
# --cross-build-dir and --abiflags arguments
|
||||
for cmd in [
|
||||
clean,
|
||||
configure_build,
|
||||
@@ -994,9 +1053,20 @@ def parse_args() -> argparse.Namespace:
|
||||
dest="cross_build_dir",
|
||||
type=Path,
|
||||
help=(
|
||||
"Path to the cross-build directory "
|
||||
"Base path of the cross-build directory "
|
||||
f"(default: {CROSS_BUILD_DIR}). Can also be set "
|
||||
"with the CROSS_BUILD_DIR environment variable."
|
||||
"with the CROSS_BUILD_DIR environment variable. If "
|
||||
"--abiflags is specified, '-<abiflags>' is appended."
|
||||
),
|
||||
)
|
||||
cmd.add_argument(
|
||||
"--abiflags",
|
||||
default="",
|
||||
type=validate_abiflags,
|
||||
help=(
|
||||
"The ABI flags of the build: 't' (free-threaded; "
|
||||
"--disable-gil), 'd' (debug; --with-pydebug), or 'td' (both). "
|
||||
"Defaults to '' (a standard build)."
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1082,10 +1152,12 @@ def main() -> None:
|
||||
# Process command line arguments
|
||||
context = parse_args()
|
||||
|
||||
# Set the CROSS_BUILD_DIR if an argument was provided
|
||||
# Set the CROSS_BUILD_DIR, using any provided base directory, plus any ABI
|
||||
# flags as a suffix.
|
||||
global CROSS_BUILD_DIR
|
||||
if context.cross_build_dir:
|
||||
global CROSS_BUILD_DIR
|
||||
CROSS_BUILD_DIR = context.cross_build_dir.resolve()
|
||||
CROSS_BUILD_DIR = cross_build_dir(CROSS_BUILD_DIR, context.abiflags)
|
||||
|
||||
dispatch: dict[str, Callable] = {
|
||||
"clean": clean,
|
||||
|
||||
@@ -75,6 +75,39 @@ 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 with ABI flags
|
||||
|
||||
By default, the `Platforms/Apple` build script produces a GIL-enabled,
|
||||
non-debug build of Python. The `--abiflags` option can be passed to any
|
||||
command of the build script to build a variant of Python with different
|
||||
ABI flags:
|
||||
|
||||
* `t` - a free-threaded build (`--disable-gil`)
|
||||
* `d` - a debug build (`--with-pydebug`)
|
||||
* `td` - a free-threaded debug build
|
||||
|
||||
For example, to build and test a free-threaded XCframework, run:
|
||||
|
||||
python Platforms/Apple ci iOS --abiflags t
|
||||
|
||||
When ABI flags are specified:
|
||||
|
||||
* The ABI flags are appended to the name of the cross-build directory. The
|
||||
free-threaded example above uses `cross-build-t`. If you specify a custom
|
||||
location with `--cross-build-dir` (or the `CROSS_BUILD_DIR` environment
|
||||
variable), that location is used as the base name, so `--cross-build-dir foo
|
||||
--abiflags t` uses `foo-t`. This lets builds with different ABI flags
|
||||
co-exist.
|
||||
* The framework is named `Python` followed by the uppercased ABI flags. For
|
||||
example, a free-threaded build produces `PythonT.framework` and
|
||||
`PythonT.xcframework`, and its headers are included as
|
||||
`#include <PythonT/Python.h>`.
|
||||
* The release archive includes the ABI flags in the version, e.g.
|
||||
`python-3.X.Yt-iOS-XCframework.tar.gz`.
|
||||
|
||||
The same `--abiflags` value must be passed to every command that operates on a
|
||||
given build.
|
||||
|
||||
### Building a single-architecture framework
|
||||
|
||||
If you're using the `Platforms/Apple` build script, you won't need to build
|
||||
@@ -102,11 +135,13 @@ Python build for a single framework, the following options are available.
|
||||
Specify the name for the Python framework; defaults to `Python`.
|
||||
|
||||
> [!NOTE]
|
||||
> Unless you know what you're doing, changing the name of the Python
|
||||
> framework on iOS is not advised. If you use this option, you won't be able
|
||||
> to run the `Platforms/Apple` build script without making significant manual
|
||||
> alterations, and you won't be able to use any binary packages unless you
|
||||
> compile them yourself using your own framework name.
|
||||
> Builds that use ABI flags (such as free-threaded or debug builds) should
|
||||
> use a framework name of `Python`, followed by the uppercased ABI flags
|
||||
> (e.g., `PythonT` for a free-threaded build). The `Platforms/Apple` build
|
||||
> script and the testbed rely on this naming convention. Other than this
|
||||
> convention, changing the name of the Python framework on iOS is not
|
||||
> advised; you won't be able to use any binary packages unless you compile
|
||||
> them yourself using your own framework name.
|
||||
|
||||
#### Building Python for iOS
|
||||
|
||||
@@ -339,10 +374,11 @@ project, and then boot and prepare the iOS simulator.
|
||||
|
||||
Running `python Platforms/Apple test iOS` generates a standalone version of the
|
||||
`Platforms/Apple/testbed` project, and runs the full test suite. It does this using
|
||||
`Platforms/Apple/testbed` itself - the folder is an executable module that can be used
|
||||
to create and run a clone of the testbed project. The standalone version of the
|
||||
testbed will be created in a directory named
|
||||
`cross-build/iOS-testbed.<timestamp>`.
|
||||
`Platforms/Apple/testbed` itself - the folder is a template for 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>`. Builds that use ABI flags use the
|
||||
`cross-build-<abiflags>` directory instead (e.g., `cross-build-t`).
|
||||
|
||||
You can generate your own standalone testbed instance by running:
|
||||
```
|
||||
@@ -358,9 +394,22 @@ you can construct a standalone testbed instance by running:
|
||||
python Platforms/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
|
||||
The framework path can be the path to a `Python.xcframework`, or the
|
||||
path to a folder that contains a single-platform `Python.framework`.
|
||||
|
||||
The `Platforms/Apple/testbed` folder is a template. Any reference to the
|
||||
Python framework uses the placeholder `Python{{abiflags}}`. When the template
|
||||
project is cloned, the placeholder is replaced with the name of the framework
|
||||
being installed; the ABI flags are inferred automatically from the
|
||||
`--framework` argument. For example, to clone a testbed for a free-threaded
|
||||
framework:
|
||||
```
|
||||
python Platforms/Apple/testbed clone --platform iOS --framework <path/to/PythonT.xcframework> my-testbed
|
||||
```
|
||||
|
||||
A cloned testbed is configured for one specific framework. It can be cloned
|
||||
again, but it can't be re-targeted to a framework with different ABI flags.
|
||||
|
||||
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:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Python</string>
|
||||
<string>@PYTHONFRAMEWORK@</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>Python Runtime and Library</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Python</string>
|
||||
<string>@PYTHONFRAMEWORK@</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<array>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>Python.framework/Python</string>
|
||||
<string>Python{{abiflags}}.framework/Python{{abiflags}}</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>Python.framework</string>
|
||||
<string>Python{{abiflags}}.framework</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
@@ -20,11 +20,11 @@
|
||||
</dict>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>Python.framework/Python</string>
|
||||
<string>Python{{abiflags}}.framework/Python{{abiflags}}</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64_x86_64-simulator</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>Python.framework</string>
|
||||
<string>Python{{abiflags}}.framework</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
|
||||
@@ -140,7 +140,7 @@ install_python() {
|
||||
shift
|
||||
|
||||
install_stdlib $PYTHON_XCFRAMEWORK_PATH
|
||||
PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib" | grep -E "^python3\.\d+$")
|
||||
PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib" | grep -E "^python3\.\d+[a-z]*$")
|
||||
echo "Install Python $PYTHON_VER standard library extension modules..."
|
||||
process_dylibs $PYTHON_XCFRAMEWORK_PATH python/lib/$PYTHON_VER/lib-dynload
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <Python/Python.h>
|
||||
#import <Python{{abiflags}}/Python.h>
|
||||
|
||||
@interface TestbedTests : XCTestCase
|
||||
|
||||
|
||||
@@ -12,6 +12,15 @@ TEST_SLICES = {
|
||||
"iOS": "ios-arm64_x86_64-simulator",
|
||||
}
|
||||
|
||||
# The checked-in testbed is a template. Any file containing this placeholder
|
||||
# is rewritten by `clone`, replacing the placeholder with the (uppercased)
|
||||
# ABI flags of the framework being installed. The placeholder is constructed
|
||||
# so that it doesn't appear literally in this file.
|
||||
ABIFLAGS_PLACEHOLDER = "{{" + "abiflags" + "}}"
|
||||
|
||||
FRAMEWORK_NAME_RE = re.compile(r"Python([A-Z]*)")
|
||||
|
||||
|
||||
DECODE_ARGS = ("UTF-8", "backslashreplace")
|
||||
|
||||
# The system log prefixes each line:
|
||||
@@ -127,37 +136,156 @@ def copy(src, tgt):
|
||||
shutil.copytree(src, tgt, symlinks=True)
|
||||
|
||||
|
||||
def framework_name(abiflags: str) -> str:
|
||||
"""The name of the Python framework for the given ABI flags."""
|
||||
return f"Python{abiflags.upper()}"
|
||||
|
||||
|
||||
def is_template(location: Path) -> bool:
|
||||
"""Is the testbed at the given location an unexpanded template?
|
||||
|
||||
A template's XCframework `Info.plist` still contains the literal
|
||||
ABIFLAGS_PLACEHOLDER; once expanded, the placeholder is gone.
|
||||
"""
|
||||
info_plist = location / "Python.xcframework/Info.plist"
|
||||
return (
|
||||
info_plist.is_file()
|
||||
and ABIFLAGS_PLACEHOLDER.encode() in info_plist.read_bytes()
|
||||
)
|
||||
|
||||
|
||||
def abiflags_from_framework(framework: Path) -> str:
|
||||
"""Infer the (lowercase) ABI flags from a `--framework` path.
|
||||
|
||||
`framework` may be a `Python{FLAGS}.xcframework`, or a directory
|
||||
containing a single simulator-slice `Python{FLAGS}.framework`.
|
||||
|
||||
Raises ValueError if the ABI flags can't be determined.
|
||||
"""
|
||||
if framework.suffix == ".xcframework":
|
||||
match = FRAMEWORK_NAME_RE.fullmatch(framework.stem)
|
||||
if not match:
|
||||
raise ValueError(f"{framework} is not a valid Python XCframework.")
|
||||
return match.group(1).lower()
|
||||
|
||||
candidates = sorted(
|
||||
path
|
||||
for path in framework.glob("Python*.framework")
|
||||
if FRAMEWORK_NAME_RE.fullmatch(path.stem)
|
||||
)
|
||||
if len(candidates) != 1:
|
||||
raise ValueError(
|
||||
f"Expected exactly one Python framework in {framework}; "
|
||||
f"found {len(candidates)}."
|
||||
)
|
||||
return FRAMEWORK_NAME_RE.fullmatch(candidates[0].stem).group(1).lower()
|
||||
|
||||
|
||||
def find_xcframework(location: Path) -> Path:
|
||||
"""Find the Python XCframework in a testbed project.
|
||||
|
||||
Raises ValueError if more than one XCframework is found.
|
||||
"""
|
||||
candidates = sorted(
|
||||
path
|
||||
for path in location.glob("Python*.xcframework")
|
||||
if re.fullmatch(r"Python[A-Z]*\.xcframework", path.name)
|
||||
)
|
||||
if len(candidates) != 1:
|
||||
raise ValueError(
|
||||
f"Expected exactly one Python XCframework in {location}; "
|
||||
f"found {len(candidates)}."
|
||||
)
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def expand_template(location: Path, abiflags: str) -> None:
|
||||
"""Expand the ABI flags placeholder in a cloned testbed template.
|
||||
|
||||
The placeholder is replaced in the content of every template file, then
|
||||
any file or directory whose name contains the placeholder is renamed.
|
||||
Symlinks are neither followed nor modified.
|
||||
"""
|
||||
placeholder = ABIFLAGS_PLACEHOLDER
|
||||
replacement = abiflags.upper()
|
||||
for path in location.rglob("*"):
|
||||
if (
|
||||
path.suffix in {".m", ".pbxproj", ".plist"}
|
||||
and path.is_file()
|
||||
and not path.is_symlink()
|
||||
):
|
||||
content = path.read_text(encoding="utf-8")
|
||||
if placeholder in content:
|
||||
path.write_text(
|
||||
content.replace(placeholder, replacement),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def clone_testbed(
|
||||
source: Path,
|
||||
target: Path,
|
||||
framework: Path,
|
||||
framework: Path | None,
|
||||
platform: str,
|
||||
apps: list[Path],
|
||||
) -> None:
|
||||
if target.exists():
|
||||
print(f"{target} already exists; aborting without creating project.")
|
||||
sys.exit(10)
|
||||
source_is_template = is_template(source)
|
||||
try:
|
||||
orig_xc_framework_path = find_xcframework(source)
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
sys.exit(15)
|
||||
fw_name = orig_xc_framework_path.stem
|
||||
|
||||
if framework is None:
|
||||
if not (
|
||||
source / "Python.xcframework" / TEST_SLICES[platform] / "bin"
|
||||
).is_dir():
|
||||
# No --framework; we must be cloning an an already-expanded testbed.
|
||||
# Raise an error if we're not.
|
||||
if source_is_template:
|
||||
print(
|
||||
f"The testbed being cloned ({source}) does not contain "
|
||||
"a framework with slices. Re-run with --framework"
|
||||
"The initial testbed clone must provide a framework. "
|
||||
"Re-run using --framework."
|
||||
)
|
||||
sys.exit(11)
|
||||
else:
|
||||
# Look at the simulator slice in the existing framework; if
|
||||
# it's a live framework, it will have a `bin` directory.
|
||||
slice_path = orig_xc_framework_path / TEST_SLICES[platform]
|
||||
if not (slice_path / "bin").is_dir():
|
||||
print(
|
||||
f"The testbed being cloned ({source}) does not contain "
|
||||
"a framework with slices. Re-run with --framework"
|
||||
)
|
||||
sys.exit(11)
|
||||
|
||||
else:
|
||||
if not framework.is_dir():
|
||||
print(f"{framework} does not exist.")
|
||||
sys.exit(12)
|
||||
|
||||
if source_is_template:
|
||||
# Initial testbed clone; the target framework name is
|
||||
# taken directly from the provided framework, not the
|
||||
# existing framework in the testbed.
|
||||
try:
|
||||
abiflags = abiflags_from_framework(framework)
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
sys.exit(13)
|
||||
|
||||
fw_name = framework_name(abiflags)
|
||||
|
||||
elif not (
|
||||
framework.suffix == ".xcframework"
|
||||
or (framework / "Python.framework").is_dir()
|
||||
framework.name == f"{fw_name}.xcframework"
|
||||
or (framework / f"{fw_name}.framework").is_dir()
|
||||
):
|
||||
# Re-cloning an already-expanded testbed: the ABI for the
|
||||
# new framework must match the existing testbed.
|
||||
print(
|
||||
f"{framework} is not an XCframework, "
|
||||
f"or a simulator slice of a framework build."
|
||||
f"{framework} is not a {fw_name}.xcframework, "
|
||||
f"or a simulator slice of a {fw_name}.framework build."
|
||||
)
|
||||
sys.exit(13)
|
||||
|
||||
@@ -179,9 +307,23 @@ def clone_testbed(
|
||||
|
||||
print(" done")
|
||||
|
||||
orig_xc_framework_path = source / "Python.xcframework"
|
||||
xc_framework_path = target / "Python.xcframework"
|
||||
xc_framework_path = target / f"{fw_name}.xcframework"
|
||||
test_framework_path = xc_framework_path / TEST_SLICES[platform]
|
||||
|
||||
if source_is_template:
|
||||
# Unless an XCframework is being symlinked in, the testbed uses a copy
|
||||
# of the template XCframework (either as-is, or with a simulator slice
|
||||
# installed into it); that copy requires expansion.
|
||||
if framework.suffix != ".xcframework":
|
||||
shutil.copytree(
|
||||
orig_xc_framework_path,
|
||||
xc_framework_path,
|
||||
symlinks=True,
|
||||
)
|
||||
print(f" Expanding template for {fw_name}...", end="")
|
||||
expand_template(target, abiflags)
|
||||
print(" done")
|
||||
|
||||
if framework is not None:
|
||||
if framework.suffix == ".xcframework":
|
||||
print(" Installing XCFramework...", end="")
|
||||
@@ -193,9 +335,18 @@ def clone_testbed(
|
||||
print(" Installing simulator framework...", end="")
|
||||
# We're only installing a slice of a framework; we need
|
||||
# to do a full tree copy to make sure we don't damage
|
||||
# symlinked content.
|
||||
shutil.copytree(orig_xc_framework_path, xc_framework_path)
|
||||
if test_framework_path.is_dir():
|
||||
# symlinked content. If this is an initial template clone,
|
||||
# this copy has already been done as part of rolling out
|
||||
# the template.
|
||||
if not source_is_template:
|
||||
shutil.copytree(
|
||||
orig_xc_framework_path, xc_framework_path, symlinks=True
|
||||
)
|
||||
|
||||
if (
|
||||
test_framework_path.is_dir()
|
||||
and not test_framework_path.is_symlink()
|
||||
):
|
||||
shutil.rmtree(test_framework_path)
|
||||
else:
|
||||
test_framework_path.unlink(missing_ok=True)
|
||||
@@ -231,7 +382,7 @@ def clone_testbed(
|
||||
# Simulator framework is a relative symlink. Rewrite the symlink
|
||||
# relative to the new location.
|
||||
orig_test_framework_path = (
|
||||
source / "Python.XCframework" / test_framework_path.readlink()
|
||||
orig_xc_framework_path / test_framework_path.readlink()
|
||||
).resolve()
|
||||
test_framework_path.unlink()
|
||||
test_framework_path.symlink_to(
|
||||
@@ -327,7 +478,9 @@ def main():
|
||||
"--framework",
|
||||
help=(
|
||||
"The location of the XCFramework (or simulator-only slice of an "
|
||||
"XCFramework) to use when running the testbed"
|
||||
"XCFramework) to use when running the testbed. The framework's "
|
||||
"ABI flags (e.g., free-threaded, debug) are inferred from its "
|
||||
"name."
|
||||
),
|
||||
)
|
||||
clone.add_argument(
|
||||
@@ -408,12 +561,17 @@ def main():
|
||||
)
|
||||
elif context.subcommand == "run":
|
||||
if test_args:
|
||||
try:
|
||||
xc_framework_path = find_xcframework(Path(__file__).parent)
|
||||
except ValueError:
|
||||
xc_framework_path = None
|
||||
|
||||
if not (
|
||||
Path(__file__).parent
|
||||
/ "Python.xcframework"
|
||||
/ TEST_SLICES[context.platform]
|
||||
/ "bin"
|
||||
).is_dir():
|
||||
xc_framework_path
|
||||
and (
|
||||
xc_framework_path / TEST_SLICES[context.platform] / "bin"
|
||||
).is_dir()
|
||||
):
|
||||
print(
|
||||
"Testbed does not contain a compiled Python framework. "
|
||||
f"Use `python {sys.argv[0]} clone ...` to create a "
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; };
|
||||
607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; };
|
||||
607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
607A664C2B0EFC080010BFC8 /* Python{{abiflags}}.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python{{abiflags}}.xcframework */; };
|
||||
607A664D2B0EFC080010BFC8 /* Python{{abiflags}}.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python{{abiflags}}.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
607A66502B0EFFE00010BFC8 /* Python{{abiflags}}.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python{{abiflags}}.xcframework */; };
|
||||
607A66512B0EFFE00010BFC8 /* Python{{abiflags}}.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 607A664A2B0EFB310010BFC8 /* Python{{abiflags}}.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -30,7 +30,7 @@
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
607A664D2B0EFC080010BFC8 /* Python.xcframework in Embed Frameworks */,
|
||||
607A664D2B0EFC080010BFC8 /* Python{{abiflags}}.xcframework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -41,7 +41,7 @@
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
607A66512B0EFFE00010BFC8 /* Python.xcframework in Embed Frameworks */,
|
||||
607A66512B0EFFE00010BFC8 /* Python{{abiflags}}.xcframework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -51,7 +51,7 @@
|
||||
/* Begin PBXFileReference section */
|
||||
607A66122B0EFA380010BFC8 /* iOSTestbed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSTestbed.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607A662D2B0EFA3A0010BFC8 /* iOSTestbedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSTestbedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
607A664A2B0EFB310010BFC8 /* Python.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Python.xcframework; sourceTree = "<group>"; };
|
||||
607A664A2B0EFB310010BFC8 /* Python{{abiflags}}.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "Python{{abiflags}}.xcframework"; sourceTree = "<group>"; };
|
||||
60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = iOSTestbed.xctestplan; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
607A664C2B0EFC080010BFC8 /* Python.xcframework in Frameworks */,
|
||||
607A664C2B0EFC080010BFC8 /* Python{{abiflags}}.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -98,7 +98,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
607A66502B0EFFE00010BFC8 /* Python.xcframework in Frameworks */,
|
||||
607A66502B0EFFE00010BFC8 /* Python{{abiflags}}.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -109,7 +109,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */,
|
||||
607A664A2B0EFB310010BFC8 /* Python.xcframework */,
|
||||
607A664A2B0EFB310010BFC8 /* Python{{abiflags}}.xcframework */,
|
||||
607A66142B0EFA380010BFC8 /* iOSTestbed */,
|
||||
607A66302B0EFA3A0010BFC8 /* TestbedTests */,
|
||||
607A66132B0EFA380010BFC8 /* Products */,
|
||||
@@ -250,7 +250,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\nsource $PROJECT_DIR/Python.xcframework/build/utils.sh\ninstall_python Python.xcframework app app_packages\n";
|
||||
shellScript = "set -e\nsource $PROJECT_DIR/Python{{abiflags}}.xcframework/build/utils.sh\ninstall_python Python{{abiflags}}.xcframework app app_packages\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
@@ -407,7 +407,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
|
||||
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python{{abiflags}}.framework/Headers\"";
|
||||
INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
@@ -438,7 +438,7 @@
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
|
||||
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python{{abiflags}}.framework/Headers\"";
|
||||
INFOPLIST_FILE = "iOSTestbed/iOSTestbed-Info.plist";
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
@@ -467,7 +467,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 3HEZE76D99;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
|
||||
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python{{abiflags}}.framework/Headers\"";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests;
|
||||
@@ -487,7 +487,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 3HEZE76D99;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
|
||||
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python{{abiflags}}.framework/Headers\"";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests;
|
||||
|
||||
Reference in new issue
Block a user