@hominis/fireforge 0.50.0 → 0.52.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 +67 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/check-report.d.ts +17 -4
- package/dist/src/commands/check-report.js +19 -6
- package/dist/src/commands/check-run.d.ts +5 -0
- package/dist/src/commands/check-run.js +9 -1
- package/dist/src/commands/check-steps.d.ts +8 -0
- package/dist/src/commands/check-steps.js +31 -6
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/furnace/rename-registrations.js +7 -2
- package/dist/src/commands/furnace/validate.js +26 -1
- package/dist/src/commands/lint-per-patch.js +6 -2
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.js +7 -2
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete.js +11 -6
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +8 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.js +39 -29
- package/dist/src/commands/patch/move-files-create.js +11 -4
- package/dist/src/commands/patch/move-files-into.js +10 -4
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/rename.js +15 -3
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.js +11 -6
- package/dist/src/commands/patch/split-plan.d.ts +10 -1
- package/dist/src/commands/patch/split-plan.js +16 -1
- package/dist/src/commands/patch/split.js +14 -5
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +40 -98
- package/dist/src/commands/re-export-adjacent.d.ts +5 -0
- package/dist/src/commands/re-export-adjacent.js +4 -3
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-options.js +29 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +15 -1
- package/dist/src/commands/re-export-removed.d.ts +1 -1
- package/dist/src/commands/re-export-removed.js +17 -2
- package/dist/src/commands/re-export-scan.d.ts +2 -0
- package/dist/src/commands/re-export-scan.js +4 -1
- package/dist/src/commands/re-export-single.js +102 -42
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export.js +35 -34
- package/dist/src/commands/status-output.js +5 -1
- package/dist/src/commands/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.d.ts +2 -0
- package/dist/src/commands/test-build-phase.js +34 -17
- package/dist/src/commands/test-checkjs.js +3 -2
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +220 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-orchestrate.d.ts +14 -0
- package/dist/src/commands/test-orchestrate.js +25 -4
- package/dist/src/commands/test-perf-scope.d.ts +21 -0
- package/dist/src/commands/test-perf-scope.js +55 -0
- package/dist/src/commands/test-prelock.js +100 -4
- package/dist/src/commands/test-register.js +18 -3
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +73 -101
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +11 -2
- package/dist/src/commands/test-verdict.d.ts +2 -1
- package/dist/src/commands/test.d.ts +1 -1
- package/dist/src/commands/test.js +68 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-baseline-types.d.ts +13 -0
- package/dist/src/core/build-baseline-validate.js +9 -0
- package/dist/src/core/build-baseline.js +7 -0
- package/dist/src/core/build-prepare.d.ts +6 -0
- package/dist/src/core/build-prepare.js +29 -3
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-check.js +83 -7
- package/dist/src/core/config-validate-test-toolchains.js +14 -0
- package/dist/src/core/config-validate-token-scales.d.ts +3 -0
- package/dist/src/core/config-validate-token-scales.js +47 -5
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/coverage-extend.js +2 -2
- package/dist/src/core/coverage-records.d.ts +11 -0
- package/dist/src/core/coverage-records.js +34 -0
- package/dist/src/core/engine-session-lock.d.ts +6 -0
- package/dist/src/core/engine-session-lock.js +10 -2
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +9 -0
- package/dist/src/core/file-lock.js +58 -2
- package/dist/src/core/furnace-apply.js +10 -10
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +11 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +59 -8
- package/dist/src/core/furnace-config-validate.js +7 -1
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +5 -3
- package/dist/src/core/furnace-validate-accessibility.js +75 -30
- package/dist/src/core/furnace-validate-exceptions.d.ts +13 -4
- package/dist/src/core/furnace-validate-exceptions.js +59 -4
- package/dist/src/core/furnace-validate-icon-button.js +30 -1
- package/dist/src/core/history-log-file.js +8 -1
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +24 -9
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +36 -0
- package/dist/src/core/negative-control-compiled.js +87 -0
- package/dist/src/core/negative-control-diff.d.ts +14 -2
- package/dist/src/core/negative-control-diff.js +49 -6
- package/dist/src/core/negative-control.d.ts +12 -0
- package/dist/src/core/negative-control.js +48 -5
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +4 -0
- package/dist/src/core/patch-export-update.js +12 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +21 -14
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
- package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +13 -3
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +5 -0
- package/dist/src/core/patch-lint-css.js +14 -10
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
- package/dist/src/core/patch-lint-ignore.d.ts +56 -12
- package/dist/src/core/patch-lint-ignore.js +173 -43
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size.js +9 -1
- package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
- package/dist/src/core/patch-lint-token-scale.js +206 -29
- package/dist/src/core/patch-lint.js +2 -2
- package/dist/src/core/patch-manifest-io.js +8 -1
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +27 -1
- package/dist/src/core/patch-order-legal.d.ts +2 -1
- package/dist/src/core/patch-order-legal.js +7 -2
- package/dist/src/core/patch-policy.d.ts +6 -2
- package/dist/src/core/patch-policy.js +14 -7
- package/dist/src/core/patch-retired.d.ts +26 -4
- package/dist/src/core/patch-retired.js +40 -2
- package/dist/src/core/run-log.d.ts +6 -0
- package/dist/src/core/run-log.js +17 -1
- package/dist/src/core/test-harness-crash.d.ts +2 -1
- package/dist/src/core/test-harness-crash.js +5 -8
- package/dist/src/core/test-harness-signature.d.ts +16 -0
- package/dist/src/core/test-harness-signature.js +19 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +47 -7
- package/dist/src/core/test-run-completeness.js +70 -23
- package/dist/src/core/test-stale-check.d.ts +10 -1
- package/dist/src/core/test-stale-check.js +15 -1
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +5 -2
- package/dist/src/core/token-lint.js +8 -3
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +2 -0
- package/dist/src/core/typecheck-shim.d.ts +6 -1
- package/dist/src/core/typecheck-shim.js +139 -2
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +9 -3
- package/dist/src/index.js +4 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +68 -7
- package/dist/src/types/commands/patches.d.ts +43 -0
- package/dist/src/types/config.d.ts +74 -2
- package/dist/src/types/furnace.d.ts +29 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +3 -2
- package/dist/src/utils/options.js +9 -4
- package/docs/configuration.md +110 -9
- package/docs/furnace.md +61 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +68 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +89 -5
- package/package.json +3 -3
|
@@ -21,7 +21,7 @@ import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
|
21
21
|
import { isGitRepository } from '../core/git.js';
|
|
22
22
|
import { withPrivateGitIndex } from '../core/git-readonly-index.js';
|
|
23
23
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
24
|
-
import { findUnresolvedSystemModuleImports, formatUnresolvedSystemModuleImports, } from '../core/module-resolution-preflight.js';
|
|
24
|
+
import { collectWorktreeModuleFiles, findUnresolvedSystemModuleImports, formatUnresolvedSystemModuleImports, } from '../core/module-resolution-preflight.js';
|
|
25
25
|
import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../core/patch-lint.js';
|
|
26
26
|
import { loadPatchesManifest, validatePatchesManifestConsistency } from '../core/patch-manifest.js';
|
|
27
27
|
import { evaluatePatchPolicy } from '../core/patch-policy.js';
|
|
@@ -206,8 +206,15 @@ export async function collectPatchQueueHealth(projectRoot, scope) {
|
|
|
206
206
|
// CREATES. This asks the same question of the engine, so an import ADDED
|
|
207
207
|
// to an existing module is named as text instead of surfacing later as a
|
|
208
208
|
// bare `xpcshell return code: -11` with zero output.
|
|
209
|
+
// Unscoped, the engine worktree joins in: an unexported chrome script
|
|
210
|
+
// importing an unexported, unregistered module crashes every objdir
|
|
211
|
+
// start before either file is in the queue. A scoped run never judges
|
|
212
|
+
// unmanaged files.
|
|
209
213
|
if (await pathExists(paths.engine)) {
|
|
210
|
-
const
|
|
214
|
+
const worktree = scope === undefined && (await isGitRepository(paths.engine))
|
|
215
|
+
? await collectWorktreeModuleFiles(paths.engine)
|
|
216
|
+
: undefined;
|
|
217
|
+
const unresolved = await findUnresolvedSystemModuleImports(paths.engine, scopePatches(manifest.patches, scope).flatMap((patch) => patch.filesAffected), worktree);
|
|
211
218
|
if (unresolved.length > 0) {
|
|
212
219
|
groups.push({
|
|
213
220
|
title: `Unresolvable system-module imports (${unresolved.length})`,
|
|
@@ -144,6 +144,19 @@ export interface BuildBaseline {
|
|
|
144
144
|
* as "unknown" when absent.
|
|
145
145
|
*/
|
|
146
146
|
recordedBy?: string;
|
|
147
|
+
/**
|
|
148
|
+
* The process that wrote this baseline: its pid, its invocation id (the
|
|
149
|
+
* one its history records carry) and the caller's `FIREFORGE_SESSION_ID`
|
|
150
|
+
* when set. A coverage refusal names it, so a run whose claim another
|
|
151
|
+
* session's scoped build replaced can tell whose build that was.
|
|
152
|
+
*/
|
|
153
|
+
recorder?: BaselineRecorder;
|
|
154
|
+
}
|
|
155
|
+
/** See {@link BuildBaseline.recorder}. */
|
|
156
|
+
export interface BaselineRecorder {
|
|
157
|
+
pid: number;
|
|
158
|
+
invocationId: string;
|
|
159
|
+
sessionId?: string;
|
|
147
160
|
}
|
|
148
161
|
/**
|
|
149
162
|
* State of the engine at the last full build, as far as compiled-in XPCOM
|
|
@@ -32,6 +32,13 @@ function isCoverageRecordMap(value) {
|
|
|
32
32
|
Array.isArray(record['inputs']) &&
|
|
33
33
|
record['inputs'].every(isBaselinePath)));
|
|
34
34
|
}
|
|
35
|
+
/** A `recorder` stamp: an integer pid, an invocation id and an optional session id. */
|
|
36
|
+
function isBaselineRecorder(value) {
|
|
37
|
+
return (isObject(value) &&
|
|
38
|
+
Number.isInteger(value['pid']) &&
|
|
39
|
+
typeof value['invocationId'] === 'string' &&
|
|
40
|
+
(value['sessionId'] === undefined || typeof value['sessionId'] === 'string'));
|
|
41
|
+
}
|
|
35
42
|
/** Validates persisted fields without converting malformed coverage into a full claim. */
|
|
36
43
|
export function isBuildBaseline(value) {
|
|
37
44
|
if (!isObject(value))
|
|
@@ -59,6 +66,8 @@ export function isBuildBaseline(value) {
|
|
|
59
66
|
return false;
|
|
60
67
|
if (value['recordedBy'] !== undefined && typeof value['recordedBy'] !== 'string')
|
|
61
68
|
return false;
|
|
69
|
+
if (value['recorder'] !== undefined && !isBaselineRecorder(value['recorder']))
|
|
70
|
+
return false;
|
|
62
71
|
if (value['testCoverageRecords'] !== undefined &&
|
|
63
72
|
!isCoverageRecordMap(value['testCoverageRecords']))
|
|
64
73
|
return false;
|
|
@@ -32,6 +32,7 @@ import { hashEngineFile } from './coverage-extend.js';
|
|
|
32
32
|
import { getHead, hasChanges, isMissingHeadError } from './git.js';
|
|
33
33
|
import { git } from './git-base.js';
|
|
34
34
|
import { getUntrackedFiles } from './git-status.js';
|
|
35
|
+
import { historyRecordIdentity } from './history-log-file.js';
|
|
35
36
|
/*
|
|
36
37
|
* Keep the IO fan-out bounded: a patched Firefox checkout can carry hundreds
|
|
37
38
|
* of dirty packageable paths and serial reads add directly to every build.
|
|
@@ -72,6 +73,11 @@ export async function readBuildBaseline(projectRoot, invalid = 'ignore') {
|
|
|
72
73
|
return undefined;
|
|
73
74
|
}
|
|
74
75
|
}
|
|
76
|
+
/** This process's {@link BuildBaseline.recorder} stamp. */
|
|
77
|
+
function baselineRecorder() {
|
|
78
|
+
const { pid, invocationId, sessionId } = historyRecordIdentity();
|
|
79
|
+
return { pid, invocationId, ...(sessionId !== undefined ? { sessionId } : {}) };
|
|
80
|
+
}
|
|
75
81
|
/**
|
|
76
82
|
* Records a successful build by writing a fresh baseline marker. Captures
|
|
77
83
|
* engine HEAD SHA (or an empty string when the engine has no HEAD yet) and
|
|
@@ -121,6 +127,7 @@ export async function writeBuildBaseline(options) {
|
|
|
121
127
|
...(mozconfigHash !== undefined ? { mozconfigHash } : {}),
|
|
122
128
|
...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
|
|
123
129
|
...(recordedBy !== undefined ? { recordedBy } : {}),
|
|
130
|
+
recorder: baselineRecorder(),
|
|
124
131
|
};
|
|
125
132
|
await writeJson(getBuildBaselinePath(projectRoot), baseline);
|
|
126
133
|
}
|
|
@@ -42,6 +42,12 @@ export interface PrepareBuildOptions {
|
|
|
42
42
|
* (`fireforge test --build --no-furnace-apply`).
|
|
43
43
|
*/
|
|
44
44
|
skipFurnaceApply?: boolean | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Apply only these components (`fireforge test --build --furnace-apply
|
|
47
|
+
* reached`). Undefined applies every component with undeployed edits; an
|
|
48
|
+
* empty list applies none.
|
|
49
|
+
*/
|
|
50
|
+
furnaceComponents?: readonly string[] | undefined;
|
|
45
51
|
}
|
|
46
52
|
/**
|
|
47
53
|
* Describes an exit code in the shell's 128+N signal convention. Truncated
|
|
@@ -170,14 +170,35 @@ async function reportUnexportedDriftBeforeOverwrite(projectRoot, config, options
|
|
|
170
170
|
}
|
|
171
171
|
notice(message);
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Applies the named components one at a time, each under its own Furnace
|
|
175
|
+
* mutation (one rollback journal per apply), and merges the results.
|
|
176
|
+
*/
|
|
177
|
+
async function applySelectedComponents(projectRoot, names) {
|
|
178
|
+
const merged = {
|
|
179
|
+
applied: [],
|
|
180
|
+
skipped: [],
|
|
181
|
+
errors: [],
|
|
182
|
+
warnings: [],
|
|
183
|
+
};
|
|
184
|
+
for (const componentName of names) {
|
|
185
|
+
const result = await runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) => applyAllComponents(projectRoot, false, { operationContext: ctx, componentName }));
|
|
186
|
+
merged.applied.push(...result.applied);
|
|
187
|
+
merged.skipped.push(...result.skipped);
|
|
188
|
+
merged.errors.push(...result.errors);
|
|
189
|
+
merged.warnings.push(...(result.warnings ?? []));
|
|
190
|
+
}
|
|
191
|
+
return merged;
|
|
192
|
+
}
|
|
173
193
|
/**
|
|
174
194
|
* Applies Furnace components before the build when `furnace.json` exists.
|
|
175
195
|
* Prints every warning the apply computed, then the loud sync banner.
|
|
176
196
|
*
|
|
177
197
|
* @param projectRoot - Project root
|
|
198
|
+
* @param only - Apply just these components; every component when undefined
|
|
178
199
|
* @returns Names of the components written to engine/
|
|
179
200
|
*/
|
|
180
|
-
async function applyFurnaceBeforeBuild(projectRoot) {
|
|
201
|
+
async function applyFurnaceBeforeBuild(projectRoot, only) {
|
|
181
202
|
if (!(await furnaceConfigExists(projectRoot)))
|
|
182
203
|
return [];
|
|
183
204
|
const furnaceConfig = await loadFurnaceConfig(projectRoot);
|
|
@@ -187,7 +208,10 @@ async function applyFurnaceBeforeBuild(projectRoot) {
|
|
|
187
208
|
const furnaceSpinner = spinner('Applying Furnace components...');
|
|
188
209
|
let result;
|
|
189
210
|
try {
|
|
190
|
-
result =
|
|
211
|
+
result =
|
|
212
|
+
only === undefined
|
|
213
|
+
? await runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) => applyAllComponents(projectRoot, false, { operationContext: ctx }))
|
|
214
|
+
: await applySelectedComponents(projectRoot, only);
|
|
191
215
|
}
|
|
192
216
|
catch (error) {
|
|
193
217
|
furnaceSpinner.error('Failed to apply Furnace components');
|
|
@@ -349,7 +373,9 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
349
373
|
throw error;
|
|
350
374
|
}
|
|
351
375
|
}
|
|
352
|
-
const furnaceAppliedNames = options.skipFurnaceApply === true
|
|
376
|
+
const furnaceAppliedNames = options.skipFurnaceApply === true || options.furnaceComponents?.length === 0
|
|
377
|
+
? []
|
|
378
|
+
: await applyFurnaceBeforeBuild(projectRoot, options.furnaceComponents);
|
|
353
379
|
// Generate mozconfig
|
|
354
380
|
const mozconfigSpinner = spinner('Generating mozconfig...');
|
|
355
381
|
let mozconfigWritten;
|
|
@@ -7,7 +7,7 @@ export declare const FIREFORGE_DIR = ".fireforge";
|
|
|
7
7
|
/** Supported top-level fireforge.json keys backed by the current schema. */
|
|
8
8
|
export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "buildAudit", "test", "history", "externalToolchains", "license", "wire", "patchLint", "patchPolicy", "typecheck", "markerComment", "check"];
|
|
9
9
|
/** Supported config paths that can be read or set without --force. */
|
|
10
|
-
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "firefox.candidate", "firefox.allowUnverifiedDownload", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "test.reapOrphans", "test.mochitestPorts", "history", "history.path", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.checkJsTestFiles", "patchLint.checkJsTestShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchLint.forwardRegistration", "patchLint.fileSizeMetric", "patchLint.patchSizeMetric", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment", "check"];
|
|
10
|
+
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "firefox.candidate", "firefox.allowUnverifiedDownload", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "test.reapOrphans", "test.mochitestPorts", "test.lockMode", "test.furnaceApply", "history", "history.path", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.checkJsTestFiles", "patchLint.checkJsTestShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchLint.forwardRegistration", "patchLint.fileSizeMetric", "patchLint.patchSizeMetric", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment", "check"];
|
|
11
11
|
/**
|
|
12
12
|
* Gets all project paths based on a root directory.
|
|
13
13
|
* @param root - Root directory of the project
|
|
@@ -78,7 +78,7 @@ function parseExtraStep(raw, field) {
|
|
|
78
78
|
if (!isObject(raw)) {
|
|
79
79
|
throw new ConfigError(`Config field "${field}" must be a plain object with "name" and "command"`);
|
|
80
80
|
}
|
|
81
|
-
assertKnownKeys(raw, field, ['name', 'command', 'cwd', 'timeout']);
|
|
81
|
+
assertKnownKeys(raw, field, ['name', 'command', 'cwd', 'timeout', 'after']);
|
|
82
82
|
const name = raw['name'];
|
|
83
83
|
if (typeof name !== 'string' || !STEP_NAME_PATTERN.test(name)) {
|
|
84
84
|
throw new ConfigError(`Config field "${field}.name" must be lowercase letters, digits and dashes (e.g. "policy-audit")`);
|
|
@@ -99,14 +99,63 @@ function parseExtraStep(raw, field) {
|
|
|
99
99
|
step.cwd = cwd;
|
|
100
100
|
}
|
|
101
101
|
if (raw['timeout'] !== undefined) {
|
|
102
|
-
|
|
103
|
-
if (timeout === 0) {
|
|
104
|
-
throw new ConfigError(`Config field "${field}.timeout" must be a positive number of seconds`);
|
|
105
|
-
}
|
|
106
|
-
step.timeout = timeout;
|
|
102
|
+
step.timeout = parsePositiveSeconds(raw['timeout'], `${field}.timeout`);
|
|
107
103
|
}
|
|
104
|
+
if (raw['after'] !== undefined)
|
|
105
|
+
step.after = parseStringList(raw['after'], `${field}.after`);
|
|
108
106
|
return step;
|
|
109
107
|
}
|
|
108
|
+
function parsePositiveSeconds(raw, field) {
|
|
109
|
+
const seconds = parseNonNegativeInteger(raw, field);
|
|
110
|
+
if (seconds === 0) {
|
|
111
|
+
throw new ConfigError(`Config field "${field}" must be a positive number of seconds`);
|
|
112
|
+
}
|
|
113
|
+
return seconds;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Checks every `after` names a known step and the extra steps' ordering
|
|
117
|
+
* has no cycle: a cycle would leave its steps waiting forever.
|
|
118
|
+
*/
|
|
119
|
+
function assertExtraStepOrdering(steps) {
|
|
120
|
+
const extras = new Map(steps.map((step) => [step.name, step]));
|
|
121
|
+
for (const [index, step] of steps.entries()) {
|
|
122
|
+
for (const dependency of step.after ?? []) {
|
|
123
|
+
if (dependency === step.name) {
|
|
124
|
+
throw new ConfigError(`Config field "check.extraSteps[${String(index)}].after" names the step itself`);
|
|
125
|
+
}
|
|
126
|
+
if (!isBuiltinStep(dependency) && !extras.has(dependency)) {
|
|
127
|
+
throw new ConfigError(`Config field "check.extraSteps[${String(index)}].after" names unknown step "${dependency}"`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const state = new Map();
|
|
132
|
+
const visit = (name, path) => {
|
|
133
|
+
if (state.get(name) === 'done')
|
|
134
|
+
return;
|
|
135
|
+
if (state.get(name) === 'visiting') {
|
|
136
|
+
throw new ConfigError(`Config field "check.extraSteps" orders its steps in a cycle: ${[...path, name].join(' -> ')}`);
|
|
137
|
+
}
|
|
138
|
+
state.set(name, 'visiting');
|
|
139
|
+
for (const dependency of extras.get(name)?.after ?? []) {
|
|
140
|
+
if (extras.has(dependency))
|
|
141
|
+
visit(dependency, [...path, name]);
|
|
142
|
+
}
|
|
143
|
+
state.set(name, 'done');
|
|
144
|
+
};
|
|
145
|
+
for (const step of steps)
|
|
146
|
+
visit(step.name, []);
|
|
147
|
+
}
|
|
148
|
+
function parseTimeouts(raw) {
|
|
149
|
+
if (!isObject(raw))
|
|
150
|
+
throw new ConfigError('Config field "check.timeouts" must be a plain object');
|
|
151
|
+
assertKnownKeys(raw, 'check.timeouts', [...CHECK_BUILTIN_STEPS]);
|
|
152
|
+
const out = {};
|
|
153
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
154
|
+
if (isBuiltinStep(name))
|
|
155
|
+
out[name] = parsePositiveSeconds(value, `check.timeouts.${name}`);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
110
159
|
function parseExtraSteps(raw) {
|
|
111
160
|
if (!Array.isArray(raw))
|
|
112
161
|
throw new ConfigError('Config field "check.extraSteps" must be an array');
|
|
@@ -118,6 +167,7 @@ function parseExtraSteps(raw) {
|
|
|
118
167
|
}
|
|
119
168
|
seen.add(step.name);
|
|
120
169
|
}
|
|
170
|
+
assertExtraStepOrdering(steps);
|
|
121
171
|
return steps;
|
|
122
172
|
}
|
|
123
173
|
/**
|
|
@@ -129,7 +179,16 @@ function parseExtraSteps(raw) {
|
|
|
129
179
|
export function parseCheckConfig(raw) {
|
|
130
180
|
if (!isObject(raw))
|
|
131
181
|
throw new ConfigError('Config field "check" must be a plain object');
|
|
132
|
-
assertKnownKeys(raw, 'check', [
|
|
182
|
+
assertKnownKeys(raw, 'check', [
|
|
183
|
+
'skip',
|
|
184
|
+
'status',
|
|
185
|
+
'maxWarnings',
|
|
186
|
+
'extraSteps',
|
|
187
|
+
'timeout',
|
|
188
|
+
'timeouts',
|
|
189
|
+
'failOnSkipped',
|
|
190
|
+
'lintReport',
|
|
191
|
+
]);
|
|
133
192
|
const out = {};
|
|
134
193
|
if (raw['skip'] !== undefined)
|
|
135
194
|
out.skip = parseSkip(raw['skip']);
|
|
@@ -139,5 +198,22 @@ export function parseCheckConfig(raw) {
|
|
|
139
198
|
out.maxWarnings = parseMaxWarnings(raw['maxWarnings']);
|
|
140
199
|
if (raw['extraSteps'] !== undefined)
|
|
141
200
|
out.extraSteps = parseExtraSteps(raw['extraSteps']);
|
|
201
|
+
if (raw['timeout'] !== undefined)
|
|
202
|
+
out.timeout = parsePositiveSeconds(raw['timeout'], 'check.timeout');
|
|
203
|
+
if (raw['timeouts'] !== undefined)
|
|
204
|
+
out.timeouts = parseTimeouts(raw['timeouts']);
|
|
205
|
+
if (raw['failOnSkipped'] !== undefined) {
|
|
206
|
+
if (typeof raw['failOnSkipped'] !== 'boolean') {
|
|
207
|
+
throw new ConfigError('Config field "check.failOnSkipped" must be a boolean');
|
|
208
|
+
}
|
|
209
|
+
out.failOnSkipped = raw['failOnSkipped'];
|
|
210
|
+
}
|
|
211
|
+
if (raw['lintReport'] !== undefined) {
|
|
212
|
+
const path = raw['lintReport'];
|
|
213
|
+
if (typeof path !== 'string' || path.trim() === '' || !isContainedRelativePath(path)) {
|
|
214
|
+
throw new ConfigError('Config field "check.lintReport" must be a project-relative path');
|
|
215
|
+
}
|
|
216
|
+
out.lintReport = path;
|
|
217
|
+
}
|
|
142
218
|
return out;
|
|
143
219
|
}
|
|
@@ -59,6 +59,20 @@ export function parseTestBlock(rec, config) {
|
|
|
59
59
|
}
|
|
60
60
|
out.mochitestPorts = mochitestPorts;
|
|
61
61
|
}
|
|
62
|
+
const lockMode = testRec.raw('lockMode');
|
|
63
|
+
if (lockMode !== undefined) {
|
|
64
|
+
if (lockMode !== 'exclusive' && lockMode !== 'shared') {
|
|
65
|
+
throw new ConfigError('Config field "test.lockMode" must be "exclusive" or "shared"');
|
|
66
|
+
}
|
|
67
|
+
out.lockMode = lockMode;
|
|
68
|
+
}
|
|
69
|
+
const furnaceApply = testRec.raw('furnaceApply');
|
|
70
|
+
if (furnaceApply !== undefined) {
|
|
71
|
+
if (furnaceApply !== 'all' && furnaceApply !== 'reached' && furnaceApply !== 'none') {
|
|
72
|
+
throw new ConfigError('Config field "test.furnaceApply" must be "all", "reached" or "none"');
|
|
73
|
+
}
|
|
74
|
+
out.furnaceApply = furnaceApply;
|
|
75
|
+
}
|
|
62
76
|
config.test = out;
|
|
63
77
|
}
|
|
64
78
|
/** Parses optional project-specific external toolchain doctor probes. */
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validation for `fireforge.json#patchLint.tokenScales`.
|
|
3
|
+
*
|
|
4
|
+
* Unknown keys are refused: a misspelt `allowInCalc` or `minFiles` would
|
|
5
|
+
* otherwise loosen or drop a scale without a word.
|
|
3
6
|
*/
|
|
4
7
|
import type { PatchLintTokenScale } from '../types/config.js';
|
|
5
8
|
/** Parses and validates `patchLint.tokenScales`. */
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
3
|
* Validation for `fireforge.json#patchLint.tokenScales`.
|
|
4
|
+
*
|
|
5
|
+
* Unknown keys are refused: a misspelt `allowInCalc` or `minFiles` would
|
|
6
|
+
* otherwise loosen or drop a scale without a word.
|
|
4
7
|
*/
|
|
5
8
|
import { ConfigError } from '../errors/config.js';
|
|
6
9
|
import { parseObject } from '../utils/parse.js';
|
|
10
|
+
const SCALE_KEYS = [
|
|
11
|
+
'properties',
|
|
12
|
+
'prefix',
|
|
13
|
+
'allowLiterals',
|
|
14
|
+
'allow',
|
|
15
|
+
'allowInCalc',
|
|
16
|
+
'offsetCap',
|
|
17
|
+
'exclude',
|
|
18
|
+
'minFiles',
|
|
19
|
+
];
|
|
20
|
+
const PREFIX_PATTERN = /^--[\w-]+$/;
|
|
7
21
|
function parseNames(raw, label, required) {
|
|
8
22
|
if (raw === undefined && !required)
|
|
9
23
|
return undefined;
|
|
@@ -14,6 +28,21 @@ function parseNames(raw, label, required) {
|
|
|
14
28
|
}
|
|
15
29
|
return raw;
|
|
16
30
|
}
|
|
31
|
+
function parsePrefix(raw, label) {
|
|
32
|
+
const prefixes = Array.isArray(raw) ? raw : [raw];
|
|
33
|
+
if (prefixes.length === 0 ||
|
|
34
|
+
prefixes.some((prefix) => typeof prefix !== 'string' || !PREFIX_PATTERN.test(prefix))) {
|
|
35
|
+
throw new ConfigError(`Config field "${label}.prefix" must be a custom-property prefix such as "--mybrowser-space-", or a non-empty list of them`);
|
|
36
|
+
}
|
|
37
|
+
return Array.isArray(raw) ? prefixes : raw;
|
|
38
|
+
}
|
|
39
|
+
function parseNumber(raw, label, integer) {
|
|
40
|
+
const valid = typeof raw === 'number' && Number.isFinite(raw) && (integer ? Number.isInteger(raw) : true);
|
|
41
|
+
if (!valid || raw < (integer ? 1 : 0)) {
|
|
42
|
+
throw new ConfigError(`Config field "${label}" must be ${integer ? 'a positive integer' : 'a non-negative number'}`);
|
|
43
|
+
}
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
17
46
|
function parseTokenScale(raw, label) {
|
|
18
47
|
let rec;
|
|
19
48
|
try {
|
|
@@ -22,12 +51,13 @@ function parseTokenScale(raw, label) {
|
|
|
22
51
|
catch {
|
|
23
52
|
throw new ConfigError(`Config field "${label}" must be an object`);
|
|
24
53
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
54
|
+
for (const key of Object.keys(raw)) {
|
|
55
|
+
if (!SCALE_KEYS.includes(key)) {
|
|
56
|
+
throw new ConfigError(`Config field "${label}" has unknown key "${key}"`);
|
|
57
|
+
}
|
|
29
58
|
}
|
|
30
|
-
const
|
|
59
|
+
const properties = parseNames(rec.raw('properties'), `${label}.properties`, true) ?? [];
|
|
60
|
+
const scale = { properties, prefix: parsePrefix(rec.raw('prefix'), label) };
|
|
31
61
|
const allowLiterals = parseNames(rec.raw('allowLiterals'), `${label}.allowLiterals`, false);
|
|
32
62
|
if (allowLiterals !== undefined)
|
|
33
63
|
scale.allowLiterals = allowLiterals;
|
|
@@ -39,6 +69,18 @@ function parseTokenScale(raw, label) {
|
|
|
39
69
|
}
|
|
40
70
|
scale.allow = allow;
|
|
41
71
|
}
|
|
72
|
+
const allowInCalc = parseNames(rec.raw('allowInCalc'), `${label}.allowInCalc`, false);
|
|
73
|
+
if (allowInCalc !== undefined)
|
|
74
|
+
scale.allowInCalc = allowInCalc;
|
|
75
|
+
const offsetCap = rec.raw('offsetCap');
|
|
76
|
+
if (offsetCap !== undefined)
|
|
77
|
+
scale.offsetCap = parseNumber(offsetCap, `${label}.offsetCap`, false);
|
|
78
|
+
const exclude = parseNames(rec.raw('exclude'), `${label}.exclude`, false);
|
|
79
|
+
if (exclude !== undefined)
|
|
80
|
+
scale.exclude = exclude;
|
|
81
|
+
const minFiles = rec.raw('minFiles');
|
|
82
|
+
if (minFiles !== undefined)
|
|
83
|
+
scale.minFiles = parseNumber(minFiles, `${label}.minFiles`, true);
|
|
42
84
|
return scale;
|
|
43
85
|
}
|
|
44
86
|
/** Parses and validates `patchLint.tokenScales`. */
|
|
@@ -471,6 +471,12 @@ function parseTypecheckBlock(rec) {
|
|
|
471
471
|
if (undefinedIdentifiers !== undefined) {
|
|
472
472
|
out.undefinedIdentifiers = undefinedIdentifiers;
|
|
473
473
|
}
|
|
474
|
+
const litTypes = rec.raw('litTypes');
|
|
475
|
+
if (litTypes !== undefined && typeof litTypes !== 'boolean') {
|
|
476
|
+
throw new ConfigError('Config field "typecheck.litTypes" must be a boolean');
|
|
477
|
+
}
|
|
478
|
+
if (litTypes !== undefined)
|
|
479
|
+
out.litTypes = litTypes;
|
|
474
480
|
return out;
|
|
475
481
|
}
|
|
476
482
|
/**
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { toError } from '../utils/errors.js';
|
|
15
15
|
import { verbose } from '../utils/logger.js';
|
|
16
|
-
import { collectStagingInputs, createCoverageRecord, findChangedInputs, hashEngineFile, isUnderPath, narrowAnchor, normalizeRequestPath, readEngineContentState, refreshAnchor, } from './coverage-records.js';
|
|
16
|
+
import { collectStagingInputs, createCoverageRecord, findChangedInputs, hashEngineFile, isUnderPath, narrowAnchor, normalizeRequestPath, readEngineContentState, refreshAnchor, widenWithRestagedManifests, } from './coverage-records.js';
|
|
17
17
|
export { hashEngineFile } from './coverage-records.js';
|
|
18
18
|
/** Filename of the generated mozconfig, relative to the engine directory. */
|
|
19
19
|
const MOZCONFIG_FILENAME = 'mozconfig';
|
|
@@ -96,7 +96,7 @@ export async function planCoverageRetention(engineDir, previous, requested) {
|
|
|
96
96
|
if (previous === undefined)
|
|
97
97
|
return emptyPlan();
|
|
98
98
|
const coverage = previous.testPackagingCoverage ?? 'full';
|
|
99
|
-
const rebuilt = requested.map(normalizeRequestPath).filter((path) => path !== '');
|
|
99
|
+
const rebuilt = await widenWithRestagedManifests(engineDir, requested.map(normalizeRequestPath).filter((path) => path !== ''));
|
|
100
100
|
let state;
|
|
101
101
|
try {
|
|
102
102
|
state = await readEngineContentState(engineDir);
|
|
@@ -67,6 +67,17 @@ export declare function findChangedInputs(state: EngineContentState, anchor: Cov
|
|
|
67
67
|
* cannot be read or the anchor cannot be compared.
|
|
68
68
|
*/
|
|
69
69
|
export declare function refreshAnchor<T extends CoverageAnchor>(state: EngineContentState, anchor: T, rebuilt: readonly string[]): Promise<T | undefined>;
|
|
70
|
+
/**
|
|
71
|
+
* The request paths a scoped build restages, widened by the test manifests
|
|
72
|
+
* of every requested file: packaging one file of a manifest re-processes
|
|
73
|
+
* the manifest itself, so a manifest that gained an entry for that file is
|
|
74
|
+
* restaged too, and must not count as a changed input of the manifest's
|
|
75
|
+
* other files afterwards.
|
|
76
|
+
*
|
|
77
|
+
* @param engineDir - Path to the engine directory
|
|
78
|
+
* @param requested - Normalized request paths of the build
|
|
79
|
+
*/
|
|
80
|
+
export declare function widenWithRestagedManifests(engineDir: string, requested: readonly string[]): Promise<string[]>;
|
|
70
81
|
/**
|
|
71
82
|
* Engine-relative paths a manifest names outside its own directory. Bare
|
|
72
83
|
* names stay inside it. `!/` and `/` prefixes are engine-root relative, as
|
|
@@ -217,6 +217,40 @@ async function findManifestDirectory(engineDir, dir) {
|
|
|
217
217
|
return undefined;
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* The request paths a scoped build restages, widened by the test manifests
|
|
222
|
+
* of every requested file: packaging one file of a manifest re-processes
|
|
223
|
+
* the manifest itself, so a manifest that gained an entry for that file is
|
|
224
|
+
* restaged too, and must not count as a changed input of the manifest's
|
|
225
|
+
* other files afterwards.
|
|
226
|
+
*
|
|
227
|
+
* @param engineDir - Path to the engine directory
|
|
228
|
+
* @param requested - Normalized request paths of the build
|
|
229
|
+
*/
|
|
230
|
+
export async function widenWithRestagedManifests(engineDir, requested) {
|
|
231
|
+
const widened = new Set(requested);
|
|
232
|
+
for (const path of requested) {
|
|
233
|
+
if (!TEST_FILE_PATTERN.test(posix.basename(path)))
|
|
234
|
+
continue;
|
|
235
|
+
const dir = await findManifestDirectory(engineDir, posix.dirname(path));
|
|
236
|
+
if (dir === undefined)
|
|
237
|
+
continue;
|
|
238
|
+
let entries;
|
|
239
|
+
try {
|
|
240
|
+
entries = await readdir(join(engineDir, dir));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
if (TEST_MANIFEST_BASENAMES.has(entry))
|
|
247
|
+
widened.add(dir === '' ? entry : `${dir}/${entry}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return [...widened];
|
|
251
|
+
}
|
|
252
|
+
/** A test implementation file (`browser_*.js`, `test_*.js`, …), not a directory. */
|
|
253
|
+
const TEST_FILE_PATTERN = /^(?:browser|test)_.*\.m?js$/;
|
|
220
254
|
/** Test manifests at or beneath `dir`, bounded by {@link MAX_WALK_ENTRIES}. */
|
|
221
255
|
async function findManifestsBeneath(engineDir, dir) {
|
|
222
256
|
const manifests = [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LockMode } from './file-lock.js';
|
|
1
2
|
import { type LockStatusSnapshot } from './file-lock.js';
|
|
2
3
|
export interface EngineSessionLockOptions {
|
|
3
4
|
/**
|
|
@@ -7,6 +8,11 @@ export interface EngineSessionLockOptions {
|
|
|
7
8
|
* fail-fast behaviour.
|
|
8
9
|
*/
|
|
9
10
|
waitLockSeconds?: number | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* `shared` lets build-less test runs overlap each other; see
|
|
13
|
+
* `file-lock-shared.ts`. Every other caller holds the lock exclusively.
|
|
14
|
+
*/
|
|
15
|
+
mode?: LockMode | undefined;
|
|
10
16
|
}
|
|
11
17
|
/**
|
|
12
18
|
* Formats the notice printed when the wait budget is extended because the
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { InconclusiveVerdictError } from '../errors/base.js';
|
|
4
4
|
import { toError } from '../utils/errors.js';
|
|
5
5
|
import { info, warn } from '../utils/logger.js';
|
|
6
|
+
import { MAX_WAIT_LOCK_SECONDS } from '../utils/options.js';
|
|
6
7
|
import { sleep } from '../utils/sleep.js';
|
|
7
8
|
import { formatLockQueuePosition, readLockStatus, withFileLock, } from './file-lock.js';
|
|
8
9
|
import { git } from './git-base.js';
|
|
@@ -14,7 +15,7 @@ const ENGINE_SESSION_LOCK_PATH = join('.fireforge', 'engine-session.lock');
|
|
|
14
15
|
* never gets one longer than the largest wait they could have asked for.
|
|
15
16
|
*/
|
|
16
17
|
const WAIT_EXTENSION_FACTOR = 4;
|
|
17
|
-
const MAX_WAIT_LOCK_MS =
|
|
18
|
+
const MAX_WAIT_LOCK_MS = MAX_WAIT_LOCK_SECONDS * 1000;
|
|
18
19
|
/**
|
|
19
20
|
* Formats the periodic operator-facing waiting line printed while `--wait-lock`
|
|
20
21
|
* polls a contended engine session lock. The holder identification comes from
|
|
@@ -66,6 +67,7 @@ export async function withEngineSessionLock(projectRoot, command, operation, opt
|
|
|
66
67
|
}
|
|
67
68
|
const { waitLockSeconds } = options;
|
|
68
69
|
return withFileLock(join(projectRoot, ENGINE_SESSION_LOCK_PATH), operation, {
|
|
70
|
+
...(options.mode === 'shared' ? { mode: 'shared' } : {}),
|
|
69
71
|
timeoutMs: waitLockSeconds !== undefined ? waitLockSeconds * 1000 : 1000,
|
|
70
72
|
...(waitLockSeconds !== undefined
|
|
71
73
|
? {
|
|
@@ -121,8 +123,13 @@ export async function readEngineSessionLockStatus(projectRoot) {
|
|
|
121
123
|
* Pure, so the wording is unit-testable without a lock on disk.
|
|
122
124
|
*/
|
|
123
125
|
export function formatEngineSessionLockStatus(snapshot, holderCpuSeconds) {
|
|
126
|
+
const shared = snapshot.sharedHolders !== undefined && snapshot.sharedHolders > 0
|
|
127
|
+
? `Shared holders: ${snapshot.sharedHolders} build-less test run(s) under a shared lock.`
|
|
128
|
+
: undefined;
|
|
124
129
|
if (!snapshot.held) {
|
|
125
|
-
return
|
|
130
|
+
return shared === undefined
|
|
131
|
+
? ['Engine session lock: free (no FireForge engine-mutating command is running).']
|
|
132
|
+
: ['Engine session lock: no exclusive holder.', shared];
|
|
126
133
|
}
|
|
127
134
|
const elapsed = snapshot.heldForMs === undefined
|
|
128
135
|
? 'unknown duration'
|
|
@@ -135,6 +142,7 @@ export function formatEngineSessionLockStatus(snapshot, holderCpuSeconds) {
|
|
|
135
142
|
const lines = [
|
|
136
143
|
`Engine session lock: ${identity}, for ${elapsed}.`,
|
|
137
144
|
`Queue depth: ${snapshot.queueDepth} waiter(s).`,
|
|
145
|
+
...(shared === undefined ? [] : [shared]),
|
|
138
146
|
];
|
|
139
147
|
// Liveness answers "does the process exist". It does not answer "is it
|
|
140
148
|
// getting anywhere". A holder alive for minutes having used a fraction of
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared holds on a FireForge directory lock.
|
|
3
|
+
*
|
|
4
|
+
* Every lock used to be exclusive, so two build-less `fireforge test` runs
|
|
5
|
+
* on one objdir, each with its own profile and (under auto ports) its own
|
|
6
|
+
* ports, still waited for each other: the gate's browser tiers ran strictly
|
|
7
|
+
* serially on a 14-core host. A shared hold lets such runs overlap while
|
|
8
|
+
* anything that writes (a build, a re-export, a test run that builds)
|
|
9
|
+
* still holds the lock alone.
|
|
10
|
+
*
|
|
11
|
+
* The scheme reuses the exclusive lock as a turnstile, so arrival order and
|
|
12
|
+
* stale-holder reaping come from the machinery that already exists:
|
|
13
|
+
*
|
|
14
|
+
* - A shared holder takes the exclusive lock in the ordinary way, records
|
|
15
|
+
* itself under `<lock>.shared/<uuid>/` (an owner record like the lock's
|
|
16
|
+
* own), and releases the exclusive lock at once.
|
|
17
|
+
* - An exclusive holder takes the exclusive lock, then waits until no live
|
|
18
|
+
* shared holder is left. While it waits it already holds the turnstile,
|
|
19
|
+
* so no new shared holder can slip in ahead of it: a writer queued
|
|
20
|
+
* behind readers is served once they finish, never starved by newcomers.
|
|
21
|
+
*
|
|
22
|
+
* A shared holder whose process died is reaped by the same PID-first
|
|
23
|
+
* liveness check the exclusive lock uses.
|
|
24
|
+
*/
|
|
25
|
+
/** How a caller holds a lock. */
|
|
26
|
+
export type LockMode = 'exclusive' | 'shared';
|
|
27
|
+
/** The directory holding a lock's shared-holder records. */
|
|
28
|
+
export declare function sharedHoldersDir(lockPath: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Counts the live shared holders of a lock, reaping any whose process is
|
|
31
|
+
* gone. A missing directory means none.
|
|
32
|
+
*
|
|
33
|
+
* @param lockPath - The lock directory
|
|
34
|
+
* @param options - `reap: false` only counts (status reads)
|
|
35
|
+
*/
|
|
36
|
+
export declare function countLiveSharedHolders(lockPath: string, options?: {
|
|
37
|
+
reap?: boolean;
|
|
38
|
+
}): Promise<number>;
|
|
39
|
+
/** A registered shared hold. */
|
|
40
|
+
export interface SharedHold {
|
|
41
|
+
/** Removes the hold. Never throws. */
|
|
42
|
+
release(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Records a shared hold. Call while holding the lock's exclusive turnstile.
|
|
46
|
+
*
|
|
47
|
+
* @param lockPath - The lock directory
|
|
48
|
+
* @param metadata - Owner metadata lines (`command=…`)
|
|
49
|
+
*/
|
|
50
|
+
export declare function registerSharedHold(lockPath: string, metadata: readonly string[] | undefined): Promise<SharedHold>;
|
|
51
|
+
/**
|
|
52
|
+
* Removes every shared hold this process registered, for the signal path.
|
|
53
|
+
* Never throws.
|
|
54
|
+
*
|
|
55
|
+
* @returns The holder paths removed
|
|
56
|
+
*/
|
|
57
|
+
export declare function releaseSharedHoldsForSignal(): Promise<string[]>;
|