Release Vulpes OS 2.7-preview.24

This commit is contained in:
OverSu 2026-11-04 12:52:20 +01:00
commit 97ef99f827
135 changed files with 7717 additions and 2524 deletions

View file

@ -2,6 +2,7 @@
# SPDX-License-Identifier: MPL-2.0
"""Synchronize the shared host and run a bounded private Nokia Gaia trial."""
import argparse
import hashlib
import json
from pathlib import Path
import shutil
@ -15,6 +16,26 @@ OUT=ROOT/'tundra/out/beatles/gecko-156.0.1'
REMOTE='/data/local/vulpes-beatles'
def compile_if_changed(command, output, sources, signature):
"""Avoid relinking the same ARM tools on every bounded trial."""
stamp = output.with_name(output.name + '.build.json')
inputs = []
for path in sources:
stat = path.stat()
digest = hashlib.sha256(path.read_bytes()).hexdigest()
inputs.append({'path':str(path), 'size':stat.st_size, 'sha256':digest})
record = {'command':command,'signature':signature, 'inputs':inputs}
if output.is_file() and stamp.is_file():
try:
if json.loads(stamp.read_text()) == record:
print('Using cached ARM tool:', output.name, flush=True)
return
except (OSError, ValueError):
pass
subprocess.run(command, check=True)
stamp.write_text(json.dumps(record, sort_keys=True)+'\n')
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--interactive',action='store_true',help='Manual session, no automatic keypad actions; KaiOS returns within 30 minutes')
@ -23,17 +44,46 @@ def main():
parser.add_argument('--phone',action='store_true',help='Enable Nokia radio, Wi-Fi, SMS/call and hardware control brokers')
parser.add_argument('--camera-probe',action='store_true',help='Run the bounded Qualcomm HAL preview probe before Gaia starts')
parser.add_argument('--camera-preview-binary',type=Path,help='Use a private QCamera preview-dump binary for a bounded RAM-only trial')
parser.add_argument('--camera-service-probe',action='store_true',help='Test Android Camera1/CameraService as the camera system UID in a bounded RAM-only trial')
parser.add_argument('--camera-service-probe-binary',type=Path,help='Private Android API 23 Camera1 diagnostic binary')
parser.add_argument('--camera-gl-probe',action='store_true',help='Test Camera1 through EGL/GLConsumer in a bounded RAM-only trial')
parser.add_argument('--camera-gl-probe-binary',type=Path,help='Private Android API 23 EGL/GLConsumer diagnostic binary')
parser.add_argument('--camera-permission-bridge-binary',type=Path,help='Temporary camera-only Android API 23 permission controller')
parser.add_argument('--camera-permission-bridge-runtime',type=Path,help='Android API 23 C++ runtime required by the permission bridge')
parser.add_argument('--camera-broker',action='store_true',help='Run the fixed-command CameraService bridge under UID 1006 during Gaia qualification')
parser.add_argument('--no-camera-broker',action='store_true',help='Disable the CameraService bridge for a non-camera session')
parser.add_argument('--camera-broker-worker',type=Path,help='Private Android API 23 persistent Camera1 worker binary')
parser.add_argument('--camera-broker-binary',type=Path,help='Private Android API 23 local camera broker binary')
parser.add_argument('--display-session-binary',type=Path,help='Use a prebuilt compatible session supervisor')
args=parser.parse_args()
session_option=(('--phone-' if args.phone else '--interactive-')+str(args.minutes*60)+'s') if args.interactive else ('--phone-180s' if args.phone else '--temporary-90s' if (args.camera_probe or args.camera_preview_binary) else '--temporary-180s')
camera_diagnostics=(args.camera_probe, args.camera_preview_binary is not None,
args.camera_service_probe, args.camera_gl_probe)
camera_broker=args.camera_broker or (not args.no_camera_broker and not any(camera_diagnostics))
camera_modes=sum(camera_diagnostics)+(args.camera_broker is True)
if camera_modes > 1:
parser.error('Choose one camera probe per RAM trial; all use the same sensor')
session_option=(('--phone-' if args.phone else '--interactive-')+str(args.minutes*60)+'s') if args.interactive else ('--phone-180s' if args.phone else '--temporary-90s' if (args.camera_probe or args.camera_preview_binary or args.camera_service_probe or args.camera_gl_probe) else '--temporary-240s')
rows=subprocess.check_output(['adb','devices','-l'],text=True).splitlines()
devices=[r.split()[0] for r in rows if 'model:Nokia_2720_Flip' in r.split() and r.split()[1]=='device']
if len(devices)!=1:raise RuntimeError('Exactly one connected Nokia required')
adb=['adb','-s',devices[0]]
if subprocess.check_output(adb+['shell','getprop ro.product.codename'],text=True).strip()!='beatles':
raise RuntimeError('Unexpected device')
# A previous interrupted run must never make an old frame or log appear
# to belong to this trial. These paths are private diagnostics only.
stale = [REMOTE+'/camera-service-probe-once', REMOTE+'/camera-service-probe.log',
REMOTE+'/camera-gl-probe-once', REMOTE+'/camera-gl-probe.log',
REMOTE+'/camera-permission-bridge.log', REMOTE+'/qcamera-probe-once',
REMOTE+'/camera-broker-once',REMOTE+'/camera-broker.log',REMOTE+'/camera-ipc/camera.sock',
REMOTE+'/qcamera-probe.log', REMOTE+'/qcamera-preview-dump-once',
REMOTE+'/qcamera-preview-dump', REMOTE+'/rootfs/work/camera-frame.yuv',
REMOTE+'/rootfs/work/camera-photo.jpg']
cleanup=subprocess.run(adb+['shell','rm -f '+' '.join(stale)],capture_output=True,text=True)
if cleanup.returncode:
print('Could not clear every prior camera artifact:',cleanup.stderr.strip(),flush=True)
if not args.interactive:
lid=subprocess.check_output(adb+['shell','cat /sys/devices/soc.0/hall_sensor1.73/hall_status'],text=True).strip()
if lid=='1' and not (args.camera_probe or args.camera_preview_binary):
if lid=='1' and not (args.camera_probe or args.camera_preview_binary or args.camera_service_probe or args.camera_gl_probe):
raise RuntimeError('Open the Nokia flip before running keypad qualification; no session started')
ini=subprocess.check_output(adb+['shell','cat '+REMOTE+'/rootfs/opt/firefox/application.ini'],text=True)
if 'Version=156.0.1' not in ini.splitlines():raise RuntimeError('Stage Gecko 156.0.1 before launching')
@ -45,12 +95,25 @@ def main():
else:
subprocess.run(adb+['shell','rm -f '+REMOTE+'/rootfs/work/cover-animation.rgb565'],check=True,capture_output=True)
sysroot=OUT/'sysroot/rootfs'
compiler=[str(OUT/'mozbuild/clang/bin/clang'),'--target=arm-linux-gnueabihf',
'--sysroot='+str(sysroot),'--gcc-install-dir='+str(sysroot/'usr/lib/gcc/arm-linux-gnueabihf/12'),
'-fuse-ld=lld','-march=armv7-a','-mfpu=neon','-mfloat-abi=hard','-O2','-Wall','-Wextra','-Werror']
subprocess.run(compiler+['-static',str(ROOT/'tundra/probes/beatles-control-client.c'),'-o',str(OUT/'control-client')],check=True)
subprocess.run(compiler+['-static',str(ROOT/'tundra/probes/beatles-display-session.c'),'-o',str(OUT/'display-session')],check=True)
subprocess.run(compiler+[str(ROOT/'tundra/probes/beatles-keypad.c'),'-lX11','-lXtst','-o',str(OUT/'keypad')],check=True)
display_session=OUT/'display-session'
if args.display_session_binary:
if not args.display_session_binary.is_file():raise RuntimeError('Prebuilt session supervisor not found')
display_session=args.display_session_binary
else:
compiler=[str(OUT/'mozbuild/clang/bin/clang'),'--target=arm-linux-gnueabihf',
'--sysroot='+str(sysroot),'--gcc-install-dir='+str(sysroot/'usr/lib/gcc/arm-linux-gnueabihf/12'),
'-fuse-ld=lld','-march=armv7-a','-mfpu=neon','-mfloat-abi=hard','-O2','-Wall','-Wextra','-Werror']
compiler_stat=Path(compiler[0]).stat()
runtime_lock=sysroot.parent/'runtime-lock.json'
signature={'command':compiler,'compilerSize':compiler_stat.st_size,
'compilerMtimeNs':compiler_stat.st_mtime_ns,
'runtimeLockSha256':hashlib.sha256(runtime_lock.read_bytes()).hexdigest()}
compile_if_changed(compiler+['-static',str(ROOT/'tundra/probes/beatles-control-client.c'),'-o',str(OUT/'control-client')],
OUT/'control-client',[ROOT/'tundra/probes/beatles-control-client.c'],signature)
compile_if_changed(compiler+['-static',str(ROOT/'tundra/probes/beatles-display-session.c'),'-o',str(OUT/'display-session')],
OUT/'display-session',[ROOT/'tundra/probes/beatles-display-session.c'],signature)
compile_if_changed(compiler+[str(ROOT/'tundra/probes/beatles-keypad.c'),'-lX11','-lXtst','-o',str(OUT/'keypad')],
OUT/'keypad',[ROOT/'tundra/probes/beatles-keypad.c'],signature)
(OUT/'gaia-test.sh').write_text('exec /usr/bin/python3.11 /work/gaia-session.py\n')
dirs=' '.join(REMOTE+'/rootfs/'+d for d in ('opt/firefox/vulpes/tests','opt/firefox/defaults/pref','work','proc','sys','dev','tmp','run'))
subprocess.run(adb+['shell','mkdir -p '+dirs+'; chmod 1777 '+REMOTE+'/rootfs/tmp; rm -f '+REMOTE+'/display-session.done '+REMOTE+'/rootfs/work/gaia-audit.json'],capture_output=True,check=True)
@ -65,6 +128,57 @@ def main():
subprocess.run(adb+['shell','rm -f '+REMOTE+'/qcamera-preview-dump '+REMOTE+'/qcamera-preview-dump-once '+REMOTE+'/qcamera-probe.log '+REMOTE+'/rootfs/work/camera-frame.yuv '+REMOTE+'/rootfs/work/camera-photo.jpg'],check=True,capture_output=True)
subprocess.run(adb+['push',str(args.camera_preview_binary),REMOTE+'/qcamera-preview-dump'],check=True,capture_output=True)
subprocess.run(adb+['shell','chmod 755 '+REMOTE+'/qcamera-preview-dump; touch '+REMOTE+'/qcamera-preview-dump-once'],check=True,capture_output=True)
if args.camera_service_probe:
if not args.camera_service_probe_binary or not args.camera_service_probe_binary.is_file():
raise RuntimeError('--camera-service-probe requires --camera-service-probe-binary')
if not args.camera_permission_bridge_binary or not args.camera_permission_bridge_binary.is_file():
raise RuntimeError('--camera-service-probe requires --camera-permission-bridge-binary')
if not args.camera_permission_bridge_runtime or not args.camera_permission_bridge_runtime.is_file():
raise RuntimeError('--camera-service-probe requires --camera-permission-bridge-runtime')
probe='/data/local/tmp/vulpes-camera-probe-20261023'
subprocess.run(adb+['push',str(args.camera_service_probe_binary),probe],check=True,capture_output=True)
bridge='/data/local/tmp/vulpes-camera-permission-bridge'
runtime='/data/local/tmp/libc++_shared.so'
subprocess.run(adb+['push',str(args.camera_permission_bridge_binary),bridge],check=True,capture_output=True)
subprocess.run(adb+['push',str(args.camera_permission_bridge_runtime),runtime],check=True,capture_output=True)
subprocess.run(adb+['shell','chmod 755 '+probe+' '+bridge+' '+runtime+'; rm -f '+REMOTE+'/camera-permission-bridge.log; touch '+REMOTE+'/camera-service-probe-once'],check=True,capture_output=True)
if args.camera_gl_probe:
if not args.camera_gl_probe_binary or not args.camera_gl_probe_binary.is_file():
raise RuntimeError('--camera-gl-probe requires --camera-gl-probe-binary')
if not args.camera_permission_bridge_binary or not args.camera_permission_bridge_binary.is_file():
raise RuntimeError('--camera-gl-probe requires --camera-permission-bridge-binary')
if not args.camera_permission_bridge_runtime or not args.camera_permission_bridge_runtime.is_file():
raise RuntimeError('--camera-gl-probe requires --camera-permission-bridge-runtime')
probe='/data/local/tmp/vulpes-camera-gl-probe-20261023'
subprocess.run(adb+['push',str(args.camera_gl_probe_binary),probe],check=True,capture_output=True)
bridge='/data/local/tmp/vulpes-camera-permission-bridge'
runtime='/data/local/tmp/libc++_shared.so'
subprocess.run(adb+['push',str(args.camera_permission_bridge_binary),bridge],check=True,capture_output=True)
subprocess.run(adb+['push',str(args.camera_permission_bridge_runtime),runtime],check=True,capture_output=True)
subprocess.run(adb+['shell','chmod 755 '+probe+' '+bridge+' '+runtime+'; rm -f '+REMOTE+'/camera-permission-bridge.log; touch '+REMOTE+'/camera-gl-probe-once'],check=True,capture_output=True)
if camera_broker:
subprocess.run([sys.executable,str(ROOT/'tundra/tools/build-beatles-camera.py')],check=True)
broker_build=OUT/'camera-broker-build'
worker_path=args.camera_broker_worker or broker_build/'beatles-camera-worker'
broker_path=args.camera_broker_binary or broker_build/'beatles-camera-broker'
permission_bridge_path=args.camera_permission_bridge_binary or OUT/'camera-probe-runtime/beatles-camera-permission-bridge'
permission_runtime_path=args.camera_permission_bridge_runtime or OUT/'camera-probe-runtime/libc++_shared.so'
if not worker_path.is_file():
raise RuntimeError('CameraService worker binary not found: '+str(worker_path))
if not broker_path.is_file():
raise RuntimeError('CameraService broker binary not found: '+str(broker_path))
if not permission_bridge_path.is_file():
raise RuntimeError('Camera permission bridge binary not found: '+str(permission_bridge_path))
if not permission_runtime_path.is_file():
raise RuntimeError('Camera permission bridge runtime not found: '+str(permission_runtime_path))
broker_worker='/data/local/tmp/vulpes-beatles-camera-worker'
broker='/data/local/tmp/vulpes-beatles-camera-broker'
bridge='/data/local/tmp/vulpes-camera-permission-bridge'
runtime='/data/local/tmp/libc++_shared.so'
for source,target in ((worker_path,broker_worker),(broker_path,broker),
(permission_bridge_path,bridge),(permission_runtime_path,runtime)):
subprocess.run(adb+['push',str(source),target],check=True,capture_output=True)
subprocess.run(adb+['shell','chmod 755 '+broker_worker+' '+broker+' '+bridge+' '+runtime+'; mkdir -p '+REMOTE+'/camera-ipc; rm -f '+REMOTE+'/camera-ipc/camera.sock '+REMOTE+'/camera-permission-bridge.log '+REMOTE+'/camera-broker.log; touch '+REMOTE+'/camera-broker-once'],check=True,capture_output=True)
subprocess.run(adb+['push',str(ROOT/'tundra/devices/beatles/t9'),REMOTE+'/rootfs/opt/vulpes/tundra/devices/beatles/'],check=True,capture_output=True,timeout=120)
# Seed the isolated Gaia media store with small, known-good playback samples.
media=REMOTE+'/rootfs/work/gaia-profile/vulpes-media'
@ -86,7 +200,9 @@ def main():
for directory in ('host','services','adapters'):
subprocess.run(adb+['push',str(ROOT/directory),REMOTE+'/rootfs/opt/firefox/vulpes/'],check=True,capture_output=True,timeout=120)
subprocess.run(adb+['push',str(ROOT/'host'),REMOTE+'/rootfs/opt/vulpes/'],check=True,capture_output=True,timeout=120)
files=((OUT/'control-client','rootfs/work/control-client'),(OUT/'display-session','display-session'),(OUT/'keypad','rootfs/work/keypad'),
subprocess.run(adb+['shell','mkdir -p '+REMOTE+'/rootfs/opt/vulpes/tundra/runtime'],check=True,capture_output=True)
subprocess.run(adb+['push',str(ROOT/'tundra/runtime/beatles-camera-client.py'),REMOTE+'/rootfs/opt/vulpes/tundra/runtime/beatles-camera-client.py'],check=True,capture_output=True)
files=((OUT/'control-client','rootfs/work/control-client'),(display_session,'display-session'),(OUT/'keypad','rootfs/work/keypad'),
(OUT/'gaia-test.sh','rootfs/work/display-test.sh'),
(ROOT/'tundra/devices/beatles/gaia-session.py','rootfs/work/gaia-session.py'),
(ROOT/'tundra/devices/beatles/cover-display.py','rootfs/work/cover-display.py'),
@ -113,6 +229,9 @@ def main():
# Its restoration marker must not be mistaken for this trial.
subprocess.run(adb+['shell','rm -f '+REMOTE+'/display-session.done'],check=True,capture_output=True)
script='''(function(){var Cc=Components.classes,Ci=Components.interfaces;
var b=Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
b.setBoolPref("devtools.debugger.remote-enabled",true);
Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).savePrefFile(null);
var f=Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);f.initWithPath(%s);
var p=Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);p.init(f);
p.runAsync([%s],1,null,false);return "started";})()''' % (json.dumps(REMOTE+'/display-session'),json.dumps(session_option))
@ -141,7 +260,7 @@ def main():
time.sleep(1)
subprocess.run(adb+['shell','touch '+REMOTE+'/stop-session'],check=True,capture_output=True)
raise RuntimeError('Manual session startup timed out; requested KaiOS restoration')
for second in range(205):
for second in range(265):
state=subprocess.check_output(adb+['shell','cat '+REMOTE+'/display-session.done'],text=True).strip()
if state=='restored':break
if second and second%30==0:print('Trial running:',second,'seconds',flush=True)
@ -149,12 +268,19 @@ def main():
else:raise RuntimeError('No restoration marker; inspect the watchdog before retrying')
report=OUT/('gaia-trial-'+time.strftime('%Y%m%d-%H%M%S'))
report.mkdir()
for relative in ('display-session.log','qcamera-probe.log','framebuffer.ppm','rootfs/work/camera-frame.yuv','rootfs/work/camera-photo.jpg','rootfs/work/Xorg.log','rootfs/work/xorg-console.log',
for relative in ('display-session.log','qcamera-probe.log','camera-service-probe.log','camera-gl-probe.log','camera-permission-bridge.log','camera-broker.log','framebuffer.ppm','rootfs/work/camera-frame.yuv','rootfs/work/camera-photo.jpg','rootfs/work/Xorg.log','rootfs/work/xorg-console.log',
'rootfs/run/tundra-hardware/status.json','rootfs/work/radio-status.log',
'rootfs/work/cover-display.log','rootfs/work/cover-preview.png','rootfs/work/cover-refresh.json','rootfs/work/lid-state.json',
'rootfs/work/keypad.log','rootfs/work/gecko.log','rootfs/work/gaia-server.log',
'rootfs/work/gaia-audit.json','rootfs/work/gaia-native.png','rootfs/work/memory-samples.jsonl'):
subprocess.run(adb+['pull',REMOTE+'/'+relative,str(report/Path(relative).name)],capture_output=True)
if args.camera_gl_probe:
for remote, name in (('/data/local/tmp/vulpes-camera-glconsumer.ppm','camera-glconsumer.ppm'),
('/data/local/tmp/vulpes-camera-gonkqueue.nv21','camera-gonkqueue.nv21'),
('/data/local/tmp/vulpes-camera-gonkqueue.txt','camera-gonkqueue.txt'),
('/data/local/tmp/vulpes-camera-callback.yuv','camera-callback.yuv'),
('/data/local/tmp/vulpes-camera-callback.jpg','camera-callback.jpg')):
subprocess.run(adb+['pull',remote,str(report/name)],capture_output=True)
service=subprocess.check_output(adb+['shell','getprop init.svc.b2g'],text=True).strip()
print('KaiOS service:',service,'Report:',report,flush=True)
# Keep both outputs from the bounded Qualcomm probe. Its still-snapshot
@ -170,6 +296,10 @@ def main():
if service!='running':raise RuntimeError('KaiOS did not restart')
if args.camera_preview_binary:
subprocess.run(adb+['shell','rm -f '+REMOTE+'/qcamera-preview-dump '+REMOTE+'/qcamera-preview-dump-once'],capture_output=True)
if args.camera_service_probe:
subprocess.run(adb+['shell','rm -f /data/local/tmp/vulpes-camera-probe-20261023 /data/local/tmp/vulpes-camera-permission-bridge /data/local/tmp/libc++_shared.so /data/local/tmp/vulpes-camera-frame.nv21'],capture_output=True)
if args.camera_gl_probe:
subprocess.run(adb+['shell','rm -f /data/local/tmp/vulpes-camera-gl-probe-20261023 /data/local/tmp/vulpes-camera-permission-bridge /data/local/tmp/libc++_shared.so'],capture_output=True)
if not (report/'gaia-audit.json').is_file():raise RuntimeError('Gaia audit did not complete')
audit=json.loads((report/'gaia-audit.json').read_text())
summary={key:value for key,value in audit.items() if key not in ('icons','layout')}