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