mirror of
https://github.com/python/cpython.git
synced 2025-09-23 17:03:23 +00:00

Note that the support is not actually enabled yet, and so we won't be publishing these packages. However, for those who want to build it themselves (even by reusing the Azure Pipelines definition), it's now relatively easy to enable.
506 lines
17 KiB
Python
506 lines
17 KiB
Python
"""
|
|
File generation for APPX/MSIX manifests.
|
|
"""
|
|
|
|
__author__ = "Steve Dower <steve.dower@python.org>"
|
|
__version__ = "3.8"
|
|
|
|
|
|
import collections
|
|
import ctypes
|
|
import io
|
|
import os
|
|
import sys
|
|
|
|
from pathlib import Path, PureWindowsPath
|
|
from xml.etree import ElementTree as ET
|
|
|
|
from .constants import *
|
|
|
|
__all__ = ["get_appx_layout"]
|
|
|
|
|
|
APPX_DATA = dict(
|
|
Name="PythonSoftwareFoundation.Python.{}".format(VER_DOT),
|
|
Version="{}.{}.{}.0".format(VER_MAJOR, VER_MINOR, VER_FIELD3),
|
|
Publisher=os.getenv(
|
|
"APPX_DATA_PUBLISHER", "CN=4975D53F-AA7E-49A5-8B49-EA4FDC1BB66B"
|
|
),
|
|
DisplayName="Python {}".format(VER_DOT),
|
|
Description="The Python {} runtime and console.".format(VER_DOT),
|
|
)
|
|
|
|
APPX_PLATFORM_DATA = dict(
|
|
_keys=("ProcessorArchitecture",),
|
|
win32=("x86",),
|
|
amd64=("x64",),
|
|
arm32=("arm",),
|
|
arm64=("arm64",),
|
|
)
|
|
|
|
PYTHON_VE_DATA = dict(
|
|
DisplayName="Python {}".format(VER_DOT),
|
|
Description="Python interactive console",
|
|
Square150x150Logo="_resources/pythonx150.png",
|
|
Square44x44Logo="_resources/pythonx44.png",
|
|
BackgroundColor="transparent",
|
|
)
|
|
|
|
PYTHONW_VE_DATA = dict(
|
|
DisplayName="Python {} (Windowed)".format(VER_DOT),
|
|
Description="Python windowed app launcher",
|
|
Square150x150Logo="_resources/pythonwx150.png",
|
|
Square44x44Logo="_resources/pythonwx44.png",
|
|
BackgroundColor="transparent",
|
|
AppListEntry="none",
|
|
)
|
|
|
|
PIP_VE_DATA = dict(
|
|
DisplayName="pip (Python {})".format(VER_DOT),
|
|
Description="pip package manager for Python {}".format(VER_DOT),
|
|
Square150x150Logo="_resources/pythonx150.png",
|
|
Square44x44Logo="_resources/pythonx44.png",
|
|
BackgroundColor="transparent",
|
|
AppListEntry="none",
|
|
)
|
|
|
|
IDLE_VE_DATA = dict(
|
|
DisplayName="IDLE (Python {})".format(VER_DOT),
|
|
Description="IDLE editor for Python {}".format(VER_DOT),
|
|
Square150x150Logo="_resources/pythonwx150.png",
|
|
Square44x44Logo="_resources/pythonwx44.png",
|
|
BackgroundColor="transparent",
|
|
)
|
|
|
|
PY_PNG = "_resources/py.png"
|
|
|
|
APPXMANIFEST_NS = {
|
|
"": "http://schemas.microsoft.com/appx/manifest/foundation/windows10",
|
|
"m": "http://schemas.microsoft.com/appx/manifest/foundation/windows10",
|
|
"uap": "http://schemas.microsoft.com/appx/manifest/uap/windows10",
|
|
"rescap": "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities",
|
|
"rescap4": "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/4",
|
|
"desktop4": "http://schemas.microsoft.com/appx/manifest/desktop/windows10/4",
|
|
"desktop6": "http://schemas.microsoft.com/appx/manifest/desktop/windows10/6",
|
|
"uap3": "http://schemas.microsoft.com/appx/manifest/uap/windows10/3",
|
|
"uap4": "http://schemas.microsoft.com/appx/manifest/uap/windows10/4",
|
|
"uap5": "http://schemas.microsoft.com/appx/manifest/uap/windows10/5",
|
|
}
|
|
|
|
APPXMANIFEST_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
|
|
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
|
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
|
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
|
xmlns:rescap4="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/4"
|
|
xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
|
|
xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4"
|
|
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5">
|
|
<Identity Name=""
|
|
Version=""
|
|
Publisher=""
|
|
ProcessorArchitecture="" />
|
|
<Properties>
|
|
<DisplayName></DisplayName>
|
|
<PublisherDisplayName>Python Software Foundation</PublisherDisplayName>
|
|
<Description></Description>
|
|
<Logo>_resources/pythonx50.png</Logo>
|
|
</Properties>
|
|
<Resources>
|
|
<Resource Language="en-US" />
|
|
</Resources>
|
|
<Dependencies>
|
|
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="" />
|
|
</Dependencies>
|
|
<Capabilities>
|
|
<rescap:Capability Name="runFullTrust"/>
|
|
</Capabilities>
|
|
<Applications>
|
|
</Applications>
|
|
<Extensions>
|
|
</Extensions>
|
|
</Package>"""
|
|
|
|
|
|
RESOURCES_XML_TEMPLATE = r"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<!--This file is input for makepri.exe. It should be excluded from the final package.-->
|
|
<resources targetOsVersion="10.0.0" majorVersion="1">
|
|
<packaging>
|
|
<autoResourcePackage qualifier="Language"/>
|
|
<autoResourcePackage qualifier="Scale"/>
|
|
<autoResourcePackage qualifier="DXFeatureLevel"/>
|
|
</packaging>
|
|
<index root="\" startIndexAt="\">
|
|
<default>
|
|
<qualifier name="Language" value="en-US"/>
|
|
<qualifier name="Contrast" value="standard"/>
|
|
<qualifier name="Scale" value="100"/>
|
|
<qualifier name="HomeRegion" value="001"/>
|
|
<qualifier name="TargetSize" value="256"/>
|
|
<qualifier name="LayoutDirection" value="LTR"/>
|
|
<qualifier name="Theme" value="dark"/>
|
|
<qualifier name="AlternateForm" value=""/>
|
|
<qualifier name="DXFeatureLevel" value="DX9"/>
|
|
<qualifier name="Configuration" value=""/>
|
|
<qualifier name="DeviceFamily" value="Universal"/>
|
|
<qualifier name="Custom" value=""/>
|
|
</default>
|
|
<indexer-config type="folder" foldernameAsQualifier="true" filenameAsQualifier="true" qualifierDelimiter="$"/>
|
|
<indexer-config type="resw" convertDotsToSlashes="true" initialPath=""/>
|
|
<indexer-config type="resjson" initialPath=""/>
|
|
<indexer-config type="PRI"/>
|
|
</index>
|
|
</resources>"""
|
|
|
|
|
|
SCCD_FILENAME = "PC/classicAppCompat.sccd"
|
|
|
|
SPECIAL_LOOKUP = object()
|
|
|
|
REGISTRY = {
|
|
"HKCU\\Software\\Python\\PythonCore": {
|
|
VER_DOT: {
|
|
"DisplayName": APPX_DATA["DisplayName"],
|
|
"SupportUrl": "https://www.python.org/",
|
|
"SysArchitecture": SPECIAL_LOOKUP,
|
|
"SysVersion": VER_DOT,
|
|
"Version": "{}.{}.{}".format(VER_MAJOR, VER_MINOR, VER_MICRO),
|
|
"InstallPath": {
|
|
"": "[{AppVPackageRoot}]",
|
|
"ExecutablePath": "[{{AppVPackageRoot}}]\\python{}.exe".format(VER_DOT),
|
|
"WindowedExecutablePath": "[{{AppVPackageRoot}}]\\pythonw{}.exe".format(
|
|
VER_DOT
|
|
),
|
|
},
|
|
"Help": {
|
|
"Main Python Documentation": {
|
|
"_condition": lambda ns: ns.include_chm,
|
|
"": "[{{AppVPackageRoot}}]\\Doc\\{}".format(PYTHON_CHM_NAME),
|
|
},
|
|
"Local Python Documentation": {
|
|
"_condition": lambda ns: ns.include_html_doc,
|
|
"": "[{AppVPackageRoot}]\\Doc\\html\\index.html",
|
|
},
|
|
"Online Python Documentation": {
|
|
"": "https://docs.python.org/{}".format(VER_DOT)
|
|
},
|
|
},
|
|
"Idle": {
|
|
"_condition": lambda ns: ns.include_idle,
|
|
"": "[{AppVPackageRoot}]\\Lib\\idlelib\\idle.pyw",
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
def get_packagefamilyname(name, publisher_id):
|
|
class PACKAGE_ID(ctypes.Structure):
|
|
_fields_ = [
|
|
("reserved", ctypes.c_uint32),
|
|
("processorArchitecture", ctypes.c_uint32),
|
|
("version", ctypes.c_uint64),
|
|
("name", ctypes.c_wchar_p),
|
|
("publisher", ctypes.c_wchar_p),
|
|
("resourceId", ctypes.c_wchar_p),
|
|
("publisherId", ctypes.c_wchar_p),
|
|
]
|
|
_pack_ = 4
|
|
|
|
pid = PACKAGE_ID(0, 0, 0, name, publisher_id, None, None)
|
|
result = ctypes.create_unicode_buffer(256)
|
|
result_len = ctypes.c_uint32(256)
|
|
r = ctypes.windll.kernel32.PackageFamilyNameFromId(
|
|
pid, ctypes.byref(result_len), result
|
|
)
|
|
if r:
|
|
raise OSError(r, "failed to get package family name")
|
|
return result.value[: result_len.value]
|
|
|
|
|
|
def _fixup_sccd(ns, sccd, new_hash=None):
|
|
if not new_hash:
|
|
return sccd
|
|
|
|
NS = dict(s="http://schemas.microsoft.com/appx/2016/sccd")
|
|
with open(sccd, "rb") as f:
|
|
xml = ET.parse(f)
|
|
|
|
pfn = get_packagefamilyname(APPX_DATA["Name"], APPX_DATA["Publisher"])
|
|
|
|
ae = xml.find("s:AuthorizedEntities", NS)
|
|
ae.clear()
|
|
|
|
e = ET.SubElement(ae, ET.QName(NS["s"], "AuthorizedEntity"))
|
|
e.set("AppPackageFamilyName", pfn)
|
|
e.set("CertificateSignatureHash", new_hash)
|
|
|
|
for e in xml.findall("s:Catalog", NS):
|
|
e.text = "FFFF"
|
|
|
|
sccd = ns.temp / sccd.name
|
|
sccd.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(sccd, "wb") as f:
|
|
xml.write(f, encoding="utf-8")
|
|
|
|
return sccd
|
|
|
|
|
|
def find_or_add(xml, element, attr=None, always_add=False):
|
|
if always_add:
|
|
e = None
|
|
else:
|
|
q = element
|
|
if attr:
|
|
q += "[@{}='{}']".format(*attr)
|
|
e = xml.find(q, APPXMANIFEST_NS)
|
|
if e is None:
|
|
prefix, _, name = element.partition(":")
|
|
name = ET.QName(APPXMANIFEST_NS[prefix or ""], name)
|
|
e = ET.SubElement(xml, name)
|
|
if attr:
|
|
e.set(*attr)
|
|
return e
|
|
|
|
|
|
def _get_app(xml, appid):
|
|
if appid:
|
|
app = xml.find(
|
|
"m:Applications/m:Application[@Id='{}']".format(appid), APPXMANIFEST_NS
|
|
)
|
|
if app is None:
|
|
raise LookupError(appid)
|
|
else:
|
|
app = xml
|
|
return app
|
|
|
|
|
|
def add_visual(xml, appid, data):
|
|
app = _get_app(xml, appid)
|
|
e = find_or_add(app, "uap:VisualElements")
|
|
for i in data.items():
|
|
e.set(*i)
|
|
return e
|
|
|
|
|
|
def add_alias(xml, appid, alias, subsystem="windows"):
|
|
app = _get_app(xml, appid)
|
|
e = find_or_add(app, "m:Extensions")
|
|
e = find_or_add(e, "uap5:Extension", ("Category", "windows.appExecutionAlias"))
|
|
e = find_or_add(e, "uap5:AppExecutionAlias")
|
|
e.set(ET.QName(APPXMANIFEST_NS["desktop4"], "Subsystem"), subsystem)
|
|
e = find_or_add(e, "uap5:ExecutionAlias", ("Alias", alias))
|
|
|
|
|
|
def add_file_type(xml, appid, name, suffix, parameters='"%1"', info=None, logo=None):
|
|
app = _get_app(xml, appid)
|
|
e = find_or_add(app, "m:Extensions")
|
|
e = find_or_add(e, "uap3:Extension", ("Category", "windows.fileTypeAssociation"))
|
|
e = find_or_add(e, "uap3:FileTypeAssociation", ("Name", name))
|
|
e.set("Parameters", parameters)
|
|
if info:
|
|
find_or_add(e, "uap:DisplayName").text = info
|
|
if logo:
|
|
find_or_add(e, "uap:Logo").text = logo
|
|
e = find_or_add(e, "uap:SupportedFileTypes")
|
|
if isinstance(suffix, str):
|
|
suffix = [suffix]
|
|
for s in suffix:
|
|
ET.SubElement(e, ET.QName(APPXMANIFEST_NS["uap"], "FileType")).text = s
|
|
|
|
|
|
def add_application(
|
|
ns, xml, appid, executable, aliases, visual_element, subsystem, file_types
|
|
):
|
|
node = xml.find("m:Applications", APPXMANIFEST_NS)
|
|
suffix = "_d.exe" if ns.debug else ".exe"
|
|
app = ET.SubElement(
|
|
node,
|
|
ET.QName(APPXMANIFEST_NS[""], "Application"),
|
|
{
|
|
"Id": appid,
|
|
"Executable": executable + suffix,
|
|
"EntryPoint": "Windows.FullTrustApplication",
|
|
ET.QName(APPXMANIFEST_NS["desktop4"], "SupportsMultipleInstances"): "true",
|
|
},
|
|
)
|
|
if visual_element:
|
|
add_visual(app, None, visual_element)
|
|
for alias in aliases:
|
|
add_alias(app, None, alias + suffix, subsystem)
|
|
if file_types:
|
|
add_file_type(app, None, *file_types)
|
|
return app
|
|
|
|
|
|
def _get_registry_entries(ns, root="", d=None):
|
|
r = root if root else PureWindowsPath("")
|
|
if d is None:
|
|
d = REGISTRY
|
|
for key, value in d.items():
|
|
if key == "_condition":
|
|
continue
|
|
elif isinstance(value, dict):
|
|
cond = value.get("_condition")
|
|
if cond and not cond(ns):
|
|
continue
|
|
fullkey = r
|
|
for part in PureWindowsPath(key).parts:
|
|
fullkey /= part
|
|
if len(fullkey.parts) > 1:
|
|
yield str(fullkey), None, None
|
|
yield from _get_registry_entries(ns, fullkey, value)
|
|
elif value is SPECIAL_LOOKUP:
|
|
if key == "SysArchitecture":
|
|
return {
|
|
"win32": "32bit",
|
|
"amd64": "64bit",
|
|
"arm32": "32bit",
|
|
"arm64": "64bit",
|
|
}[ns.arch]
|
|
else:
|
|
raise ValueError(f"Key '{key}' unhandled for special lookup")
|
|
elif len(r.parts) > 1:
|
|
yield str(r), key, value
|
|
|
|
|
|
def add_registry_entries(ns, xml):
|
|
e = find_or_add(xml, "m:Extensions")
|
|
e = find_or_add(e, "rescap4:Extension")
|
|
e.set("Category", "windows.classicAppCompatKeys")
|
|
e.set("EntryPoint", "Windows.FullTrustApplication")
|
|
e = ET.SubElement(e, ET.QName(APPXMANIFEST_NS["rescap4"], "ClassicAppCompatKeys"))
|
|
for name, valuename, value in _get_registry_entries(ns):
|
|
k = ET.SubElement(
|
|
e, ET.QName(APPXMANIFEST_NS["rescap4"], "ClassicAppCompatKey")
|
|
)
|
|
k.set("Name", name)
|
|
if value:
|
|
k.set("ValueName", valuename)
|
|
k.set("Value", value)
|
|
k.set("ValueType", "REG_SZ")
|
|
|
|
|
|
def disable_registry_virtualization(xml):
|
|
e = find_or_add(xml, "m:Properties")
|
|
e = find_or_add(e, "desktop6:RegistryWriteVirtualization")
|
|
e.text = "disabled"
|
|
e = find_or_add(xml, "m:Capabilities")
|
|
e = find_or_add(e, "rescap:Capability", ("Name", "unvirtualizedResources"))
|
|
|
|
|
|
def get_appxmanifest(ns):
|
|
for k, v in APPXMANIFEST_NS.items():
|
|
ET.register_namespace(k, v)
|
|
ET.register_namespace("", APPXMANIFEST_NS["m"])
|
|
|
|
xml = ET.parse(io.StringIO(APPXMANIFEST_TEMPLATE))
|
|
NS = APPXMANIFEST_NS
|
|
QN = ET.QName
|
|
|
|
data = dict(APPX_DATA)
|
|
for k, v in zip(APPX_PLATFORM_DATA["_keys"], APPX_PLATFORM_DATA[ns.arch]):
|
|
data[k] = v
|
|
|
|
node = xml.find("m:Identity", NS)
|
|
for k in node.keys():
|
|
value = data.get(k)
|
|
if value:
|
|
node.set(k, value)
|
|
|
|
for node in xml.find("m:Properties", NS):
|
|
value = data.get(node.tag.rpartition("}")[2])
|
|
if value:
|
|
node.text = value
|
|
|
|
winver = sys.getwindowsversion()[:3]
|
|
if winver < (10, 0, 17763):
|
|
winver = 10, 0, 17763
|
|
find_or_add(xml, "m:Dependencies/m:TargetDeviceFamily").set(
|
|
"MaxVersionTested", "{}.{}.{}.0".format(*winver)
|
|
)
|
|
|
|
if winver > (10, 0, 17763):
|
|
disable_registry_virtualization(xml)
|
|
|
|
app = add_application(
|
|
ns,
|
|
xml,
|
|
"Python",
|
|
"python{}".format(VER_DOT),
|
|
["python", "python{}".format(VER_MAJOR), "python{}".format(VER_DOT)],
|
|
PYTHON_VE_DATA,
|
|
"console",
|
|
("python.file", [".py"], '"%1"', "Python File", PY_PNG),
|
|
)
|
|
|
|
add_application(
|
|
ns,
|
|
xml,
|
|
"PythonW",
|
|
"pythonw{}".format(VER_DOT),
|
|
["pythonw", "pythonw{}".format(VER_MAJOR), "pythonw{}".format(VER_DOT)],
|
|
PYTHONW_VE_DATA,
|
|
"windows",
|
|
("python.windowedfile", [".pyw"], '"%1"', "Python File (no console)", PY_PNG),
|
|
)
|
|
|
|
if ns.include_pip and ns.include_launchers:
|
|
add_application(
|
|
ns,
|
|
xml,
|
|
"Pip",
|
|
"pip{}".format(VER_DOT),
|
|
["pip", "pip{}".format(VER_MAJOR), "pip{}".format(VER_DOT)],
|
|
PIP_VE_DATA,
|
|
"console",
|
|
("python.wheel", [".whl"], 'install "%1"', "Python Wheel"),
|
|
)
|
|
|
|
if ns.include_idle and ns.include_launchers:
|
|
add_application(
|
|
ns,
|
|
xml,
|
|
"Idle",
|
|
"idle{}".format(VER_DOT),
|
|
["idle", "idle{}".format(VER_MAJOR), "idle{}".format(VER_DOT)],
|
|
IDLE_VE_DATA,
|
|
"windows",
|
|
None,
|
|
)
|
|
|
|
if (ns.source / SCCD_FILENAME).is_file():
|
|
add_registry_entries(ns, xml)
|
|
node = xml.find("m:Capabilities", NS)
|
|
node = ET.SubElement(node, QN(NS["uap4"], "CustomCapability"))
|
|
node.set("Name", "Microsoft.classicAppCompat_8wekyb3d8bbwe")
|
|
|
|
buffer = io.BytesIO()
|
|
xml.write(buffer, encoding="utf-8", xml_declaration=True)
|
|
return buffer.getbuffer()
|
|
|
|
|
|
def get_resources_xml(ns):
|
|
return RESOURCES_XML_TEMPLATE.encode("utf-8")
|
|
|
|
|
|
def get_appx_layout(ns):
|
|
if not ns.include_appxmanifest:
|
|
return
|
|
|
|
yield "AppxManifest.xml", ("AppxManifest.xml", get_appxmanifest(ns))
|
|
yield "_resources.xml", ("_resources.xml", get_resources_xml(ns))
|
|
icons = ns.source / "PC" / "icons"
|
|
for px in [44, 50, 150]:
|
|
src = icons / "pythonx{}.png".format(px)
|
|
yield f"_resources/pythonx{px}.png", src
|
|
yield f"_resources/pythonx{px}$targetsize-{px}_altform-unplated.png", src
|
|
for px in [44, 150]:
|
|
src = icons / "pythonwx{}.png".format(px)
|
|
yield f"_resources/pythonwx{px}.png", src
|
|
yield f"_resources/pythonwx{px}$targetsize-{px}_altform-unplated.png", src
|
|
yield f"_resources/py.png", icons / "py.png"
|
|
sccd = ns.source / SCCD_FILENAME
|
|
if sccd.is_file():
|
|
# This should only be set for side-loading purposes.
|
|
sccd = _fixup_sccd(ns, sccd, os.getenv("APPX_DATA_SHA256"))
|
|
yield sccd.name, sccd
|