#!/usr/bin/env python3 """Checks a built MSI for the mistakes that cannot be caught without Windows (run by build-msi.sh and CI). verify-msi.py """ import os import subprocess import sys import tempfile DEFERRED = 0x400 IN_SCRIPT_NO_IMPERSONATE = 0x800 def tables(msi): with tempfile.TemporaryDirectory() as folder: subprocess.run(["msidump", "-t", "-d", folder, os.path.abspath(msi)], check=True, capture_output=True, cwd=folder) result = {} for name in (n for n in os.listdir(folder) if n.endswith(".idt")): with open(os.path.join(folder, name), "rb") as f: raw = f.read() lines = raw.decode("utf-8", errors="replace").splitlines() header = lines[0].split("\t") result[name[:-4]] = ([dict(zip(header, row.split("\t"))) for row in lines[3:] if row], raw) return result def main(msi): t = tables(msi) errors = [] def rows(name): return t.get(name, ([], b""))[0] for name, (_, raw) in t.items(): if any(b > 127 for b in raw): errors.append(f"{name}: indeholder ikke-ASCII-tegn (databasen er codepage-neutral)") sequence = {r["Action"]: int(r["Sequence"]) for r in rows("InstallExecuteSequence")} actions = {r["Action"]: int(r["Type"]) for r in rows("CustomAction")} start, end = sequence["InstallInitialize"], sequence["InstallFinalize"] for action, kind in actions.items(): if kind & 0x3F == 50 and not kind & DEFERRED: errors.append(f"{action}: kører et program uden at være deferred (ville køre som brugeren, uden rollback)") if action not in sequence: errors.append(f"{action}: er ikke i InstallExecuteSequence") elif kind & DEFERRED and not start < sequence[action] < end: errors.append(f"{action}: deferred, men ligger uden for InstallInitialize..InstallFinalize ({sequence[action]})") elif kind & DEFERRED and not kind & IN_SCRIPT_NO_IMPERSONATE: errors.append(f"{action}: deferred uden NoImpersonate (kører ikke som SYSTEM)") def after(action, standard): if sequence.get(action, 0) <= sequence[standard]: errors.append(f"{action} skal ligge efter {standard}") after("SetDataFolderAcl", "InstallServices") # the virtual service account must exist after("CreateUpdateTask", "InstallFiles") after("OpenFirewall", "ReplaceFirewallRule") after("RemoveExistingProducts", "InstallExecute") # upgrade in place: settings files are kept for action in ("DeleteUpdateTask", "CloseFirewall"): condition = next(r["Condition"] for r in rows("InstallExecuteSequence") if r["Action"] == action) if "UPGRADINGPRODUCTCODE" not in condition: errors.append(f"{action}: skal ikke køre, når en opgradering fjerner den gamle version") components = {r["Component"]: r for r in rows("Component")} for name, bits in (("AgentSettings", 0x90), ("WebSettings", 0x90), ("AgentIni", 0x10), ("WebIni", 0x10)): if int(components[name]["Attributes"]) & bits != bits: errors.append(f"{name}: mangler Permanent/NeverOverwrite") if not components["AgentServiceStart"]["Condition"]: errors.append("AgentServiceStart: mangler betingelse") if any(int(r["Attributes"]) & 0x100 == 0 for r in components.values()): errors.append("Komponenter uden 64-bit-attribut") for service in rows("ServiceInstall"): if int(service["ErrorControl"]) & 0x8000 == 0: errors.append(f"{service['Name']}: tjenesten er ikke Vital") if not any(r["Feature_"] == "UpdateTask" for r in rows("Condition")): errors.append("UpdateTask: mangler betingelsen, der bevarer opgaven ved opgradering") if not any(r["Value"].startswith("#%") for r in rows("Registry") if r["Name"] == "EventMessageFile"): errors.append("EventMessageFile er ikke REG_EXPAND_SZ") if not any(r["Name"] == "Installer" and r["Value"] == "msi" for r in rows("Registry")): errors.append("Registreringen Installer=msi mangler (opdateringer ville installere zip-pakker oven i MSI'en)") if not any(int(r["Attributes"]) & 1 for r in rows("Upgrade") if r["ActionProperty"] == "OLDVERSIONFOUND"): errors.append("Upgrade: MigrateFeatures mangler") for error in errors: print("FEJL:", error) print(f"{os.path.basename(msi)}: {len(components)} komponenter, {len(rows('File'))} filer, {len(errors)} fejl") return 1 if errors else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1]))