Skip to content

Commit 0430135

Browse files
gh-135455: Fix version and architecture detection in PC/layout script. (GH-135461)
(cherry picked from commit afc5ab6) Co-authored-by: Steve Dower <steve.dower@python.org>
1 parent c260ed1 commit 0430135

File tree

3 files changed

+95
-12
lines changed

3 files changed

+95
-12
lines changed

PC/layout/main.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,15 @@ def in_build(f, dest="", new_name=None, no_lib=False):
247247
if ns.include_freethreaded:
248248
yield from in_build("venvlaunchert.exe", "Lib/venv/scripts/nt/")
249249
yield from in_build("venvwlaunchert.exe", "Lib/venv/scripts/nt/")
250-
else:
250+
elif (VER_MAJOR, VER_MINOR) > (3, 12):
251251
yield from in_build("venvlauncher.exe", "Lib/venv/scripts/nt/")
252252
yield from in_build("venvwlauncher.exe", "Lib/venv/scripts/nt/")
253+
else:
254+
# Older versions of venv expected the scripts to be named 'python'
255+
# and they were renamed at this stage. We need to replicate that
256+
# when packaging older versions.
257+
yield from in_build("venvlauncher.exe", "Lib/venv/scripts/nt/", "python")
258+
yield from in_build("venvwlauncher.exe", "Lib/venv/scripts/nt/", "pythonw")
253259

254260
if ns.include_tools:
255261

@@ -652,22 +658,24 @@ def main():
652658
ns.doc_build = (Path.cwd() / ns.doc_build).resolve()
653659
if ns.include_cat and not ns.include_cat.is_absolute():
654660
ns.include_cat = (Path.cwd() / ns.include_cat).resolve()
655-
if not ns.arch:
656-
# TODO: Calculate arch from files in ns.build instead
657-
if sys.winver.endswith("-arm64"):
658-
ns.arch = "arm64"
659-
elif sys.winver.endswith("-32"):
660-
ns.arch = "win32"
661-
else:
662-
ns.arch = "amd64"
663-
664661
if ns.zip and not ns.zip.is_absolute():
665662
ns.zip = (Path.cwd() / ns.zip).resolve()
666663
if ns.catalog and not ns.catalog.is_absolute():
667664
ns.catalog = (Path.cwd() / ns.catalog).resolve()
668665

669666
configure_logger(ns)
670667

668+
if not ns.arch:
669+
from .support.arch import calculate_from_build_dir
670+
ns.arch = calculate_from_build_dir(ns.build)
671+
672+
expect = f"{VER_MAJOR}.{VER_MINOR}.{VER_MICRO}{VER_SUFFIX}"
673+
actual = check_patchlevel_version(ns.source)
674+
if actual and actual != expect:
675+
log_error(f"Inferred version {expect} does not match {actual} from patchlevel.h. "
676+
"You should set %PYTHONINCLUDE% or %PYTHON_HEXVERSION% before launching.")
677+
return 5
678+
671679
log_info(
672680
"""OPTIONS
673681
Source: {ns.source}

PC/layout/support/arch.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from struct import unpack
2+
from .constants import *
3+
from .logging import *
4+
5+
def calculate_from_build_dir(root):
6+
candidates = [
7+
root / PYTHON_DLL_NAME,
8+
root / FREETHREADED_PYTHON_DLL_NAME,
9+
*root.glob("*.dll"),
10+
*root.glob("*.pyd"),
11+
# Check EXE last because it's easier to have cross-platform EXE
12+
*root.glob("*.exe"),
13+
]
14+
15+
ARCHS = {
16+
b"PE\0\0\x4c\x01": "win32",
17+
b"PE\0\0\x64\x86": "amd64",
18+
b"PE\0\0\x64\xAA": "arm64"
19+
}
20+
21+
first_exc = None
22+
for pe in candidates:
23+
try:
24+
# Read the PE header to grab the machine type
25+
with open(pe, "rb") as f:
26+
f.seek(0x3C)
27+
offset = int.from_bytes(f.read(4), "little")
28+
f.seek(offset)
29+
arch = ARCHS[f.read(6)]
30+
except (FileNotFoundError, PermissionError, LookupError) as ex:
31+
log_debug("Failed to open {}: {}", pe, ex)
32+
continue
33+
log_info("Inferred architecture {} from {}", arch, pe)
34+
return arch

PC/layout/support/constants.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,24 @@
66
__version__ = "3.8"
77

88
import os
9+
import pathlib
10+
import re
911
import struct
1012
import sys
1113

1214

1315
def _unpack_hexversion():
1416
try:
1517
hexversion = int(os.getenv("PYTHON_HEXVERSION"), 16)
18+
return struct.pack(">i", hexversion)
1619
except (TypeError, ValueError):
17-
hexversion = sys.hexversion
18-
return struct.pack(">i", hexversion)
20+
pass
21+
if os.getenv("PYTHONINCLUDE"):
22+
try:
23+
return _read_patchlevel_version(pathlib.Path(os.getenv("PYTHONINCLUDE")))
24+
except OSError:
25+
pass
26+
return struct.pack(">i", sys.hexversion)
1927

2028

2129
def _get_suffix(field4):
@@ -26,6 +34,39 @@ def _get_suffix(field4):
2634
return ""
2735

2836

37+
def _read_patchlevel_version(sources):
38+
if not sources.match("Include"):
39+
sources /= "Include"
40+
values = {}
41+
with open(sources / "patchlevel.h", "r", encoding="utf-8") as f:
42+
for line in f:
43+
m = re.match(r'#\s*define\s+(PY_\S+?)\s+(\S+)', line.strip(), re.I)
44+
if m and m.group(2):
45+
v = m.group(2)
46+
if v.startswith('"'):
47+
v = v[1:-1]
48+
else:
49+
v = values.get(v, v)
50+
if isinstance(v, str):
51+
try:
52+
v = int(v, 16 if v.startswith("0x") else 10)
53+
except ValueError:
54+
pass
55+
values[m.group(1)] = v
56+
return (
57+
values["PY_MAJOR_VERSION"],
58+
values["PY_MINOR_VERSION"],
59+
values["PY_MICRO_VERSION"],
60+
values["PY_RELEASE_LEVEL"] << 4 | values["PY_RELEASE_SERIAL"],
61+
)
62+
63+
64+
def check_patchlevel_version(sources):
65+
got = _read_patchlevel_version(sources)
66+
if got != (VER_MAJOR, VER_MINOR, VER_MICRO, VER_FIELD4):
67+
return f"{got[0]}.{got[1]}.{got[2]}{_get_suffix(got[3])}"
68+
69+
2970
VER_MAJOR, VER_MINOR, VER_MICRO, VER_FIELD4 = _unpack_hexversion()
3071
VER_SUFFIX = _get_suffix(VER_FIELD4)
3172
VER_FIELD3 = VER_MICRO << 8 | VER_FIELD4

0 commit comments

Comments
 (0)