Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,129 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Assemble a private Sargo system image, including its Android HAL reference."""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[1]
PROJECT = ROOT.parent
ANDROID_SHA = '132d0e18c6a6a111293447e940267f35aa2b14fa00a872ef160b20dc2014d886'
def digest(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
result = importlib.util.module_from_spec(spec)
spec.loader.exec_module(result)
return result
def pack(root, image):
if not os.environ.get('FAKEROOTKEY'):
raise RuntimeError('Image ownership must be prepared under fakeroot')
for directory, dirs, files in os.walk(root):
for path in [Path(directory), *[Path(directory)/n for n in dirs+files]]:
os.lchown(path, 0, 0)
# The trial session refreshes its local Gecko host files before starting.
for base in (root/'opt/vulpes', root/'home/droidian'):
for directory, dirs, files in os.walk(base):
for path in [Path(directory), *[Path(directory)/n for n in dirs+files]]:
os.lchown(path, 32011, 32011)
features = 'none,has_journal,ext_attr,dir_index,filetype,extent,sparse_super,large_file,uninit_bg,dir_nlink,extra_isize'
subprocess.run(['mke2fs', '-q', '-t', 'ext4', '-F', '-b', '4096', '-I', '256',
'-O', features, '-L', 'TUNDRA_SYSTEM', '-d', str(root), str(image)], check=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--reference', type=Path, default=ROOT/'downloads/sargo-installed/runtime-reference-20260925')
parser.add_argument('--product', type=Path, default=ROOT/'out/droidian/product')
parser.add_argument('--pack', nargs=2, type=Path, help=argparse.SUPPRESS)
args = parser.parse_args()
if args.pack:
pack(*args.pack)
return
if os.geteuid() == 0:
parser.error('Build as a regular user')
android = ROOT/'downloads/sargo-installed/android-rootfs.img'
if digest(android) != ANDROID_SHA:
raise ValueError('Android HAL reference changed')
lock = json.loads((args.product/'engine-lock.json').read_text())
current = json.loads((PROJECT/'engine-lock.json').read_text())
if lock.get('architecture') != 'aarch64' or lock['version'] != current['version']:
raise ValueError('Prepare the matching ARM64 engine first')
output = args.output.resolve()
output.mkdir(parents=True, exist_ok=False)
root = output/'root'
staging = module('stage', ROOT/'tools/stage-runtime.py').stage(args.reference, root)
product = root/'opt/vulpes'
# Read UI/services from the common checkout, and only the engine/EGL adapter
# from the checked ARM64 product. Never copy profiles or phone configuration.
for name in ('assets', 'host', 'services', 'adapters', 'overrides', 'gaia/shared', 'gaia/apps/system'):
shutil.copytree(PROJECT/name, product/name, symlinks=True,
ignore=shutil.ignore_patterns('__pycache__', 'test', 'tests'))
for name in ('tools', 'tests', 'logs', 'profiles'):
(product/name).mkdir(exist_ok=True)
for name in ('serve-gaia.py', 'project.py', 'run-host.py'):
shutil.copy2(PROJECT/'tools'/name, product/'tools'/name)
shutil.copy2(PROJECT/'tests/ProbeChild.sys.mjs', product/'tests/ProbeChild.sys.mjs')
shutil.copy2(PROJECT/'release.json', product/'release.json')
shutil.copy2(args.product/'engine-lock.json', product/'engine-lock.json')
shutil.copytree(args.product/'engines', product/'engines', symlinks=True)
shutil.copytree(ROOT/'droidian', product/'trial', ignore=shutil.ignore_patterns('__pycache__'))
for name in ('libEGL_vulpes_hybris.so', 'manifest.json'):
shutil.copy2(args.product/'trial/graphics'/name, product/'trial/graphics'/name)
for name in ('home/droidian', 'etc/dbus-1', 'reference'):
(root/name).mkdir(parents=True, exist_ok=True)
shutil.copy2(android, root/'reference/android-rootfs.img')
module('native', ROOT/'runtime/prepare-native.py').prepare(root)
shutil.copy2(ROOT/'droidian/initialize-runtime.sh', root/'etc/tundra/initialize-runtime.sh')
# Caches are architecture dependent; generate them with the target binaries.
(root/'etc/systemd/system/tundra-initialize.service').write_text('''[Unit]
Description=Initialize Tundra runtime caches
Before=tundra-hal.service tundra-display.service
[Service]
Type=oneshot
ExecStart=/bin/sh /etc/tundra/initialize-runtime.sh
RemainAfterExit=yes
''')
for service in ('tundra-hal', 'tundra-display'):
folder = root/f'etc/systemd/system/{service}.service.d'
folder.mkdir(exist_ok=True)
(folder/'initialize.conf').write_text('[Unit]\nRequires=tundra-initialize.service\nAfter=tundra-initialize.service\n')
image = output/'tundra-sargo-system.ext4'
with image.open('xb') as stream:
stream.truncate(3*1024**3)
subprocess.run(['fakeroot', '--', sys.executable, str(Path(__file__).resolve()),
'--output', str(output), '--pack', str(root), str(image)], check=True)
check = subprocess.run(['e2fsck', '-f', '-n', str(image)], capture_output=True, text=True)
(output/'filesystem-check.txt').write_text(check.stdout+check.stderr)
if check.returncode:
raise RuntimeError('System image failed filesystem verification')
manifest = {'schema': 1, 'device': 'sargo', 'image': image.name,
'imageSha256': digest(image), 'bytes': image.stat().st_size,
'androidSha256': ANDROID_SHA, 'engine': lock,
'release': json.loads((product/'release.json').read_text()),
'staging': staging, 'filesystemCheckPassed': True,
'flashReady': False, 'hardwareBootTested': False,
'scope': 'Self-contained userspace; vendor/firmware partitions still required; RAM boot candidate',
'sourceHashes': {str(p.relative_to(PROJECT)): digest(p) for p in
[Path(__file__).resolve(), ROOT/'tools/stage-runtime.py',
*sorted((ROOT/'runtime').glob('*.py'))]}}
(output/'system.json').write_text(json.dumps(manifest, indent=2)+'\n')
print(image, manifest['imageSha256'], flush=True)
if __name__ == '__main__':
main()