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