VulpesOS/tundra/tools/build-standalone-boot.py
OverSu 0041cd9660 Update shared Gaia workflows and Sargo development tooling
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.
2026-10-08 14:27:17 +02:00

146 lines
9.9 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Build a raw-userdata Tundra boot, or its nondestructive container test variant."""
import argparse
import json
import os
from pathlib import Path
import shlex
import shutil
import stat
import subprocess
import sys
import tempfile
from ramdisk_reference import read_newc
from standalone_volume import checked_volume
from tundra import ROOT, checked_source, cpio, digest
def storage_scripts(init, native, volume, trial):
start = init.index(b'# Use the inventoried filesystem')
end = init.index(b'echo TUNDRA_RESCUE_READY', start)
legacy = init[start:end]
# The container test keeps the old mount solely as a carrier for the image.
if trial:
legacy += b'mkdir -p /run/legacy-storage\nmount --move /installed /run/legacy-storage\n'
else:
legacy = b'# Standalone boot does not open the previous operating system.\n'
init = init[:start]+legacy+init[end:]
init = init.replace(b'exec /bin/sh /native-root.sh', b'exec /bin/sh /standalone-mount.sh')
native = native.replace(b'LOWER=/installed/home/droidian/vulpes-tundra-tests/runtime-reference-20260925/root',
b': "${SYSTEM_IMAGE:?A bundled system image is required}"\nLOWER=/run/tundra-system')
native = native.replace(b'/installed', b'/volume')
native = native.replace(b'/reference/volume', b'/reference/tundra-volume')
if volume.get('hardwareImages'):
lines = ['mkdir -p /newroot/reference']
for name in ('vendor.img', 'modem.img'):
lines += ['test -s /volume/tundra/hardware/'+name,
': > /newroot/reference/'+name,
'mount --bind /volume/tundra/hardware/'+name+' /newroot/reference/'+name,
'mount -o remount,bind,ro /newroot/reference/'+name]
config = json.dumps({'images':volume['hardwareImages']}, separators=(',',':'))
lines += ['mkdir -p /newroot/etc/tundra',
'printf \'%s\\n\' '+shlex.quote(config)+' > /newroot/etc/tundra/hardware-images.json']
native = native.replace(b'# Installed lower layers', ('\n'.join(lines)+'\n# Installed lower layers').encode())
if not trial:
if b'/dev/mapper' in init or b'EXPECTED_STORAGE_HASH' in init:
# The old expected hash assignment is unnecessary in the raw image.
init = b'\n'.join(line for line in init.split(b'\n') if not line.startswith(b'EXPECTED_STORAGE_HASH='))
if b'/dev/mapper' in init or b'/installed/' in native:
raise ValueError('Legacy storage leaked into standalone boot')
device = '/run/legacy-storage/tundra/standalone/'+volume['uuid']+'.ext4' if trial else '/dev/mmcblk0p72'
config = 'VOLUME_DEVICE='+shlex.quote(device)+'\nVOLUME_UUID='+shlex.quote(volume['uuid'])+'\nVOLUME_OPTIONS='+('loop,rw' if trial else 'rw')+'\n'
mount = (ROOT/'boot/standalone-mount.sh').read_bytes().replace(b'# VOLUME_CONFIGURATION', config.encode())
return init, native, mount
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--volume', type=Path, required=True)
parser.add_argument('--prepared', type=Path, required=True)
parser.add_argument('--device-workspace', type=Path, required=True)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--container-trial', action='store_true')
parser.add_argument('--no-deadline', action='store_true')
parser.add_argument('--boot-control', type=Path, help='Pinned ARM64 qbootctl binary')
parser.add_argument('--no-maintenance', action='store_true', help='Exclude private USB SSH credentials and server')
parser.add_argument('--local-debugger', action='store_true', help='Private RAM tests: loopback Marionette, never a public image')
args = parser.parse_args(); os.umask(0o077)
if args.local_debugger and args.no_maintenance: parser.error('Local debugger requires a private maintenance image')
volume = checked_volume(args.volume)
if args.no_deadline and not args.container_trial and not args.boot_control:
parser.error('Persistent standalone images require --boot-control')
if args.boot_control and digest(args.boot_control) != 'dcb00e4c4f038071a5d9979263128325e16b848903371f6c00d49cf3e25c34d9':
raise ValueError('Unqualified boot-control binary')
if args.boot_control and digest(args.boot_control.with_name('copyright')) != '8969e659aff7ed5bb3249341764c2bd048d5abe7400076f01485650e727e8c42':
raise ValueError('Boot-control license notice missing or changed')
system = json.loads((args.prepared/'system.json').read_text())
state = json.loads((args.prepared/'data/state.json').read_text())
if volume['systemImageSha256'] != system['imageSha256'] or volume['stateUuid'] != state['uuid']:
raise ValueError('Volume does not contain this system/data pair')
command = [sys.executable,str(ROOT/'tools/build-rescue-boot.py'), '--output',str(args.output),
'--device-workspace',str(args.device_workspace),'--layout',str(args.device_workspace/'layout.json'),
'--native','--runtime-config','--network','--hardware',
'--system-image',str(args.prepared/'system.json'),'--state',str(args.prepared/'data/state.json')]
if args.no_deadline: command.append('--no-deadline')
if args.local_debugger: command.append('--local-debugger')
subprocess.run(command,check=True)
out = args.output.resolve(); build = json.loads((out/'build.json').read_text())
ramdisk = out/'initramfs.cpio.gz'; files = read_newc(ramdisk.read_bytes())
init,native,mount = storage_scripts(files['init']['data'],files['native-root.sh']['data'],volume,args.container_trial)
if args.boot_control:
files['sbin/tundra-qbootctl']={'mode':stat.S_IFREG|0o755,'data':args.boot_control.read_bytes()}
files['qbootctl-copyright']={'mode':stat.S_IFREG|0o644,'data':args.boot_control.with_name('copyright').read_bytes()}
files['standalone-boot-success.py']={'mode':stat.S_IFREG|0o755,'data':(ROOT/'runtime/mark-boot-success.py').read_bytes()}
unit=b'[Unit]\nDescription=Acknowledge a healthy standalone Vulpes boot\nAfter=tundra-display.service\n[Service]\nType=oneshot\nTimeoutStartSec=650\nExecStart=/usr/bin/python3 /usr/libexec/tundra-mark-boot-success.py\n'
files['standalone-boot-success.service']={'mode':stat.S_IFREG|0o644,'data':unit}
setup=b"""mkdir -p /newroot/usr/libexec /newroot/etc/systemd/system/tundra.target.wants
mkdir -p /newroot/usr/share/doc/qbootctl
cp /qbootctl-copyright /newroot/usr/share/doc/qbootctl/copyright
cp /sbin/tundra-qbootctl /newroot/usr/libexec/tundra-qbootctl
cp /standalone-boot-success.py /newroot/usr/libexec/tundra-mark-boot-success.py
cp /standalone-boot-success.service /newroot/etc/systemd/system/tundra-boot-success.service
ln -sf ../tundra-boot-success.service /newroot/etc/systemd/system/tundra.target.wants/tundra-boot-success.service
"""
native=native.replace(b'# Installed lower layers',setup+b'# Installed lower layers')
if args.no_maintenance:
init=b'\n'.join(line for line in init.split(b'\n') if not line.startswith(b'/sbin/dropbear '))
init=init.replace(b'TUNDRA_USB_SSH_READY',b'TUNDRA_USB_READY')
start=native.index(b'# Keep a minimal, already tested rescue environment')
end=native.index(b'for fs in dev proc sys run;',start)
native=native[:start]+b'# Public boot: no USB maintenance account or shared SSH key.\n'+native[end:]
for name in ('etc/tundra-hostkey','root/.ssh/authorized_keys','sbin/dropbear'):
files.pop(name,None)
files['init']['data']=init; files['native-root.sh']['data']=native
files['standalone-mount.sh']={'mode':stat.S_IFREG|0o755,'data':mount}
files['volume']={'mode':stat.S_IFDIR|0o755,'data':b''}
entries=[]
for name, item in sorted(files.items()):
entries.append((name,item['mode'],item['data'],5 if name=='dev/console' else 0,1 if name=='dev/console' else 0))
ramdisk.write_bytes(cpio(entries))
packager=checked_source('aosp-mkbootimg'); original=out/build['image']
with tempfile.TemporaryDirectory() as temp:
unpack=Path(temp)/'unpack'
text=subprocess.check_output([sys.executable,str(packager/'unpack_bootimg.py'),'--boot_img',str(original),'--out',str(unpack),'--format=mkbootimg'],text=True)
options=shlex.split(text); fields=dict(zip(options[::2],options[1::2]));fields['--ramdisk']=str(ramdisk)
image=out/'boot.img'; command=[sys.executable,str(packager/'mkbootimg.py')]
for key,value in fields.items():command.extend([key,value])
subprocess.run(command+['-o',str(image)],check=True)
verify=Path(temp)/'verify'
subprocess.run([sys.executable,str(packager/'unpack_bootimg.py'),'--boot_img',str(image),'--out',str(verify)],check=True,stdout=subprocess.DEVNULL)
if digest(verify/'kernel')!=build['kernelSha256'] or digest(verify/'ramdisk')!=digest(ramdisk):
raise ValueError('Standalone boot roundtrip failed')
original.unlink()
for path in (Path(__file__).resolve(),ROOT/'tools/standalone_volume.py',ROOT/'boot/standalone-mount.sh',ROOT/'runtime/mark-boot-success.py'):
relative=str(path.relative_to(ROOT)); target=out/'sources/tundra'/relative
target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(path,target);build['sources'][relative]=digest(path)
build.update(image='boot.img',imageSha256=digest(image),ramdiskSha256=digest(ramdisk),
storageMode='container-test' if args.container_trial else 'raw-userdata-ext4',
standaloneVolume=volume,flashReady=False,bootControlSha256=digest(args.boot_control) if args.boot_control else None,
containsPrivateTrialHostKey=not args.no_maintenance,maintenanceEnabled=not args.no_maintenance,
scope='Standalone Tundra boot candidate; hardware qualification required')
(out/'build.json').write_text(json.dumps(build,indent=2)+'\n')
print(image)
if __name__=='__main__':main()