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
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.2
hooks:
- id: ruff-check
name: Run Ruff (lint) on Apple/
args: [--exit-non-zero-on-fix, --config=Apple/.ruff.toml]
files: ^Apple/
- id: ruff-check
name: Run Ruff (lint) on Doc/
args: [--exit-non-zero-on-fix]
Expand Down Expand Up @@ -30,6 +34,10 @@ repos:
name: Run Ruff (lint) on Tools/wasm/
args: [--exit-non-zero-on-fix, --config=Tools/wasm/.ruff.toml]
files: ^Tools/wasm/
- id: ruff-format
name: Run Ruff (format) on Apple/
args: [--exit-non-zero-on-fix, --config=Apple/.ruff.toml]
files: ^Apple
- id: ruff-format
name: Run Ruff (format) on Doc/
args: [--check]
Expand Down
22 changes: 22 additions & 0 deletions Apple/.ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
extend = "../.ruff.toml" # Inherit the project-wide settings

[format]
preview = true
docstring-code-format = true

[lint]
select = [
"C4", # flake8-comprehensions
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"PGH", # pygrep-hooks
"PT", # flake8-pytest-style
"PYI", # flake8-pyi
"RUF100", # Ban unused `# noqa` comments
"UP", # pyupgrade
"W", # pycodestyle
"YTT", # flake8-2020
]
154 changes: 77 additions & 77 deletions Apple/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,12 @@
import sys
import sysconfig
import time
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from contextlib import contextmanager
from datetime import datetime, timezone
from os.path import basename, relpath
from pathlib import Path
from subprocess import CalledProcessError
from typing import Callable

EnvironmentT = dict[str, str]
ArgsT = Sequence[str | Path]
Expand Down Expand Up @@ -140,17 +139,15 @@ def print_env(env: EnvironmentT) -> None:
def apple_env(host: str) -> EnvironmentT:
"""Construct an Apple development environment for the given host."""
env = {
"PATH": ":".join(
[
str(PYTHON_DIR / "Apple/iOS/Resources/bin"),
str(subdir(host) / "prefix"),
"/usr/bin",
"/bin",
"/usr/sbin",
"/sbin",
"/Library/Apple/usr/bin",
]
),
"PATH": ":".join([
str(PYTHON_DIR / "Apple/iOS/Resources/bin"),
str(subdir(host) / "prefix"),
"/usr/bin",
"/bin",
"/usr/sbin",
"/sbin",
"/Library/Apple/usr/bin",
]),
}

return env
Expand Down Expand Up @@ -196,14 +193,10 @@ def clean(context: argparse.Namespace, target: str = "all") -> None:
paths.append(target)

if target in {"all", "hosts", "test"}:
paths.extend(
[
path.name
for path in CROSS_BUILD_DIR.glob(
f"{context.platform}-testbed.*"
)
]
)
paths.extend([
path.name
for path in CROSS_BUILD_DIR.glob(f"{context.platform}-testbed.*")
])

for path in paths:
delete_path(path)
Expand Down Expand Up @@ -352,18 +345,16 @@ def download(url: str, target_dir: Path) -> Path:

out_path = target_path / basename(url)
if not Path(out_path).is_file():
run(
[
"curl",
"-Lf",
"--retry",
"5",
"--retry-all-errors",
"-o",
out_path,
url,
]
)
run([
"curl",
"-Lf",
"--retry",
"5",
"--retry-all-errors",
"-o",
out_path,
url,
])
else:
print(f"Using cached version of {basename(url)}")
return out_path
Expand Down Expand Up @@ -468,8 +459,7 @@ def package_version(prefix_path: Path) -> str:


def lib_platform_files(dirname, names):
"""A file filter that ignores platform-specific files in the lib directory.
"""
"""A file filter that ignores platform-specific files in lib."""
path = Path(dirname)
if (
path.parts[-3] == "lib"
Expand All @@ -478,15 +468,15 @@ def lib_platform_files(dirname, names):
):
return names
elif path.parts[-2] == "lib" and path.parts[-1].startswith("python"):
ignored_names = set(
ignored_names = {
name
for name in names
if (
name.startswith("_sysconfigdata_")
or name.startswith("_sysconfig_vars_")
or name == "build-details.json"
)
)
}
else:
ignored_names = set()

