1"""
2File generation for APPX/MSIX manifests.
3"""
4
5__author__ = "Steve Dower <steve.dower@python.org>"
6__version__ = "3.8"
7
8
9import collections
10import ctypes
11import io
12import os
13import sys
14
15from pathlib import Path, PureWindowsPath
16from xml.etree import ElementTree as ET
17
18from .constants import *
19
20__all__ = ["get_appx_layout"]
21
22
23APPX_DATA = dict(
24    Name="PythonSoftwareFoundation.Python.{}".format(VER_DOT),
25    Version="{}.{}.{}.0".format(VER_MAJOR, VER_MINOR, VER_FIELD3),
26    Publisher=os.getenv(
27        "APPX_DATA_PUBLISHER", "CN=4975D53F-AA7E-49A5-8B49-EA4FDC1BB66B"
28    ),
29    DisplayName="Python {}".format(VER_DOT),
30    Description="The Python {} runtime and console.".format(VER_DOT),
31)
32
33APPX_PLATFORM_DATA = dict(
34    _keys=("ProcessorArchitecture",),
35    win32=("x86",),
36    amd64=("x64",),
37    arm32=("arm",),
38    arm64=("arm64",),
39)
40
41PYTHON_VE_DATA = dict(
42    DisplayName="Python {}".format(VER_DOT),
43    Description="Python interactive console",
44    Square150x150Logo="_resources/pythonx150.png",
45    Square44x44Logo="_resources/pythonx44.png",
46    BackgroundColor="transparent",
47)
48
49PYTHONW_VE_DATA = dict(
50    DisplayName="Python {} (Windowed)".format(VER_DOT),
51    Description="Python windowed app launcher",
52    Square150x150Logo="_resources/pythonwx150.png",
53    Square44x44Logo="_resources/pythonwx44.png",
54    BackgroundColor="transparent",
55    AppListEntry="none",
56)
57
58PIP_VE_DATA = dict(
59    DisplayName="pip (Python {})".format(VER_DOT),
60    Description="pip package manager for Python {}".format(VER_DOT),
61    Square150x150Logo="_resources/pythonx150.png",
62    Square44x44Logo="_resources/pythonx44.png",
63    BackgroundColor="transparent",
64    AppListEntry="none",
65)
66
67IDLE_VE_DATA = dict(
68    DisplayName="IDLE (Python {})".format(VER_DOT),
69    Description="IDLE editor for Python {}".format(VER_DOT),
70    Square150x150Logo="_resources/idlex150.png",
71    Square44x44Logo="_resources/idlex44.png",
72    BackgroundColor="transparent",
73)
74
75PY_PNG = "_resources/py.png"
76
77APPXMANIFEST_NS = {
78    "": "http://schemas.microsoft.com/appx/manifest/foundation/windows10",
79    "m": "http://schemas.microsoft.com/appx/manifest/foundation/windows10",
80    "uap": "http://schemas.microsoft.com/appx/manifest/uap/windows10",
81    "rescap": "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities",
82    "rescap4": "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/4",
83    "desktop4": "http://schemas.microsoft.com/appx/manifest/desktop/windows10/4",
84    "desktop6": "http://schemas.microsoft.com/appx/manifest/desktop/windows10/6",
85    "uap3": "http://schemas.microsoft.com/appx/manifest/uap/windows10/3",
86    "uap4": "http://schemas.microsoft.com/appx/manifest/uap/windows10/4",
87    "uap5": "http://schemas.microsoft.com/appx/manifest/uap/windows10/5",
88}
89
90APPXMANIFEST_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
91<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
92    xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
93    xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
94    xmlns:rescap4="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/4"
95    xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
96    xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4"
97    xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5">
98    <Identity Name=""
99              Version=""
100              Publisher=""
101              ProcessorArchitecture="" />
102    <Properties>
103        <DisplayName></DisplayName>
104        <PublisherDisplayName>Python Software Foundation</PublisherDisplayName>
105        <Description></Description>
106        <Logo>_resources/pythonx50.png</Logo>
107    </Properties>
108    <Resources>
109        <Resource Language="en-US" />
110    </Resources>
111    <Dependencies>
112        <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="" />
113    </Dependencies>
114    <Capabilities>
115        <rescap:Capability Name="runFullTrust"/>
116    </Capabilities>
117    <Applications>
118    </Applications>
119    <Extensions>
120    </Extensions>
121</Package>"""
122
123
124RESOURCES_XML_TEMPLATE = r"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
125<!--This file is input for makepri.exe. It should be excluded from the final package.-->
126<resources targetOsVersion="10.0.0" majorVersion="1">
127    <packaging>
128        <autoResourcePackage qualifier="Language"/>
129        <autoResourcePackage qualifier="Scale"/>
130        <autoResourcePackage qualifier="DXFeatureLevel"/>
131    </packaging>
132    <index root="\" startIndexAt="\">
133        <default>
134            <qualifier name="Language" value="en-US"/>
135            <qualifier name="Contrast" value="standard"/>
136            <qualifier name="Scale" value="100"/>
137            <qualifier name="HomeRegion" value="001"/>
138            <qualifier name="TargetSize" value="256"/>
139            <qualifier name="LayoutDirection" value="LTR"/>
140            <qualifier name="Theme" value="dark"/>
141            <qualifier name="AlternateForm" value=""/>
142            <qualifier name="DXFeatureLevel" value="DX9"/>
143            <qualifier name="Configuration" value=""/>
144            <qualifier name="DeviceFamily" value="Universal"/>
145            <qualifier name="Custom" value=""/>
146        </default>
147        <indexer-config type="folder" foldernameAsQualifier="true" filenameAsQualifier="true" qualifierDelimiter="$"/>
148        <indexer-config type="resw" convertDotsToSlashes="true" initialPath=""/>
149        <indexer-config type="resjson" initialPath=""/>
150        <indexer-config type="PRI"/>
151    </index>
152</resources>"""
153
154
155SCCD_FILENAME = "PC/classicAppCompat.sccd"
156
157SPECIAL_LOOKUP = object()
158
159REGISTRY = {
160    "HKCU\\Software\\Python\\PythonCore": {
161        VER_DOT: {
162            "DisplayName": APPX_DATA["DisplayName"],
163            "SupportUrl": "https://www.python.org/",
164            "SysArchitecture": SPECIAL_LOOKUP,
165            "SysVersion": VER_DOT,
166            "Version": "{}.{}.{}".format(VER_MAJOR, VER_MINOR, VER_MICRO),
167            "InstallPath": {
168                "": "[{AppVPackageRoot}]",
169                "ExecutablePath": "[{{AppVPackageRoot}}]\\python{}.exe".format(VER_DOT),
170                "WindowedExecutablePath": "[{{AppVPackageRoot}}]\\pythonw{}.exe".format(
171                    VER_DOT
172                ),
173            },
174            "Help": {
175                "Main Python Documentation": {
176                    "_condition": lambda ns: ns.include_chm,
177                    "": "[{{AppVPackageRoot}}]\\Doc\\{}".format(PYTHON_CHM_NAME),
178                },
179                "Local Python Documentation": {
180                    "_condition": lambda ns: ns.include_html_doc,
181                    "": "[{AppVPackageRoot}]\\Doc\\html\\index.html",
182                },
183                "Online Python Documentation": {
184                    "": "https://docs.python.org/{}".format(VER_DOT)
185                },
186            },
187            "Idle": {
188                "_condition": lambda ns: ns.include_idle,
189                "": "[{AppVPackageRoot}]\\Lib\\idlelib\\idle.pyw",
190            },
191        }
192    }
193}
194
195
196def get_packagefamilyname(name, publisher_id):
197    class PACKAGE_ID(ctypes.Structure):
198        _fields_ = [
199            ("reserved", ctypes.c_uint32),
200            ("processorArchitecture", ctypes.c_uint32),
201            ("version", ctypes.c_uint64),
202            ("name", ctypes.c_wchar_p),
203            ("publisher", ctypes.c_wchar_p),
204            ("resourceId", ctypes.c_wchar_p),
205            ("publisherId", ctypes.c_wchar_p),
206        ]
207        _pack_ = 4
208
209    pid = PACKAGE_ID(0, 0, 0, name, publisher_id, None, None)
210    result = ctypes.create_unicode_buffer(256)
211    result_len = ctypes.c_uint32(256)
212    r = ctypes.windll.kernel32.PackageFamilyNameFromId(
213        pid, ctypes.byref(result_len), result
214    )
215    if r:
216        raise OSError(r, "failed to get package family name")
217    return result.value[: result_len.value]
218
219
220def _fixup_sccd(ns, sccd, new_hash=None):
221    if not new_hash:
222        return sccd
223
224    NS = dict(s="http://schemas.microsoft.com/appx/2016/sccd")
225    with open(sccd, "rb") as f:
226        xml = ET.parse(f)
227
228    pfn = get_packagefamilyname(APPX_DATA["Name"], APPX_DATA["Publisher"])
229
230    ae = xml.find("s:AuthorizedEntities", NS)
231    ae.clear()
232
233    e = ET.SubElement(ae, ET.QName(NS["s"], "AuthorizedEntity"))
234    e.set("AppPackageFamilyName", pfn)
235    e.set("CertificateSignatureHash", new_hash)
236
237    for e in xml.findall("s:Catalog", NS):
238        e.text = "FFFF"
239
240    sccd = ns.temp / sccd.name
241    sccd.parent.mkdir(parents=True, exist_ok=True)
242    with open(sccd, "wb") as f:
243        xml.write(f, encoding="utf-8")
244
245    return sccd
246
247
248def find_or_add(xml, element, attr=None, always_add=False):
249    if always_add:
250        e = None
251    else:
252        q = element
253        if attr:
254            q += "[@{}='{}']".format(*attr)
255        e = xml.find(q, APPXMANIFEST_NS)
256    if e is None:
257        prefix, _, name = element.partition(":")
258        name = ET.QName(APPXMANIFEST_NS[prefix or ""], name)
259        e = ET.SubElement(xml, name)
260        if attr:
261            e.set(*attr)
262    return e
263
264
265def _get_app(xml, appid):
266    if appid:
267        app = xml.find(
268            "m:Applications/m:Application[@Id='{}']".format(appid), APPXMANIFEST_NS
269        )
270        if app is None:
271            raise LookupError(appid)
272    else:
273        app = xml
274    return app
275
276
277def add_visual(xml, appid, data):
278    app = _get_app(xml, appid)
279    e = find_or_add(app, "uap:VisualElements")
280    for i in data.items():
281        e.set(*i)
282    return e
283
284
285def add_alias(xml, appid, alias, subsystem="windows"):
286    app = _get_app(xml, appid)
287    e = find_or_add(app, "m:Extensions")
288    e = find_or_add(e, "uap5:Extension", ("Category", "windows.appExecutionAlias"))
289    e = find_or_add(e, "uap5:AppExecutionAlias")
290    e.set(ET.QName(APPXMANIFEST_NS["desktop4"], "Subsystem"), subsystem)
291    e = find_or_add(e, "uap5:ExecutionAlias", ("Alias", alias))
292
293
294def add_file_type(xml, appid, name, suffix, parameters='"%1"', info=None, logo=None):
295    app = _get_app(xml, appid)
296    e = find_or_add(app, "m:Extensions")
297    e = find_or_add(e, "uap3:Extension", ("Category", "windows.fileTypeAssociation"))
298    e = find_or_add(e, "uap3:FileTypeAssociation", ("Name", name))
299    e.set("Parameters", parameters)
300    if info:
301        find_or_add(e, "uap:DisplayName").text = info
302    if logo:
303        find_or_add(e, "uap:Logo").text = logo
304    e = find_or_add(e, "uap:SupportedFileTypes")
305    if isinstance(suffix, str):
306        suffix = [suffix]
307    for s in suffix:
308        ET.SubElement(e, ET.QName(APPXMANIFEST_NS["uap"], "FileType")).text = s
309
310
311def add_application(
312    ns, xml, appid, executable, aliases, visual_element, subsystem, file_types
313):
314    node = xml.find("m:Applications", APPXMANIFEST_NS)
315    suffix = "_d.exe" if ns.debug else ".exe"
316    app = ET.SubElement(
317        node,
318        ET.QName(APPXMANIFEST_NS[""], "Application"),
319        {
320            "Id": appid,
321            "Executable": executable + suffix,
322            "EntryPoint": "Windows.FullTrustApplication",
323            ET.QName(APPXMANIFEST_NS["desktop4"], "SupportsMultipleInstances"): "true",
324        },
325    )
326    if visual_element:
327        add_visual(app, None, visual_element)
328    for alias in aliases:
329        add_alias(app, None, alias + suffix, subsystem)
330    if file_types:
331        add_file_type(app, None, *file_types)
332    return app
333
334
335def _get_registry_entries(ns, root="", d=None):
336    r = root if root else PureWindowsPath("")
337    if d is None:
338        d = REGISTRY
339    for key, value in d.items():
340        if key == "_condition":
341            continue
342        if value is SPECIAL_LOOKUP:
343            if key == "SysArchitecture":
344                value = {
345                    "win32": "32bit",
346                    "amd64": "64bit",
347                    "arm32": "32bit",
348                    "arm64": "64bit",
349                }[ns.arch]
350            else:
351                raise ValueError(f"Key '{key}' unhandled for special lookup")
352        if isinstance(value, dict):
353            cond = value.get("_condition")
354            if cond and not cond(ns):
355                continue
356            fullkey = r
357            for part in PureWindowsPath(key).parts:
358                fullkey /= part
359                if len(fullkey.parts) > 1:
360                    yield str(fullkey), None, None
361            yield from _get_registry_entries(ns, fullkey, value)
362        elif len(r.parts) > 1:
363            yield str(r), key, value
364
365
366def add_registry_entries(ns, xml):
367    e = find_or_add(xml, "m:Extensions")
368    e = find_or_add(e, "rescap4:Extension")
369    e.set("Category", "windows.classicAppCompatKeys")
370    e.set("EntryPoint", "Windows.FullTrustApplication")
371    e = ET.SubElement(e, ET.QName(APPXMANIFEST_NS["rescap4"], "ClassicAppCompatKeys"))
372    for name, valuename, value in _get_registry_entries(ns):
373        k = ET.SubElement(
374            e, ET.QName(APPXMANIFEST_NS["rescap4"], "ClassicAppCompatKey")
375        )
376        k.set("Name", name)
377        if value:
378            k.set("ValueName", valuename)
379            k.set("Value", value)
380            k.set("ValueType", "REG_SZ")
381
382
383def disable_registry_virtualization(xml):
384    e = find_or_add(xml, "m:Properties")
385    e = find_or_add(e, "desktop6:RegistryWriteVirtualization")
386    e.text = "disabled"
387    e = find_or_add(xml, "m:Capabilities")
388    e = find_or_add(e, "rescap:Capability", ("Name", "unvirtualizedResources"))
389
390
391def get_appxmanifest(ns):
392    for k, v in APPXMANIFEST_NS.items():
393        ET.register_namespace(k, v)
394    ET.register_namespace("", APPXMANIFEST_NS["m"])
395
396    xml = ET.parse(io.StringIO(APPXMANIFEST_TEMPLATE))
397    NS = APPXMANIFEST_NS
398    QN = ET.QName
399
400    data = dict(APPX_DATA)
401    for k, v in zip(APPX_PLATFORM_DATA["_keys"], APPX_PLATFORM_DATA[ns.arch]):
402        data[k] = v
403
404    node = xml.find("m:Identity", NS)
405    for k in node.keys():
406        value = data.get(k)
407        if value:
408            node.set(k, value)
409
410    for node in xml.find("m:Properties", NS):
411        value = data.get(node.tag.rpartition("}")[2])
412        if value:
413            node.text = value
414
415    winver = sys.getwindowsversion()[:3]
416    if winver < (10, 0, 17763):
417        winver = 10, 0, 17763
418    find_or_add(xml, "m:Dependencies/m:TargetDeviceFamily").set(
419        "MaxVersionTested", "{}.{}.{}.0".format(*winver)
420    )
421
422    if winver > (10, 0, 17763):
423        disable_registry_virtualization(xml)
424
425    app = add_application(
426        ns,
427        xml,
428        "Python",
429        "python{}".format(VER_DOT),
430        ["python", "python{}".format(VER_MAJOR), "python{}".format(VER_DOT)],
431        PYTHON_VE_DATA,
432        "console",
433        ("python.file", [".py"], '"%1"', "Python File", PY_PNG),
434    )
435
436    add_application(
437        ns,
438        xml,
439        "PythonW",
440        "pythonw{}".format(VER_DOT),
441        ["pythonw", "pythonw{}".format(VER_MAJOR), "pythonw{}".format(VER_DOT)],
442        PYTHONW_VE_DATA,
443        "windows",
444        ("python.windowedfile", [".pyw"], '"%1"', "Python File (no console)", PY_PNG),
445    )
446
447    if ns.include_pip and ns.include_launchers:
448        add_application(
449            ns,
450            xml,
451            "Pip",
452            "pip{}".format(VER_DOT),
453            ["pip", "pip{}".format(VER_MAJOR), "pip{}".format(VER_DOT)],
454            PIP_VE_DATA,
455            "console",
456            ("python.wheel", [".whl"], 'install "%1"', "Python Wheel"),
457        )
458
459    if ns.include_idle and ns.include_launchers:
460        add_application(
461            ns,
462            xml,
463            "Idle",
464            "idle{}".format(VER_DOT),
465            ["idle", "idle{}".format(VER_MAJOR), "idle{}".format(VER_DOT)],
466            IDLE_VE_DATA,
467            "windows",
468            None,
469        )
470
471    if (ns.source / SCCD_FILENAME).is_file():
472        add_registry_entries(ns, xml)
473        node = xml.find("m:Capabilities", NS)
474        node = ET.SubElement(node, QN(NS["uap4"], "CustomCapability"))
475        node.set("Name", "Microsoft.classicAppCompat_8wekyb3d8bbwe")
476
477    buffer = io.BytesIO()
478    xml.write(buffer, encoding="utf-8", xml_declaration=True)
479    return buffer.getbuffer()
480
481
482def get_resources_xml(ns):
483    return RESOURCES_XML_TEMPLATE.encode("utf-8")
484
485
486def get_appx_layout(ns):
487    if not ns.include_appxmanifest:
488        return
489
490    yield "AppxManifest.xml", ("AppxManifest.xml", get_appxmanifest(ns))
491    yield "_resources.xml", ("_resources.xml", get_resources_xml(ns))
492    icons = ns.source / "PC" / "icons"
493    for px in [44, 50, 150]:
494        src = icons / "pythonx{}.png".format(px)
495        yield f"_resources/pythonx{px}.png", src
496        yield f"_resources/pythonx{px}$targetsize-{px}_altform-unplated.png", src
497    for px in [44, 150]:
498        src = icons / "pythonwx{}.png".format(px)
499        yield f"_resources/pythonwx{px}.png", src
500        yield f"_resources/pythonwx{px}$targetsize-{px}_altform-unplated.png", src
501    if ns.include_idle and ns.include_launchers:
502        for px in [44, 150]:
503            src = icons / "idlex{}.png".format(px)
504            yield f"_resources/idlex{px}.png", src
505            yield f"_resources/idlex{px}$targetsize-{px}_altform-unplated.png", src
506    yield f"_resources/py.png", icons / "py.png"
507    sccd = ns.source / SCCD_FILENAME
508    if sccd.is_file():
509        # This should only be set for side-loading purposes.
510        sccd = _fixup_sccd(ns, sccd, os.getenv("APPX_DATA_SHA256"))
511        yield sccd.name, sccd
512