From 5445d34382a0406397212ea184ac5639a363ab54 Mon Sep 17 00:00:00 2001 From: mementorc Date: Mon, 14 Sep 2026 14:46:16 -0500 Subject: [PATCH 1/3] Select the abi3 suffix matching the host GIL mode --- crossenv/__init__.py | 9 +++++++++ crossenv/scripts/importlib-machinery-patch.py.tmpl | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/crossenv/__init__.py b/crossenv/__init__.py index b1119f1..65b5acd 100644 --- a/crossenv/__init__.py +++ b/crossenv/__init__.py @@ -616,6 +616,15 @@ def get_uname_info(self): "EXT_SUFFIX" ] + # CPython lists the abi3 suffixes in Python/dynload_shlib.c. The plain + # ".abi3" entries are inside an "#ifndef Py_GIL_DISABLED" block, so a + # free-threaded build has only the ".abi3t" forms in its filetab. + # Pick the one the HOST interpreter can actually load. + if self.host_sysconfigdata.build_time_vars.get("Py_GIL_DISABLED"): + self.sysconfig_abi3_suffix = ".abi3t.so" + else: + self.sysconfig_abi3_suffix = ".abi3.so" + # Normalize case of the host system/sysname. self.host_system = { "ios": "iOS", diff --git a/crossenv/scripts/importlib-machinery-patch.py.tmpl b/crossenv/scripts/importlib-machinery-patch.py.tmpl index a0bca50..521f85e 100644 --- a/crossenv/scripts/importlib-machinery-patch.py.tmpl +++ b/crossenv/scripts/importlib-machinery-patch.py.tmpl @@ -29,4 +29,8 @@ def _PathFinder_find_spec(cls, fullname, path=None, target=None): PathFinder.find_spec = _PathFinder_find_spec -EXTENSION_SUFFIXES = [{{repr(self.sysconfig_ext_suffix)}}, ".abi3.so", ".so"] +EXTENSION_SUFFIXES = [ + {{repr(self.sysconfig_ext_suffix)}}, + {{repr(self.sysconfig_abi3_suffix)}}, + ".so", +] From 46b5b23879dc512cda1e83f61f8083f806ef8331 Mon Sep 17 00:00:00 2001 From: mementorc Date: Mon, 14 Sep 2026 15:52:36 -0500 Subject: [PATCH 2/3] Guard abi3 suffixes by host version, add platform-tagged form --- crossenv/__init__.py | 52 ++++++++++++++++--- .../scripts/importlib-machinery-patch.py.tmpl | 6 +-- tests/test_environment.py | 44 ++++++++++++++++ 3 files changed, 89 insertions(+), 13 deletions(-) diff --git a/crossenv/__init__.py b/crossenv/__init__.py index 65b5acd..5cd68bc 100644 --- a/crossenv/__init__.py +++ b/crossenv/__init__.py @@ -616,14 +616,9 @@ def get_uname_info(self): "EXT_SUFFIX" ] - # CPython lists the abi3 suffixes in Python/dynload_shlib.c. The plain - # ".abi3" entries are inside an "#ifndef Py_GIL_DISABLED" block, so a - # free-threaded build has only the ".abi3t" forms in its filetab. - # Pick the one the HOST interpreter can actually load. - if self.host_sysconfigdata.build_time_vars.get("Py_GIL_DISABLED"): - self.sysconfig_abi3_suffix = ".abi3t.so" - else: - self.sysconfig_abi3_suffix = ".abi3.so" + self.sysconfig_extension_suffixes = ( + [self.sysconfig_ext_suffix] + self._host_abi3_suffixes() + [".so"] + ) # Normalize case of the host system/sysname. self.host_system = { @@ -640,6 +635,47 @@ def get_uname_info(self): "watchos": "Darwin", }.get(self.host_sys_platform, self.host_sys_platform.title()) + def _host_soabi_platform(self): + """ + SOABI_PLATFORM from the host's EXT_SUFFIX, or "" if absent. + + EXT_SUFFIX is "." + SOABI + ".so", and SOABI is + "cpython-[-]". CPython only emits the + platform-tagged abi3 suffixes when that triplet is defined. + """ + ext_suffix = self.sysconfig_ext_suffix + if not ext_suffix.startswith(".") or not ext_suffix.endswith(".so"): + return "" + return "-".join(ext_suffix[1:-3].split("-")[2:]) + + def _host_abi3_suffixes(self): + """ + Stable-ABI suffixes the HOST interpreter can load. + + CPython builds these in Python/dynload_shlib.c. The plain ".abi3" + entries sit inside an "#ifndef Py_GIL_DISABLED" block, and both the + platform-tagged forms and the ".abi3t" forms only exist from 3.15. + """ + free_threaded = self.host_sysconfigdata.build_time_vars.get("Py_GIL_DISABLED") + try: + version_info = tuple(int(p) for p in self.host_version.split(".")) + except ValueError: + version_info = () + + if version_info < (3, 15): + # Free-threaded builds had no stable ABI before 3.15. + return [] if free_threaded else [".abi3.so"] + + stem = "abi3t" if free_threaded else "abi3" + # Plain form first, deliberately: setuptools get_abi3_suffix() returns + # the first entry containing ".abi3", and the portable name is the one + # we want emitted for a cross build. + suffixes = [".%s.so" % stem] + soabi_platform = self._host_soabi_platform() + if soabi_platform: + suffixes.append(".%s-%s.so" % (stem, soabi_platform)) + return suffixes + def expand_platform_tags(self): """ Convert legacy manylinux tags to PEP600, because pip only looks for one diff --git a/crossenv/scripts/importlib-machinery-patch.py.tmpl b/crossenv/scripts/importlib-machinery-patch.py.tmpl index 521f85e..82df061 100644 --- a/crossenv/scripts/importlib-machinery-patch.py.tmpl +++ b/crossenv/scripts/importlib-machinery-patch.py.tmpl @@ -29,8 +29,4 @@ def _PathFinder_find_spec(cls, fullname, path=None, target=None): PathFinder.find_spec = _PathFinder_find_spec -EXTENSION_SUFFIXES = [ - {{repr(self.sysconfig_ext_suffix)}}, - {{repr(self.sysconfig_abi3_suffix)}}, - ".so", -] +EXTENSION_SUFFIXES = {{repr(self.sysconfig_extension_suffixes)}} diff --git a/tests/test_environment.py b/tests/test_environment.py index f0c875e..e0ca610 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -294,3 +294,47 @@ def test_machine_override(tmp_path, host_python, build_python): ) out = out.strip() assert out == "foobar foobar" + + +def test_abi3_extension_suffixes(crossenv): + out = crossenv.check_output( + [ + "python", + "-c", + dedent("""\ + import sys + import sysconfig + from importlib.machinery import EXTENSION_SUFFIXES + print(sys.version_info.major, sys.version_info.minor) + print(1 if sysconfig.get_config_var("Py_GIL_DISABLED") else 0) + print(" ".join(EXTENSION_SUFFIXES)) + """), + ], + universal_newlines=True, + ) + version_line, gil_line, suffix_line = out.strip().splitlines() + version_info = tuple(int(part) for part in version_line.split()) + free_threaded = gil_line == "1" + suffixes = suffix_line.split() + + ext_suffix = suffixes[0] + assert suffixes[-1] == ".so" + + abi3 = [suffix for suffix in suffixes if ".abi3" in suffix] + + if version_info < (3, 15): + # Free-threaded builds had no stable ABI before 3.15, and the + # platform-tagged forms did not exist yet. + assert abi3 == ([] if free_threaded else [".abi3.so"]) + return + + # setuptools get_abi3_suffix() returns the first entry containing ".abi3", + # so the untagged form has to come first. + stem = ".abi3t" if free_threaded else ".abi3" + assert abi3[0] == stem + ".so" + + # Any platform-tagged form must carry the host's own tag, not the build + # machine's. + for suffix in abi3[1:]: + tag = suffix.split("-", 1)[1] + assert ext_suffix.endswith("-" + tag) From ca5095a61917b5bdba719183b3143848e11df7a9 Mon Sep 17 00:00:00 2001 From: mementorc Date: Mon, 14 Sep 2026 17:05:20 -0500 Subject: [PATCH 3/3] Assert the tagged abi3 form is offered when the host has a triplet --- tests/test_environment.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_environment.py b/tests/test_environment.py index e0ca610..2721ccb 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -333,6 +333,11 @@ def test_abi3_extension_suffixes(crossenv): stem = ".abi3t" if free_threaded else ".abi3" assert abi3[0] == stem + ".so" + # A host with a PLATFORM_TRIPLET offers the tagged form too, so the loop + # below is not vacuous. + if len(ext_suffix[1:-3].split("-")) > 2: + assert len(abi3) == 2 + # Any platform-tagged form must carry the host's own tag, not the build # machine's. for suffix in abi3[1:]: