@hominis/fireforge 0.44.3 → 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 +7 -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/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/docs/testing.md +31 -0
- package/package.json +1 -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
|
|
|
@@ -161,6 +167,7 @@
|
|
|
161
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.
|
|
162
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.
|
|
163
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.
|
|
164
171
|
|
|
165
172
|
### Windows
|
|
166
173
|
|
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}`);
|
|
@@ -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) {
|
|
@@ -20,3 +20,21 @@ import type { BuildBaseline } from './build-baseline-types.js';
|
|
|
20
20
|
* @returns Sorted engine-relative POSIX paths
|
|
21
21
|
*/
|
|
22
22
|
export declare function collectChangedEnginePaths(engineDir: string, baseline: BuildBaseline | undefined, contextLabel: string): Promise<string[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Drops every path whose live content still matches the fingerprint the
|
|
25
|
+
* last successful build recorded for it, keeping the rest. Shared by the
|
|
26
|
+
* stale-build preflight (`packageableFingerprints`) and the auto-configure /
|
|
27
|
+
* full-build escalation in `build-prepare` (`buildInputFingerprints`).
|
|
28
|
+
*
|
|
29
|
+
* A path with no recorded entry, an unreadable file, or a differing hash
|
|
30
|
+
* is kept — "cannot prove unchanged" must never turn into "unchanged".
|
|
31
|
+
* With no fingerprint set at all (a baseline written before the field
|
|
32
|
+
* existed) every path is kept, which is the path-only comparison the
|
|
33
|
+
* fingerprints refine.
|
|
34
|
+
*
|
|
35
|
+
* @param engineDir - Path to the engine directory
|
|
36
|
+
* @param paths - Candidate engine-relative POSIX paths
|
|
37
|
+
* @param fingerprints - Recorded hashes keyed by engine-relative path
|
|
38
|
+
* @returns The subset of `paths` that cannot be proven unchanged
|
|
39
|
+
*/
|
|
40
|
+
export declare function dropPathsMatchingFingerprints(engineDir: string, paths: readonly string[], fingerprints: Record<string, string> | undefined): Promise<string[]>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { toError } from '../utils/errors.js';
|
|
8
8
|
import { verbose } from '../utils/logger.js';
|
|
9
|
+
import { hashEngineFile } from './coverage-extend.js';
|
|
9
10
|
import { hasChanges, isMissingHeadError } from './git.js';
|
|
10
11
|
import { git } from './git-base.js';
|
|
11
12
|
import { getUntrackedFiles } from './git-status.js';
|
|
@@ -59,3 +60,36 @@ export async function collectChangedEnginePaths(engineDir, baseline, contextLabe
|
|
|
59
60
|
}
|
|
60
61
|
return [...collected].sort();
|
|
61
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Drops every path whose live content still matches the fingerprint the
|
|
65
|
+
* last successful build recorded for it, keeping the rest. Shared by the
|
|
66
|
+
* stale-build preflight (`packageableFingerprints`) and the auto-configure /
|
|
67
|
+
* full-build escalation in `build-prepare` (`buildInputFingerprints`).
|
|
68
|
+
*
|
|
69
|
+
* A path with no recorded entry, an unreadable file, or a differing hash
|
|
70
|
+
* is kept — "cannot prove unchanged" must never turn into "unchanged".
|
|
71
|
+
* With no fingerprint set at all (a baseline written before the field
|
|
72
|
+
* existed) every path is kept, which is the path-only comparison the
|
|
73
|
+
* fingerprints refine.
|
|
74
|
+
*
|
|
75
|
+
* @param engineDir - Path to the engine directory
|
|
76
|
+
* @param paths - Candidate engine-relative POSIX paths
|
|
77
|
+
* @param fingerprints - Recorded hashes keyed by engine-relative path
|
|
78
|
+
* @returns The subset of `paths` that cannot be proven unchanged
|
|
79
|
+
*/
|
|
80
|
+
export async function dropPathsMatchingFingerprints(engineDir, paths, fingerprints) {
|
|
81
|
+
if (!fingerprints)
|
|
82
|
+
return [...paths];
|
|
83
|
+
const kept = [];
|
|
84
|
+
for (const path of paths) {
|
|
85
|
+
const recorded = fingerprints[path];
|
|
86
|
+
if (recorded === undefined) {
|
|
87
|
+
kept.push(path);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const live = await hashEngineFile(engineDir, path);
|
|
91
|
+
if (live === undefined || live !== recorded)
|
|
92
|
+
kept.push(path);
|
|
93
|
+
}
|
|
94
|
+
return kept;
|
|
95
|
+
}
|
|
@@ -12,12 +12,33 @@ export declare const JAR_MN = "toolkit/content/jar.mn";
|
|
|
12
12
|
export declare const WIDGETS_DIR = "toolkit/content/widgets";
|
|
13
13
|
/** Default Fluent localization directory for toolkit global components, relative to engine root */
|
|
14
14
|
export declare const FTL_DIR = "toolkit/locales/en-US/toolkit/global";
|
|
15
|
+
/**
|
|
16
|
+
* Engine-relative root every browser-chrome / xpcshell test scaffold lives
|
|
17
|
+
* under, trailing slash included. A `--test-dir` override must stay below
|
|
18
|
+
* it so the `browser/base/moz.build` manifest registration keeps working.
|
|
19
|
+
*/
|
|
20
|
+
export declare const BROWSER_TEST_SCAFFOLD_ROOT = "browser/base/content/test/";
|
|
15
21
|
/**
|
|
16
22
|
* Returns the engine-relative directory that holds xpcshell scaffolds for
|
|
17
23
|
* a given binary. Matches the form `create-xpcshell.ts` writes and the
|
|
18
24
|
* path `remove.ts` / `rename.ts` / `validate.ts` must clean up.
|
|
19
25
|
*/
|
|
20
26
|
export declare function xpcshellTestParentDir(binaryName: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Resolves the engine-relative directory a browser-chrome scaffold is
|
|
29
|
+
* written to: the `--test-dir` override when given, else
|
|
30
|
+
* `browser/base/content/test/<binaryName>`. The scaffolder AND the
|
|
31
|
+
* dry-run / success formatters resolve through this one function, so the
|
|
32
|
+
* printed plan cannot disagree with the files that land on disk.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveBrowserChromeTestDir(binaryName: string, override?: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Resolves the engine-relative directory an xpcshell scaffold is written
|
|
37
|
+
* to. A `--test-dir` override names the FINAL directory (no per-component
|
|
38
|
+
* segment is appended); the default is
|
|
39
|
+
* `browser/base/content/test/<binaryName>-xpcshell/<componentName>`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveXpcshellTestDir(binaryName: string, componentName: string, override?: string): string;
|
|
21
42
|
/** Returns true when `fileName` has one of the standard component file extensions. */
|
|
22
43
|
export declare function isComponentSourceFile(fileName: string): boolean;
|
|
23
44
|
/**
|
|
@@ -13,6 +13,12 @@ export const JAR_MN = 'toolkit/content/jar.mn';
|
|
|
13
13
|
export const WIDGETS_DIR = 'toolkit/content/widgets';
|
|
14
14
|
/** Default Fluent localization directory for toolkit global components, relative to engine root */
|
|
15
15
|
export const FTL_DIR = 'toolkit/locales/en-US/toolkit/global';
|
|
16
|
+
/**
|
|
17
|
+
* Engine-relative root every browser-chrome / xpcshell test scaffold lives
|
|
18
|
+
* under, trailing slash included. A `--test-dir` override must stay below
|
|
19
|
+
* it so the `browser/base/moz.build` manifest registration keeps working.
|
|
20
|
+
*/
|
|
21
|
+
export const BROWSER_TEST_SCAFFOLD_ROOT = 'browser/base/content/test/';
|
|
16
22
|
/**
|
|
17
23
|
* Suffix for the per-binary xpcshell scaffold parent directory. Components
|
|
18
24
|
* created with `furnace create --with-tests --xpcshell` land at
|
|
@@ -27,7 +33,26 @@ const XPCSHELL_TEST_DIR_SUFFIX = '-xpcshell';
|
|
|
27
33
|
* path `remove.ts` / `rename.ts` / `validate.ts` must clean up.
|
|
28
34
|
*/
|
|
29
35
|
export function xpcshellTestParentDir(binaryName) {
|
|
30
|
-
return
|
|
36
|
+
return `${BROWSER_TEST_SCAFFOLD_ROOT}${binaryName}${XPCSHELL_TEST_DIR_SUFFIX}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the engine-relative directory a browser-chrome scaffold is
|
|
40
|
+
* written to: the `--test-dir` override when given, else
|
|
41
|
+
* `browser/base/content/test/<binaryName>`. The scaffolder AND the
|
|
42
|
+
* dry-run / success formatters resolve through this one function, so the
|
|
43
|
+
* printed plan cannot disagree with the files that land on disk.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveBrowserChromeTestDir(binaryName, override) {
|
|
46
|
+
return override ?? `${BROWSER_TEST_SCAFFOLD_ROOT}${binaryName}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the engine-relative directory an xpcshell scaffold is written
|
|
50
|
+
* to. A `--test-dir` override names the FINAL directory (no per-component
|
|
51
|
+
* segment is appended); the default is
|
|
52
|
+
* `browser/base/content/test/<binaryName>-xpcshell/<componentName>`.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveXpcshellTestDir(binaryName, componentName, override) {
|
|
55
|
+
return override ?? `${xpcshellTestParentDir(binaryName)}/${componentName}`;
|
|
31
56
|
}
|
|
32
57
|
/** File extensions that constitute a Furnace component's source files. */
|
|
33
58
|
const COMPONENT_FILE_EXTENSIONS = ['.mjs', '.css', '.ftl'];
|
|
@@ -69,7 +69,9 @@ export declare function hasUpstreamMplBlockHeader(content: string): boolean;
|
|
|
69
69
|
export declare function hasAnyLicenseHeaderAnyStyle(content: string): boolean;
|
|
70
70
|
/**
|
|
71
71
|
* Returns true if the first few lines of `content` contain a recognized
|
|
72
|
-
* upstream license identifier string
|
|
72
|
+
* upstream license identifier string — Mozilla's MPL boilerplate, an SPDX
|
|
73
|
+
* tag, the common permissive/copyleft license names, or the CC0
|
|
74
|
+
* public-domain dedication upstream test files carry.
|
|
73
75
|
*
|
|
74
76
|
* @param content - File content to check
|
|
75
77
|
* @param maxLines - Number of leading lines to inspect (default 10)
|
|
@@ -77,10 +79,12 @@ export declare function hasAnyLicenseHeaderAnyStyle(content: string): boolean;
|
|
|
77
79
|
export declare function containsUpstreamLicenseText(content: string, maxLines?: number): boolean;
|
|
78
80
|
/**
|
|
79
81
|
* Returns true when the head of `content` carries a recognized THIRD-PARTY
|
|
80
|
-
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0)
|
|
81
|
-
* comment style. Used by `export` to
|
|
82
|
-
* to prepend the project's own
|
|
83
|
-
* upstream bundle
|
|
82
|
+
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0) or the
|
|
83
|
+
* CC0 public-domain dedication, in any comment style. Used by `export` to
|
|
84
|
+
* treat such files as vendored: offering to prepend the project's own
|
|
85
|
+
* license header onto a byte-identity-pinned upstream bundle — or onto a
|
|
86
|
+
* test file Mozilla dedicated to the public domain — would mislicense code
|
|
87
|
+
* the project does not own.
|
|
84
88
|
*
|
|
85
89
|
* @param content - File content to check
|
|
86
90
|
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
@@ -227,9 +227,31 @@ function normalizeLicenseHeadForScan(head) {
|
|
|
227
227
|
s = s.replace(/\n[ \t]*\*[ \t]*/g, ' ');
|
|
228
228
|
return s.replace(/\s+/g, ' ').trim();
|
|
229
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Recognizes the CC0 public-domain dedication Mozilla puts on most of its
|
|
232
|
+
* test files, in every spelling the tree carries:
|
|
233
|
+
*
|
|
234
|
+
* - the prose (`Any copyright is dedicated to the Public Domain.`), which
|
|
235
|
+
* the block, `//` and `#` comment forms all reduce to after
|
|
236
|
+
* {@link normalizeLicenseHeadForScan};
|
|
237
|
+
* - the dedication URL (`creativecommons.org/publicdomain/zero/1.0/`, with
|
|
238
|
+
* either scheme);
|
|
239
|
+
* - the SPDX identifier `CC0-1.0`.
|
|
240
|
+
*
|
|
241
|
+
* Deliberately narrow: a bare "public domain" mention, a different Creative
|
|
242
|
+
* Commons license (`licenses/by/4.0/`, `CC-BY-4.0`) or a made-up
|
|
243
|
+
* `publicdomain/zero/2.0/` is NOT a CC0 dedication and must not be
|
|
244
|
+
* accepted as one. `scanText` must already be normalized (and may be
|
|
245
|
+
* lowercased — the match is case-insensitive).
|
|
246
|
+
*/
|
|
247
|
+
function hasCc0Dedication(scanText) {
|
|
248
|
+
return /dedicated to the public domain|creativecommons\.org\/publicdomain\/zero\/1\.0|spdx-license-identifier:\s*cc0-1\.0\b/i.test(scanText);
|
|
249
|
+
}
|
|
230
250
|
/**
|
|
231
251
|
* Returns true if the first few lines of `content` contain a recognized
|
|
232
|
-
* upstream license identifier string
|
|
252
|
+
* upstream license identifier string — Mozilla's MPL boilerplate, an SPDX
|
|
253
|
+
* tag, the common permissive/copyleft license names, or the CC0
|
|
254
|
+
* public-domain dedication upstream test files carry.
|
|
233
255
|
*
|
|
234
256
|
* @param content - File content to check
|
|
235
257
|
* @param maxLines - Number of leading lines to inspect (default 10)
|
|
@@ -244,14 +266,16 @@ export function containsUpstreamLicenseText(content, maxLines = 10) {
|
|
|
244
266
|
'MIT License',
|
|
245
267
|
'GNU General Public License',
|
|
246
268
|
];
|
|
247
|
-
return markers.some((marker) => scanText.includes(marker));
|
|
269
|
+
return markers.some((marker) => scanText.includes(marker)) || hasCc0Dedication(scanText);
|
|
248
270
|
}
|
|
249
271
|
/**
|
|
250
272
|
* Returns true when the head of `content` carries a recognized THIRD-PARTY
|
|
251
|
-
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0)
|
|
252
|
-
* comment style. Used by `export` to
|
|
253
|
-
* to prepend the project's own
|
|
254
|
-
* upstream bundle
|
|
273
|
+
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0) or the
|
|
274
|
+
* CC0 public-domain dedication, in any comment style. Used by `export` to
|
|
275
|
+
* treat such files as vendored: offering to prepend the project's own
|
|
276
|
+
* license header onto a byte-identity-pinned upstream bundle — or onto a
|
|
277
|
+
* test file Mozilla dedicated to the public domain — would mislicense code
|
|
278
|
+
* the project does not own.
|
|
255
279
|
*
|
|
256
280
|
* @param content - File content to check
|
|
257
281
|
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
@@ -271,7 +295,9 @@ export function hasThirdPartyPermissiveBanner(content, maxLines = 30) {
|
|
|
271
295
|
];
|
|
272
296
|
if (markers.some((marker) => scanText.includes(marker)))
|
|
273
297
|
return true;
|
|
274
|
-
|
|
298
|
+
if (hasCc0Dedication(scanText))
|
|
299
|
+
return true;
|
|
300
|
+
return /spdx-license-identifier:\s*(mit|isc|bsd-2-clause|bsd-3-clause|apache-2\.0|cc0-1\.0)\b/.test(scanText);
|
|
275
301
|
}
|
|
276
302
|
/**
|
|
277
303
|
* Prepends the license header to a file on disk if it is not already present.
|
|
@@ -27,7 +27,7 @@ import { verbose, warn } from '../utils/logger.js';
|
|
|
27
27
|
import { isPackageablePath, isXpcomManifestPath } from './build-audit.js';
|
|
28
28
|
import { readBuildBaseline } from './build-baseline.js';
|
|
29
29
|
import { hashEngineFile } from './coverage-extend.js';
|
|
30
|
-
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
30
|
+
import { collectChangedEnginePaths, dropPathsMatchingFingerprints } from './engine-changes.js';
|
|
31
31
|
export { isXpcomManifestPath };
|
|
32
32
|
/** Cap on the number of changed paths rendered inline. */
|
|
33
33
|
const STALE_PATHS_LIMIT = 10;
|
|
@@ -60,18 +60,7 @@ export async function checkStaleBuildForTest(projectRoot, engineDir) {
|
|
|
60
60
|
// and the stale check fires immediately after a successful one. The
|
|
61
61
|
// fingerprints capture "these files had this content when the build ran";
|
|
62
62
|
// a path stays stale only when its live hash diverges.
|
|
63
|
-
|
|
64
|
-
if (fingerprints) {
|
|
65
|
-
const staleAfterHashCheck = [];
|
|
66
|
-
for (const path of packageable) {
|
|
67
|
-
const recorded = fingerprints[path];
|
|
68
|
-
const live = await hashEngineFile(engineDir, path);
|
|
69
|
-
if (recorded === undefined || live === undefined || recorded !== live) {
|
|
70
|
-
staleAfterHashCheck.push(path);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
packageable = staleAfterHashCheck;
|
|
74
|
-
}
|
|
63
|
+
packageable = await dropPathsMatchingFingerprints(engineDir, packageable, baseline.packageableFingerprints);
|
|
75
64
|
if (packageable.length === 0) {
|
|
76
65
|
return { stale: false, changedPaths: [], truncated: 0, baseline };
|
|
77
66
|
}
|
package/docs/testing.md
CHANGED
|
@@ -38,6 +38,37 @@ coverage instead of replacing it; it is refused when the build anchor moved
|
|
|
38
38
|
(engine HEAD, `engine/mozconfig`, or a previously fingerprinted packageable
|
|
39
39
|
file).
|
|
40
40
|
|
|
41
|
+
### What a pre-test build costs
|
|
42
|
+
|
|
43
|
+
`test --build` runs `mach build faster`, with two escalations decided by
|
|
44
|
+
comparing build inputs against the **last successful build** recorded in
|
|
45
|
+
`.fireforge/last-build.json` — not against engine HEAD, since a fork's
|
|
46
|
+
worktree is permanently dirty (imported patches, Furnace-applied
|
|
47
|
+
components):
|
|
48
|
+
|
|
49
|
+
- a changed `moz.build` / `moz.configure` / `Makefile.in` runs
|
|
50
|
+
`mach configure` first;
|
|
51
|
+
- a changed `jar.mn` escalates the whole build to a full `mach build`
|
|
52
|
+
(minutes rather than seconds), so that new install-manifest destinations
|
|
53
|
+
exist before the run.
|
|
54
|
+
|
|
55
|
+
Each escalation is paid once per actual content change: a `jar.mn` that is
|
|
56
|
+
dirty against HEAD but byte-identical to what the last full build consumed
|
|
57
|
+
does not escalate again. `fireforge build --ui` is itself a
|
|
58
|
+
`mach build faster` and never escalates; the `jar.mn` record it writes is
|
|
59
|
+
carried forward from the previous full build, so a registration made
|
|
60
|
+
between a `build --ui` and the next `test --build` still triggers the full
|
|
61
|
+
build once.
|
|
62
|
+
|
|
63
|
+
Whether a full build is really required for an entry added to an existing
|
|
64
|
+
`jar.mn` under `dist/bin` is an open question, recorded in the 0.44.0
|
|
65
|
+
changelog: mozbuild regenerates its backend from a newer `jar.mn` on any
|
|
66
|
+
`mach build`, `faster` included, and the FasterMake backend consumes
|
|
67
|
+
`dist/bin` jar manifests. The escalation stays until that is demonstrated
|
|
68
|
+
end to end against a real objdir (`build --ui` after a jar-only
|
|
69
|
+
registration, then a plain `test`), because relaxing a stale-artifact guard
|
|
70
|
+
on source reading alone trades a slow build for a silently wrong test.
|
|
71
|
+
|
|
41
72
|
## Resilience
|
|
42
73
|
|
|
43
74
|
- Recognized harness crashes retry up to `--harness-retries <n>` times
|