4646import sys
4747import sysconfig
4848import time
49- from collections .abc import Sequence
49+ from collections .abc import Callable , Sequence
5050from contextlib import contextmanager
5151from datetime import datetime , timezone
5252from os .path import basename , relpath
5353from pathlib import Path
5454from subprocess import CalledProcessError
55- from typing import Callable
5655
5756EnvironmentT = dict [str , str ]
5857ArgsT = Sequence [str | Path ]
@@ -140,17 +139,15 @@ def print_env(env: EnvironmentT) -> None:
140139def apple_env (host : str ) -> EnvironmentT :
141140 """Construct an Apple development environment for the given host."""
142141 env = {
143- "PATH" : ":" .join (
144- [
145- str (PYTHON_DIR / "Apple/iOS/Resources/bin" ),
146- str (subdir (host ) / "prefix" ),
147- "/usr/bin" ,
148- "/bin" ,
149- "/usr/sbin" ,
150- "/sbin" ,
151- "/Library/Apple/usr/bin" ,
152- ]
153- ),
142+ "PATH" : ":" .join ([
143+ str (PYTHON_DIR / "Apple/iOS/Resources/bin" ),
144+ str (subdir (host ) / "prefix" ),
145+ "/usr/bin" ,
146+ "/bin" ,
147+ "/usr/sbin" ,
148+ "/sbin" ,
149+ "/Library/Apple/usr/bin" ,
150+ ]),
154151 }
155152
156153 return env
@@ -196,14 +193,10 @@ def clean(context: argparse.Namespace, target: str = "all") -> None:
196193 paths .append (target )
197194
198195 if target in {"all" , "hosts" , "test" }:
199- paths .extend (
200- [
201- path .name
202- for path in CROSS_BUILD_DIR .glob (
203- f"{ context .platform } -testbed.*"
204- )
205- ]
206- )
196+ paths .extend ([
197+ path .name
198+ for path in CROSS_BUILD_DIR .glob (f"{ context .platform } -testbed.*" )
199+ ])
207200
208201 for path in paths :
209202 delete_path (path )
@@ -352,18 +345,16 @@ def download(url: str, target_dir: Path) -> Path:
352345
353346 out_path = target_path / basename (url )
354347 if not Path (out_path ).is_file ():
355- run (
356- [
357- "curl" ,
358- "-Lf" ,
359- "--retry" ,
360- "5" ,
361- "--retry-all-errors" ,
362- "-o" ,
363- out_path ,
364- url ,
365- ]
366- )
348+ run ([
349+ "curl" ,
350+ "-Lf" ,
351+ "--retry" ,
352+ "5" ,
353+ "--retry-all-errors" ,
354+ "-o" ,
355+ out_path ,
356+ url ,
357+ ])
367358 else :
368359 print (f"Using cached version of { basename (url )} " )
369360 return out_path
@@ -468,8 +459,7 @@ def package_version(prefix_path: Path) -> str:
468459
469460
470461def lib_platform_files (dirname , names ):
471- """A file filter that ignores platform-specific files in the lib directory.
472- """
462+ """A file filter that ignores platform-specific files in lib."""
473463 path = Path (dirname )
474464 if (
475465 path .parts [- 3 ] == "lib"
@@ -478,15 +468,21 @@ def lib_platform_files(dirname, names):
478468 ):
479469 return names
480470 elif path .parts [- 2 ] == "lib" and path .parts [- 1 ].startswith ("python" ):
481- ignored_names = set (
471+ ignored_names = {
482472 name
483473 for name in names
484474 if (
485475 name .startswith ("_sysconfigdata_" )
486476 or name .startswith ("_sysconfig_vars_" )
487477 or name == "build-details.json"
488478 )
489- )
479+ }
480+ elif path .parts [- 1 ] == "lib" :
481+ ignored_names = {
482+ name
483+ for name in names
484+ if name .startswith ("libpython" ) and name .endswith (".dylib" )
485+ }
490486 else :
491487 ignored_names = set ()
492488
@@ -499,7 +495,9 @@ def lib_non_platform_files(dirname, names):
499495 """
500496 path = Path (dirname )
501497 if path .parts [- 2 ] == "lib" and path .parts [- 1 ].startswith ("python" ):
502- return set (names ) - lib_platform_files (dirname , names ) - {"lib-dynload" }
498+ return (
499+ set (names ) - lib_platform_files (dirname , names ) - {"lib-dynload" }
500+ )
503501 else :
504502 return set ()
505503
@@ -514,7 +512,8 @@ def create_xcframework(platform: str) -> str:
514512 package_path .mkdir ()
515513 except FileExistsError :
516514 raise RuntimeError (
517- f"{ platform } XCframework already exists; do you need to run with --clean?"
515+ f"{ platform } XCframework already exists; do you need to run "
516+ "with --clean?"
518517 ) from None
519518
520519 frameworks = []
@@ -607,7 +606,7 @@ def create_xcframework(platform: str) -> str:
607606 print (f" - { slice_name } binaries" )
608607 shutil .copytree (first_path / "bin" , slice_path / "bin" )
609608
610- # Copy the include path (this will be a symlink to the framework headers)
609+ # Copy the include path (a symlink to the framework headers)
611610 print (f" - { slice_name } include files" )
612611 shutil .copytree (
613612 first_path / "include" ,
@@ -621,6 +620,12 @@ def create_xcframework(platform: str) -> str:
621620 slice_framework / "Headers/pyconfig.h" ,
622621 )
623622
623+ print (f" - { slice_name } shared library" )
624+ # Create a simlink for the fat library
625+ shared_lib = slice_path / f"lib/libpython{ version_tag } .dylib"
626+ shared_lib .parent .mkdir ()
627+ shared_lib .symlink_to ("../Python.framework/Python" )
628+
624629 print (f" - { slice_name } architecture-specific files" )
625630 for host_triple , multiarch in slice_parts .items ():
626631 print (f" - { multiarch } standard library" )
@@ -632,13 +637,15 @@ def create_xcframework(platform: str) -> str:
632637 framework_path (host_triple , multiarch ) / "lib" ,
633638 package_path / "Python.xcframework/lib" ,
634639 ignore = lib_platform_files ,
640+ symlinks = True ,
635641 )
636642 has_common_stdlib = True
637643
638644 shutil .copytree (
639645 framework_path (host_triple , multiarch ) / "lib" ,
640646 slice_path / f"lib-{ arch } " ,
641647 ignore = lib_non_platform_files ,
648+ symlinks = True ,
642649 )
643650
644651 # Copy the host's pyconfig.h to an architecture-specific name.
@@ -659,7 +666,8 @@ def create_xcframework(platform: str) -> str:
659666 # statically link those libraries into a Framework, you become
660667 # responsible for providing a privacy manifest for that framework.
661668 xcprivacy_file = {
662- "OpenSSL" : subdir (host_triple ) / "prefix/share/OpenSSL.xcprivacy"
669+ "OpenSSL" : subdir (host_triple )
670+ / "prefix/share/OpenSSL.xcprivacy"
663671 }
664672 print (f" - { multiarch } xcprivacy files" )
665673 for module , lib in [
@@ -669,7 +677,8 @@ def create_xcframework(platform: str) -> str:
669677 shutil .copy (
670678 xcprivacy_file [lib ],
671679 slice_path
672- / f"lib-{ arch } /python{ version_tag } /lib-dynload/{ module } .xcprivacy" ,
680+ / f"lib-{ arch } /python{ version_tag } "
681+ / f"lib-dynload/{ module } .xcprivacy" ,
673682 )
674683
675684 print (" - build tools" )
@@ -692,18 +701,16 @@ def package(context: argparse.Namespace) -> None:
692701
693702 # Clone testbed
694703 print ()
695- run (
696- [
697- sys .executable ,
698- "Apple/testbed" ,
699- "clone" ,
700- "--platform" ,
701- context .platform ,
702- "--framework" ,
703- CROSS_BUILD_DIR / context .platform / "Python.xcframework" ,
704- CROSS_BUILD_DIR / context .platform / "testbed" ,
705- ]
706- )
704+ run ([
705+ sys .executable ,
706+ "Apple/testbed" ,
707+ "clone" ,
708+ "--platform" ,
709+ context .platform ,
710+ "--framework" ,
711+ CROSS_BUILD_DIR / context .platform / "Python.xcframework" ,
712+ CROSS_BUILD_DIR / context .platform / "testbed" ,
713+ ])
707714
708715 # Build the final archive
709716 archive_name = (
@@ -757,7 +764,7 @@ def build(context: argparse.Namespace, host: str | None = None) -> None:
757764 package (context )
758765
759766
760- def test (context : argparse .Namespace , host : str | None = None ) -> None :
767+ def test (context : argparse .Namespace , host : str | None = None ) -> None : # noqa: PT028
761768 """The implementation of the "test" command."""
762769 if host is None :
763770 host = context .host
@@ -795,18 +802,16 @@ def test(context: argparse.Namespace, host: str | None = None) -> None:
795802 / f"Frameworks/{ apple_multiarch (host )} "
796803 )
797804
798- run (
799- [
800- sys .executable ,
801- "Apple/testbed" ,
802- "clone" ,
803- "--platform" ,
804- context .platform ,
805- "--framework" ,
806- framework_path ,
807- testbed_dir ,
808- ]
809- )
805+ run ([
806+ sys .executable ,
807+ "Apple/testbed" ,
808+ "clone" ,
809+ "--platform" ,
810+ context .platform ,
811+ "--framework" ,
812+ framework_path ,
813+ testbed_dir ,
814+ ])
810815
811816 run (
812817 [
@@ -840,7 +845,7 @@ def apple_sim_host(platform_name: str) -> str:
840845 """Determine the native simulator target for this platform."""
841846 for _ , slice_parts in HOSTS [platform_name ].items ():
842847 for host_triple in slice_parts :
843- parts = host_triple .split ('-' )
848+ parts = host_triple .split ("-" )
844849 if parts [0 ] == platform .machine () and parts [- 1 ] == "simulator" :
845850 return host_triple
846851
@@ -968,20 +973,29 @@ def parse_args() -> argparse.Namespace:
968973 cmd .add_argument (
969974 "--simulator" ,
970975 help = (
971- "The name of the simulator to use (eg: 'iPhone 16e'). Defaults to "
972- "the most recently released 'entry level' iPhone device. Device "
973- "architecture and OS version can also be specified; e.g., "
974- "`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would run on "
975- "an ARM64 iPhone 16 Pro simulator running iOS 26.0."
976+ "The name of the simulator to use (eg: 'iPhone 16e'). "
977+ "Defaults to the most recently released 'entry level' "
978+ "iPhone device. Device architecture and OS version can also "
979+ "be specified; e.g., "
980+ "`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would "
981+ "run on an ARM64 iPhone 16 Pro simulator running iOS 26.0."
976982 ),
977983 )
978984 group = cmd .add_mutually_exclusive_group ()
979985 group .add_argument (
980- "--fast-ci" , action = "store_const" , dest = "ci_mode" , const = "fast" ,
981- help = "Add test arguments for GitHub Actions" )
986+ "--fast-ci" ,
987+ action = "store_const" ,
988+ dest = "ci_mode" ,
989+ const = "fast" ,
990+ help = "Add test arguments for GitHub Actions" ,
991+ )
982992 group .add_argument (
983- "--slow-ci" , action = "store_const" , dest = "ci_mode" , const = "slow" ,
984- help = "Add test arguments for buildbots" )
993+ "--slow-ci" ,
994+ action = "store_const" ,
995+ dest = "ci_mode" ,
996+ const = "slow" ,
997+ help = "Add test arguments for buildbots" ,
998+ )
985999
9861000 for subcommand in [configure_build , configure_host , build , ci ]:
9871001 subcommand .add_argument (
0 commit comments