Files
AutistMask/script/verify-build
clawbot 9f3cc05985
All checks were successful
check / check (push) Successful in 47s
e2e / e2e-chrome (push) Successful in 1m26s
e2e / e2e-firefox (push) Successful in 44s
fix: verify the build against its own receipt, with the expected mode as an argument (closes #309)
script/verify-build computed its expectation from AUTISTMASK_DEBUG in its own
environment, and the Makefile invoked it bare, so an operator with that flag
exported who ran the release target got a debug bundle -- every wallet it
creates carrying the publicly committed test recovery phrase -- verified green
at exit 0. The mode is now the required argument --expect release|debug, with
no default and nothing read from the environment; make build passes
--expect release on an env -u AUTISTMASK_DEBUG environment and make build-debug
passes --expect debug. The flag is deliberately still allowed to reach the
compiler, so a shell that has it exported fails make build loudly rather than
quietly receiving something other than the release build it asked for.

The other half was provenance. The check was a marker grep over a file list
read back out of dist/, so a 26-byte file containing only
autistmask-build-debug=off verified ok, manifest.json and the content script
that runs on every page were never read at all, and an entire hand-written
dist/ passed as "1 bundle(s) verified".

build.js now records every file it emits and writes a receipt of them -- path,
sha256, and whether the file is one of the bundles containing constants.js --
to a path the Makefile creates with mktemp per invocation, outside the repo,
and deletes afterwards; a receipt path inside dist/ is refused. dist/ is
cleared before a build, so it holds only what that build wrote.
dist/constants-bundles.txt is gone, and with it the standalone make verify-build
target: re-verifying a dist/ out of the dist/ itself is the thing that was
broken.

verify-build now checks the receipt's shape, then that dist/ contains nothing
the build did not emit and no symlinks, then each recorded file's bytes against
its digest and each audited bundle's marker against --expect. The guarantee is
narrow and README.md states it as such: dist/ is byte for byte the output of
the build.js run that just finished. It proves nothing about the honesty of the
source tree or of build.js, and offers nothing to a third party holding a
dist/. That is signing:
#310

script/test-verify-build goes from 18 cases to 39, extended in place: one per
demonstrated bypass, the missing/invalid argument cases, an AUTISTMASK_DEBUG=1
environment that the verifier must ignore, debug bundles that must fail
--expect release, and four checks that read the make build and make build-debug
recipes back out of make -n. The existing failure modes (grep exit-2, find's
status, newline and trailing-space paths, symlinked dist/, and the root probe
that refuses to count permission cases vacuously) are kept.

Verified: make check green (39 suites / 811 tests, 39 verify-build cases,
permission cases enabled), and green again inside the pinned image via
script/cibuild with --no-cache-filter=check, where the harness runs as root and
reports the setpriv runner rather than skipping. Non-vacuity proved by
mutation: disabling the digest comparison fails exactly the four bypass cases,
removing the dist/ walk fails the eight extra-file and symlink cases, restoring
the ambient AUTISTMASK_DEBUG fallback fails the no---expect case, breaking the
Makefile recipe fails the wiring cases, and dropping manifest.json from the
recorded emissions fails a real make build.
2026-08-20 12:11:04 +00:00

600 lines
24 KiB
Bash
Executable File

#!/bin/sh
# script/verify-build: assert that dist/ holds exactly what the build that just
# ran emitted, and that the compiled DEBUG state of that output is the one the
# caller asked for. Our own extension to scripts-to-rule-them-all, run at the
# end of make build / make build-debug.
#
# Why the DEBUG half exists: DEBUG makes the publicly committed test recovery
# phrase the output of wallet creation, so a release artifact built with it live
# hands every new wallet to anyone who reads the repo. The test suite cannot see
# this, because it loads src/shared/constants.js outside a bundle and takes the
# fallback branch; the property only exists in the emitted output, so it has to
# be asserted against the emitted output.
#
# Which mode to expect is an ARGUMENT (--expect release|debug) and is never
# taken from this script's environment. It used to be read from
# AUTISTMASK_DEBUG here, which meant an operator with AUTISTMASK_DEBUG=1
# exported in their shell could run the release target, get a debug build, and
# have it verified green and exit 0. There is also no default: a caller that
# does not say what it built gets a failure, because "no opinion" is not a
# state this can check anything against.
#
# Why the provenance half exists: on its own, a marker grep proves nothing
# about where the bytes came from. A 26-byte file containing only the marker
# string used to verify ok; the content script and manifest.json were not read
# at all; an entire hand-written dist/ passed. The list of files to check has
# therefore moved OUT of dist/: build.js writes a receipt naming every file it
# emitted, with each file's sha256 and whether it is one of the bundles
# containing src/shared/constants.js, and the Makefile creates that receipt
# path fresh per invocation, outside the repo, and deletes it afterwards.
#
# What that does and does not establish. It establishes that dist/ is byte for
# byte the output of the build.js run that just finished, with nothing added,
# nothing missing and nothing altered in between, and that the audited bundles
# in it compiled to the requested mode. It does NOT establish that the source
# tree or build.js were honest, and it says nothing at all to someone handed a
# dist/ from elsewhere: without the receipt from its own build they have no
# input to this check. That is signing, and it is not this control.
#
# It fails rather than passes whenever it cannot determine something. Minified
# output is not a stable contract, so "matched neither marker" is not evidence
# of anything and must never read as green; the same discipline applies to
# every read here, which is why a grep or a digest that could not be taken is
# a hard failure and not an absence of a problem.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Absolute path to this script, resolved before anything cd's anywhere.
# check_dist_tree re-invokes it through xargs, and $0 on its own may be
# relative to a directory we are about to leave.
SELF="$(cd "$(dirname "$0")" && pwd -P)/$(basename "$0")"
# Internal re-entry flag; see scan_dist_paths.
SCAN_FLAG="--scan-dist-paths"
# A literal newline and tab, for the receipt-shape guards.
NEWLINE='
'
TAB=' '
MARKER_ON="autistmask-build-debug=on"
MARKER_OFF="autistmask-build-debug=off"
RECEIPT_HEADER="autistmask-build-receipt v1"
# Set by the arguments.
RECEIPT=""
EXPECT=""
# Set by read_marker, read_sha256 and parse_file_line respectively, plus the
# receipt line number the diagnostics quote.
MARKER=""
SHA=""
ENTRY_HASH=""
ENTRY_FLAG=""
ENTRY_PATH=""
LINENO_R=0
# The sha256 command, chosen by pick_sha256.
SHA256=""
# Totals: the shape pass counts what the receipt claims, the entries pass
# counts what was actually checked against dist/, and the summary reports the
# latter.
SHAPE_COUNT=0
SHAPE_AUDITED=0
COUNT=0
AUDITED=0
# Temporary file holding the NUL-delimited dist/ listing, removed by the EXIT
# trap because fail() exits from wherever it is called.
LISTING=""
fail() {
echo "verify-build: FAIL: $*" >&2
exit 1
}
usage() {
echo "usage: verify-build --expect release|debug --receipt PATH" >&2
}
cleanup() {
[ -z "$LISTING" ] || rm -f "$LISTING"
}
trap cleanup EXIT
# --- reading files ----------------------------------------------------------
# Is the literal $1 present in the file $2? Match (grep exit 0) and no-match
# (exit 1) are answers about the emitted output. Anything else (exit 2: the
# file could not be read) is not an answer at all, and must not be reported as
# "no marker" — that would blame the bundle for a permissions or I/O fault.
has_marker() {
_hm_status=0
grep -q -F -e "$1" -- "$2" || _hm_status=$?
case "$_hm_status" in
0) return 0 ;;
1) return 1 ;;
*)
fail "grep exited $_hm_status reading $2, so the file could not be
searched and its DEBUG state was not checked at all. That is a permissions
or I/O fault on the artifact, not a change in the emitted output. Refusing
to report success."
;;
esac
}
# Pick the sha256 command once. All three print the digest as the first
# whitespace-delimited field. If none is present the digests cannot be taken at
# all, and this script has nothing left to check with, so it fails rather than
# degrading to the marker grep it used to be.
pick_sha256() {
if command -v sha256sum >/dev/null 2>&1; then
SHA256="sha256sum"
elif command -v shasum >/dev/null 2>&1; then
SHA256="shasum -a 256"
elif command -v openssl >/dev/null 2>&1; then
SHA256="openssl dgst -sha256 -r"
else
fail "no sha256 command found (tried sha256sum, shasum, openssl), so
the emitted files cannot be checked against the build receipt at all.
Refusing to report success."
fi
}
# Digest of $1 into SHA. A digest that could not be taken is not a mismatch and
# not a pass: it means the artifact was never read.
read_sha256() {
_rs_status=0
# Word-split on purpose: SHA256 is a command with its arguments.
# shellcheck disable=SC2086
_rs_out="$($SHA256 "$1" 2>/dev/null)" || _rs_status=$?
[ "$_rs_status" -eq 0 ] ||
fail "$SHA256 exited $_rs_status on $1, so its bytes were never read
and nothing was established about them. That is a permissions or I/O fault
on the artifact, not a mismatch. Refusing to report success."
SHA="${_rs_out%% *}"
case "$SHA" in
"" | *[!0-9a-f]*)
fail "$SHA256 produced no usable digest for $1, so its bytes were never
checked. Refusing to report success."
;;
esac
[ "${#SHA}" -eq 64 ] ||
fail "$SHA256 produced a ${#SHA}-character digest for $1, which is not
a sha256. Refusing to report success."
}
# Read one bundle's DEBUG state into MARKER. Exactly one marker must be
# present. Both means the ternary in constants.js was never folded, which is
# what happens when the __BUILD_DEBUG__ define goes missing from build.js:
# DEBUG stops being known at build time. Neither means we are reading output
# we do not understand. Both are hard failures; neither is ever treated as
# absence of a problem.
read_marker() {
_file="$1"
_on=no
_off=no
if has_marker "$MARKER_ON" "$_file"; then _on=yes; fi
if has_marker "$MARKER_OFF" "$_file"; then _off=yes; fi
if [ "$_on" = yes ] && [ "$_off" = yes ]; then
fail "$_file carries both debug markers, so DEBUG was not resolved at
build time: the ternary in src/shared/constants.js survived into the
emitted output. This does not mean the debug branch is live in this
artifact: an unresolved __BUILD_DEBUG__ is undeclared in extension
context, so DEBUG evaluates to false at runtime. It does mean the
release/debug distinction is no longer enforced at build time, and which
way that fallback happens to evaluate is then an accident a refactor can
flip. Check that build.js still defines __BUILD_DEBUG__."
fi
if [ "$_on" = no ] && [ "$_off" = no ]; then
fail "$_file carries no debug marker, so its DEBUG state cannot be
determined. Either BUILD_DEBUG_MARKER is gone from src/shared/constants.js
or the emitted output changed shape. Refusing to report success."
fi
if [ "$_on" = yes ]; then
MARKER="$MARKER_ON"
else
MARKER="$MARKER_OFF"
fi
}
# --- the receipt ------------------------------------------------------------
# Split one "file <sha256> <A|P> <path>" line into ENTRY_HASH, ENTRY_FLAG and
# ENTRY_PATH, and require the shape rather than assuming it. The path is the
# remainder of the line, so a path carrying a space or a tab would be read back
# as something other than what was written; build.js refuses to emit such a
# name, and a receipt that contains one is malformed rather than describing a
# file. Every rejection here is a failure: a line that cannot be understood is
# a file that would otherwise go unchecked.
parse_file_line() {
case "$1" in
"file "*) ;;
*)
fail "$RECEIPT line $LINENO_R is not a file entry and this script does
not know what it means: ${1}. Refusing to report success."
;;
esac
_pl="${1#file }"
ENTRY_HASH="${_pl%% *}"
_pl="${_pl#* }"
ENTRY_FLAG="${_pl%% *}"
ENTRY_PATH="${_pl#* }"
case "$ENTRY_HASH" in
"" | *[!0-9a-f]*) fail "$RECEIPT line $LINENO_R has no sha256: $1" ;;
esac
[ "${#ENTRY_HASH}" -eq 64 ] ||
fail "$RECEIPT line $LINENO_R has a ${#ENTRY_HASH}-character digest,
which is not a sha256: $1"
case "$ENTRY_FLAG" in
A | P) ;;
*) fail "$RECEIPT line $LINENO_R has no A/P audit flag: $1" ;;
esac
case "$ENTRY_PATH" in
dist/*) ;;
*)
fail "$RECEIPT line $LINENO_R names a path that is not under dist/:
$ENTRY_PATH. The receipt describes the emitted tree and nothing else."
;;
esac
case "$ENTRY_PATH" in
*" "* | *"$TAB"* | *"$NEWLINE"*)
fail "$RECEIPT line $LINENO_R names a path containing whitespace, which
cannot be read back unambiguously from a line-oriented receipt: $1"
;;
esac
}
# Check one emitted file against its receipt entry: it must be a regular file
# with exactly the recorded bytes, and its debug marker must match what the
# caller said this build was.
check_entry() {
[ ! -h "$ENTRY_PATH" ] ||
fail "the receipt names $ENTRY_PATH but that path is a symlink. The
build emits regular files only, so this is not the file it wrote. Refusing
to report success."
[ -f "$ENTRY_PATH" ] ||
fail "the receipt names $ENTRY_PATH, which does not exist. dist/ does
not hold what the build emitted."
[ -s "$ENTRY_PATH" ] ||
fail "the receipt names $ENTRY_PATH, which is empty. An empty file
carries no marker and matches no digest, so this is a failure and not a
pass."
read_sha256 "$ENTRY_PATH"
[ "$SHA" = "$ENTRY_HASH" ] ||
fail "$ENTRY_PATH does not contain the bytes this build emitted: the
receipt records $ENTRY_HASH and the file on disk is $SHA. Something wrote
to dist/ after the build, so this artifact is not the one that was built."
if [ "$ENTRY_FLAG" = A ]; then
read_marker "$ENTRY_PATH"
[ "$MARKER" = "$EXPECT" ] ||
fail "$ENTRY_PATH is $MARKER but this build was told to expect
$EXPECT. If AUTISTMASK_DEBUG=1 is exported in the shell that ran make
build, that is why: the flag still reaches the compiler, and this is the
check that stops the debug artifact being taken for a release one."
echo " ok: $ENTRY_PATH ($MARKER)"
AUDITED=$((AUDITED + 1))
else
if has_marker "$MARKER_ON" "$ENTRY_PATH" ||
has_marker "$MARKER_OFF" "$ENTRY_PATH"; then
fail "$ENTRY_PATH carries a debug marker but the build did not
record it as containing src/shared/constants.js. build.js selects audited
bundles with an endsWith(\".js\") test; a marker-carrying file outside that
set means the test no longer describes what is emitted, and the DEBUG state
of this file was never asserted against anything."
fi
fi
COUNT=$((COUNT + 1))
}
# Walk the receipt line by line, applying $1 to each file entry. The header and
# the root line are checked on the way past; the root line is what stops a
# receipt written by a build of some other tree being pointed at this one.
walk_receipt() {
_wr_each="$1"
LINENO_R=0
_line=""
while IFS= read -r _line || [ -n "$_line" ]; do
LINENO_R=$((LINENO_R + 1))
if [ "$LINENO_R" -eq 1 ]; then
[ "$_line" = "$RECEIPT_HEADER" ] ||
fail "$RECEIPT does not start with \"$RECEIPT_HEADER\", so it
is not a build receipt this script understands. Refusing to report
success."
continue
fi
if [ "$LINENO_R" -eq 2 ]; then
[ "$_line" = "root $ROOT" ] ||
fail "$RECEIPT was written by a build of a different tree: it
says \"$_line\" and this is $ROOT. A receipt only describes the dist/ of
the tree it was built in."
continue
fi
parse_file_line "$_line"
"$_wr_each"
done <"$RECEIPT"
[ "$LINENO_R" -ge 2 ] ||
fail "$RECEIPT is truncated: it has no root line, so it is not a
receipt this script can check anything against."
}
# Pass one: the receipt has to be a receipt before anything is concluded from
# it. A line this script cannot read is a file that would go unchecked, and a
# receipt naming no audited bundle asserts no DEBUG state at all — both are
# failures, and both have to be established before the tree is walked against
# it, because a receipt entry that was misread would otherwise surface as a
# complaint about dist/.
count_entry() {
SHAPE_COUNT=$((SHAPE_COUNT + 1))
if [ "$ENTRY_FLAG" = A ]; then
SHAPE_AUDITED=$((SHAPE_AUDITED + 1))
fi
}
check_receipt_shape() {
SHAPE_COUNT=0
SHAPE_AUDITED=0
walk_receipt count_entry
[ "$SHAPE_COUNT" -gt 0 ] ||
fail "$RECEIPT names no emitted files, so nothing was inspected. A
build always emits some."
[ "$SHAPE_AUDITED" -gt 0 ] ||
fail "$RECEIPT names no bundle containing src/shared/constants.js, so
no DEBUG state would be asserted at all. That is never correct, so it is a
failure and not a pass."
}
# Pass three: every file the receipt names, checked against the bytes on disk.
check_receipt_entries() {
walk_receipt check_entry
}
# --- the emitted tree -------------------------------------------------------
# The receipt says which files the build emitted. This says dist/ contains no
# others: an artifact that was added after the build, or that a hand-written
# dist/ brought with it, is not something the build vouches for and is not
# something this check may pass over.
#
# The walk has to be exhaustive and every name has to survive it intact, so
# four things are enforced rather than assumed:
#
# - the walk is NUL-delimited and the paths reach the check as arguments, so
# no name can be reshaped on the way in. Read line by line, a name with a
# trailing space lost it to read's field splitting and the remnant then
# matched a listed path, and a name containing a newline arrived as a
# listed path plus an empty one. Both left an unchecked file in dist/ while
# the script still reported success.
# - find's exit status is checked. A subtree it cannot descend is reported on
# stderr and then simply missing from the listing, so an unchecked status
# turns "could not look" into "nothing was there" — the same conflation
# has_marker exists to prevent. The status cannot be read off a pipeline,
# so the listing lands in a file that xargs then reads back.
# - symlinks are walked too (-type l), not skipped. The build emits none, so
# a symlink under dist/ is a path the build did not produce, whatever it
# points at, and it fails as one instead of being read through.
# - dist/ itself must be a directory and not a symlink, which main asserts
# before anything reads through it. find does not follow a symlink named on
# its own command line, so a linked dist/ collapses this walk to one entry
# and cross-checks nothing.
#
# Types other than regular files and symlinks are left out on purpose: a build
# emits none of them, and grep on a fifo would hang rather than fail.
check_dist_tree() {
LISTING="$(mktemp "${TMPDIR:-/tmp}/verify-build-dist.XXXXXX")" ||
fail "could not create a temporary file for the dist/ listing, so the
tree was never walked. Refusing to report success."
_find_status=0
find dist \( -type f -o -type l \) -print0 >"$LISTING" || _find_status=$?
[ "$_find_status" -eq 0 ] ||
fail "find exited $_find_status enumerating dist/, so part of the tree
was never walked and nothing was established about the files in it. Any
file the build did not emit could be sitting there unchecked. That is a
permissions or I/O fault on the artifact. Refusing to report success."
_scan_status=0
xargs -0 "$SELF" "$SCAN_FLAG" "$RECEIPT" <"$LISTING" || _scan_status=$?
[ "$_scan_status" -eq 0 ] ||
fail "the dist/ tree scan exited $_scan_status: either a path under
dist/ failed the check reported above, or the scan could not be run at all.
Refusing to report success."
}
# Does the receipt name the path $1? Compared as whole strings, never through
# grep: a path found under dist/ is attacker-shaped input, and a pattern is not
# the place to put one. The receipt's own paths are known to carry no
# whitespace by the time this runs — verify_receipt failed the run otherwise —
# so stripping the three leading fields recovers each one exactly.
receipt_names() {
_rn_want="$1"
_rn_line=""
while IFS= read -r _rn_line || [ -n "$_rn_line" ]; do
case "$_rn_line" in
"file "*) ;;
*) continue ;;
esac
[ "${_rn_line#file * * }" != "$_rn_want" ] || return 0
done <"$RECEIPT"
return 1
}
# The per-path half of check_dist_tree. It runs in a re-invocation of this
# script, so it uses the same helpers as the rest of the file rather than a
# second copy of them that could drift. Paths arrive as arguments and are never
# split, joined or trimmed.
scan_dist_paths() {
for _file in "$@"; do
if [ -h "$_file" ]; then
fail "$_file is a symlink under dist/. The build emits regular
files only, so this path is not something it produced, and what it points
at is not what was verified. Refusing to report success."
fi
if receipt_names "$_file"; then
continue
fi
fail "$_file is under dist/ but the build that just ran did not emit
it. dist/ must contain exactly what the build produced: an extra file there
is an artifact nothing vouches for, and shipping the directory ships it."
done
}
# --- arguments --------------------------------------------------------------
# The expected mode and the receipt are stated by the caller. Nothing is read
# from the environment, and there is no default for either.
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
--expect)
[ "$#" -ge 2 ] || fail "--expect needs an argument (release|debug)."
set_expect "$2"
shift 2
;;
--expect=*)
set_expect "${1#--expect=}"
shift
;;
--receipt)
[ "$#" -ge 2 ] || fail "--receipt needs a path."
set_receipt "$2"
shift 2
;;
--receipt=*)
set_receipt "${1#--receipt=}"
shift
;;
*)
usage
fail "unknown argument: $1"
;;
esac
done
}
set_expect() {
[ -z "$EXPECT" ] || fail "--expect given more than once."
case "$1" in
release) EXPECT="$MARKER_OFF" ;;
debug) EXPECT="$MARKER_ON" ;;
*) fail "--expect takes release or debug, not \"$1\"." ;;
esac
}
set_receipt() {
[ -z "$RECEIPT" ] || fail "--receipt given more than once."
[ -n "$1" ] || fail "--receipt was given an empty path."
# Resolved against the caller's directory, before main cd's to the repo
# root.
case "$1" in
/*) RECEIPT="$1" ;;
*) RECEIPT="$PWD/$1" ;;
esac
}
# --- main -------------------------------------------------------------------
main() {
# Internal re-entry from check_dist_tree's xargs. Not part of the
# command-line interface: nothing else invokes it, and it is a distinct
# entry point rather than a mode flag threaded through the checks below.
if [ "${1-}" = "$SCAN_FLAG" ]; then
shift
[ "$#" -ge 1 ] || fail "internal: $SCAN_FLAG needs the receipt path."
RECEIPT="$1"
shift
cd "$ROOT"
[ -r "$RECEIPT" ] ||
fail "$RECEIPT became unreadable during the run, so the dist/ tree
could not be checked against it. Refusing to report success."
scan_dist_paths "$@"
return 0
fi
parse_args "$@"
[ -n "$EXPECT" ] || {
usage
fail "no --expect argument. The mode this build was supposed to produce
has to be stated by whoever ran the build; it is not a default and it is
not read from AUTISTMASK_DEBUG in this script's environment, because an
operator with that exported would then have their debug build verified as
the release one they asked for."
}
[ -n "$RECEIPT" ] || {
usage
fail "no --receipt argument. The list of files to check comes from the
build that just ran, not from dist/: without it, a hand-written dist/ would
be verifying itself. make build and make build-debug pass one."
}
pick_sha256
cd "$ROOT"
# Asserted here rather than left to grep. A symlinked dist/ used to fail
# only because GNU grep exits 2 on a directory, so the tree walk hit
# has_marker's I/O path by luck; under a grep that exits 1 instead, the
# whole cross-check would have collapsed into a pass.
if [ -h dist ]; then
fail "dist is a symlink, not a directory. find does not follow a
symlink named on its own command line, so the tree walk would see one entry
instead of the emitted tree and establish nothing about it. Refusing to
report success."
fi
[ -d dist ] ||
fail "dist is not a directory, so there is no emitted tree to verify.
build.js writes it; run make build first."
case "$RECEIPT" in
"$ROOT/dist" | "$ROOT/dist/"*)
fail "the receipt is inside dist/ ($RECEIPT). A receipt that lives in
the tree it describes is rewritten by whoever rewrites the tree, and vouches
for nothing. make build keeps it outside the repo."
;;
esac
[ -e "$RECEIPT" ] ||
fail "$RECEIPT is missing. build.js writes it at the end of a
successful build; run make build rather than invoking this directly."
[ -f "$RECEIPT" ] ||
fail "$RECEIPT is not a regular file, so it is not a build receipt."
[ -s "$RECEIPT" ] ||
fail "$RECEIPT is empty, so the build wrote nothing to it and there is
no account of what it emitted. build.js writes the receipt last, so an
empty one means the build did not finish."
[ -r "$RECEIPT" ] ||
fail "$RECEIPT is not readable, so nothing was inspected. That is a
permissions or I/O fault, not a pass."
echo "Verifying emitted files against the build receipt (expecting" \
"$EXPECT)..."
# Order matters. The receipt has to be well-formed before it is used as an
# expectation, and the tree has to be walkable in full before any single
# file in it is pronounced on: a subtree that cannot be descended makes
# every file under it look absent, and "could not look" must never be
# reported as "was not there".
check_receipt_shape
check_dist_tree
check_receipt_entries
echo "verify-build: $COUNT emitted file(s) verified against the receipt," \
"$AUDITED bundle(s) $EXPECT"
}
main "$@"