VulpesOS/tundra/tools/sync-beatles-gaia.py
2026-10-22 20:42:26 +02:00

62 lines
3.4 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Synchronize the shared Gaia payload into an idle private Nokia runtime."""
import argparse
import hashlib
import importlib.util
import json
from pathlib import Path, PurePosixPath
import shlex
import subprocess
import tarfile
ROOT=Path(__file__).resolve().parents[2]
REMOTE='/data/local/vulpes-beatles/rootfs'
PREFIXES=('assets/','host/','services/','adapters/','overrides/','gaia/shared/','gaia/apps/')
FILES={'release.json','engine-lock.json','tools/project.py','tools/serve-gaia.py','tools/installed_apps.py'}
def managed(path):
p=PurePosixPath(path)
return not p.is_absolute() and '..' not in p.parts and (path in FILES or path.startswith(PREFIXES))
def changes(current, previous):
if not isinstance(previous,dict) or any(not managed(p) for p in previous):
raise ValueError('Invalid previous Gaia inventory')
return sorted(p for p,digest in current.items() if previous.get(p)!=digest), sorted(set(previous)-set(current))
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--serial',required=True)
args=parser.parse_args()
adb=['adb','-s',args.serial]
def shell(command):return subprocess.check_output(adb+['shell',command],text=True,timeout=180).strip()
if shell('getprop ro.product.codename')!='beatles':raise ValueError('Unexpected device')
if shell('getprop init.svc.b2g')!='running':raise ValueError('Stop Vulpes before synchronizing Gaia')
if shell('test -d '+REMOTE+'/opt/vulpes/assets && echo ready')!='ready':raise ValueError('Stage the Nokia runtime first')
spec=importlib.util.spec_from_file_location('package_gaia',ROOT/'tundra/tools/package-beatles-gaia.py')
package=importlib.util.module_from_spec(spec);spec.loader.exec_module(package)
files=package.shared_files()
current={str(p.relative_to(ROOT)):hashlib.sha256(p.read_bytes()).hexdigest() for p in files}
raw=shell('cat '+REMOTE+'/work/gaia-sync.json 2>/dev/null || true')
previous=json.loads(raw).get('files',{}) if raw else {}
changed,removed=changes(current,previous)
out=ROOT/'tundra/out/beatles/gecko-156.0.1'
manifest=out/'gaia-sync.json'
manifest.write_text(json.dumps({'files':current},indent=2)+'\n')
if changed or removed:
archive=out/'gaia-sync.tar.gz'
with tarfile.open(archive,'w:gz',compresslevel=1,dereference=True) as tar:
for name in changed:tar.add(ROOT/name,arcname='opt/vulpes/'+name)
digest=hashlib.sha256(archive.read_bytes()).hexdigest()
remote=REMOTE+'/work/gaia-sync.tar.gz'
subprocess.run(adb+['push',str(archive),remote],check=True,capture_output=True,timeout=300)
if shell('/system/bin/busybox sha256sum '+remote).split()[0]!=digest:raise ValueError('Gaia transfer checksum mismatch')
shell('/system/bin/busybox tar -xzf '+remote+' -C '+REMOTE+' && rm '+remote)
# Only files recorded by this synchronizer may be removed; profiles stay outside its inventory.
for start in range(0,len(removed),100):
shell('rm -f '+' '.join(shlex.quote(REMOTE+'/opt/vulpes/'+p) for p in removed[start:start+100]))
subprocess.run(adb+['push',str(manifest),REMOTE+'/work/gaia-sync.json.new'],check=True,capture_output=True)
shell('mv '+REMOTE+'/work/gaia-sync.json.new '+REMOTE+'/work/gaia-sync.json')
print(json.dumps({'files':len(current),'updated':len(changed),'removed':len(removed),'profilePreserved':True}))
if __name__=='__main__':main()