Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[3.13] Backport PyManager support to PC/layout script #135096

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 3, 2025
Merged
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
2 changes: 1 addition & 1 deletion PC/layout/__main__.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
29 changes: 28 additions & 1 deletion PC/layout/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
__version__ = "3.8"

import argparse
import json
import os
import shutil
import sys
Expand All @@ -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*")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"):
Expand Down
31 changes: 31 additions & 0 deletions PC/layout/support/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
}


Expand Down Expand Up @@ -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",
],
},
}


Expand Down
256 changes: 256 additions & 0 deletions PC/layout/support/pymanager.py
Original file line number Diff line number Diff line change
@@ -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
Loading