https://github.com/python/cpython/commit/b679b74132bf37312c158b4c5cf07f76024a23f1
commit: b679b74132bf37312c158b4c5cf07f76024a23f1
branch: 3.13
author: Steve Dower <steve.do...@python.org>
committer: zooba <steve.do...@microsoft.com>
date: 2025-06-03T21:21:11+01:00
summary:

[3.13] Backport PyManager support to PC/layout script (GH-135096)

files:
A PC/layout/support/pymanager.py
M PC/layout/__main__.py
M PC/layout/main.py
M PC/layout/support/options.py

diff --git a/PC/layout/__main__.py b/PC/layout/__main__.py
index f7aa1e6d261f4a..05a059eee7c1d7 100644
--- a/PC/layout/__main__.py
+++ b/PC/layout/__main__.py
@@ -1,7 +1,7 @@
 import sys
 
 try:
-    import layout
+    import layout  # noqa: F401
 except ImportError:
     # Failed to import our package, which likely means we were started directly
     # Add the additional search path needed to locate our module.
diff --git a/PC/layout/main.py b/PC/layout/main.py
index 6321c33b3f780a..7324a135133b66 100644
--- a/PC/layout/main.py
+++ b/PC/layout/main.py
@@ -8,6 +8,7 @@
 __version__ = "3.8"
 
 import argparse
+import json
 import os
 import shutil
 import sys
@@ -28,6 +29,7 @@
 from .support.options import *
 from .support.pip import *
 from .support.props import *
+from .support.pymanager import *
 from .support.nuspec import *
 
 TEST_PYDS_ONLY = FileStemSet("xxlimited", "xxlimited_35", "_ctypes_test", 
"_test*")
@@ -265,7 +267,12 @@ def _c(d):
     if ns.include_dev:
         for dest, src in rglob(ns.source / "Include", "**/*.h"):
             yield "include/{}".format(dest), src
-        yield "include/pyconfig.h", ns.build / "pyconfig.h"
+        # Support for layout of new and old releases.
+        pc = ns.source / "PC"
+        if (pc / "pyconfig.h.in").is_file():
+            yield "include/pyconfig.h", ns.build / "pyconfig.h"
+        else:
+            yield "include/pyconfig.h", pc / "pyconfig.h"
 
     for dest, src in get_tcltk_lib(ns):
         yield dest, src
@@ -303,6 +310,9 @@ def _c(d):
         else:
             yield "DLLs/{}".format(ns.include_cat.name), ns.include_cat
 
+    if ns.include_install_json or ns.include_install_embed_json or 
ns.include_install_test_json:
+        yield "__install__.json", ns.temp / "__install__.json"
+
 
 def _compile_one_py(src, dest, name, optimize, checked=True):
     import py_compile
@@ -394,6 +404,22 @@ def generate_source_files(ns):
         log_info("Extracting pip")
         extract_pip_files(ns)
 
+    if ns.include_install_json:
+        log_info("Generating __install__.json in {}", ns.temp)
+        ns.temp.mkdir(parents=True, exist_ok=True)
+        with open(ns.temp / "__install__.json", "w", encoding="utf-8") as f:
+            json.dump(calculate_install_json(ns), f, indent=2)
+    elif ns.include_install_embed_json:
+        log_info("Generating embeddable __install__.json in {}", ns.temp)
+        ns.temp.mkdir(parents=True, exist_ok=True)
+        with open(ns.temp / "__install__.json", "w", encoding="utf-8") as f:
+            json.dump(calculate_install_json(ns, for_embed=True), f, indent=2)
+    elif ns.include_install_test_json:
+        log_info("Generating test __install__.json in {}", ns.temp)
+        ns.temp.mkdir(parents=True, exist_ok=True)
+        with open(ns.temp / "__install__.json", "w", encoding="utf-8") as f:
+            json.dump(calculate_install_json(ns, for_test=True), f, indent=2)
+
 
 def _create_zip_file(ns):
     if not ns.zip:
@@ -627,6 +653,7 @@ def main():
     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"):
diff --git a/PC/layout/support/options.py b/PC/layout/support/options.py
index f1a8eb0b317744..e8c393385425e7 100644
--- a/PC/layout/support/options.py
+++ b/PC/layout/support/options.py
@@ -36,6 +36,9 @@ def public(f):
     "alias": {"help": "aliased python.exe entry-point binaries"},
     "alias3": {"help": "aliased python3.exe entry-point binaries"},
     "alias3x": {"help": "aliased python3.x.exe entry-point binaries"},
