VulpesOS/tundra/tools/install-sargo.py
OverSu d04824ec89 Prepare Sargo installations with private per-device workspaces
Bind backups, storage layout, RAM builds, file staging and recovery to each phone. Verify atomic transfers without replacing existing state and record the unresolved Android HAL source provenance.

Validated with 53 native tooling tests, 5 changelog tests and two RAM boots with persistent Gaia state on the development Sargo. No new flash or native binary release.
2026-10-04 00:06:13 +02:00

108 lines
5.5 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Review, install or restore a qualified private Sargo boot image.
The system and data files must already be staged and tested on the inventoried
phone. Only boot_a can be written. No userdata formatting, slot switch or erase.
"""
import argparse
import importlib.util
import json
import os
from pathlib import Path
import subprocess
from tundra import ROOT, digest, now, save
def load(name, path):
spec=importlib.util.spec_from_file_location(name,path)
value=importlib.util.module_from_spec(spec);spec.loader.exec_module(value)
return value
def checked_build(folder):
folder=Path(folder).resolve()
build=json.loads((folder/'build.json').read_text())
if build.get('localDebugger'):raise ValueError('Diagnostic debugger image cannot be flashed')
qualification=json.loads((folder/'qualification.json').read_text())
if (not qualification.get('passed') or not qualification.get('privateInstallReady')
or not build.get('persistentState') or build.get('watchdogSeconds') is not None
or build.get('interactiveDiagnostics')):
raise ValueError('Expected a qualified persistent image without interactive tests or a deadline')
image=(folder/build['image']).resolve();image.relative_to(folder)
if digest(image)!=build['imageSha256'] or qualification.get('imageSha256')!=build['imageSha256']:
raise ValueError('Boot image provenance mismatch')
evidence=qualification.get('evidenceSha256')
if not evidence:raise ValueError('Qualification evidence missing')
for name,expected in evidence.items():
path=(folder/name).resolve();path.relative_to(folder)
if digest(path)!=expected:raise ValueError('Qualification evidence changed: '+name)
persistence=qualification.get('persistence',{})
if not (persistence.get('passed') and persistence.get('distinctBoots')
and persistence.get('sameGaiaSetting') and persistence.get('restored')):
raise ValueError('A real persistence round trip is required')
load('rescue_check',ROOT/'tools/test-rescue-boot.py').verify_sources(folder,build)
return build,qualification,image
def flash_boot_a(serial, image):
# Keep the write surface separate from the RAM boot tools.
subprocess.run(['fastboot','-s',serial,'flash','boot_a',str(image)],check=True,timeout=90)
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--build',type=Path,required=True)
parser.add_argument('--device-workspace',type=Path)
action=parser.add_mutually_exclusive_group()
action.add_argument('--flash',action='store_true',help='Install this boot image in boot_a; does not reboot automatically')
action.add_argument('--restore',action='store_true',help='Restore the verified original boot_a backup; retain Tundra data')
args=parser.parse_args();os.umask(0o077)
boot=load('sargo_boot',ROOT/'tools/test-sargo-boot.py')
workspace_plan = None
if args.device_workspace:
from sargo_workspace import checked_workspace
workspace_plan, workspace_layout = checked_workspace(args.device_workspace)
if args.restore:
# Recovery must still work if the candidate or its proof was damaged.
plan=workspace_plan or boot.checked_backup_plan()
image=Path(plan['backupDirectory'])/'boot_a.img'
build={'serialSha256':plan['serialSha256']}
else:
build,qualification,image=checked_build(args.build)
if workspace_plan:
plan = workspace_plan
if (build.get('deviceWorkspaceSha256') != plan['workspaceSha256']
or build['layout'] != workspace_layout):
raise ValueError('Build was not prepared with this device workspace')
else:
if build.get('deviceWorkspaceSha256'):
raise ValueError('This build requires --device-workspace; no fallback to shared inventory')
plan,_=boot.checked_plan()
if plan['serialSha256']!=build['serialSha256']:raise ValueError('Build targets another phone')
print('Private Sargo installation plan')
print('Boot image:',image)
if not args.restore:
print('System file: /tundra/images/'+build['systemImage']['imageSha256']+'.ext4')
print('Data file: /tundra/data/'+build['persistentState']['uuid']+'.ext4')
print('Only boot_a may be written. Existing files, userdata and slot B are retained.')
if not (args.flash or args.restore):
print('Review only. Use --flash or --restore explicitly with the inventoried phone in Fastboot.')
return
serial=boot.find_fastboot(build['serialSha256'],15)
identity=boot.check_device(serial,build['serialSha256'])
report={'recordedAt':now(),'operation':'restore' if args.restore else 'install',
'partition':'boot_a','imageSha256':digest(image),'identity':identity,
'erasesSent':False,'slotChanged':False,'completed':False}
path=args.build/('restore-report.json' if args.restore else 'install-report.json')
save(path,report)
flash_boot_a(serial,image)
report['completed']=True;save(path,report)
print('boot_a written. Select Start in Fastboot to boot. No other partition was flashed.')
if not args.restore:
print('After Gaia passes startup checks, finalize the A/B success flag:')
print(' python3 tundra/tools/finalize-sargo-slot.py --build '+str(args.build)+' --mark-successful')
print('Do not repeat test reboots before this step: the bootloader has a limited retry count.')
if __name__=='__main__':main()