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
81120def 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