Compare commits

..

1 Commits

Author SHA1 Message Date
177f592094 build: remove dist/ when a release build fails (closes #333)
All checks were successful
check / check (push) Successful in 50s
e2e / e2e-chrome (push) Successful in 1m25s
e2e / e2e-firefox (push) Successful in 43s
With AUTISTMASK_DEBUG=1 exported in the calling shell, make build compiled a
debug bundle and failed on it in script/verify-build, but left the bundle in
dist/: loadable, with every wallet it creates using the publicly committed test
recovery phrase from src/shared/constants.js. A failed release build that leaves
a loadable debug build behind is the trap the verifier exists to close.

Every step of make build now runs through script/discard-dist-on-failure, which
removes dist/ when its step fails and says on stderr that it did and why, then
returns the step's own exit status. A removal it cannot complete is reported as
loudly as one it can, naming what is still on disk. A step that succeeds removes
nothing, including the final check-censored --require-dist pass. It composes
with the existing receipt trap: the receipt is still deleted on the way out.

make build-debug is deliberately not wrapped. A debug build that failed is not
producing an artifact mistakable for a release one, and its dist/ is the
evidence of what went wrong.

script/test-verify-build asserts the state of dist/ on disk after a failing and
a succeeding step rather than the exit status alone, plus a step that fails with
no dist/ and a wrapper handed no command, and reads make -n to check the wrapper
is on the release path and absent from the debug one. Both directions were also
run end to end: AUTISTMASK_DEBUG=1 make build fails and leaves no dist/, plain
make build passes with all 15 emitted files intact, and a debug build failed
mid-write keeps its dist/.

verify-build itself is unchanged; this is only what happens after it says no.
2026-08-23 13:25:41 +00:00
3 changed files with 27 additions and 51 deletions

View File

@@ -85,21 +85,17 @@ 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 no regular file or symlink under `dist/` that the requested `DEBUG` marker, and nothing under `dist/` that the build did not
build did not write. The `Makefile` creates the receipt path with `mktemp` per write. The `Makefile` creates the receipt path with `mktemp` per invocation,
invocation, outside the repo, and deletes it afterwards. 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 no regular file or symlink output of the `build.js` run that just finished, with nothing added, removed or
added, removed or altered in between. Regular files and symlinks are the whole altered in between. It establishes nothing about whether the source tree or
of what the tree walk covers; fifos, sockets, device nodes and empty directories `build.js` were honest, and it offers nothing to someone handed a `dist/` from
under `dist/` are not checked, because a build emits none of them, none can elsewhere — without the receipt from its own build there is no input to the
carry a shippable payload, and `grep` on a fifo would hang rather than fail. It check. Verifiable provenance for a third party is signing, which this is not.
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
@@ -151,13 +147,12 @@ 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 the - `script/verify-build --expect release|debug --receipt PATH` — assert that
regular files and symlinks under `dist/` are exactly what the build that just `dist/` is exactly what the build that just ran emitted, and that the compiled
ran emitted (other file types are out of scope), and that the compiled `DEBUG` `DEBUG` state of the bundles in it is the one that was asked for. Both
state of the bundles in it is the one that was asked for. Both arguments are arguments are required and neither has a default: the expected mode is stated
required and neither has a default: the expected mode is stated by the caller by the caller rather than read from `AUTISTMASK_DEBUG`, and the file list
rather than read from `AUTISTMASK_DEBUG`, and the file list comes from the comes from the build's receipt rather than from `dist/` (see
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

13
TODO.md
View File

@@ -26,8 +26,7 @@ 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
hold exactly the regular files and symlinks that build emitted, with `DEBUG` be exactly what that build emitted with `DEBUG` compiled off.
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
@@ -58,16 +57,6 @@ but the review is broader than any of them.
`script/test-verify-build` asserts the state of `dist/` on disk after a `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` 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. 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-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

View File

@@ -1,10 +1,8 @@
#!/bin/sh #!/bin/sh
# script/verify-build: assert that the regular files and symlinks under dist/ # script/verify-build: assert that dist/ holds exactly what the build that just
# are exactly what the build that just ran emitted (other file types are out of # ran emitted, and that the compiled DEBUG state of that output is the one the
# scope; see "What that does and does not establish" below), and that the # caller asked for. Our own extension to scripts-to-rule-them-all, run at the
# compiled DEBUG state of that output is the one the caller asked for. Our own # end of make build / make build-debug.
# 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
@@ -31,16 +29,12 @@
# 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 no regular file # byte the output of the build.js run that just finished, with nothing added,
# or symlink added, missing or altered in between, and that the audited bundles # nothing missing and nothing altered in between, and that the audited bundles
# in it compiled to the requested mode. Regular files and symlinks are the whole # in it compiled to the requested mode. It does NOT establish that the source
# of what the tree walk covers; fifos, sockets, device nodes and empty # tree or build.js were honest, and it says nothing at all to someone handed a
# directories under dist/ are not checked, because a build emits none of them, # dist/ from elsewhere: without the receipt from its own build they have no
# none can carry a shippable payload, and grep on a fifo would hang rather than # input to this check. That is signing, and it is not this control.
# 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
@@ -398,10 +392,8 @@ 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 — fifos, sockets, device nodes and # Types other than regular files and symlinks are left out on purpose: a build
# empty directories — are left out on purpose, and the guarantee is bounded to # emits none of them, and grep on a fifo would hang rather than fail.
# 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