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