mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +08:00
6d742eb343
Add control trace and platform qualification tools that exercise explicit safety modes and vehicle coverage. Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com> Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com> Co-authored-by: Kacper Rączy <gfw.kra@gmail.com> Co-authored-by: YassineYousfi <yyousfi1@binghamton.edu> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
96 lines
3.9 KiB
Python
96 lines
3.9 KiB
Python
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import stat
|
|
import subprocess
|
|
|
|
|
|
def git(root, *args):
|
|
return subprocess.check_output(['git', '-C', str(root), *args])
|
|
|
|
|
|
def tracked(root):
|
|
return [os.fsdecode(path) for path in git(root, 'ls-files', '-z').split(b'\0') if path]
|
|
|
|
|
|
def fingerprint(path):
|
|
mode = path.lstat().st_mode
|
|
if stat.S_ISLNK(mode):
|
|
return {'kind': 'symlink', 'mode': stat.S_IMODE(mode), 'sha256': hashlib.sha256(os.fsencode(os.readlink(path))).hexdigest()}
|
|
if not stat.S_ISREG(mode):
|
|
raise ValueError(f'Not a regular file: {path}')
|
|
return {'kind': 'file', 'mode': stat.S_IMODE(mode), 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()}
|
|
|
|
|
|
def read_manifest(path):
|
|
path = Path(path)
|
|
metadata = path.lstat()
|
|
if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != os.getuid() or stat.S_IMODE(metadata.st_mode) != 0o600:
|
|
raise ValueError('Unsafe build manifest')
|
|
if metadata.st_size > 16 * 1024 * 1024:
|
|
raise ValueError('Oversized build manifest')
|
|
value = json.loads(path.read_text())
|
|
if value.get('format') != 'starpilot-ci-build' or value.get('version') != 1:
|
|
raise ValueError('Unsupported build manifest')
|
|
return value
|
|
|
|
|
|
def capture(root, destination, outputs, config):
|
|
root = Path(root).resolve()
|
|
destination = Path(destination).absolute()
|
|
if destination.resolve().is_relative_to(root):
|
|
raise ValueError('Build manifest must be outside checkout')
|
|
revision = git(root, 'rev-parse', 'HEAD').decode().strip()
|
|
if destination.exists():
|
|
existing = read_manifest(destination)
|
|
if existing['root'] != str(root) or existing['revision'] != revision or existing['config'] != config:
|
|
raise ValueError('Stale build manifest')
|
|
return
|
|
if git(root, 'status', '--porcelain', '--untracked-files=all'):
|
|
raise ValueError('Checkout must be clean before build')
|
|
files = {name: fingerprint(root / name) for name in tracked(root)}
|
|
selected = sorted(set(outputs) & files.keys())
|
|
if any(files[name]['kind'] != 'file' for name in selected):
|
|
raise ValueError('Derived output is not a regular file')
|
|
value = {'format': 'starpilot-ci-build', 'version': 1, 'root': str(root), 'revision': revision,
|
|
'config': config, 'files': files, 'outputs': selected}
|
|
with open(destination, 'x', opener=lambda path, flags: os.open(path, flags, 0o600)) as stream:
|
|
json.dump(value, stream, sort_keys=True)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
|
|
|
|
def verify(root, manifest):
|
|
root = Path(root).resolve()
|
|
value = read_manifest(manifest)
|
|
if value['root'] != str(root) or value['revision'] != git(root, 'rev-parse', 'HEAD').decode().strip():
|
|
raise ValueError('Build manifest belongs to a different checkout or revision')
|
|
files = value['files']
|
|
outputs = set(value['outputs'])
|
|
if not outputs <= files.keys() or set(tracked(root)) != files.keys():
|
|
raise ValueError('Tracked inventory changed')
|
|
if git(root, 'diff', '--cached', '--name-only'):
|
|
raise ValueError('Index changed during build')
|
|
if git(root, 'ls-files', '--others', '--exclude-standard', '-z'):
|
|
raise ValueError('Untracked files created during build')
|
|
for name, before in files.items():
|
|
if Path(name).is_absolute() or '..' in Path(name).parts:
|
|
raise ValueError('Unsafe manifest path')
|
|
after = fingerprint(root / name)
|
|
if after['kind'] != before['kind'] or after['mode'] != before['mode']:
|
|
raise ValueError(f'File type or mode changed: {name}')
|
|
if name in outputs:
|
|
if after['kind'] != 'file' or (root / name).stat().st_size == 0:
|
|
raise ValueError(f'Missing or empty derived output: {name}')
|
|
elif after != before:
|
|
raise ValueError(f'Source or undeclared output changed: {name}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--manifest', required=True)
|
|
args = parser.parse_args()
|
|
verify(git(Path.cwd(), 'rev-parse', '--show-toplevel').decode().strip(), args.manifest)
|