Skip to content

gh-132930: Implement PEP 773 #132931

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 15 commits into from
Apr 28, 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
1,890 changes: 1,191 additions & 699 deletions Doc/using/windows.rst

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Marks the installer for Windows as deprecated and updates documentation to
cover the new Python install manager.
17 changes: 16 additions & 1 deletion PC/launcher2.c
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,7 @@ checkShebang(SearchInfo *search)
debug(L"# Failed to open %s for shebang parsing (0x%08X)\n",
scriptFile, GetLastError());
free(scriptFile);
return 0;
return RC_NO_SCRIPT;
}

DWORD bytesRead = 0;
Expand Down Expand Up @@ -2665,6 +2665,21 @@ performSearch(SearchInfo *search, EnvironmentInfo **envs)
case RC_NO_SHEBANG:
case RC_RECURSIVE_SHEBANG:
break;
case RC_NO_SCRIPT:
if (!_comparePath(search->scriptFile, search->scriptFileLength, L"install", -1) ||
!_comparePath(search->scriptFile, search->scriptFileLength, L"uninstall", -1) ||
!_comparePath(search->scriptFile, search->scriptFileLength, L"list", -1) ||
!_comparePath(search->scriptFile, search->scriptFileLength, L"help", -1)) {
fprintf(
stderr,
"WARNING: The '%.*ls' command is unavailable because this is the legacy py.exe command.\n"
"If you have already installed the Python install manager, open Installed Apps and "
"remove 'Python Launcher' to enable the new py.exe command.\n",
search->scriptFileLength,
search->scriptFile
);
}
break;
default:
return exitCode;
}
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
249 changes: 249 additions & 0 deletions PC/layout/support/pymanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
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
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("freethreaded")
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

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.
if ALIAS_PREFIX:
for prefix, base in [
(ALIAS_PREFIX, {"target": TARGET}),
(f"{ALIAS_PREFIX}w", {"target": TARGETW, "windowed": 1}),
]:
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"},
])

STD_PEP514.append({
"kind": "pep514",
"Key": rf"{COMPANY}\{ID_TAG}",
"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:
STD_PEP514[0]["InstallPath"]["WindowedExecutablePath"] = f"%PREFIX%{TARGETW}"

if ns.include_idle:
STD_START[0]["Items"].append({
"Name": f"IDLE {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 {VER_DOT}{DISPLAY_SUFFIX}",
"Target": f"%PREFIX%{TARGET}",
"Arguments": "-m pydoc -b",
"Icon": r"%PREFIX%Lib\idlelib\Icons\idle.ico",
"IconIndex": 0,
})

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}",
})
Comment on lines +213 to +221
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had thought we removed CHM ages ago?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script can be used to repackage releases a long way back, so it still has the support in there. Even though it lives in the repo alongside a particular version, it is pretty independent of the actual version it's laying out.


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
Loading