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

103
tundra/tools/run-vm.py Normal file
View file

@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Boot Tundra's graphical guest; no NIC, no host shares, disposable disk overlay."""
import argparse
import fcntl
import json
import hashlib
import os
from pathlib import Path
import shutil
import socket
import subprocess
import tempfile
import time
ROOT=Path(__file__).resolve().parents[1]
OUT=ROOT/'out/graphical-vm'
LOG=ROOT/'logs'
def qmp(path, command, arguments=None):
with socket.socket(socket.AF_UNIX) as sock:
sock.settimeout(5)
sock.connect(str(path))
stream=sock.makefile('rwb',buffering=0)
json.loads(stream.readline())
stream.write(b'{"execute":"qmp_capabilities"}\n')
while 'return' not in json.loads(stream.readline()): pass
stream.write((json.dumps({'execute':command,'arguments':arguments or {}})+'\n').encode())
while True:
result=json.loads(stream.readline())
if 'error' in result: raise RuntimeError(result)
if 'return' in result: return result['return']
def main():
parser=argparse.ArgumentParser()
parser.add_argument('--test',action='store_true',help='Headless real DRM display, guest tests and screenshot')
parser.add_argument('--usb-test',action='store_true',help='Also test read-only FunctionFS on dummy_hcd inside the VM')
parser.add_argument('--show',action='store_true',help='Show the QEMU window while running the automated test')
parser.add_argument('--memory',type=int,default=3072)
args=parser.parse_args()
for name in ('root.ext4','vmlinuz','initramfs.cpio.gz','build.json'):
if not (OUT/name).is_file(): raise SystemExit('Build first: python3 tundra/tools/build-vm.py')
LOG.mkdir(exist_ok=True)
label='graphical-vm' if args.test else 'graphical-vm-session'
with (LOG/'graphical-vm.lock').open('w') as guard, tempfile.TemporaryDirectory(prefix='tundra-qmp-') as temp:
fcntl.flock(guard,fcntl.LOCK_EX|fcntl.LOCK_NB)
qmp_path=Path(temp)/'qmp.sock'
serial=LOG/(label+'.log')
serial.write_text('')
command=['qemu-system-x86_64','-enable-kvm','-cpu','host','-smp','2','-m',str(args.memory),
'-kernel',str(OUT/'vmlinuz'),'-initrd',str(OUT/'initramfs.cpio.gz'),
'-append','console=ttyS0 rdinit=/init panic=-1'+(' tundra.test=1' if args.test else '')+(' tundra.usb=1' if args.usb_test else ''),
'-drive',f'file={OUT}/root.ext4,format=raw,if=virtio,snapshot=on',
'-net','none','-device','virtio-vga,xres=600,yres=960','-vga','none',
'-device','virtio-keyboard-pci','-device','virtio-tablet-pci',
'-display','none' if args.test and not args.show else 'gtk,zoom-to-fit=off',
'-serial',f'file:{serial}','-qmp',f'unix:{qmp_path},server=on,wait=off','-no-reboot']
captured=False
clicked=False
print(f'Tundra VM — {args.memory} MiB RAM, disque de session jetable, réseau local uniquement.',flush=True)
proc=subprocess.Popen(command)
deadline=time.monotonic()+240 if args.test else float('inf')
try:
while proc.poll() is None:
log=serial.read_text(errors='replace')
if 'TUNDRA_POINTER_HOME' in log and not clicked:
qmp(qmp_path,'screendump',{'filename':str(LOG/(label+'-clock.ppm'))})
qmp(qmp_path,'input-send-event',{'events':[
{'type':'abs','data':{'axis':'x','value':16384}},
{'type':'abs','data':{'axis':'y','value':32085}}]})
time.sleep(.5)
qmp(qmp_path,'input-send-event',{'events':[{'type':'btn','data':{'button':'left','down':True}}]})
time.sleep(.1)
qmp(qmp_path,'input-send-event',{'events':[{'type':'btn','data':{'button':'left','down':False}}]})
clicked=True
if 'TUNDRA_GUI_READY' in log and not captured:
qmp(qmp_path,'screendump',{'filename':str(LOG/(label+'.ppm'))})
captured=True
print('Gaia guest checks passed; screenshot captured.',flush=True)
if time.monotonic()>deadline:
try: qmp(qmp_path,'screendump',{'filename':str(LOG/(label+'-failed.ppm'))})
except Exception: pass
raise TimeoutError(f'Guest did not finish; see {serial}')
time.sleep(1)
finally:
if proc.poll() is None:
proc.terminate()
try: proc.wait(timeout=10)
except subprocess.TimeoutExpired: proc.kill(); proc.wait()
text=serial.read_text(errors='replace')
reports=[line.removeprefix('TUNDRA_GUI_TEST ') for line in text.splitlines() if line.startswith('TUNDRA_GUI_TEST ')]
decoded=[json.loads(item) for item in reports]
report=next((item for item in decoded if 'uid' in item), decoded[-1] if decoded else {'passed':False,'error':'No guest test result'})
usb_reports=[json.loads(line.removeprefix('TUNDRA_USB_TEST ')) for line in text.splitlines() if line.startswith('TUNDRA_USB_TEST ')]
if args.usb_test:
report['usb']=usb_reports[-1] if usb_reports else {'passed':False,'error':'No USB result'}
report['passed']=report['passed'] and report['usb']['passed']
report['buildManifestSha256']=hashlib.sha256((OUT/'build.json').read_bytes()).hexdigest()
report.update(screenshot=captured, qemuExit=proc.returncode, scope='x86_64 VM only; no Pixel/HAL test')
(LOG/(label+'-test.json')).write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report,indent=2))
if args.test and not (report['passed'] and captured and proc.returncode==0): raise SystemExit(1)
if __name__=='__main__': main()