@hominis/fireforge 0.44.3 → 0.45.0
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 +47 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/build.js +9 -4
- package/dist/src/commands/doctor-source-pin.d.ts +29 -0
- package/dist/src/commands/doctor-source-pin.js +81 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/export-flow.js +3 -18
- 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/patch/move-files-into.js +3 -6
- package/dist/src/commands/patch/split-plan.js +3 -6
- package/dist/src/commands/re-export-adjacent.d.ts +8 -0
- package/dist/src/commands/re-export-adjacent.js +26 -1
- package/dist/src/commands/re-export-drift.d.ts +8 -0
- package/dist/src/commands/re-export-drift.js +61 -0
- package/dist/src/commands/re-export-files.js +3 -6
- package/dist/src/commands/re-export-options.js +19 -5
- package/dist/src/commands/re-export-register.js +5 -1
- package/dist/src/commands/re-export-scan.js +3 -6
- package/dist/src/commands/re-export.js +16 -16
- package/dist/src/commands/test-build-phase.js +3 -2
- package/dist/src/commands/test-diagnose.js +14 -5
- package/dist/src/commands/test-register.js +1 -0
- package/dist/src/commands/test-run.js +4 -3
- package/dist/src/commands/test-stale-gate.js +5 -2
- package/dist/src/commands/test.js +9 -2
- package/dist/src/core/build-audit.d.ts +20 -0
- package/dist/src/core/build-audit.js +69 -3
- 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-jar-escalation.d.ts +64 -0
- package/dist/src/core/build-jar-escalation.js +127 -0
- package/dist/src/core/build-prepare.d.ts +12 -1
- package/dist/src/core/build-prepare.js +68 -6
- package/dist/src/core/config-validate-file-size.d.ts +12 -0
- package/dist/src/core/config-validate-file-size.js +65 -0
- package/dist/src/core/config-validate.js +6 -0
- 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/harness-orphans.d.ts +73 -0
- package/dist/src/core/harness-orphans.js +186 -0
- package/dist/src/core/license-headers.d.ts +9 -5
- package/dist/src/core/license-headers.js +33 -7
- package/dist/src/core/mach-known-noise-filter.d.ts +21 -0
- package/dist/src/core/mach-known-noise-filter.js +24 -1
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +18 -2
- package/dist/src/core/patch-apply.js +49 -20
- package/dist/src/core/patch-lint-checkjs.js +6 -1
- package/dist/src/core/patch-lint-file-size.d.ts +52 -0
- package/dist/src/core/patch-lint-file-size.js +53 -0
- package/dist/src/core/patch-lint-prettier.d.ts +36 -0
- package/dist/src/core/patch-lint-prettier.js +126 -0
- package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
- package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
- package/dist/src/core/patch-lint.js +23 -24
- package/dist/src/core/patch-transform.d.ts +21 -0
- package/dist/src/core/patch-transform.js +38 -0
- package/dist/src/core/smoke-patterns.js +12 -0
- package/dist/src/core/test-harness-crash.js +61 -48
- package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
- package/dist/src/core/test-harness-verdict-notes.js +48 -0
- package/dist/src/core/test-stale-check.d.ts +23 -6
- package/dist/src/core/test-stale-check.js +77 -15
- package/dist/src/core/test-stall-triage.js +37 -19
- package/dist/src/errors/build.d.ts +28 -0
- package/dist/src/errors/build.js +49 -0
- package/dist/src/types/commands/options.d.ts +19 -0
- package/dist/src/types/commands/patches.d.ts +17 -6
- package/dist/src/types/config.d.ts +41 -0
- package/dist/src/utils/fs.d.ts +4 -2
- package/dist/src/utils/fs.js +3 -1
- package/docs/configuration.md +47 -0
- package/docs/exit-codes.md +4 -2
- package/docs/testing.md +83 -0
- package/package.json +1 -1
|
@@ -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`
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether a changed `jar.mn` really forces the pre-test build to
|
|
3
|
+
* escalate from `mach build faster` to a full `mach build`.
|
|
4
|
+
*
|
|
5
|
+
* The 0.41.0 rule was `any changed jar.mn ⇒ full build`, which costs ~10
|
|
6
|
+
* minutes against ~20 s for a chrome-only slice and is paid TWICE per
|
|
7
|
+
* green/no-change pair on a Furnace-touching slice. A downstream experiment
|
|
8
|
+
* (two clean runs, 2026-08-29, against 0.44.4) showed the rule is over-broad
|
|
9
|
+
* for the common case: a jar-only registration of a NEW content file added
|
|
10
|
+
* to an EXISTING `dist/bin` manifest was installed by a plain
|
|
11
|
+
* `fireforge build --ui` both times — `faster/install_dist_bin_browser` named
|
|
12
|
+
* the destination, the file landed in `dist/bin/…/content/browser/` and in
|
|
13
|
+
* the `.app` bundle, and a plain `fireforge test` fetched it over
|
|
14
|
+
* `chrome://` at runtime. Neither run escalated, and neither needed to.
|
|
15
|
+
*
|
|
16
|
+
* What the experiment did NOT exercise, and what therefore still escalates:
|
|
17
|
+
*
|
|
18
|
+
* - a NEW `jar.mn` file. The install manifest for a manifest the backend
|
|
19
|
+
* has never seen does not exist yet, and `mach build faster` consumes
|
|
20
|
+
* install manifests rather than regenerating the backend that writes
|
|
21
|
+
* them.
|
|
22
|
+
* - a manifest whose jar declaration carries a bracketed base-directory
|
|
23
|
+
* prefix (`[some/dir] name.jar:`), which redirects the install
|
|
24
|
+
* destination away from the default chrome root. That redirect is the
|
|
25
|
+
* "non-dist/bin target" half nobody has run.
|
|
26
|
+
*
|
|
27
|
+
* Everything else about the rule is unchanged, and every failure to read or
|
|
28
|
+
* parse a manifest escalates: this narrows a conservative rule, so its
|
|
29
|
+
* error direction stays the slow-but-correct one.
|
|
30
|
+
*/
|
|
31
|
+
import type { BuildBaseline } from './build-baseline-types.js';
|
|
32
|
+
/** Why one changed `jar.mn` still forces a full build. */
|
|
33
|
+
export interface JarEscalationCause {
|
|
34
|
+
/** Engine-relative manifest path. */
|
|
35
|
+
path: string;
|
|
36
|
+
/** Operator-facing reason, printed with the escalation notice. */
|
|
37
|
+
reason: string;
|
|
38
|
+
}
|
|
39
|
+
/** Outcome of {@link evaluateJarManifestEscalation}. */
|
|
40
|
+
export interface JarEscalationDecision {
|
|
41
|
+
/** True when at least one changed manifest still needs a full build. */
|
|
42
|
+
escalate: boolean;
|
|
43
|
+
/** One entry per manifest that forces the escalation. */
|
|
44
|
+
causes: JarEscalationCause[];
|
|
45
|
+
/** Manifests the narrowing cleared; kept for the verbose explanation. */
|
|
46
|
+
cleared: string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Evaluates every changed `jar.mn` against the narrowed rule.
|
|
50
|
+
*
|
|
51
|
+
* @param engineDir - Absolute engine directory
|
|
52
|
+
* @param changedPaths - Engine-relative paths changed since the last
|
|
53
|
+
* successful build (non-`jar.mn` entries are ignored)
|
|
54
|
+
* @param baseline - Last successful build's baseline (reserved: the
|
|
55
|
+
* fingerprint map already gated which paths reach here)
|
|
56
|
+
* @returns The escalation decision with per-manifest causes
|
|
57
|
+
*/
|
|
58
|
+
export declare function evaluateJarManifestEscalation(engineDir: string, changedPaths: readonly string[], baseline: BuildBaseline | undefined): Promise<JarEscalationDecision>;
|
|
59
|
+
/**
|
|
60
|
+
* Renders the operator-facing escalation notice. Names the manifest AND the
|
|
61
|
+
* reason: "a jar.mn changed" is the sentence that made the previous rule
|
|
62
|
+
* impossible to argue with from outside.
|
|
63
|
+
*/
|
|
64
|
+
export declare function formatJarEscalationNotice(decision: JarEscalationDecision): string;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Decides whether a changed `jar.mn` really forces the pre-test build to
|
|
4
|
+
* escalate from `mach build faster` to a full `mach build`.
|
|
5
|
+
*
|
|
6
|
+
* The 0.41.0 rule was `any changed jar.mn ⇒ full build`, which costs ~10
|
|
7
|
+
* minutes against ~20 s for a chrome-only slice and is paid TWICE per
|
|
8
|
+
* green/no-change pair on a Furnace-touching slice. A downstream experiment
|
|
9
|
+
* (two clean runs, 2026-08-29, against 0.44.4) showed the rule is over-broad
|
|
10
|
+
* for the common case: a jar-only registration of a NEW content file added
|
|
11
|
+
* to an EXISTING `dist/bin` manifest was installed by a plain
|
|
12
|
+
* `fireforge build --ui` both times — `faster/install_dist_bin_browser` named
|
|
13
|
+
* the destination, the file landed in `dist/bin/…/content/browser/` and in
|
|
14
|
+
* the `.app` bundle, and a plain `fireforge test` fetched it over
|
|
15
|
+
* `chrome://` at runtime. Neither run escalated, and neither needed to.
|
|
16
|
+
*
|
|
17
|
+
* What the experiment did NOT exercise, and what therefore still escalates:
|
|
18
|
+
*
|
|
19
|
+
* - a NEW `jar.mn` file. The install manifest for a manifest the backend
|
|
20
|
+
* has never seen does not exist yet, and `mach build faster` consumes
|
|
21
|
+
* install manifests rather than regenerating the backend that writes
|
|
22
|
+
* them.
|
|
23
|
+
* - a manifest whose jar declaration carries a bracketed base-directory
|
|
24
|
+
* prefix (`[some/dir] name.jar:`), which redirects the install
|
|
25
|
+
* destination away from the default chrome root. That redirect is the
|
|
26
|
+
* "non-dist/bin target" half nobody has run.
|
|
27
|
+
*
|
|
28
|
+
* Everything else about the rule is unchanged, and every failure to read or
|
|
29
|
+
* parse a manifest escalates: this narrows a conservative rule, so its
|
|
30
|
+
* error direction stays the slow-but-correct one.
|
|
31
|
+
*/
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
import { readText } from '../utils/fs.js';
|
|
34
|
+
import { verbose } from '../utils/logger.js';
|
|
35
|
+
import { isJarManifestPath } from './build-audit.js';
|
|
36
|
+
import { getUntrackedFilesInDir } from './git-status.js';
|
|
37
|
+
/**
|
|
38
|
+
* A jar declaration line: an optional bracketed base-directory prefix, then
|
|
39
|
+
* the jar name. Matched at column zero because entry lines inside a section
|
|
40
|
+
* are indented — an indented `foo.jar:` is content, not a declaration.
|
|
41
|
+
*/
|
|
42
|
+
const JAR_DECLARATION_PATTERN = /^(\[[^\]]*\]\s*)?[^\s[\]]+\.jar:\s*$/;
|
|
43
|
+
/** True when the jar declaration redirects the install base directory. */
|
|
44
|
+
function declaresNonDefaultBase(line) {
|
|
45
|
+
const match = JAR_DECLARATION_PATTERN.exec(line);
|
|
46
|
+
if (!match)
|
|
47
|
+
return false;
|
|
48
|
+
return match[1] !== undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* True when a changed `jar.mn` is NEW to the last successful build.
|
|
52
|
+
*
|
|
53
|
+
* Untracked in the engine repo is the reliable tell: a fork's engine tree is
|
|
54
|
+
* a checkout of upstream with patches applied to the WORKTREE, so a manifest
|
|
55
|
+
* a patch created is untracked and one that shipped upstream is tracked. The
|
|
56
|
+
* baseline's fingerprints cannot answer this on their own — they record only
|
|
57
|
+
* the paths that were DIRTY at the last build, so a clean pre-existing
|
|
58
|
+
* manifest has no entry either.
|
|
59
|
+
*/
|
|
60
|
+
async function isNewJarManifest(engineDir, path) {
|
|
61
|
+
const untracked = await getUntrackedFilesInDir(engineDir, path);
|
|
62
|
+
return untracked.includes(path);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Evaluates every changed `jar.mn` against the narrowed rule.
|
|
66
|
+
*
|
|
67
|
+
* @param engineDir - Absolute engine directory
|
|
68
|
+
* @param changedPaths - Engine-relative paths changed since the last
|
|
69
|
+
* successful build (non-`jar.mn` entries are ignored)
|
|
70
|
+
* @param baseline - Last successful build's baseline (reserved: the
|
|
71
|
+
* fingerprint map already gated which paths reach here)
|
|
72
|
+
* @returns The escalation decision with per-manifest causes
|
|
73
|
+
*/
|
|
74
|
+
export async function evaluateJarManifestEscalation(engineDir, changedPaths, baseline) {
|
|
75
|
+
void baseline;
|
|
76
|
+
const causes = [];
|
|
77
|
+
const cleared = [];
|
|
78
|
+
for (const path of changedPaths.filter(isJarManifestPath)) {
|
|
79
|
+
let isNew;
|
|
80
|
+
try {
|
|
81
|
+
isNew = await isNewJarManifest(engineDir, path);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// Fail closed: an unanswerable "is this new?" keeps the old rule.
|
|
85
|
+
verbose(`jar escalation: could not classify engine/${path} (${String(error)}); escalating.`);
|
|
86
|
+
causes.push({ path, reason: 'could not determine whether the manifest is new' });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (isNew) {
|
|
90
|
+
causes.push({ path, reason: 'new jar.mn (no install manifest exists for it yet)' });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
let body;
|
|
94
|
+
try {
|
|
95
|
+
body = await readText(join(engineDir, path));
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
verbose(`jar escalation: could not read engine/${path} (${String(error)}); escalating.`);
|
|
99
|
+
causes.push({ path, reason: 'manifest could not be read' });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const redirected = body
|
|
103
|
+
.split(/\r?\n/)
|
|
104
|
+
.filter((line) => declaresNonDefaultBase(line))
|
|
105
|
+
.map((line) => line.trim());
|
|
106
|
+
if (redirected.length > 0) {
|
|
107
|
+
causes.push({
|
|
108
|
+
path,
|
|
109
|
+
reason: `jar declaration redirects the install base directory (${redirected[0] ?? ''})`,
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
cleared.push(path);
|
|
114
|
+
}
|
|
115
|
+
return { escalate: causes.length > 0, causes, cleared };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Renders the operator-facing escalation notice. Names the manifest AND the
|
|
119
|
+
* reason: "a jar.mn changed" is the sentence that made the previous rule
|
|
120
|
+
* impossible to argue with from outside.
|
|
121
|
+
*/
|
|
122
|
+
export function formatJarEscalationNotice(decision) {
|
|
123
|
+
const detail = decision.causes.map((c) => `engine/${c.path} (${c.reason})`).join('; ');
|
|
124
|
+
return (`Escalating this pre-test build to a full mach build so new install-manifest destinations ` +
|
|
125
|
+
`are created: ${detail}. A changed jar.mn that merely adds entries to an existing ` +
|
|
126
|
+
`dist/bin manifest no longer escalates.`);
|
|
127
|
+
}
|
|
@@ -14,6 +14,11 @@ export interface BuildPreparation {
|
|
|
14
14
|
reconfigured: boolean;
|
|
15
15
|
/** A changed packaging manifest cannot be safely handled by `mach build faster`. */
|
|
16
16
|
fullBuildRequired?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Operator-facing explanation for {@link fullBuildRequired}, naming the
|
|
19
|
+
* manifest and why it still escalates. Absent when no escalation applies.
|
|
20
|
+
*/
|
|
21
|
+
fullBuildReason?: string;
|
|
17
22
|
}
|
|
18
23
|
/** Options for {@link prepareBuildEnvironment}. */
|
|
19
24
|
export interface PrepareBuildOptions {
|
|
@@ -31,7 +36,13 @@ export interface PrepareBuildOptions {
|
|
|
31
36
|
*/
|
|
32
37
|
refuseUnexportedDrift?: boolean | undefined;
|
|
33
38
|
}
|
|
34
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Packaging manifests whose graph/destination directories MAY need a full
|
|
41
|
+
* build. Path-shape only: {@link evaluateJarManifestEscalation} decides
|
|
42
|
+
* whether a given changed manifest actually forces one (a new manifest, or
|
|
43
|
+
* one that redirects its install base directory), so an entry added to an
|
|
44
|
+
* existing `dist/bin` manifest no longer costs a ~10-minute build.
|
|
45
|
+
*/
|
|
35
46
|
export declare function requiresFullBuildForIncrementalTest(path: string): boolean;
|
|
36
47
|
/**
|
|
37
48
|
* Describes an exit code in the shell's 128+N signal convention. Truncated
|
|
@@ -11,8 +11,10 @@ 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';
|
|
15
|
+
import { evaluateJarManifestEscalation, formatJarEscalationNotice, } from './build-jar-escalation.js';
|
|
14
16
|
import { findUnexportedDriftAtRisk, formatUnexportedDriftWarning, } from './build-overwrite-guard.js';
|
|
15
|
-
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
17
|
+
import { collectChangedEnginePaths, dropPathsMatchingFingerprints } from './engine-changes.js';
|
|
16
18
|
import { applyAllComponents } from './furnace-apply.js';
|
|
17
19
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
|
|
18
20
|
import { runFurnaceMutation } from './furnace-operation.js';
|
|
@@ -22,9 +24,43 @@ import { generateMozconfig, runMachCapture } from './mach.js';
|
|
|
22
24
|
import { explainMachError } from './mach-error-hints.js';
|
|
23
25
|
/** Path fragments of files whose edits invalidate the recursive-make backend. */
|
|
24
26
|
const BACKEND_INVALIDATING_SUFFIXES = ['moz.build', 'moz.configure', 'Makefile.in'];
|
|
25
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Packaging manifests whose graph/destination directories MAY need a full
|
|
29
|
+
* build. Path-shape only: {@link evaluateJarManifestEscalation} decides
|
|
30
|
+
* whether a given changed manifest actually forces one (a new manifest, or
|
|
31
|
+
* one that redirects its install base directory), so an entry added to an
|
|
32
|
+
* existing `dist/bin` manifest no longer costs a ~10-minute build.
|
|
33
|
+
*/
|
|
26
34
|
export function requiresFullBuildForIncrementalTest(path) {
|
|
27
|
-
return path
|
|
35
|
+
return isJarManifestPath(path);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build-input manifests changed since the last SUCCESSFUL build — not
|
|
39
|
+
* merely dirty against HEAD. `collectChangedEnginePaths` unions the
|
|
40
|
+
* baseline-to-HEAD diff with every worktree modification, and a fork's
|
|
41
|
+
* worktree is permanently dirty (imported patches, Furnace-applied
|
|
42
|
+
* components), so on its own it reports a patch-touched `jar.mn` or
|
|
43
|
+
* `moz.build` as changed on every run: `mach configure` re-ran and the
|
|
44
|
+
* pre-test build escalated to a full `mach build` after each full build
|
|
45
|
+
* that had already consumed exactly this content. The baseline's
|
|
46
|
+
* `buildInputFingerprints` say what the last successful build saw;
|
|
47
|
+
* anything byte-identical to that is dropped here. A baseline without the
|
|
48
|
+
* field (written before it existed) keeps every dirty input, which is the
|
|
49
|
+
* old behaviour.
|
|
50
|
+
*
|
|
51
|
+
* @param engineDir - Path to the engine directory
|
|
52
|
+
* @param baseline - Last successful build's baseline
|
|
53
|
+
* @returns Engine-relative build-input paths that still need acting on
|
|
54
|
+
*/
|
|
55
|
+
async function collectChangedBuildInputs(engineDir, baseline) {
|
|
56
|
+
const changed = await collectChangedEnginePaths(engineDir, baseline, 'Auto-configure');
|
|
57
|
+
const dirtyInputs = changed.filter(isBuildInputPath);
|
|
58
|
+
const stillChanged = await dropPathsMatchingFingerprints(engineDir, dirtyInputs, baseline.buildInputFingerprints);
|
|
59
|
+
const unchangedSinceBuild = dirtyInputs.filter((path) => !stillChanged.includes(path));
|
|
60
|
+
if (unchangedSinceBuild.length > 0) {
|
|
61
|
+
verbose(`Auto-configure: ${unchangedSinceBuild.length} dirty build input(s) byte-identical to the last successful build, not treated as changed: ${unchangedSinceBuild.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
return stillChanged;
|
|
28
64
|
}
|
|
29
65
|
/**
|
|
30
66
|
* Extracts the tail of captured `mach configure` output so the underlying
|
|
@@ -78,6 +114,24 @@ function buildConfigureFailureError(captured) {
|
|
|
78
114
|
(hints.length > 0 ? `\n\n${hints.map((hint) => `Hint: ${hint}`).join('\n')}` : '') +
|
|
79
115
|
'\n\nBuild stopped because continuing would hide the real configure failure.', 'mach configure');
|
|
80
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Runs the narrowed `jar.mn` escalation rule over this run's changed build
|
|
119
|
+
* inputs and returns the operator-facing reason when a full build is still
|
|
120
|
+
* required, or undefined when the narrowing cleared every changed manifest.
|
|
121
|
+
*
|
|
122
|
+
* Extracted so `prepareBuildEnvironment` stays under the complexity budget.
|
|
123
|
+
*
|
|
124
|
+
* @param engineDir - Absolute engine directory
|
|
125
|
+
* @param changed - Build inputs changed since the last successful build
|
|
126
|
+
* @param baseline - Last successful build's baseline
|
|
127
|
+
*/
|
|
128
|
+
async function decideJarEscalation(engineDir, changed, baseline) {
|
|
129
|
+
const decision = await evaluateJarManifestEscalation(engineDir, changed.filter(requiresFullBuildForIncrementalTest), baseline);
|
|
130
|
+
if (decision.cleared.length > 0) {
|
|
131
|
+
verbose(`jar escalation: ${String(decision.cleared.length)} changed jar.mn add entries to an existing dist/bin manifest; not escalating for ${decision.cleared.join(', ')}.`);
|
|
132
|
+
}
|
|
133
|
+
return decision.escalate ? formatJarEscalationNotice(decision) : undefined;
|
|
134
|
+
}
|
|
81
135
|
/**
|
|
82
136
|
* Returns true when the file path matches a pattern that forces
|
|
83
137
|
* `mach configure` to regenerate the backend. Exported for testing.
|
|
@@ -148,10 +202,13 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
148
202
|
// work against a stale recursive-make backend.
|
|
149
203
|
let reconfigured = false;
|
|
150
204
|
let fullBuildRequired = false;
|
|
205
|
+
let fullBuildReason;
|
|
151
206
|
if (options.previousBaseline) {
|
|
152
|
-
const changed = await
|
|
207
|
+
const changed = await collectChangedBuildInputs(paths.engine, options.previousBaseline);
|
|
153
208
|
const invalidating = changed.filter(isBackendInvalidatingFile);
|
|
154
|
-
|
|
209
|
+
const jarEscalation = await decideJarEscalation(paths.engine, changed, options.previousBaseline);
|
|
210
|
+
fullBuildRequired = jarEscalation !== undefined;
|
|
211
|
+
fullBuildReason = jarEscalation;
|
|
155
212
|
if (invalidating.length > 0) {
|
|
156
213
|
// Warning severity: this is the "why is this run slow" explanation,
|
|
157
214
|
// and an info-level line does not survive an agent output filter that
|
|
@@ -271,5 +328,10 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
271
328
|
mozconfigSpinner.error('Failed to generate mozconfig');
|
|
272
329
|
throw error;
|
|
273
330
|
}
|
|
274
|
-
return {
|
|
331
|
+
return {
|
|
332
|
+
furnaceApplied,
|
|
333
|
+
reconfigured,
|
|
334
|
+
fullBuildRequired,
|
|
335
|
+
...(fullBuildReason !== undefined ? { fullBuildReason } : {}),
|
|
336
|
+
};
|
|
275
337
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation for `patchLint.fileSizeThresholds`.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `config-validate.ts`, which is at its per-file line budget.
|
|
5
|
+
*/
|
|
6
|
+
import type { PatchLintFileSizeThresholds } from '../types/config.js';
|
|
7
|
+
/**
|
|
8
|
+
* Parses the `patchLint.fileSizeThresholds` block. Ordering is validated
|
|
9
|
+
* against the MERGED triple (overrides on top of the defaults), so setting
|
|
10
|
+
* only `warning` cannot land below the default `notice`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parsePatchLintFileSizeThresholds(raw: unknown): PatchLintFileSizeThresholds;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Validation for `patchLint.fileSizeThresholds`.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `config-validate.ts`, which is at its per-file line budget.
|
|
6
|
+
*/
|
|
7
|
+
import { ConfigError } from '../errors/config.js';
|
|
8
|
+
import { isObject } from '../utils/validation.js';
|
|
9
|
+
import { DEFAULT_FILE_SIZE_THRESHOLDS } from './patch-lint-file-size.js';
|
|
10
|
+
/**
|
|
11
|
+
* Parses one `patchLint.fileSizeThresholds.<tier>` triple. Every field is
|
|
12
|
+
* optional and merges over the built-in default; values must be positive
|
|
13
|
+
* integers and stay ordered `notice <= warning <= error`, because an
|
|
14
|
+
* out-of-order triple would silently disable a band instead of failing.
|
|
15
|
+
*/
|
|
16
|
+
function parseFileSizeTier(raw, field) {
|
|
17
|
+
if (!isObject(raw)) {
|
|
18
|
+
throw new ConfigError(`Config field "${field}" must be a plain object`);
|
|
19
|
+
}
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const key of ['notice', 'warning', 'error']) {
|
|
22
|
+
const value = raw[key];
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
continue;
|
|
25
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
26
|
+
throw new ConfigError(`Config field "${field}.${key}" must be a positive integer`);
|
|
27
|
+
}
|
|
28
|
+
out[key] = value;
|
|
29
|
+
}
|
|
30
|
+
for (const key of Object.keys(raw)) {
|
|
31
|
+
if (!['notice', 'warning', 'error'].includes(key)) {
|
|
32
|
+
throw new ConfigError(`Config field "${field}" has unknown key "${key}"`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parses the `patchLint.fileSizeThresholds` block. Ordering is validated
|
|
39
|
+
* against the MERGED triple (overrides on top of the defaults), so setting
|
|
40
|
+
* only `warning` cannot land below the default `notice`.
|
|
41
|
+
*/
|
|
42
|
+
export function parsePatchLintFileSizeThresholds(raw) {
|
|
43
|
+
if (!isObject(raw)) {
|
|
44
|
+
throw new ConfigError('Config field "patchLint.fileSizeThresholds" must be a plain object');
|
|
45
|
+
}
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const tier of ['general', 'test']) {
|
|
48
|
+
const value = raw[tier];
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
const parsed = parseFileSizeTier(value, `patchLint.fileSizeThresholds.${tier}`);
|
|
52
|
+
const merged = { ...DEFAULT_FILE_SIZE_THRESHOLDS[tier], ...parsed };
|
|
53
|
+
if (merged.notice > merged.warning || merged.warning > merged.error) {
|
|
54
|
+
throw new ConfigError(`Config field "patchLint.fileSizeThresholds.${tier}" must satisfy notice <= warning <= error ` +
|
|
55
|
+
`(resolved: ${String(merged.notice)}/${String(merged.warning)}/${String(merged.error)})`);
|
|
56
|
+
}
|
|
57
|
+
out[tier] = parsed;
|
|
58
|
+
}
|
|
59
|
+
for (const key of Object.keys(raw)) {
|
|
60
|
+
if (key !== 'general' && key !== 'test') {
|
|
61
|
+
throw new ConfigError(`Config field "patchLint.fileSizeThresholds" has unknown key "${key}"`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
@@ -8,6 +8,7 @@ import { parseObject } from '../utils/parse.js';
|
|
|
8
8
|
import { isContainedRelativePath, isExplicitAbsolutePath } from '../utils/paths.js';
|
|
9
9
|
import { describeProductVersionIncompatibility, FIREFOX_PRODUCTS, isObject, isValidAppId, isValidFirefoxCandidate, isValidFirefoxProduct, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LICENSES, } from '../utils/validation.js';
|
|
10
10
|
import { SUPPORTED_CONFIG_ROOT_KEYS } from './config-paths.js';
|
|
11
|
+
import { parsePatchLintFileSizeThresholds } from './config-validate-file-size.js';
|
|
11
12
|
import { parsePatchPolicyBlock } from './config-validate-patch-policy.js';
|
|
12
13
|
import { parseExternalToolchainsBlock, parseTestBlock } from './config-validate-test-toolchains.js';
|
|
13
14
|
/**
|
|
@@ -323,6 +324,7 @@ function optionalPatchLintBoolean(rec, key) {
|
|
|
323
324
|
}
|
|
324
325
|
/** Severity-gate fields, each parsed and assigned identically. */
|
|
325
326
|
const PATCH_LINT_SEVERITY_GATE_KEYS = [
|
|
327
|
+
'prettier',
|
|
326
328
|
'jsdocClassMethods',
|
|
327
329
|
'testAssertionFloor',
|
|
328
330
|
'chromeScriptJsDoc',
|
|
@@ -359,6 +361,10 @@ function parsePatchLintBlock(rec) {
|
|
|
359
361
|
}
|
|
360
362
|
out.rawColorAllowlist = rawColorAllowlist;
|
|
361
363
|
}
|
|
364
|
+
const fileSizeThresholds = rec.raw('fileSizeThresholds');
|
|
365
|
+
if (fileSizeThresholds !== undefined) {
|
|
366
|
+
out.fileSizeThresholds = parsePatchLintFileSizeThresholds(fileSizeThresholds);
|
|
367
|
+
}
|
|
362
368
|
for (const key of PATCH_LINT_SEVERITY_GATE_KEYS) {
|
|
363
369
|
const gate = parseSeverityGate(rec.raw(key), `patchLint.${key}`);
|
|
364
370
|
if (gate !== undefined)
|
|
@@ -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
|
+
}
|