"""Prepare the audited Windows runtime from a pinned APK and rebuilt replacements. The donor was built from a CI merge, not just the PR head. Only unchanged components are reused. Session helpers come from this checkout, libblsession must be rebuilt for all three architectures, Gamescope comes from the current checksum-pinned release, and the VR components are rebuilt in Docker (or reused from a verified build manifest). Store engines are omitted (build with skipRust). """ import argparse import hashlib import io import json from pathlib import Path, PurePosixPath import shutil import struct import subprocess import sys import tarfile import tempfile from zipfile import ZipFile ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / 'tools/release')) import runtime_inputs as runtime from check_session_assets import source_assets sys.path.insert(0, str(ROOT / 'tools/vr')) import build_windows as vr_build DONOR_SHA256 = 'b7efef6442f384446bdd1750cec21f0eda61b370bc21d9f55187448e5c256760' DONOR_PR_SOURCE = 'bdf876d47cc9bb6fa810d014370da5bbb3c28039' DONOR_MAIN_SOURCE = '46fac69438ed428f2ca43ea935cd47790264f380' DONOR_TREE = 'ddbca4d572b809fb969be0cf80ef83d6ef6580ce' DONOR_REVISION = 'cf24c5f2c771633ea426046006c2b977c7bd7d32' DONOR_CERT_SHA256 = 'b241ea7d77822d39b89d3227e06f3ecf62adec6cbfd38ad06196787991b5d3f0' DONOR_URL = 'https://github.com/Droid-Deck/DroidDeck-CI/releases/download/pr-732/DroidDeck-PR.732-bdf876d.apk' AUDITED_SOURCE = '1dc66bf9be64a404c80400049c0803614814461b' LOCAL_BUILD_PLUMBING = {'app/build.gradle', 'tools/release/runtime_inputs.py'} # These Android-only inputs are compiled by Gradle, never taken from the donor. # Keep this list explicit so unrelated native or Linux runtime edits still fail. LOCAL_ANDROID_SOURCES = {'app/src/main/cpp/CMakeLists.txt', 'app/src/main/cpp/viture/sdk_bridge.cpp', 'app/src/main/cpp/viture/display_modes.h', 'app/src/main/cpp/viture/stereo_frame_gate.h', 'app/src/main/cpp/viture/uvc_bridge.cpp', 'app/src/main/cpp/waylandcomp/src/cardboard.c', 'app/src/main/cpp/waylandcomp/src/cardboard.h', 'app/src/main/cpp/waylandcomp/src/cardboard.frag', 'app/src/main/cpp/waylandcomp/src/cardboard_frag.h', 'app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c', 'app/src/main/cpp/waylandcomp/src/spatialpose.c', 'app/src/main/cpp/waylandcomp/CMakeLists.txt'} # These audited helper updates are copied from this checkout by runtime.record(). # Permit them only while source_assets() still guarantees that staging path. LOCAL_SESSION_HELPERS = {'tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat', 'tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr'} VR_PREFIX = 'assets/linuxfs/usr/local/lib/droiddeck-vr/' CHECK_PATHS = list(dict.fromkeys(runtime.SOURCE_PATHS + [ 'app/src/main/cpp', 'app/src/main/jniLibs', 'app/src/main/rust/stores', 'tools/vr'])) REBUILT = {'libblsession.so': 183, 'x86_64/libblsession.so': 62, 'i386/libblsession.so': 3} REBUILT_SHA256 = { 'libblsession.so': '1093ce33ca6d0a17a2eec7b3a3cc53969b01ffc459aef16b320cd3857825b738', 'x86_64/libblsession.so': 'd0a558fee1ac4d5b898170a96b03e15610d9835e7097425027b8c31728da870a', 'i386/libblsession.so': '785d8d9bf1fa20d083a7fed65d064c7580684d7065530d95a648e2d32901d2a2', } JNI = ('libproot.so', 'libproot-loader.so', 'libdirectaudiorelay.so') PREPARED = ('pulseaudio.tzst', 'graphics_driver/adrenotools-turnip25.1.0-quest1.tzst', 'graphics_driver/adrenotools-turnip26.3.0-a6xx.tzst', 'graphics_driver/adrenotools-turnip26.3.0-quest2.tzst') def git(*args): return subprocess.check_output(['git', '-C', str(ROOT), *args]) def changed(base, target=None): args = ['diff', '--no-ext-diff', '--ignore-cr-at-eol', '--name-only', '-z', base] if target: args.append(target) return set(filter(None, git(*args, '--', *CHECK_PATHS).decode().split('\0'))) def check_sources(): # A branch label alone is insufficient: CI merged this main commit into it. if git('merge-tree', '--write-tree', DONOR_PR_SOURCE, DONOR_MAIN_SOURCE).decode().splitlines()[0] != DONOR_TREE: raise ValueError('The donor CI merge does not match the audited source tree') helper_sources = {path.relative_to(ROOT).as_posix() for path in source_assets()} reviewed = LOCAL_BUILD_PLUMBING | LOCAL_ANDROID_SOURCES | (LOCAL_SESSION_HELPERS & helper_sources) unexpected = {p for p in changed(AUDITED_SOURCE, 'HEAD') - reviewed if not vr_build.is_source(p)} if unexpected: raise ValueError('Sources changed since the component audit:\n' + '\n'.join(sorted(unexpected))) local = changed('HEAD') untracked = set(filter(None, git('ls-files', '--others', '--exclude-standard', '-z', '--', *CHECK_PATHS).decode().split('\0'))) unexpected = {p for p in (local - reviewed) | (untracked - LOCAL_ANDROID_SOURCES) if not vr_build.is_source(p)} if unexpected: raise ValueError('Unreviewed runtime/native/Rust changes:\n' + '\n'.join(sorted(unexpected))) classifications = {} for path in sorted(changed(DONOR_TREE, 'HEAD') | local | untracked): if vr_build.is_source(path): action = 'VR component rebuilt from current source and verified; never imported from donor' elif path in LOCAL_ANDROID_SOURCES: action = 'Android native source compiled by Gradle; not imported from donor' elif path.startswith('app/src/main/rust/stores/'): action = 'store engines omitted; Gradle must use skipRust=true' elif path in helper_sources: action = 'restaged from current source with LF line endings' elif path in {'tools/linuxfs/preload/net.c', 'tools/linuxfs/preload/webhelper.c'}: action = 'all three libblsession libraries rebuilt from current source' elif path == 'tools/gamescope/release.env': action = 'current checksum-pinned Gamescope archive replaces donor' elif path in {'tools/build_local.sh', 'tools/directaudio/fetch.sh'}: action = 'audited download/build cache plumbing; component inputs unchanged' elif path == 'app/src/main/cpp/waylandcomp/src/compositor.c': action = 'Android native library rebuilt by Gradle; not imported' elif path in LOCAL_BUILD_PLUMBING: action = 'local Windows build plumbing; changes recorded below' else: raise ValueError('Unclassified donor source difference: ' + path) classifications[path] = action return classifications def validate_relative(name): path = PurePosixPath(name) if path.is_absolute() or not path.parts or any(part in {'.', '..'} or ':' in part for part in path.parts): raise ValueError('Unsafe archive member: ' + name) if '\\' in name: raise ValueError('Backslash in archive member: ' + name) return path def checked_output(relative): validate_relative(relative) target = (ROOT / relative).resolve() if not target.is_relative_to(ROOT.resolve()): raise ValueError('Output escapes the checkout: ' + relative) if git('ls-files', '--', relative).strip(): raise ValueError('Refusing to overwrite tracked file: ' + relative) return target def read_gamescope(path): pins = dict(line.split('=', 1) for line in (ROOT / 'tools/gamescope/release.env').read_text().splitlines() if line and not line.startswith('#')) if runtime.digest(path) != pins['GAMESCOPE_SHA256']: raise ValueError('Gamescope archive checksum does not match tools/gamescope/release.env') data = subprocess.run(['zstd', '-dc', str(path)], capture_output=True, check=True).stdout files = {} with tarfile.open(fileobj=io.BytesIO(data)) as archive: for member in archive: name = member.name.removeprefix('./') validate_relative(name) if member.isdir(): continue if not member.isfile(): raise ValueError('Gamescope archive contains a non-regular member: ' + name) if name in files: raise ValueError('Duplicate Gamescope archive member: ' + name) files[name] = archive.extractfile(member).read() if set(files) != {'usr/local/bin/gamescope'}: raise ValueError('Unexpected Gamescope archive payload: ' + ', '.join(files)) return files, pins def validate_rebuilt(path, machine): data = path.read_bytes() if len(data) < 20 or data[:4] != b'\x7fELF' or data[5] != 1 or struct.unpack('