Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 3 additions & 5 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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;\
Expand Down

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this isn't the correct category, although maybe we should add sections for all support platforms (and one for unsupported?)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOS, Android, and Emscripten work has always been in a bit of an odd position with blurb - I would'nt object at all to an "other platforms" section in the docs (or however it ends up being spelled) as a better home for this sort of change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm working on a little patch. :-)

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
iOS builds can now be configured to generate free-threading or debug output.
124 changes: 98 additions & 26 deletions Platforms/Apple/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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()
]
Expand All @@ -593,15 +643,15 @@ 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])

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
Expand All @@ -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")
Expand All @@ -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():
Expand All @@ -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,
)
Expand All @@ -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",
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -727,15 +781,18 @@ 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",
])

# Build the final archive
archive_name = (
CROSS_BUILD_DIR
/ "dist"
/ f"python-{version}-{context.platform}-XCframework"
/ (
f"python-{version}{context.abiflags}"
f"-{context.platform}-XCframework"
)
)

print()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand All @@ -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)."
),
)

Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading