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

@ -20,7 +20,7 @@ NETWORK_SERVICES = frozenset(('irsc_util', 'vendor.per_mgr', 'vendor.rmt_storage
'vendor.tftp_server', 'cnss-daemon', 'pd_mapper'))
HARDWARE_SERVICES = frozenset(('vendor.qcrild', 'vendor.google.radioext@1.0',
HARDWARE_SERVICES = frozenset(('vendor.bluetooth-1-0', 'vendor.qcrild', 'vendor.google.radioext@1.0',
'vendor.audio-hal-2-0', 'vendor.camera-provider-2-4',
'camera_service', 'minimedia', 'minisf', 'miniaf'))

View file

@ -0,0 +1,38 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Acknowledge slot A only after this standalone installation has started Gaia."""
import json
from pathlib import Path
import subprocess
import time
def eligible(boot_id, gaia, mount, cmdline, services_ready):
return (cmdline.get('androidboot.slot_suffix') == '_a'
and mount.get('source') == '/dev/mmcblk0p72'
and mount.get('fstype') == 'ext4'
and gaia.get('bootId') == boot_id and gaia.get('passed') is True
and bool(gaia.get('checks')) and all(gaia['checks'].values())
and services_ready)
def main():
cmdline=dict(x.split('=',1) for x in Path('/proc/cmdline').read_text().split() if '=' in x)
boot_id=Path('/proc/sys/kernel/random/boot_id').read_text().strip()
mounts=json.loads(subprocess.check_output(['findmnt','--json','--mountpoint','/reference/tundra-volume','--output','SOURCE,FSTYPE'],text=True))['filesystems']
if len(mounts)!=1 or mounts[0]['source']!='/dev/mmcblk0p72':
return # Container/RAM experiments must not mark a flashed slot successful.
for _ in range(120):
try:
gaia=json.loads(Path('/opt/vulpes/logs/test.json').read_text())
ready=subprocess.run(['systemctl','is-active','--quiet','tundra-hal','tundra-display','tundra-radio']).returncode==0
if eligible(boot_id,gaia,mounts[0],cmdline,ready):break
except (OSError,ValueError):pass
time.sleep(5)
else:raise RuntimeError('Gaia did not pass startup; slot success left unchanged')
subprocess.run(['/usr/libexec/tundra-qbootctl','-m','a'],check=True)
Path('/var/lib/tundra').mkdir(parents=True,exist_ok=True)
Path('/var/lib/tundra/boot-success.json').write_text(json.dumps({'bootId':boot_id,'gaiaPassed':True,'slot':'a'})+'\n')
if __name__=='__main__':main()

View file

@ -0,0 +1,34 @@
# SPDX-License-Identifier: MPL-2.0
"""Connect the Sargo Bluetooth HAL to BlueZ; pairing stays user initiated."""
def prepare(root, write):
write('etc/dbus-1/system.d/tundra-bluetooth.conf', '''<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig><policy user="root"><allow own="org.bluez"/><allow send_destination="org.bluez"/></policy></busconfig>
''')
write('etc/bluetooth/main.conf', '[General]\nName=Vulpes OS\nDiscoverableTimeout=120\n[Policy]\nAutoEnable=false\n')
write('etc/systemd/system/tundra-bluebinder.service', '''[Unit]
Description=Sargo Bluetooth HAL bridge
Requires=tundra-hal.service
After=tundra-hal.service
Before=bluetooth.service
[Service]
Type=notify
ExecStartPre=/usr/bin/binder-wait android.hardware.bluetooth@1.0::IBluetoothHci/default
ExecStart=/usr/sbin/bluebinder
Restart=on-failure
RestartSec=5
TimeoutStartSec=60
NoNewPrivileges=yes
ProtectHome=yes
ProtectSystem=strict
PrivateTmp=yes
''')
write('etc/systemd/system/bluetooth.service.d/tundra.conf', '''[Unit]
Requires=tundra-bluebinder.service dbus.service
After=tundra-bluebinder.service dbus.service
''')
wants = root/'etc/systemd/system/tundra.target.wants'
wants.mkdir(parents=True, exist_ok=True)
(wants/'bluetooth.service').symlink_to('/usr/lib/systemd/system/bluetooth.service')

View file

@ -3,10 +3,13 @@
"""Configure isolated oFono/PulseAudio diagnostics; no call or SMS on startup."""
from pathlib import Path
import shutil
import runpy
def prepare(root, write):
write('etc/tundra/hardware-enabled', 'sargo\n')
runpy.run_path(str(Path(__file__).with_name('runtime-packages.py')))['stage'](root)
runpy.run_path(str(Path(__file__).with_name('prepare-bluetooth.py')))['prepare'](root, write)
shutil.copy2(Path(__file__).with_name('modem-recovery.py'),
root/'etc/tundra/modem-recovery.py')
write('etc/systemd/system/tundra-hal.service.d/modem-recovery.conf', '''[Service]

View file

@ -0,0 +1,82 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Stage pinned package payloads missing from the original ARM64 reference."""
import hashlib
import io
import json
from pathlib import Path
import shutil
import subprocess
import tarfile
import tempfile
import urllib.request
PACKAGES = (
('libdw1t64_0.196-1_arm64.deb', 'e/elfutils', 'a82634454de2b28d73374f693e4ab6c1c2634fa53b0139312c5f439c294dad2b'),
('libelf1t64_0.196-1_arm64.deb', 'e/elfutils', 'ead8a1244824d982ce51fb82be4904312d5dc44ddc157e589def58c257738476'),
('bluez_5.87-4_arm64.deb', 'b/bluez', 'fe733034dbdfa6146fc94112ad8ca0fce71d8e01d22a59ddb49ef2df3777d6b5'),
('ca-certificates_20260816_all.deb', 'c/ca-certificates', '1cb8b73c74c47677b329fad4095fb9370227d4b8f1969372411478a4586c46cf'),
)
# Existing libelf 0.194-4 files in the inventoried runtime; reject other replacements.
REPLACES = {'usr/lib/aarch64-linux-gnu/libelf-0.194.so': 'ade23632ea253cb4ecfe95d5436bfeb3d40ae2003c5fe10bf1ac1c5b30bdf2e2', 'usr/share/doc/libelf1t64/changelog.Debian.gz': 'ffb0ea69fdbf6f32053059985048676931fd7d5c4458f8ca4bc54750cf4b1b31', 'usr/share/doc/libelf1t64/changelog.gz': '2e8e0af5c39ab744ac6934fe07419b5fd5504a50cd2f42110dd04315c548a708', 'usr/share/doc/libelf1t64/copyright': '2409860c86675ab33f8ad22480608faf849cbc2559d6478f708c23c508a1dbf2', 'usr/share/locale/de/LC_MESSAGES/elfutils.mo': 'f72328e130e63f231acb8e1a56ce5f4f3e91d0fc290db95d7ba4e4b2448d4d93', 'usr/share/locale/en@boldquot/LC_MESSAGES/elfutils.mo': '90158a24e3f85aaacc64085ef2644ba19075cb847c754341c9f245aaa69e94c6', 'usr/share/locale/en@quot/LC_MESSAGES/elfutils.mo': '33c6cda39c2145463dfdb2eb849922dbf6ccf51a9e4bfa8fd757744712d9abfa', 'usr/share/locale/es/LC_MESSAGES/elfutils.mo': '1ebff7e5583759b3f08eb5caaf18dd1a6ed6eb5ec71a02502ed8e21b134ad54f', 'usr/share/locale/ja/LC_MESSAGES/elfutils.mo': '764ed859f3aebd861da0b73c69da4d72d0f252952bb3be1c9c66360258a1661c', 'usr/share/locale/pl/LC_MESSAGES/elfutils.mo': '8abb000f43024994a9ec3032b52eb10858e004b81bc61c7ceea96f7467d13fdd', 'usr/share/locale/uk/LC_MESSAGES/elfutils.mo': 'ce0d747f478ee91a1cdef66bfd1e230a3ff1e787358f4c6e1ffb6513df713117'}
def stage(root, cache=None):
cache = cache or Path(__file__).resolve().parents[1]/'downloads/native-packages-20261008'
cache.mkdir(parents=True, exist_ok=True)
records = []
for name, directory, sha in PACKAGES:
url = 'https://deb.debian.org/debian/pool/main/'+directory+'/'+name
package = cache/name
if not package.exists():
with urllib.request.urlopen(url, timeout=60) as response:
data = response.read()
if hashlib.sha256(data).hexdigest() != sha:
raise ValueError('Package checksum mismatch: '+name)
package.write_bytes(data)
if hashlib.sha256(package.read_bytes()).hexdigest() != sha:
raise ValueError('Package checksum mismatch: '+name)
members = subprocess.check_output(['ar', 't', str(package)], text=True).splitlines()
member = next(m for m in members if m.startswith('data.tar'))
payload = subprocess.check_output(['ar', 'p', str(package), member])
with tempfile.TemporaryDirectory() as folder:
source = Path(folder)
with tarfile.open(fileobj=io.BytesIO(payload)) as archive:
archive.extractall(source, filter='data')
if name.startswith('ca-certificates'):
certificates = sorted((source/'usr/share/ca-certificates/mozilla').glob('*.crt'))
if len(certificates) < 50:
raise ValueError('Incomplete CA bundle')
target = root/'etc/ssl/certs/ca-certificates.crt'
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(b'\n'.join(p.read_bytes() for p in certificates)+b'\n')
target.chmod(0o644)
cert_link = root/'usr/lib/ssl/cert.pem'
cert_link.parent.mkdir(parents=True, exist_ok=True)
if not cert_link.exists() and not cert_link.is_symlink():
cert_link.symlink_to('../../../etc/ssl/certs/ca-certificates.crt')
selected = [source/'usr/share/doc/ca-certificates/copyright']
else:
selected = [p for p in (source/'usr').rglob('*') if p.is_file() or p.is_symlink()]
for path in selected:
target = root/path.relative_to(source)
target.parent.mkdir(parents=True, exist_ok=True)
if path.is_symlink():
if target.exists() or target.is_symlink():
if not target.is_symlink() or target.readlink() != path.readlink():
raise ValueError('Conflicting package link: '+str(target))
else:
target.symlink_to(path.readlink())
else:
if target.exists() and target.read_bytes() != path.read_bytes():
expected = REPLACES.get(str(path.relative_to(source)))
if not expected or hashlib.sha256(target.read_bytes()).hexdigest() != expected:
raise ValueError('Conflicting package file: '+str(target))
shutil.copyfile(path, target)
target.chmod(path.stat().st_mode & 0o777)
records.append({'file': name, 'url': url, 'sha256': sha})
record = root/'usr/share/vulpes/runtime-packages.json'
record.parent.mkdir(parents=True, exist_ok=True)
record.write_text(json.dumps(records, indent=2)+'\n')
return records

View file

@ -53,7 +53,19 @@ def main():
with image.open('rb') as f:sha=hashlib.file_digest(f,'sha256').hexdigest()
if sha!='132d0e18c6a6a111293447e940267f35aa2b14fa00a872ef160b20dc2014d886':raise RuntimeError('Android reference changed')
run('mount','-t','ext4','-o','loop,ro,noload',str(image),'/run/tundra/android-original')
run('mount','-t','ext4','-o','ro,noload','/dev/mmcblk0p70','/run/tundra/vendor-original')
hardware = Path('/etc/tundra/hardware-images.json')
bundled = json.loads(hardware.read_text())['images'] if hardware.exists() else None
if bundled:
if set(bundled) != {'vendor.img', 'modem.img'}:
raise RuntimeError('Expected both standalone hardware images')
for name, record in bundled.items():
path = Path('/reference')/name
with path.open('rb') as source:
if path.stat().st_size != record['bytes'] or hashlib.file_digest(source, 'sha256').hexdigest() != record['sha256']:
raise RuntimeError('Standalone hardware image changed: '+name)
run('mount','-t','ext4','-o','loop,ro,noload','/reference/vendor.img','/run/tundra/vendor-original')
else:
run('mount','-t','ext4','-o','ro,noload','/dev/mmcblk0p70','/run/tundra/vendor-original')
run('mount','-t','ext4','-o','ro,noload','/dev/mmcblk0p48','/mnt/vendor/persist')
overlay('/run/tundra/android-original','/android','android')
overlay('/run/tundra/vendor-original','/android/vendor','vendor')
@ -65,7 +77,10 @@ def main():
if count != 1:raise RuntimeError('Expected exactly one Sargo WLAN IPA setting')
config.write_text(text) # vendor overlay upper is tmpfs, never the partition
directory('/android/vendor/firmware_mnt')
run('mount','-t','vfat','-o','ro','/dev/mmcblk0p21','/android/vendor/firmware_mnt')
if bundled:
run('mount','-t','vfat','-o','loop,ro','/reference/modem.img','/android/vendor/firmware_mnt')
else:
run('mount','-t','vfat','-o','ro','/dev/mmcblk0p21','/android/vendor/firmware_mnt')
report={}
for p in sorted(Path('/android').rglob('*.rc')):
if not p.is_file() or p.is_symlink() or not is_init_script(p):continue