Skip to content

Commit 1eb3ade

Browse files
authored
GH-107956: install build-details.json (PEP 739) (#130069)
1 parent 140e69c commit 1eb3ade

File tree

5 files changed

+356
-30
lines changed

5 files changed

+356
-30
lines changed

Lib/sysconfig/__init__.py

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -666,34 +666,34 @@ def get_platform():
666666

667667
# Set for cross builds explicitly
668668
if "_PYTHON_HOST_PLATFORM" in os.environ:
669-
return os.environ["_PYTHON_HOST_PLATFORM"]
670-
671-
# Try to distinguish various flavours of Unix
672-
osname, host, release, version, machine = os.uname()
673-
674-
# Convert the OS name to lowercase, remove '/' characters, and translate
675-
# spaces (for "Power Macintosh")
676-
osname = osname.lower().replace('/', '')
677-
machine = machine.replace(' ', '_')
678-
machine = machine.replace('/', '-')
679-
680-
if osname[:5] == "linux":
681-
if sys.platform == "android":
682-
osname = "android"
683-
release = get_config_var("ANDROID_API_LEVEL")
684-
685-
# Wheel tags use the ABI names from Android's own tools.
686-
machine = {
687-
"x86_64": "x86_64",
688-
"i686": "x86",
689-
"aarch64": "arm64_v8a",
690-
"armv7l": "armeabi_v7a",
691-
}[machine]
692-
else:
693-
# At least on Linux/Intel, 'machine' is the processor --
694-
# i386, etc.
695-
# XXX what about Alpha, SPARC, etc?
696-
return f"{osname}-{machine}"
669+
osname, _, machine = os.environ["_PYTHON_HOST_PLATFORM"].partition('-')
670+
release = None
671+
else:
672+
# Try to distinguish various flavours of Unix
673+
osname, host, release, version, machine = os.uname()
674+
675+
# Convert the OS name to lowercase, remove '/' characters, and translate
676+
# spaces (for "Power Macintosh")
677+
osname = osname.lower().replace('/', '')
678+
machine = machine.replace(' ', '_')
679+
machine = machine.replace('/', '-')
680+
681+
if osname == "android" or sys.platform == "android":
682+
osname = "android"
683+
release = get_config_var("ANDROID_API_LEVEL")
684+
685+
# Wheel tags use the ABI names from Android's own tools.
686+
machine = {
687+
"x86_64": "x86_64",
688+
"i686": "x86",
689+
"aarch64": "arm64_v8a",
690+
"armv7l": "armeabi_v7a",
691+
}[machine]
692+
elif osname == "linux":
693+
# At least on Linux/Intel, 'machine' is the processor --
694+
# i386, etc.
695+
# XXX what about Alpha, SPARC, etc?
696+
return f"{osname}-{machine}"
697697
elif osname[:5] == "sunos":
698698
if release[0] >= "5": # SunOS 5 == Solaris 2
699699
osname = "solaris"
@@ -725,7 +725,7 @@ def get_platform():
725725
get_config_vars(),
726726
osname, release, machine)
727727

728-
return f"{osname}-{release}-{machine}"
728+
return '-'.join(map(str, filter(None, (osname, release, machine))))
729729

730730

731731
def get_python_version():

