https://github.com/python/cpython/commit/d6f2129abdf14c2d653b8fc03a40d4a61674ec54
commit: d6f2129abdf14c2d653b8fc03a40d4a61674ec54
branch: main
author: Russell Keith-Magee <[email protected]>
committer: freakboy3742 <[email protected]>
date: 2026-10-14T14:48:50+08:00
summary:
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.
files:
A Misc/NEWS.d/next/Tools-Demos/2026-10-14-09-48-26.gh-issue-159385.0aWOA9.rst
M .github/workflows/build.yml
M .gitignore
M Makefile.pre.in
M Platforms/Apple/__main__.py
M Platforms/Apple/iOS/README.md
M Platforms/Apple/iOS/Resources/Info.plist.in
M Platforms/Apple/testbed/Python.xcframework/Info.plist
M Platforms/Apple/testbed/Python.xcframework/build/utils.sh
M Platforms/Apple/testbed/TestbedTests/TestbedTests.m
M Platforms/Apple/testbed/__main__.py
M Platforms/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 00d64b3df7e9654..98752414a87646d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -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'
diff --git a/.gitignore b/.gitignore
index 00813f9d1b6b00a..2fce2ac10ae9a74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/Makefile.pre.in b/Makefile.pre.in
index 36515dbd146a1e3..308fb964a0cd57e 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -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;\
diff --git
a/Misc/NEWS.d/next/Tools-Demos/2026-10-14-09-48-26.gh-issue-159385.0aWOA9.rst
b/Misc/NEWS.d/next/Tools-Demos/2026-10-14-09-48-26.gh-issue-159385.0aWOA9.rst
new file mode 100644
index 000000000000000..e76a5e666df2364
--- /dev/null
+++
b/Misc/NEWS.d/next/Tools-Demos/2026-10-14-09-48-26.gh-issue-159385.0aWOA9.rst
@@ -0,0 +1 @@
+iOS builds can now be configured to generate free-threading or debug output.
diff --git a/Platforms/Apple/__main__.py b/Platforms/Apple/__main__.py
index 86efbd20eac9efa..fa40f45ae2805cc 100644
--- a/Platforms/Apple/__main__.py
+++ b/Platforms/Apple/__main__.py
@@ -77,6 +77,45 @@
},
}
+# 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 signal_handler(*args):
# 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,
diff --git a/Platforms/Apple/iOS/README.md b/Platforms/Apple/iOS/README.md
index 1a03664cd837683..975c3056e98e0ae 100644
--- a/Platforms/Apple/iOS/README.md
+++ b/Platforms/Apple/iOS/README.md
@@ -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:
diff --git a/Platforms/Apple/iOS/Resources/Info.plist.in
b/Platforms/Apple/iOS/Resources/Info.plist.in
index c3e261ecd9eff70..032f6aa362224d3 100644
--- a/Platforms/Apple/iOS/Resources/Info.plist.in
+++ b/Platforms/Apple/iOS/Resources/Info.plist.in
@@ -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>
diff --git a/Platforms/Apple/testbed/Python.xcframework/Info.plist
b/Platforms/Apple/testbed/Python.xcframework/Info.plist
index c6418de6e74a4e9..653a5386e4601e0 100644
--- a/Platforms/Apple/testbed/Python.xcframework/Info.plist
+++ b/Platforms/Apple/testbed/Python.xcframework/Info.plist
@@ -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>
diff --git a/Platforms/Apple/testbed/Python.xcframework/build/utils.sh
b/Platforms/Apple/testbed/Python.xcframework/build/utils.sh
index e54471f68b7cb2c..ad3e62f9f53bd5a 100755
--- a/Platforms/Apple/testbed/Python.xcframework/build/utils.sh
+++ b/Platforms/Apple/testbed/Python.xcframework/build/utils.sh
@@ -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
diff --git a/Platforms/Apple/testbed/TestbedTests/TestbedTests.m
b/Platforms/Apple/testbed/TestbedTests/TestbedTests.m
index f7788c47f2c2296..435872e5e162199 100644
--- a/Platforms/Apple/testbed/TestbedTests/TestbedTests.m
+++ b/Platforms/Apple/testbed/TestbedTests/TestbedTests.m
@@ -1,5 +1,5 @@
#import <XCTest/XCTest.h>
-#import <Python/Python.h>
+#import <Python{{abiflags}}/Python.h>
@interface TestbedTests : XCTestCase
diff --git a/Platforms/Apple/testbed/__main__.py
b/Platforms/Apple/testbed/__main__.py
index c4c60e99ca974de..697f5d64b835b01 100644
--- a/Platforms/Apple/testbed/__main__.py
+++ b/Platforms/Apple/testbed/__main__.py
@@ -12,6 +12,15 @@
"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 "
diff --git a/Platforms/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj
b/Platforms/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj
index a0dd6851dc103da..6ac40a325045bd6 100644
--- a/Platforms/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj
+++ b/Platforms/Apple/testbed/iOSTestbed.xcodeproj/project.pbxproj
@@ -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;
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]