VulpesOS/tundra/tools/build-standalone-volume.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

83 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Build a fresh ext4 userdata image. No connection to a phone or partition writes."""
import argparse
import importlib.util
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import uuid
from persistent_state import FEATURES
from tundra import digest
from standalone_volume import checked_volume
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--prepared', required=True, type=Path)
parser.add_argument('--output', required=True, type=Path)
parser.add_argument('--hardware', type=Path, help='Locally supplied pinned vendor/modem images')
parser.add_argument('--size-mib', type=int, default=8192)
args = parser.parse_args()
if not 5120 <= args.size_mib <= 32768:
parser.error('Choose 5120–32768 MiB')
spec = importlib.util.spec_from_file_location('stage', Path(__file__).with_name('stage-sargo-files.py'))
stage = importlib.util.module_from_spec(spec); spec.loader.exec_module(stage)
files = stage.inputs(args.prepared)
hardware = {}
if args.hardware:
lock = json.loads((Path(__file__).resolve().parents[1]/'manifests/sargo-hardware.json').read_text())
hardware = lock['images']
for name, record in hardware.items():
source = args.hardware/record['source']
if source.is_symlink() or source.stat().st_size != record['bytes'] or digest(source) != record['sha256']:
raise ValueError('Hardware image changed: '+name)
payload_bytes = sum(p.stat().st_size for _,_,p,_ in files)
payload_bytes += sum(record['bytes'] for record in hardware.values())
if args.size_mib*1024**2 < payload_bytes+512*1024**2:
raise ValueError('Insufficient space for payload and filesystem metadata')
os.umask(0o077)
out = args.output.resolve(); out.mkdir(parents=True, exist_ok=False)
(out/'.incomplete').touch()
seed = out/'seed'; seed.mkdir()
for kind, name, source, record in files:
target = seed/'tundra'/kind/name; target.parent.mkdir(parents=True, exist_ok=True)
# mke2fs reads the file; a hard link avoids another multi-gigabyte copy.
os.link(source, target)
if hardware:
directory = seed/'tundra/hardware'; directory.mkdir()
for name, record in hardware.items():
os.link(args.hardware/record['source'], directory/name)
system = json.loads((args.prepared/'system.json').read_text())
state = json.loads((args.prepared/'data/state.json').read_text())
identifier = str(uuid.uuid4())
(seed/'tundra-volume-id').write_text(identifier+'\n')
image = out/'userdata.ext4'
with image.open('xb') as f: f.truncate(args.size_mib*1024**2)
subprocess.run(['mke2fs','-q','-t','ext4','-F','-b','4096','-I','256','-O',FEATURES,
'-U',identifier,'-L','TUNDRA_VOLUME','-d',str(seed),str(image)],check=True)
names=['/tundra-volume-id','/tundra','/tundra/images','/tundra/data']
names += ['/tundra/'+kind+'/'+name for kind,name,_,_ in files]
if hardware:
names += ['/tundra/hardware', *['/tundra/hardware/'+name for name in hardware]]
for name in names:
for field in ('uid','gid'):
subprocess.run(['debugfs','-w','-R',f'set_inode_field {name} {field} 0',str(image)],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
result = subprocess.run(['e2fsck','-f','-n',str(image)],capture_output=True,text=True)
(out/'filesystem-check.txt').write_text(result.stdout+result.stderr)
if result.returncode: raise ValueError('Standalone filesystem check failed')
manifest={'schema':1,'device':'sargo','storage':'raw-userdata-ext4','image':image.name,
'uuid':identifier,'bytes':image.stat().st_size,'imageSha256':digest(image),
'systemImageSha256':system['imageSha256'],'stateUuid':state['uuid'],
'filesystemCheckPassed':True,'flashReady':False,'hardwareImages':hardware,
'scope':'Fresh standalone storage; flashing userdata destroys previous user data'}
(out/'volume.json').write_text(json.dumps(manifest,indent=2)+'\n')
checked_volume(out/'volume.json')
(out/'.incomplete').unlink()
print(out/'volume.json')
if __name__ == '__main__': main()