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
|
|
@ -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'))
|
||||
|
||||
|
|
|
|||
38
tundra/runtime/mark-boot-success.py
Normal file
38
tundra/runtime/mark-boot-success.py
Normal 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()
|
||||
34
tundra/runtime/prepare-bluetooth.py
Normal file
34
tundra/runtime/prepare-bluetooth.py
Normal 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')
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
82
tundra/runtime/runtime-packages.py
Normal file
82
tundra/runtime/runtime-packages.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue