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:
Russell Keith-Magee authored and GitHub committed 2026-10-14 14:48:50 +08:00
1 parent 32fd69bdea
commit d6f2129abd
12 files changed
+380 -92

No files matched your search

+12 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+59 -10
View File
@@ -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:
+2 -2
View File
@@ -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
+180 -22
View File
@@ -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;