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.
600 lines
24 KiB
Bash
Executable File
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 "$@"
|