| """ |
| 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__ = ["get_appx_layout"] |
| |
| |
| 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), |
| ) |
| |
| APPX_PLATFORM_DATA = dict( |
| _keys=("ProcessorArchitecture",), |
| win32=("x86",), |
| amd64=("x64",), |
| arm32=("arm",), |
| arm64=("arm64",), |
| ) |
| |
| 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/idlex150.png", |
| Square44x44Logo="_resources/idlex44.png", |
| BackgroundColor="transparent", |
| ) |
| |
| PY_PNG = "_resources/py.png" |
| |
| 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" |
| |
| SPECIAL_LOOKUP = object() |
| |
| REGISTRY = { |
| "HKCU\\Software\\Python\\PythonCore": { |
| VER_DOT: { |
| "DisplayName": APPX_DATA["DisplayName"], |
| "SupportUrl": "https://www.python.org/", |
| "SysArchitecture": SPECIAL_LOOKUP, |
| "SysVersion": VER_DOT, |
| "Version": "{}.{}.{}".format(VER_MAJOR, VER_MINOR, VER_MICRO), |
| "InstallPath": { |
| "": "[{AppVPackageRoot}]", |
| "ExecutablePath": "[{{AppVPackageRoot}}]\\python{}.exe".format(VER_DOT), |
| "WindowedExecutablePath": "[{{AppVPackageRoot}}]\\pythonw{}.exe".format( |
| VER_DOT |
| ), |
| }, |
| "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 |
| |
| |
| 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"', info=None, logo=None): |
| 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) |
| if info: |
| find_or_add(e, "uap:DisplayName").text = info |
| if logo: |
| find_or_add(e, "uap:Logo").text = logo |
| 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 |
| if value is SPECIAL_LOOKUP: |
| if key == "SysArchitecture": |
| value = { |
| "win32": "32bit", |
| "amd64": "64bit", |
| "arm32": "32bit", |
| "arm64": "64bit", |
| }[ns.arch] |
| else: |
| raise ValueError(f"Key '{key}' unhandled for special lookup") |
| if 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")) |
| |
| |
| 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 |
| |
| data = dict(APPX_DATA) |
| for k, v in zip(APPX_PLATFORM_DATA["_keys"], APPX_PLATFORM_DATA[ns.arch]): |
| data[k] = v |
| |
| node = xml.find("m:Identity", NS) |
| for k in node.keys(): |
| value = data.get(k) |
| if value: |
| node.set(k, value) |
| |
| for node in xml.find("m:Properties", NS): |
| value = 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{}".format(VER_DOT), |
| ["python", "python{}".format(VER_MAJOR), "python{}".format(VER_DOT)], |
| PYTHON_VE_DATA, |
| "console", |
| ("python.file", [".py"], '"%1"', "Python File", PY_PNG), |
| ) |
| |
| add_application( |
| ns, |
| xml, |
| "PythonW", |
| "pythonw{}".format(VER_DOT), |
| ["pythonw", "pythonw{}".format(VER_MAJOR), "pythonw{}".format(VER_DOT)], |
| PYTHONW_VE_DATA, |
| "windows", |
| ("python.windowedfile", [".pyw"], '"%1"', "Python File (no console)", PY_PNG), |
| ) |
| |
| if ns.include_pip and ns.include_launchers: |
| add_application( |
| ns, |
| xml, |
| "Pip", |
| "pip{}".format(VER_DOT), |
| ["pip", "pip{}".format(VER_MAJOR), "pip{}".format(VER_DOT)], |
| PIP_VE_DATA, |
| "console", |
| ("python.wheel", [".whl"], 'install "%1"', "Python Wheel"), |
| ) |
| |
| if ns.include_idle and ns.include_launchers: |
| add_application( |
| ns, |
| xml, |
| "Idle", |
| "idle{}".format(VER_DOT), |
| ["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() |
| |
| |
| def get_resources_xml(ns): |
| return RESOURCES_XML_TEMPLATE.encode("utf-8") |
| |
| |
| def get_appx_layout(ns): |
| if not ns.include_appxmanifest: |
| return |
| |
| yield "AppxManifest.xml", ("AppxManifest.xml", get_appxmanifest(ns)) |
| yield "_resources.xml", ("_resources.xml", get_resources_xml(ns)) |
| icons = ns.source / "PC" / "icons" |
| for px in [44, 50, 150]: |
| src = icons / "pythonx{}.png".format(px) |
| yield f"_resources/pythonx{px}.png", src |
| yield f"_resources/pythonx{px}$targetsize-{px}_altform-unplated.png", src |
| for px in [44, 150]: |
| src = icons / "pythonwx{}.png".format(px) |
| yield f"_resources/pythonwx{px}.png", src |
| yield f"_resources/pythonwx{px}$targetsize-{px}_altform-unplated.png", src |
| if ns.include_idle and ns.include_launchers: |
| for px in [44, 150]: |
| src = icons / "idlex{}.png".format(px) |
| yield f"_resources/idlex{px}.png", src |
| yield f"_resources/idlex{px}$targetsize-{px}_altform-unplated.png", src |
| yield f"_resources/py.png", icons / "py.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 |