+    "install-json": {"help": "a PyManager __install__.json file"},
+    "install-embed-json": {"help": "a PyManager __install__.json file for 
embeddable distro"},
+    "install-test-json": {"help": "a PyManager __install__.json for the test 
distro"},
 }
 
 
@@ -95,6 +98,34 @@ def public(f):
             "precompile",
         ],
     },
+    "pymanager": {
+        "help": "PyManager package",
+        "options": [
+            "stable",
+            "pip",
+            "tcltk",
+            "idle",
+            "venv",
+            "dev",
+            "html-doc",
+            "install-json",
+        ],
+    },
+    "pymanager-test": {
+        "help": "PyManager test package",
+        "options": [
+            "stable",
+            "pip",
+            "tcltk",
+            "idle",
+            "venv",
+            "dev",
+            "html-doc",
+            "symbols",
+            "tests",
+            "install-test-json",
+        ],
+    },
 }
 
 
diff --git a/PC/layout/support/pymanager.py b/PC/layout/support/pymanager.py
new file mode 100644
index 00000000000000..667c89cdd2cc7a
--- /dev/null
+++ b/PC/layout/support/pymanager.py
@@ -0,0 +1,256 @@
+from .constants import *
+
+URL_BASE = "https://www.python.org/ftp/python/";
+
+XYZ_VERSION = f"{VER_MAJOR}.{VER_MINOR}.{VER_MICRO}"
+WIN32_VERSION = f"{VER_MAJOR}.{VER_MINOR}.{VER_MICRO}.{VER_FIELD4}"
+FULL_VERSION = f"{VER_MAJOR}.{VER_MINOR}.{VER_MICRO}{VER_SUFFIX}"
+
+
+def _not_empty(n, key=None):
+    result = []
+    for i in n:
+        if key:
+            i_l = i[key]
+        else:
+            i_l = i
+        if not i_l:
+            continue
+        result.append(i)
+    return result
+
+
+def calculate_install_json(ns, *, for_embed=False, for_test=False):
+    TARGET = "python.exe"
+    TARGETW = "pythonw.exe"
+
+    SYS_ARCH = {
+        "win32": "32bit",
+        "amd64": "64bit",
+        "arm64": "64bit", # Unfortunate, but this is how it's spec'd
+    }[ns.arch]
+    TAG_ARCH = {
+        "win32": "-32",
+        "amd64": "-64",
+        "arm64": "-arm64",
+    }[ns.arch]
+
+    COMPANY = "PythonCore"
+    DISPLAY_NAME = "Python"
+    TAG_SUFFIX = ""
+    ALIAS_PREFIX = "python"
+    ALIAS_WPREFIX = "pythonw"
+    FILE_PREFIX = "python-"
+    FILE_SUFFIX = f"-{ns.arch}"
+    DISPLAY_TAGS = [{
+        "win32": "32-bit",
+        "amd64": "",
+        "arm64": "ARM64",
+    }[ns.arch]]
+
+    if for_test:
+        # Packages with the test suite come under a different Company
+        COMPANY = "PythonTest"
+        DISPLAY_TAGS.append("with tests")
+        FILE_SUFFIX = f"-test-{ns.arch}"
+    if for_embed:
+        # Embeddable distro comes under a different Company
+        COMPANY = "PythonEmbed"
+        TARGETW = None
+        ALIAS_PREFIX = None
+        ALIAS_WPREFIX = None
+        DISPLAY_TAGS.append("embeddable")
+        # Deliberately name the file differently from the existing distro
+        # so we can republish old versions without replacing files.
+        FILE_SUFFIX = f"-embeddable-{ns.arch}"
+    if ns.include_freethreaded:
+        # Free-threaded distro comes with a tag suffix
+        TAG_SUFFIX = "t"
+        TARGET = f"python{VER_MAJOR}.{VER_MINOR}t.exe"
+        TARGETW = f"pythonw{VER_MAJOR}.{VER_MINOR}t.exe"
+        DISPLAY_TAGS.append("free-threaded")
+        FILE_SUFFIX = f"t-{ns.arch}"
+
+    FULL_TAG = f"{VER_MAJOR}.{VER_MINOR}.{VER_MICRO}{VER_SUFFIX}{TAG_SUFFIX}"
+    FULL_ARCH_TAG = f"{FULL_TAG}{TAG_ARCH}"
+    XY_TAG = f"{VER_MAJOR}.{VER_MINOR}{TAG_SUFFIX}"
+    XY_ARCH_TAG = f"{XY_TAG}{TAG_ARCH}"
+    X_TAG = f"{VER_MAJOR}{TAG_SUFFIX}"
+    X_ARCH_TAG = f"{X_TAG}{TAG_ARCH}"
+
+    # Tag used in runtime ID (for side-by-side install/updates)
+    ID_TAG = XY_ARCH_TAG
+    # Tag shown in 'py list' output
+    DISPLAY_TAG = f"{XY_TAG}-dev{TAG_ARCH}" if VER_SUFFIX else XY_ARCH_TAG
+    # Tag used for PEP 514 registration
+    SYS_WINVER = XY_TAG + (TAG_ARCH if TAG_ARCH != '-64' else '')
+
+    DISPLAY_SUFFIX = ", ".join(i for i in DISPLAY_TAGS if i)
+    if DISPLAY_SUFFIX:
+        DISPLAY_SUFFIX = f" ({DISPLAY_SUFFIX})"
+    DISPLAY_VERSION = f"{XYZ_VERSION}{VER_SUFFIX}{DISPLAY_SUFFIX}"
+
+    STD_RUN_FOR = []
+    STD_ALIAS = []
+    STD_PEP514 = []
+    STD_START = []
+    STD_UNINSTALL = []
+
+    # The list of 'py install <TAG>' tags that will match this runtime.
+    # Architecture should always be included here because PyManager will add 
it.
+    INSTALL_TAGS = [
+        FULL_ARCH_TAG,
+        XY_ARCH_TAG,
+        X_ARCH_TAG,
+        # X_TAG and XY_TAG doesn't include VER_SUFFIX, so create -dev versions
+        f"{XY_TAG}-dev{TAG_ARCH}" if XY_TAG and VER_SUFFIX else "",
+        f"{X_TAG}-dev{TAG_ARCH}" if X_TAG and VER_SUFFIX else "",
+    ]
+
+    # Generate run-for entries for each target.
+    # Again, include architecture because PyManager will add it.
+    for base in [
+        {"target": TARGET},
+        {"target": TARGETW, "windowed": 1},
+    ]:
+        if not base["target"]:
+            continue
+        STD_RUN_FOR.append({**base, "tag": FULL_ARCH_TAG})
+        if XY_TAG:
+            STD_RUN_FOR.append({**base, "tag": XY_ARCH_TAG})
+        if X_TAG:
+            STD_RUN_FOR.append({**base, "tag": X_ARCH_TAG})
+        if VER_SUFFIX:
+            STD_RUN_FOR.extend([
+                {**base, "tag": f"{XY_TAG}-dev{TAG_ARCH}" if XY_TAG else ""},
+                {**base, "tag": f"{X_TAG}-dev{TAG_ARCH}" if X_TAG else ""},
+            ])
+
+    # Generate alias entries for each target. We need both arch and non-arch
+    # versions as well as windowed/non-windowed versions to make sure that all
+    # necessary aliases are created.
+    for prefix, base in (
+        (ALIAS_PREFIX, {"target": TARGET}),
+        (ALIAS_WPREFIX, {"target": TARGETW, "windowed": 1}),
+    ):
+        if not prefix:
+            continue
+        if not base["target"]:
+            continue
+        if XY_TAG:
+            STD_ALIAS.extend([
+                {**base, "name": f"{prefix}{XY_TAG}.exe"},
+                {**base, "name": f"{prefix}{XY_ARCH_TAG}.exe"},
+            ])
+        if X_TAG:
+            STD_ALIAS.extend([
+                {**base, "name": f"{prefix}{X_TAG}.exe"},
+                {**base, "name": f"{prefix}{X_ARCH_TAG}.exe"},
+            ])
+
+    if SYS_WINVER:
+        STD_PEP514.append({
+            "kind": "pep514",
+            "Key": rf"{COMPANY}\{SYS_WINVER}",
+            "DisplayName": f"{DISPLAY_NAME} {DISPLAY_VERSION}",
+            "SupportUrl": "https://www.python.org/";,
+            "SysArchitecture": SYS_ARCH,
+            "SysVersion": VER_DOT,
+            "Version": FULL_VERSION,
+            "InstallPath": {
+                "_": "%PREFIX%",
+                "ExecutablePath": f"%PREFIX%{TARGET}",
+                # WindowedExecutablePath is added below
+            },
+            "Help": {
+                "Online Python Documentation": {
+                    "_": f"https://docs.python.org/{VER_DOT}/";
+                },
+            },
+        })
+
+    STD_START.append({
+        "kind": "start",
+        "Name": f"{DISPLAY_NAME} {VER_DOT}{DISPLAY_SUFFIX}",
+        "Items": [
+            {
+                "Name": f"{DISPLAY_NAME} {VER_DOT}{DISPLAY_SUFFIX}",
+                "Target": f"%PREFIX%{TARGET}",
+                "Icon": f"%PREFIX%{TARGET}",
+            },
+            {
+                "Name": f"{DISPLAY_NAME} {VER_DOT} Online Documentation",
+                "Icon": r"%SystemRoot%\System32\SHELL32.dll",
+                "IconIndex": 13,
+                "Target": f"https://docs.python.org/{VER_DOT}/";,
+            },
+            # IDLE and local documentation items are added below
+        ],
+    })
+
+    if TARGETW and STD_PEP514:
+        STD_PEP514[0]["InstallPath"]["WindowedExecutablePath"] = 
f"%PREFIX%{TARGETW}"
+
+    if ns.include_idle:
+        STD_START[0]["Items"].append({
+            "Name": f"IDLE (Python {VER_DOT}{DISPLAY_SUFFIX})",
+            "Target": f"%PREFIX%{TARGETW or TARGET}",
+            "Arguments": r'"%PREFIX%Lib\idlelib\idle.pyw"',
+            "Icon": r"%PREFIX%Lib\idlelib\Icons\idle.ico",
+            "IconIndex": 0,
+        })
+        STD_START[0]["Items"].append({
+            "Name": f"PyDoc (Python {VER_DOT}{DISPLAY_SUFFIX})",
+            "Target": f"%PREFIX%{TARGET}",
+            "Arguments": "-m pydoc -b",
+            "Icon": r"%PREFIX%Lib\idlelib\Icons\idle.ico",
+            "IconIndex": 0,
+        })
+        if STD_PEP514:
+            STD_PEP514[0]["InstallPath"]["IdlePath"] = 
f"%PREFIX%Lib\\idlelib\\idle.pyw"
+
+    if ns.include_html_doc:
+        STD_PEP514[0]["Help"]["Main Python Documentation"] = {
+            "_": rf"%PREFIX%Doc\html\index.html",
+        }
+        STD_START[0]["Items"].append({
+            "Name": f"{DISPLAY_NAME} {VER_DOT} Manuals{DISPLAY_SUFFIX}",
+            "Target": r"%PREFIX%Doc\html\index.html",
+        })
+    elif ns.include_chm:
+        STD_PEP514[0]["Help"]["Main Python Documentation"] = {
+            "_": rf"%PREFIX%Doc\{PYTHON_CHM_NAME}",
+        }
+        STD_START[0]["Items"].append({
+            "Name": f"{DISPLAY_NAME} {VER_DOT} Manuals{DISPLAY_SUFFIX}",
+            "Target": "%WINDIR%hhc.exe",
+            "Arguments": rf"%PREFIX%Doc\{PYTHON_CHM_NAME}",
+        })
+
+    STD_UNINSTALL.append({
+        "kind": "uninstall",
+        # Other settings will pick up sensible defaults
+        "Publisher": "Python Software Foundation",
+        "HelpLink": f"https://docs.python.org/{VER_DOT}/";,
+    })
+
+    data = {
+        "schema": 1,
+        "id": f"{COMPANY.lower()}-{ID_TAG}",
+        "sort-version": FULL_VERSION,
+        "company": COMPANY,
+        "tag": DISPLAY_TAG,
+        "install-for": _not_empty(INSTALL_TAGS),
+        "run-for": _not_empty(STD_RUN_FOR, "tag"),
+        "alias": _not_empty(STD_ALIAS, "name"),
+        "shortcuts": [
+            *STD_PEP514,
+            *STD_START,
+            *STD_UNINSTALL,
+        ],
+        "display-name": f"{DISPLAY_NAME} {DISPLAY_VERSION}",
+        "executable": rf".\{TARGET}",
+        "url": 
f"{URL_BASE}{XYZ_VERSION}/{FILE_PREFIX}{FULL_VERSION}{FILE_SUFFIX}.zip"
+    }
+
+    return data

_______________________________________________
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