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.
108 lines
5.5 KiB
Python
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()
|