mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
152 lines
6.9 KiB
Python
152 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the Microsoft Store packages from the staged Windows release files.
|
|
|
|
`pack` turns one architecture's portable layout (target/package/windows) into
|
|
an unsigned .msix with the manifest and logos from platform/windows/msix.
|
|
`bundle` combines the architectures' packages into one .msixbundle, the file
|
|
uploaded to Partner Center. The Store signs both, so neither is signed here.
|
|
|
|
The tools come from the newest installed Windows SDK. The MAKEAPPX and
|
|
MAKEPRI environment variables can name them explicitly.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
|
|
PROJECT = Path(__file__).resolve().parent.parent
|
|
MSIX = PROJECT / "platform/windows/msix"
|
|
ARCHITECTURES = ("x64", "arm64")
|
|
MANIFEST = "{http://schemas.microsoft.com/appx/manifest/foundation/windows10}"
|
|
# The manifest ships with this publisher until the Partner Center identity
|
|
# replaces it; Partner Center rejects packages that still carry it.
|
|
PLACEHOLDER_PUBLISHER = "CN=00000000-0000-0000-0000-000000000000"
|
|
|
|
|
|
def sdk_tool(name):
|
|
"""The tool from the newest Windows SDK, preferring native Arm64 builds on Arm64."""
|
|
override = os.environ.get(Path(name).stem.upper())
|
|
if override:
|
|
return override
|
|
kits = Path(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")) / "Windows Kits/10/bin"
|
|
hosts = ("arm64", "x64", "x86") if platform.machine().upper() == "ARM64" else ("x64", "x86")
|
|
versions = [path for path in kits.glob("10.*") if re.fullmatch(r"[0-9.]+", path.name)]
|
|
for version in sorted(versions, key=lambda path: tuple(map(int, path.name.split("."))), reverse=True):
|
|
for host in hosts:
|
|
if (tool := version / host / name).is_file():
|
|
return str(tool)
|
|
raise FileNotFoundError(f"{name} was not found in a Windows SDK under {kits}")
|
|
|
|
|
|
def run(*command):
|
|
print("+", " ".join(map(str, command)), flush=True)
|
|
subprocess.run([str(part) for part in command], check=True)
|
|
|
|
|
|
def check_version(version):
|
|
if not re.fullmatch(r"[1-9][0-9]{0,4}\.[0-9]{1,5}\.[0-9]{1,5}\.0", version) or any(
|
|
int(part) > 65535 for part in version.split(".")):
|
|
raise ValueError(f"Store versions are Major.Minor.Build.0 with parts up to 65535, not {version}")
|
|
|
|
|
|
def manifest(version, arch):
|
|
check_version(version)
|
|
if arch not in ARCHITECTURES:
|
|
raise ValueError(f"Unsupported Store package architecture: {arch}")
|
|
text = (MSIX / "AppxManifest.xml.in").read_text(encoding="utf-8")
|
|
for placeholder, value in (("@VERSION@", version), ("@ARCHITECTURE@", arch)):
|
|
if text.count(placeholder) != 1:
|
|
raise ValueError(f"The manifest template must contain {placeholder} once")
|
|
text = text.replace(placeholder, value)
|
|
return text
|
|
|
|
|
|
def warn_placeholder_identity(text):
|
|
publisher = ET.fromstring(text).find(f"{MANIFEST}Identity").get("Publisher")
|
|
if publisher == PLACEHOLDER_PUBLISHER:
|
|
message = ("The Store package still has the placeholder identity. "
|
|
"See docs/microsoft-store.md before uploading it to Partner Center.")
|
|
prefix = "::warning title=Placeholder Store identity::" if os.environ.get("GITHUB_ACTIONS") else "warning: "
|
|
print(prefix + message, flush=True)
|
|
|
|
|
|
def pri_config(makepri, directory):
|
|
"""makepri's default configuration without automatic resource packages,
|
|
which only apply to bundles that split out their resources."""
|
|
config = directory / "priconfig.xml"
|
|
run(makepri, "createconfig", "/cf", config, "/dq", "en-US", "/pv", "10.0.0", "/o")
|
|
tree = ET.parse(config)
|
|
for packaging in tree.getroot().findall("packaging"):
|
|
tree.getroot().remove(packaging)
|
|
tree.write(config, encoding="utf-8", xml_declaration=True)
|
|
return config
|
|
|
|
|
|
def pack(arch, version, layout_source, output):
|
|
for name in ("flectar-mail.exe", "pdfium.dll"):
|
|
if not (layout_source / name).is_file():
|
|
raise FileNotFoundError(f"{layout_source / name} is missing; stage the Windows package first")
|
|
text = manifest(version, arch)
|
|
makeappx, makepri = sdk_tool("makeappx.exe"), sdk_tool("makepri.exe")
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
with tempfile.TemporaryDirectory(prefix="flectar-msix-") as directory:
|
|
directory = Path(directory)
|
|
layout = directory / "layout"
|
|
shutil.copytree(layout_source, layout)
|
|
(layout / "AppxManifest.xml").write_text(text, encoding="utf-8")
|
|
# Index only the logos: Windows resolves their scale-, targetsize- and
|
|
# altform- variants through resources.pri.
|
|
indexed = directory / "pri"
|
|
shutil.copytree(MSIX / "Assets", indexed / "Assets")
|
|
shutil.copytree(MSIX / "Assets", layout / "Assets")
|
|
run(makepri, "new", "/pr", indexed, "/cf", pri_config(makepri, directory),
|
|
"/mn", layout / "AppxManifest.xml", "/of", layout / "resources.pri", "/o")
|
|
run(makeappx, "pack", "/o", "/h", "SHA256", "/d", layout, "/p", output)
|
|
warn_placeholder_identity(text)
|
|
print(f"Built {output}")
|
|
|
|
|
|
def bundle(version, packages, output):
|
|
check_version(version)
|
|
if len({package.name for package in packages}) != len(packages):
|
|
raise ValueError("Bundled packages need distinct file names")
|
|
makeappx = sdk_tool("makeappx.exe")
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
with tempfile.TemporaryDirectory(prefix="flectar-msixbundle-") as directory:
|
|
for package in packages:
|
|
shutil.copyfile(package, Path(directory) / package.name)
|
|
run(makeappx, "bundle", "/o", "/bv", version, "/d", directory, "/p", output)
|
|
print(f"Built {output}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
single = commands.add_parser("pack", help="package one architecture")
|
|
single.add_argument("--arch", choices=ARCHITECTURES, required=True)
|
|
single.add_argument("--version", required=True, help="Store version, from release-metadata.py")
|
|
single.add_argument("layout", type=Path, help="staged Windows release files")
|
|
single.add_argument("output", type=Path, help=".msix to write")
|
|
combined = commands.add_parser("bundle", help="bundle the architectures' packages")
|
|
combined.add_argument("--version", required=True, help="Store version, from release-metadata.py")
|
|
combined.add_argument("output", type=Path, help=".msixbundle to write")
|
|
combined.add_argument("packages", type=Path, nargs="+", help=".msix packages")
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.command == "pack":
|
|
pack(args.arch, args.version, args.layout, args.output)
|
|
else:
|
|
bundle(args.version, args.packages, args.output)
|
|
except (OSError, ValueError, subprocess.CalledProcessError) as error:
|
|
raise SystemExit(f"error: {error}") from error
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|