diff options
Diffstat (limited to 'prototypes/connections/check_profile.py')
| -rw-r--r-- | prototypes/connections/check_profile.py | 82 |
1 files changed, 82 insertions, 0 deletions
diff --git a/prototypes/connections/check_profile.py b/prototypes/connections/check_profile.py new file mode 100644 index 0000000..ac2ba00 --- /dev/null +++ b/prototypes/connections/check_profile.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Check the selected default hosts' linked code and direct dependencies. + +This is a Linux/GNU release-build gate, separate from sanitizer and core checks. +It does not estimate process RAM or the cost of optional UI/network hosts. +""" +import argparse +import hashlib +import json +from pathlib import Path +import platform +import re +import subprocess + +ROOT = Path(__file__).resolve().parent +NEEDED = re.compile(r"\(NEEDED\).*Shared library: \[([^]]+)\]") + + +def run(*args): + return subprocess.check_output([str(arg) for arg in args], text=True) + + +def inspect(path): + rows = [row.split() for row in run('size', path).splitlines()] + if len(rows) != 2 or len(rows[1]) < 6 or not all( + rows[1][i].isdigit() for i in range(3)): + raise ValueError(f'unsupported size output for {path}') + sections = dict(zip(('text', 'data', 'bss'), map(int, rows[1][:3]))) + needed = sorted(NEEDED.findall(run('readelf', '-d', path))) + return {'sections': sections, 'needed': needed, + 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--build', type=Path, required=True) + parser.add_argument('--budget', type=Path, default=ROOT / 'profile-budget.json') + parser.add_argument('--only', action='append', metavar='BINARY', + help='check only this selected binary (repeatable)') + parser.add_argument('--output', type=Path) + args = parser.parse_args() + if args.output and args.output.exists(): + parser.error('evidence file already exists') + budget = json.loads(args.budget.read_text()) + selected = args.only or list(budget['binaries']) + unknown = set(selected) - set(budget['binaries']) + if unknown: + parser.error(f'unknown budget binary: {sorted(unknown)}') + binaries = {} + failures = [] + for name in selected: + limits = budget['binaries'][name] + path = args.build / name + try: + observed = inspect(path) + except (OSError, subprocess.CalledProcessError, ValueError) as error: + failures.append(f'{name}: cannot inspect: {error}') + continue + binaries[name] = observed + for section, value in observed['sections'].items(): + limit = limits[f'max_{section}_bytes'] + if value > limit: + failures.append(f'{name} {section}: {value} > {limit}') + if observed['needed'] != sorted(limits['needed']): + failures.append(f'{name} dependencies: {observed["needed"]} ' + f'!= {sorted(limits["needed"])}') + result = {'passed': not failures, 'failures': failures, + 'profile': budget['profile'], 'scope': budget['scope'], + 'budget_sha256': hashlib.sha256(args.budget.read_bytes()).hexdigest(), + 'platform': platform.platform(), 'binaries': binaries} + output = json.dumps(result, indent=2) + '\n' + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + with args.output.open('x') as destination: + destination.write(output) + print(output, end='') + if failures: + raise SystemExit(1) + + +if __name__ == '__main__': + main() |
