Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source. Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
136 lines
7.4 KiB
Python
136 lines
7.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-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))
|