diff --git a/Lib/test/test_build_details.py b/Lib/test/test_build_details.py index 30d9c213077ab75..b6f4b6fe26379bb 100644 --- a/Lib/test/test_build_details.py +++ b/Lib/test/test_build_details.py @@ -7,6 +7,7 @@ import string import unittest from pathlib import Path +from unittest import mock from test.support import is_android, is_apple_mobile, is_wasm32 @@ -173,6 +174,23 @@ def test_c_api(self): version = sysconfig.get_config_var('VERSION') self.assertTrue(os.path.exists(os.path.join(value['pkgconfig_path'], f'python-{version}.pc'))) + @needs_installed_python + @unittest.skipIf( + is_android or is_apple_mobile, + "Android and iOS run tests via a custom testbed method that doesn't ship libpython" + ) + def test_libpython_static(self): + value = self.data.get('libpython', {}) + if 'static' not in value: + self.skipTest("No static libpython") + + if sysconfig._installation_is_relocated(): + self.skipTest("Installation is relocated") + + # The path must name an installed static library (an 'ar' archive). + with open(value['static'], 'rb') as f: + self.assertEqual(f.read(8), b'!\n') + @unittest.skipIf( generate_build_details is None, @@ -233,5 +251,61 @@ def test_round_trip(self): self.assertEqual(data_abs_path[part][key], abs_rel_path) +@unittest.skipIf( + generate_build_details is None, + "Failed to import generate-build-details" +) +@unittest.skipIf(os.name != 'posix', 'Feature only implemented on POSIX') +@unittest.skipIf(is_wasm32, 'Feature not available on WebAssembly builds') +class BuildDetailsLibpythonTests(unittest.TestCase): + LIBDIR = '/prefix/lib' + LIBPL = '/prefix/lib/python3.X/config-3.X' + + def generate_libpython(self, **config_vars): + """Generate the libpython section with overridden config vars.""" + config_vars = dict(LIBDIR=self.LIBDIR, LIBPL=self.LIBPL, + LIBRARY='libpython3.X.a', **config_vars) + get_config_var = sysconfig.get_config_var + def patched_get_config_var(name): + if name in config_vars: + return config_vars[name] + return get_config_var(name) + with mock.patch('sysconfig.get_config_var', patched_get_config_var): + data = generate_build_details.generate_data(schema_version='1.0') + return json.loads(json.dumps(data)).get('libpython', {}) + + def test_static_only(self): + libpython = self.generate_libpython( + LDLIBRARY='libpython3.X.a', STATIC_LIBPYTHON=1, + PYTHONFRAMEWORK='') + self.assertNotIn('dynamic', libpython) + self.assertEqual(libpython['static'], + os.path.join(self.LIBDIR, 'libpython3.X.a')) + + def test_shared_and_static(self): + # The static library is only installed into LIBPL, not LIBDIR. + libpython = self.generate_libpython( + LDLIBRARY='libpython3.X.so', STATIC_LIBPYTHON=1, + PYTHONFRAMEWORK='') + self.assertEqual(libpython['dynamic'], + os.path.join(self.LIBDIR, 'libpython3.X.so')) + self.assertEqual(libpython['static'], + os.path.join(self.LIBPL, 'libpython3.X.a')) + + def test_shared_without_static(self): + libpython = self.generate_libpython( + LDLIBRARY='libpython3.X.so', STATIC_LIBPYTHON=0, + PYTHONFRAMEWORK='') + self.assertNotIn('static', libpython) + + def test_framework(self): + # Framework builds don't install the static library. LIBPL contains + # a libpython3.X.a symlink to the framework's shared library instead. + libpython = self.generate_libpython( + LDLIBRARY='Python.framework/Versions/3.X/Python', + STATIC_LIBPYTHON=1, PYTHONFRAMEWORK='Python') + self.assertNotIn('static', libpython) + + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Build/2026-10-18-12-00-00.gh-issue-159759.LibStA.rst b/Misc/NEWS.d/next/Build/2026-10-18-12-00-00.gh-issue-159759.LibStA.rst new file mode 100644 index 000000000000000..1fb6a51fded6dcb --- /dev/null +++ b/Misc/NEWS.d/next/Build/2026-10-18-12-00-00.gh-issue-159759.LibStA.rst @@ -0,0 +1,4 @@ +Fix the ``libpython.static`` path in :pep:`739` ``build-details.json``, which +pointed to a file that is not installed when building with +:option:`--enable-shared`, and no longer list a static library for framework +builds, which do not install one. diff --git a/Tools/build/generate-build-details.py b/Tools/build/generate-build-details.py index 8272635bc627d6a..738c51026b77cc8 100644 --- a/Tools/build/generate-build-details.py +++ b/Tools/build/generate-build-details.py @@ -72,6 +72,7 @@ def generate_data(schema_version: str) -> collections.defaultdict[str, Any]: data['suffixes']['extensions'] = importlib.machinery.EXTENSION_SUFFIXES LIBDIR = sysconfig.get_config_var('LIBDIR') + LIBPL = sysconfig.get_config_var('LIBPL') LDLIBRARY = sysconfig.get_config_var('LDLIBRARY') LIBRARY = sysconfig.get_config_var('LIBRARY') PY3LIBRARY = sysconfig.get_config_var('PY3LIBRARY') @@ -84,7 +85,14 @@ def generate_data(schema_version: str) -> collections.defaultdict[str, Any]: # dynamic library if enabled, otherwise it's the static library. # If LIBRARY != LDLIBRARY, support for the dynamic library is enabled. has_dynamic_library = LDLIBRARY != LIBRARY - has_static_library = sysconfig.get_config_var('STATIC_LIBPYTHON') + # Framework builds don't install the static library (see libainstall). + has_static_library = ( + sysconfig.get_config_var('STATIC_LIBPYTHON') + and not sysconfig.get_config_var('PYTHONFRAMEWORK') + ) + # The static library is only installed into LIBDIR when it is also + # LDLIBRARY; otherwise it is only installed into LIBPL. + static_library_dir = LIBPL if has_dynamic_library else LIBDIR elif os.name == 'nt': # Windows can only use a dynamic library or a static library. # If it's using a dynamic library, sys.dllhandle will be set. @@ -92,6 +100,7 @@ def generate_data(schema_version: str) -> collections.defaultdict[str, Any]: # More context: https://github.com/python/cpython/issues/110234 has_dynamic_library = hasattr(sys, 'dllhandle') has_static_library = not has_dynamic_library + static_library_dir = LIBDIR else: raise NotADirectoryError(f'Unknown platform: {os.name}') @@ -123,7 +132,7 @@ def generate_data(schema_version: str) -> collections.defaultdict[str, Any]: data['libpython']['link_extensions'] = bool(LIBPYTHON) if has_static_library: - data['libpython']['static'] = os.path.join(LIBDIR, LIBRARY) + data['libpython']['static'] = os.path.join(static_library_dir, LIBRARY) data['c_api']['headers'] = INCLUDEPY if LIBPC: