diff options
author | Steve Dower <steve.dower@microsoft.com> | 2018-12-11 02:52:57 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-12-11 02:52:57 (GMT) |
commit | 0cd6391fd890368ea1743dac50c366b42f2fd126 (patch) | |
tree | 1e2d8fd6c85a08477d3eb4082badd9a50386e63e /PC/layout/support | |
parent | 1c3de541e64f75046b20cdd27bada1557e550bcd (diff) | |
download | cpython-0cd6391fd890368ea1743dac50c366b42f2fd126.zip cpython-0cd6391fd890368ea1743dac50c366b42f2fd126.tar.gz cpython-0cd6391fd890368ea1743dac50c366b42f2fd126.tar.bz2 |
bpo-34977: Add Windows App Store package (GH-11027)
Also adds the PC/layout script for generating layouts on Windows.
Diffstat (limited to 'PC/layout/support')
-rw-r--r-- | PC/layout/support/__init__.py | 0 | ||||
-rw-r--r-- | PC/layout/support/appxmanifest.py | 487 | ||||
-rw-r--r-- | PC/layout/support/catalog.py | 44 | ||||
-rw-r--r-- | PC/layout/support/constants.py | 28 | ||||
-rw-r--r-- | PC/layout/support/distutils.command.bdist_wininst.py | 25 | ||||
-rw-r--r-- | PC/layout/support/filesets.py | 100 | ||||
-rw-r--r-- | PC/layout/support/logging.py | 93 | ||||
-rw-r--r-- | PC/layout/support/options.py | 122 | ||||
-rw-r--r-- | PC/layout/support/pip.py | 79 | ||||
-rw-r--r-- | PC/layout/support/props.py | 110 | ||||
-rw-r--r-- | PC/layout/support/python.props | 56 |
11 files changed, 1144 insertions, 0 deletions
diff --git a/PC/layout/support/__init__.py b/PC/layout/support/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/PC/layout/support/__init__.py diff --git a/PC/layout/support/appxmanifest.py b/PC/layout/support/appxmanifest.py new file mode 100644 index 0000000..c5dda70 --- /dev/null +++ b/PC/layout/support/appxmanifest.py @@ -0,0 +1,487 @@ +""" +File generation for APPX/MSIX manifests. +""" + +__author__ = "Steve Dower <steve.dower@python.org>" +__version__ = "3.8" + + +import collections +import ctypes +import io +import os +import sys + +from pathlib import Path, PureWindowsPath +from xml.etree import ElementTree as ET + +from .constants import * + +__all__ = [] + + +def public(f): + __all__.append(f.__name__) + return f + + +APPX_DATA = dict( + Name="PythonSoftwareFoundation.Python.{}".format(VER_DOT), + Version="{}.{}.{}.0".format(VER_MAJOR, VER_MINOR, VER_FIELD3), + Publisher=os.getenv( + "APPX_DATA_PUBLISHER", "CN=4975D53F-AA7E-49A5-8B49-EA4FDC1BB66B" + ), + DisplayName="Python {}".format(VER_DOT), + Description="The Python {} runtime and console.".format(VER_DOT), + ProcessorArchitecture="x64" if IS_X64 else "x86", +) + +PYTHON_VE_DATA = dict( + DisplayName="Python {}".format(VER_DOT), + Description="Python interactive console", + Square150x150Logo="_resources/pythonx150.png", + Square44x44Logo="_resources/pythonx44.png", + BackgroundColor="transparent", +) + +PYTHONW_VE_DATA = dict( + DisplayName="Python {} (Windowed)".format(VER_DOT), + Description="Python windowed app launcher", + Square150x150Logo="_resources/pythonwx150.png", + Square44x44Logo="_resources/pythonwx44.png", + BackgroundColor="transparent", + AppListEntry="none", +) + +PIP_VE_DATA = dict( + DisplayName="pip (Python {})".format(VER_DOT), + Description="pip package manager for Python {}".format(VER_DOT), + Square150x150Logo="_resources/pythonx150.png", + Square44x44Logo="_resources/pythonx44.png", + BackgroundColor="transparent", + AppListEntry="none", +) + +IDLE_VE_DATA = dict( + DisplayName="IDLE (Python {})".format(VER_DOT), + Description="IDLE editor for Python {}".format(VER_DOT), + Square150x150Logo="_resources/pythonwx150.png", + Square44x44Logo="_resources/pythonwx44.png", + BackgroundColor="transparent", +) + +APPXMANIFEST_NS = { + "": "http://schemas.microsoft.com/appx/manifest/foundation/windows10", + "m": "http://schemas.microsoft.com/appx/manifest/foundation/windows10", + "uap": "http://schemas.microsoft.com/appx/manifest/uap/windows10", + "rescap": "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities", + "rescap4": "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/4", + "desktop4": "http://schemas.microsoft.com/appx/manifest/desktop/windows10/4", + "desktop6": "http://schemas.microsoft.com/appx/manifest/desktop/windows10/6", + "uap3": "http://schemas.microsoft.com/appx/manifest/uap/windows10/3", + "uap4": "http://schemas.microsoft.com/appx/manifest/uap/windows10/4", + "uap5": "http://schemas.microsoft.com/appx/manifest/uap/windows10/5", +} + +APPXMANIFEST_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?> +<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" + xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" + xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" + xmlns:rescap4="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/4" + xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" + xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4" + xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"> + <Identity Name="" + Version="" + Publisher="" + ProcessorArchitecture="" /> + <Properties> + <DisplayName></DisplayName> + <PublisherDisplayName>Python Software Foundation</PublisherDisplayName> + <Description></Description> + <Logo>_resources/pythonx50.png</Logo> + </Properties> + <Resources> + <Resource Language="en-US" /> + </Resources> + <Dependencies> + <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="" /> + </Dependencies> + <Capabilities> + <rescap:Capability Name="runFullTrust"/> + </Capabilities> + <Applications> + </Applications> + <Extensions> + </Extensions> +</Package>""" + + +RESOURCES_XML_TEMPLATE = r"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<!--This file is input for makepri.exe. It should be excluded from the final package.--> +<resources targetOsVersion="10.0.0" majorVersion="1"> + <packaging> + <autoResourcePackage qualifier="Language"/> + <autoResourcePackage qualifier="Scale"/> + <autoResourcePackage qualifier="DXFeatureLevel"/> + </packaging> + <index root="\" startIndexAt="\"> + <default> + <qualifier name="Language" value="en-US"/> + <qualifier name="Contrast" value="standard"/> + <qualifier name="Scale" value="100"/> + <qualifier name="HomeRegion" value="001"/> + <qualifier name="TargetSize" value="256"/> + <qualifier name="LayoutDirection" value="LTR"/> + <qualifier name="Theme" value="dark"/> + <qualifier name="AlternateForm" value=""/> + <qualifier name="DXFeatureLevel" value="DX9"/> + <qualifier name="Configuration" value=""/> + <qualifier name="DeviceFamily" value="Universal"/> + <qualifier name="Custom" value=""/> + </default> + <indexer-config type="folder" foldernameAsQualifier="true" filenameAsQualifier="true" qualifierDelimiter="$"/> + <indexer-config type="resw" convertDotsToSlashes="true" initialPath=""/> + <indexer-config type="resjson" initialPath=""/> + <indexer-config type="PRI"/> + </index> +</resources>""" + + +SCCD_FILENAME = "PC/classicAppCompat.sccd" + +REGISTRY = { + "HKCU\\Software\\Python\\PythonCore": { + VER_DOT: { + "DisplayName": APPX_DATA["DisplayName"], + "SupportUrl": "https://www.python.org/", + "SysArchitecture": "64bit" if IS_X64 else "32bit", + "SysVersion": VER_DOT, + "Version": "{}.{}.{}".format(VER_MAJOR, VER_MINOR, VER_MICRO), + "InstallPath": { + # I have no idea why the trailing spaces are needed, but they seem to be needed. + "": "[{AppVPackageRoot}][ ]", + "ExecutablePath": "[{AppVPackageRoot}]python.exe[ ]", + "WindowedExecutablePath": "[{AppVPackageRoot}]pythonw.exe[ ]", + }, + "Help": { + "Main Python Documentation": { + "_condition": lambda ns: ns.include_chm, + "": "[{{AppVPackageRoot}}]Doc\\{}[ ]".format( + PYTHON_CHM_NAME + ), + }, + "Local Python Documentation": { + "_condition": lambda ns: ns.include_html_doc, + "": "[{AppVPackageRoot}]Doc\\html\\index.html[ ]", + }, + "Online Python Documentation": { + "": "https://docs.python.org/{}".format(VER_DOT) + }, + }, + "Idle": { + "_condition": lambda ns: ns.include_idle, + "": "[{AppVPackageRoot}]Lib\\idlelib\\idle.pyw[ ]", + }, + } + } +} + + +def get_packagefamilyname(name, publisher_id): + class PACKAGE_ID(ctypes.Structure): + _fields_ = [ + ("reserved", ctypes.c_uint32), + ("processorArchitecture", ctypes.c_uint32), + ("version", ctypes.c_uint64), + ("name", ctypes.c_wchar_p), + ("publisher", ctypes.c_wchar_p), + ("resourceId", ctypes.c_wchar_p), + ("publisherId", ctypes.c_wchar_p), + ] + _pack_ = 4 + + pid = PACKAGE_ID(0, 0, 0, name, publisher_id, None, None) + result = ctypes.create_unicode_buffer(256) + result_len = ctypes.c_uint32(256) + r = ctypes.windll.kernel32.PackageFamilyNameFromId( + pid, ctypes.byref(result_len), result + ) + if r: + raise OSError(r, "failed to get package family name") + return result.value[: result_len.value] + + +def _fixup_sccd(ns, sccd, new_hash=None): + if not new_hash: + return sccd + + NS = dict(s="http://schemas.microsoft.com/appx/2016/sccd") + with open(sccd, "rb") as f: + xml = ET.parse(f) + + pfn = get_packagefamilyname(APPX_DATA["Name"], APPX_DATA["Publisher"]) + + ae = xml.find("s:AuthorizedEntities", NS) + ae.clear() + + e = ET.SubElement(ae, ET.QName(NS["s"], "AuthorizedEntity")) + e.set("AppPackageFamilyName", pfn) + e.set("CertificateSignatureHash", new_hash) + + for e in xml.findall("s:Catalog", NS): + e.text = "FFFF" + + sccd = ns.temp / sccd.name + sccd.parent.mkdir(parents=True, exist_ok=True) + with open(sccd, "wb") as f: + xml.write(f, encoding="utf-8") + + return sccd + + +@public +def get_appx_layout(ns): + if not ns.include_appxmanifest: + return + + yield "AppxManifest.xml", ns.temp / "AppxManifest.xml" + yield "_resources.xml", ns.temp / "_resources.xml" + icons = ns.source / "PC" / "icons" + yield "_resources/pythonx44.png", icons / "pythonx44.png" + yield "_resources/pythonx44$targetsize-44_altform-unplated.png", icons / "pythonx44.png" + yield "_resources/pythonx50.png", icons / "pythonx50.png" + yield "_resources/pythonx50$targetsize-50_altform-unplated.png", icons / "pythonx50.png" + yield "_resources/pythonx150.png", icons / "pythonx150.png" + yield "_resources/pythonx150$targetsize-150_altform-unplated.png", icons / "pythonx150.png" + yield "_resources/pythonwx44.png", icons / "pythonwx44.png" + yield "_resources/pythonwx44$targetsize-44_altform-unplated.png", icons / "pythonwx44.png" + yield "_resources/pythonwx150.png", icons / "pythonwx150.png" + yield "_resources/pythonwx150$targetsize-150_altform-unplated.png", icons / "pythonwx150.png" + sccd = ns.source / SCCD_FILENAME + if sccd.is_file(): + # This should only be set for side-loading purposes. + sccd = _fixup_sccd(ns, sccd, os.getenv("APPX_DATA_SHA256")) + yield sccd.name, sccd + + +def find_or_add(xml, element, attr=None, always_add=False): + if always_add: + e = None + else: + q = element + if attr: + q += "[@{}='{}']".format(*attr) + e = xml.find(q, APPXMANIFEST_NS) + if e is None: + prefix, _, name = element.partition(":") + name = ET.QName(APPXMANIFEST_NS[prefix or ""], name) + e = ET.SubElement(xml, name) + if attr: + e.set(*attr) + return e + + +def _get_app(xml, appid): + if appid: + app = xml.find( + "m:Applications/m:Application[@Id='{}']".format(appid), APPXMANIFEST_NS + ) + if app is None: + raise LookupError(appid) + else: + app = xml + return app + + +def add_visual(xml, appid, data): + app = _get_app(xml, appid) + e = find_or_add(app, "uap:VisualElements") + for i in data.items(): + e.set(*i) + return e + + +def add_alias(xml, appid, alias, subsystem="windows"): + app = _get_app(xml, appid) + e = find_or_add(app, "m:Extensions") + e = find_or_add(e, "uap5:Extension", ("Category", "windows.appExecutionAlias")) + e = find_or_add(e, "uap5:AppExecutionAlias") + e.set(ET.QName(APPXMANIFEST_NS["desktop4"], "Subsystem"), subsystem) + e = find_or_add(e, "uap5:ExecutionAlias", ("Alias", alias)) + + +def add_file_type(xml, appid, name, suffix, parameters='"%1"'): + app = _get_app(xml, appid) + e = find_or_add(app, "m:Extensions") + e = find_or_add(e, "uap3:Extension", ("Category", "windows.fileTypeAssociation")) + e = find_or_add(e, "uap3:FileTypeAssociation", ("Name", name)) + e.set("Parameters", parameters) + e = find_or_add(e, "uap:SupportedFileTypes") + if isinstance(suffix, str): + suffix = [suffix] + for s in suffix: + ET.SubElement(e, ET.QName(APPXMANIFEST_NS["uap"], "FileType")).text = s + + +def add_application( + ns, xml, appid, executable, aliases, visual_element, subsystem, file_types +): + node = xml.find("m:Applications", APPXMANIFEST_NS) + suffix = "_d.exe" if ns.debug else ".exe" + app = ET.SubElement( + node, + ET.QName(APPXMANIFEST_NS[""], "Application"), + { + "Id": appid, + "Executable": executable + suffix, + "EntryPoint": "Windows.FullTrustApplication", + ET.QName(APPXMANIFEST_NS["desktop4"], "SupportsMultipleInstances"): "true", + }, + ) + if visual_element: + add_visual(app, None, visual_element) + for alias in aliases: + add_alias(app, None, alias + suffix, subsystem) + if file_types: + add_file_type(app, None, *file_types) + return app + + +def _get_registry_entries(ns, root="", d=None): + r = root if root else PureWindowsPath("") + if d is None: + d = REGISTRY + for key, value in d.items(): + if key == "_condition": + continue + elif isinstance(value, dict): + cond = value.get("_condition") + if cond and not cond(ns): + continue + fullkey = r + for part in PureWindowsPath(key).parts: + fullkey /= part + if len(fullkey.parts) > 1: + yield str(fullkey), None, None + yield from _get_registry_entries(ns, fullkey, value) + elif len(r.parts) > 1: + yield str(r), key, value + + +def add_registry_entries(ns, xml): + e = find_or_add(xml, "m:Extensions") + e = find_or_add(e, "rescap4:Extension") + e.set("Category", "windows.classicAppCompatKeys") + e.set("EntryPoint", "Windows.FullTrustApplication") + e = ET.SubElement(e, ET.QName(APPXMANIFEST_NS["rescap4"], "ClassicAppCompatKeys")) + for name, valuename, value in _get_registry_entries(ns): + k = ET.SubElement( + e, ET.QName(APPXMANIFEST_NS["rescap4"], "ClassicAppCompatKey") + ) + k.set("Name", name) + if value: + k.set("ValueName", valuename) + k.set("Value", value) + k.set("ValueType", "REG_SZ") + + +def disable_registry_virtualization(xml): + e = find_or_add(xml, "m:Properties") + e = find_or_add(e, "desktop6:RegistryWriteVirtualization") + e.text = "disabled" + e = find_or_add(xml, "m:Capabilities") + e = find_or_add(e, "rescap:Capability", ("Name", "unvirtualizedResources")) + + +@public +def get_appxmanifest(ns): + for k, v in APPXMANIFEST_NS.items(): + ET.register_namespace(k, v) + ET.register_namespace("", APPXMANIFEST_NS["m"]) + + xml = ET.parse(io.StringIO(APPXMANIFEST_TEMPLATE)) + NS = APPXMANIFEST_NS + QN = ET.QName + + node = xml.find("m:Identity", NS) + for k in node.keys(): + value = APPX_DATA.get(k) + if value: + node.set(k, value) + + for node in xml.find("m:Properties", NS): + value = APPX_DATA.get(node.tag.rpartition("}")[2]) + if value: + node.text = value + + winver = sys.getwindowsversion()[:3] + if winver < (10, 0, 17763): + winver = 10, 0, 17763 + find_or_add(xml, "m:Dependencies/m:TargetDeviceFamily").set( + "MaxVersionTested", "{}.{}.{}.0".format(*winver) + ) + + if winver > (10, 0, 17763): + disable_registry_virtualization(xml) + + app = add_application( + ns, + xml, + "Python", + "python", + ["python", "python{}".format(VER_MAJOR), "python{}".format(VER_DOT)], + PYTHON_VE_DATA, + "console", + ("python.file", [".py"]), + ) + + add_application( + ns, + xml, + "PythonW", + "pythonw", + ["pythonw", "pythonw{}".format(VER_MAJOR), "pythonw{}".format(VER_DOT)], + PYTHONW_VE_DATA, + "windows", + ("python.windowedfile", [".pyw"]), + ) + + if ns.include_pip and ns.include_launchers: + add_application( + ns, + xml, + "Pip", + "pip", + ["pip", "pip{}".format(VER_MAJOR), "pip{}".format(VER_DOT)], + PIP_VE_DATA, + "console", + ("python.wheel", [".whl"], 'install "%1"'), + ) + + if ns.include_idle and ns.include_launchers: + add_application( + ns, + xml, + "Idle", + "idle", + ["idle", "idle{}".format(VER_MAJOR), "idle{}".format(VER_DOT)], + IDLE_VE_DATA, + "windows", + None, + ) + + if (ns.source / SCCD_FILENAME).is_file(): + add_registry_entries(ns, xml) + node = xml.find("m:Capabilities", NS) + node = ET.SubElement(node, QN(NS["uap4"], "CustomCapability")) + node.set("Name", "Microsoft.classicAppCompat_8wekyb3d8bbwe") + + buffer = io.BytesIO() + xml.write(buffer, encoding="utf-8", xml_declaration=True) + return buffer.getbuffer() + + +@public +def get_resources_xml(ns): + return RESOURCES_XML_TEMPLATE.encode("utf-8") diff --git a/PC/layout/support/catalog.py b/PC/layout/support/catalog.py new file mode 100644 index 0000000..4312118 --- /dev/null +++ b/PC/layout/support/catalog.py @@ -0,0 +1,44 @@ +""" +File generation for catalog signing non-binary contents. +""" + +__author__ = "Steve Dower <steve.dower@python.org>" +__version__ = "3.8" + + +import sys + +__all__ = ["PYTHON_CAT_NAME", "PYTHON_CDF_NAME"] + + +def public(f): + __all__.append(f.__name__) + return f + + +PYTHON_CAT_NAME = "python.cat" +PYTHON_CDF_NAME = "python.cdf" + + +CATALOG_TEMPLATE = r"""[CatalogHeader] +Name={target.stem}.cat +ResultDir={target.parent} +PublicVersion=1 +CatalogVersion=2 +HashAlgorithms=SHA256 +PageHashes=false +EncodingType= + +[CatalogFiles] +""" + + +def can_sign(file): + return file.is_file() and file.stat().st_size + + +@public +def write_catalog(target, files): + with target.open("w", encoding="utf-8") as cat: + cat.write(CATALOG_TEMPLATE.format(target=target)) + cat.writelines("<HASH>{}={}\n".format(n, f) for n, f in files if can_sign(f)) diff --git a/PC/layout/support/constants.py b/PC/layout/support/constants.py new file mode 100644 index 0000000..88ea410 --- /dev/null +++ b/PC/layout/support/constants.py @@ -0,0 +1,28 @@ +""" +Constants for generating the layout. +""" + +__author__ = "Steve Dower <steve.dower@python.org>" +__version__ = "3.8" + +import struct +import sys + +VER_MAJOR, VER_MINOR, VER_MICRO, VER_FIELD4 = struct.pack(">i", sys.hexversion) +VER_FIELD3 = VER_MICRO << 8 | VER_FIELD4 +VER_NAME = {"alpha": "a", "beta": "b", "rc": "rc"}.get( + sys.version_info.releaselevel, "" +) +VER_SERIAL = sys.version_info.serial if VER_NAME else "" +VER_DOT = "{}.{}".format(VER_MAJOR, VER_MINOR) + +PYTHON_DLL_NAME = "python{}{}.dll".format(VER_MAJOR, VER_MINOR) +PYTHON_STABLE_DLL_NAME = "python{}.dll".format(VER_MAJOR) +PYTHON_ZIP_NAME = "python{}{}.zip".format(VER_MAJOR, VER_MINOR) +PYTHON_PTH_NAME = "python{}{}._pth".format(VER_MAJOR, VER_MINOR) + +PYTHON_CHM_NAME = "python{}{}{}{}{}.chm".format( + VER_MAJOR, VER_MINOR, VER_MICRO, VER_NAME, VER_SERIAL +) + +IS_X64 = sys.maxsize > 2 ** 32 diff --git a/PC/layout/support/distutils.command.bdist_wininst.py b/PC/layout/support/distutils.command.bdist_wininst.py new file mode 100644 index 0000000..6e9b49f --- /dev/null +++ b/PC/layout/support/distutils.command.bdist_wininst.py @@ -0,0 +1,25 @@ +"""distutils.command.bdist_wininst + +Suppress the 'bdist_wininst' command, while still allowing +setuptools to import it without breaking.""" + +from distutils.core import Command +from distutils.errors import DistutilsPlatformError + + +class bdist_wininst(Command): + description = "create an executable installer for MS Windows" + + # Marker for tests that we have the unsupported bdist_wininst + _unsupported = True + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + raise DistutilsPlatformError( + "bdist_wininst is not supported in this Python distribution" + ) diff --git a/PC/layout/support/filesets.py b/PC/layout/support/filesets.py new file mode 100644 index 0000000..47f727c --- /dev/null +++ b/PC/layout/support/filesets.py @@ -0,0 +1,100 @@ +""" +File sets and globbing helper for make_layout. +""" + +__author__ = "Steve Dower <steve.dower@python.org>" +__version__ = "3.8" + +import os + + +class FileStemSet: + def __init__(self, *patterns): + self._names = set() + self._prefixes = [] + self._suffixes = [] + for p in map(os.path.normcase, patterns): + if p.endswith("*"): + self._prefixes.append(p[:-1]) + elif p.startswith("*"): + self._suffixes.append(p[1:]) + else: + self._names.add(p) + + def _make_name(self, f): + return os.path.normcase(f.stem) + + def __contains__(self, f): + bn = self._make_name(f) + return ( + bn in self._names + or any(map(bn.startswith, self._prefixes)) + or any(map(bn.endswith, self._suffixes)) + ) + + +class FileNameSet(FileStemSet): + def _make_name(self, f): + return os.path.normcase(f.name) + + +class FileSuffixSet: + def __init__(self, *patterns): + self._names = set() + self._prefixes = [] + self._suffixes = [] + for p in map(os.path.normcase, patterns): + if p.startswith("*."): + self._names.add(p[1:]) + elif p.startswith("*"): + self._suffixes.append(p[1:]) + elif p.endswith("*"): + self._prefixes.append(p[:-1]) + elif p.startswith("."): + self._names.add(p) + else: + self._names.add("." + p) + + def _make_name(self, f): + return os.path.normcase(f.suffix) + + def __contains__(self, f): + bn = self._make_name(f) + return ( + bn in self._names + or any(map(bn.startswith, self._prefixes)) + or any(map(bn.endswith, self._suffixes)) + ) + + +def _rglob(root, pattern, condition): + dirs = [root] + recurse = pattern[:3] in {"**/", "**\\"} + if recurse: + pattern = pattern[3:] + + while dirs: + d = dirs.pop(0) + if recurse: + dirs.extend( + filter( + condition, (type(root)(f2) for f2 in os.scandir(d) if f2.is_dir()) + ) + ) + yield from ( + (f.relative_to(root), f) + for f in d.glob(pattern) + if f.is_file() and condition(f) + ) + + +def _return_true(f): + return True + + +def rglob(root, patterns, condition=None): + if isinstance(patterns, tuple): + for p in patterns: + yield from _rglob(root, p, condition or _return_true) + else: + yield from _rglob(root, patterns, condition or _return_true) diff --git a/PC/layout/support/logging.py b/PC/layout/support/logging.py new file mode 100644 index 0000000..30869b9 --- /dev/null +++ b/PC/layout/support/logging.py @@ -0,0 +1,93 @@ +""" +Logging support for make_layout. +""" + +__author__ = "Steve Dower <steve.dower@python.org>" +__version__ = "3.8" + +import logging +import sys + +__all__ = [] + +LOG = None +HAS_ERROR = False + + +def public(f): + __all__.append(f.__name__) + return f + + +@public +def configure_logger(ns): + global LOG + if LOG: + return + + LOG = logging.getLogger("make_layout") + LOG.level = logging.DEBUG + + if ns.v: + s_level = max(logging.ERROR - ns.v * 10, logging.DEBUG) + f_level = max(logging.WARNING - ns.v * 10, logging.DEBUG) + else: + s_level = logging.ERROR + f_level = logging.INFO + + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(logging.Formatter("{levelname:8s} {message}", style="{")) + handler.setLevel(s_level) + LOG.addHandler(handler) + + if ns.log: + handler = logging.FileHandler(ns.log, encoding="utf-8", delay=True) + handler.setFormatter( + logging.Formatter("[{asctime}]{levelname:8s}: {message}", style="{") + ) + handler.setLevel(f_level) + LOG.addHandler(handler) + + +class BraceMessage: + def __init__(self, fmt, *args, **kwargs): + self.fmt = fmt + self.args = args + self.kwargs = kwargs + + def __str__(self): + return self.fmt.format(*self.args, **self.kwargs) + + +@public +def log_debug(msg, *args, **kwargs): + return LOG.debug(BraceMessage(msg, *args, **kwargs)) + + +@public +def log_info(msg, *args, **kwargs): + return LOG.info(BraceMessage(msg, *args, **kwargs)) + + +@public +def log_warning(msg, *args, **kwargs): + return LOG.warning(BraceMessage(msg, *args, **kwargs)) + + +@public +def log_error(msg, *args, **kwargs): + global HAS_ERROR + HAS_ERROR = True + return LOG.error(BraceMessage(msg, *args, **kwargs)) + + +@public +def log_exception(msg, *args, **kwargs): + global HAS_ERROR + HAS_ERROR = True + return LOG.exception(BraceMessage(msg, *args, **kwargs)) + + +@public +def error_was_logged(): + return HAS_ERROR diff --git a/PC/layout/support/options.py b/PC/layout/support/options.py new file mode 100644 index 0000000..76d9e34 --- /dev/null +++ b/PC/layout/support/options.py @@ -0,0 +1,122 @@ +""" +List of optional components. +""" + +__author__ = "Steve Dower <steve.dower@python.org>" +__version__ = "3.8" + + +__all__ = [] + + +def public(f): + __all__.append(f.__name__) + return f + + +OPTIONS = { + "stable": {"help": "stable ABI stub"}, + "pip": {"help": "pip"}, + "distutils": {"help": "distutils"}, + "tcltk": {"help": "Tcl, Tk and tkinter"}, + "idle": {"help": "Idle"}, + "tests": {"help": "test suite"}, + "tools": {"help": "tools"}, + "venv": {"help": "venv"}, + "dev": {"help": "headers and libs"}, + "symbols": {"help": "symbols"}, + "bdist-wininst": {"help": "bdist_wininst support"}, + "underpth": {"help": "a python._pth file", "not-in-all": True}, + "launchers": {"help": "specific launchers"}, + "appxmanifest": {"help": "an appxmanifest"}, + "props": {"help": "a python.props file"}, + "chm": {"help": "the CHM documentation"}, + "html-doc": {"help": "the HTML documentation"}, +} + + +PRESETS = { + "appx": { + "help": "APPX package", + "options": [ + "stable", + "pip", + "distutils", + "tcltk", + "idle", + "venv", + "dev", + "launchers", + "appxmanifest", + # XXX: Disabled for now "precompile", + ], + }, + "nuget": { + "help": "nuget package", + "options": ["stable", "pip", "distutils", "dev", "props"], + }, + "default": { + "help": "development kit package", + "options": [ + "stable", + "pip", + "distutils", + "tcltk", + "idle", + "tests", + "tools", + "venv", + "dev", + "symbols", + "bdist-wininst", + "chm", + ], + }, + "embed": { + "help": "embeddable package", + "options": ["stable", "zip-lib", "flat-dlls", "underpth", "precompile"], + }, +} + + +@public +def get_argparse_options(): + for opt, info in OPTIONS.items(): + help = "When specified, includes {}".format(info["help"]) + if info.get("not-in-all"): + help = "{}. Not affected by --include-all".format(help) + + yield "--include-{}".format(opt), help + + for opt, info in PRESETS.items(): + help = "When specified, includes default options for {}".format(info["help"]) + yield "--preset-{}".format(opt), help + + +def ns_get(ns, key, default=False): + return getattr(ns, key.replace("-", "_"), default) + + +def ns_set(ns, key, value=True): + k1 = key.replace("-", "_") + k2 = "include_{}".format(k1) + if hasattr(ns, k2): + setattr(ns, k2, value) + elif hasattr(ns, k1): + setattr(ns, k1, value) + else: + raise AttributeError("no argument named '{}'".format(k1)) + + +@public +def update_presets(ns): + for preset, info in PRESETS.items(): + if ns_get(ns, "preset-{}".format(preset)): + for opt in info["options"]: + ns_set(ns, opt) + + if ns.include_all: + for opt in OPTIONS: + if OPTIONS[opt].get("not-in-all"): + continue + ns_set(ns, opt) diff --git a/PC/layout/support/pip.py b/PC/layout/support/pip.py new file mode 100644 index 0000000..369a923 --- /dev/null +++ b/PC/layout/support/pip.py @@ -0,0 +1,79 @@ +""" +Extraction and file list generation for pip. +""" + +__author__ = "Steve Dower <steve.dower@python.org>" +__version__ = "3.8" + + +import os +import shutil +import subprocess +import sys + +__all__ = [] + + +def public(f): + __all__.append(f.__name__) + return f + + +@public +def get_pip_dir(ns): + if ns.copy: + if ns.zip_lib: + return ns.copy / "packages" + return ns.copy / "Lib" / "site-packages" + else: + return ns.temp / "packages" + + +@public +def extract_pip_files(ns): + dest = get_pip_dir(ns) + dest.mkdir(parents=True, exist_ok=True) + + src = ns.source / "Lib" / "ensurepip" / "_bundled" + + ns.temp.mkdir(parents=True, exist_ok=True) + wheels = [shutil.copy(whl, ns.temp) for whl in src.glob("*.whl")] + search_path = os.pathsep.join(wheels) + if os.environ.get("PYTHONPATH"): + search_path += ";" + os.environ["PYTHONPATH"] + + env = os.environ.copy() + env["PYTHONPATH"] = search_path + + output = subprocess.check_output( + [ + sys.executable, + "-m", + "pip", + "--no-color", + "install", + "pip", + "setuptools", + "--upgrade", + "--target", + str(dest), + "--no-index", + "--no-cache-dir", + "-f", + str(src), + "--only-binary", + ":all:", + ], + env=env, + ) + + try: + shutil.rmtree(dest / "bin") + except OSError: + pass + + for file in wheels: + try: + os.remove(file) + except OSError: + pass diff --git a/PC/layout/support/props.py b/PC/layout/support/props.py new file mode 100644 index 0000000..3a047d2 --- /dev/null +++ b/PC/layout/support/props.py @@ -0,0 +1,110 @@ +""" +Provides .props file. +""" + +import os + +from .constants import * + +__all__ = ["PYTHON_PROPS_NAME"] + + +def public(f): + __all__.append(f.__name__) + return f + + +PYTHON_PROPS_NAME = "python.props" + +PROPS_DATA = { + "PYTHON_TAG": VER_DOT, + "PYTHON_VERSION": os.getenv("PYTHON_NUSPEC_VERSION"), + "PYTHON_PLATFORM": os.getenv("PYTHON_PROPS_PLATFORM"), + "PYTHON_TARGET": "", +} + +if not PROPS_DATA["PYTHON_VERSION"]: + if VER_NAME: + PROPS_DATA["PYTHON_VERSION"] = "{}.{}-{}{}".format( + VER_DOT, VER_MICRO, VER_NAME, VER_SERIAL + ) + else: + PROPS_DATA["PYTHON_VERSION"] = "{}.{}".format(VER_DOT, VER_MICRO) + +if not PROPS_DATA["PYTHON_PLATFORM"]: + PROPS_DATA["PYTHON_PLATFORM"] = "x64" if IS_X64 else "Win32" + +PROPS_DATA["PYTHON_TARGET"] = "_GetPythonRuntimeFilesDependsOn{}{}_{}".format( + VER_MAJOR, VER_MINOR, PROPS_DATA["PYTHON_PLATFORM"] +) + +PROPS_TEMPLATE = r"""<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup Condition="$(Platform) == '{PYTHON_PLATFORM}'"> + <PythonHome Condition="$(Configuration) == 'Debug'">$([msbuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), "python_d.exe")</PythonHome> + <PythonHome Condition="$(PythonHome) == ''">$([msbuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), "python.exe")</PythonHome> + <PythonInclude>$(PythonHome)\include</PythonInclude> + <PythonLibs>$(PythonHome)\libs</PythonLibs> + <PythonTag>{PYTHON_TAG}</PythonTag> + <PythonVersion>{PYTHON_VERSION}</PythonVersion> + + <IncludePythonExe Condition="$(IncludePythonExe) == ''">true</IncludePythonExe> + <IncludeDistutils Condition="$(IncludeDistutils) == ''">false</IncludeDistutils> + <IncludeLib2To3 Condition="$(IncludeLib2To3) == ''">false</IncludeLib2To3> + <IncludeVEnv Condition="$(IncludeVEnv) == ''">false</IncludeVEnv> + + <GetPythonRuntimeFilesDependsOn>{PYTHON_TARGET};$(GetPythonRuntimeFilesDependsOn)</GetPythonRuntimeFilesDependsOn> + </PropertyGroup> + + <ItemDefinitionGroup Condition="$(Platform) == '{PYTHON_PLATFORM}'"> + <ClCompile> + <AdditionalIncludeDirectories>$(PythonInclude);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary> + </ClCompile> + <Link> + <AdditionalLibraryDirectories>$(PythonLibs);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> + </Link> + </ItemDefinitionGroup> + + <Target Name="GetPythonRuntimeFiles" Returns="@(PythonRuntime)" DependsOnTargets="$(GetPythonRuntimeFilesDependsOn)" /> + + <Target Name="{PYTHON_TARGET}" Returns="@(PythonRuntime)"> + <ItemGroup> + <_PythonRuntimeExe Include="$(PythonHome)\python*.dll" /> + <_PythonRuntimeExe Include="$(PythonHome)\python*.exe" Condition="$(IncludePythonExe) == 'true'" /> + <_PythonRuntimeExe> + <Link>%(Filename)%(Extension)</Link> + </_PythonRuntimeExe> + <_PythonRuntimeDlls Include="$(PythonHome)\DLLs\*.pyd" /> + <_PythonRuntimeDlls Include="$(PythonHome)\DLLs\*.dll" /> + <_PythonRuntimeDlls> + <Link>DLLs\%(Filename)%(Extension)</Link> + </_PythonRuntimeDlls> + <_PythonRuntimeLib Include="$(PythonHome)\Lib\**\*" Exclude="$(PythonHome)\Lib\**\*.pyc;$(PythonHome)\Lib\site-packages\**\*" /> + <_PythonRuntimeLib Remove="$(PythonHome)\Lib\distutils\**\*" Condition="$(IncludeDistutils) != 'true'" /> + <_PythonRuntimeLib Remove="$(PythonHome)\Lib\lib2to3\**\*" Condition="$(IncludeLib2To3) != 'true'" /> + <_PythonRuntimeLib Remove="$(PythonHome)\Lib\ensurepip\**\*" Condition="$(IncludeVEnv) != 'true'" /> + <_PythonRuntimeLib Remove="$(PythonHome)\Lib\venv\**\*" Condition="$(IncludeVEnv) != 'true'" /> + <_PythonRuntimeLib> + <Link>Lib\%(RecursiveDir)%(Filename)%(Extension)</Link> + </_PythonRuntimeLib> + <PythonRuntime Include="@(_PythonRuntimeExe);@(_PythonRuntimeDlls);@(_PythonRuntimeLib)" /> + </ItemGroup> + + <Message Importance="low" Text="Collected Python runtime from $(PythonHome):%0D%0A@(PythonRuntime->' %(Link)','%0D%0A')" /> + </Target> +</Project> +""" + + +@public +def get_props_layout(ns): + if ns.include_all or ns.include_props: + yield "python.props", ns.temp / "python.props" + + +@public +def get_props(ns): + # TODO: Filter contents of props file according to included/excluded items + props = PROPS_TEMPLATE.format_map(PROPS_DATA) + return props.encode("utf-8") diff --git a/PC/layout/support/python.props b/PC/layout/support/python.props new file mode 100644 index 0000000..4cc7008 --- /dev/null +++ b/PC/layout/support/python.props @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup Condition="$(Platform) == '$$PYTHON_PLATFORM$$'"> + <PythonHome>$(MSBuildThisFileDirectory)\..\..\tools</PythonHome> + <PythonInclude>$(PythonHome)\include</PythonInclude> + <PythonLibs>$(PythonHome)\libs</PythonLibs> + <PythonTag>$$PYTHON_TAG$$</PythonTag> + <PythonVersion>$$PYTHON_VERSION$$</PythonVersion> + + <IncludePythonExe Condition="$(IncludePythonExe) == ''">true</IncludePythonExe> + <IncludeDistutils Condition="$(IncludeDistutils) == ''">false</IncludeDistutils> + <IncludeLib2To3 Condition="$(IncludeLib2To3) == ''">false</IncludeLib2To3> + <IncludeVEnv Condition="$(IncludeVEnv) == ''">false</IncludeVEnv> + + <GetPythonRuntimeFilesDependsOn>$$PYTHON_TARGET$$;$(GetPythonRuntimeFilesDependsOn)</GetPythonRuntimeFilesDependsOn> + </PropertyGroup> + + <ItemDefinitionGroup Condition="$(Platform) == '$$PYTHON_PLATFORM$$'"> + <ClCompile> + <AdditionalIncludeDirectories>$(PythonInclude);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary> + </ClCompile> + <Link> + <AdditionalLibraryDirectories>$(PythonLibs);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> + </Link> + </ItemDefinitionGroup> + + <Target Name="GetPythonRuntimeFiles" Returns="@(PythonRuntime)" DependsOnTargets="$(GetPythonRuntimeFilesDependsOn)" /> + + <Target Name="$$PYTHON_TARGET$$" Returns="@(PythonRuntime)"> + <ItemGroup> + <_PythonRuntimeExe Include="$(PythonHome)\python*.dll" /> + <_PythonRuntimeExe Include="$(PythonHome)\vcruntime140.dll" /> + <_PythonRuntimeExe Include="$(PythonHome)\python*.exe" Condition="$(IncludePythonExe) == 'true'" /> + <_PythonRuntimeExe> + <Link>%(Filename)%(Extension)</Link> + </_PythonRuntimeExe> + <_PythonRuntimeDlls Include="$(PythonHome)\DLLs\*.pyd" /> + <_PythonRuntimeDlls Include="$(PythonHome)\DLLs\*.dll" /> + <_PythonRuntimeDlls> + <Link>DLLs\%(Filename)%(Extension)</Link> + </_PythonRuntimeDlls> + <_PythonRuntimeLib Include="$(PythonHome)\Lib\**\*" Exclude="$(PythonHome)\Lib\**\*.pyc;$(PythonHome)\Lib\site-packages\**\*" /> + <_PythonRuntimeLib Remove="$(PythonHome)\Lib\distutils\**\*" Condition="$(IncludeDistutils) != 'true'" /> + <_PythonRuntimeLib Remove="$(PythonHome)\Lib\lib2to3\**\*" Condition="$(IncludeLib2To3) != 'true'" /> + <_PythonRuntimeLib Remove="$(PythonHome)\Lib\ensurepip\**\*" Condition="$(IncludeVEnv) != 'true'" /> + <_PythonRuntimeLib Remove="$(PythonHome)\Lib\venv\**\*" Condition="$(IncludeVEnv) != 'true'" /> + <_PythonRuntimeLib> + <Link>Lib\%(RecursiveDir)%(Filename)%(Extension)</Link> + </_PythonRuntimeLib> + <PythonRuntime Include="@(_PythonRuntimeExe);@(_PythonRuntimeDlls);@(_PythonRuntimeLib)" /> + </ItemGroup> + + <Message Importance="low" Text="Collected Python runtime from $(PythonHome):%0D%0A@(PythonRuntime->' %(Link)','%0D%0A')" /> + </Target> +</Project> |