Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion Lib/test/test_build_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
import sys
import sysconfig
import string
import textwrap
import unittest
from pathlib import Path
from unittest import mock

from test.support import is_android, is_apple_mobile, is_wasm32
from test.support import is_android, is_apple_mobile, is_wasm32, os_helper

BASE_PATH = Path(
__file__, # Lib/test/test_build_details.py
Expand Down Expand Up @@ -289,6 +290,44 @@ def check_relative_paths(self, data_abs_path, data_rel_path):
self.assertEqual(data_abs_path[part][key], abs_rel_path)


@unittest.skipIf(
generate_build_details is None,
"Failed to import generate-build-details"
)
class BuildDetailsVersionTests(unittest.TestCase):
def test_native(self):
version_info = generate_build_details.get_version_info()
self.assertEqual(version_info,
generate_build_details.version_info_to_dict(
sys.version_info))
self.assertEqual(generate_build_details.hexversion(version_info),
sys.hexversion)

def test_patchlevel(self):
patchlevel_h = os.path.join(os_helper.TESTFN, 'patchlevel.h')
self.addCleanup(os_helper.rmtree, os_helper.TESTFN)
os.mkdir(os_helper.TESTFN)
with open(patchlevel_h, 'w', encoding='utf-8') as f:
f.write(textwrap.dedent('''\
#define PY_RELEASE_LEVEL_GAMMA 0xC /* For release candidates */
#define PY_MAJOR_VERSION 3
#define PY_MINOR_VERSION 99
#define PY_MICRO_VERSION 7
#define PY_RELEASE_LEVEL PY_RELEASE_LEVEL_GAMMA
#define PY_RELEASE_SERIAL 2
#define PY_VERSION "3.99.7rc2"
'''))
with mock.patch.object(generate_build_details, 'PATCHLEVEL_H',
patchlevel_h):
version_info = generate_build_details.get_version_info()
self.assertEqual(version_info, {
'major': 3, 'minor': 99, 'micro': 7,
'releaselevel': 'candidate', 'serial': 2,
})
self.assertEqual(generate_build_details.hexversion(version_info),
0x036307c2)


@unittest.skipIf(
generate_build_details is None,
"Failed to import generate-build-details"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
``build-details.json`` now takes ``language.version_info``,
``implementation.version`` and ``implementation.hexversion`` from
:file:`Include/patchlevel.h` instead of the interpreter running the generator,
which can be a different patch release under :option:`--with-build-python`.
48 changes: 46 additions & 2 deletions Tools/build/generate-build-details.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import importlib.machinery
import json
import os
import re
import sys
import sysconfig

Expand All @@ -18,11 +19,52 @@
from typing import Any


PATCHLEVEL_H = os.path.join(
os.path.dirname(__file__), '..', '..', 'Include', 'patchlevel.h',
)
RELEASE_LEVELS = {0xA: 'alpha', 0xB: 'beta', 0xC: 'candidate', 0xF: 'final'}


def version_info_to_dict(obj: sys._version_info) -> dict[str, Any]:
field_names = ('major', 'minor', 'micro', 'releaselevel', 'serial')
return {field: getattr(obj, field) for field in field_names}


def get_version_info() -> dict[str, Any]:
"""Return the target's version as a version_info dict.

sys.version_info describes the running interpreter, which under
--with-build-python can differ from the target in micro version and
release level. Read Include/patchlevel.h instead when it is available.
"""
try:
with open(PATCHLEVEL_H, encoding='utf-8') as f:
patchlevel = f.read()
except FileNotFoundError:
return version_info_to_dict(sys.version_info)
defines = dict(re.findall(r'^#define\s+(PY_\w+)\s+(\S+)', patchlevel, re.M))
# PY_RELEASE_LEVEL is defined as another macro, e.g. PY_RELEASE_LEVEL_ALPHA.
level = int(defines[defines['PY_RELEASE_LEVEL']], 16)
return {
'major': int(defines['PY_MAJOR_VERSION']),
'minor': int(defines['PY_MINOR_VERSION']),
'micro': int(defines['PY_MICRO_VERSION']),
'releaselevel': RELEASE_LEVELS[level],
'serial': int(defines['PY_RELEASE_SERIAL']),
}


def hexversion(version_info: dict[str, Any]) -> int:
level = {name: value for value, name in RELEASE_LEVELS.items()}
return int(
version_info['major'] << 24
| version_info['minor'] << 16
| version_info['micro'] << 8
| level[version_info['releaselevel']] << 4
| version_info['serial']
)


def get_dict_key(container: dict[str, Any], key: str) -> dict[str, Any]:
for part in key.split('.'):
container = container[part]
Expand Down Expand Up @@ -54,11 +96,13 @@ def generate_data(schema_version: str) -> collections.defaultdict[str, Any]:
)
data['platform'] = sysconfig.get_platform()

version_info = get_version_info()
data['language']['version'] = sysconfig.get_python_version()
data['language']['version_info'] = version_info_to_dict(sys.version_info)
data['language']['version_info'] = version_info

data['implementation'] = vars(sys.implementation).copy()
data['implementation']['version'] = version_info_to_dict(sys.implementation.version)
data['implementation']['version'] = version_info
data['implementation']['hexversion'] = hexversion(version_info)
# Fix cross-compilation
if '_multiarch' in data['implementation']:
data['implementation']['_multiarch'] = sysconfig.get_config_var('MULTIARCH')
Expand Down
Loading