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:
OverSu 2026-10-08 14:27:17 +02:00
commit 0041cd9660
101 changed files with 3883 additions and 335 deletions

View 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()