Skip to content

Commit e20ca6d

Browse files
authored
gh-132930: Implement PEP 773 (GH-132931)
This change to the core CPython repo: * Adds PyManager support to PC/layout * Adds a warning message to the legacy py.exe if subcommands are invoked * Add deprecation message to traditional installer * Updates using/windows docs
1 parent 11f457c commit e20ca6d

File tree

8 files changed

+1525
-704
lines changed

8 files changed

+1525
-704
lines changed

Doc/using/windows.rst

Lines changed: 1191 additions & 699 deletions
Large diffs are not rendered by default.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Marks the installer for Windows as deprecated and updates documentation to
2+
cover the new Python install manager.

PC/launcher2.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1058,7 +1058,7 @@ checkShebang(SearchInfo *search)
10581058
debug(L"# Failed to open %s for shebang parsing (0x%08X)\n",
10591059
scriptFile, GetLastError());
10601060
free(scriptFile);
1061-
return 0;
1061+
return RC_NO_SCRIPT;
10621062
}
10631063

10641064
DWORD bytesRead = 0;
@@ -2665,6 +2665,21 @@ performSearch(SearchInfo *search, EnvironmentInfo **envs)
26652665
case RC_NO_SHEBANG:
26662666
case RC_RECURSIVE_SHEBANG:
26672667
break;
2668+
case RC_NO_SCRIPT:
2669+
if (!_comparePath(search->scriptFile, search->scriptFileLength, L"install", -1) ||
2670+
!_comparePath(search->scriptFile, search->scriptFileLength, L"uninstall", -1) ||
2671+
!_comparePath(search->scriptFile, search->scriptFileLength, L"list", -1) ||
2672+
!_comparePath(search->scriptFile, search->scriptFileLength, L"help", -1)) {
2673+
fprintf(
2674+
stderr,
2675+
"WARNING: The '%.*ls' command is unavailable because this is the legacy py.exe command.\n"
2676+
"If you have already installed the Python install manager, open Installed Apps and "
2677+
"remove 'Python Launcher' to enable the new py.exe command.\n",
2678+
search->scriptFileLength,
2679+
search->scriptFile
2680+
);
2681+
}
2682+
break;
26682683
default:
26692684
return exitCode;
26702685
}

PC/layout/main.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
__version__ = "3.8"
99

1010
import argparse
11+
import json
1112
import os
1213
import shutil
1314
import sys
@@ -28,6 +29,7 @@
2829
from .support.options import *
2930
from .support.pip import *
3031
from .support.props import *
32+
from .support.pymanager import *
3133
from .support.nuspec import *
3234

3335
TEST_PYDS_ONLY = FileStemSet("xxlimited", "xxlimited_35", "_ctypes_test", "_test*")
@@ -265,7 +267,12 @@ def _c(d):
265267
if ns.include_dev:
266268
for dest, src in rglob(ns.source / "Include", "**/*.h"):
267269
yield "include/{}".format(dest), src
268-
yield "include/pyconfig.h", ns.build / "pyconfig.h"
270+
# Support for layout of new and old releases.
271+
pc = ns.source / "PC"
272+
if (pc / "pyconfig.h.in").is_file():
273+
yield "include/pyconfig.h", ns.build / "pyconfig.h"
274+
else:
275+
yield "include/pyconfig.h", pc / "pyconfig.h"
269276

270277
for dest, src in get_tcltk_lib(ns):
271278
yield dest, src
@@ -303,6 +310,9 @@ def _c(d):
303310
else:
304311
yield "DLLs/{}".format(ns.include_cat.name), ns.include_cat
305312

313+
if ns.include_install_json or ns.include_install_embed_json or ns.include_install_test_json:
314+
yield "__install__.json", ns.temp / "__install__.json"
315+
306316

307317
def _compile_one_py(src, dest, name, optimize, checked=True):
308318
import py_compile
@@ -394,6 +404,22 @@ def generate_source_files(ns):
394404
log_info("Extracting pip")
395405
extract_pip_files(ns)
396406

407+
if ns.include_install_json:
408+
log_info("Generating __install__.json in {}", ns.temp)
409+
ns.temp.mkdir(parents=True, exist_ok=True)
410+
with open(ns.temp / "__install__.json", "w", encoding="utf-8") as f:
411+
json.dump(calculate_install_json(ns), f, indent=2)
412+
elif ns.include_install_embed_json:
413+
log_info("Generating embeddable __install__.json in {}", ns.temp)
414+
ns.temp.mkdir(parents=True, exist_ok=True)
415+
with open(ns.temp / "__install__.json", "w", encoding="utf-8") as f:
416+
json.dump(calculate_install_json(ns, for_embed=True), f, indent=2)
417+
elif ns.include_install_test_json:
418+
log_info("Generating test __install__.json in {}", ns.temp)
419+
ns.temp.mkdir(parents=True, exist_ok=True)
420+
with open(ns.temp / "__install__.json", "w", encoding="utf-8") as f:
421+
json.dump(calculate_install_json(ns, for_test=True), f, indent=2)
422+
397423

