https://github.com/python/cpython/commit/0430135aac62aaf87dc9d8281d240a8b577a129b
commit: 0430135aac62aaf87dc9d8281d240a8b577a129b
branch: 3.13
author: Miss Islington (bot) <31488909+miss-isling...@users.noreply.github.com>
committer: zooba <steve.do...@microsoft.com>
date: 2025-06-13T15:28:02Z
summary:

gh-135455: Fix version and architecture detection in PC/layout script. 
(GH-135461)

(cherry picked from commit afc5ab6cce9d7095b99c1410a6762bc4a96504dd)

Co-authored-by: Steve Dower <steve.do...@python.org>

files:
A PC/layout/support/arch.py
M PC/layout/main.py
M PC/layout/support/constants.py

diff --git a/PC/layout/main.py b/PC/layout/main.py
index 7324a135133b66..8543e7c56e1c41 100644
--- a/PC/layout/main.py
+++ b/PC/layout/main.py
@@ -247,9 +247,15 @@ def in_build(f, dest="", new_name=None, no_lib=False):
             if ns.include_freethreaded:
                 yield from in_build("venvlaunchert.exe", 
"Lib/venv/scripts/nt/")
                 yield from in_build("venvwlaunchert.exe", 
"Lib/venv/scripts/nt/")
-            else:
+            elif (VER_MAJOR, VER_MINOR) > (3, 12):
                 yield from in_build("venvlauncher.exe", "Lib/venv/scripts/nt/")
                 yield from in_build("venvwlauncher.exe", 
"Lib/venv/scripts/nt/")
+            else:
+                # Older versions of venv expected the scripts to be named 
'python'
+                # and they were renamed at this stage. We need to replicate 
that
+                # when packaging older versions.
+                yield from in_build("venvlauncher.exe", 
"Lib/venv/scripts/nt/", "python")
+                yield from in_build("venvwlauncher.exe", 
"Lib/venv/scripts/nt/", "pythonw")
 
     if ns.include_tools:
 
@@ -652,15 +658,6 @@ def main():
         ns.doc_build = (Path.cwd() / ns.doc_build).resolve()
     if ns.include_cat and not ns.include_cat.is_absolute():
         ns.include_cat = (Path.cwd() / ns.include_cat).resolve()
-    if not ns.arch:
-        # TODO: Calculate arch from files in ns.build instead
-        if sys.winver.endswith("-arm64"):
-            ns.arch = "arm64"
-        elif sys.winver.endswith("-32"):
-            ns.arch = "win32"
-        else:
-            ns.arch = "amd64"
-
     if ns.zip and not ns.zip.is_absolute():
         ns.zip = (Path.cwd() / ns.zip).resolve()
     if ns.catalog and not ns.catalog.is_absolute():
@@ -668,6 +665,17 @@ def main():
 
     configure_logger(ns)
 
+    if not ns.arch:
+        from .support.arch import calculate_from_build_dir
+        ns.arch = calculate_from_build_dir(ns.build)
+
+    expect = f"{VER_MAJOR}.{VER_MINOR}.{VER_MICRO}{VER_SUFFIX}"
+    actual = check_patchlevel_version(ns.source)
+    if actual and actual != expect:
+        log_error(f"Inferred version {expect} does not match {actual} from 
patchlevel.h. "
+                   "You should set %PYTHONINCLUDE% or %PYTHON_HEXVERSION% 
before launching.")
+        return 5
+
     log_info(
         """OPTIONS
 Source: {ns.source}
diff --git a/PC/layout/support/arch.py b/PC/layout/support/arch.py
new file mode 100644
index 00000000000000..daf4efbc7ab674
--- /dev/null
+++ b/PC/layout/support/arch.py
@@ -0,0 +1,34 @@
+from struct import unpack
+from .constants import *
+from .logging import *
+
+def calculate_from_build_dir(root):
+    candidates = [
+        root / PYTHON_DLL_NAME,
+        root / FREETHREADED_PYTHON_DLL_NAME,
+        *root.glob("*.dll"),
+        *root.glob("*.pyd"),
+        # Check EXE last because it's easier to have cross-platform EXE
+        *root.glob("*.exe"),
+    ]
+
+    ARCHS = {
+        b"PE\0\0\x4c\x01": "win32",
+        b"PE\0\0\x64\x86": "amd64",
+        b"PE\0\0\x64\xAA": "arm64"
+    }
+
+    first_exc = None
+    for pe in candidates:
+        try:
+            # Read the PE header to grab the machine type
+            with open(pe, "rb") as f:
+                f.seek(0x3C)
+                offset = int.from_bytes(f.read(4), "little")
+                f.seek(offset)
+                arch = ARCHS[f.read(6)]
+        except (FileNotFoundError, PermissionError, LookupError) as ex:
+            log_debug("Failed to open {}: {}", pe, ex)
+            continue
+        log_info("Inferred architecture {} from {}", arch, pe)
+        return arch
diff --git a/PC/layout/support/constants.py b/PC/layout/support/constants.py
index ae22aa16ebfa5d..6b8c915e519743 100644
--- a/PC/layout/support/constants.py
+++ b/PC/layout/support/constants.py
@@ -6,6 +6,8 @@
 __version__ = "3.8"
 
 import os
+import pathlib
+import re
 import struct
 import sys
 
@@ -13,9 +15,15 @@
 def _unpack_hexversion():
     try:
         hexversion = int(os.getenv("PYTHON_HEXVERSION"), 16)
+        return struct.pack(">i", hexversion)
     except (TypeError, ValueError):
-        hexversion = sys.hexversion
-    return struct.pack(">i", hexversion)
+        pass
+    if os.getenv("PYTHONINCLUDE"):
+        try:
+            return 
_read_patchlevel_version(pathlib.Path(os.getenv("PYTHONINCLUDE")))
+        except OSError:
+            pass
+    return struct.pack(">i", sys.hexversion)
 
 
 def _get_suffix(field4):
@@ -26,6 +34,39 @@ def _get_suffix(field4):
     return ""
 
 
+def _read_patchlevel_version(sources):
+    if not sources.match("Include"):
+        sources /= "Include"
+    values = {}
+    with open(sources / "patchlevel.h", "r", encoding="utf-8") as f:
+        for line in f:
+            m = re.match(r'#\s*define\s+(PY_\S+?)\s+(\S+)', line.strip(), re.I)
+            if m and m.group(2):
+                v = m.group(2)
+                if v.startswith('"'):
+                    v = v[1:-1]
+                else:
+                    v = values.get(v, v)
+                    if isinstance(v, str):
+                        try:
+                            v = int(v, 16 if v.startswith("0x") else 10)
+                        except ValueError:
+                            pass
+                values[m.group(1)] = v
+    return (
+        values["PY_MAJOR_VERSION"],
+        values["PY_MINOR_VERSION"],
+        values["PY_MICRO_VERSION"],
+        values["PY_RELEASE_LEVEL"] << 4 | values["PY_RELEASE_SERIAL"],
+    )
+
+
+def check_patchlevel_version(sources):
+    got = _read_patchlevel_version(sources)
+    if got != (VER_MAJOR, VER_MINOR, VER_MICRO, VER_FIELD4):
+        return f"{got[0]}.{got[1]}.{got[2]}{_get_suffix(got[3])}"
+
+
 VER_MAJOR, VER_MINOR, VER_MICRO, VER_FIELD4 = _unpack_hexversion()
 VER_SUFFIX = _get_suffix(VER_FIELD4)
 VER_FIELD3 = VER_MICRO << 8 | VER_FIELD4

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: arch...@mail-archive.com

Reply via email to