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.
70 lines
2.7 KiB
Python
70 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: MPL-2.0
|
|
"""Export the qualified original boot backup and a standalone restoration tool.
|
|
|
|
The kit is private and specific to one phone. Keep it outside the project and
|
|
do not upload it as a release. Exporting performs no phone operations.
|
|
"""
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
from tundra import ROOT, digest
|
|
|
|
|
|
def load(name, path):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def export(plan, output):
|
|
output = Path(output).absolute()
|
|
if output.exists() or output.is_symlink():
|
|
raise ValueError('Recovery output exists; keep the existing backup')
|
|
backup = Path(plan['backupDirectory'])
|
|
from boot_backup import verify
|
|
manifest = verify(backup)
|
|
if digest(backup/'manifest.json') != plan['backupManifestSha256']:
|
|
raise ValueError('Backup manifest changed')
|
|
output.mkdir(mode=0o700)
|
|
marker = output/'.incomplete'
|
|
marker.write_text('Recovery export incomplete\n')
|
|
shutil.copyfile(backup/'boot_a.img', output/'boot_a.img')
|
|
(output/'boot_a.img').chmod(0o600)
|
|
tool = ROOT/'tools/restore-sargo.py'
|
|
shutil.copyfile(tool, output/tool.name)
|
|
(output/tool.name).chmod(0o700)
|
|
recovery = {'schema': 1, 'device': 'sargo', 'partition': 'boot_a', 'slot': 'a',
|
|
'serialSha256': plan['serialSha256'], 'image': 'boot_a.img',
|
|
'bytes': manifest['files']['boot_a.img']['bytes'],
|
|
'imageSha256': manifest['files']['boot_a.img']['sha256'],
|
|
'backupManifestSha256': plan['backupManifestSha256'],
|
|
'restoreToolSha256': digest(tool), 'private': True,
|
|
'scope': 'Original qualified boot_a only; not a userdata backup or a generic ROM'}
|
|
(output/'recovery.json').write_text(json.dumps(recovery, indent=2)+'\n')
|
|
load('restore_check', tool).checked_kit(output)
|
|
marker.unlink()
|
|
return output
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--output', type=Path, required=True)
|
|
parser.add_argument('--device-workspace', type=Path)
|
|
args = parser.parse_args()
|
|
os.umask(0o077)
|
|
if args.device_workspace:
|
|
from sargo_workspace import checked_workspace
|
|
plan, _ = checked_workspace(args.device_workspace)
|
|
else:
|
|
plan = load('backup_plan', ROOT/'tools/test-sargo-boot.py').checked_backup_plan()
|
|
print(export(plan, args.output))
|
|
print('Keep this private kit on another disk. Its boot image belongs only to this phone.')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|