@hominis/fireforge 0.37.0 → 0.38.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 +13 -0
- package/README.md +1 -1
- package/dist/src/commands/build.js +5 -5
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/download.js +5 -2
- package/dist/src/commands/export-flow.d.ts +5 -1
- package/dist/src/commands/export-flow.js +10 -7
- package/dist/src/commands/export-placement-gate.js +3 -5
- package/dist/src/commands/export-placement-policy.d.ts +11 -3
- package/dist/src/commands/export-placement-policy.js +32 -29
- package/dist/src/commands/export.js +5 -5
- package/dist/src/commands/furnace/index.js +23 -18
- package/dist/src/commands/patch/move-files-create.d.ts +19 -0
- package/dist/src/commands/patch/move-files-create.js +116 -0
- package/dist/src/commands/patch/move-files.d.ts +3 -1
- package/dist/src/commands/patch/move-files.js +49 -4
- package/dist/src/commands/patch/split.d.ts +11 -1
- package/dist/src/commands/patch/split.js +6 -5
- package/dist/src/commands/re-export-scan.d.ts +17 -0
- package/dist/src/commands/re-export-scan.js +51 -0
- package/dist/src/commands/re-export.js +17 -6
- package/dist/src/commands/source.js +28 -3
- package/dist/src/commands/test-register.js +6 -5
- package/dist/src/commands/test-stale-gate.d.ts +44 -0
- package/dist/src/commands/test-stale-gate.js +81 -0
- package/dist/src/commands/test.js +15 -50
- package/dist/src/core/build-audit.d.ts +12 -0
- package/dist/src/core/build-audit.js +14 -0
- package/dist/src/core/build-baseline-types.d.ts +34 -0
- package/dist/src/core/build-baseline.d.ts +6 -1
- package/dist/src/core/build-baseline.js +47 -7
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +6 -1
- package/dist/src/core/engine-session-lock.d.ts +10 -1
- package/dist/src/core/engine-session-lock.js +28 -2
- package/dist/src/core/file-lock.d.ts +33 -0
- package/dist/src/core/file-lock.js +32 -6
- package/dist/src/core/firefox-archive.d.ts +2 -1
- package/dist/src/core/firefox-archive.js +32 -8
- package/dist/src/core/firefox.d.ts +8 -3
- package/dist/src/core/firefox.js +11 -6
- package/dist/src/core/furnace-validate-helpers.js +28 -1
- package/dist/src/core/license-headers.d.ts +12 -0
- package/dist/src/core/license-headers.js +76 -1
- package/dist/src/core/patch-lint.d.ts +3 -0
- package/dist/src/core/patch-lint.js +25 -7
- package/dist/src/core/status-classify.d.ts +11 -2
- package/dist/src/core/status-classify.js +49 -25
- package/dist/src/core/test-stale-check.d.ts +41 -1
- package/dist/src/core/test-stale-check.js +77 -3
- package/dist/src/types/commands/options.d.ts +57 -3
- package/dist/src/types/config.d.ts +7 -0
- package/dist/src/utils/options.d.ts +20 -0
- package/dist/src/utils/options.js +39 -0
- package/dist/src/utils/validation.d.ts +5 -0
- package/dist/src/utils/validation.js +7 -0
- package/package.json +3 -3
|
@@ -10,17 +10,17 @@ import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionette
|
|
|
10
10
|
import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
|
|
11
11
|
import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
12
12
|
import { analyzeTestPathScopes, formatScopeNotice, } from '../core/test-path-scope.js';
|
|
13
|
-
import { checkStaleBuildForTest, findUncoveredRequestPaths, formatStaleBuildWarning, formatTestCoverageRefusal, } from '../core/test-stale-check.js';
|
|
14
13
|
import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
15
14
|
import { GeneralError } from '../errors/base.js';
|
|
16
15
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
17
16
|
import { toError } from '../utils/errors.js';
|
|
18
17
|
import { pathExists } from '../utils/fs.js';
|
|
19
|
-
import { info, intro, outro, spinner, success, verbose
|
|
18
|
+
import { info, intro, outro, spinner, success, verbose } from '../utils/logger.js';
|
|
20
19
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
21
20
|
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
22
21
|
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
|
|
23
22
|
import { DEFAULT_HARNESS_RETRIES, runShardedTests, runTestsWithRetries, } from './test-run.js';
|
|
23
|
+
import { enforceStaleBuildGate, enforceStaticComponentsGate } from './test-stale-gate.js';
|
|
24
24
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
25
25
|
const missingPaths = [];
|
|
26
26
|
for (const testPath of testPaths) {
|
|
@@ -135,9 +135,12 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
|
|
|
135
135
|
// same files — in any invocation shape — is not refused. A failed
|
|
136
136
|
// write never fails the run. The coverage claim is scoped to the
|
|
137
137
|
// requested test paths, since a file-scoped `test --build` only
|
|
138
|
-
// guarantees packaging for those manifests.
|
|
138
|
+
// guarantees packaging for those manifests. The previous baseline is
|
|
139
|
+
// passed through so a scoped write carries the static-components
|
|
140
|
+
// anchor forward (a `mach build faster` does not rebake
|
|
141
|
+
// components.conf into the compiled table).
|
|
139
142
|
try {
|
|
140
|
-
await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, testPackagingCoverage);
|
|
143
|
+
await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, testPackagingCoverage, previousBaseline);
|
|
141
144
|
}
|
|
142
145
|
catch (baselineError) {
|
|
143
146
|
verbose(`Could not persist build baseline: ${toError(baselineError).message}`);
|
|
@@ -150,51 +153,6 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
|
|
|
150
153
|
: 'Pre-test build failed', 'mach build faster');
|
|
151
154
|
});
|
|
152
155
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Stale-build preflight — when `--build` was NOT requested, detect
|
|
155
|
-
* packageable engine edits since the last successful build and fail
|
|
156
|
-
* UP-FRONT unless the operator explicitly accepts the stale package risk.
|
|
157
|
-
*
|
|
158
|
-
* Packaging COVERAGE is checked first, on EVERY non-`--build` run and
|
|
159
|
-
* regardless of staleness or `--allow-stale-build`: a runtime packaged by
|
|
160
|
-
* a file-scoped `test --build` can lack support fixtures for OTHER
|
|
161
|
-
* manifests even when nothing changed since — dispatching such a run
|
|
162
|
-
* hangs on missing fixtures rather than failing, so the flag (which only
|
|
163
|
-
* accepts stale CONTENT) must not be the trigger. Field incident: a
|
|
164
|
-
* three-file scoped rebuild left `file_tiles_audio.html` unpackaged and a
|
|
165
|
-
* later run over different files timed out twice at 45s waiting on
|
|
166
|
-
* `DOMAudioPlaybackStarted`.
|
|
167
|
-
*
|
|
168
|
-
* Exception: a path-less `test --doctor` stops at the Marionette health
|
|
169
|
-
* check (`runDoctorPreflight` returns 'stop' when no test paths were
|
|
170
|
-
* given) and never dispatches a test, so it needs no packaging coverage —
|
|
171
|
-
* treating it as a full-suite request would refuse a probe that touches
|
|
172
|
-
* no fixtures. The stale-content refusal still applies to it unchanged.
|
|
173
|
-
*/
|
|
174
|
-
async function enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths) {
|
|
175
|
-
const stale = await checkStaleBuildForTest(projectRoot, engineDir);
|
|
176
|
-
const dispatchesNoTests = options.doctor === true && normalizedPaths.length === 0;
|
|
177
|
-
if (!dispatchesNoTests) {
|
|
178
|
-
const recordedCoverage = stale.baseline?.testPackagingCoverage;
|
|
179
|
-
const uncovered = findUncoveredRequestPaths(recordedCoverage, normalizedPaths);
|
|
180
|
-
if (uncovered.length > 0) {
|
|
181
|
-
throw new GeneralError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : []));
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const staleMessage = stale.stale
|
|
185
|
-
? `${formatStaleBuildWarning(stale)}\n\n` +
|
|
186
|
-
'Run `fireforge test --build` to refresh the packaged runtime first, or pass ' +
|
|
187
|
-
'`--allow-stale-build` if you intentionally rebuilt out-of-band and accept the risk.'
|
|
188
|
-
: undefined;
|
|
189
|
-
if (staleMessage !== undefined) {
|
|
190
|
-
if (options.allowStaleBuild === true) {
|
|
191
|
-
warn(staleMessage);
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
throw new GeneralError(staleMessage);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
156
|
function logTestSelection(scopes) {
|
|
199
157
|
if (scopes.length > 0) {
|
|
200
158
|
const labels = scopes.map((scope) => scope.isDirectory && scope.testFileCount > 0
|
|
@@ -366,8 +324,15 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
366
324
|
// Run incremental build if requested
|
|
367
325
|
if (options.build) {
|
|
368
326
|
// A path-less `test --build` runs (and packages for) the full suite;
|
|
369
|
-
// a scoped invocation only vouches for the requested paths.
|
|
327
|
+
// a scoped invocation only vouches for the requested paths. A SCOPED
|
|
328
|
+
// rebuild also cannot regenerate the compiled StaticComponents table,
|
|
329
|
+
// so it runs the components.conf gate up-front (the path-less shape
|
|
330
|
+
// refreshes the anchor itself and skips it).
|
|
370
331
|
const coverage = normalizedPaths.length === 0 ? 'full' : normalizedPaths;
|
|
332
|
+
if (normalizedPaths.length > 0) {
|
|
333
|
+
const previousBaseline = await readBuildBaseline(projectRoot);
|
|
334
|
+
await enforceStaticComponentsGate(paths.engine, previousBaseline, options);
|
|
335
|
+
}
|
|
371
336
|
await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, coverage);
|
|
372
337
|
info('');
|
|
373
338
|
}
|
|
@@ -36,6 +36,18 @@ export interface AuditSummary {
|
|
|
36
36
|
* @returns True when the path implies packaging.
|
|
37
37
|
*/
|
|
38
38
|
export declare function isPackageablePath(sourcePath: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Decides whether a source path is an XPCOM static-component manifest —
|
|
41
|
+
* i.e. a file whose registrations are baked into the compiled
|
|
42
|
+
* StaticComponents table at FULL-build time rather than packaged into
|
|
43
|
+
* `dist/`. This basename check is the single extension point for that
|
|
44
|
+
* classification; parsing `moz.build` `XPCOM_MANIFESTS` entries to follow
|
|
45
|
+
* renamed manifests is out of scope.
|
|
46
|
+
*
|
|
47
|
+
* @param sourcePath Engine-relative POSIX path.
|
|
48
|
+
* @returns True when the path is a `components.conf` manifest.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isXpcomManifestPath(sourcePath: string): boolean;
|
|
39
51
|
/**
|
|
40
52
|
* Runs the post-build audit. Emits per-file warnings for missing or
|
|
41
53
|
* stale artifacts and a summary info line at the end. Always returns
|
|
@@ -111,6 +111,20 @@ export function isPackageablePath(sourcePath) {
|
|
|
111
111
|
}
|
|
112
112
|
return false;
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Decides whether a source path is an XPCOM static-component manifest —
|
|
116
|
+
* i.e. a file whose registrations are baked into the compiled
|
|
117
|
+
* StaticComponents table at FULL-build time rather than packaged into
|
|
118
|
+
* `dist/`. This basename check is the single extension point for that
|
|
119
|
+
* classification; parsing `moz.build` `XPCOM_MANIFESTS` entries to follow
|
|
120
|
+
* renamed manifests is out of scope.
|
|
121
|
+
*
|
|
122
|
+
* @param sourcePath Engine-relative POSIX path.
|
|
123
|
+
* @returns True when the path is a `components.conf` manifest.
|
|
124
|
+
*/
|
|
125
|
+
export function isXpcomManifestPath(sourcePath) {
|
|
126
|
+
return basename(sourcePath.replace(/\\/g, '/')) === 'components.conf';
|
|
127
|
+
}
|
|
114
128
|
/*
|
|
115
129
|
* Finds the unique obj-star directory with a dist subtree, or undefined
|
|
116
130
|
* when zero or multiple match. The ambiguous case is already rejected
|
|
@@ -55,8 +55,42 @@ export interface BuildBaseline {
|
|
|
55
55
|
* The record is project-scoped, which is also per-obj-dir: multi-objdir
|
|
56
56
|
* checkouts are refused up-front (`AmbiguousBuildArtifactsError`), so at
|
|
57
57
|
* most one obj dir exists per project.
|
|
58
|
+
*
|
|
59
|
+
* Union/"shared coverage" across successive scoped builds was evaluated
|
|
60
|
+
* and rejected as unsound — every baseline write refreshes
|
|
61
|
+
* `packageableFingerprints` for ALL dirty packageable paths, so a union
|
|
62
|
+
* would whitewash an earlier scope's edited fixtures while
|
|
63
|
+
* `obj-*`/`_tests/` still holds its stale staging; coverage therefore
|
|
64
|
+
* REPLACES.
|
|
58
65
|
*/
|
|
59
66
|
testPackagingCoverage?: TestPackagingCoverage;
|
|
67
|
+
/**
|
|
68
|
+
* Anchor for the compiled StaticComponents table: the engine HEAD SHA of
|
|
69
|
+
* the last FULL-coverage build plus content fingerprints of the
|
|
70
|
+
* `components.conf` manifests that were dirty at that moment. Written
|
|
71
|
+
* fresh only on FULL-coverage baseline writes (`fireforge build`,
|
|
72
|
+
* `build --ui`, path-less `test --build`); a scoped `test --build`
|
|
73
|
+
* carries the previous record forward verbatim, because `mach build
|
|
74
|
+
* faster` does not rebake `components.conf` registrations into the
|
|
75
|
+
* compiled table. Missing on pre-0.38.0 baselines — the
|
|
76
|
+
* static-components stale check degrades to "fresh" in that case.
|
|
77
|
+
*/
|
|
78
|
+
staticComponentsBaseline?: StaticComponentsBaseline;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* State of the engine at the last FULL build, as far as compiled-in XPCOM
|
|
82
|
+
* component registration is concerned. See
|
|
83
|
+
* {@link BuildBaseline.staticComponentsBaseline}.
|
|
84
|
+
*/
|
|
85
|
+
export interface StaticComponentsBaseline {
|
|
86
|
+
/** Engine HEAD SHA at the time of the last full build. */
|
|
87
|
+
engineHeadSha: string;
|
|
88
|
+
/**
|
|
89
|
+
* Hex-encoded SHA-256 per `components.conf` path that was dirty
|
|
90
|
+
* (modified-against-HEAD or untracked) when the full build ran. Keys are
|
|
91
|
+
* engine-relative POSIX paths.
|
|
92
|
+
*/
|
|
93
|
+
fingerprints: Record<string, string>;
|
|
60
94
|
}
|
|
61
95
|
/** Coverage claim of the packaged test runtime. See {@link BuildBaseline.testPackagingCoverage}. */
|
|
62
96
|
export type TestPackagingCoverage = 'full' | string[];
|
|
@@ -42,5 +42,10 @@ export declare function readBuildBaseline(projectRoot: string): Promise<BuildBas
|
|
|
42
42
|
* @param testPackagingCoverage - Coverage claim of the packaged test
|
|
43
43
|
* runtime this build produced (`'full'`, or the scoped request paths of
|
|
44
44
|
* a `test --build` invocation). Omitted → field left off the marker.
|
|
45
|
+
* @param previousBaseline - The baseline this write replaces, when the
|
|
46
|
+
* caller has it. A scoped (non-`'full'`) write carries its
|
|
47
|
+
* `staticComponentsBaseline` forward verbatim — `mach build faster`
|
|
48
|
+
* does not rebake `components.conf` registrations, so the last FULL
|
|
49
|
+
* build stays the honest anchor for the compiled StaticComponents table.
|
|
45
50
|
*/
|
|
46
|
-
export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage): Promise<void>;
|
|
51
|
+
export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage, previousBaseline?: BuildBaseline): Promise<void>;
|
|
@@ -22,7 +22,7 @@ import { join } from 'node:path';
|
|
|
22
22
|
import { toError } from '../utils/errors.js';
|
|
23
23
|
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
24
24
|
import { verbose } from '../utils/logger.js';
|
|
25
|
-
import { isPackageablePath } from './build-audit.js';
|
|
25
|
+
import { isPackageablePath, isXpcomManifestPath } from './build-audit.js';
|
|
26
26
|
import { FIREFORGE_DIR } from './config-paths.js';
|
|
27
27
|
import { getHead, hasChanges, isMissingHeadError } from './git.js';
|
|
28
28
|
import { git } from './git-base.js';
|
|
@@ -68,8 +68,13 @@ export async function readBuildBaseline(projectRoot) {
|
|
|
68
68
|
* @param testPackagingCoverage - Coverage claim of the packaged test
|
|
69
69
|
* runtime this build produced (`'full'`, or the scoped request paths of
|
|
70
70
|
* a `test --build` invocation). Omitted → field left off the marker.
|
|
71
|
+
* @param previousBaseline - The baseline this write replaces, when the
|
|
72
|
+
* caller has it. A scoped (non-`'full'`) write carries its
|
|
73
|
+
* `staticComponentsBaseline` forward verbatim — `mach build faster`
|
|
74
|
+
* does not rebake `components.conf` registrations, so the last FULL
|
|
75
|
+
* build stays the honest anchor for the compiled StaticComponents table.
|
|
71
76
|
*/
|
|
72
|
-
export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage) {
|
|
77
|
+
export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline) {
|
|
73
78
|
let engineHeadSha = '';
|
|
74
79
|
try {
|
|
75
80
|
engineHeadSha = await getHead(engineDir);
|
|
@@ -84,12 +89,20 @@ export async function writeBuildBaseline(projectRoot, engineDir, binaryName, tes
|
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
const packageableFingerprints = await collectPackageableFingerprints(engineDir);
|
|
92
|
+
// A full-coverage write (and the legacy no-claim `fireforge build` shape)
|
|
93
|
+
// just recompiled the StaticComponents table, so it anchors the table to
|
|
94
|
+
// the current engine state. A scoped write did not — carry the previous
|
|
95
|
+
// anchor forward verbatim.
|
|
96
|
+
const staticComponentsBaseline = testPackagingCoverage === undefined || testPackagingCoverage === 'full'
|
|
97
|
+
? await collectStaticComponentsBaseline(engineDir, engineHeadSha)
|
|
98
|
+
: previousBaseline?.staticComponentsBaseline;
|
|
87
99
|
const baseline = {
|
|
88
100
|
engineHeadSha,
|
|
89
101
|
builtAt: new Date().toISOString(),
|
|
90
102
|
binaryName,
|
|
91
103
|
...(packageableFingerprints !== undefined ? { packageableFingerprints } : {}),
|
|
92
104
|
...(testPackagingCoverage !== undefined ? { testPackagingCoverage } : {}),
|
|
105
|
+
...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
|
|
93
106
|
};
|
|
94
107
|
await writeJson(getBuildBaselinePath(projectRoot), baseline);
|
|
95
108
|
}
|
|
@@ -107,6 +120,33 @@ export async function writeBuildBaseline(projectRoot, engineDir, binaryName, tes
|
|
|
107
120
|
* back to the pre-0.16.0 "path-only" behavior on the next test run.
|
|
108
121
|
*/
|
|
109
122
|
async function collectPackageableFingerprints(engineDir) {
|
|
123
|
+
return collectDirtyFingerprints(engineDir, isPackageablePath, 'packageable fingerprint');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Anchor for the compiled StaticComponents table: the engine HEAD SHA of
|
|
127
|
+
* this (full) build plus content fingerprints of the `components.conf`
|
|
128
|
+
* manifests dirty right now — the same dirty-path probe family as
|
|
129
|
+
* {@link collectPackageableFingerprints}, filtered to XPCOM manifests.
|
|
130
|
+
* Returns `undefined` on probe failure so a broken probe omits the field
|
|
131
|
+
* (the static-components stale check then degrades to "fresh") rather than
|
|
132
|
+
* anchoring to a garbage record.
|
|
133
|
+
*/
|
|
134
|
+
async function collectStaticComponentsBaseline(engineDir, engineHeadSha) {
|
|
135
|
+
const fingerprints = await collectDirtyFingerprints(engineDir, isXpcomManifestPath, 'static-components fingerprint');
|
|
136
|
+
if (fingerprints === undefined) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
return { engineHeadSha, fingerprints };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Shared dirty-path fingerprint probe backing
|
|
143
|
+
* {@link collectPackageableFingerprints} and
|
|
144
|
+
* {@link collectStaticComponentsBaseline}: enumerates engine workdir
|
|
145
|
+
* modifications (tracked and untracked), keeps the paths `includePath`
|
|
146
|
+
* accepts, and hashes each. Returns `undefined` on any git failure so a
|
|
147
|
+
* broken probe never corrupts the on-disk baseline with `{}`.
|
|
148
|
+
*/
|
|
149
|
+
async function collectDirtyFingerprints(engineDir, includePath, contextLabel) {
|
|
110
150
|
try {
|
|
111
151
|
const dirtyPaths = new Set();
|
|
112
152
|
if (await hasChanges(engineDir)) {
|
|
@@ -120,12 +160,12 @@ async function collectPackageableFingerprints(engineDir) {
|
|
|
120
160
|
dirtyPaths.add(untracked);
|
|
121
161
|
}
|
|
122
162
|
}
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
163
|
+
const included = [...dirtyPaths].filter(includePath);
|
|
164
|
+
if (included.length === 0) {
|
|
125
165
|
return {};
|
|
126
166
|
}
|
|
127
167
|
const fingerprints = {};
|
|
128
|
-
for (const relPath of
|
|
168
|
+
for (const relPath of included) {
|
|
129
169
|
try {
|
|
130
170
|
const buffer = await readFile(join(engineDir, relPath));
|
|
131
171
|
fingerprints[relPath] = createHash('sha256').update(buffer).digest('hex');
|
|
@@ -134,13 +174,13 @@ async function collectPackageableFingerprints(engineDir) {
|
|
|
134
174
|
// A file that disappeared between status probe and hash is
|
|
135
175
|
// expected in concurrent scenarios; skip it without failing the
|
|
136
176
|
// whole baseline write.
|
|
137
|
-
verbose(`Build baseline: skipping
|
|
177
|
+
verbose(`Build baseline: skipping ${contextLabel} for ${relPath} — ${toError(fileError).message}`);
|
|
138
178
|
}
|
|
139
179
|
}
|
|
140
180
|
return fingerprints;
|
|
141
181
|
}
|
|
142
182
|
catch (error) {
|
|
143
|
-
verbose(`Build baseline:
|
|
183
|
+
verbose(`Build baseline: ${contextLabel} probe failed — ${toError(error).message}`);
|
|
144
184
|
return undefined;
|
|
145
185
|
}
|
|
146
186
|
}
|
|
@@ -11,7 +11,7 @@ export declare const STATE_FILENAME = "state.json";
|
|
|
11
11
|
/** Supported top-level fireforge.json keys backed by the current schema. */
|
|
12
12
|
export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "test", "externalToolchains", "license", "wire", "patchLint", "patchPolicy", "typecheck", "markerComment"];
|
|
13
13
|
/** Supported config paths that can be read or set without --force. */
|
|
14
|
-
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
|
|
14
|
+
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "firefox.candidate", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
|
|
15
15
|
/**
|
|
16
16
|
* Gets all project paths based on a root directory.
|
|
17
17
|
* @param root - Root directory of the project
|
|
@@ -6,7 +6,7 @@ import { ConfigError } from '../errors/config.js';
|
|
|
6
6
|
import { verbose } from '../utils/logger.js';
|
|
7
7
|
import { parseObject } from '../utils/parse.js';
|
|
8
8
|
import { isContainedRelativePath, isExplicitAbsolutePath } from '../utils/paths.js';
|
|
9
|
-
import { isValidAppId, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LICENSES, validateFirefoxProductVersionCompatibility, } from '../utils/validation.js';
|
|
9
|
+
import { isValidAppId, isValidFirefoxCandidate, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LICENSES, validateFirefoxProductVersionCompatibility, } from '../utils/validation.js';
|
|
10
10
|
import { SUPPORTED_CONFIG_ROOT_KEYS } from './config-paths.js';
|
|
11
11
|
import { parsePatchPolicyBlock } from './config-validate-patch-policy.js';
|
|
12
12
|
import { parseExternalToolchainsBlock, parseTestBlock } from './config-validate-test-toolchains.js';
|
|
@@ -81,10 +81,15 @@ function parseFirefoxBlock(rec) {
|
|
|
81
81
|
if (firefoxSha256 !== undefined && !/^[a-f0-9]{64}$/i.test(firefoxSha256)) {
|
|
82
82
|
throw new ConfigError('Config field "firefox.sha256" must be a 64-character SHA-256 hex digest');
|
|
83
83
|
}
|
|
84
|
+
const firefoxCandidate = optionalConfigString(firefoxRec, 'candidate', 'firefox.candidate');
|
|
85
|
+
if (firefoxCandidate !== undefined && !isValidFirefoxCandidate(firefoxCandidate)) {
|
|
86
|
+
throw new ConfigError('Config field "firefox.candidate" must look like "buildN" (e.g. "build2")');
|
|
87
|
+
}
|
|
84
88
|
return {
|
|
85
89
|
version: firefoxVersion,
|
|
86
90
|
product: firefoxProduct,
|
|
87
91
|
...(firefoxSha256 !== undefined ? { sha256: firefoxSha256.toLowerCase() } : {}),
|
|
92
|
+
...(firefoxCandidate !== undefined ? { candidate: firefoxCandidate } : {}),
|
|
88
93
|
};
|
|
89
94
|
}
|
|
90
95
|
/** Parses the optional `build` block (currently just `build.jobs`). */
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
export interface EngineSessionLockOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Wait up to this many seconds for the engine session lock instead of the
|
|
4
|
+
* legacy ~1 s fail-fast. Enables exponential poll backoff (100 ms → 2 s)
|
|
5
|
+
* and a holder-identified progress line roughly every 5 s. `undefined`
|
|
6
|
+
* preserves the historical fail-fast behavior exactly.
|
|
7
|
+
*/
|
|
8
|
+
waitLockSeconds?: number | undefined;
|
|
9
|
+
}
|
|
1
10
|
/**
|
|
2
11
|
*
|
|
3
12
|
*/
|
|
4
|
-
export declare function withEngineSessionLock<T>(projectRoot: string, command: string, operation: () => Promise<T
|
|
13
|
+
export declare function withEngineSessionLock<T>(projectRoot: string, command: string, operation: () => Promise<T>, options?: EngineSessionLockOptions): Promise<T>;
|
|
5
14
|
/**
|
|
6
15
|
*
|
|
7
16
|
*/
|
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { GeneralError } from '../errors/base.js';
|
|
4
|
+
import { info } from '../utils/logger.js';
|
|
4
5
|
import { withFileLock } from './file-lock.js';
|
|
5
6
|
import { git } from './git-base.js';
|
|
6
7
|
const ENGINE_SESSION_LOCK_PATH = join('.fireforge', 'engine-session.lock');
|
|
8
|
+
/**
|
|
9
|
+
* Formats the periodic operator-facing waiting line printed while `--wait-lock`
|
|
10
|
+
* polls a contended engine session lock. The holder identification comes from
|
|
11
|
+
* the lock's owner-metadata lines (`command=…`, `started=…`); an unreadable
|
|
12
|
+
* owner file degrades to the anonymous form.
|
|
13
|
+
*/
|
|
14
|
+
function formatWaitProgressLine(waitedMs, timeoutMs, holder) {
|
|
15
|
+
const progress = `${String(Math.round(waitedMs / 1000))}s of up to ${String(Math.round(timeoutMs / 1000))}s`;
|
|
16
|
+
if (holder === undefined) {
|
|
17
|
+
return `Waiting for the FireForge engine lock — ${progress}.`;
|
|
18
|
+
}
|
|
19
|
+
const details = holder.metadata.length > 0 ? ` (${holder.metadata.join(', ')})` : '';
|
|
20
|
+
return `Waiting for the FireForge engine lock held by PID ${String(holder.pid)}${details} — ${progress}.`;
|
|
21
|
+
}
|
|
7
22
|
/**
|
|
8
23
|
*
|
|
9
24
|
*/
|
|
10
|
-
export async function withEngineSessionLock(projectRoot, command, operation) {
|
|
25
|
+
export async function withEngineSessionLock(projectRoot, command, operation, options = {}) {
|
|
11
26
|
if (process.env['FIREFORGE_ENABLE_ENGINE_SESSION_LOCK_IN_TEST'] !== '1' &&
|
|
12
27
|
(process.env['NODE_ENV'] === 'test' || process.env['VITEST'] !== undefined)) {
|
|
13
28
|
return operation();
|
|
14
29
|
}
|
|
30
|
+
const { waitLockSeconds } = options;
|
|
15
31
|
return withFileLock(join(projectRoot, ENGINE_SESSION_LOCK_PATH), operation, {
|
|
16
|
-
timeoutMs: 1000,
|
|
32
|
+
timeoutMs: waitLockSeconds !== undefined ? waitLockSeconds * 1000 : 1000,
|
|
33
|
+
...(waitLockSeconds !== undefined
|
|
34
|
+
? {
|
|
35
|
+
pollMs: 100,
|
|
36
|
+
pollMaxMs: 2000,
|
|
37
|
+
waitProgressMs: 5000,
|
|
38
|
+
onWaitProgress: ({ waitedMs, timeoutMs, holder }) => {
|
|
39
|
+
info(formatWaitProgressLine(waitedMs, timeoutMs, holder));
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
: {}),
|
|
17
43
|
ownerMetadata: [`command=${command}`, `started=${new Date().toISOString()}`],
|
|
18
44
|
onTimeoutMessage: `Another FireForge engine-mutating command is already running. ` +
|
|
19
45
|
`Wait for it to finish, then retry \`${command}\`.`,
|
|
@@ -1,6 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot of the current lock holder handed to {@link FileLockOptions.onWaitProgress}.
|
|
3
|
+
* `metadata` carries the pid file's human-diagnostic lines (line 3 onward,
|
|
4
|
+
* e.g. `command=build`, `started=…`) written via
|
|
5
|
+
* {@link FileLockOptions.ownerMetadata}.
|
|
6
|
+
*/
|
|
7
|
+
export interface LockHolder {
|
|
8
|
+
pid: number;
|
|
9
|
+
alive: boolean;
|
|
10
|
+
metadata: string[];
|
|
11
|
+
}
|
|
1
12
|
export interface FileLockOptions {
|
|
2
13
|
timeoutMs?: number;
|
|
3
14
|
pollMs?: number;
|
|
15
|
+
/**
|
|
16
|
+
* When set, the poll interval doubles after every contended poll (starting
|
|
17
|
+
* from `pollMs`) up to this cap. When absent, polling stays at the fixed
|
|
18
|
+
* `pollMs` interval — zero behavior change for existing callers.
|
|
19
|
+
*/
|
|
20
|
+
pollMaxMs?: number;
|
|
4
21
|
staleMs?: number;
|
|
5
22
|
/**
|
|
6
23
|
* Interval between stale-lock probes while waiting (defaults to
|
|
@@ -8,6 +25,22 @@ export interface FileLockOptions {
|
|
|
8
25
|
* the mid-wait reaping path without multi-second sleeps.
|
|
9
26
|
*/
|
|
10
27
|
staleReprobeMs?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Interval between {@link onWaitProgress} callbacks while the lock is
|
|
30
|
+
* contended. Progress reporting is off unless both this and
|
|
31
|
+
* `onWaitProgress` are set.
|
|
32
|
+
*/
|
|
33
|
+
waitProgressMs?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Invoked roughly every `waitProgressMs` while waiting for a contended
|
|
36
|
+
* lock. `holder` is `undefined` when the owner PID file is missing or
|
|
37
|
+
* unreadable.
|
|
38
|
+
*/
|
|
39
|
+
onWaitProgress?: (progress: {
|
|
40
|
+
waitedMs: number;
|
|
41
|
+
timeoutMs: number;
|
|
42
|
+
holder: LockHolder | undefined;
|
|
43
|
+
}) => void;
|
|
11
44
|
onTimeoutMessage?: string;
|
|
12
45
|
onStaleLockMessage?: (ageMs: number) => string | undefined;
|
|
13
46
|
/** Extra owner-file lines for human diagnostics; ignored by lock mechanics. */
|
|
@@ -50,7 +50,8 @@ function isProcessAlive(pid) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* Reads the owner PID (line 1)
|
|
53
|
+
* Reads the owner PID (line 1), acquisition token (line 2), and diagnostic
|
|
54
|
+
* metadata (lines 3+, see {@link FileLockOptions.ownerMetadata}) from a lock
|
|
54
55
|
* directory's PID file. Returns `{ present: false }` when the PID file is
|
|
55
56
|
* missing or the PID does not parse as a finite integer (caller falls back
|
|
56
57
|
* to the age-only staleness heuristic).
|
|
@@ -62,14 +63,15 @@ function isProcessAlive(pid) {
|
|
|
62
63
|
async function readLockOwner(lockPath) {
|
|
63
64
|
try {
|
|
64
65
|
const pidContent = await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8');
|
|
65
|
-
const [pidLine, tokenLine] = pidContent.split('\n');
|
|
66
|
+
const [pidLine, tokenLine, ...metadataLines] = pidContent.split('\n');
|
|
66
67
|
const pid = parseInt((pidLine ?? '').trim(), 10);
|
|
67
68
|
if (Number.isFinite(pid)) {
|
|
69
|
+
const metadata = metadataLines.map((line) => line.trim()).filter((line) => line.length > 0);
|
|
68
70
|
const token = tokenLine?.trim();
|
|
69
71
|
if (token !== undefined && token.length > 0) {
|
|
70
|
-
return { present: true, pid, token };
|
|
72
|
+
return { present: true, pid, token, metadata };
|
|
71
73
|
}
|
|
72
|
-
return { present: true, pid };
|
|
74
|
+
return { present: true, pid, metadata };
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
catch {
|
|
@@ -77,6 +79,18 @@ async function readLockOwner(lockPath) {
|
|
|
77
79
|
}
|
|
78
80
|
return { present: false };
|
|
79
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Reads the contended lock's owner and reports wait progress to the caller's
|
|
84
|
+
* `onWaitProgress` callback. Failure to read the owner degrades to
|
|
85
|
+
* `holder: undefined` — progress reporting must never break the wait loop.
|
|
86
|
+
*/
|
|
87
|
+
async function reportWaitProgress(lockPath, waitedMs, timeoutMs, onWaitProgress) {
|
|
88
|
+
const owner = await readLockOwner(lockPath);
|
|
89
|
+
const holder = owner.present
|
|
90
|
+
? { pid: owner.pid, alive: isProcessAlive(owner.pid), metadata: owner.metadata }
|
|
91
|
+
: undefined;
|
|
92
|
+
onWaitProgress({ waitedMs, timeoutMs, holder });
|
|
93
|
+
}
|
|
80
94
|
/**
|
|
81
95
|
* Reaps a lock directory believed to be stale, safely against concurrent
|
|
82
96
|
* reapers and a concurrent legitimate re-acquisition.
|
|
@@ -216,8 +230,11 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
216
230
|
const pollMs = options.pollMs ?? DEFAULT_LOCK_POLL_MS;
|
|
217
231
|
const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
|
|
218
232
|
const staleReprobeMs = options.staleReprobeMs ?? STALE_REPROBE_INTERVAL_MS;
|
|
219
|
-
const
|
|
233
|
+
const startedAt = Date.now();
|
|
234
|
+
const deadline = startedAt + timeoutMs;
|
|
220
235
|
let lastStaleProbeAt;
|
|
236
|
+
let currentPollMs = pollMs;
|
|
237
|
+
let lastProgressAt = startedAt;
|
|
221
238
|
await ensureDir(dirname(lockPath));
|
|
222
239
|
for (;;) {
|
|
223
240
|
try {
|
|
@@ -246,7 +263,16 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
246
263
|
: ' If no other FireForge process is running, remove the lock directory and retry.';
|
|
247
264
|
throw new Error(options.onTimeoutMessage ?? `Timed out waiting for file lock ${lockPath}.${holderHint}`, { cause: error });
|
|
248
265
|
}
|
|
249
|
-
|
|
266
|
+
if (options.onWaitProgress !== undefined &&
|
|
267
|
+
options.waitProgressMs !== undefined &&
|
|
268
|
+
Date.now() - lastProgressAt >= options.waitProgressMs) {
|
|
269
|
+
lastProgressAt = Date.now();
|
|
270
|
+
await reportWaitProgress(lockPath, Date.now() - startedAt, timeoutMs, options.onWaitProgress);
|
|
271
|
+
}
|
|
272
|
+
await sleep(currentPollMs);
|
|
273
|
+
if (options.pollMaxMs !== undefined) {
|
|
274
|
+
currentPollMs = Math.min(currentPollMs * 2, options.pollMaxMs);
|
|
275
|
+
}
|
|
250
276
|
}
|
|
251
277
|
}
|
|
252
278
|
// Stamp ownership (PID + per-acquisition token) into the lock directory so
|
|
@@ -39,6 +39,7 @@ export declare function validateArchiveMetadata(data: unknown): ArchiveMetadata;
|
|
|
39
39
|
* Resolves archive identity for URL generation and cache storage.
|
|
40
40
|
* @param version - Requested Firefox version
|
|
41
41
|
* @param product - Firefox product type
|
|
42
|
+
* @param candidate - Optional release-candidate build directory (e.g. "build2")
|
|
42
43
|
* @returns Resolved archive descriptor
|
|
43
44
|
*/
|
|
44
|
-
export declare function resolveArchive(version: string, product?: FirefoxProduct): ResolvedArchive;
|
|
45
|
+
export declare function resolveArchive(version: string, product?: FirefoxProduct, candidate?: string): ResolvedArchive;
|
|
@@ -4,15 +4,26 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { ConfigError } from '../errors/config.js';
|
|
6
6
|
import { parseObject } from '../utils/parse.js';
|
|
7
|
-
import { isValidFirefoxProduct } from '../utils/validation.js';
|
|
7
|
+
import { isValidFirefoxCandidate, isValidFirefoxProduct } from '../utils/validation.js';
|
|
8
8
|
/**
|
|
9
9
|
* Base URLs for Firefox source archives on archive.mozilla.org.
|
|
10
10
|
*/
|
|
11
|
-
const FIREFOX_ARCHIVE_BASE_URL = 'https://archive.mozilla.org/pub/firefox
|
|
12
|
-
const DEVEDITION_ARCHIVE_BASE_URL = 'https://archive.mozilla.org/pub/devedition
|
|
11
|
+
const FIREFOX_ARCHIVE_BASE_URL = 'https://archive.mozilla.org/pub/firefox';
|
|
12
|
+
const DEVEDITION_ARCHIVE_BASE_URL = 'https://archive.mozilla.org/pub/devedition';
|
|
13
13
|
function getArchiveBaseUrl(product) {
|
|
14
14
|
return product === 'firefox-devedition' ? DEVEDITION_ARCHIVE_BASE_URL : FIREFOX_ARCHIVE_BASE_URL;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Directory holding the release's artifacts: `releases/<version>/` for
|
|
18
|
+
* final releases, `candidates/<version>-candidates/<buildN>/` when a
|
|
19
|
+
* release-candidate build is requested for pre-release verification.
|
|
20
|
+
*/
|
|
21
|
+
function getArchiveDirUrl(product, archiveVersion, candidate) {
|
|
22
|
+
const base = getArchiveBaseUrl(product);
|
|
23
|
+
return candidate === undefined
|
|
24
|
+
? `${base}/releases/${archiveVersion}`
|
|
25
|
+
: `${base}/candidates/${archiveVersion}-candidates/${candidate}`;
|
|
26
|
+
}
|
|
16
27
|
/**
|
|
17
28
|
* Validates raw JSON data as ArchiveMetadata.
|
|
18
29
|
* @param data - Unknown data to validate
|
|
@@ -41,13 +52,20 @@ export function validateArchiveMetadata(data) {
|
|
|
41
52
|
* Resolves archive identity for URL generation and cache storage.
|
|
42
53
|
* @param version - Requested Firefox version
|
|
43
54
|
* @param product - Firefox product type
|
|
55
|
+
* @param candidate - Optional release-candidate build directory (e.g. "build2")
|
|
44
56
|
* @returns Resolved archive descriptor
|
|
45
57
|
*/
|
|
46
|
-
export function resolveArchive(version, product = 'firefox') {
|
|
58
|
+
export function resolveArchive(version, product = 'firefox', candidate) {
|
|
47
59
|
// Reject versions containing path traversal characters
|
|
48
60
|
if (version.includes('/') || version.includes('..') || version.includes('\\')) {
|
|
49
61
|
throw new ConfigError(`Invalid Firefox version "${version}": contains disallowed characters`, 'firefox.version');
|
|
50
62
|
}
|
|
63
|
+
// Defense-in-depth: config validation enforces this shape already, but a
|
|
64
|
+
// caller-supplied candidate becomes both a URL path segment and part of
|
|
65
|
+
// the cache filename, so re-validate here like the version check above.
|
|
66
|
+
if (candidate !== undefined && !isValidFirefoxCandidate(candidate)) {
|
|
67
|
+
throw new ConfigError(`Invalid Firefox candidate "${candidate}": must look like "buildN" (e.g. "build2")`, 'firefox.candidate');
|
|
68
|
+
}
|
|
51
69
|
// ESR status is determined solely by the product field. Config validation
|
|
52
70
|
// ensures product and version are consistent, so we never need to infer
|
|
53
71
|
// ESR from the version string independently.
|
|
@@ -55,18 +73,24 @@ export function resolveArchive(version, product = 'firefox') {
|
|
|
55
73
|
const isEsr = product === 'firefox-esr';
|
|
56
74
|
const archiveVersion = isEsr ? `${cleanVersion}esr` : cleanVersion;
|
|
57
75
|
const safeProduct = product.replace(/[^a-z0-9-]/gi, '-');
|
|
76
|
+
const dirUrl = getArchiveDirUrl(product, archiveVersion, candidate);
|
|
77
|
+
// Candidate artifacts get a `-<candidate>` filename suffix so a build2
|
|
78
|
+
// archive can never collide with the final release artifact (same
|
|
79
|
+
// archiveVersion, potentially different bytes) in .fireforge/cache.
|
|
80
|
+
const candidateSuffix = candidate === undefined ? '' : `-${candidate}`;
|
|
81
|
+
const filename = `firefox-${safeProduct}-${archiveVersion}${candidateSuffix}.source.tar.xz`;
|
|
58
82
|
return {
|
|
59
83
|
requestedVersion: version,
|
|
60
84
|
product,
|
|
61
85
|
archiveVersion,
|
|
62
|
-
url: `${
|
|
63
|
-
filename
|
|
64
|
-
metadataFilename:
|
|
86
|
+
url: `${dirUrl}/source/firefox-${archiveVersion}.source.tar.xz`,
|
|
87
|
+
filename,
|
|
88
|
+
metadataFilename: `${filename}.json`,
|
|
65
89
|
// Mozilla publishes one SHA256SUMS per release directory listing every
|
|
66
90
|
// artifact by its release-relative path. Downloads verify against it by
|
|
67
91
|
// default (fail closed on mismatch), so a compromised CDN response
|
|
68
92
|
// cannot silently become the trusted git baseline.
|
|
69
|
-
checksumsUrl: `${
|
|
93
|
+
checksumsUrl: `${dirUrl}/SHA256SUMS`,
|
|
70
94
|
pathInChecksums: `source/firefox-${archiveVersion}.source.tar.xz`,
|
|
71
95
|
};
|
|
72
96
|
}
|