@hominis/fireforge 0.44.2 → 0.44.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/build.js +9 -4
- package/dist/src/commands/furnace/create-browser-test.d.ts +9 -0
- package/dist/src/commands/furnace/create-browser-test.js +6 -6
- package/dist/src/commands/furnace/create-dry-run.d.ts +10 -0
- package/dist/src/commands/furnace/create-dry-run.js +16 -11
- package/dist/src/commands/furnace/create-xpcshell.js +2 -2
- package/dist/src/commands/furnace/create.js +5 -0
- package/dist/src/commands/test-build-phase.js +1 -1
- package/dist/src/commands/token.js +3 -1
- package/dist/src/core/build-audit.d.ts +20 -0
- package/dist/src/core/build-audit.js +25 -1
- package/dist/src/core/build-baseline-types.d.ts +28 -0
- package/dist/src/core/build-baseline.d.ts +16 -1
- package/dist/src/core/build-baseline.js +38 -2
- package/dist/src/core/build-prepare.js +32 -3
- package/dist/src/core/engine-changes.d.ts +18 -0
- package/dist/src/core/engine-changes.js +34 -0
- package/dist/src/core/furnace-constants.d.ts +21 -0
- package/dist/src/core/furnace-constants.js +26 -1
- package/dist/src/core/license-headers.d.ts +9 -5
- package/dist/src/core/license-headers.js +33 -7
- package/dist/src/core/test-stale-check.js +2 -13
- package/dist/src/core/token-variant.d.ts +12 -2
- package/dist/src/core/token-variant.js +106 -17
- package/docs/README.md +35 -0
- package/docs/configuration.md +116 -0
- package/docs/exit-codes.md +63 -0
- package/docs/furnace.md +57 -0
- package/docs/lifecycle-invariants.md +309 -0
- package/docs/machine-output.md +94 -0
- package/docs/patch-workflow.md +119 -0
- package/docs/run-logs.md +60 -0
- package/docs/testing.md +123 -0
- package/docs/verification-trees.md +48 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -86,6 +86,8 @@
|
|
|
86
86
|
- **`token add --variant` required a flag that described nothing, rejected its companion, and reported a no-op as success.** Three separate frictions, all in the variant path. `--category` was an unconditional `requiredOption` that had to name a REAL category even under `--variant`, where the declaration is routed into a `:root<selector>` block and never into a category section — the requirement existed so the argument would not be "silently discarded", a fair goal whose effect was a mandatory argument saying nothing about where the token lands. It is now optional there (still validated when supplied, so a typo is not accepted just because the flag is inert), and its absence on a BASE declaration is refused with the REASON stated rather than commander's bare "required option not specified".
|
|
87
87
|
`--create-category` is still rejected with `--variant` — the two really are incompatible — but the working create-then-variant order (author the category with the base token, then add each variant) was discoverable only by failing twice. It is now in both the error and `--variant`'s help text.
|
|
88
88
|
And a token already present in the target variant block was skipped with a bare `{ added: false }`, so a re-run intending to CHANGE a value exited 0 having changed nothing, while the base path had been reporting the category and line for the same situation all along. The variant path now returns the location too, and the skip reads `already exists in :root[…] (line N), unchanged` — the block being the location, since a variant declaration has no category to name.
|
|
89
|
+
- **The CC0 public-domain dedication Mozilla puts on its test files was not a recognized license header.** `containsUpstreamLicenseText` knew MPL, Apache, MIT, GPL and a bare SPDX tag; nothing knew `Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/`. A modified upstream test whose notice was UNCHANGED therefore warned `modified-file-missing-header`, and the downstream workaround — two identifier-only edits, an `SPDX-License-Identifier: CC0-1.0` line above each untouched notice on patch 905 — was editing Mozilla's notice to satisfy FireForge's recognizer. Worse than filed: `hasThirdPartyPermissiveBanner` did not know it either, so a NEW file copied from the upstream test tree was neither "acceptable" nor "vendored", and `export -y` would have prepended the PROJECT's license header onto code Mozilla dedicated to the public domain — precisely the mislicensing the vendored carve-out (0.38.0) exists to prevent.
|
|
90
|
+
Both predicates now recognize the dedication in every spelling the tree carries — the prose, the `creativecommons.org/publicdomain/zero/1.0/` URL with either scheme, and the `CC0-1.0` SPDX identifier — through one shared matcher, so the two cannot drift. Deliberately narrow, with the negative controls pinned: a different Creative Commons license (`licenses/by/4.0/`, `CC-BY-4.0`), a made-up `publicdomain/zero/2.0/`, a bare "public domain" mention, and a dedication below the scanned head are all still refused.
|
|
89
91
|
|
|
90
92
|
### Security
|
|
91
93
|
|
|
@@ -108,6 +110,10 @@
|
|
|
108
110
|
### Performance
|
|
109
111
|
|
|
110
112
|
- **`getAllDiff` spawned one `git hash-object` per untracked file, uncapped.** Its sibling `getDiffForFilesAgainstHead` was reshaped into classify → one batched hash → render for exactly this reason; this path kept the per-file form, and it is the only path behind `tree create`, `tree list`, `tree remove --all`, `lint` and `export-all` — against a Firefox tree where "untracked" can mean a very large number of files. Now batched. Everything observable is deliberately unchanged (renames, `ls-files` ordering, tracked-block-first emission, the `'\n'` empty sentinel) because this output is SHA-256'd into every tree fingerprint and written verbatim into `.patch` files; a new real-git test pins those bytes, which nothing did before.
|
|
113
|
+
- **A dirty `jar.mn` escalated EVERY `test --build` to a full `mach build`, and a dirty `moz.build` re-ran `mach configure` every time — including immediately after the full build that had consumed exactly that content.** A downstream session measured it: 15:00 for the first full build, 14:03 for the next `test --build` because "`jar.mn` changed relative to its own successful-build record", and 17 s for the CSS-only fix that followed. Two of those three builds were the same work twice.
|
|
114
|
+
The cause is what "changed" meant. `collectChangedEnginePaths` unions the baseline-to-HEAD diff with every WORKTREE modification, and a fork's worktree is permanently dirty — imported patches plus Furnace-applied components are a standing diff against HEAD — so a patch-touched `jar.mn` was "changed" on every run for as long as the patch existed. The baseline already solved this for packageable files (`packageableFingerprints`, 0.40.0) and for `components.conf` (the static-components anchor), and both consumers compare content, not dirtiness; the build inputs were the one family with no fingerprint, because `isPackageablePath` excludes them by design and nothing else recorded them.
|
|
115
|
+
`last-build.json` now carries `buildInputFingerprints` — a content hash per dirty `jar.mn` / `moz.build` / `moz.configure` / `Makefile.in` / `mozbuild.in` — and `build-prepare` drops any input whose live hash still matches before deciding either escalation. The two halves refresh on different rules, and the difference is load-bearing: backend inputs are refreshed on every successful write, because every baseline-writing path ran the auto-configure preflight against the previous record first; `jar.mn` entries are refreshed only by a FULL build and carried forward verbatim on a `faster` one, because `fireforge build --ui` is a `mach build faster` that never escalates, and recording a `jar.mn` no full build has installed would silence the next escalation. `build --ui` therefore now says which kind of build it was (`writeBuildBaseline`'s new `buildKind`), and passes the previous baseline it was already holding. An unreadable file, a missing entry, or a marker written before the field existed all count as changed — "cannot prove unchanged" never becomes "unchanged". The stale-build preflight's hash loop became the shared `dropPathsMatchingFingerprints` so both consumers compare the same way.
|
|
116
|
+
**On the escalation itself** — the downstream ask was a scoped install-manifest refresh for jar-only registrations, or a reason full compilation is necessary. Read against mozbuild: every `mach build`, `faster` included, first runs `backend_out_of_date` and regenerates the backend through `config.status` when any `backend.*Backend.in` dependency is newer, and `jar.mn` files are such dependencies; the FasterMake backend consumes `JARManifest` objects for `dist/bin` targets into its own install manifests. So for an entry added to an EXISTING `jar.mn` under `dist/bin` — the Furnace widget case — a `faster` build likely installs the destination, and the 0.41.0 rule ("full build for new `jar.mn` registrations") looks over-broad. It is NOT relaxed here: the rule came from an observed failure whose specifics were not recorded, and the claim needs a real objdir to prove, which a source reading is not. The seam is already available to prove it with: `fireforge build --ui` never escalates, so a jar-only registration followed by `build --ui`, a check that `obj-*/faster/install_dist_bin` names the new destination and the file landed under `dist/bin`, then a plain `fireforge test`, is the experiment; two clean runs of it make narrowing the escalation (to new `jar.mn` files and non-`dist/bin` targets) a safe change. Recorded in `docs/testing.md` so the next reader inherits the analysis rather than the guess. Also recorded, not fixed: `build --ui` ignores `fullBuildRequired` and `package` runs `prepareBuildEnvironment` with no baseline at all, so neither auto-configures nor escalates.
|
|
111
117
|
|
|
112
118
|
### Drift collapsed
|
|
113
119
|
|
|
@@ -149,6 +155,9 @@
|
|
|
149
155
|
- **`fireforge test --doctor` printed the same preflight line three times** on a terminal: a 2026-04-24 fix added an emission for non-TTY capture without removing the two it superseded. The raw writes are gated on non-TTY, which is where the capture requirement applies; the clack framing stays for terminals.
|
|
150
156
|
- **All seven** `--wait-lock` commands that take the patch-directory lock now honour it: `patch tier`, `patch lint-ignore`, `patch staged-dependency`, `export-all`, `resolve`, `doctor` and `rebase`. Each routes through a different lock entry point, so the budget is threaded through `updatePatchMetadata`, `mutatePatchMetadata`, `commitExportedPatch`, `updatePatchAndMetadata`, `rebuildPatchesManifest` and the rebase patch loop.
|
|
151
157
|
- `status --json` outside a project wrote a clack-styled block to **stdout** and exited 2, because the config error is raised before the command body engages machine mode. The boundary now engages it from the flag, so stdout carries exactly one JSON envelope and the human guidance goes to stderr — rule 3 of `docs/machine-output.md`, which was aspirational until now. A payload that already owns stdout (`status --json --fail-on`) suppresses the envelope, so the "exactly one document" half of the contract holds too.
|
|
158
|
+
- **`--variant` refused selectors the matcher had just learned to find.** 0.44.2 widened `rootAttributeSelector` to consume consecutive `[…]` groups and to stop at a pseudo-class tail, so `:root[data-skin="humanist"][data-theme="dark"]` and `:root[data-theme="light"]:not([data-private])` became blocks FireForge can locate and write through. The CLI validator did not move with it: its own regex still accepted exactly one `[attr]` fragment, so `token add --variant '[data-skin=humanist]:not([data-private])'` was refused as "must be a single attribute selector" — a block the tool can FIND was one it would not let you NAME, and the private-exclusion selectors the widening exists for are precisely that shape. Downstream the values were maintained by hand in the allowlisted block instead.
|
|
159
|
+
The validator now runs the MATCHER'S parse and requires it to consume the whole fragment, rather than carrying a second grammar that can drift from the first again. Character safety stays a separate check, because that parse finds bracket boundaries and validates no characters at all — it is applied one bracket group at a time rather than as a repeated pattern, which is the shape that backtracks (`js/polynomial-redos`), and a linearity test pins that alongside the matcher's own. Normalization to `="value"` now covers every fragment in a run and the `:not(…)` argument, not just a trailing one.
|
|
160
|
+
`findVariantBlock` had to move with it: it compared a request against a block's ATTRIBUTE run only, so a variant carrying its own qualifier would have matched nothing and spliced a fresh duplicate block on every add. Request and candidate go through the same parse and are compared tail-to-tail. An UNqualified request still matches a qualified block — that is the 0.44.2 behaviour `variantBlockQualifier` exists to report out loud, since silently skipping the block is the worse failure — but a request that names a qualifier means it.
|
|
152
161
|
- `tree list --json` gained the failure envelope `status --json` already emitted, so a scripted consumer no longer gets a parseable refusal from one and a bare non-zero exit from the other. The contract is written down in `docs/machine-output.md`.
|
|
153
162
|
|
|
154
163
|
### More correctness
|
|
@@ -158,6 +167,7 @@
|
|
|
158
167
|
- `RebasePatchEntry` became a discriminated union on `status`, so a status flip can no longer strand the previous status's payload — which `rebase --continue` did, persisting `{ status: 'resolved', error, conflictingFiles }` to the session file. The on-disk validator is deliberately NOT tightened to match: the session file carries no schema version, so a stricter validator would refuse a 0.43.x file mid-rebase, where the only remedies discard the operator's conflict resolution. Entries are normalized on read instead, and two tests pin that an older session still loads.
|
|
159
168
|
- **A `patches.json` from a newer FireForge was reported as malformed.** `patches.json version must be 1` sent the operator looking for damage in a file that only needed a newer build to read it, and the manifest had no migration hook at all. It now refuses by name — naming the found and supported schema versions and telling the operator not to delete the file — while a version below 1 or non-integer keeps the plain malformed message, because that one really is malformed.
|
|
160
169
|
- The three JSDoc walkers in `patch-lint-jsdoc.ts` return their issues instead of pushing into an out-param, matching the ~30 other patch-lint entry points. The new `jsdoc/check-param-names` rule immediately caught the stale `@param issues` the change left behind — which is the point of enabling it.
|
|
170
|
+
- **`furnace create --test-dir` was honoured by the scaffolder and ignored by everything that told the operator where the files went.** `create.ts` resolved the override once and passed it to the scaffolders, but `formatDryRunPlan` and `formatSuccessNote` received only `binaryName` and recomputed the DEFAULT from it — so `--dry-run` planned `test/hominis/` plus `hominis/browser.toml` in `moz.build` for a command that would write to `test/hominis-tiles/`, and the success note after the real run named `hominis/` while the files sat in `hominis-tiles/`. The one message an operator gets as confirmation was the one thing that could not be trusted. Both formatters now take the resolved directory, and the default lives in one place: `resolveBrowserChromeTestDir` / `resolveXpcshellTestDir` in `furnace-constants.ts`, called by the scaffolders AND the formatters, so the two cannot disagree again. The moz.build registration line follows the override too (it is the path below `browser/base/content/test/`, which under `--test-dir` is not the binary name), and a test scaffolds for real and asserts the printed directory is the one on disk — default and overridden, browser-chrome and xpcshell.
|
|
161
171
|
|
|
162
172
|
### Windows
|
|
163
173
|
|
|
@@ -180,6 +190,8 @@ On the test side: path expectations and mock keys are rendered through a new `na
|
|
|
180
190
|
- **`test.test.ts` was 2,943 lines under one `describe`.** What made it unsplittable was its own 190-line `vi.mock` header: any split either duplicated the header into each new file or did not happen. The header moved to `test-command-mocks.ts` as one factory per mocked module, called from inside each file's own `vi.mock` callback — hoisting is unaffected and every suite still gets fresh `vi.fn()`s rather than shared call history. The file is now six, largest 952 lines, split on the seams the tests already formed: discovery and failure rewriting, staleness and packaging coverage, xpcshell/mochitest hints, Marionette forwarding, harness resilience, verdict contract.
|
|
181
191
|
The split exposed a real test-isolation bug: `keeps the Marionette preflight for xpcshell-only --doctor runs` had never set a preflight result, and passed only because an `it` roughly 1,000 lines earlier had left one on the shared mock with `mockResolvedValue`. It sets its own now. Coverage of `src/commands/test.ts` is byte-identical before and after the split (95.12 / 94.28 / 100 / 86.23), which is the check that the mock rewiring did not quietly stop exercising anything.
|
|
182
192
|
- Five rollback tests moved from asserting a module-level spy to asserting the persisted pending-repair marker. The spy stopped working when the sequence moved into `completeJournalRollback` (its call to the recorder is intra-module), and the outcome is the better assertion anyway.
|
|
193
|
+
- **Fourteen verdict assertions passed only because writing to a filesystem root fails.** The command suites pass `/project` as the project root and assert the `FIREFORGE-VERDICT:` line by exact string — which holds only while no run log is open, since the line carries an open log's path as a ` log=<path>` key. On POSIX `mkdir('/project/.fireforge/logs')` fails at the root, the best-effort open degrades to "no log", and the assertions passed for a reason none of them stated. On Windows the same path resolves against the CURRENT DRIVE, the mkdir succeeds, and all fourteen failed — while the run also created real `C:\project\.fireforge\logs` directories on the runner. `createRunLogMock()` joins the two factories in `module-mocks.ts` and the five suites state the precondition instead of inheriting it from a permission error. Because mocking the module away removes the suffix's only coverage, `test-verdict.test.ts` gains the opposite assertion: with a log open, the path rides the line, once, as the run's last stdout write.
|
|
194
|
+
- **A lock-wait test decided its result on a `readdir`.** `a wait whose queue position keeps improving > survives a drain longer than its budget` runs on real timers and a real filesystem, and gave each probe about 20 ms of slack inside a 100 ms budget; it also retired its simulated queue with un-awaited `rm` calls, so a retirement could land after the probe meant to observe it and a release could leave `mkdir` seeing `EEXIST` past the intended free. On a Windows runner the second probe missed the deadline, no extension was ever granted, and the wait expired still three from the head — the exact refusal the test exists to prove does NOT happen. The retirement and release are synchronous now (the callback returns `void` and cannot await), and the constants are scaled to ~100 ms of slack per probe while keeping the two invariants that fix them: the budget must span two probes, and the drain must outlast the budget or the sibling test would acquire instead of expiring. No production change — `withFileLock` checking its deadline before probing is correct, since a wait must not extend itself on a probe taken after it expired.
|
|
183
195
|
- `git-readonly-index.test.ts` carried a latent racy-clean flake: it edited `const b = 2;` to `const b = 3;` — the same byte length, within the same second as the index entry — without invalidating the stat cache, so git could report the file unmodified. Its sibling test already guarded for this.
|
|
184
196
|
|
|
185
197
|
### Documentation
|
|
@@ -187,6 +199,7 @@ On the test side: path expectations and mock keys are rendered through a new `na
|
|
|
187
199
|
- **README.md had become the reference manual.** Its "Regular Workflow" section had grown into fourteen paragraphs of flag semantics — scan manifests, shard behaviour, packaging-coverage anchors, tree CoW, shim configuration — read by nobody starting out and found by nobody looking for a specific rule, because prose in a getting-started page has no index. The reference material moves to `docs/`, split by the thing a reader is actually doing: `patch-workflow.md`, `testing.md`, `furnace.md`, `verification-trees.md`, `configuration.md`, plus `docs/README.md` as the index. README keeps installation, the loop's shape, rebasing, Furnace's premise, the roadmap and the licence — the parts that are project statement rather than manual — and links out for the rest.
|
|
188
200
|
- **`docs/run-logs.md`** states the `.fireforge/logs/` contract that previously existed only as a table cell in `machine-output.md`: raw bytes rather than the echo filter's form, opened before any preflight, retained 20 deep per command kind, best-effort throughout, and the tee sitting at the two mach capture funnels rather than at the commands.
|
|
189
201
|
- Two documents had gone stale against the code. `machine-output.md`'s surface table omitted `status --raw`, which engages the same stdout discipline through the same argv check in `cli.ts` — a machine surface to which rules 3 and 4 apply and rules 1 and 2 do not — and omitted `build`'s direct log announcement, which exists precisely because `build` prints no verdict line to carry a `log=` key. `lifecycle-invariants.md` still described `--wait-lock` as the engine-session lock's budget alone, which stopped being true when the budget was threaded into the furnace lock as well; both lock rows now state the resolution order (`lockTimeoutMs` → `FIREFORGE_WAIT_LOCK` → the 30 s default) and which mutators each covers.
|
|
202
|
+
- **The package did not ship the `docs/` tree its own README links to.** The `files` allowlist was `dist/`, `templates/` and the three root markdown files, so `node_modules/@hominis/fireforge/docs` did not exist and all eleven relative `docs/…` links dead-ended for a consumer — against a doctrine that names the INSTALLED package as the authority, precisely so an agent never has to leave its repo to answer a tool question. The links resolved on GitHub and on the registry page, which rewrites them, which is why nothing caught it. `docs/` is now published (~100 KB of markdown against a package that already ships `dist/`), and `wrapper-smoke.test.ts` asserts the tree is both packed and present after install, the way it already did for `templates/` — so a future `files` edit that drops it fails the gate rather than the reader.
|
|
190
203
|
- `FIREFORGE_WAIT_LOCK` and the three other environment variables FireForge reads were documented only in help text and JSDoc. They are tabulated in `configuration.md` with their defaults and their behaviour on a malformed value.
|
|
191
204
|
|
|
192
205
|
## 0.43.0
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.44.
|
|
4
|
-
"commit": "
|
|
5
|
-
"shortCommit": "
|
|
3
|
+
"version": "0.44.4",
|
|
4
|
+
"commit": "b8742ddc056124f81624d9c13d182d0b0c57c86c",
|
|
5
|
+
"shortCommit": "b8742ddc0561",
|
|
6
6
|
"dirty": false,
|
|
7
7
|
"dirtyHash": null,
|
|
8
|
-
"buildTime": "2026-08-
|
|
8
|
+
"buildTime": "2026-08-28T23:48:06.918Z"
|
|
9
9
|
}
|
|
@@ -342,13 +342,18 @@ async function runBuildCommandBody(projectRoot, options) {
|
|
|
342
342
|
// Record a fresh baseline only on clean success so the next run audits
|
|
343
343
|
// against this build's HEAD. A failed build keeps the prior baseline so
|
|
344
344
|
// the next attempt still catches long-standing packaging drops.
|
|
345
|
-
await recordFullBuildBaseline(projectRoot, paths.engine, config.binaryName, options.ui === true);
|
|
345
|
+
await recordFullBuildBaseline(projectRoot, paths.engine, config.binaryName, options.ui === true, previousBaseline);
|
|
346
346
|
outro(`Build completed in ${timeStr}!`);
|
|
347
347
|
}
|
|
348
|
-
/**
|
|
349
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Persists the full-coverage baseline; a failed write never fails the build.
|
|
350
|
+
* `--ui` is a `mach build faster`, so it is recorded as such: its `jar.mn`
|
|
351
|
+
* fingerprints are carried forward from the previous baseline rather than
|
|
352
|
+
* refreshed, which is why that baseline is threaded through.
|
|
353
|
+
*/
|
|
354
|
+
async function recordFullBuildBaseline(projectRoot, engineDir, binaryName, ui, previousBaseline) {
|
|
350
355
|
try {
|
|
351
|
-
await writeBuildBaseline(projectRoot, engineDir, binaryName, 'full',
|
|
356
|
+
await writeBuildBaseline(projectRoot, engineDir, binaryName, 'full', previousBaseline, ui ? 'fireforge build --ui' : 'fireforge build', 'auto', ui ? 'faster' : 'full');
|
|
352
357
|
}
|
|
353
358
|
catch (baselineError) {
|
|
354
359
|
verbose(`Could not persist build baseline: ${toError(baselineError).message}`);
|
|
@@ -14,6 +14,15 @@ import type { ResolvedTestStyle } from '../../types/furnace.js';
|
|
|
14
14
|
* tree, so a redirect makes no sense there.
|
|
15
15
|
*/
|
|
16
16
|
export declare function resolveValidatedTestDir(rawTestDir: string | undefined, testStyle: ResolvedTestStyle): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Scaffolds browser mochitest files for a newly created custom component.
|
|
19
|
+
* @param componentName - Custom element tag name
|
|
20
|
+
* @param license - Project license used for generated headers
|
|
21
|
+
* @param forgeConfig - Project config fields needed for test naming
|
|
22
|
+
* @param paths - Resolved project paths used to place test files
|
|
23
|
+
* @param journal - Optional rollback journal that snapshots files before writes
|
|
24
|
+
* @returns Relative test filenames created or updated for the component
|
|
25
|
+
*/
|
|
17
26
|
/**
|
|
18
27
|
* Normalizes and validates a `--test-dir` override: engine-relative,
|
|
19
28
|
* under `browser/base/content/test/` (so manifest registration keeps
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* the per-file line budget.
|
|
8
8
|
*/
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
+
import { BROWSER_TEST_SCAFFOLD_ROOT, resolveBrowserChromeTestDir, } from '../../core/furnace-constants.js';
|
|
10
11
|
import { recordCreatedDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
11
12
|
import { getLicenseHeader } from '../../core/license-headers.js';
|
|
12
13
|
import { registerTestManifest } from '../../core/moz-manifest-register.js';
|
|
@@ -40,7 +41,6 @@ export function resolveValidatedTestDir(rawTestDir, testStyle) {
|
|
|
40
41
|
* @param journal - Optional rollback journal that snapshots files before writes
|
|
41
42
|
* @returns Relative test filenames created or updated for the component
|
|
42
43
|
*/
|
|
43
|
-
const TEST_SCAFFOLD_ROOT = 'browser/base/content/test/';
|
|
44
44
|
/**
|
|
45
45
|
* Normalizes and validates a `--test-dir` override: engine-relative,
|
|
46
46
|
* under `browser/base/content/test/` (so manifest registration keeps
|
|
@@ -48,9 +48,9 @@ const TEST_SCAFFOLD_ROOT = 'browser/base/content/test/';
|
|
|
48
48
|
*/
|
|
49
49
|
export function resolveTestDirOverride(raw) {
|
|
50
50
|
const normalized = stripEnginePrefix(raw).replace(/\/+$/, '');
|
|
51
|
-
if (!normalized.startsWith(
|
|
52
|
-
normalized ===
|
|
53
|
-
throw new InvalidArgumentError(`--test-dir must be an engine-relative directory under ${
|
|
51
|
+
if (!normalized.startsWith(BROWSER_TEST_SCAFFOLD_ROOT) ||
|
|
52
|
+
normalized === BROWSER_TEST_SCAFFOLD_ROOT.slice(0, -1)) {
|
|
53
|
+
throw new InvalidArgumentError(`--test-dir must be an engine-relative directory under ${BROWSER_TEST_SCAFFOLD_ROOT} (got "${raw}").`, 'testDir');
|
|
54
54
|
}
|
|
55
55
|
return normalized;
|
|
56
56
|
}
|
|
@@ -78,8 +78,8 @@ export async function scaffoldTestFiles(componentName, license, forgeConfig, pat
|
|
|
78
78
|
// `.../test/<binaryName>/` target can collide with a test suite owned by a
|
|
79
79
|
// different patch. The manifest-registration name is the path below
|
|
80
80
|
// browser/base/content/test/ (nested manifests are supported).
|
|
81
|
-
const testDirRel = testDirOverride
|
|
82
|
-
const testDirName = testDirRel.slice(
|
|
81
|
+
const testDirRel = resolveBrowserChromeTestDir(binaryName, testDirOverride);
|
|
82
|
+
const testDirName = testDirRel.slice(BROWSER_TEST_SCAFFOLD_ROOT.length);
|
|
83
83
|
const testDir = join(paths.engine, testDirRel);
|
|
84
84
|
if (journal && !(await pathExists(testDir))) {
|
|
85
85
|
recordCreatedDir(journal, testDir);
|
|
@@ -15,6 +15,14 @@ export interface DryRunPlanInput {
|
|
|
15
15
|
testStyle: ResolvedTestStyle;
|
|
16
16
|
description: string;
|
|
17
17
|
binaryName: string;
|
|
18
|
+
/**
|
|
19
|
+
* Resolved `--test-dir` override (engine-relative, already validated by
|
|
20
|
+
* `resolveValidatedTestDir`). Omit for the default `<binaryName>`-derived
|
|
21
|
+
* scaffold directory. The formatters MUST receive the same value the
|
|
22
|
+
* scaffolders get — the plan used to recompute the default from
|
|
23
|
+
* `binaryName` and named a directory the real run never wrote to.
|
|
24
|
+
*/
|
|
25
|
+
testDir?: string;
|
|
18
26
|
}
|
|
19
27
|
/**
|
|
20
28
|
* Builds the success-note body printed after `furnace create` has applied
|
|
@@ -27,6 +35,8 @@ export declare function formatSuccessNote(args: {
|
|
|
27
35
|
testFiles: string[];
|
|
28
36
|
testStyle: ResolvedTestStyle;
|
|
29
37
|
binaryName: string;
|
|
38
|
+
/** Resolved `--test-dir` override; see {@link DryRunPlanInput.testDir}. */
|
|
39
|
+
testDir?: string | undefined;
|
|
30
40
|
}): string;
|
|
31
41
|
/**
|
|
32
42
|
* Builds the planned component + test file list for a dry-run preview.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* resolved by the command's validation phase — so it can be exercised
|
|
8
8
|
* independently of the mutation plumbing.
|
|
9
9
|
*/
|
|
10
|
+
import { BROWSER_TEST_SCAFFOLD_ROOT, resolveBrowserChromeTestDir, resolveXpcshellTestDir, } from '../../core/furnace-constants.js';
|
|
10
11
|
/**
|
|
11
12
|
* Builds the test-section fragment of the dry-run plan for a given
|
|
12
13
|
* harness choice. Kept separate from the top-level formatter so the
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
* complexity budget.
|
|
15
16
|
*/
|
|
16
17
|
function formatTestSection(args) {
|
|
17
|
-
const { testStyle, componentName, binaryName } = args;
|
|
18
|
+
const { testStyle, componentName, binaryName, testDir } = args;
|
|
18
19
|
if (testStyle === 'none')
|
|
19
20
|
return '';
|
|
20
21
|
const strippedName = componentName.startsWith('moz-') ? componentName.slice(4) : componentName;
|
|
@@ -23,14 +24,18 @@ function formatTestSection(args) {
|
|
|
23
24
|
: strippedName;
|
|
24
25
|
const underscored = withoutBinaryPrefix.replace(/-/g, '_');
|
|
25
26
|
if (testStyle === 'browser-chrome') {
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// Same resolver as scaffoldTestFiles: the registration name is the
|
|
28
|
+
// path below browser/base/content/test/, which under --test-dir is
|
|
29
|
+
// NOT the binary name.
|
|
30
|
+
const testDirRel = resolveBrowserChromeTestDir(binaryName, testDir);
|
|
31
|
+
const manifestName = testDirRel.slice(BROWSER_TEST_SCAFFOLD_ROOT.length);
|
|
32
|
+
return (`\n\nWould create test files in engine/${testDirRel}/:\n` +
|
|
28
33
|
` browser.toml\n head.js\n browser_${binaryName}_${underscored}.js` +
|
|
29
|
-
`\n\nWould register ${
|
|
34
|
+
`\n\nWould register ${manifestName}/browser.toml in engine/browser/base/moz.build`);
|
|
30
35
|
}
|
|
31
36
|
if (testStyle === 'xpcshell') {
|
|
32
|
-
const
|
|
33
|
-
return `\n\nWould create xpcshell test files in
|
|
37
|
+
const testDirRel = resolveXpcshellTestDir(binaryName, componentName, testDir);
|
|
38
|
+
return `\n\nWould create xpcshell test files in engine/${testDirRel}/`;
|
|
34
39
|
}
|
|
35
40
|
// testStyle === 'mochikit' (last remaining branch in ResolvedTestStyle).
|
|
36
41
|
const testRoot = 'engine/toolkit/content/tests/widgets/';
|
|
@@ -42,19 +47,19 @@ function formatTestSection(args) {
|
|
|
42
47
|
* stay in lock-step when the scaffolded layout changes.
|
|
43
48
|
*/
|
|
44
49
|
export function formatSuccessNote(args) {
|
|
45
|
-
const { componentName, files, testFiles, testStyle, binaryName } = args;
|
|
50
|
+
const { componentName, files, testFiles, testStyle, binaryName, testDir } = args;
|
|
46
51
|
let note = `Files created in components/custom/${componentName}/:\n` +
|
|
47
52
|
files.map((f) => ` ${f}`).join('\n');
|
|
48
53
|
if (testFiles.length > 0) {
|
|
49
54
|
let testRoot;
|
|
50
55
|
if (testStyle === 'xpcshell') {
|
|
51
|
-
testRoot = `engine
|
|
56
|
+
testRoot = `engine/${resolveXpcshellTestDir(binaryName, componentName, testDir)}/`;
|
|
52
57
|
}
|
|
53
58
|
else if (testStyle === 'mochikit') {
|
|
54
59
|
testRoot = 'engine/toolkit/content/tests/widgets/';
|
|
55
60
|
}
|
|
56
61
|
else {
|
|
57
|
-
testRoot = `engine
|
|
62
|
+
testRoot = `engine/${resolveBrowserChromeTestDir(binaryName, testDir)}/`;
|
|
58
63
|
}
|
|
59
64
|
note += `\n\nTest files in ${testRoot}:\n` + testFiles.map((f) => ` ${f}`).join('\n');
|
|
60
65
|
}
|
|
@@ -75,7 +80,7 @@ export function formatSuccessNote(args) {
|
|
|
75
80
|
* `components/custom/` to match the wording of the real success note.
|
|
76
81
|
*/
|
|
77
82
|
export function formatDryRunPlan(args) {
|
|
78
|
-
const { componentName, localized, register, composes, stockAdditions, sharedFtl, testStyle, description, binaryName, } = args;
|
|
83
|
+
const { componentName, localized, register, composes, stockAdditions, sharedFtl, testStyle, description, binaryName, testDir, } = args;
|
|
79
84
|
const componentFiles = [`${componentName}.mjs`, `${componentName}.css`];
|
|
80
85
|
// A per-component .ftl is scaffolded only when the component does NOT
|
|
81
86
|
// opt into a shared feature-scoped bundle. Mirrors writeComponentFiles.
|
|
@@ -83,7 +88,7 @@ export function formatDryRunPlan(args) {
|
|
|
83
88
|
componentFiles.push(`${componentName}.ftl`);
|
|
84
89
|
let plan = `Would create files in components/custom/${componentName}/:\n` +
|
|
85
90
|
componentFiles.map((f) => ` ${f}`).join('\n');
|
|
86
|
-
plan += formatTestSection({ testStyle, componentName, binaryName });
|
|
91
|
+
plan += formatTestSection({ testStyle, componentName, binaryName, testDir });
|
|
87
92
|
plan += `\n\nWould add custom entry to furnace.json:`;
|
|
88
93
|
plan += `\n name: ${componentName}`;
|
|
89
94
|
plan += `\n description: ${description || '(empty)'}`;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* per-file LOC budget and the scaffolder is unit-testable in isolation.
|
|
6
6
|
*/
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
-
import { xpcshellTestParentDir } from '../../core/furnace-constants.js';
|
|
8
|
+
import { resolveXpcshellTestDir, xpcshellTestParentDir } from '../../core/furnace-constants.js';
|
|
9
9
|
import { recordCreatedDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
10
10
|
import { getLicenseHeader } from '../../core/license-headers.js';
|
|
11
11
|
import { ensureDir, pathExists, readText, writeText } from '../../utils/fs.js';
|
|
@@ -31,7 +31,7 @@ export async function scaffoldXpcshellTestFiles(componentName, license, forgeCon
|
|
|
31
31
|
const parentDirName = parentRelDir.split('/').slice(-1)[0] ?? `${forgeConfig.binaryName}-xpcshell`;
|
|
32
32
|
// --test-dir names the FINAL directory (no per-component segment is
|
|
33
33
|
// appended) so the operator controls the exact scaffold target.
|
|
34
|
-
const testDirRel =
|
|
34
|
+
const testDirRel = resolveXpcshellTestDir(forgeConfig.binaryName, componentName, testDirOverride);
|
|
35
35
|
const testDir = join(paths.engine, testDirRel);
|
|
36
36
|
if (journal && !(await pathExists(testDir))) {
|
|
37
37
|
recordCreatedDir(journal, testDir);
|
|
@@ -356,6 +356,10 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
356
356
|
testStyle,
|
|
357
357
|
description,
|
|
358
358
|
binaryName: forgeConfig.binaryName,
|
|
359
|
+
// The plan must name the directory the scaffolder will actually use:
|
|
360
|
+
// an omitted override here is how the printed path came to disagree
|
|
361
|
+
// with the files a real run wrote.
|
|
362
|
+
...(testDir !== undefined ? { testDir } : {}),
|
|
359
363
|
});
|
|
360
364
|
note(plan, componentName);
|
|
361
365
|
outro('Dry run complete (no files modified)');
|
|
@@ -391,6 +395,7 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
391
395
|
testFiles,
|
|
392
396
|
testStyle,
|
|
393
397
|
binaryName: forgeConfig.binaryName,
|
|
398
|
+
...(testDir !== undefined ? { testDir } : {}),
|
|
394
399
|
}), componentName);
|
|
395
400
|
outro('Component created');
|
|
396
401
|
}
|
|
@@ -84,7 +84,7 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
|
|
|
84
84
|
? 'refresh'
|
|
85
85
|
: extend !== undefined
|
|
86
86
|
? 'carry-forward'
|
|
87
|
-
: 'auto');
|
|
87
|
+
: 'auto', buildKind);
|
|
88
88
|
}
|
|
89
89
|
catch (baselineError) {
|
|
90
90
|
verbose(`Could not persist build baseline: ${toError(baselineError).message}`);
|
|
@@ -180,7 +180,9 @@ export function registerToken(program, { getProjectRoot, withErrorHandling }) {
|
|
|
180
180
|
.option('--description <desc>', 'Comment description for the CSS file')
|
|
181
181
|
.option('--dark-value <val>', 'Dark mode value (required if mode is "override")')
|
|
182
182
|
.option('--variant <selector>', 'Attribute selector fragment routing the declaration into a `:root<selector>` block ' +
|
|
183
|
-
"(e.g. '[data-skin=precision]'
|
|
183
|
+
"(e.g. '[data-skin=precision]', '[data-private]', a run such as " +
|
|
184
|
+
"'[data-skin=precision][data-theme=dark]', or one with a pseudo-class tail such as " +
|
|
185
|
+
"'[data-skin=precision]:not([data-private])'); creates or updates the block. " +
|
|
184
186
|
'CSS-only — cannot be combined with --mode override or --create-category. To author ' +
|
|
185
187
|
'a new category and its variants, add the BASE token first (with --create-category), ' +
|
|
186
188
|
'then add each variant with --variant.')
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import type { BuildBaseline } from './build-baseline-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* True for the build-input manifests the build consumes but never packages
|
|
4
|
+
* (`jar.mn`, `moz.build`, `moz.configure`, `Makefile.in`, `mozbuild.in`).
|
|
5
|
+
* Excluded from the packaging audit and from `packageableFingerprints`;
|
|
6
|
+
* fingerprinted separately as `buildInputFingerprints` so `build-prepare`
|
|
7
|
+
* can tell "dirty against HEAD" from "changed since the last successful
|
|
8
|
+
* build".
|
|
9
|
+
*
|
|
10
|
+
* @param sourcePath Engine-relative POSIX path.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isBuildInputPath(sourcePath: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* True for a chrome packaging manifest (`jar.mn`). Its install-manifest
|
|
15
|
+
* destinations are the one build input whose change escalates a pre-test
|
|
16
|
+
* `mach build faster` to a full build, and whose fingerprint is therefore
|
|
17
|
+
* refreshed only by a full build.
|
|
18
|
+
*
|
|
19
|
+
* @param sourcePath Engine-relative POSIX path.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isJarManifestPath(sourcePath: string): boolean;
|
|
2
22
|
/** Result of a single artifact lookup. */
|
|
3
23
|
export interface AuditEntry {
|
|
4
24
|
/** Engine-relative source file path (POSIX separators). */
|
|
@@ -74,6 +74,30 @@ const BUILD_INPUT_BASENAMES = new Set([
|
|
|
74
74
|
'Makefile.in',
|
|
75
75
|
'mozbuild.in',
|
|
76
76
|
]);
|
|
77
|
+
/**
|
|
78
|
+
* True for the build-input manifests the build consumes but never packages
|
|
79
|
+
* (`jar.mn`, `moz.build`, `moz.configure`, `Makefile.in`, `mozbuild.in`).
|
|
80
|
+
* Excluded from the packaging audit and from `packageableFingerprints`;
|
|
81
|
+
* fingerprinted separately as `buildInputFingerprints` so `build-prepare`
|
|
82
|
+
* can tell "dirty against HEAD" from "changed since the last successful
|
|
83
|
+
* build".
|
|
84
|
+
*
|
|
85
|
+
* @param sourcePath Engine-relative POSIX path.
|
|
86
|
+
*/
|
|
87
|
+
export function isBuildInputPath(sourcePath) {
|
|
88
|
+
return BUILD_INPUT_BASENAMES.has(basename(sourcePath));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* True for a chrome packaging manifest (`jar.mn`). Its install-manifest
|
|
92
|
+
* destinations are the one build input whose change escalates a pre-test
|
|
93
|
+
* `mach build faster` to a full build, and whose fingerprint is therefore
|
|
94
|
+
* refreshed only by a full build.
|
|
95
|
+
*
|
|
96
|
+
* @param sourcePath Engine-relative POSIX path.
|
|
97
|
+
*/
|
|
98
|
+
export function isJarManifestPath(sourcePath) {
|
|
99
|
+
return sourcePath === 'jar.mn' || sourcePath.endsWith('/jar.mn');
|
|
100
|
+
}
|
|
77
101
|
/**
|
|
78
102
|
* Decides whether a source path should be packaged. Returns true for paths
|
|
79
103
|
* whose extension or directory fragment matches a known-packaged pattern,
|
|
@@ -88,7 +112,7 @@ export function isPackageablePath(sourcePath) {
|
|
|
88
112
|
if (sourcePath.includes(fragment))
|
|
89
113
|
return false;
|
|
90
114
|
}
|
|
91
|
-
if (
|
|
115
|
+
if (isBuildInputPath(sourcePath))
|
|
92
116
|
return false;
|
|
93
117
|
// `.inc.xhtml` fragments are consumed via `#include` from a registered
|
|
94
118
|
// chrome document and resolved at packaging time — they never ship as a
|
|
@@ -35,6 +35,34 @@ export interface BuildBaseline {
|
|
|
35
35
|
* a tracked deletion.
|
|
36
36
|
*/
|
|
37
37
|
packageableFingerprints?: Record<string, string>;
|
|
38
|
+
/**
|
|
39
|
+
* Content hash per build-input manifest (`jar.mn`, `moz.build`,
|
|
40
|
+
* `moz.configure`, `Makefile.in`, `mozbuild.in`) that was dirty
|
|
41
|
+
* (modified-against-HEAD or untracked) when the recorded build succeeded.
|
|
42
|
+
* Same key/value shape as {@link BuildBaseline.packageableFingerprints}.
|
|
43
|
+
*
|
|
44
|
+
* Consumed by `build-prepare`: a dirty build input whose live hash still
|
|
45
|
+
* matches its entry was already consumed by the recorded build, so it
|
|
46
|
+
* neither re-runs `mach configure` nor escalates a pre-test build to a
|
|
47
|
+
* full `mach build`. Without this the persistent worktree diff every
|
|
48
|
+
* fork carries (imported patches + Furnace-applied components) made a
|
|
49
|
+
* patch-touched `jar.mn` "changed" on EVERY run, and each `test --build`
|
|
50
|
+
* after a full build paid the full build again.
|
|
51
|
+
*
|
|
52
|
+
* Refresh rules, per entry kind:
|
|
53
|
+
* - backend inputs (`moz.build` & co) are refreshed on every successful
|
|
54
|
+
* write — every baseline-writing path ran the auto-configure preflight
|
|
55
|
+
* against the previous baseline, so a changed one was reconfigured;
|
|
56
|
+
* - `jar.mn` entries are refreshed only by a FULL build and carried
|
|
57
|
+
* forward verbatim from the previous record on a `faster` write, since
|
|
58
|
+
* `fireforge build --ui` never escalates and recording a `jar.mn` no
|
|
59
|
+
* full build has installed would hide the next escalation.
|
|
60
|
+
*
|
|
61
|
+
* Absent on baselines written before the field existed; `build-prepare`
|
|
62
|
+
* then falls back to the path-only comparison (every dirty build input
|
|
63
|
+
* counts as changed).
|
|
64
|
+
*/
|
|
65
|
+
buildInputFingerprints?: Record<string, string>;
|
|
38
66
|
/**
|
|
39
67
|
* What the packaged test runtime produced by the recorded build covers.
|
|
40
68
|
*
|
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
* subsequent run still audits against the last known-good tree.
|
|
17
17
|
*/
|
|
18
18
|
import { type BuildBaseline, type TestPackagingCoverage } from './build-baseline-types.js';
|
|
19
|
+
/**
|
|
20
|
+
* Which mach build produced the baseline being written. Decides whether
|
|
21
|
+
* the `jar.mn` half of {@link BuildBaseline.buildInputFingerprints} is
|
|
22
|
+
* refreshed (`'full'`) or carried forward from the previous record
|
|
23
|
+
* (`'faster'`). `fireforge build --ui` and a non-escalated `test --build`
|
|
24
|
+
* are `'faster'`; `fireforge build` and an escalated `test --build` are
|
|
25
|
+
* `'full'`.
|
|
26
|
+
*/
|
|
27
|
+
export type BaselineBuildKind = 'full' | 'faster';
|
|
19
28
|
/** Name of the last-build marker file under `.fireforge/`. */
|
|
20
29
|
export declare const BUILD_BASELINE_FILENAME = "last-build.json";
|
|
21
30
|
/**
|
|
@@ -56,5 +65,11 @@ export declare function readBuildBaseline(projectRoot: string): Promise<BuildBas
|
|
|
56
65
|
* keeps the previous anchor: needed by `--extend-coverage`, whose union
|
|
57
66
|
* can EVALUATE to `'full'` while the build that produced it was still a
|
|
58
67
|
* scoped `mach build faster` that did not rebake the compiled table.
|
|
68
|
+
* @param buildKind - Which mach build produced this baseline (default
|
|
69
|
+
* `'full'`). A `'faster'` write carries the previous record's `jar.mn`
|
|
70
|
+
* fingerprints forward instead of refreshing them — see
|
|
71
|
+
* {@link BuildBaseline.buildInputFingerprints}. Every caller that ran
|
|
72
|
+
* `mach build faster` MUST say so, or the next pre-test build skips an
|
|
73
|
+
* escalation no full build has honoured.
|
|
59
74
|
*/
|
|
60
|
-
export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage, previousBaseline?: BuildBaseline, recordedBy?: string, staticComponentsHandling?: 'auto' | 'refresh' | 'carry-forward'): Promise<void>;
|
|
75
|
+
export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage, previousBaseline?: BuildBaseline, recordedBy?: string, staticComponentsHandling?: 'auto' | 'refresh' | 'carry-forward', buildKind?: BaselineBuildKind): Promise<void>;
|
|
@@ -23,7 +23,7 @@ import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
|
23
23
|
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
24
24
|
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
25
25
|
import { verbose } from '../utils/logger.js';
|
|
26
|
-
import { isPackageablePath, isXpcomManifestPath } from './build-audit.js';
|
|
26
|
+
import { isBuildInputPath, isJarManifestPath, isPackageablePath, isXpcomManifestPath, } from './build-audit.js';
|
|
27
27
|
import { DELETED_FILE_FINGERPRINT, } from './build-baseline-types.js';
|
|
28
28
|
import { FIREFORGE_DIR } from './config-paths.js';
|
|
29
29
|
import { hashEngineFile } from './coverage-extend.js';
|
|
@@ -90,8 +90,14 @@ export async function readBuildBaseline(projectRoot) {
|
|
|
90
90
|
* keeps the previous anchor: needed by `--extend-coverage`, whose union
|
|
91
91
|
* can EVALUATE to `'full'` while the build that produced it was still a
|
|
92
92
|
* scoped `mach build faster` that did not rebake the compiled table.
|
|
93
|
+
* @param buildKind - Which mach build produced this baseline (default
|
|
94
|
+
* `'full'`). A `'faster'` write carries the previous record's `jar.mn`
|
|
95
|
+
* fingerprints forward instead of refreshing them — see
|
|
96
|
+
* {@link BuildBaseline.buildInputFingerprints}. Every caller that ran
|
|
97
|
+
* `mach build faster` MUST say so, or the next pre-test build skips an
|
|
98
|
+
* escalation no full build has honoured.
|
|
93
99
|
*/
|
|
94
|
-
export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline, recordedBy, staticComponentsHandling = 'auto') {
|
|
100
|
+
export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline, recordedBy, staticComponentsHandling = 'auto', buildKind = 'full') {
|
|
95
101
|
let engineHeadSha = '';
|
|
96
102
|
try {
|
|
97
103
|
engineHeadSha = await getHead(engineDir);
|
|
@@ -106,6 +112,7 @@ export async function writeBuildBaseline(projectRoot, engineDir, binaryName, tes
|
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
114
|
const packageableFingerprints = await collectPackageableFingerprints(engineDir);
|
|
115
|
+
const buildInputFingerprints = await collectBuildInputFingerprints(engineDir, previousBaseline, buildKind);
|
|
109
116
|
// A full-coverage write (and the legacy no-claim `fireforge build` shape)
|
|
110
117
|
// just recompiled the StaticComponents table, so it anchors the table to
|
|
111
118
|
// the current engine state. A scoped write did not — carry the previous
|
|
@@ -121,6 +128,7 @@ export async function writeBuildBaseline(projectRoot, engineDir, binaryName, tes
|
|
|
121
128
|
builtAt: new Date().toISOString(),
|
|
122
129
|
binaryName,
|
|
123
130
|
...(packageableFingerprints !== undefined ? { packageableFingerprints } : {}),
|
|
131
|
+
...(buildInputFingerprints !== undefined ? { buildInputFingerprints } : {}),
|
|
124
132
|
...(testPackagingCoverage !== undefined ? { testPackagingCoverage } : {}),
|
|
125
133
|
...(mozconfigHash !== undefined ? { mozconfigHash } : {}),
|
|
126
134
|
...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
|
|
@@ -143,6 +151,34 @@ export async function writeBuildBaseline(projectRoot, engineDir, binaryName, tes
|
|
|
143
151
|
async function collectPackageableFingerprints(engineDir) {
|
|
144
152
|
return collectDirtyFingerprints(engineDir, isPackageablePath, 'packageable fingerprint');
|
|
145
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Content fingerprints of the dirty build-input manifests
|
|
156
|
+
* ({@link isBuildInputPath}) this build consumed. Backend inputs
|
|
157
|
+
* (`moz.build` & co) are always taken from the live tree: the
|
|
158
|
+
* auto-configure preflight reconfigured against them before this build
|
|
159
|
+
* ran. `jar.mn` entries are taken from the live tree only after a FULL
|
|
160
|
+
* build; a `faster` write carries the previous record's `jar.mn` entries
|
|
161
|
+
* forward verbatim, because `mach build faster` is exactly the build the
|
|
162
|
+
* `jar.mn` escalation exists to bypass, and recording a `jar.mn` it did
|
|
163
|
+
* not install would silence the next escalation. Returns `undefined` on
|
|
164
|
+
* probe failure so the field is omitted rather than written as `{}`.
|
|
165
|
+
*/
|
|
166
|
+
async function collectBuildInputFingerprints(engineDir, previousBaseline, buildKind) {
|
|
167
|
+
const live = await collectDirtyFingerprints(engineDir, isBuildInputPath, 'build-input fingerprint');
|
|
168
|
+
if (live === undefined || buildKind === 'full') {
|
|
169
|
+
return live;
|
|
170
|
+
}
|
|
171
|
+
const fingerprints = {};
|
|
172
|
+
for (const [path, hash] of Object.entries(live)) {
|
|
173
|
+
if (!isJarManifestPath(path))
|
|
174
|
+
fingerprints[path] = hash;
|
|
175
|
+
}
|
|
176
|
+
for (const [path, hash] of Object.entries(previousBaseline?.buildInputFingerprints ?? {})) {
|
|
177
|
+
if (isJarManifestPath(path))
|
|
178
|
+
fingerprints[path] = hash;
|
|
179
|
+
}
|
|
180
|
+
return fingerprints;
|
|
181
|
+
}
|
|
146
182
|
/**
|
|
147
183
|
* Anchor for the compiled StaticComponents table: the engine HEAD SHA of
|
|
148
184
|
* this (full) build plus content fingerprints of the `components.conf`
|
|
@@ -11,8 +11,9 @@ import { toError } from '../utils/errors.js';
|
|
|
11
11
|
import { pathExists } from '../utils/fs.js';
|
|
12
12
|
import { info, notice, spinner, verbose, warn } from '../utils/logger.js';
|
|
13
13
|
import { isBrandingSetup, setupBranding } from './branding.js';
|
|
14
|
+
import { isBuildInputPath, isJarManifestPath } from './build-audit.js';
|
|
14
15
|
import { findUnexportedDriftAtRisk, formatUnexportedDriftWarning, } from './build-overwrite-guard.js';
|
|
15
|
-
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
16
|
+
import { collectChangedEnginePaths, dropPathsMatchingFingerprints } from './engine-changes.js';
|
|
16
17
|
import { applyAllComponents } from './furnace-apply.js';
|
|
17
18
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
|
|
18
19
|
import { runFurnaceMutation } from './furnace-operation.js';
|
|
@@ -24,7 +25,35 @@ import { explainMachError } from './mach-error-hints.js';
|
|
|
24
25
|
const BACKEND_INVALIDATING_SUFFIXES = ['moz.build', 'moz.configure', 'Makefile.in'];
|
|
25
26
|
/** Packaging manifests whose graph/destination directories need a full build. */
|
|
26
27
|
export function requiresFullBuildForIncrementalTest(path) {
|
|
27
|
-
return path
|
|
28
|
+
return isJarManifestPath(path);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build-input manifests changed since the last SUCCESSFUL build — not
|
|
32
|
+
* merely dirty against HEAD. `collectChangedEnginePaths` unions the
|
|
33
|
+
* baseline-to-HEAD diff with every worktree modification, and a fork's
|
|
34
|
+
* worktree is permanently dirty (imported patches, Furnace-applied
|
|
35
|
+
* components), so on its own it reports a patch-touched `jar.mn` or
|
|
36
|
+
* `moz.build` as changed on every run: `mach configure` re-ran and the
|
|
37
|
+
* pre-test build escalated to a full `mach build` after each full build
|
|
38
|
+
* that had already consumed exactly this content. The baseline's
|
|
39
|
+
* `buildInputFingerprints` say what the last successful build saw;
|
|
40
|
+
* anything byte-identical to that is dropped here. A baseline without the
|
|
41
|
+
* field (written before it existed) keeps every dirty input, which is the
|
|
42
|
+
* old behaviour.
|
|
43
|
+
*
|
|
44
|
+
* @param engineDir - Path to the engine directory
|
|
45
|
+
* @param baseline - Last successful build's baseline
|
|
46
|
+
* @returns Engine-relative build-input paths that still need acting on
|
|
47
|
+
*/
|
|
48
|
+
async function collectChangedBuildInputs(engineDir, baseline) {
|
|
49
|
+
const changed = await collectChangedEnginePaths(engineDir, baseline, 'Auto-configure');
|
|
50
|
+
const dirtyInputs = changed.filter(isBuildInputPath);
|
|
51
|
+
const stillChanged = await dropPathsMatchingFingerprints(engineDir, dirtyInputs, baseline.buildInputFingerprints);
|
|
52
|
+
const unchangedSinceBuild = dirtyInputs.filter((path) => !stillChanged.includes(path));
|
|
53
|
+
if (unchangedSinceBuild.length > 0) {
|
|
54
|
+
verbose(`Auto-configure: ${unchangedSinceBuild.length} dirty build input(s) byte-identical to the last successful build, not treated as changed: ${unchangedSinceBuild.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
return stillChanged;
|
|
28
57
|
}
|
|
29
58
|
/**
|
|
30
59
|
* Extracts the tail of captured `mach configure` output so the underlying
|
|
@@ -149,7 +178,7 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
149
178
|
let reconfigured = false;
|
|
150
179
|
let fullBuildRequired = false;
|
|
151
180
|
if (options.previousBaseline) {
|
|
152
|
-
const changed = await
|
|
181
|
+
const changed = await collectChangedBuildInputs(paths.engine, options.previousBaseline);
|
|
153
182
|
const invalidating = changed.filter(isBackendInvalidatingFile);
|
|
154
183
|
fullBuildRequired = changed.some(requiresFullBuildForIncrementalTest);
|
|
155
184
|
if (invalidating.length > 0) {
|