aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/connections/check_profile.py
blob: ac2ba00db6fa55cecb66602939d238b268d604dd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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()