v0.33.0 62a09f5

generate_build_manifest.py RawHistoryFolder

1#!/usr/bin/env python3
2"""Generate the build manifest required by scripts/collect_first_presentation.sh.
3
4The collector binds the measured binary to its source and dependency
5revisions via `go version -m` plus this manifest, and fails closed when
6attribution is missing or mismatched. Generate the manifest at build time
7from the same tree that produced the binary:
8
9    python3 scripts/generate_build_manifest.py --binary dist/dumber
10    DUMBER_CEF_DIR=/path/to/cef-runtime scripts/collect_first_presentation.sh
11
12Manifest schema (JSON, sorted keys):
13    {
14      "binary_sha256": "<hex sha256 of the measured binary>",
15      "source_revision": "<40-hex git revision of the built source>",
16      "modules": {
17        "<module path>": {"version": "<exact version>", "revision": "<40-hex>"},
18        ...
19      }
20    }
21"""
22
23import argparse
24import hashlib
25import json
26import os
27import re
28import subprocess
29import sys
30
31UPSTREAM_MODULE = "github.com/bnema/purego-cef2gtk"
32VERSION_RE = r"(v\d+\.\d+\.\d+)-0\.\d{14}-([0-9a-f]{12})"
33TAG_RE = r"v\d+\.\d+\.\d+"
34
35
36def run(args, **kwargs):
37    return subprocess.check_output(args, text=True, **kwargs).strip()
38
39
40def fail(message):
41    print("generate-build-manifest: {}".format(message), file=sys.stderr)
42    raise SystemExit(2)
43
44
45def main():
46    parser = argparse.ArgumentParser(description="Generate a collector build manifest.")
47    parser.add_argument("--binary", required=True, help="Measured binary to attribute.")
48    parser.add_argument("--output", default=None, help="Manifest path (default: <binary>.manifest.json).")
49    parser.add_argument("--source-revision", default=None,
50                        help="40-hex source revision (default: git rev-parse HEAD).")
51    parser.add_argument("--module", default=UPSTREAM_MODULE, help="Dependency module to bind.")
52    args = parser.parse_args()
53
54    if not os.path.isfile(args.binary):
55        fail("binary not found")
56    output = args.output or (args.binary + ".manifest.json")
57
58    with open(args.binary, "rb") as candidate:
59        # Chunked streaming hash: hashlib.file_digest needs 3.11+, this
60        # stays compatible with older 3.x interpreters.
61        digest = hashlib.sha256()
62        for chunk in iter(lambda: candidate.read(65536), b""):
63            digest.update(chunk)
64        binary_sha256 = digest.hexdigest()
65
66    try:
67        buildinfo = run(["go", "version", "-m", args.binary], stderr=subprocess.DEVNULL)
68    except (OSError, subprocess.CalledProcessError):
69        fail("could not read embedded build info")
70    if "=>" in buildinfo:
71        fail("binary was built with a replacement; refusing to attribute it")
72    dep_version = None
73    vcs_revision = None
74    vcs_modified = None
75    for line in buildinfo.splitlines():
76        parts = line.split()
77        if len(parts) >= 3 and parts[0] == "dep" and parts[1] == args.module:
78            dep_version = parts[2]
79        # `go version -m` reports stamping as "build vcs.revision=<hex>"
80        # (two tab-separated fields, key=value form).
81        elif len(parts) >= 2 and parts[0] == "build" and parts[1].startswith("vcs."):
82            key, _, value = parts[1].partition("=")
83            if key == "vcs.revision":
84                vcs_revision = value
85            elif key == "vcs.modified":
86                vcs_modified = value
87    # The measured binary, not the ambient checkout, is the provenance
88    # source: build-manifest builds with VCS stamping enabled, so the
89    # embedded revision describes the exact built tree. Binaries without
90    # stamping (for example -buildvcs=false quick builds) or built from a
91    # dirty tree are rejected instead of inheriting the checkout HEAD.
92    if vcs_revision is None or not re.fullmatch(r"[0-9a-f]{40}", vcs_revision):
93        fail("binary lacks embedded VCS revision; rebuild with VCS stamping enabled")
94    if vcs_modified != "false":
95        fail("binary was built from a dirty checkout; refusing to attribute it")
96    if dep_version is None:
97        fail("dependency {} not found in build info".format(args.module))
98    pseudo = re.fullmatch(VERSION_RE, dep_version)
99    tag = re.fullmatch(TAG_RE, dep_version)
100    if not pseudo and not tag:
101        fail("dependency version is not an immutable tag or pseudo-version")
102
103    try:
104        downloaded = json.loads(run(["go", "mod", "download", "-json", "{}@{}".format(args.module, dep_version)],
105                                    stderr=subprocess.DEVNULL))
106    except (OSError, subprocess.CalledProcessError, ValueError):
107        fail("could not resolve dependency origin")
108    origin = downloaded.get("Origin") or {}
109    revision = origin.get("Hash", "")
110    if origin.get("VCS") != "git" or not re.fullmatch(r"[0-9a-f]{40}", revision):
111        fail("dependency origin is not an immutable git hash")
112    if pseudo and not revision.startswith(pseudo.group(2)):
113        fail("dependency revision does not match its pseudo-version")
114
115    source_revision = args.source_revision or vcs_revision
116    if not re.fullmatch(r"[0-9a-f]{40}", source_revision or ""):
117        fail("source revision must be a 40-hex git revision")
118    if source_revision != vcs_revision:
119        fail("source revision does not match the revision embedded in the binary")
120
121    manifest = {
122        "binary_sha256": binary_sha256,
123        "source_revision": source_revision,
124        "modules": {
125            args.module: {"version": dep_version, "revision": revision},
126        },
127    }
128    with open(output, "w", encoding="utf-8") as handle:
129        json.dump(manifest, handle, indent=2, sort_keys=True)
130        handle.write("\n")
131    print("manifest: {}".format(output))
132
133
134if __name__ == "__main__":
135    main()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close