VulpesOS/tools/check-parity.py
OverSu 0041cd9660 Update shared Gaia workflows and Sargo development tooling
Fix camera switching and Contacts/SMS activity routing across shared hosts.
Integrate Marketplace installation and native standalone recovery tools,
Bluetooth runtime packages and trusted certificates.

Update English/French READMEs and the shared changelog with tested scope
and remaining radio/audio/Bluetooth limitations. Preview17 remains the
existing APK; no new native image is released.

Validation: shared JavaScript and native Python tests; Android packaging
and payload parity from an isolated publication checkout.
2026-10-08 14:27:17 +02:00

126 lines
6.4 KiB
Python

#!/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-restoration.js",
"gaia-wifi.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"),
]:
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", "assets/webapps"),
("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()
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()
for name in ["platform.mjs", "settings.mjs", "datastores.mjs", "screen-power.mjs", "activities.mjs", "packages.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))