Repository navigation
fix(build): make the runtime chunk reproducible across build paths - #46829
gauthierpetetin wants to merge 1 commit into
Conversation
`runtime.[contenthash].js` could differ between rebuilds of identical sources, which breaks the `mtree` comparison Firefox AMO reviewers run and has been blocking Firefox submissions. SWC orders its base54 identifier alphabet by the character frequencies of the chunk it is minifying, so anything that changes the chunk's *contents* can reshuffle the alphabet and rename every short identifier. In the runtime chunk `c` and `l` are near-tied, and that chunk embeds webpack's provisional (pre-`RealContentHashPlugin`) chunk-filename hashes — hex, so they contain `c` but never `l`. Those hashes varied with the absolute build path because `swcLoader` returned its source map as a JSON string: webpack rewrites `sources` relative to the build context only for *object* maps (`NormalModule.contextifySourceMap` passes strings through), and the map is then hashed into `buildInfo.hash` verbatim, so the absolute `sourceFileName` reached every module's hash. `RealContentHashPlugin` later rewrote the hashes, which is why ~190 swapped identifiers were the only visible difference and the cause was invisible. - Mangle the runtime chunk with SWC's fixed alphabet (`mangle.disableCharFreq`) rather than not mangling it at all, recovering the ~12.4 KB (~1.9 KB gzipped) that #46236 traded away. - Hand webpack a parsed source map so absolute paths stay out of module hashes. Verified by building the same tree at two paths of different lengths: all 663 emitted files are now byte-identical, where previously the runtime chunk differed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
✨ Files requiring CODEOWNER review ✨👨🔧 @MetaMask/extension-platform (3 files, +50 -13)
👨🔧 @itsyoboieltr (3 files, +50 -13)
|
Builds ready [949cd25]
⚡ Performance Benchmarks (Total: 🟢 10 pass · 🟡 1 warn · 🔴 2 fail)
Bundle Size Diffs [🚀 Bundle size reduced!]
|
|
Full explanation (reference)The PR description is kept short; this comment preserves the complete plain-language walkthrough of the two root causes, with diagrams, for future readers. 1. The problem in one pictureMozilla reviewers rebuild MetaMask from our source and compare their result to the Exactly one file out of ~250 could differ, and only sometimes. That blocked Firefox releases. 2. What the difference actually was190 bytes out of 790,000. Every single one was the letter That's it. The code is identical in behaviour. But different bytes = different checksum = the 3. Why
|
|
Superseded by #46845, which goes straight to the end state: it fixes the root causes (swc loader source map, |


Description
runtime.[contenthash].jscould differ between rebuilds of identical sources, which fails thebyte-for-byte
mtreecomparison Firefox AMO reviewers run and has been blocking Firefoxsubmissions. The visible symptom was ~190 bytes of
c↔lswaps in mangled identifiers; nothingelse in the bundle changed. There are two root causes, and this PR fixes one of them and makes
the symptom impossible regardless of either.
Root cause 1 — the absolute build path leaks into every module hash (fixed here).
swcLoader.tshanded webpack the SWC source mapas a JSON string. webpack only strips absolute paths from object maps
(
NormalModule.contextifySourceMappasses strings through), and the map is then hashed intobuildInfo.hashverbatim. So the absolutesourceFileNamereached 11,331 / 11,435 module hashes and214 / 216 chunks' provisional
[contenthash]. This is whyprepare_release.sh(different path)flaked while the regular CI build and
bundle.sh(same path) never did.Root cause 2 — SWC's
process.envinlining cache carries stale positions (not in this PR).jsc.transform.optimizer.globals.envscaches the parsed env values per process with the spans ofwhichever file was transformed first, so later modules get one bogus source-map segment that
depends on thread timing. Already fixed upstream in
@swc/core1.16.2(swc-project/swc#12129, #12166); we are on 1.13.3. This is the flake seen at a fixed path and
will be closed by the
@swc/corebump.Why either shows up as
c↔l. The runtime chunk embeds the provisional hashes of all otherchunks as hex string literals. SWC orders its mangling alphabet by letter frequency over the chunk,
string literals included;
cis a hex digit,lis not. On v13.47.1candlwere adjacentranks with the flip at exactly 199 vs 200
cs in that table against a natural count of 201 ± 14(≈45 % of paths).
RealContentHashPluginthen rewrites the hashes, leaving the swapped identifiersas the only trace.
Changes
helpers.ts— mangle the runtime chunk with SWC's fixed alphabet(
mangle: { disableCharFreq: true }) instead of not mangling it at all (fix(build): disable SWC mangling on the runtime chunk for AMO determinism #46236). Identifierscan then no longer depend on the chunk's string contents, so neither root cause can reach the
output. Mangling is back on for the runtime chunk:
mangle: true(original, flaky)mangle: false(maintoday, fix(build): disable SWC mangling on the runtime chunk for AMO determinism #46236)disableCharFreq(this PR)swcLoader.ts— hand webpack a parsed source-map object so it relativizessources(root cause 1). Also makes the persistent build cache portable between checkouts.
loaders.swcLoader.test.ts— the existing test asserted the string form; it now assertsthe object contract and documents why.
Verification
v13.47.1 (the release that flaked) built at 20 different absolute paths, untouched (
control)and with this PR's changes applied as a patch (
fixed). The Report job's summary tabulatesthe runtime chunk name and a digest of all of
dist/firefoxper build. Result: controlproduced 2 different runtime chunks (12 ×
6197764d…, 8 ×d269eac6…, i.e. a 40 % fliprate across path lengths 89–204); fixed produced 1 runtime chunk and 1 dist digest across all
20 paths. (Hashes differ from the published release because the experiment uses placeholder
API keys; both variants share them.) Workflow and patch live on
test/amo-determinism-proof.runtime alphabet is the fixed
abcdef…. On v13.47.1,disableCharFreqproduced zero identifierswaps across the full range of
ccounts.yarn test:unit:webpack516/516;yarn webpack:tscclean.Full plain-language walkthrough with diagrams, kept for reference:
#46829 (comment)
Follow-ups
@swc/coreto ≥ 1.16.2 (root cause 2);bump/swc-core-amo-determinismalready carriesmost of the fallout (
IN_TESTinlining for npm loaders, LavaMoat policy updates — themselves aconsequence of swc#12166 keying the env cache correctly).
disableCharFreqcan optionally be dropped (+31 B gzipped to keep it).Changelog
CHANGELOG entry: null
Manual testing steps
yarn install, thenSOURCE_DATE_EPOCH=1700000000 ENABLE_MV3=false yarn webpack:lavamoat:build:mv2 --env productiondiff -r <copyA>/dist/firefox <copyB>/dist/firefox— verify no differences. Onmainbeforefix(build): disable SWC mangling on the runtime chunk for AMO determinism #46236,
runtime.<hash>.jsdiffered between the two roughly half the time.dist/firefoxas a temporary add-on in Firefox (about:debugging), unlock or onboard,and verify the UI renders with no
LavaDome/lockdown/module is undefinedconsole errors(the runtime chunk hosts the SES/LavaMoat runtime and is mangled again by this change).
Pre-merge author checklist
Pre-merge reviewer checklist
🤖 Generated with Claude Code