Expand All @@ -499,7 +489,9 @@ def lib_non_platform_files(dirname, names):
"""
path = Path(dirname)
if path.parts[-2] == "lib" and path.parts[-1].startswith("python"):
return set(names) - lib_platform_files(dirname, names) - {"lib-dynload"}
return (
set(names) - lib_platform_files(dirname, names) - {"lib-dynload"}
)
else:
return set()

Expand All @@ -514,7 +506,8 @@ def create_xcframework(platform: str) -> str:
package_path.mkdir()
except FileExistsError:
raise RuntimeError(
f"{platform} XCframework already exists; do you need to run with --clean?"
f"{platform} XCframework already exists; do you need to run "
"with --clean?"
) from None

frameworks = []
Expand Down Expand Up @@ -607,7 +600,7 @@ def create_xcframework(platform: str) -> str:
print(f" - {slice_name} binaries")
shutil.copytree(first_path / "bin", slice_path / "bin")

# Copy the include path (this will be a symlink to the framework headers)
# Copy the include path (a symlink to the framework headers)
print(f" - {slice_name} include files")
shutil.copytree(
first_path / "include",
Expand Down Expand Up @@ -659,7 +652,8 @@ def create_xcframework(platform: str) -> str:
# statically link those libraries into a Framework, you become
# responsible for providing a privacy manifest for that framework.
xcprivacy_file = {
"OpenSSL": subdir(host_triple) / "prefix/share/OpenSSL.xcprivacy"
"OpenSSL": subdir(host_triple)
/ "prefix/share/OpenSSL.xcprivacy"
}
print(f" - {multiarch} xcprivacy files")
for module, lib in [
Expand All @@ -669,7 +663,8 @@ def create_xcframework(platform: str) -> str:
shutil.copy(
xcprivacy_file[lib],
slice_path
/ f"lib-{arch}/python{version_tag}/lib-dynload/{module}.xcprivacy",
/ f"lib-{arch}/python{version_tag}"
/ f"lib-dynload/{module}.xcprivacy",
)

print(" - build tools")
Expand All @@ -692,18 +687,16 @@ def package(context: argparse.Namespace) -> None:

# Clone testbed
print()
run(
[
sys.executable,
"Apple/testbed",
"clone",
"--platform",
context.platform,
"--framework",
CROSS_BUILD_DIR / context.platform / "Python.xcframework",
CROSS_BUILD_DIR / context.platform / "testbed",
]
)
run([
sys.executable,
"Apple/testbed",
"clone",
"--platform",
context.platform,
"--framework",
CROSS_BUILD_DIR / context.platform / "Python.xcframework",
CROSS_BUILD_DIR / context.platform / "testbed",
])

# Build the final archive
archive_name = (
Expand Down Expand Up @@ -757,7 +750,7 @@ def build(context: argparse.Namespace, host: str | None = None) -> None:
package(context)


def test(context: argparse.Namespace, host: str | None = None) -> None:
def test(context: argparse.Namespace, host: str | None = None) -> None: # noqa: PT028
"""The implementation of the "test" command."""
if host is None:
host = context.host
Expand Down Expand Up @@ -795,18 +788,16 @@ def test(context: argparse.Namespace, host: str | None = None) -> None:
/ f"Frameworks/{apple_multiarch(host)}"
)

run(
[
sys.executable,
"Apple/testbed",
"clone",
"--platform",
context.platform,
"--framework",
framework_path,
testbed_dir,
]
)
run([
sys.executable,
"Apple/testbed",
"clone",
"--platform",
context.platform,
"--framework",
framework_path,
testbed_dir,
])

run(
[
Expand Down Expand Up @@ -840,7 +831,7 @@ def apple_sim_host(platform_name: str) -> str:
"""Determine the native simulator target for this platform."""
for _, slice_parts in HOSTS[platform_name].items():
for host_triple in slice_parts:
parts = host_triple.split('-')
parts = host_triple.split("-")
if parts[0] == platform.machine() and parts[-1] == "simulator":
return host_triple

Expand Down Expand Up @@ -968,20 +959,29 @@ def parse_args() -> argparse.Namespace:
cmd.add_argument(
"--simulator",
help=(
"The name of the simulator to use (eg: 'iPhone 16e'). Defaults to "
"the most recently released 'entry level' iPhone device. Device "
"architecture and OS version can also be specified; e.g., "
"`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would run on "
"an ARM64 iPhone 16 Pro simulator running iOS 26.0."
"The name of the simulator to use (eg: 'iPhone 16e'). "
"Defaults to the most recently released 'entry level' "
"iPhone device. Device architecture and OS version can also "
"be specified; e.g., "
"`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would "
"run on an ARM64 iPhone 16 Pro simulator running iOS 26.0."
),
)
group = cmd.add_mutually_exclusive_group()
group.add_argument(
"--fast-ci", action="store_const", dest="ci_mode", const="fast",
help="Add test arguments for GitHub Actions")
"--fast-ci",
action="store_const",
dest="ci_mode",
const="fast",
help="Add test arguments for GitHub Actions",
)
group.add_argument(
"--slow-ci", action="store_const", dest="ci_mode", const="slow",
help="Add test arguments for buildbots")
"--slow-ci",
action="store_const",
dest="ci_mode",
const="slow",
help="Add test arguments for buildbots",
)

for subcommand in [configure_build, configure_host, build, ci]:
subcommand.add_argument(
Expand Down
33 changes: 19 additions & 14 deletions Apple/testbed/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ def select_simulator_device(platform):
json_data = json.loads(raw_json)

if platform == "iOS":
# Any iOS device will do; we'll look for "SE" devices - but the name isn't
# consistent over time. Older Xcode versions will use "iPhone SE (Nth
# generation)"; As of 2025, they've started using "iPhone 16e".
# Any iOS device will do; we'll look for "SE" devices - but the name
# isn't consistent over time. Older Xcode versions will use "iPhone SE
# (Nth generation)"; As of 2025, they've started using "iPhone 16e".
#
# When Xcode is updated after a new release, new devices will be available
# and old ones will be dropped from the set available on the latest iOS
# version. Select the one with the highest minimum runtime version - this
# is an indicator of the "newest" released device, which should always be
# supported on the "most recent" iOS version.
# When Xcode is updated after a new release, new devices will be
# available and old ones will be dropped from the set available on the
# latest iOS version. Select the one with the highest minimum runtime
# version - this is an indicator of the "newest" released device, which
# should always be supported on the "most recent" iOS version.
se_simulators = sorted(
(devicetype["minRuntimeVersion"], devicetype["name"])
for devicetype in json_data["devicetypes"]
Expand Down Expand Up @@ -295,7 +295,8 @@ def main():

parser = argparse.ArgumentParser(
description=(
"Manages the process of testing an Apple Python project through Xcode."
"Manages the process of testing an Apple Python project "
"through Xcode."
),
)

Expand Down Expand Up @@ -336,7 +337,10 @@ def main():

run = subcommands.add_parser(
"run",
usage="%(prog)s [-h] [--simulator SIMULATOR] -- <test arg> [<test arg> ...]",
usage=(
"%(prog)s [-h] [--simulator SIMULATOR] -- "
"<test arg> [<test arg> ...]"
),
description=(
"Run a testbed project. The arguments provided after `--` will be "
"passed to the running iOS process as if they were arguments to "
Expand Down Expand Up @@ -397,9 +401,9 @@ def main():
/ "bin"
).is_dir():
print(
f"Testbed does not contain a compiled Python framework. Use "
f"`python {sys.argv[0]} clone ...` to create a runnable "
f"clone of this testbed."
"Testbed does not contain a compiled Python framework. "
f"Use `python {sys.argv[0]} clone ...` to create a "
"runnable clone of this testbed."
)
sys.exit(20)

Expand All @@ -411,7 +415,8 @@ def main():
)
else:
print(
f"Must specify test arguments (e.g., {sys.argv[0]} run -- test)"
"Must specify test arguments "
f"(e.g., {sys.argv[0]} run -- test)"
)
print()
parser.print_help(sys.stderr)
Expand Down
Loading