mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-27 06:01:49 +01:00
115 lines
5.5 KiB
Python
115 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Run every managed backend shim in native, isolated, published Decky 3.3.0.
|
|
|
|
Downloads a digest-pinned loader, or accepts --loader for an offline cached copy.
|
|
Only image build/download use network. All official plugin execution is in
|
|
network/device/session-isolated disposable Docker containers. Never uses Frame.
|
|
"""
|
|
import argparse
|
|
import ast
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import urllib.request
|
|
import uuid
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
RELEASE = 'v3.3.0'
|
|
LOADERS = {
|
|
'x86_64': ('PluginLoader', 'e80d03e641d1766a33a817cb27ab8d2e0a4633685e5d27e4de948a6687526181', 62),
|
|
'aarch64': ('PluginLoader-arm64', '8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6', 183),
|
|
}
|
|
|
|
|
|
def check_coverage():
|
|
"""A new production shim must declare an actual-runtime probe before CI runs."""
|
|
sys.path.insert(0, str(ROOT/'ui'))
|
|
import frame_decky_store
|
|
tree = ast.parse(frame_decky_store.APPLY)
|
|
shims = set()
|
|
for node in ast.walk(tree):
|
|
if (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)
|
|
and isinstance(node.func.value, ast.Name)
|
|
and node.func.value.id == 'compat_shims' and node.func.attr == 'append'):
|
|
if len(node.args) != 1 or not isinstance(node.args[0], ast.Constant) or not isinstance(node.args[0].value, str):
|
|
raise ValueError('Managed shims must declare a literal name for runtime test coverage')
|
|
shims.add(node.args[0].value)
|
|
cases = json.loads((ROOT/'tests/decky-runtime/cases.json').read_text())
|
|
if shims != {case['shim'] for case in cases}:
|
|
raise ValueError('Every managed shim needs an actual Decky runtime case')
|
|
return cases
|
|
|
|
|
|
def loader_bytes(path):
|
|
arch = platform.machine()
|
|
if arch not in LOADERS:
|
|
raise ValueError('Run on native Linux x86_64 or aarch64')
|
|
asset, digest, machine = LOADERS[arch]
|
|
if path:
|
|
data = path.read_bytes()
|
|
else:
|
|
url = f'https://github.com/SteamDeckHomebrew/decky-loader/releases/download/{RELEASE}/{asset}'
|
|
request = urllib.request.Request(url, headers={'User-Agent': 'Frame-Control-runtime-tests'})
|
|
with urllib.request.urlopen(request, timeout=60) as response:
|
|
data = response.read(80*1024*1024+1)
|
|
if hashlib.sha256(data).hexdigest() != digest:
|
|
raise ValueError('Decky 3.3.0 loader digest mismatch')
|
|
if data[:4] != b'\x7fELF' or int.from_bytes(data[18:20], 'little') != machine:
|
|
raise ValueError('Loader architecture does not match the native runner')
|
|
return data, digest
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--loader', type=Path, help='Optional cached, digest-verified native Decky 3.3.0 binary')
|
|
parser.add_argument('--output', type=Path, required=True)
|
|
args = parser.parse_args()
|
|
if sys.platform != 'linux' or not shutil.which('docker'):
|
|
parser.error('Linux with Docker is required')
|
|
cases = check_coverage()
|
|
data, digest = loader_bytes(args.loader)
|
|
tag = 'framey-decky-runtime:'+uuid.uuid4().hex[:12]
|
|
report = {'decky': RELEASE, 'architecture': platform.machine(), 'loaderSha256': digest, 'cases': []}
|
|
try:
|
|
subprocess.run(['docker', 'build', '-q', '-t', tag, '-f', str(ROOT/'tests/decky-runtime/Containerfile'),
|
|
str(ROOT/'tests/decky-runtime')], check=True, timeout=300)
|
|
with tempfile.TemporaryDirectory(prefix='framey-decky-runtime-') as temp:
|
|
loader = Path(temp)/'PluginLoader'
|
|
loader.write_bytes(data)
|
|
loader.chmod(0o644) # Private container user copies/chmods its own executable.
|
|
probes = [(case['id'], False) for case in cases] + [(111, True)]
|
|
for plugin_id, negative in probes:
|
|
name = 'framey-decky-'+uuid.uuid4().hex[:12]
|
|
command = ['docker', 'run', '--rm', '--name', name, '--network', 'none', '--read-only',
|
|
'--cap-drop', 'ALL', '--security-opt', 'no-new-privileges', '--pids-limit', '128',
|
|
'--tmpfs', '/tmp:rw,exec,mode=1777',
|
|
'--tmpfs', '/home/tester:rw,exec,uid=1100,gid=1100,mode=700',
|
|
'--tmpfs', '/run/user/1100:rw,uid=1100,gid=1100,mode=700',
|
|
'--mount', f'type=bind,source={ROOT},target=/repo,readonly',
|
|
'--mount', f'type=bind,source={loader},target=/input/PluginLoader,readonly',
|
|
tag, str(plugin_id)]
|
|
if negative:
|
|
command.append('--reproduce-bound-method-regression')
|
|
try:
|
|
result = subprocess.run(command, capture_output=True, text=True, timeout=120)
|
|
if result.returncode:
|
|
raise RuntimeError(f'Decky runtime case {plugin_id} failed:\n{result.stdout}\n{result.stderr}')
|
|
observation = json.loads(result.stdout)
|
|
report['cases'].append(observation)
|
|
print(f"PASS {plugin_id}: {observation['result']}", flush=True)
|
|
finally:
|
|
subprocess.run(['docker', 'rm', '-f', name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
finally:
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(json.dumps(report, indent=2)+'\n')
|
|
subprocess.run(['docker', 'image', 'rm', tag], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|