Fix camera switching and Contacts/SMS activity routing across shared hosts. Integrate Marketplace installation and native standalone recovery tools, Bluetooth runtime packages and trusted certificates. Update English/French READMEs and the shared changelog with tested scope and remaining radio/audio/Bluetooth limitations. Preview17 remains the existing APK; no new native image is released. Validation: shared JavaScript and native Python tests; Android packaging and payload parity from an isolated publication checkout.
110 lines
5.7 KiB
Python
110 lines
5.7 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('storageMode') in ('raw-userdata-ext4', 'container-test'):
|
|
raise ValueError('Use the standalone installer; the old boot-only plan is incompatible')
|
|
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()
|