398424
def _create_zip_file(ns):
399425
if not ns.zip:
@@ -627,6 +653,7 @@ def main():
627653
if ns.include_cat and not ns.include_cat.is_absolute():
628654
ns.include_cat = (Path.cwd() / ns.include_cat).resolve()
629655
if not ns.arch:
656+
# TODO: Calculate arch from files in ns.build instead
630657
if sys.winver.endswith("-arm64"):
631658
ns.arch = "arm64"
632659
elif sys.winver.endswith("-32"):

PC/layout/support/options.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ def public(f):
3636
"alias": {"help": "aliased python.exe entry-point binaries"},
3737
"alias3": {"help": "aliased python3.exe entry-point binaries"},
3838
"alias3x": {"help": "aliased python3.x.exe entry-point binaries"},
39+
"install-json": {"help": "a PyManager __install__.json file"},
40+
"install-embed-json": {"help": "a PyManager __install__.json file for embeddable distro"},
41+
"install-test-json": {"help": "a PyManager __install__.json for the test distro"},
3942
}
4043

4144

@@ -95,6 +98,34 @@ def public(f):
9598
"precompile",
9699
],
97100
},
101+
"pymanager": {
102+
"help": "PyManager package",
103+
"options": [
104+
"stable",
105+
"pip",
106+
"tcltk",
107+
"idle",
108+
"venv",
109+
"dev",
110+
"html-doc",
111+
"install-json",
112+
],
113+
},
114+
"pymanager-test": {
115+
"help": "PyManager test package",
116+
"options": [
117+
"stable",
118+
"pip",
119+
"tcltk",
120+
"idle",
121+
"venv",
122+
"dev",
123+
"html-doc",
124+
"symbols",
125+
"tests",
126+
"install-test-json",
127+
],
128+
},
98129
}
99130

100131

