VulpesOS/android/tools/pack-assets.py
OverSu 1311bed5e9 Release Preview 20 shared desktop, Android and Sargo updates
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.
2026-10-13 00:19:45 +02:00

184 lines
9.1 KiB
Python

#!/usr/bin/env python3
"""Snapshot the same Gaia assets/overlays as the desktop, without changing them."""
import hashlib
import json
import re
import shutil
import subprocess
import zipfile
import sys
import base64
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools"))
from project import SOURCE_OVERRIDES, build_info, validate_assets, release_info, application_manifest
validate_assets()
OUT = ROOT / "android/app/src/main/assets"
OUT.mkdir(parents=True, exist_ok=True)
PORT = 18765
pattern = re.compile(
r"app://([a-zA-Z0-9_-]+)\.gaiamobile\.org|http://([a-zA-Z0-9_-]+)\.localhost:8765"
)
def translate(text):
return pattern.sub(lambda m: f"http://{m[1] or m[2]}.localhost:{PORT}", text)
APPS = ROOT / "assets/webapps"
registry = []
for manifest in sorted(APPS.glob("*/manifest.webapp")):
name = manifest.parent.name.removesuffix(".gaiamobile.org")
if not re.fullmatch("[a-z0-9_-]+", name):
continue
origin = f"http://{name}.localhost:{PORT}"
registry.append(
dict(
id=name,
origin=origin,
manifestURL=origin + "/manifest.webapp",
manifest=application_manifest(name, json.loads(translate(manifest.read_text())), "--include-dev-apps" in sys.argv),
)
)
info = build_info()
release = release_info()
revision = info["revision"]
commit_time = str(info["timestamp"])
textsuffix = {".html", ".js", ".css", ".json", ".webapp", ".properties", ".svg"}
head = b'<head><script src="/_vulpes/platform-config.js"></script><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"><link rel="stylesheet" href="/_vulpes/desktop.css"><link rel="stylesheet" href="/_vulpes/android.css"><script src="/_vulpes/settings-codec.js"></script><script src="/_vulpes/gaia-compat.js"></script><script src="/_vulpes/gaia-wifi.js"></script><script src="/_vulpes/gaia-mobile-settings.js"></script><script src="/_vulpes/gaia-bluetooth.js"></script><script src="/_vulpes/gaia-input.js"></script><script src="/_vulpes/gaia-services.js"></script><script src="/_vulpes/gaia-styles.js"></script><link rel="stylesheet" href="/_vulpes/gaia-theme.css"><script src="/_vulpes/gaia-theme.js"></script><script src="/_vulpes/gaia-select.js"></script><script src="/_vulpes/android-ui.js"></script><script src="/_vulpes/gaia-restoration.js"></script><script src="/_vulpes/platform-ui.js"></script><script src="/_vulpes/gaia-camera.js"></script>'
if "--bridge-only" not in sys.argv or not (OUT / "gaia.zip").exists():
with zipfile.ZipFile(OUT / "gaia.zip", "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z:
hashes = {}
def put(name, data):
z.writestr(name, data)
hashes[name] = hashlib.sha256(data).hexdigest()
for app in registry:
folder = APPS / (app["id"] + ".gaiamobile.org")
files = {p.relative_to(folder).as_posix(): p for p in folder.rglob("*") if p.is_file()}
common = ROOT / "overrides/shared"
if common.is_dir():
for source in common.rglob("*"):
if source.is_file():
files["shared/" + source.relative_to(common).as_posix()] = source
overlay = ROOT / "overrides" / folder.name
if overlay.exists():
files.update(
{
p.relative_to(overlay).as_posix(): p
for p in overlay.rglob("*")
if p.is_file() and p.name != "manifest.webapp"
}
)
for name in SOURCE_OVERRIDES.get(app["id"], []):
files[name] = ROOT / "gaia/apps" / app["id"] / name
for name, p in files.items():
data = p.read_bytes()
if p.suffix in textsuffix:
text = translate(data.decode("utf8"))
if p.suffix in {".html", ".css", ".js"}:
for a, b 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:"),
]:
text = text.replace(a, b)
# Clearing a media URL with null requests the literal /null path.
if app["id"] == "music" and p.suffix == ".js":
text = text.replace("audio.src=null;", "audio.removeAttribute('src');")
if name == "resources/gaia_commit.txt":
text = revision + "\n" + commit_time + "\n"
data = text.encode()
if p.suffix == ".html" and not (app["id"] == "search" and name.startswith("home/")):
data = data.replace(b"<head>", head, 1)
if app["id"] == "communications" and name.startswith("dialer/"):
data = data.replace(
b"<head>",
b'<head><script src="/shared/js/accessibility_helper.js"></script>',
1,
)
put(app["id"] + "/" + name, data)
for p in (ROOT / "gaia/shared").rglob("*"):
if p.is_file():
data = p.read_bytes()
if p.suffix in textsuffix:
data = translate(data.decode("utf8")).encode()
put("_shared/" + p.relative_to(ROOT / "gaia/shared").as_posix(), data)
put("_vulpes/platform-config.js", b'window.VulpesPlatform="android";')
for name in ["gaia-compat.js", "gaia-input.js", "gaia-services.js", "gaia-styles.js", "gaia-theme.js", "gaia-select.js", "gaia-mobile-settings.js", "gaia-theme.css", "gaia-restoration.js", "gaia-wifi.js", "gaia-bluetooth.js", "desktop.css", "platform-ui.js", "gaia-camera.js"]:
text = (
(ROOT / "host" / name)
.read_text()
)
put("_vulpes/" + name, translate(text).encode())
put("_vulpes/settings-codec.js", (ROOT / "host/settings-codec.js").read_bytes())
put("_vulpes/android-ui.js", (ROOT / "android/ui/android-ui.js").read_bytes())
put(
"_vulpes/android.css",
b'html,body{overscroll-behavior:none} html[data-vulpes-app="system"] #keyboard-frame{display:none!important} html[data-vulpes-app="system"] #screen{height:100%!important} input,textarea{touch-action:manipulation}',
)
(OUT / "payload-checksums.json").write_text(json.dumps(hashes, sort_keys=True))
ext = OUT / "bridge"
shutil.copytree(ROOT / "android/extension", ext, dirs_exist_ok=True)
for folder in ["services", "adapters"]:
shutil.copytree(ROOT / folder, ext / folder, dirs_exist_ok=True)
for name in ["Connections", "Contacts", "Messages"]:
text = (
(ROOT / "host" / f"{name}.sys.mjs")
.read_text()
.replace("Services.uuid.generateUUID().toString()", "crypto.randomUUID()")
)
(ext / f"{name}.mjs").write_text(text)
shutil.copy2(ROOT / "host/Alarms.sys.mjs", ext / "Alarms.mjs")
(ext / "input-editor.js").write_text((ROOT / "services/input-editor.mjs").read_text().replace('export class InputEditor', 'class InputEditor'))
shutil.copy2(ROOT / "host/marketplace-page.js", ext / "marketplace-page.js")
(ext / "release.json").write_text(json.dumps(release))
(ext / "registry.json").write_text(json.dumps(registry, ensure_ascii=False))
settings = json.loads(translate((ROOT / "assets/settings-defaults.json").read_text()))
wallpaper = ROOT / "gaia/apps/default_theme/wallpapers/Illus_Flow2_Blue@2x.jpg"
if not wallpaper.is_file():
raise RuntimeError("Default wallpaper is missing: " + str(wallpaper))
settings["wallpaper.image"] = (
"data:image/jpeg;base64," + base64.b64encode(wallpaper.read_bytes()).decode()
)
settings.update(
{
"ftu.manifestURL": "",
"lockscreen.enabled": True,
"screen.timeout": 0,
"language.current": "fr",
"software-button.enabled": False,
"metrics.selectedMetrics.level": "None",
"ftu.pingEnabled": False,
"search.suggestions.enabled": False,
"search.marketplace.url": "",
"gaia.system.checkForUpdates": False,
"deviceinfo.os": release["gaia"],
"deviceinfo.software": "Vulpes OS " + release["version"],
"deviceinfo.product_model": "Vulpes Android Preview",
"deviceinfo.product_device": "android",
"deviceinfo.platform_version": release["androidEngine"],
"deviceinfo.platform_build_id": release["androidEngineBuildID"],
}
)
(ext / "defaults.json").write_text(json.dumps(settings, ensure_ascii=False))
(OUT / "build-info.json").write_text(
json.dumps(
{
"revision": revision,
"geckoview": release["geckoview"],
"version": release["version"],
"minSdk": 26,
"targetSdk": 37,
}
)
)
print(
f'Packaged {len(registry)} Gaia apps, {round((OUT/"gaia.zip").stat().st_size/1024/1024,1)} MiB'
)