diff --git a/crossenv/__init__.py b/crossenv/__init__.py index b1119f1..5cd68bc 100644 --- a/crossenv/__init__.py +++ b/crossenv/__init__.py @@ -616,6 +616,10 @@ def get_uname_info(self): "EXT_SUFFIX" ] + self.sysconfig_extension_suffixes = ( + [self.sysconfig_ext_suffix] + self._host_abi3_suffixes() + [".so"] + ) + # Normalize case of the host system/sysname. self.host_system = { "ios": "iOS", @@ -631,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 a0bca50..82df061 100644 --- a/crossenv/scripts/importlib-machinery-patch.py.tmpl +++ b/crossenv/scripts/importlib-machinery-patch.py.tmpl @@ -29,4 +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)}}, ".abi3.so", ".so"] +EXTENSION_SUFFIXES = {{repr(self.sysconfig_extension_suffixes)}} diff --git a/tests/test_environment.py b/tests/test_environment.py index f0c875e..2721ccb 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -294,3 +294,52 @@ 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" + + # 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:]: + tag = suffix.split("-", 1)[1] + assert ext_suffix.endswith("-" + tag)