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