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 ' 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