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

@ -50,7 +50,11 @@ class HardwareTests(unittest.TestCase):
self.assertIn('unmanaged-devices=*,except:interface-name:wlan0', config)
self.assertIn('auth-polkit=root-only', config)
self.assertIn('enabled=false', config)
self.assertFalse((root/'etc/NetworkManager/system-connections').exists())
connections = root/'etc/NetworkManager/system-connections'
self.assertTrue(connections.is_dir())
self.assertEqual(list(connections.iterdir()), [])
wifi_service = (root/'etc/systemd/system/tundra-wifi.service').read_text()
self.assertIn('ReadWritePaths=/run /etc/NetworkManager/system-connections', wifi_service)
self.assertIn('VULPES_TUNDRA=1', (root/'etc/systemd/system/tundra-display.service').read_text())

View file

@ -0,0 +1,47 @@
"""Check the pinned Bluetooth libraries and native TLS trust store offline."""
import importlib.util
import json
from pathlib import Path
import ssl
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location('packages', ROOT/'runtime/runtime-packages.py')
packages = importlib.util.module_from_spec(spec)
spec.loader.exec_module(packages)
CACHE = ROOT/'downloads/native-packages-20261008'
class RuntimePackages(unittest.TestCase):
def test_corrupt_archive_is_rejected_before_extraction(self):
with tempfile.TemporaryDirectory() as folder:
base = Path(folder)
cache = base/'cache'
cache.mkdir()
(cache/packages.PACKAGES[0][0]).write_bytes(b'corrupt')
with self.assertRaisesRegex(ValueError, 'checksum mismatch'):
packages.stage(base/'root', cache)
self.assertFalse((base/'root').exists())
@unittest.skipUnless(all((CACHE/name).exists() for name, _, _ in packages.PACKAGES),
'Pinned package cache is not installed')
def test_staged_payload_is_repeatable_and_certificates_are_usable(self):
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
first = packages.stage(root, CACHE)
self.assertEqual(packages.stage(root, CACHE), first)
self.assertEqual(json.loads((root/'usr/share/vulpes/runtime-packages.json').read_text()), first)
self.assertTrue((root/'usr/libexec/bluetooth/bluetoothd').is_file())
context = ssl.create_default_context(cafile=str(root/'etc/ssl/certs/ca-certificates.crt'))
self.assertGreater(context.cert_store_stats()['x509_ca'], 50)
self.assertTrue((root/'usr/lib/ssl/cert.pem').is_file())
target = root/'usr/libexec/bluetooth/bluetoothd'
target.write_bytes(b'unknown local binary')
with self.assertRaisesRegex(ValueError, 'Conflicting package file'):
packages.stage(root, CACHE)
self.assertEqual(target.read_bytes(), b'unknown local binary')
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,37 @@
# SPDX-License-Identifier: MPL-2.0
import io
from pathlib import Path
import struct
import sys
import unittest
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
from sparse_backup import receive
def record(offset,data):return struct.pack('<QQ',offset,len(data))+data
class BackupTests(unittest.TestCase):
def test_holes_and_final_size_are_restored(self):
stream=b'TUNRAW1\0'+record(2,b'abc')+record(9,b'x')+record(12,b'')
target=io.BytesIO();receive(io.BytesIO(stream),target,12)
# BytesIO does not extend on truncate; actual disk files do.
self.assertEqual(target.getvalue(),b'\0\0abc\0\0\0\0x')
def test_empty_disk_has_a_required_trailer(self):
receive(io.BytesIO(b'TUNRAW1\0'+record(100,b'')),io.BytesIO(),100)
with self.assertRaises(ValueError):receive(io.BytesIO(b'TUNRAW1\0'),io.BytesIO(),100)
def test_overlapping_or_out_of_bounds_blocks_rejected(self):
for records in (record(2,b'abc')+record(4,b'bad'),record(8,b'too long')):
with self.assertRaises(ValueError):receive(io.BytesIO(b'TUNRAW1\0'+records),io.BytesIO(),10)
def test_wrong_trailer_and_trailing_bytes_rejected(self):
for stream in (b'TUNRAW1\0'+record(9,b''),b'TUNRAW1\0'+record(10,b'')+b'extra'):
with self.assertRaises(ValueError):receive(io.BytesIO(stream),io.BytesIO(),10)
def test_truncated_payload_rejected(self):
with self.assertRaises(ValueError):receive(io.BytesIO(b'TUNRAW1\0'+struct.pack('<QQ',0,5)+b'four'),io.BytesIO(),10)
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,91 @@
# SPDX-License-Identifier: MPL-2.0
import importlib.util
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
TOOLS=Path(__file__).resolve().parents[1]/'tools'
sys.path.insert(0,str(TOOLS))
from standalone_volume import checked_volume
spec=importlib.util.spec_from_file_location('standalone_boot',TOOLS/'build-standalone-boot.py')
boot=importlib.util.module_from_spec(spec);spec.loader.exec_module(boot)
class StandaloneBootTests(unittest.TestCase):
def setUp(self):
self.volume={'schema':1,'device':'sargo','storage':'raw-userdata-ext4','image':'userdata.ext4',
'uuid':'12345678-1234-1234-1234-123456789abc','bytes':8*1024**3,
'imageSha256':'a'*64,'systemImageSha256':'b'*64,'filesystemCheckPassed':True}
self.init=b'''EXPECTED_STORAGE_HASH=old
# Use the inventoried filesystem
mount /dev/mapper/tundra-installed-ro /installed
echo TUNDRA_RESCUE_READY
exec /bin/sh /native-root.sh
'''
self.native=b'''SYSTEM_IMAGE=/installed/tundra/images/system.ext4
LOWER=/installed/home/droidian/vulpes-tundra-tests/runtime-reference-20260925/root
mkdir -p /newroot/reference/installed
mount --move /installed /newroot/reference/installed
'''
def test_direct_boot_has_no_legacy_storage_path(self):
init,native,mount=boot.storage_scripts(self.init,self.native,self.volume,False)
self.assertNotIn(b'EXPECTED_STORAGE_HASH',init)
self.assertNotIn(b'/dev/mapper',init)
self.assertNotIn(b'/installed',native)
self.assertNotIn(b'/home/droidian',native)
self.assertIn(b'/dev/mmcblk0p72',mount)
self.assertIn(b'VOLUME_OPTIONS=rw',mount)
self.assertNotIn(b'mkfs',mount)
self.assertIn(b'/newroot/reference/tundra-volume',native)
def test_container_boot_changes_only_volume_access(self):
init,native,mount=boot.storage_scripts(self.init,self.native,self.volume,True)
self.assertIn(b'/run/legacy-storage',init)
self.assertIn(b'VOLUME_OPTIONS=loop,rw',mount)
self.assertNotIn(b'/dev/mmcblk0p72',mount)
self.assertIn(b'/volume/tundra/images',native)
for script in (init,native,mount):
self.assertEqual(subprocess.run(['sh','-n'],input=script,capture_output=True).returncode,0)
def test_bundled_hardware_is_bound_read_only(self):
volume={**self.volume,'hardwareImages':{'vendor.img':{'sha256':'a'*64,'bytes':4096},'modem.img':{'sha256':'b'*64,'bytes':4096}}}
native=self.native+b'# Installed lower layers stay reachable\n'
_,script,_=boot.storage_scripts(self.init,native,volume,False)
self.assertIn(b'mount -o remount,bind,ro /newroot/reference/vendor.img',script)
self.assertIn(b'/newroot/etc/tundra/hardware-images.json',script)
self.assertEqual(subprocess.run(['sh','-n'],input=script,capture_output=True).returncode,0)
def test_wrong_volume_metadata_rejected(self):
with tempfile.TemporaryDirectory() as directory:
path=Path(directory)/'volume.json'
path.write_text(json.dumps(self.volume));checked_volume(path,False)
for key,value in [('device','bonito'),('image','../userdata.ext4'),('bytes',1),('uuid','$(reboot)'),('storage','lvm'),('filesystemCheckPassed',False)]:
path.write_text(json.dumps({**self.volume,key:value}))
with self.assertRaises(ValueError):checked_volume(path,False)
def test_boot_success_requires_current_gaia_on_raw_userdata(self):
spec=importlib.util.spec_from_file_location('success',TOOLS.parent/'runtime/mark-boot-success.py')
success=importlib.util.module_from_spec(spec);spec.loader.exec_module(success)
gaia={'bootId':'current','passed':True,'checks':{'home':True}}
mount={'source':'/dev/mmcblk0p72','fstype':'ext4'}
self.assertTrue(success.eligible('current',gaia,mount,{'androidboot.slot_suffix':'_a'},True))
self.assertFalse(success.eligible('new',gaia,mount,{'androidboot.slot_suffix':'_a'},True))
self.assertFalse(success.eligible('current',gaia,{'source':'/dev/loop1','fstype':'ext4'},{'androidboot.slot_suffix':'_a'},True))
self.assertFalse(success.eligible('current',gaia,mount,{'androidboot.slot_suffix':'_b'},True))
self.assertFalse(success.eligible('current',gaia,mount,{'androidboot.slot_suffix':'_a'},False))
def test_old_installer_refuses_standalone_image(self):
spec=importlib.util.spec_from_file_location('install',TOOLS/'install-sargo.py')
install=importlib.util.module_from_spec(spec);spec.loader.exec_module(install)
with tempfile.TemporaryDirectory() as directory:
path=Path(directory);(path/'build.json').write_text(json.dumps({'storageMode':'raw-userdata-ext4'}))
with self.assertRaisesRegex(ValueError,'standalone installer'):
install.checked_build(path)
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,80 @@
# SPDX-License-Identifier: MPL-2.0
import importlib.util
from pathlib import Path
import struct
import sys
import tempfile
import unittest
from unittest.mock import patch
spec=importlib.util.spec_from_file_location('flash',Path(__file__).resolve().parents[1]/'tools/flash-standalone-sargo.py')
flash=importlib.util.module_from_spec(spec);spec.loader.exec_module(flash)
class FlashTests(unittest.TestCase):
def plan(self):
return {'userdataBytes':8*1024**3,'partitions':{name:{'bytes':4096} for name in flash.PARTITIONS}}
def values(self,**changes):
values={'product':'sargo','unlocked':'yes','current-slot':'b',
**{'partition-size:'+name:hex(64*1024**3 if name=='userdata' else 64*1024**2) for name in flash.PARTITIONS}}
values.update(changes);return values
def test_both_slots_accepted_for_full_install(self):
for slot in ('a','b'):
values=self.values(**{'current-slot':slot})
with patch.object(flash,'getvar',side_effect=lambda _,__,name:values[name]):
self.assertEqual(flash.preflight('fastboot','serial',self.plan())['current-slot'],slot)
def test_wrong_model_locked_or_small_partition_rejected(self):
for change in ({'product':'bonito'},{'unlocked':'no'},{'partition-size:userdata':'0x1000'}):
values=self.values(**change)
with patch.object(flash,'getvar',side_effect=lambda _,__,name:values[name]):
with self.assertRaises(ValueError):flash.preflight('fastboot','serial',self.plan())
def test_review_never_contacts_device(self):
with patch.object(sys,'argv',['flash']),patch.object(flash,'package',return_value=self.plan()),patch.object(flash.subprocess,'run') as run,patch.object(flash.subprocess,'check_output') as read:
flash.main();run.assert_not_called();read.assert_not_called()
def test_install_requires_explicit_userdata_replacement(self):
with patch.object(sys,'argv',['flash','--install']),patch.object(flash,'package',return_value=self.plan()),patch.object(flash.subprocess,'run') as run:
with self.assertRaises(SystemExit):flash.main()
run.assert_not_called()
def test_unqualified_package_requires_experimental_flag(self):
with patch.object(sys,'argv',['flash','--install','--erase-userdata']),patch.object(flash,'package',return_value=self.plan()),patch.object(flash.subprocess,'run') as run:
with self.assertRaises(SystemExit):flash.main()
run.assert_not_called()
def test_sparse_geometry(self):
with tempfile.TemporaryDirectory() as directory:
image=Path(directory)/'userdata.img'
image.write_bytes(struct.pack('<I4H4I',0xed26ff3a,1,0,28,12,4096,2097152,3,0))
self.assertEqual(flash.sparse_size(image),8*1024**3)
image.write_bytes(b'bad')
with self.assertRaises(ValueError):flash.sparse_size(image)
def test_incomplete_package_is_rejected_before_manifest_read(self):
with tempfile.TemporaryDirectory() as directory:
(Path(directory)/'.incomplete').touch()
with self.assertRaisesRegex(ValueError,'construction is incomplete'):
flash.package(directory)
def test_install_writes_only_planned_partitions_after_preflight(self):
with tempfile.TemporaryDirectory() as directory:
argv=['flash','--package',directory,'--install','--erase-userdata','--experimental']
with patch.object(sys,'argv',argv),patch.object(flash,'package',return_value=self.plan()), \
patch.object(flash.shutil,'which',return_value='fastboot'), \
patch.object(flash.subprocess,'check_output',return_value='test-device fastboot\n'), \
patch.object(flash,'preflight',return_value={'product':'sargo'}) as preflight, \
patch.object(flash.subprocess,'run') as run:
flash.main()
preflight.assert_called_once()
commands=[call.args[0][3:] for call in run.call_args_list]
self.assertEqual(commands[0],['erase','userdata'])
self.assertEqual([command[:2] for command in commands[1:-1]],
[['flash',partition] for partition in flash.PARTITIONS])
self.assertEqual(commands[-1],['set_active','a'])
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,69 @@
# SPDX-License-Identifier: MPL-2.0
import gzip
import hashlib
import importlib.util
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('restore', Path(__file__).resolve().parents[1]/'tools/restore-standalone-sargo.py')
restore = importlib.util.module_from_spec(spec)
spec.loader.exec_module(restore)
class RestoreTests(unittest.TestCase):
def make_backup(self, folder):
payload = b'original userdata\0' * 100
sha = hashlib.sha256(payload).hexdigest()
data = {'archive': 'userdata-original.raw.gz', 'bytes': len(payload),
'sha256': sha, 'checked': True, 'sourceWasUnmounted': True}
(folder/data['archive']).write_bytes(gzip.compress(payload))
(folder/'userdata.json').write_text(json.dumps(data))
files = {}
for name in ('boot_a.img', 'dtbo_a.img', 'vbmeta_a.img'):
content = name.encode()
(folder/name).write_bytes(content)
files[name] = {'bytes': len(content), 'sha256': hashlib.sha256(content).hexdigest()}
manifest = {'device': 'sargo', 'activeSlot': 'a', 'files': files,
'userdataSha256': sha, 'userdataBytes': len(payload)}
(folder/'recovery.json').write_text(json.dumps(manifest))
return manifest
def test_review_verifies_backup_without_phone_commands(self):
with tempfile.TemporaryDirectory() as directory:
folder = Path(directory)
self.make_backup(folder)
with patch.object(sys, 'argv', ['restore', '--backup', directory]), \
patch.object(restore.subprocess, 'run') as run, \
patch.object(restore.subprocess, 'check_output') as read:
restore.main()
run.assert_not_called()
read.assert_not_called()
def test_unbound_userdata_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
folder = Path(directory)
manifest = self.make_backup(folder)
manifest['userdataSha256'] = '0' * 64
(folder/'recovery.json').write_text(json.dumps(manifest))
with patch.object(sys, 'argv', ['restore', '--backup', directory]), \
patch.object(restore.subprocess, 'run') as run:
with self.assertRaisesRegex(ValueError, 'not bound'):
restore.main()
run.assert_not_called()
def test_altered_userdata_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
folder = Path(directory)
self.make_backup(folder)
(folder/'userdata-original.raw.gz').write_bytes(gzip.compress(b'changed'))
with patch.object(sys, 'argv', ['restore', '--backup', directory]):
with self.assertRaisesRegex(ValueError, 'Storage backup changed'):
restore.main()
if __name__ == '__main__':
unittest.main()