PC/layout/support/pymanager.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
from .constants import *
2+
3+
URL_BASE = "https://www.python.org/ftp/python/"
4+
5+
XYZ_VERSION = f"{VER_MAJOR}.{VER_MINOR}.{VER_MICRO}"
6+
WIN32_VERSION = f"{VER_MAJOR}.{VER_MINOR}.{VER_MICRO}.{VER_FIELD4}"
7+
FULL_VERSION = f"{VER_MAJOR}.{VER_MINOR}.{VER_MICRO}{VER_SUFFIX}"
8+
9+
10+
def _not_empty(n, key=None):
11+
result = []
12+
for i in n:
13+
if key:
14+
i_l = i[key]
15+
else:
16+
i_l = i
17+
if not i_l:
18+
continue
19+
result.append(i)
20+
return result
21+
22+
23+
def calculate_install_json(ns, *, for_embed=False, for_test=False):
24+
TARGET = "python.exe"
25+
TARGETW = "pythonw.exe"
26+
27+
SYS_ARCH = {
28+
"win32": "32bit",
29+
"amd64": "64bit",
30+
"arm64": "64bit", # Unfortunate, but this is how it's spec'd
31+
}[ns.arch]
32+
TAG_ARCH = {
33+
"win32": "-32",
34+
"amd64": "-64",
35+
"arm64": "-arm64",
36+
}[ns.arch]
37+
38+
COMPANY = "PythonCore"
39+
DISPLAY_NAME = "Python"
40+
TAG_SUFFIX = ""
41+
ALIAS_PREFIX = "python"
42+
ALIAS_WPREFIX = "pythonw"
43+
FILE_PREFIX = "python-"
44+
FILE_SUFFIX = f"-{ns.arch}"
45+
DISPLAY_TAGS = [{
46+
"win32": "32-bit",
47+
"amd64": "",
48+
"arm64": "ARM64",
49+
}[ns.arch]]
50+
51+
if for_test:
52+
# Packages with the test suite come under a different Company
53+
COMPANY = "PythonTest"
54+
DISPLAY_TAGS.append("with tests")
55+
FILE_SUFFIX = f"-test-{ns.arch}"
56+
if for_embed:
57+
# Embeddable distro comes under a different Company
58+
COMPANY = "PythonEmbed"
59+
TARGETW = None
60+
ALIAS_PREFIX = None
61+
DISPLAY_TAGS.append("embeddable")
62+
# Deliberately name the file differently from the existing distro
63+
# so we can republish old versions without replacing files.
64+
FILE_SUFFIX = f"-embeddable-{ns.arch}"
65+
if ns.include_freethreaded:
66+
# Free-threaded distro comes with a tag suffix
67+
TAG_SUFFIX = "t"
68+
TARGET = f"python{VER_MAJOR}.{VER_MINOR}t.exe"
69+
TARGETW = f"pythonw{VER_MAJOR}.{VER_MINOR}t.exe"
70+
DISPLAY_TAGS.append("freethreaded")
71+
FILE_SUFFIX = f"t-{ns.arch}"
72+
73+
FULL_TAG = f"{VER_MAJOR}.{VER_MINOR}.{VER_MICRO}{VER_SUFFIX}{TAG_SUFFIX}"
74+
FULL_ARCH_TAG = f"{FULL_TAG}{TAG_ARCH}"
75+
XY_TAG = f"{VER_MAJOR}.{VER_MINOR}{TAG_SUFFIX}"
76+
XY_ARCH_TAG = f"{XY_TAG}{TAG_ARCH}"
77+
X_TAG = f"{VER_MAJOR}{TAG_SUFFIX}"
78+
X_ARCH_TAG = f"{X_TAG}{TAG_ARCH}"
79+
80+
# Tag used in runtime ID (for side-by-side install/updates)
81+
ID_TAG = XY_ARCH_TAG
82+
# Tag shown in 'py list' output
83+
DISPLAY_TAG = f"{XY_TAG}-dev{TAG_ARCH}" if VER_SUFFIX else XY_ARCH_TAG
84+
85+
DISPLAY_SUFFIX = ", ".join(i for i in DISPLAY_TAGS if i)
86+
if DISPLAY_SUFFIX:
87+
DISPLAY_SUFFIX = f" ({DISPLAY_SUFFIX})"
88+
DISPLAY_VERSION = f"{XYZ_VERSION}{VER_SUFFIX}{DISPLAY_SUFFIX}"
89+
90+
STD_RUN_FOR = []
91+
STD_ALIAS = []
92+
STD_PEP514 = []
93+
STD_START = []
94+
STD_UNINSTALL = []
95+
96+
# The list of 'py install <TAG>' tags that will match this runtime.
97+
# Architecture should always be included here because PyManager will add it.
98+
INSTALL_TAGS = [
99+
FULL_ARCH_TAG,
100+
XY_ARCH_TAG,
101+
X_ARCH_TAG,
102+
# X_TAG and XY_TAG doesn't include VER_SUFFIX, so create -dev versions
103+
f"{XY_TAG}-dev{TAG_ARCH}" if XY_TAG and VER_SUFFIX else "",
104+
f"{X_TAG}-dev{TAG_ARCH}" if X_TAG and VER_SUFFIX else "",
105+
]
106+
107+
# Generate run-for entries for each target.
108+
# Again, include architecture because PyManager will add it.
109+
for base in [
110+
{"target": TARGET},
111+
{"target": TARGETW, "windowed": 1},
112+
]:
113+
if not base["target"]:
114+
continue
115+
STD_RUN_FOR.append({**base, "tag": FULL_ARCH_TAG})
116+
if XY_TAG:
117+
STD_RUN_FOR.append({**base, "tag": XY_ARCH_TAG})
118+
if X_TAG:
119+
STD_RUN_FOR.append({**base, "tag": X_ARCH_TAG})
120+
if VER_SUFFIX:
121+
STD_RUN_FOR.extend([
122+
{**base, "tag": f"{XY_TAG}-dev{TAG_ARCH}" if XY_TAG else ""},
123+
{**base, "tag": f"{X_TAG}-dev{TAG_ARCH}" if X_TAG else ""},
124+
])
125+
126+
# Generate alias entries for each target. We need both arch and non-arch
127+
# versions as well as windowed/non-windowed versions to make sure that all
128+
# necessary aliases are created.
129+
if ALIAS_PREFIX:
130+
for prefix, base in [
131+
(ALIAS_PREFIX, {"target": TARGET}),
132+
(f"{ALIAS_PREFIX}w", {"target": TARGETW, "windowed": 1}),
133+
]:
134+
if not base["target"]:
135+
continue
136+
if XY_TAG:
137+
STD_ALIAS.extend([
138+
{**base, "name": f"{prefix}{XY_TAG}.exe"},
139+
{**base, "name": f"{prefix}{XY_ARCH_TAG}.exe"},
140+
])
141+
if X_TAG:
142+
STD_ALIAS.extend([
143+
{**base, "name": f"{prefix}{X_TAG}.exe"},
144+
{**base, "name": f"{prefix}{X_ARCH_TAG}.exe"},
145+
])
146+
147+
STD_PEP514.append({
148+
"kind": "pep514",
149+
"Key": rf"{COMPANY}\{ID_TAG}",
150+
"DisplayName": f"{DISPLAY_NAME} {DISPLAY_VERSION}",
151+
"SupportUrl": "https://www.python.org/",
152+
"SysArchitecture": SYS_ARCH,
153+
"SysVersion": VER_DOT,
154+
"Version": FULL_VERSION,
155+
"InstallPath": {
156+
"_": "%PREFIX%",
157+
"ExecutablePath": f"%PREFIX%{TARGET}",
158+
# WindowedExecutablePath is added below
159+
},
160+
"Help": {
161+
"Online Python Documentation": {
162+
"_": f"https://docs.python.org/{VER_DOT}/"
163+
},
164+
},
165+
})
166+
167+
STD_START.append({
168+
"kind": "start",
169+
"Name": f"{DISPLAY_NAME} {VER_DOT}{DISPLAY_SUFFIX}",
170+
"Items": [
171+
{
172+
"Name": f"{DISPLAY_NAME} {VER_DOT}{DISPLAY_SUFFIX}",
173+
"Target": f"%PREFIX%{TARGET}",
174+
"Icon": f"%PREFIX%{TARGET}",
175+
},
176+
{
177+
"Name": f"{DISPLAY_NAME} {VER_DOT} Online Documentation",
178+
"Icon": r"%SystemRoot%\System32\SHELL32.dll",
179+
"IconIndex": 13,
180+
"Target": f"https://docs.python.org/{VER_DOT}/",
181+
},
182+
# IDLE and local documentation items are added below
183+
],
184+
})
185+
186+
if TARGETW:
187+
STD_PEP514[0]["InstallPath"]["WindowedExecutablePath"] = f"%PREFIX%{TARGETW}"
188+
189+
if ns.include_idle:
190+
STD_START[0]["Items"].append({
191+
"Name": f"IDLE {VER_DOT}{DISPLAY_SUFFIX}",
192+
"Target": f"%PREFIX%{TARGETW or TARGET}",
193+
"Arguments": r'"%PREFIX%Lib\idlelib\idle.pyw"',
194+
"Icon": r"%PREFIX%Lib\idlelib\Icons\idle.ico",
195+
"IconIndex": 0,
196+
})
197+
STD_START[0]["Items"].append({
198+
"Name": f"PyDoc {VER_DOT}{DISPLAY_SUFFIX}",
199+
"Target": f"%PREFIX%{TARGET}",
200+
"Arguments": "-m pydoc -b",
201+
"Icon": r"%PREFIX%Lib\idlelib\Icons\idle.ico",
202+
"IconIndex": 0,
203+
})
204+
205+
if ns.include_html_doc:
206+
STD_PEP514[0]["Help"]["Main Python Documentation"] = {
207+
"_": rf"%PREFIX%Doc\html\index.html",
208+
}
209+
STD_START[0]["Items"].append({
210+
"Name": f"{DISPLAY_NAME} {VER_DOT} Manuals{DISPLAY_SUFFIX}",
211+
"Target": r"%PREFIX%Doc\html\index.html",
212+
})
213+
elif ns.include_chm:
214+
STD_PEP514[0]["Help"]["Main Python Documentation"] = {
215+
"_": rf"%PREFIX%Doc\{PYTHON_CHM_NAME}",
216+
}
217+
STD_START[0]["Items"].append({
218+
"Name": f"{DISPLAY_NAME} {VER_DOT} Manuals{DISPLAY_SUFFIX}",
219+
"Target": "%WINDIR%hhc.exe",
220+
"Arguments": rf"%PREFIX%Doc\{PYTHON_CHM_NAME}",
221+
})
222+
223+
STD_UNINSTALL.append({
224+
"kind": "uninstall",
225+
# Other settings will pick up sensible defaults
226+
"Publisher": "Python Software Foundation",
227+
"HelpLink": f"https://docs.python.org/{VER_DOT}/",
228+
})
229+
230+
data = {
231+
"schema": 1,
232+
"id": f"{COMPANY.lower()}-{ID_TAG}",
233+
"sort-version": FULL_VERSION,
234+
"company": COMPANY,
235+
"tag": DISPLAY_TAG,
236+
"install-for": _not_empty(INSTALL_TAGS),
237+
"run-for": _not_empty(STD_RUN_FOR, "tag"),
238+
"alias": _not_empty(STD_ALIAS, "name"),
239+
"shortcuts": [
240+
*STD_PEP514,
241+
*STD_START,
242+
*STD_UNINSTALL,
243+
],
244+
"display-name": f"{DISPLAY_NAME} {DISPLAY_VERSION}",
245+
"executable": rf".\{TARGET}",
246+
"url": f"{URL_BASE}{XYZ_VERSION}/{FILE_PREFIX}{FULL_VERSION}{FILE_SUFFIX}.zip"
247+
}
248+
249+
return data

0 commit comments

Comments
 (0)