Files
StarPilot/openpilot/tools/arabesque_firefox/setup.py
T
2026-10-28 08:27:36 -05:00

154 lines
6.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Stage the pinned ARM64 browser under /data; never install packages into AGNOS."""
import fcntl
import hashlib
import json
import os
from pathlib import Path
import platform
import pwd
import shutil
import subprocess
import tarfile
import tempfile
import urllib.request
ROOT = Path('/data/arabesque-firefox')
BROWSER_UID = 61234
def report(message):
print(message, flush=True)
def verify(path, package):
algorithm = 'sha512' if 'sha512' in package else 'sha256'
with path.open('rb') as stream:
digest = hashlib.file_digest(stream, algorithm).hexdigest()
if path.stat().st_size != package['size'] or digest != package[algorithm]:
raise ValueError(f'Package checksum mismatch: {package["name"]}')
def download(package, cache):
path = cache / package['name']
if not path.exists():
partial = path.with_suffix('.partial')
try:
with urllib.request.urlopen(package['url'], timeout=30) as response, partial.open('wb') as target:
if not response.url.startswith('https://'):
raise ValueError('Refusing an insecure browser package redirect')
remaining = package['size']
while block := response.read(min(1024 * 1024, remaining + 1)):
remaining -= len(block)
if remaining < 0:
raise ValueError('Browser package exceeds pinned size')
target.write(block)
label = 'Firefox' if package['name'].startswith('firefox-') else 'browser libraries'
report(f'Downloading {label}… {100 * (package["size"] - remaining) // package["size"]}%')
verify(partial, package)
partial.rename(path)
finally:
partial.unlink(missing_ok=True)
verify(path, package)
return path
def unpack(path, destination):
if path.suffix == '.deb':
subprocess.run(['/usr/bin/dpkg-deb', '--extract', str(path), str(destination)], check=True, timeout=30)
else:
with tarfile.open(path) as archive:
archive.extractall(destination, filter='data')
def prepare():
manifest = json.loads(Path(__file__).with_name('runtime.json').read_text())
if os.geteuid() != 0 or platform.machine() != 'aarch64' or Path('/VERSION').read_text().strip() != manifest['agnos']:
raise RuntimeError('Prepare this runtime as root on ARM64 AGNOS 19.8.1')
for path in (ROOT, ROOT / 'runtime', ROOT / 'state', ROOT / 'packages'):
if path.is_symlink():
raise RuntimeError(f'Refusing symlink: {path}')
if ROOT.exists() and (ROOT.stat().st_uid != 0 or ROOT.stat().st_mode & 0o022):
raise RuntimeError('Unsafe browser directory ownership')
ROOT.mkdir(mode=0o755, exist_ok=True)
with (ROOT / '.prepare.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
prepare_locked(manifest)
def prepare_locked(manifest):
cache = ROOT / 'packages'
if (ROOT / 'runtime').exists():
raise RuntimeError('Runtime already exists; stop and review an update separately. The profile is never replaced.')
try:
pwd.getpwuid(BROWSER_UID)
except KeyError:
pass
else:
raise RuntimeError(f'UID {BROWSER_UID} is already assigned')
for entry in Path('/proc').iterdir():
if entry.name.isdigit():
try:
if entry.stat().st_uid == BROWSER_UID:
raise RuntimeError(f'UID {BROWSER_UID} already has running processes')
except FileNotFoundError:
pass
if shutil.disk_usage('/data').free < 6 * 1024**3:
raise RuntimeError('At least 6 GiB free on /data is required to prepare Firefox')
active = subprocess.check_output(['systemctl', '--no-pager', '--plain', '--no-legend', 'list-units', 'arabesque-firefox-*'], text=True)
own_unit = next((Path(line[3:]).name for line in Path('/proc/self/cgroup').read_text().splitlines() if line.startswith('0::')), '')
if any(line.split()[0] != own_unit for line in active.splitlines() if line.split()):
raise RuntimeError('Close all Firefox sessions before preparing the runtime')
# A hard stop can interrupt extraction; only this root-owned staging area is disposable.
for path in ROOT.glob('.prepare-*'):
if path.is_symlink():
path.unlink()
elif path.is_dir():
shutil.rmtree(path)
state = ROOT / 'state'
if state.exists() and (state.stat().st_uid != BROWSER_UID or state.stat().st_mode & 0o077):
raise RuntimeError('Unexpected browser state owner or permissions')
if not state.exists():
state.mkdir(mode=0o700)
os.chown(state, BROWSER_UID, BROWSER_UID)
cache.mkdir(mode=0o700, exist_ok=True)
if cache.stat().st_uid != 0 or cache.stat().st_mode & 0o077:
raise RuntimeError('The browser package cache must be owned by root with mode 0700')
with tempfile.TemporaryDirectory(prefix='.prepare-', dir=ROOT) as temporary:
staging = Path(temporary) / 'runtime'
staging.mkdir()
for index, package in enumerate(manifest['packages'], 1):
report(f'Preparing Firefox ({index}/{len(manifest["packages"])})…')
archive = download(package, cache)
if archive.is_symlink() or archive.stat().st_uid != 0 or archive.stat().st_mode & 0o022:
raise RuntimeError('Unsafe browser package cache file')
report('Unpacking Firefox…' if package['name'].startswith('firefox-') else 'Preparing browser libraries…')
unpack(archive, staging)
schemas = staging / 'usr/share/glib-2.0/schemas'
for source in Path('/usr/share/glib-2.0/schemas').glob('*.xml'):
if not (schemas / source.name).exists():
shutil.copyfile(source, schemas / source.name)
subprocess.run(['/usr/bin/glib-compile-schemas', '--strict', str(schemas)], check=True, timeout=10)
distribution = staging / 'firefox/distribution'
distribution.mkdir(exist_ok=True)
(distribution / 'policies.json').write_text(json.dumps({'policies': {
'DisableAppUpdate': True, 'DontCheckDefaultBrowser': True, 'DisableTelemetry': True, 'DisableFirefoxStudies': True,
}}))
for name in ('empty', 'devices', 'devices/shm'):
(staging / 'isolation' / name).mkdir(parents=True, exist_ok=True)
for name in ('null', 'zero', 'random', 'urandom'):
(staging / 'isolation/devices' / name).touch()
(staging / 'ready.json').write_text(json.dumps(manifest, indent=2) + '\n')
for path in (staging, *staging.rglob('*')):
if not path.is_symlink():
os.chown(path, 0, 0)
path.chmod(0o755 if path.is_dir() or path.stat().st_mode & 0o111 else 0o644)
(ROOT / '.isolation-probe').write_text('This file must be hidden from the Firefox worker.\n')
staging.rename(ROOT / 'runtime')
report('Firefox is prepared. Starting the browser…')
if __name__ == '__main__':
prepare()