Compare commits

..

3 Commits

Author SHA1 Message Date
578a3bc862 fix: never render a nonzero approval amount as zero (closes #322)
All checks were successful
check / check (push) Successful in 31s
e2e / e2e-chrome (push) Successful in 1m11s
e2e / e2e-firefox (push) Successful in 24s
Amounts are truncated to four decimal places per README.md's Display
Consistency rule. With the token's true scale resolved, an amount below 0.0001
still printed as 0.0000 — 1 base unit of an 18-decimal token, 500 base units of
an 8-decimal one. On the dApp approval screen, and on the wait/success/error
screens that carry the same string forward as txInfo.amount, a real transfer,
allowance or swap was therefore stated as nothing. A swap's "Min. received" is
the sharper case: a slippage floor shown as 0.0000 states that the swap may
return nothing.

That truncation existed in three separate copies — src/popup/views/approval.js
(the ERC-20 amount, the ETH value, the max fee), src/shared/uniswap.js (the
swap's Amount and Min. received lines, on that same screen) and
src/shared/transactions.js (history and balance lists). They now share
src/shared/amountDisplay.js, which holds the rule and its one exception side by
side: truncateAmount() truncates, truncateAmountNeverZero() truncates with the
nonzero floor.

approval.js and uniswap.js take the floored function, so every amount those
screens display or hand to the confirmation screens obeys the invariant. When
the truncated string would carry no digit from 1 to 9 and the value does, the
amount is extended to its first significant digit: 0.000000000000000001 DAI,
not 0.0000 DAI. It stays in token units, the same unit as the symbol beside it;
the base-unit rendering already on this screen means "the scale is unknown", and
reusing it for a known scale would blur the two. A genuine zero still renders
0.0000, an amount at or above the floor is untouched, and truncation stays
truncation — 0.99999 shows as 0.9999, never rounded up.

transactions.js takes the unfloored function, keeping its current behaviour
exactly: balance lists and history are out of scope by the issue's definition of
done, and the transaction detail view already shows exact precision there. The
code path is shared; the policy is not.

tests/approvalDisplayFloor.test.js drives decodeCalldata() and uniswap.decode()
and asserts both the displayed line and the rawValue the confirmation screens
carry. Against this tree with src/shared/uniswap.js reverted to its previous
formatAmount(), its two new swap cases fail: a 50-base-unit USDT input gives
"0.0000 USDT" where "0.00005 USDT" is expected, and a 1-wei amountOutMin gives
"0.0000 WETH" where "0.000000000000000001 WETH" is expected. A case pinning the
list rule as unfloored is included so the shared module cannot drift into one
policy.

make check green: 42 suites, 848 tests; verify-build 39 cases; check-censored
153 files; eslint and prettier clean in the pinned container.
2026-08-23 13:40:10 +00:00
12b0c4d1c6 build: remove dist/ when a release build fails (closes #333)
Some checks failed
check / check (push) Successful in 30s
e2e / e2e-chrome (push) Has been cancelled
e2e / e2e-firefox (push) Has been cancelled
A failed release build no longer leaves a complete, loadable debug bundle in dist/ whose every wallet uses the publicly committed test recovery phrase. Each step of the release build runs through script/discard-dist-on-failure, which removes dist/ on failure, says on stderr that it did and why, and returns the step's own status. build-debug is deliberately unwrapped. script/verify-build is untouched.
2026-08-23 15:39:04 +02:00
c36d8b6ddf docs: state the enforced dist/ verification scope precisely (closes #331)
All checks were successful
check / check (push) Successful in 29s
e2e / e2e-chrome (push) Successful in 1m11s
e2e / e2e-firefox (push) Successful in 22s
README, the script synopsis, its header paragraph and the check_dist_tree comment now all say the same thing: regular files and symlinks under dist/ are covered; fifos, sockets, device nodes and empty directories are not, and why. No behaviour change — the walk is untouched.
2026-08-23 15:33:58 +02:00
6 changed files with 376 additions and 59 deletions

View File

@@ -60,19 +60,31 @@ hooks:
# scrubbed from the build itself: with AUTISTMASK_DEBUG=1 exported, this target # scrubbed from the build itself: with AUTISTMASK_DEBUG=1 exported, this target
# compiles a debug bundle and then fails on it, loudly, rather than quietly # compiles a debug bundle and then fails on it, loudly, rather than quietly
# handing back something other than the release build that was asked for. # handing back something other than the release build that was asked for.
#
# Every step of this target is wrapped in script/discard-dist-on-failure, so a
# release build that fails removes dist/ instead of leaving a complete, loadable
# debug bundle there for whoever runs the build, sees it fail, and loads
# dist/chrome/ anyway. A step that succeeds removes nothing, and build-debug is
# deliberately not wrapped.
build: build:
@echo "Building extension..." @echo "Building extension..."
@set -eu; \ @set -eu; \
receipt="$$(mktemp "$${TMPDIR:-/tmp}/autistmask-build-receipt.XXXXXX")"; \ receipt="$$(mktemp "$${TMPDIR:-/tmp}/autistmask-build-receipt.XXXXXX")"; \
trap 'rm -f "$$receipt"' EXIT INT TERM; \ trap 'rm -f "$$receipt"' EXIT INT TERM; \
AUTISTMASK_BUILD_RECEIPT="$$receipt" yarn run build 2>&1; \ script/discard-dist-on-failure \
env AUTISTMASK_BUILD_RECEIPT="$$receipt" yarn run build 2>&1; \
script/discard-dist-on-failure \
env -u AUTISTMASK_DEBUG script/verify-build --expect release \ env -u AUTISTMASK_DEBUG script/verify-build --expect release \
--receipt "$$receipt" --receipt "$$receipt"
@script/check-censored --require-dist @script/discard-dist-on-failure script/check-censored --require-dist
# Development-only build: enables the red DEBUG / INSECURE banner and makes # Development-only build: enables the red DEBUG / INSECURE banner and makes
# the hardcoded test recovery phrase the output of wallet creation. Never # the hardcoded test recovery phrase the output of wallet creation. Never
# distribute the artifacts this produces. # distribute the artifacts this produces.
#
# No discard-dist-on-failure here, on purpose: a debug build that fails is not
# producing an artifact anyone could mistake for a release one, and its dist/ is
# the evidence of what went wrong.
build-debug: build-debug:
@echo "Building extension (DEBUG)..." @echo "Building extension (DEBUG)..."
@set -eu; \ @set -eu; \

View File

@@ -63,8 +63,12 @@ that runs `make build`, that target compiles a debug bundle and then **fails**,
because it tells `script/verify-build` in so many words that it was supposed to because it tells `script/verify-build` in so many words that it was supposed to
produce a release build. It used to be that the verifier read the same variable produce a release build. It used to be that the verifier read the same variable
out of its own environment, agreed with itself, and reported a debug artifact as out of its own environment, agreed with itself, and reported a debug artifact as
verified. The build prints which mode it used. See the verified. The build prints which mode it used. A release build that fails also
[DEBUG Mode Policy](#debug-mode-policy) for what the flag changes. **Never **removes `dist/`**, and says so: the bundle it had already written is loadable,
and a loud failure is no protection against someone loading `dist/chrome/`
anyway. `make build-debug` keeps its `dist/` on failure — that output is not
mistakable for a release build, and it is the evidence of what went wrong. See
the [DEBUG Mode Policy](#debug-mode-policy) for what the flag changes. **Never
distribute a debug build** — every wallet it creates gets the same publicly distribute a debug build** — every wallet it creates gets the same publicly
known test recovery phrase. known test recovery phrase.
@@ -81,17 +85,21 @@ lives.
one of the bundles containing `src/shared/constants.js` — into a build receipt, one of the bundles containing `src/shared/constants.js` — into a build receipt,
and `script/verify-build` checks `dist/` against that receipt: every recorded and `script/verify-build` checks `dist/` against that receipt: every recorded
file present with exactly the recorded bytes, every audited bundle carrying the file present with exactly the recorded bytes, every audited bundle carrying the
requested `DEBUG` marker, and nothing under `dist/` that the build did not requested `DEBUG` marker, and no regular file or symlink under `dist/` that the
write. The `Makefile` creates the receipt path with `mktemp` per invocation, build did not write. The `Makefile` creates the receipt path with `mktemp` per
outside the repo, and deletes it afterwards. invocation, outside the repo, and deletes it afterwards.
That is what ties the check to a build rather than to a directory. What it That is what ties the check to a build rather than to a directory. What it
establishes is narrow and worth stating exactly: `dist/` is byte for byte the establishes is narrow and worth stating exactly: `dist/` is byte for byte the
output of the `build.js` run that just finished, with nothing added, removed or output of the `build.js` run that just finished, with no regular file or symlink
altered in between. It establishes nothing about whether the source tree or added, removed or altered in between. Regular files and symlinks are the whole
`build.js` were honest, and it offers nothing to someone handed a `dist/` from of what the tree walk covers; fifos, sockets, device nodes and empty directories
elsewhere — without the receipt from its own build there is no input to the under `dist/` are not checked, because a build emits none of them, none can
check. Verifiable provenance for a third party is signing, which this is not. carry a shippable payload, and `grep` on a fifo would hang rather than fail. It
establishes nothing about whether the source tree or `build.js` were honest, and
it offers nothing to someone handed a `dist/` from elsewhere — without the
receipt from its own build there is no input to the check. Verifiable provenance
for a third party is signing, which this is not.
There is deliberately no target that re-verifies an existing `dist/` on its own. There is deliberately no target that re-verifies an existing `dist/` on its own.
The list of files to check has to come from the build that produced them; read The list of files to check has to come from the build that produced them; read
@@ -143,26 +151,35 @@ provide:
serves. Run deliberately, never as part of a build: the output is committed serves. Run deliberately, never as part of a build: the output is committed
and there is no runtime fetch, so the shipped list is as fresh as the last and there is no runtime fetch, so the shipped list is as fresh as the last
vendoring run that was released vendoring run that was released
- `script/verify-build --expect release|debug --receipt PATH` — assert that - `script/verify-build --expect release|debug --receipt PATH` — assert that the
`dist/` is exactly what the build that just ran emitted, and that the compiled regular files and symlinks under `dist/` are exactly what the build that just
`DEBUG` state of the bundles in it is the one that was asked for. Both ran emitted (other file types are out of scope), and that the compiled `DEBUG`
arguments are required and neither has a default: the expected mode is stated state of the bundles in it is the one that was asked for. Both arguments are
by the caller rather than read from `AUTISTMASK_DEBUG`, and the file list required and neither has a default: the expected mode is stated by the caller
comes from the build's receipt rather than from `dist/` (see rather than read from `AUTISTMASK_DEBUG`, and the file list comes from the
build's receipt rather than from `dist/` (see
[Build Receipts](#build-receipts)). Run automatically at the end of [Build Receipts](#build-receipts)). Run automatically at the end of
`make build` and `make build-debug`; fails loudly rather than passing whenever `make build` and `make build-debug`; fails loudly rather than passing whenever
it cannot determine something. Not part of `make check`, which does not depend it cannot determine something. Not part of `make check`, which does not depend
on build artifacts existing. on build artifacts existing.
- `script/discard-dist-on-failure COMMAND [ARG...]` — run one step of the
**release** build and, if it fails, remove `dist/` before returning that
step's exit status, saying on stderr that it did and why. Every step of
`make build` runs through it; `make build-debug` runs none of them through it.
A step that succeeds removes nothing, and a removal that cannot be completed
is reported as loudly as one that was
- `script/test-verify-build` — exercise every failure mode of - `script/test-verify-build` — exercise every failure mode of
`script/verify-build` against a fixture tree in a temp dir, asserting the exit `script/verify-build` against a fixture tree in a temp dir, asserting the exit
status and the message of each, and read the `make build` and status and the message of each, assert the state of `dist/` on disk after a
failing and a succeeding release build step, and read the `make build` and
`make build-debug` recipes back out of `make -n` to check that they pass the `make build-debug` recipes back out of `make -n` to check that they pass the
mode as an argument on a scrubbed environment. Part of `make check`; it reads mode as an argument on a scrubbed environment and wrap only the release path.
no build artifacts and writes nothing under `dist/`. The cases that depend on Part of `make check`; it reads no build artifacts and writes nothing under
file permissions cannot mean anything for a process that is not subject to `dist/`. The cases that depend on file permissions cannot mean anything for a
them, so the harness proves its runner against a mode-000 file before counting process that is not subject to them, so the harness proves its runner against
them, dropping to an unprivileged user when run as root; if it cannot, it a mode-000 file before counting them, dropping to an unprivileged user when
skips those cases and says so in a banner rather than passing them. run as root; if it cannot, it skips those cases and says so in a banner rather
than passing them.
- `script/docker` — build the Docker image tagged via `script/projectname` - `script/docker` — build the Docker image tagged via `script/projectname`
- `script/cibuild` — CI entrypoint: plain `docker build .` - `script/cibuild` — CI entrypoint: plain `docker build .`
- `script/precommit` — run by the git pre-commit hook; runs `script/check` - `script/precommit` — run by the git pre-commit hook; runs `script/check`
@@ -176,9 +193,11 @@ The Makefile shims to those. It also carries a few targets that have no
silently rewritten. Use `make setup` for a fresh clone. silently rewritten. Use `make setup` for a fresh clone.
- `make hooks` — shims to `script/install-precommit` - `make hooks` — shims to `script/install-precommit`
- `make build` — build the extension into `dist/chrome/` and `dist/firefox/`, - `make build` — build the extension into `dist/chrome/` and `dist/firefox/`,
then verify the result against the build's receipt as a release build then verify the result against the build's receipt as a release build. A
failure at any step removes `dist/`
- `make build-debug` — the same build with `AUTISTMASK_DEBUG=1`, verified as a - `make build-debug` — the same build with `AUTISTMASK_DEBUG=1`, verified as a
debug build (see [Debug Builds](#debug-builds)) debug build, and keeping its `dist/` on failure (see
[Debug Builds](#debug-builds))
- `make clean` — remove `dist/` - `make clean` — remove `dist/`
- `make dev` — build in watch mode - `make dev` — build in watch mode

27
TODO.md
View File

@@ -26,7 +26,8 @@ milestone is in flight on `next`; its `next` -> `main` PR is
[#190](https://git.eeqj.de/sneak/AutistMask/pulls/190). `make check` verified [#190](https://git.eeqj.de/sneak/AutistMask/pulls/190). `make check` verified
green on `next` at `e9fa8be` on 2026-08-10, and `make build` produces green on `next` at `e9fa8be` on 2026-08-10, and `make build` produces
`dist/chrome/` and `dist/firefox/`, verified against the build's own receipt to `dist/chrome/` and `dist/firefox/`, verified against the build's own receipt to
be exactly what that build emitted with `DEBUG` compiled off. hold exactly the regular files and symlinks that build emitted, with `DEBUG`
compiled off.
The backlog lives on the The backlog lives on the
[Gitea tracker](https://git.eeqj.de/sneak/AutistMask/issues), which is [Gitea tracker](https://git.eeqj.de/sneak/AutistMask/issues), which is
@@ -44,6 +45,29 @@ but the review is broader than any of them.
# Completed Steps # Completed Steps
- 2026-08-23: A failed release build no longer leaves a loadable debug bundle in
`dist/` ([#333](https://git.eeqj.de/sneak/AutistMask/issues/333)). With
`AUTISTMASK_DEBUG=1` exported, `make build` compiled a debug bundle and failed
on it in `script/verify-build` — but the bundle stayed on disk, loadable, with
every wallet it creates using the publicly committed test recovery phrase.
Every step of `make build` now runs through `script/discard-dist-on-failure`,
which removes `dist/` when a step fails and says on stderr that it did and
why; a removal it cannot complete is reported just as loudly.
`make build-debug` is deliberately not wrapped: its output is not mistakable
for a release build and is the evidence of the failure.
`script/test-verify-build` asserts the state of `dist/` on disk after a
failing and a succeeding step, not just the exit status, and reads `make -n`
to check the wrapper is on the release path and only there.
- 2026-08-23: `README.md` and `script/verify-build`'s own comments now state the
emitted-tree guarantee at the width the code actually enforces
([#331](https://git.eeqj.de/sneak/AutistMask/issues/331)). The tree walk is
`-type f -o -type l`, so the guarantee covers regular files and symlinks under
`dist/`; fifos, sockets, device nodes and empty directories are not checked,
because a build emits none of them, none can carry a shippable payload, and
`grep` on a fifo would hang rather than fail. The exclusion is deliberate and
unchanged — the README said "nothing under `dist/` that the build did not
write", which was broader than that. Documentation only; no executable line
changed.
- 2026-08-23: An amount below the 4-decimal display floor no longer reads as - 2026-08-23: An amount below the 4-decimal display floor no longer reads as
zero on the approval screens zero on the approval screens
([#322](https://git.eeqj.de/sneak/AutistMask/issues/322)). With the token's ([#322](https://git.eeqj.de/sneak/AutistMask/issues/322)). With the token's
@@ -60,7 +84,6 @@ but the review is broader than any of them.
lists (`src/shared/transactions.js`) keep the unfloored rule, which is out of lists (`src/shared/transactions.js`) keep the unfloored rule, which is out of
scope by the issue's definition of done. `README.md`'s Display Consistency scope by the issue's definition of done. `README.md`'s Display Consistency
section records the exception. section records the exception.
- 2026-08-20: A second extension page can no longer silently delete a wallet - 2026-08-20: A second extension page can no longer silently delete a wallet
([#304](https://git.eeqj.de/sneak/AutistMask/issues/304)). `saveState()` wrote ([#304](https://git.eeqj.de/sneak/AutistMask/issues/304)). `saveState()` wrote
the entire state blob, and every extension page — the toolbar popup, a dApp the entire state blob, and every extension page — the toolbar popup, a dApp

78
script/discard-dist-on-failure Executable file
View File

@@ -0,0 +1,78 @@
#!/bin/sh
# script/discard-dist-on-failure: run one step of the RELEASE build, and if that
# step fails, remove dist/ before returning its exit status. Our own extension
# to scripts-to-rule-them-all, wrapped around every step of make build.
#
# Why: with AUTISTMASK_DEBUG=1 exported in the calling shell, make build
# compiles a debug bundle and then fails on it in script/verify-build — but the
# bundle is already written. It is loadable, and every wallet it creates gets
# the publicly committed test recovery phrase from src/shared/constants.js. A
# failed release build that leaves that behind is a smaller version of the trap
# the verifier exists to close, and "the failure was loud" only works on an
# operator who does not load dist/chrome/ anyway. Removing the artifact does not
# depend on that.
#
# Two things this deliberately does not do. It does not wrap make build-debug: a
# debug build that failed is not a mistakable artifact, and its output is the
# evidence of what went wrong. And it never removes anything on a step that
# SUCCEEDS, including the final check-censored --require-dist pass.
#
# The removal is never silent: it says dist/ is gone and why, on stderr, above
# the build's own failure.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
DIST="$ROOT/dist"
usage() {
echo "usage: discard-dist-on-failure COMMAND [ARG...]" >&2
}
# Remove dist/, and say so. A removal that could not be completed is reported as
# loudly as one that was: the artifact is still on disk, and reporting nothing
# would leave the operator believing it is not.
discard_dist() {
if [ ! -e "$DIST" ] && [ ! -h "$DIST" ]; then
echo "discard-dist-on-failure: the release build failed. There was no" \
"dist/ to remove." >&2
return 0
fi
rm -rf "$DIST" || true
if [ -e "$DIST" ] || [ -h "$DIST" ]; then
echo "discard-dist-on-failure: the release build failed and dist/" \
"COULD NOT BE REMOVED, so it is still on disk. Do not load it:" \
"a release build that failed may hold a complete debug bundle," \
"whose wallets all use the publicly committed test recovery" \
"phrase. Remove it by hand (make clean)." >&2
return 0
fi
echo "discard-dist-on-failure: the release build failed, so dist/ WAS" \
"REMOVED and no longer exists. A release build that fails has often" \
"already emitted a complete, loadable debug bundle — every wallet it" \
"creates gets the publicly committed test recovery phrase — so the" \
"failed build is not left behind to be loaded. Fix the failure and" \
"re-run make build, or run make build-debug if a debug build is what" \
"was wanted; that target keeps its output." >&2
}
main() {
[ "$#" -ge 1 ] || {
usage
echo "discard-dist-on-failure: no command given, so no build step ran" \
"and nothing was removed." >&2
exit 1
}
_status=0
"$@" || _status=$?
[ "$_status" -ne 0 ] || return 0
discard_dist
exit "$_status"
}
main "$@"

View File

@@ -1,7 +1,8 @@
#!/bin/sh #!/bin/sh
# script/test-verify-build: exercise every failure mode of # script/test-verify-build: exercise every failure mode of
# script/verify-build. Our own extension to scripts-to-rule-them-all, run # script/verify-build, and what make build does with dist/ after one of them
# from script/check so make check covers it. # (script/discard-dist-on-failure). Our own extension to
# scripts-to-rule-them-all, run from script/check so make check covers it.
# #
# Why this exists: verify-build is the build-integrity guard, and four separate # Why this exists: verify-build is the build-integrity guard, and four separate
# reviews of it each found a fresh vacuous pass — the grep exit-2 conflation, # reviews of it each found a fresh vacuous pass — the grep exit-2 conflation,
@@ -31,6 +32,7 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
VERIFY_BUILD="$ROOT/script/verify-build" VERIFY_BUILD="$ROOT/script/verify-build"
DISCARD_DIST="$ROOT/script/discard-dist-on-failure"
MARKER_ON="autistmask-build-debug=on" MARKER_ON="autistmask-build-debug=on"
MARKER_OFF="autistmask-build-debug=off" MARKER_OFF="autistmask-build-debug=off"
@@ -150,6 +152,7 @@ build_fixture() {
mkdir -p "$FIXTURE/script" mkdir -p "$FIXTURE/script"
ln -s "$VERIFY_BUILD" "$FIXTURE/script/verify-build" ln -s "$VERIFY_BUILD" "$FIXTURE/script/verify-build"
ln -s "$DISCARD_DIST" "$FIXTURE/script/discard-dist-on-failure"
mkdir -p "$FIXTURE/dist/chrome/src/popup" \ mkdir -p "$FIXTURE/dist/chrome/src/popup" \
"$FIXTURE/dist/chrome/src/content" \ "$FIXTURE/dist/chrome/src/content" \
@@ -513,12 +516,125 @@ c_debug_build() {
write_receipt write_receipt
} }
c_no_dist() { rm -rf dist; }
# --- dist discard -----------------------------------------------------------
#
# make build wraps every step of the release path in
# script/discard-dist-on-failure, so a release build that fails removes dist/:
# with AUTISTMASK_DEBUG=1 exported it has already emitted a complete, loadable
# debug bundle whose every wallet uses the publicly committed test recovery
# phrase, and a loud failure alone does not stop someone loading dist/chrome/
# anyway. make build-debug is deliberately not wrapped.
#
# Both directions are asserted against the state of dist/ ON DISK after the run,
# not against the exit status: a case reading only the status would keep passing
# if the removal quietly stopped happening, which is the flip this exists to
# catch. The wrapper runs against the fixture — its ROOT is the fixture, via the
# symlink in the fixture's script/ — with trivial commands standing in for the
# build steps, because what is under test is what happens after a step says no,
# not the step.
# discard_case <name> <setup> <status> <gone|kept> <want> <unwanted> [cmd...]
discard_case() {
_dc_name="$1"
_dc_setup="$2"
_dc_want_status="$3"
_dc_want_dist="$4"
_dc_want="$5"
_dc_unwanted="$6"
shift 6
build_fixture
if ! (cd "$FIXTURE" && "$_dc_setup") >/dev/null 2>&1; then
FAILED=$((FAILED + 1))
echo " FAIL: $_dc_name"
echo " the case's own setup failed, so nothing was tested."
return 0
fi
_dc_status=0
_dc_out="$(cd "$FIXTURE" &&
"$FIXTURE/script/discard-dist-on-failure" "$@" 2>&1)" || _dc_status=$?
_ok=yes
_why=""
if [ "$_dc_status" -ne "$_dc_want_status" ]; then
_ok=no
_why="exit status $_dc_status, wanted $_dc_want_status"
fi
# The assertion this case exists for: what is on disk now.
if [ -e "$FIXTURE/dist" ] || [ -h "$FIXTURE/dist" ]; then
_dc_dist=kept
else
_dc_dist=gone
fi
if [ "$_dc_dist" != "$_dc_want_dist" ]; then
_ok=no
_why="${_why:+$_why; }dist/ is $_dc_dist after the run, wanted"
_why="$_why $_dc_want_dist"
elif [ "$_dc_want_dist" = kept ] &&
[ ! -f "$FIXTURE/dist/chrome/src/popup/index.js" ]; then
# Kept has to mean intact: a dist/ emptied out is not one left alone.
_ok=no
_why="${_why:+$_why; }dist/ survived but its emitted bundle did not"
fi
_dc_check_message "$_dc_want" want
_dc_check_message "$_dc_unwanted" unwanted
if [ "$_ok" = yes ]; then
PASSED=$((PASSED + 1))
echo " ok: $_dc_name"
return 0
fi
FAILED=$((FAILED + 1))
echo " FAIL: $_dc_name"
echo " $_why"
echo " --- discard-dist-on-failure output ---"
printf '%s\n' "$_dc_out" | sed 's/^/ /'
echo " --- end output ---"
}
# Require ($2 = want) or forbid ($2 = unwanted) a substring in the wrapper's
# output, updating _ok and _why. An empty substring asserts nothing. Same grep
# discipline as everywhere else here: 0 and 1 are answers, anything else means
# the message was never checked.
_dc_check_message() {
[ -n "$1" ] || return 0
_dcm_g=0
printf '%s\n' "$_dc_out" | grep -q -F -e "$1" || _dcm_g=$?
case "$_dcm_g" in
0)
[ "$2" = unwanted ] || return 0
_ok=no
_why="${_why:+$_why; }message contained: $1"
;;
1)
[ "$2" = want ] || return 0
_ok=no
_why="${_why:+$_why; }message did not contain: $1"
;;
*)
_ok=no
_why="${_why:+$_why; }grep exited $_dcm_g matching the message, so the
message was never checked"
;;
esac
}
# --- Makefile wiring -------------------------------------------------------- # --- Makefile wiring --------------------------------------------------------
# The verifier cases above prove what verify-build does when it is told what to # The verifier cases above prove what verify-build does when it is told what to
# expect. This proves the Makefile tells it — with the mode as an argument, on # expect, and the discard cases prove what the wrapper does with dist/. This
# a scrubbed environment, and identically whether or not AUTISTMASK_DEBUG is # proves the Makefile wires both up — the mode as an argument, on a scrubbed
# exported in the shell that ran make. Read off `make -n`, so no build runs. # environment, identically whether or not AUTISTMASK_DEBUG is exported in the
# shell that ran make, and the wrapper on the release path only. Read off
# `make -n`, so no build runs.
check_makefile_wiring() { check_makefile_wiring() {
if ! command -v make >/dev/null 2>&1; then if ! command -v make >/dev/null 2>&1; then
SKIPPED=$((SKIPPED + 1)) SKIPPED=$((SKIPPED + 1))
@@ -537,6 +653,34 @@ check_makefile_wiring() {
build-debug "verify-build --expect debug" build-debug "verify-build --expect debug"
_wiring_case "make build-debug scrubs AUTISTMASK_DEBUG for the verifier" \ _wiring_case "make build-debug scrubs AUTISTMASK_DEBUG for the verifier" \
build-debug "env -u AUTISTMASK_DEBUG" build-debug "env -u AUTISTMASK_DEBUG"
# The release path runs its steps through the wrapper, including the final
# check-censored pass; the debug path runs none of them through it, which is
# what keeps a failed debug build's dist/ on disk.
_wiring_case "make build wraps its steps in discard-dist-on-failure" \
build "script/discard-dist-on-failure"
_wiring_case "make build wraps check-censored --require-dist too" \
build "script/discard-dist-on-failure script/check-censored"
_wiring_case_absent "make build-debug never discards its dist/" \
build-debug "discard-dist-on-failure"
}
# Run `make -n TARGET` with AUTISTMASK_DEBUG=1 exported, into _wc_out. Returns
# non-zero, having already reported the failure, when make itself failed: a
# recipe that could not be printed was never checked.
_wiring_make_n() {
AUTISTMASK_DEBUG=1
export AUTISTMASK_DEBUG
_wc_status=0
_wc_out="$(cd "$ROOT" && make -n "$_wc_target" 2>&1)" || _wc_status=$?
unset AUTISTMASK_DEBUG
[ "$_wc_status" -ne 0 ] || return 0
FAILED=$((FAILED + 1))
echo " FAIL: $_wc_name"
echo " make -n $_wc_target exited $_wc_status"
return 1
} }
_wiring_case() { _wiring_case() {
@@ -544,18 +688,7 @@ _wiring_case() {
_wc_target="$2" _wc_target="$2"
_wc_want="$3" _wc_want="$3"
AUTISTMASK_DEBUG=1 _wiring_make_n || return 0
export AUTISTMASK_DEBUG
_wc_status=0
_wc_out="$(cd "$ROOT" && make -n "$_wc_target" 2>&1)" || _wc_status=$?
unset AUTISTMASK_DEBUG
if [ "$_wc_status" -ne 0 ]; then
FAILED=$((FAILED + 1))
echo " FAIL: $_wc_name"
echo " make -n $_wc_target exited $_wc_status"
return 0
fi
_wc_g=0 _wc_g=0
printf '%s\n' "$_wc_out" | grep -q -F -e "$_wc_want" || _wc_g=$? printf '%s\n' "$_wc_out" | grep -q -F -e "$_wc_want" || _wc_g=$?
@@ -577,6 +710,34 @@ _wiring_case() {
esac esac
} }
# The inverse: the recipe must NOT run something.
_wiring_case_absent() {
_wc_name="$1"
_wc_target="$2"
_wc_want="$3"
_wiring_make_n || return 0
_wc_g=0
printf '%s\n' "$_wc_out" | grep -q -F -e "$_wc_want" || _wc_g=$?
case "$_wc_g" in
1)
PASSED=$((PASSED + 1))
echo " ok: $_wc_name"
;;
0)
FAILED=$((FAILED + 1))
echo " FAIL: $_wc_name"
echo " make -n $_wc_target runs: $_wc_want"
;;
*)
FAILED=$((FAILED + 1))
echo " FAIL: $_wc_name"
echo " grep exited $_wc_g, so the recipe was never checked"
;;
esac
}
run_cases() { run_cases() {
check_case "control: untouched dist passes" \ check_case "control: untouched dist passes" \
no release 0 "2 bundle(s) $MARKER_OFF" c_control no release 0 "2 bundle(s) $MARKER_OFF" c_control
@@ -712,6 +873,18 @@ run_cases() {
no release 1 "carries a debug marker but the build did not" \ no release 1 "carries a debug marker but the build did not" \
c_marker_on_plain_file c_marker_on_plain_file
discard_case "a failed release build step removes dist/" \
c_control 3 gone "dist/ WAS REMOVED" "" sh -c 'exit 3'
discard_case "a successful release build step leaves dist/ alone" \
c_control 0 kept "" "REMOVED" true
discard_case "a failed release build step with no dist/ says there was none" \
c_no_dist 3 gone "There was no dist/ to remove" "" sh -c 'exit 3'
discard_case "the wrapper given no command removes nothing" \
c_control 1 kept "no command given" ""
check_makefile_wiring check_makefile_wiring
} }
@@ -740,6 +913,10 @@ main() {
echo "test-verify-build: $VERIFY_BUILD is missing or not executable" >&2 echo "test-verify-build: $VERIFY_BUILD is missing or not executable" >&2
exit 1 exit 1
} }
[ -x "$DISCARD_DIST" ] || {
echo "test-verify-build: $DISCARD_DIST is missing or not executable" >&2
exit 1
}
echo "Testing script/verify-build failure modes..." echo "Testing script/verify-build failure modes..."
pick_sha256_tool pick_sha256_tool

View File

@@ -1,8 +1,10 @@
#!/bin/sh #!/bin/sh
# script/verify-build: assert that dist/ holds exactly what the build that just # script/verify-build: assert that the regular files and symlinks under dist/
# ran emitted, and that the compiled DEBUG state of that output is the one the # are exactly what the build that just ran emitted (other file types are out of
# caller asked for. Our own extension to scripts-to-rule-them-all, run at the # scope; see "What that does and does not establish" below), and that the
# end of make build / make build-debug. # 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 # 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 # phrase the output of wallet creation, so a release artifact built with it live
@@ -29,12 +31,16 @@
# path fresh per invocation, outside the repo, and deletes it afterwards. # 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 # 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, # byte the output of the build.js run that just finished, with no regular file
# nothing missing and nothing altered in between, and that the audited bundles # or symlink added, missing or altered in between, and that the audited bundles
# in it compiled to the requested mode. It does NOT establish that the source # in it compiled to the requested mode. Regular files and symlinks are the whole
# tree or build.js were honest, and it says nothing at all to someone handed a # of what the tree walk covers; fifos, sockets, device nodes and empty
# dist/ from elsewhere: without the receipt from its own build they have no # directories under dist/ are not checked, because a build emits none of them,
# input to this check. That is signing, and it is not this control. # none can carry a shippable payload, and grep on a fifo would hang rather than
# fail. 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 # It fails rather than passes whenever it cannot determine something. Minified
# output is not a stable contract, so "matched neither marker" is not evidence # output is not a stable contract, so "matched neither marker" is not evidence
@@ -392,8 +398,10 @@ check_receipt_entries() {
# its own command line, so a linked dist/ collapses this walk to one entry # its own command line, so a linked dist/ collapses this walk to one entry
# and cross-checks nothing. # and cross-checks nothing.
# #
# Types other than regular files and symlinks are left out on purpose: a build # Types other than regular files and symlinks — fifos, sockets, device nodes and
# emits none of them, and grep on a fifo would hang rather than fail. # empty directories — are left out on purpose, and the guarantee is bounded to
# what is walked: a build emits none of them, none can carry a shippable
# payload, and grep on a fifo would hang rather than fail.
check_dist_tree() { check_dist_tree() {
LISTING="$(mktemp "${TMPDIR:-/tmp}/verify-build-dist.XXXXXX")" || LISTING="$(mktemp "${TMPDIR:-/tmp}/verify-build-dist.XXXXXX")" ||
fail "could not create a temporary file for the dist/ listing, so the fail "could not create a temporary file for the dist/ listing, so the