Skip to content

Commit d6f2129

Browse files
authored
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.
1 parent 32fd69b commit d6f2129

12 files changed

Lines changed: 380 additions & 92 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,18 +346,28 @@ jobs:
346346
run: python3 Platforms/Android ci --fast-ci ${{ matrix.arch }}-linux-android
347347

348348
build-ios:
349-
name: iOS
349+
name: >-
350+
iOS
351+
${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }}
350352
needs: build-context
351353
if: needs.build-context.outputs.run-ios == 'true'
352354
timeout-minutes: 60
353355
runs-on: macos-26
356+
strategy:
357+
fail-fast: false
358+
matrix:
359+
free-threading:
360+
- false
361+
- true
354362
steps:
355363
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
356364
with:
357365
persist-credentials: false
358366

359367
- name: Build and test
360-
run: python3 Platforms/Apple ci iOS --fast-ci
368+
run: >-
369+
python3 Platforms/Apple ci iOS --fast-ci
370+
${{ fromJSON(matrix.free-threading) && '--abiflags t' || '' }}
361371
362372
build-emscripten:
363373
name: 'Emscripten'

‎.gitignore‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ Lib/test/data/*
7676
Apple/iOS/Frameworks/
7777
Apple/iOS/Resources/Info.plist
7878
Apple/testbed/build
79-
Apple/testbed/Python.xcframework/*/bin
80-
Apple/testbed/Python.xcframework/*/include
81-
Apple/testbed/Python.xcframework/*/lib
82-
Apple/testbed/Python.xcframework/*/Python.framework
79+
Platforms/Apple/testbed/Python*.xcframework/*/bin
80+
Platforms/Apple/testbed/Python*.xcframework/*/include
81+
Platforms/Apple/testbed/Python*.xcframework/*/lib
82+
Platforms/Apple/testbed/Python*.xcframework/*/Python*.framework
8383
Apple/testbed/*Testbed.xcodeproj/project.xcworkspace
8484
Apple/testbed/*Testbed.xcodeproj/xcuserdata
8585
Mac/Makefile

‎Makefile.pre.in‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2286,7 +2286,9 @@ testuniversal: all
22862286
# Run the test suite on the iOS simulator. Must be run on a macOS machine with
22872287
# a full Xcode install that has an iPhone SE (3rd edition) simulator available.
22882288
# This must be run *after* a `make install` has completed the build. The
2289-
# `--with-framework-name` argument *cannot* be used when configuring the build.
2289+
# testbed is configured for the framework name used to configure the build;
2290+
# that name is expected to be `Python`, optionally followed by uppercased ABI
2291+
# flags (e.g., `PythonT`).
22902292
XCFOLDER:=iOSTestbed.$(MULTIARCH).$(shell date +%s).$$PPID
22912293
.PHONY: testios
22922294
testios:
@@ -2298,10 +2300,6 @@ testios:
22982300
echo "Cannot run the iOS testbed for non-simulator builds."; \
22992301
exit 1;\
23002302
fi
2301-
@if test $(PYTHONFRAMEWORK) != "Python"; then \
2302-
echo "Cannot run the iOS testbed with a non-default framework name."; \
2303-
exit 1;\
2304-
fi
23052303
@if ! test -d $(PYTHONFRAMEWORKPREFIX); then \
23062304
echo "Cannot find a finalized iOS Python.framework. Have you run 'make install' to finalize the framework build?"; \
23072305
exit 1;\
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
iOS builds can now be configured to generate free-threading or debug output.

‎Platforms/Apple/__main__.py‎

Lines changed: 98 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,45 @@
7777
},
7878
}
7979

80+
# The configure arguments needed to produce each ABI flag. Flags are listed in
81+
# the order that configure adds them to ABIFLAGS; any `--abiflags` value must
82+
# use the same order.
83+
ABIFLAG_CONFIGURE_ARGS: dict[str, list[str]] = {
84+
"t": ["--disable-gil"],
85+
"d": ["--with-pydebug"],
86+
}
87+
88+
89+
def validate_abiflags(value: str) -> str:
90+
"""Validate an `--abiflags` value (for use as an argparse type)."""
91+
known = "".join(ABIFLAG_CONFIGURE_ARGS)
92+
for flag in value:
93+
if flag not in ABIFLAG_CONFIGURE_ARGS:
94+
raise argparse.ArgumentTypeError(
95+
f"unknown ABI flag {flag!r} (known flags: {known!r})"
96+
)
97+
positions = [known.index(flag) for flag in value]
98+
if positions != sorted(set(positions)):
99+
raise argparse.ArgumentTypeError(
100+
f"ABI flags must be unique, and in the order {known!r}"
101+
)
102+
return value
103+
104+
105+
def framework_name(abiflags: str) -> str:
106+
"""The name of the Python framework for the given ABI flags."""
107+
return f"Python{abiflags.upper()}"
108+
109+
110+
def abiflag_configure_args(abiflags: str) -> list[str]:
111+
"""The configure arguments needed to produce the given ABI flags."""
112+
return [arg for flag in abiflags for arg in ABIFLAG_CONFIGURE_ARGS[flag]]
113+
114+
115+
def cross_build_dir(base: Path, abiflags: str) -> Path:
116+
"""The cross-build directory for the given base and ABI flags."""
117+
return base.with_name(f"{base.name}-{abiflags}") if abiflags else base
118+
80119

81120
def subdir(name: str, create: bool = False) -> Path:
82121
"""Ensure that a cross-build directory for the given name exists."""
@@ -258,6 +297,7 @@ def configure_build_python(context: argparse.Namespace) -> None:
258297
cwd(subdir("build", create=True)),
259298
):
260299
command = [relpath(PYTHON_DIR / "configure")]
300+
command.extend(abiflag_configure_args(context.abiflags))
261301
if context.args:
262302
command.extend(context.args)
263303
run(command)
@@ -420,6 +460,11 @@ def configure_host_python(
420460
f"LIBZSTD_CFLAGS=-I{prefix_dir}/include",
421461
f"LIBZSTD_LIBS=-L{prefix_dir}/lib -lzstd",
422462
]
463+
command.extend(abiflag_configure_args(context.abiflags))
464+
if context.abiflags:
465+
command.append(
466+
f"--with-framework-name={framework_name(context.abiflags)}"
467+
)
423468

424469
if context.args:
425470
command.extend(context.args)
@@ -521,11 +566,16 @@ def lib_non_platform_files(dirname, names):
521566
return set()
522567

523568

524-
def create_xcframework(platform: str) -> str:
569+
def create_xcframework(platform: str, abiflags: str) -> str:
525570
"""Build an XCframework from the component parts for the platform.
526571
572+
:param platform: The platform being packaged (e.g., iOS).
573+
:param abiflags: The ABI flags of the build (e.g., "t").
527574
:return: The version number of the Python version that was packaged.
528575
"""
576+
fw_name = framework_name(abiflags)
577+
# The stdlib directory only carries the threading ABI flag.
578+
abi_thread = "t" if "t" in abiflags else ""
529579
package_path = CROSS_BUILD_DIR / platform
530580
try:
531581
package_path.mkdir()
@@ -547,7 +597,7 @@ def create_xcframework(platform: str) -> str:
547597
first_host_triple, first_multiarch = next(iter(slice_parts.items()))
548598
first_framework = (
549599
framework_path(first_host_triple, first_multiarch)
550-
/ "Python.framework"
600+
/ f"{fw_name}.framework"
551601
)
552602

553603
if len(slice_parts) == 1:
@@ -557,7 +607,7 @@ def create_xcframework(platform: str) -> str:
557607
else:
558608
print(f"Merging framework for {slice_name}...")
559609
slice_path = CROSS_BUILD_DIR / slice_name
560-
slice_framework = slice_path / "Python.framework"
610+
slice_framework = slice_path / f"{fw_name}.framework"
561611
slice_framework.mkdir(exist_ok=True, parents=True)
562612

563613
# Copy the Info.plist
@@ -574,11 +624,11 @@ def create_xcframework(platform: str) -> str:
574624

575625
# Create the "fat" library binary for the slice
576626
run(
577-
["lipo", "-create", "-output", slice_framework / "Python"]
627+
["lipo", "-create", "-output", slice_framework / fw_name]
578628
+ [
579629
(
580630
framework_path(host_triple, multiarch)
581-
/ "Python.framework/Python"
631+
/ f"{fw_name}.framework/{fw_name}"
582632
)
583633
for host_triple, multiarch in slice_parts.items()
584634
]
@@ -593,15 +643,15 @@ def create_xcframework(platform: str) -> str:
593643
"xcodebuild",
594644
"-create-xcframework",
595645
"-output",
596-
package_path / "Python.xcframework",
646+
package_path / f"{fw_name}.xcframework",
597647
]
598648
for framework in frameworks:
599649
cmd.extend(["-framework", framework])
600650

601651
run(cmd)
602652

603653
# Extract the package version from the merged framework
604-
version = package_version(package_path / "Python.xcframework")
654+
version = package_version(package_path / f"{fw_name}.xcframework")
605655
version_tag = ".".join(version.split(".")[:2])
606656

607657
# On non-macOS platforms, each framework in XCframework only contains the
@@ -616,10 +666,10 @@ def create_xcframework(platform: str) -> str:
616666
# host frameworks as the source for the merged version.
617667
first_host_triple, first_multiarch = next(iter(slice_parts.items()))
618668
first_path = framework_path(first_host_triple, first_multiarch)
619-
first_framework = first_path / "Python.framework"
669+
first_framework = first_path / f"{fw_name}.framework"
620670

621-
slice_path = package_path / f"Python.xcframework/{slice_name}"
622-
slice_framework = slice_path / "Python.framework"
671+
slice_path = package_path / f"{fw_name}.xcframework/{slice_name}"
672+
slice_framework = slice_path / f"{fw_name}.framework"
623673

624674
# Copy the binary helpers
625675
print(f" - {slice_name} binaries")
@@ -641,9 +691,9 @@ def create_xcframework(platform: str) -> str:
641691

642692
print(f" - {slice_name} shared library")
643693
# Create a simlink for the fat library
644-
shared_lib = slice_path / f"lib/libpython{version_tag}.dylib"
694+
shared_lib = slice_path / f"lib/libpython{version_tag}{abiflags}.dylib"
645695
shared_lib.parent.mkdir()
646-
shared_lib.symlink_to("../Python.framework/Python")
696+
shared_lib.symlink_to(f"../{fw_name}.framework/{fw_name}")
647697

648698
print(f" - {slice_name} architecture-specific files")
649699
for host_triple, multiarch in slice_parts.items():
@@ -654,7 +704,7 @@ def create_xcframework(platform: str) -> str:
654704
print(" - using this architecture as the common stdlib")
655705
shutil.copytree(
656706
framework_path(host_triple, multiarch) / "lib",
657-
package_path / "Python.xcframework/lib",
707+
package_path / f"{fw_name}.xcframework/lib",
658708
ignore=lib_platform_files,
659709
symlinks=True,
660710
)
@@ -675,7 +725,7 @@ def create_xcframework(platform: str) -> str:
675725
/ "Platforms/Apple/iOS/Frameworks"
676726
/ multiarch
677727
)
678-
host_framework = host_path / "Python.framework"
728+
host_framework = host_path / f"{fw_name}.framework"
679729
shutil.copy(
680730
host_framework / "Headers/pyconfig.h",
681731
slice_framework / f"Headers/pyconfig-{arch}.h",
@@ -696,14 +746,17 @@ def create_xcframework(platform: str) -> str:
696746
shutil.copy(
697747
xcprivacy_file[lib],
698748
slice_path
699-
/ f"lib-{arch}/python{version_tag}"
749+
/ f"lib-{arch}/python{version_tag}{abi_thread}"
700750
/ f"lib-dynload/{module}.xcprivacy",
701751
)
702752

703753
print(" - build tools")
704754
shutil.copytree(
705-
PYTHON_DIR / "Platforms/Apple/testbed/Python.xcframework/build",
706-
package_path / "Python.xcframework/build",
755+
PYTHON_DIR
756+
/ "Platforms/Apple/testbed"
757+
/ "Python.xcframework"
758+
/ "build",
759+
package_path / f"{fw_name}.xcframework/build",
707760
)
708761

709762
return version
@@ -716,7 +769,8 @@ def package(context: argparse.Namespace) -> None:
716769

717770
with group("Building package"):
718771
# Create an XCframework
719-
version = create_xcframework(context.platform)
772+
version = create_xcframework(context.platform, context.abiflags)
773+
fw_name = framework_name(context.abiflags)
720774

721775
# Clone testbed
722776
print()
@@ -727,15 +781,18 @@ def package(context: argparse.Namespace) -> None:
727781
"--platform",
728782
context.platform,
729783
"--framework",
730-
CROSS_BUILD_DIR / context.platform / "Python.xcframework",
784+
CROSS_BUILD_DIR / context.platform / f"{fw_name}.xcframework",
731785
CROSS_BUILD_DIR / context.platform / "testbed",
732786
])
733787

734788
# Build the final archive
735789
archive_name = (
736790
CROSS_BUILD_DIR
737791
/ "dist"
738-
/ f"python-{version}-{context.platform}-XCframework"
792+
/ (
793+
f"python-{version}{context.abiflags}"
794+
f"-{context.platform}-XCframework"
795+
)
739796
)
740797

741798
print()
@@ -799,7 +856,9 @@ def test(context: argparse.Namespace, host: str | None = None) -> None: # noqa:
799856
)
800857
if host in {"all", "hosts"}:
801858
framework_path = (
802-
CROSS_BUILD_DIR / context.platform / "Python.xcframework"
859+
CROSS_BUILD_DIR
860+
/ context.platform
861+
/ f"{framework_name(context.abiflags)}.xcframework"
803862
)
804863
else:
805864
build_arch = platform.machine()
@@ -974,7 +1033,7 @@ def parse_args() -> argparse.Namespace:
9741033
),
9751034
)
9761035

977-
# --cross-build-dir argument
1036+
# --cross-build-dir and --abiflags arguments
9781037
for cmd in [
9791038
clean,
9801039
configure_build,
@@ -994,9 +1053,20 @@ def parse_args() -> argparse.Namespace:
9941053
dest="cross_build_dir",
9951054
type=Path,
9961055
help=(
997-
"Path to the cross-build directory "
1056+
"Base path of the cross-build directory "
9981057
f"(default: {CROSS_BUILD_DIR}). Can also be set "
999-
"with the CROSS_BUILD_DIR environment variable."
1058+
"with the CROSS_BUILD_DIR environment variable. If "
1059+
"--abiflags is specified, '-<abiflags>' is appended."
1060+
),
1061+
)
1062+
cmd.add_argument(
1063+
"--abiflags",
1064+
default="",
1065+
type=validate_abiflags,
1066+
help=(
1067+
"The ABI flags of the build: 't' (free-threaded; "
1068+
"--disable-gil), 'd' (debug; --with-pydebug), or 'td' (both). "
1069+
"Defaults to '' (a standard build)."
10001070
),
10011071
)
10021072

@@ -1082,10 +1152,12 @@ def signal_handler(*args):
10821152
# Process command line arguments
10831153
context = parse_args()
10841154

1085-
# Set the CROSS_BUILD_DIR if an argument was provided
1155+
# Set the CROSS_BUILD_DIR, using any provided base directory, plus any ABI
1156+
# flags as a suffix.
1157+
global CROSS_BUILD_DIR
10861158
if context.cross_build_dir:
1087-
global CROSS_BUILD_DIR
10881159
CROSS_BUILD_DIR = context.cross_build_dir.resolve()
1160+
CROSS_BUILD_DIR = cross_build_dir(CROSS_BUILD_DIR, context.abiflags)
10891161

10901162
dispatch: dict[str, Callable] = {
10911163
"clean": clean,

0 commit comments

Comments
 (0)