#!/usr/bin/env python3 """Reject stale Android payloads and inconsistent common release metadata.""" import argparse, contextlib, hashlib, io, json, re, zipfile from project import ROOT, release_info p = argparse.ArgumentParser() group = p.add_mutually_exclusive_group() group.add_argument("--apk", type=str) group.add_argument("--assets", action="store_true", help="Check staged Android resources, not a compiled APK") args = p.parse_args() r = release_info() apk = ROOT / (args.apk or f'android/dist/vulpes-os-preview-{r["version"]}.apk') pattern = re.compile( r"app://([a-zA-Z0-9_-]+)\.gaiamobile\.org|http://([a-zA-Z0-9_-]+)\.localhost:8765" ) def translate(s): return pattern.sub(lambda m: f"http://{m[1] or m[2]}.localhost:18765", s) class StagedAssets: def read(self, name): if not name.startswith('assets/') or '..' in name.split('/'): raise ValueError('Invalid packaged path') return (ROOT / 'android/app/src/main' / name).read_bytes() results = {} source = contextlib.nullcontext(StagedAssets()) if args.assets else zipfile.ZipFile(apk) with source as archive: metadata = json.loads(archive.read("assets/build-info.json")) packaged = json.loads(archive.read("assets/bridge/release.json")) assert metadata["version"] == r["version"], metadata assert metadata["geckoview"] == r["geckoview"], metadata assert packaged == r, (packaged, r) with zipfile.ZipFile(io.BytesIO(archive.read("assets/gaia.zip"))) as gaia: for name in [ "platform-ui.js", "gaia-camera.js", "gaia-compat.js", "settings-codec.js", "gaia-services.js", "gaia-styles.js", "gaia-theme.js", "gaia-select.js", "gaia-mobile-settings.js", "gaia-theme.css", "gaia-input.js", "gaia-restoration.js", "gaia-wifi.js", "gaia-bluetooth.js", "desktop.css", ]: expected = translate((ROOT / "host" / name).read_text()).encode() actual = gaia.read("_vulpes/" + name) assert actual == expected, "Stale Android shared UI: " + name results[name] = hashlib.sha256(actual).hexdigest() wifi = translate((ROOT / "overrides/system.gaiamobile.org/js/wifi.js").read_text()).encode() assert gaia.read("system/js/wifi.js") == wifi, "Stale Android Gaia Wi-Fi timer" results["system/js/wifi.js"] = hashlib.sha256(wifi).hexdigest() for app, path in [ ("settings", "js/panels/display/panel.js"), ("settings", "js/panels/wifi/panel.js"), ("system", "js/screen_manager.js"), ("system", "js/notification_screen.js"), ("system", "style/system/system.css"), ("communications", "dialer/style/call_log.css"), ] + [("settings", path.relative_to(ROOT / "overrides/settings.gaiamobile.org").as_posix()) for path in sorted((ROOT / "overrides/settings.gaiamobile.org").rglob("*.js"))]: expected = translate((ROOT / "overrides" / (app + ".gaiamobile.org") / path).read_text()) for old, new in [("-moz-calc(", "calc("), ("-moz-box-sizing:", "box-sizing:"), ("-moz-plaintext", "plaintext"), (":-moz-dir(", ":dir("), ("offset-inline-start:", "inset-inline-start:"), ("offset-inline-end:", "inset-inline-end:")]: expected = expected.replace(old, new) actual = gaia.read(app + "/" + path) assert actual == expected.encode(), "Stale Android Gaia panel: " + path results[app + "/" + path] = hashlib.sha256(actual).hexdigest() for app, path, source in [ ("calendar", "gaia_build_defer_index.js", "overrides"), ("calendar", "js/bundle.js", "overrides"), ("gallery", "js/metadata_scripts.js", "overrides"), ("gallery", "js/MetadataParser.js", "overrides"), ("video", "gaia_build_defer_index.js", "overrides"), ("camera", "js/main.js", "overrides"), ("camera", "js/lib/dcf.js", "overrides"), ("gallery", "js/ImageEditor.js", "assets/webapps"), ("communications", "contacts/js/views/form.js", "assets/webapps"), ("communications", "contacts/js/contacts.js", "assets/webapps"), ("communications", "contacts/js/activities.js", "assets/webapps"), ]: expected = translate((ROOT / source / (app + ".gaiamobile.org") / path).read_text()) for old, new in [("-moz-calc(", "calc("), ("-moz-box-sizing:", "box-sizing:"), ("-moz-plaintext", "plaintext"), (":-moz-dir(", ":dir("), ("offset-inline-start:", "inset-inline-start:"), ("offset-inline-end:", "inset-inline-end:")]: expected = expected.replace(old, new) expected = expected.encode() actual = gaia.read(app + "/" + path) assert actual == expected, "Stale application workflow: " + app + "/" + path results[app + "/" + path] = hashlib.sha256(actual).hexdigest() helper = "dialer/js/telephony_helper.js" expected = translate((ROOT / "gaia/apps/communications" / helper).read_text()).encode() assert gaia.read("communications/" + helper) == expected, "Stale call-log dial helper" results["communications/" + helper] = hashlib.sha256(expected).hexdigest() for name in ("marketplace.js", "marketplace.css", "icon.png"): expected = (ROOT / "assets/webapps/marketplace.gaiamobile.org" / name).read_bytes() if name.endswith(".js"): expected = translate(expected.decode()).encode() assert gaia.read("marketplace/" + name) == expected, "Stale Marketplace: " + name for path in (ROOT / "overrides/search.gaiamobile.org/home").rglob("*"): if path.is_file(): relative = path.relative_to(ROOT / "overrides/search.gaiamobile.org") assert gaia.read("search/" + relative.as_posix()) == path.read_bytes(), str(relative) assert b"vulpes-local-home" in gaia.read("search/newtab.html") assert archive.read("assets/bridge/Messages.mjs") == (ROOT / "host/Messages.sys.mjs").read_bytes() assert archive.read("assets/bridge/input-editor.js") == (ROOT / "services/input-editor.mjs").read_text().replace('export class InputEditor', 'class InputEditor').encode(), "Stale Android input editor" for name in ["platform.mjs", "settings.mjs", "datastores.mjs", "screen-power.mjs", "activities.mjs", "packages.mjs", "input.mjs"]: assert ( archive.read("assets/bridge/services/" + name) == (ROOT / "services" / name).read_bytes() ), name assert ( r["androidEngine"].split(".")[0] == r["desktopEngine"].split(".")[0] == r["engineAlignment"]["family"] ) report = { "version": r["version"], "gaia": r["gaia"], "desktopEngine": r["desktopEngine"], "androidEngine": r["androidEngine"], "exactEngineMatch": r["desktopEngine"] == r["androidEngine"], "sharedFiles": results, "apk": None if args.assets else str(apk), "stagedAssetsOnly": args.assets, "scope": "Payload and metadata parity; hardware behavior requires separate integration tests.", } report_path = ROOT / ("logs/parity-staged.json" if args.assets else "logs/parity-payload.json") report_path.parent.mkdir(parents=True, exist_ok=True) report_path.write_text(json.dumps(report, indent=2) + "\n") print(json.dumps(report, indent=2))