From 5f35442cff140798ebb633b569cb353cf2e00cc3 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Mon, 19 Oct 2026 13:37:09 -0700 Subject: [PATCH 1/2] gh-159759: Fix libpython.static in PEP 739 build-details.json When a shared library is also built, `make install` only installs the static library into LIBPL, so point libpython.static there instead of LIBDIR. Static-only builds keep the existing LIBDIR path, which is installed in that configuration. Framework builds don't install the static library (LIBPL contains a symlink to the framework's shared library instead), so don't list it. Co-Authored-By: Claude Opus 5.5 --- Lib/test/test_build_details.py | 75 +++++++++++++++++++ ...-10-18-12-00-00.gh-issue-159759.LibStA.rst | 4 + Tools/build/generate-build-details.py | 13 +++- 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Build/2026-10-18-12-00-00.gh-issue-159759.LibStA.rst diff --git a/Lib/test/test_build_details.py b/Lib/test/test_build_details.py index 30d9c213077ab75..eea09259741ee20 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,24 @@ 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") + + # Skip check if installation is relocated + 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 +252,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 right now') +@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: From 7ce4da3cf523bd0bb1e498e0ef43837735c593a8 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Mon, 19 Oct 2026 22:21:01 +0000 Subject: [PATCH 2/2] Apply review suggestions to test_build_details Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y2CKGDURpDRVEHD7FfWnps --- Lib/test/test_build_details.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_build_details.py b/Lib/test/test_build_details.py index eea09259741ee20..b6f4b6fe26379bb 100644 --- a/Lib/test/test_build_details.py +++ b/Lib/test/test_build_details.py @@ -184,11 +184,10 @@ def test_libpython_static(self): if 'static' not in value: self.skipTest("No static libpython") - # Skip check if installation is relocated if sysconfig._installation_is_relocated(): self.skipTest("Installation is relocated") - # The path must name an installed static library (an ar archive). + # 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') @@ -256,7 +255,7 @@ def test_round_trip(self): generate_build_details is None, "Failed to import generate-build-details" ) -@unittest.skipIf(os.name != 'posix', 'Feature only implemented on POSIX right now') +@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'