Lib/test/test_build_details.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import json
2+
import os
3+
import sys
4+
import sysconfig
5+
import string
6+
import unittest
7+
8+
from test.support import is_android, is_apple_mobile, is_emscripten, is_wasi
9+
10+
11+
class FormatTestsBase:
12+
@property
13+
def contents(self):
14+
"""Install details file contents. Should be overriden by subclasses."""
15+
raise NotImplementedError
16+
17+
@property
18+
def data(self):
19+
"""Parsed install details file data, as a Python object."""
20+
return json.loads(self.contents)
21+
22+
def key(self, name):
23+
"""Helper to fetch subsection entries.
24+
25+
It takes the entry name, allowing the usage of a dot to separate the
26+
different subsection names (eg. specifying 'a.b.c' as the key will
27+
return the value of self.data['a']['b']['c']).
28+
"""
29+
value = self.data
30+
for part in name.split('.'):
31+
value = value[part]
32+
return value
33+
34+
def test_parse(self):
35+
self.data
36+
37+
def test_top_level_container(self):
38+
self.assertIsInstance(self.data, dict)
39+
for key, value in self.data.items():
40+
with self.subTest(key=key):
41+
if key in ('schema_version', 'base_prefix', 'base_interpreter', 'platform'):
42+
self.assertIsInstance(value, str)
43+
elif key in ('language', 'implementation', 'abi', 'suffixes', 'libpython', 'c_api', 'arbitrary_data'):
44+
self.assertIsInstance(value, dict)
45+
46+
def test_base_prefix(self):
47+
self.assertIsInstance(self.key('base_prefix'), str)
48+
49+
def test_base_interpreter(self):
50+
"""Test the base_interpreter entry.
51+
52+
The generic test wants the key to be missing. If your implementation
53+
provides a value for it, you should override this test.
54+
"""
55+
with self.assertRaises(KeyError):
56+
self.key('base_interpreter')
57+
58+
def test_platform(self):
59+
self.assertEqual(self.key('platform'), sysconfig.get_platform())
60+
61+
def test_language_version(self):
62+
allowed_characters = string.digits + string.ascii_letters + '.'
63+
value = self.key('language.version')
64+
65+
self.assertLessEqual(set(value), set(allowed_characters))
66+
self.assertTrue(sys.version.startswith(value))
67+
68+
def test_language_version_info(self):
69+
value = self.key('language.version_info')
70+
71+
self.assertEqual(len(value), sys.version_info.n_fields)
72+
for part_name, part_value in value.items():
73+
with self.subTest(part=part_name):
74+
self.assertEqual(part_value, getattr(sys.version_info, part_name))
75+
76+
def test_implementation(self):
77+
for key, value in self.key('implementation').items():
78+
with self.subTest(part=key):
79+
if key == 'version':
80+
self.assertEqual(len(value), len(sys.implementation.version))
81+
for part_name, part_value in value.items():
82+
self.assertEqual(getattr(sys.implementation.version, part_name), part_value)
83+
else:
84+
self.assertEqual(getattr(sys.implementation, key), value)
85+
86+
87+
needs_installed_python = unittest.skipIf(
88+
sysconfig.is_python_build(),
89+
'This test can only run in an installed Python',
90+
)
91+
92+
93+
@unittest.skipIf(os.name != 'posix', 'Feature only implemented on POSIX right now')
94+
@unittest.skipIf(is_wasi or is_emscripten, 'Feature not available on WebAssembly builds')
95+
class CPythonBuildDetailsTests(unittest.TestCase, FormatTestsBase):
96+
"""Test CPython's install details file implementation."""
97+
98+
@property
99+
def location(self):
100+
if sysconfig.is_python_build():
101+
projectdir = sysconfig.get_config_var('projectbase')
102+
with open(os.path.join(projectdir, 'pybuilddir.txt')) as f:
103+
dirname = os.path.join(projectdir, f.read())
104+
else:
105+
dirname = sysconfig.get_path('stdlib')
106+
return os.path.join(dirname, 'build-details.json')
107+
108+
@property
109+
def contents(self):
110+
with open(self.location, 'r') as f:
111+
return f.read()
112+
113+
@needs_installed_python
114+
def test_location(self):
115+
self.assertTrue(os.path.isfile(self.location))
116+
117+
# Override generic format tests with tests for our specific implemenation.
118+
119+
@needs_installed_python
120+
@unittest.skipIf(is_android or is_apple_mobile, 'Android and iOS run tests via a custom testbed method that changes sys.executable')
121+
def test_base_interpreter(self):
122+
value = self.key('base_interpreter')
123+
124+
self.assertEqual(os.path.realpath(value), os.path.realpath(sys.executable))
125+
126+
127+
if __name__ == '__main__':
128+
unittest.main()

Makefile.pre.in

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,7 @@ list-targets:
728728

729729
.PHONY: build_all
730730
build_all: check-clean-src check-app-store-compliance $(BUILDPYTHON) platform sharedmods \
731-
gdbhooks Programs/_testembed scripts checksharedmods rundsymutil
731+
gdbhooks Programs/_testembed scripts checksharedmods rundsymutil build-details.json
732732

733733
.PHONY: build_wasm
734734
build_wasm: check-clean-src $(BUILDPYTHON) platform sharedmods \
@@ -934,6 +934,9 @@ pybuilddir.txt: $(PYTHON_FOR_BUILD_DEPS)
934934
exit 1 ; \
935935
fi
936936

937+
build-details.json: pybuilddir.txt
938+
$(RUNSHARED) $(PYTHON_FOR_BUILD) $(srcdir)/Tools/build/generate-build-details.py `cat pybuilddir.txt`/build-details.json
939+
937940
# Build static library
938941
$(LIBRARY): $(LIBRARY_OBJS)
939942
-rm -f $@
@@ -2644,6 +2647,7 @@ libinstall: all $(srcdir)/Modules/xxmodule.c
26442647
done
26452648
$(INSTALL_DATA) `cat pybuilddir.txt`/_sysconfigdata_$(ABIFLAGS)_$(MACHDEP)_$(MULTIARCH).py $(DESTDIR)$(LIBDEST); \
26462649
$(INSTALL_DATA) `cat pybuilddir.txt`/_sysconfig_vars_$(ABIFLAGS)_$(MACHDEP)_$(MULTIARCH).json $(DESTDIR)$(LIBDEST); \
2650+
$(INSTALL_DATA) `cat pybuilddir.txt`/build-details.json $(DESTDIR)$(LIBDEST); \
26472651
$(INSTALL_DATA) $(srcdir)/LICENSE $(DESTDIR)$(LIBDEST)/LICENSE.txt
26482652
@ # If app store compliance has been configured, apply the patch to the
26492653
@ # installed library code. The patch has been previously validated against
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
A ``build-details.json`` file is now install in the platform-independent
2+
standard library directory (:pep:`739` implementation).

0 commit comments

Comments
 (0)