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.
This commit is contained in:
parent
d04824ec89
commit
0041cd9660
101 changed files with 3883 additions and 335 deletions
83
tundra/tools/build-standalone-volume.py
Normal file
83
tundra/tools/build-standalone-volume.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#!/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()
|
||||
Loading…
Reference in a new issue