Skip to content

fix(build): scripts ship as written, strict directives, verify:linux works on Node 26, checked tool inputs - #311

Merged
Nawid3333 merged 14 commits into
mainfrom
claude/issues-buildtools
Oct 3, 2026
Merged

Nawid3333 merged 14 commits into
mainfrom
claude/issues-buildtools

Conversation

@Nawid3333

@Nawid3333 Nawid3333 commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Build and developer tools: fixes from the code review

What was wrong, as you'd notice it:

  • The build deleted every blank line in every script, including inside text. One error message in hls.js shipped a line short, and no shipped library matched the file it comes from on npm.
  • A misspelt build directive (// SPLICER:WEB:REMOVE_LIN) was ignored without a word, so code meant to be removed shipped. And a directive's text inside a string was obeyed.
  • The web build's skip list matched by name prefix, so a future file called backgroundAudio.mjs would have been left out silently.
  • pnpm run start:ff couldn't start Firefox on Linux or macOS.
  • pnpm run verify:linux was already broken: Node 26 no longer ships corepack, and it also needs a library WSL Ubuntu 24.04 lacks. It also skipped zizmor, so a workflow change could pass locally and fail on GitHub.
  • node localescript.mjs (part of every build) crashed if combined-locales.json was missing.
  • pnpm run gen-locale with a wrong language code paid for every translation and then saved none. Its help named the wrong defaults, and it could wait forever on a stalled request.
  • The dev profile installed whatever uBlock Origin version AMO had that day, unchecked.
  • Nothing checked that the patched libraries really had their patches applied.
  • The WSL setup checked the Firefox it installs against a checksum from the same server. That catches a broken download, but not a swapped one.

What changed:

  • Scripts now ship exactly as written, apart from the directive lines.
  • Directives are checked strictly, and the build stops on one it doesn't know.
  • The splicer code now lives in tools/splicer.mjs.
  • The launchers start web-ext correctly on every system.
  • The WSL setup installs pnpm without corepack, adds libatomic1, and runs zizmor like GitHub does.
  • localescript and gen-locale check their input first and say what's wrong.
  • uBlock Origin is pinned to 1.75.0 and its file is checked.
  • The vendor sync stops if a patched library isn't patched.
  • The WSL setup installs Firefox only when its checksum list carries a good signature from Mozilla's release key. The key is pinned by its fingerprint (14F2 6682 D091 6CDD 81E3 7B6D 61B7 B526 D98F 0353, Mozilla Software Releases); the key files of four Firefox releases since 2018 and Ubuntu's key server all show the same one. Another key, a bad, expired or missing signature, or a tarball that doesn't match stops the setup. gpg works in a throwaway folder and never touches a keyring of yours. Node's download is checked as before.

How it's proven:

  • Every fix has a unit test that fails without it. I re-checked one myself: silencing the unknown-directive error turns three tests red.
  • Every change except the blank lines leaves the built add-on byte for byte the same (all three targets compared).
  • The Linux scripts pass shellcheck, and were run piece by piece in WSL, into a temporary folder only.
  • The signature check is tested with fake releases signed by keys the test makes: a good one passes, and eight kinds of bad one are refused, each check undone once to watch the test fail. It also passed on the real Firefox 157.0 download in WSL, and refused it under another fingerprint.
  • Lint, typecheck, 1009 unit tests, every workflow script test and actionlint pass.

Note: keeping the blank lines changes every shipped script slightly (the zips grow about 6 KB), so merging this makes a release. That one change is its own commit (38f556f), if you'd rather not have it.

Fixes #165
Fixes #168
Fixes #174
Fixes #175
Fixes #176
Fixes #177
Fixes #178
Fixes #238
Fixes #239
Fixes #245
Fixes #246
Fixes #248

Not fixed here:

🤖 Generated with Claude Code

Nawid3333 and others added 11 commits October 3, 2026 15:48
splice() and spliceAndCopy() move out of build.mjs unchanged (the moved
text diffs clean against the old lines), so the unit tests can run the
splicer: build.mjs deletes and rebuilds the build directories as soon as it
is loaded.

Proven output-neutral: the three build directories (firefox-github,
firefox-amo, web) are byte-identical to a build of the parent commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
miniglob took its separator from require('path').sep, but an ES module run
by Node has no require, so it was always '/', even on Windows: a pattern
built with path.join() (backslashes) matched nothing there, silently. Under
vitest, which does define require, it was '\' instead, and build.mjs's own
glob(dir + '/**') found nothing. Its separator is now '/' everywhere (Node
takes it in Windows paths), and on Windows glob() reads a backslash in a
pattern as a separator, as match() already did (no escaping there).

tests/unit/miniglob.test.mjs covers both patterns, plus Windows simulated
on Linux; taking out either half of the fix fails it. The three build
directories are byte-identical to the baseline build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A misspelt directive (// SPLICER:WEB:REMOVE_LIN) was kept as an ordinary
line, so the line it was meant to remove shipped, with no error; a misspelt
target was never acted on by any build. And the directive's text was obeyed
anywhere in a line: SPLICER:WEB:REMOVE_FILE in a template literal emptied
the file.

A directive is now a // comment that is exactly SPLICER:<TARGET>:<COMMAND>
(alone or after code, as all 12 in the tree are); any other `// SPLICER:`
comment, an unknown target or an unknown command fails the build with the
file and line. The same text in a string or a block comment is left alone.

tests/unit/splicer.test.mjs: five of its tests fail on the old splicer;
one checks every directive in the tree parses for every target. The three
build directories are byte-identical to the baseline build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The splicer deleted every blank line of every .js/.mjs it copied, the ones
inside template literals too, so a multi-line string shipped changed. It
did today: hls.js's getSequenceError message lost its blank line. It also
meant no shipped library matched the file sync-vendor writes from npm.

Blank lines now stay. Measured on this tree, all three targets: only
scripts differ from the previous build, each only by blank lines; 177 of
the 180 shipped scripts are now byte-identical to their file in chrome/
(the rest carry a directive, or are mp4box's three files, which npm
publishes without a final newline and which get one, as before). The zips
grow by about 6 KB of 7.35 MB.

tests/unit/splicer.test.mjs: "keeps blank lines" and "ships a script
without directives exactly as written" fail with the old filter back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts (#175, #174)

#175: the web build's exclusion list was matched as a raw string prefix,
so 'background' and 'custom' (directories) would also have dropped a future
root-level backgroundAudio.mjs or customElements.mjs, silently; on Windows
an entry with a '/' never matched at all (the relative path has '\'). An
entry now names a file or a directory, by whole path segments.

#174: every file was read as UTF-8 text before the exclusion check and the
script test, the wasm, the VAD model and the images included, and the text
thrown away. Only scripts are read now; the rest is copied as bytes, as
before. Measured: copying chrome/ for one target, median of 5, 290 ms ->
208 ms.

tests/unit/splicer.test.mjs: undoing either change fails its test
("leaves out what the exclusion list names...", "reads only the
scripts..."). The three build directories are byte-identical to the build
of the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both launchers joined pnpm and web-ext's arguments into one string and
spawned it with `shell: false` everywhere but Windows, so Node looked for a
program named "pnpm exec web-ext run --source-dir ..." and the launch died
with spawn ENOENT, after the whole rebuild. The quotes around paths with
spaces would also have reached web-ext as part of the path.

tools/rebuild.mjs's new webExtSpawn() gives both: on Windows the same
quoted command line through a shell as before (pnpm.cmd needs one), and
elsewhere pnpm itself with the arguments as they are. A failed spawn now
says so instead of throwing an unhandled 'error' event.

tests/unit/launchWebExt.test.mjs: the Linux and macOS cases fail with the
old joined string, the Windows case checks the command line is unchanged,
and a check that both launchers spawn through it fails on the old launcher.
(Firefox is not launched by any test.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With combined-locales.json absent (deleted to regenerate it, or a checkout
without it), localescript.mjs read it as undefined and its key check threw
"Cannot read properties of undefined (reading 'get')", which failed every
build with an unrelated-looking error; `--combine` could not recreate the
file either. And `--whitelist` as the last argument threw on .split.

A missing file now reads as no combined locales: the build's check runs on
the locale files, `--combine` writes a new combined file, and `--split`
stops with "Nothing to split" instead of rewriting nothing. `--whitelist`
without a list (last, or followed by another option) says what it needs.
On this tree the build's run prints exactly what it printed before.

tests/unit/localescript.test.mjs runs a copy in a temp tree: all five
tests fail on the old script, and taking out any one of the three guards
fails its own test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, gives up on a stalled request (#246)

`node generate-locales.mjs --lang french` (or zh-cn, the help's own
example) ran every batch against the paid API, then wrote "0
translations": the code's shape was checked only when the results were
applied. It is now checked with the other arguments, before any request,
and the help's example is zh_CN, as chrome/_locales names it. The help also
said the default model was gpt-4o-mini (it is gpt-5-mini) and 5 threads
(it is 10).

A request that never got an answer waited forever: requests now give up
after --timeout seconds (default 300) and the batch retries, as for any
other failed request.

tests/unit/generateLocales.test.mjs (no real API: no key, or a local
server that accepts and never answers): the malformed-code, help and
timeout tests fail on the old script, and taking out the new check or the
timeout fails its test. Against that server the old script was still
waiting after 12 s; the new one gives up after its three attempts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#238, #239)

#238: setup.sh installed pnpm through corepack, which Node 25 and later no
longer ship. .nvmrc moved to 26 on 2026-10-01, so the next verify:linux
would have linked a corepack that is not there and stopped at
`corepack enable`. pnpm now comes from package.json's packageManager
version through that Node's own npm (as pnpm/action-setup gives CI), over
the old corepack links. Trying that showed a second break: Node 26's binary
needs libatomic.so.1, which a WSL Ubuntu 24.04 lacks (the runner image and
26.04 have it), so every `node` failed to start; libatomic1 joins the apt
list.

#239: CI's workflows job runs actionlint, then zizmor, then the run:
scripts' tests; verify.sh skipped zizmor, so a workflow change only zizmor
flags passed verify:linux and failed CI. setup.sh now takes the zizmor
binary out of the image .github/zizmor/Dockerfile pins, the way it takes
actionlint and shellcheck out of theirs (one digest-checked image_files
function for both), and verify.sh runs `zizmor --offline` on .github/.

setup.sh also passes shellcheck now: a comment line starting "# shellcheck
binaries..." was read as a broken directive, so shellcheck had stopped at
line 11. The AMO build doc no longer leads with `corepack enable`.

Proven without running setup.sh or verify.sh: bash -n, shellcheck 0.11.0
and actionlint clean; their pieces, taken from setup.sh itself, run in WSL
into /tmp only: both images' files came out digest-checked (a wrong digest
refused, the old directory kept), zizmor 1.30.1 passed this tree (no
findings), Node 26.10.0 did not start without libatomic1, and its npm
installed pnpm 11.28.0 with the pnpm and pnpx links.
tests/unit/linuxScripts.test.mjs: all four checks fail on the old scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… release (#177)

profile:setup put AMO's `latest` uBlock Origin into the persistent dev
profile: whatever AMO served that day, no version pin, no hash, the one
download in the tool set without either. It is now 1.75.0 by its file URL
and SHA-256, as .github/mpv-build.json pins mpv (web-ext turns add-on
updates off in that profile, so the version stays as installed either way).
The file is written under a .part name and kept only when its hash
matches, so a bad download is never taken for "already present" next time;
the version is printed. The comment says how to move the pin.

The pin was read from AMO's API (current_version 1.75.0, file 5034826) and
checked: my own SHA-256 of the download matches it, its manifest says
1.75.0, it carries Mozilla's signature files, and AMO's `latest` URL
redirects to this same file today.

The script now runs only as a command, so tests can import it.
tests/unit/setupDevProfile.test.mjs (a local server; no profile is touched,
AMO is not reached): without the hash check the wrong-hash test fails, and
with the `latest` URL back the pin test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plied (#176)

sync-vendor.mjs's `patched: true` was documentation only. With a
patchedDependencies line lost from pnpm-workspace.yaml (a bad merge), or a
library installed at another version than its patch is cut against (a
stale node_modules), the stock library was copied and the build stayed
green; e2e would likely catch hls.js or dash.js, not gif.js, Coloris or
sweetalert2.

unappliedPatches() (beside patchedDependencies() in
check-patched-updates.mjs) lists such libraries, and the sync stops on any
before copying, saying what to do. When recut-patch.mjs finds that every
change landed upstream, the sync now stops until the marks come out, which
that path already asks for (its pull request also failed the existing
"is the list Dependabot is told to ignore" test).

tests/unit/syncVendorPatches.test.mjs: each failing case fails with its
branch taken out, and one test checks this repository's marks, yaml and
installed versions. On the real sync, with hls.js's yaml line removed the
old script exits 0 and the new one exits 1 (likewise for a 1.7.2 key);
the yaml was restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread tests/unit/syncVendorPatches.test.mjs Fixed
Nawid3333 and others added 3 commits October 3, 2026 17:12
…L js/redos)

`[^}]` also took the `$` and `{` that start a `${...}`, so the two alternatives overlapped.
A `$` now counts as plain text only when no `{` follows. Same matches on tools/sync-vendor.mjs
(all six patched libraries), and CodeQL's hostile input takes 0 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ure on its sum (#248)

setup.sh checked the Firefox tarball against SHA256SUMS fetched from the
same server, which catches a broken download but not a swapped one: whoever
can serve the tarball can serve a matching sum. It now fetches SHA512SUMS,
SHA512SUMS.asc and KEY for the release, and verify_signed_sum stops setup
unless KEY holds exactly one primary key, the pinned one; SHA512SUMS.asc is
a good signature by it (GOODSIG: gpg exits 0 for a signature by an expired
key, so its exit status alone would not do); and the SHA-512 that list
gives the tarball, named exactly once, is the tarball's. gpg works in a
throwaway home with --no-autostart: no keyring of the machine's is read or
written, and no agent is left running. gpg joins the apt list. Node stays
as it is (its signers change too often to pin).

The pin, 14F26682D0916CDD81E37B6D61B7B526D98F0353 (Mozilla Software
Releases <release@mozilla.com>, rsa4096, 2015), is what gpg --show-keys
reads from the KEY files published with Firefox 60.0, 115.0, 140.0esr and
157.0 and from keyserver.ubuntu.com, all the same (in WSL, a throwaway
gpg home). Firefox 157.0's real SHA512SUMS.asc verifies with it, signed
by the current subkey 678E455D76767AA3.

tests/workflows/linux-firefox-signature.test.sh runs setup.sh's own
function on releases made with keys made by the test (no network): a good
set passes; a wrong pinned fingerprint, another key's KEY and signature, a
KEY with an extra key, a list changed after signing, a signature by a key
not in KEY, one by an expired key, a tarball that does not match and one
the list does not name are refused. Taking out the KEY check, the
signature check, the GOODSIG rule, the listed-once rule or the hash check
each fails its checks. tests/unit/linuxScripts.test.mjs checks the pin
and that the check runs before the tarball is unpacked; it fails on the
previous setup.sh. setup.sh's function on the real Firefox 157.0 files, in
WSL /tmp: passes with the pinned key, refused with another fingerprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…process.exit (CodeQL js/user-controlled-bypass)

CodeQL counts process.exit() as a security decision, and the argument checks that lead to
it as a bypass of it: the --whitelist check this PR added made two new alerts (and main
had four on the same loop). The first bad argument is now kept, reported and ends the run
with process.exitCode = 1; the work after the arguments is main(), which runs only when
they are all good. Nothing else changed: the remaining exits stay as they were.

The same in 20 runs, before and after, by exit status, stdout, stderr and the files it
leaves: no arguments (the build's run), --combine, --split with and without
combined-locales.json, --combine --split, --whitelist with no list, a list or an empty
one, invalid arguments first, last and after a list. `node localescript.mjs` on the real
tree prints the same. CodeQL 2.27.1 (security-extended, local threat model, as this
repository's code scanning): no localescript.mjs alert left, nothing new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017JiuRmQkMrNTDSUbPnwKsn
@Nawid3333
Nawid3333 merged commit fff21fc into main Oct 3, 2026
9 of 10 checks passed
@Nawid3333
Nawid3333 deleted the claude/issues-buildtools branch October 3, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants