@hominis/fireforge 0.38.0 → 0.40.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 +41 -0
- package/README.md +25 -1
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/build.js +8 -3
- package/dist/src/commands/discard.js +75 -50
- package/dist/src/commands/export-flow.d.ts +1 -1
- package/dist/src/commands/export-flow.js +21 -10
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.js +31 -5
- package/dist/src/commands/import.js +5 -0
- package/dist/src/commands/lint-per-patch.js +97 -18
- package/dist/src/commands/lint-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +23 -19
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +20 -1
- package/dist/src/commands/patch/move-files-into.d.ts +47 -0
- package/dist/src/commands/patch/move-files-into.js +245 -0
- package/dist/src/commands/patch/move-files.js +27 -13
- package/dist/src/commands/patch/rename.js +5 -2
- package/dist/src/commands/patch/staged-dependency.js +102 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +106 -0
- package/dist/src/commands/re-export-options.js +3 -0
- package/dist/src/commands/re-export.js +42 -53
- package/dist/src/commands/reset.js +5 -0
- package/dist/src/commands/run.js +18 -2
- package/dist/src/commands/status-check.d.ts +35 -0
- package/dist/src/commands/status-check.js +88 -0
- package/dist/src/commands/status-output.d.ts +9 -0
- package/dist/src/commands/status-output.js +33 -1
- package/dist/src/commands/status.js +33 -8
- package/dist/src/commands/test-diagnose.d.ts +1 -1
- package/dist/src/commands/test-diagnose.js +9 -3
- package/dist/src/commands/test-modes.d.ts +21 -0
- package/dist/src/commands/test-modes.js +42 -0
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +26 -5
- package/dist/src/commands/test.js +38 -32
- package/dist/src/commands/token.js +4 -1
- package/dist/src/commands/tree.d.ts +16 -0
- package/dist/src/commands/tree.js +174 -0
- package/dist/src/core/build-baseline-types.d.ts +7 -0
- package/dist/src/core/build-baseline.d.ts +1 -1
- package/dist/src/core/build-baseline.js +2 -1
- package/dist/src/core/build-prepare.d.ts +9 -0
- package/dist/src/core/build-prepare.js +24 -0
- package/dist/src/core/config-validate.js +25 -0
- package/dist/src/core/discard-baseline.d.ts +60 -0
- package/dist/src/core/discard-baseline.js +232 -0
- package/dist/src/core/license-headers.d.ts +12 -0
- package/dist/src/core/license-headers.js +27 -0
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +17 -5
- package/dist/src/core/patch-export.d.ts +12 -3
- package/dist/src/core/patch-export.js +13 -3
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -2
- package/dist/src/core/patch-lint-checkjs.js +85 -16
- package/dist/src/core/patch-lint-cross.d.ts +9 -1
- package/dist/src/core/patch-lint-cross.js +69 -9
- package/dist/src/core/patch-lint-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- package/dist/src/core/patch-lint-mozbuild.d.ts +18 -0
- package/dist/src/core/patch-lint-mozbuild.js +104 -0
- package/dist/src/core/patch-lint-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint.d.ts +23 -0
- package/dist/src/core/patch-lint.js +34 -3
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-policy.d.ts +14 -1
- package/dist/src/core/patch-policy.js +13 -2
- package/dist/src/core/smoke-patterns.d.ts +24 -8
- package/dist/src/core/smoke-patterns.js +23 -12
- package/dist/src/core/status-classify.d.ts +16 -0
- package/dist/src/core/status-classify.js +20 -6
- package/dist/src/core/test-harness-crash.d.ts +10 -0
- package/dist/src/core/test-harness-crash.js +16 -0
- package/dist/src/core/test-stale-check.d.ts +15 -0
- package/dist/src/core/test-stale-check.js +37 -1
- package/dist/src/core/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +82 -193
- package/dist/src/core/token-variant.d.ts +7 -0
- package/dist/src/core/token-variant.js +9 -0
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +124 -0
- package/dist/src/core/tree-store.d.ts +53 -0
- package/dist/src/core/tree-store.js +214 -0
- package/dist/src/core/typecheck-shim.d.ts +27 -0
- package/dist/src/core/typecheck-shim.js +58 -4
- package/dist/src/core/typecheck.js +14 -3
- package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +45 -2
- package/dist/src/types/config.d.ts +34 -0
- package/dist/src/utils/validation.d.ts +10 -0
- package/dist/src/utils/validation.js +29 -0
- package/package.json +6 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { loadConfig } from '../core/config.js';
|
|
3
|
+
import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
3
4
|
import { GeneralError } from '../errors/base.js';
|
|
4
5
|
import { success } from '../utils/logger.js';
|
|
5
6
|
/** Rejects pathless `fireforge test` unless the operator selected a pathless mode. */
|
|
@@ -59,3 +60,44 @@ function buildPathlessTestMessage() {
|
|
|
59
60
|
' - `fireforge test --doctor` to run the Marionette preflight only\n' +
|
|
60
61
|
' - `fireforge test --canary` to run the configured short harness canary');
|
|
61
62
|
}
|
|
63
|
+
async function classifyTestHarnesses(engineDir, normalizedPaths) {
|
|
64
|
+
const result = { xpcshell: [], nonXpcshell: [] };
|
|
65
|
+
for (const testPath of normalizedPaths) {
|
|
66
|
+
const manifest = await findNearestXpcshellManifest(engineDir, testPath);
|
|
67
|
+
if (manifest) {
|
|
68
|
+
result.xpcshell.push(testPath);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
result.nonXpcshell.push(testPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function buildMixedHarnessMessage(classification) {
|
|
77
|
+
return ('FireForge cannot run xpcshell and browser/mochitest paths in the same mach invocation.\n\n' +
|
|
78
|
+
'Split this into separate `fireforge test` commands so each manifest selects its own harness:\n' +
|
|
79
|
+
` - xpcshell: ${classification.xpcshell.join(', ')}\n` +
|
|
80
|
+
` - browser/mochitest: ${classification.nonXpcshell.join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Harness classification is a pure path→manifest lookup and must precede
|
|
84
|
+
* the build dispatch: a mixed xpcshell+mochitest request can never
|
|
85
|
+
* dispatch, so refusing it before spending minutes in a pre-test build
|
|
86
|
+
* is strictly better (FORGE F7). A nonexistent path classifies as
|
|
87
|
+
* nonXpcshell (findNearestXpcshellManifest returns null), so classifying
|
|
88
|
+
* before assertTestPathsExist is harmless — existence is still asserted
|
|
89
|
+
* after the stale/coverage gates, preserving their precedence over
|
|
90
|
+
* missing-path errors. `xpcshellOnly` (non-empty request, zero
|
|
91
|
+
* nonXpcshell paths) gates the Marionette preflight and the mochitest
|
|
92
|
+
* client flags (FORGE F10); pathless runs keep the full-suite behavior.
|
|
93
|
+
*/
|
|
94
|
+
export async function classifyBeforeDispatch(engineDir, normalizedPaths) {
|
|
95
|
+
const classification = await classifyTestHarnesses(engineDir, normalizedPaths);
|
|
96
|
+
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
|
|
97
|
+
throw new GeneralError(buildMixedHarnessMessage(classification));
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
classification,
|
|
101
|
+
xpcshellOnly: normalizedPaths.length > 0 && classification.nonXpcshell.length === 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -36,6 +36,8 @@ export interface TestRunContext {
|
|
|
36
36
|
baseExtraArgs: readonly string[];
|
|
37
37
|
/** Bounded harness-crash retry budget (0 disables retries). */
|
|
38
38
|
harnessRetries: number;
|
|
39
|
+
/** Whether --headless was requested (crash-hint copy only). */
|
|
40
|
+
headless: boolean;
|
|
39
41
|
/** Extra environment variables for the mach process. */
|
|
40
42
|
env?: Record<string, string>;
|
|
41
43
|
}
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* combined invocation stays available via `--no-shard`.
|
|
13
13
|
*/
|
|
14
14
|
import { mochitestWithOutput, testWithOutput, xpcshellTestWithOutput, } from '../core/mach.js';
|
|
15
|
-
import { buildHarnessCrashMessage, classifyHarnessRun, } from '../core/test-harness-crash.js';
|
|
15
|
+
import { buildHarnessCrashMessage, classifyHarnessRun, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
|
|
16
16
|
import { retryAfterXpcshellSymlinkRepair } from '../core/test-xpcshell-retry.js';
|
|
17
|
+
import { withXpcshellProfileDir } from '../core/xpcshell-profile-dir.js';
|
|
17
18
|
import { BuildError } from '../errors/build.js';
|
|
18
19
|
import { info, note, warn } from '../utils/logger.js';
|
|
20
|
+
import { getPlatform } from '../utils/platform.js';
|
|
19
21
|
import { maybeInjectAppdirArg } from './test-appdir.js';
|
|
20
22
|
/** Default bounded retry budget for recognized harness crashes. */
|
|
21
23
|
export const DEFAULT_HARNESS_RETRIES = 2;
|
|
@@ -33,6 +35,20 @@ function dispatchForSuite(suite) {
|
|
|
33
35
|
* stale-xpcshell-symlink repair path the single-run flow already used.
|
|
34
36
|
*/
|
|
35
37
|
export async function runTestsWithRetries(ctx, paths) {
|
|
38
|
+
// Pure mochitest dispatches keep their env byte-identical — the profile
|
|
39
|
+
// variable is xpcshell-harness-specific (FORGE G15b). Generic `mach
|
|
40
|
+
// test` runs may include xpcshell paths, so they get the isolation too.
|
|
41
|
+
if (ctx.suite === 'mochitest') {
|
|
42
|
+
return runTestsWithRetriesInner(ctx, paths, ctx.env);
|
|
43
|
+
}
|
|
44
|
+
// Fresh profile dir per harness invocation: shards isolate from each
|
|
45
|
+
// other and from concurrent `fireforge test` processes; the crash /
|
|
46
|
+
// symlink-repair retries inside one invocation deliberately REUSE the
|
|
47
|
+
// same directory (they are the same logical run and the harness
|
|
48
|
+
// re-initialises the profile contents itself).
|
|
49
|
+
return withXpcshellProfileDir(ctx.env, (env) => runTestsWithRetriesInner(ctx, paths, env));
|
|
50
|
+
}
|
|
51
|
+
async function runTestsWithRetriesInner(ctx, paths, env) {
|
|
36
52
|
const extraArgs = [...ctx.baseExtraArgs];
|
|
37
53
|
const appdirInjectionAttempted = await maybeInjectAppdirArg(ctx.engineDir, paths, ctx.objDir, extraArgs);
|
|
38
54
|
const dispatch = dispatchForSuite(ctx.suite);
|
|
@@ -42,10 +58,10 @@ export async function runTestsWithRetries(ctx, paths) {
|
|
|
42
58
|
let verdict;
|
|
43
59
|
for (;;) {
|
|
44
60
|
attempts += 1;
|
|
45
|
-
result =
|
|
46
|
-
? await dispatch(ctx.engineDir, paths, extraArgs,
|
|
61
|
+
result = env
|
|
62
|
+
? await dispatch(ctx.engineDir, paths, extraArgs, env)
|
|
47
63
|
: await dispatch(ctx.engineDir, paths, extraArgs);
|
|
48
|
-
result = await retryAfterXpcshellSymlinkRepair(ctx.engineDir, ctx.objDir, result, ctx.classification, paths, extraArgs,
|
|
64
|
+
result = await retryAfterXpcshellSymlinkRepair(ctx.engineDir, ctx.objDir, result, ctx.classification, paths, extraArgs, env, dispatch);
|
|
49
65
|
const combined = `${result.stdout}\n${result.stderr}`;
|
|
50
66
|
verdict = classifyHarnessRun(result.exitCode, combined, paths);
|
|
51
67
|
if (verdict.kind !== 'harness-crash' || attempts >= maxAttempts)
|
|
@@ -78,7 +94,12 @@ export async function runShardedTests(ctx, groups, diagnoseShardFailure) {
|
|
|
78
94
|
const outcome = await runTestsWithRetries(ctx, group.paths);
|
|
79
95
|
shards.push({ label: group.label, outcome });
|
|
80
96
|
if (outcome.verdict.kind === 'harness-crash' && outcome.verdict.signature) {
|
|
81
|
-
|
|
97
|
+
const base = buildHarnessCrashMessage(outcome.verdict.signature, outcome.attempts);
|
|
98
|
+
const hint = headedNoOutputTimeoutHint(outcome.verdict.signature, {
|
|
99
|
+
headless: ctx.headless,
|
|
100
|
+
platform: getPlatform(),
|
|
101
|
+
});
|
|
102
|
+
warn(hint ? `${base}\n\n${hint}` : base);
|
|
82
103
|
}
|
|
83
104
|
else if (outcome.verdict.kind !== 'tests-ran-ok') {
|
|
84
105
|
const diagnosis = diagnoseShardFailure(outcome, group.label);
|
|
@@ -10,7 +10,6 @@ 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 { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
14
13
|
import { GeneralError } from '../errors/base.js';
|
|
15
14
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
16
15
|
import { toError } from '../utils/errors.js';
|
|
@@ -18,7 +17,7 @@ import { pathExists } from '../utils/fs.js';
|
|
|
18
17
|
import { info, intro, outro, spinner, success, verbose } from '../utils/logger.js';
|
|
19
18
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
20
19
|
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
21
|
-
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
|
|
20
|
+
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
|
|
22
21
|
import { DEFAULT_HARNESS_RETRIES, runShardedTests, runTestsWithRetries, } from './test-run.js';
|
|
23
22
|
import { enforceStaleBuildGate, enforceStaticComponentsGate } from './test-stale-gate.js';
|
|
24
23
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
@@ -34,19 +33,6 @@ async function assertTestPathsExist(engineDir, testPaths) {
|
|
|
34
33
|
throw new GeneralError(`Test path${missingPaths.length === 1 ? '' : 's'} not found under engine/: ${missingPaths.join(', ')}\n\n` +
|
|
35
34
|
'If you expected these files to come from your patch stack, run "fireforge import" first.');
|
|
36
35
|
}
|
|
37
|
-
async function classifyTestHarnesses(engineDir, normalizedPaths) {
|
|
38
|
-
const result = { xpcshell: [], nonXpcshell: [] };
|
|
39
|
-
for (const testPath of normalizedPaths) {
|
|
40
|
-
const manifest = await findNearestXpcshellManifest(engineDir, testPath);
|
|
41
|
-
if (manifest) {
|
|
42
|
-
result.xpcshell.push(testPath);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
result.nonXpcshell.push(testPath);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
36
|
/**
|
|
51
37
|
* Picks the mach dispatch target for a (non-mixed) run. A single-suite run
|
|
52
38
|
* auto-routes to the suite-specific command (`mach xpcshell-test` /
|
|
@@ -66,12 +52,6 @@ function resolveTestSuite(classification, forceGeneric) {
|
|
|
66
52
|
}
|
|
67
53
|
return 'generic';
|
|
68
54
|
}
|
|
69
|
-
function buildMixedHarnessMessage(classification) {
|
|
70
|
-
return ('FireForge cannot run xpcshell and browser/mochitest paths in the same mach invocation.\n\n' +
|
|
71
|
-
'Split this into separate `fireforge test` commands so each manifest selects its own harness:\n' +
|
|
72
|
-
` - xpcshell: ${classification.xpcshell.join(', ')}\n` +
|
|
73
|
-
` - browser/mochitest: ${classification.nonXpcshell.join(', ')}`);
|
|
74
|
-
}
|
|
75
55
|
function filterRedundantXpcshellFlavorArgs(machArgs, classification) {
|
|
76
56
|
if (classification.xpcshell.length === 0 || classification.nonXpcshell.length > 0) {
|
|
77
57
|
return [...machArgs];
|
|
@@ -140,7 +120,9 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
|
|
|
140
120
|
// anchor forward (a `mach build faster` does not rebake
|
|
141
121
|
// components.conf into the compiled table).
|
|
142
122
|
try {
|
|
143
|
-
await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, testPackagingCoverage, previousBaseline
|
|
123
|
+
await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, testPackagingCoverage, previousBaseline, testPackagingCoverage === 'full'
|
|
124
|
+
? 'fireforge test --build'
|
|
125
|
+
: `fireforge test --build ${testPackagingCoverage.join(' ')}`);
|
|
144
126
|
}
|
|
145
127
|
catch (baselineError) {
|
|
146
128
|
verbose(`Could not persist build baseline: ${toError(baselineError).message}`);
|
|
@@ -239,7 +221,7 @@ async function runDoctorPreflight(args) {
|
|
|
239
221
|
* `--mach-arg` and the xpcshell flavor that ignores the pref entirely.
|
|
240
222
|
* Mutates `extraArgs` in place.
|
|
241
223
|
*/
|
|
242
|
-
function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
|
|
224
|
+
function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcshellOnly = false) {
|
|
243
225
|
// Auto-forward the Marionette port to mach when `--marionette-port` is
|
|
244
226
|
// set. `--setpref=marionette.port=<n>` configures where the browser
|
|
245
227
|
// listener binds; `--marionette=127.0.0.1:<n>` tells the mochitest harness
|
|
@@ -258,6 +240,14 @@ function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
|
|
|
258
240
|
// `--marionette=...` (or two-token `--marionette host:port`).
|
|
259
241
|
if (options.marionettePort === undefined)
|
|
260
242
|
return;
|
|
243
|
+
if (xpcshellOnly) {
|
|
244
|
+
// Manifest classification says every requested path is xpcshell —
|
|
245
|
+
// xpcshell ignores the browser Marionette path entirely, and the
|
|
246
|
+
// mochitest client flags previously forwarded here made mach reject
|
|
247
|
+
// the dispatch (FORGE F10).
|
|
248
|
+
info(`--marionette-port=${options.marionettePort} applied to the preflight probe only: the requested paths are xpcshell-only, and xpcshell ignores the browser Marionette port. Not forwarding --setpref=marionette.port or --marionette to mach.`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
261
251
|
{
|
|
262
252
|
const operatorAlreadyForwarded = forwardedPort !== undefined;
|
|
263
253
|
const machArgs = options.machArg ?? [];
|
|
@@ -276,8 +266,23 @@ function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
|
|
|
276
266
|
}
|
|
277
267
|
}
|
|
278
268
|
}
|
|
279
|
-
async function ensureTestMarionettePortAvailable(port, binaryName,
|
|
280
|
-
if (
|
|
269
|
+
async function ensureTestMarionettePortAvailable(port, binaryName, options, skip) {
|
|
270
|
+
if (skip.xpcshellOnly && !skip.doctor) {
|
|
271
|
+
// xpcshell does not bind the browser Marionette port, so a developer's
|
|
272
|
+
// interactive browser holding 2828 must not kill an xpcshell run
|
|
273
|
+
// (FORGE F10). --doctor keeps the preflight: its probe launches a
|
|
274
|
+
// Marionette browser regardless of the requested harness.
|
|
275
|
+
const message = 'Skipping the Marionette stale-port preflight: all requested paths are xpcshell ' +
|
|
276
|
+
'(xpcshell does not bind the browser Marionette port).';
|
|
277
|
+
if (options.marionettePort !== undefined || options.killStaleMarionette === true) {
|
|
278
|
+
info(message);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
verbose(message);
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (options.killStaleMarionette === true) {
|
|
281
286
|
await ensureMarionettePortAvailable(port, { binaryName, killStaleBrowser: true });
|
|
282
287
|
return;
|
|
283
288
|
}
|
|
@@ -321,6 +326,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
321
326
|
// stale/coverage refusals keep precedence over missing-path errors.)
|
|
322
327
|
const requestedPaths = canaryPath !== undefined ? [canaryPath] : testPaths;
|
|
323
328
|
const normalizedPaths = requestedPaths.map((p) => stripEnginePrefix(p).trim());
|
|
329
|
+
const { classification, xpcshellOnly } = await classifyBeforeDispatch(paths.engine, normalizedPaths);
|
|
324
330
|
// Run incremental build if requested
|
|
325
331
|
if (options.build) {
|
|
326
332
|
// A path-less `test --build` runs (and packages for) the full suite;
|
|
@@ -361,7 +367,10 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
361
367
|
// generic bind failure. 2026-04-21 eval (Finding #20): a stale
|
|
362
368
|
// `-marionette` process from `fresh/` poisoned a later test run in
|
|
363
369
|
// the sibling `mybrowser/` workspace.
|
|
364
|
-
await ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options
|
|
370
|
+
await ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options, {
|
|
371
|
+
xpcshellOnly,
|
|
372
|
+
doctor: options.doctor === true,
|
|
373
|
+
});
|
|
365
374
|
if (options.doctor) {
|
|
366
375
|
const doctorOutcome = await runDoctorPreflight({
|
|
367
376
|
engineDir: paths.engine,
|
|
@@ -375,10 +384,6 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
375
384
|
return;
|
|
376
385
|
}
|
|
377
386
|
await assertTestPathsExist(paths.engine, normalizedPaths);
|
|
378
|
-
const classification = await classifyTestHarnesses(paths.engine, normalizedPaths);
|
|
379
|
-
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
|
|
380
|
-
throw new GeneralError(buildMixedHarnessMessage(classification));
|
|
381
|
-
}
|
|
382
387
|
const suite = resolveTestSuite(classification, options.genericMachTest === true);
|
|
383
388
|
const forwardedMachArgs = options.machArg && options.machArg.length > 0
|
|
384
389
|
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
@@ -401,7 +406,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
401
406
|
if (forwardedMachArgs.length > 0) {
|
|
402
407
|
extraArgs.push(...forwardedMachArgs);
|
|
403
408
|
}
|
|
404
|
-
appendMarionetteForwardingArgs(extraArgs, options, forwardedPort);
|
|
409
|
+
appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcshellOnly);
|
|
405
410
|
// Directory arguments mean EXACTLY that directory: mozbuild's test
|
|
406
411
|
// resolver matches paths by string prefix, so a bare directory arg
|
|
407
412
|
// silently swept in prefix-named siblings (152.0b7 → 153.0b8 drill:
|
|
@@ -437,6 +442,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
437
442
|
suite,
|
|
438
443
|
baseExtraArgs: extraArgs,
|
|
439
444
|
harnessRetries,
|
|
445
|
+
headless: options.headless === true,
|
|
440
446
|
...(perfSampleEnv ? { env: perfSampleEnv } : {}),
|
|
441
447
|
};
|
|
442
448
|
const postRebuildContext = options.build
|
|
@@ -482,7 +488,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
482
488
|
reportCanaryOutcome(outcome);
|
|
483
489
|
return;
|
|
484
490
|
}
|
|
485
|
-
finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext);
|
|
491
|
+
finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext, options.headless === true);
|
|
486
492
|
}
|
|
487
493
|
/**
|
|
488
494
|
* Builds the perf-sample env contract for the harness run (field report
|
|
@@ -105,7 +105,10 @@ export async function tokenAddCommand(projectRoot, tokenName, value, options) {
|
|
|
105
105
|
...(options.variant !== undefined ? { variant: options.variant } : {}),
|
|
106
106
|
});
|
|
107
107
|
if (result.skipped) {
|
|
108
|
-
|
|
108
|
+
const where = result.skippedExisting
|
|
109
|
+
? ` in category "${result.skippedExisting.category ?? options.category}" (line ${String(result.skippedExisting.line)})`
|
|
110
|
+
: '';
|
|
111
|
+
info(`Token ${tokenName} already exists${where} (skipped)`);
|
|
109
112
|
}
|
|
110
113
|
else {
|
|
111
114
|
const forgeConfig = await loadConfig(projectRoot);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { CommandContext } from '../types/cli.js';
|
|
3
|
+
/** Creates a verification tree under `.fireforge/trees/<name>`. */
|
|
4
|
+
export declare function treeCreateCommand(projectRoot: string, name: string, options?: {
|
|
5
|
+
forceCopy?: boolean;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
/** Prints the tree list with staleness against the current primary state. */
|
|
8
|
+
export declare function treeListCommand(projectRoot: string, options?: {
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
/** Removes one tree (or all of them with `--all`). */
|
|
12
|
+
export declare function treeRemoveCommand(projectRoot: string, name: string | undefined, options?: {
|
|
13
|
+
all?: boolean;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
/** Registers the tree command group on the CLI program. */
|
|
16
|
+
export declare function registerTree(program: Command, context: CommandContext): void;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `fireforge tree` — copy-on-write verification clones (FORGE G15).
|
|
4
|
+
*
|
|
5
|
+
* Trees enable concurrent READ-MOSTLY verification (lint, typecheck,
|
|
6
|
+
* status, verify, doctor, export dry-runs) beside a busy primary tree.
|
|
7
|
+
* Exports and every other mutation stay strictly serial in the primary —
|
|
8
|
+
* a tree's patches/components are snapshots with no merge model, and the
|
|
9
|
+
* tree guard (cli.ts preAction + core/tree-guard.ts) refuses mutating
|
|
10
|
+
* commands inside a tree. In-tree `test` and objdir cloning are deferred:
|
|
11
|
+
* mach objdirs embed absolute source paths, so a cloned objdir would
|
|
12
|
+
* silently operate against the primary tree.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { Command } from 'commander';
|
|
17
|
+
import { withEngineSessionLock } from '../core/engine-session-lock.js';
|
|
18
|
+
import { detectCowSupport } from '../core/tree-cow.js';
|
|
19
|
+
import { cloneTree, listTrees, removeTree } from '../core/tree-store.js';
|
|
20
|
+
import { assertValidTreeName, computePrimaryFingerprint, getTreesDir, readTreeMarker, withTreeLifecycleLock, } from '../core/tree-store.js';
|
|
21
|
+
import { GeneralError } from '../errors/base.js';
|
|
22
|
+
import { pathExists } from '../utils/fs.js';
|
|
23
|
+
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
24
|
+
function assertPosix() {
|
|
25
|
+
if (process.platform === 'win32') {
|
|
26
|
+
throw new GeneralError('fireforge tree is POSIX-only; copy-on-write cloning relies on clonefile/reflink, which are not available on Windows.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Creates a verification tree under `.fireforge/trees/<name>`. */
|
|
30
|
+
export async function treeCreateCommand(projectRoot, name, options = {}) {
|
|
31
|
+
assertPosix();
|
|
32
|
+
intro('FireForge Tree Create');
|
|
33
|
+
assertValidTreeName(name);
|
|
34
|
+
if (await readTreeMarker(projectRoot)) {
|
|
35
|
+
throw new GeneralError('Verification trees cannot be nested: run tree create in the primary tree.');
|
|
36
|
+
}
|
|
37
|
+
const treesDir = getTreesDir(projectRoot);
|
|
38
|
+
const treeRoot = join(treesDir, name);
|
|
39
|
+
await withTreeLifecycleLock(projectRoot, async () => {
|
|
40
|
+
if (await pathExists(treeRoot)) {
|
|
41
|
+
throw new GeneralError(`A tree named "${name}" already exists. Remove it first (fireforge tree remove ${name}) — refresh is remove + create.`);
|
|
42
|
+
}
|
|
43
|
+
const { mkdir } = await import('node:fs/promises');
|
|
44
|
+
await mkdir(treesDir, { recursive: true });
|
|
45
|
+
const capability = await detectCowSupport(treesDir);
|
|
46
|
+
if (capability === 'none' && options.forceCopy !== true) {
|
|
47
|
+
throw new GeneralError('This filesystem cannot copy-on-write (APFS clonefile or btrfs/XFS reflink required), so a tree ' +
|
|
48
|
+
'would be a full physical copy of the applied engine tree — potentially tens of gigabytes. ' +
|
|
49
|
+
'Re-run with --force-copy to accept that explicitly.');
|
|
50
|
+
}
|
|
51
|
+
if (capability === 'none') {
|
|
52
|
+
warn('No copy-on-write support detected — performing a full physical copy (--force-copy).');
|
|
53
|
+
}
|
|
54
|
+
// Snapshot under the PRIMARY engine-session lock so a mid-mutation
|
|
55
|
+
// state (half-applied export, furnace deploy) is never captured.
|
|
56
|
+
const marker = await withEngineSessionLock(projectRoot, 'tree create', () => cloneTree({
|
|
57
|
+
primaryRoot: projectRoot,
|
|
58
|
+
treeRoot,
|
|
59
|
+
name,
|
|
60
|
+
capability,
|
|
61
|
+
createdAt: new Date().toISOString(),
|
|
62
|
+
}));
|
|
63
|
+
success(`Created verification tree "${name}" at ${treeRoot}`);
|
|
64
|
+
info(` Engine HEAD: ${marker.engineHead ?? '(no engine)'}`);
|
|
65
|
+
info(' Read-only: status, lint, typecheck, verify, doctor, and export --dry-run run inside it; mutation commands are refused.');
|
|
66
|
+
});
|
|
67
|
+
outro('Tree created');
|
|
68
|
+
}
|
|
69
|
+
/** Prints the tree list with staleness against the current primary state. */
|
|
70
|
+
export async function treeListCommand(projectRoot, options = {}) {
|
|
71
|
+
const trees = await listTrees(projectRoot);
|
|
72
|
+
if (options.json === true) {
|
|
73
|
+
process.stdout.write(`${JSON.stringify({ schemaVersion: 1, trees }, null, 2)}\n`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
intro('FireForge Trees');
|
|
77
|
+
if (trees.length === 0) {
|
|
78
|
+
info('No verification trees. Create one with: fireforge tree create <name>');
|
|
79
|
+
outro('Done');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const tree of trees) {
|
|
83
|
+
info(`${tree.name.padEnd(20)} ${tree.staleness.padEnd(24)} created ${tree.createdAt}`);
|
|
84
|
+
}
|
|
85
|
+
outro(`${trees.length} tree(s)`);
|
|
86
|
+
}
|
|
87
|
+
/** Removes one tree (or all of them with `--all`). */
|
|
88
|
+
export async function treeRemoveCommand(projectRoot, name, options = {}) {
|
|
89
|
+
assertPosix();
|
|
90
|
+
intro('FireForge Tree Remove');
|
|
91
|
+
if (options.all !== true && name === undefined) {
|
|
92
|
+
throw new GeneralError('Pass a tree name, or --all to remove every tree.');
|
|
93
|
+
}
|
|
94
|
+
await withTreeLifecycleLock(projectRoot, async () => {
|
|
95
|
+
const targets = options.all === true ? (await listTrees(projectRoot)).map((t) => t.name) : [name ?? ''];
|
|
96
|
+
if (targets.length === 0) {
|
|
97
|
+
info('No verification trees to remove.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
for (const target of targets) {
|
|
101
|
+
await removeTree(projectRoot, target);
|
|
102
|
+
success(`Removed verification tree "${target}"`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
outro('Tree removal complete');
|
|
106
|
+
}
|
|
107
|
+
/** Spawns `fireforge <args>` with the tree as its working directory. */
|
|
108
|
+
async function treeExecCommand(projectRoot, name, args) {
|
|
109
|
+
assertPosix();
|
|
110
|
+
assertValidTreeName(name);
|
|
111
|
+
const treeRoot = join(getTreesDir(projectRoot), name);
|
|
112
|
+
const marker = await readTreeMarker(treeRoot);
|
|
113
|
+
if (!marker) {
|
|
114
|
+
throw new GeneralError(`No verification tree named "${name}". List trees with: fireforge tree list`);
|
|
115
|
+
}
|
|
116
|
+
const current = await computePrimaryFingerprint(projectRoot);
|
|
117
|
+
if (marker.engineHead !== current.engineHead) {
|
|
118
|
+
warn(`Tree "${name}" is stale (primary engine advanced since the snapshot).`);
|
|
119
|
+
}
|
|
120
|
+
else if (marker.patchesFingerprint !== current.patchesFingerprint) {
|
|
121
|
+
warn(`Tree "${name}" is stale (primary patches changed since the snapshot).`);
|
|
122
|
+
}
|
|
123
|
+
const cliEntry = process.argv[1];
|
|
124
|
+
if (cliEntry === undefined) {
|
|
125
|
+
throw new GeneralError('Cannot resolve the fireforge CLI entry point for tree exec.');
|
|
126
|
+
}
|
|
127
|
+
const exitCode = await new Promise((resolvePromise, rejectPromise) => {
|
|
128
|
+
const child = spawn(process.execPath, [cliEntry, ...args], {
|
|
129
|
+
cwd: treeRoot,
|
|
130
|
+
stdio: 'inherit',
|
|
131
|
+
});
|
|
132
|
+
child.on('error', rejectPromise);
|
|
133
|
+
child.on('exit', (code) => {
|
|
134
|
+
resolvePromise(code ?? 1);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
if (exitCode !== 0) {
|
|
138
|
+
throw new GeneralError(`tree exec: fireforge ${args.join(' ')} exited with code ${String(exitCode)}.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Registers the tree command group on the CLI program. */
|
|
142
|
+
export function registerTree(program, context) {
|
|
143
|
+
const { getProjectRoot, withErrorHandling } = context;
|
|
144
|
+
const tree = program
|
|
145
|
+
.command('tree')
|
|
146
|
+
.description('Manage copy-on-write verification clones of this project for concurrent read-mostly work (lint, typecheck, status, verify, export --dry-run). Mutation commands are refused inside a tree.');
|
|
147
|
+
tree
|
|
148
|
+
.command('create <name>')
|
|
149
|
+
.description('Clone the project (engine included, obj-* excluded) into .fireforge/trees/<name> using filesystem copy-on-write (APFS clonefile / btrfs-XFS reflink). Refuses on filesystems without CoW support unless --force-copy explicitly accepts a full physical copy.')
|
|
150
|
+
.option('--force-copy', 'Allow a full physical copy when the filesystem cannot copy-on-write. This can copy tens of GB; never implied.')
|
|
151
|
+
.action(withErrorHandling(async (name, options) => {
|
|
152
|
+
await treeCreateCommand(getProjectRoot(), name, options);
|
|
153
|
+
}));
|
|
154
|
+
tree
|
|
155
|
+
.command('list')
|
|
156
|
+
.description('List verification trees with creation time and staleness vs the primary tree (engine HEAD and patches.json fingerprint).')
|
|
157
|
+
.option('--json', 'Output the tree list as JSON')
|
|
158
|
+
.action(withErrorHandling(async (options) => {
|
|
159
|
+
await treeListCommand(getProjectRoot(), options);
|
|
160
|
+
}));
|
|
161
|
+
tree
|
|
162
|
+
.command('remove [name]')
|
|
163
|
+
.description("Delete a verification tree. Refuses while a live process holds the tree's build or engine-session lock.")
|
|
164
|
+
.option('--all', 'Remove every verification tree')
|
|
165
|
+
.action(withErrorHandling(async (name, options) => {
|
|
166
|
+
await treeRemoveCommand(getProjectRoot(), name, options);
|
|
167
|
+
}));
|
|
168
|
+
tree
|
|
169
|
+
.command('exec <name> [args...]')
|
|
170
|
+
.description('Run a fireforge command inside the named tree (equivalent to cd-ing into it). Example: fireforge tree exec shard-a -- lint --per-patch')
|
|
171
|
+
.action(withErrorHandling(async (name, args) => {
|
|
172
|
+
await treeExecCommand(getProjectRoot(), name, args);
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
@@ -76,6 +76,13 @@ export interface BuildBaseline {
|
|
|
76
76
|
* static-components stale check degrades to "fresh" in that case.
|
|
77
77
|
*/
|
|
78
78
|
staticComponentsBaseline?: StaticComponentsBaseline;
|
|
79
|
+
/**
|
|
80
|
+
* Invocation shape that recorded this baseline (`'fireforge build'`,
|
|
81
|
+
* `'fireforge build --ui'`, `'fireforge test --build [paths]'`).
|
|
82
|
+
* Informational — surfaced by `fireforge status --test-coverage`.
|
|
83
|
+
* Missing on pre-0.39.0 baselines; render as "unknown".
|
|
84
|
+
*/
|
|
85
|
+
recordedBy?: string;
|
|
79
86
|
}
|
|
80
87
|
/**
|
|
81
88
|
* State of the engine at the last FULL build, as far as compiled-in XPCOM
|
|
@@ -48,4 +48,4 @@ export declare function readBuildBaseline(projectRoot: string): Promise<BuildBas
|
|
|
48
48
|
* does not rebake `components.conf` registrations, so the last FULL
|
|
49
49
|
* build stays the honest anchor for the compiled StaticComponents table.
|
|
50
50
|
*/
|
|
51
|
-
export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage, previousBaseline?: BuildBaseline): Promise<void>;
|
|
51
|
+
export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage, previousBaseline?: BuildBaseline, recordedBy?: string): Promise<void>;
|
|
@@ -74,7 +74,7 @@ export async function readBuildBaseline(projectRoot) {
|
|
|
74
74
|
* does not rebake `components.conf` registrations, so the last FULL
|
|
75
75
|
* build stays the honest anchor for the compiled StaticComponents table.
|
|
76
76
|
*/
|
|
77
|
-
export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline) {
|
|
77
|
+
export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline, recordedBy) {
|
|
78
78
|
let engineHeadSha = '';
|
|
79
79
|
try {
|
|
80
80
|
engineHeadSha = await getHead(engineDir);
|
|
@@ -103,6 +103,7 @@ export async function writeBuildBaseline(projectRoot, engineDir, binaryName, tes
|
|
|
103
103
|
...(packageableFingerprints !== undefined ? { packageableFingerprints } : {}),
|
|
104
104
|
...(testPackagingCoverage !== undefined ? { testPackagingCoverage } : {}),
|
|
105
105
|
...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
|
|
106
|
+
...(recordedBy !== undefined ? { recordedBy } : {}),
|
|
106
107
|
};
|
|
107
108
|
await writeJson(getBuildBaselinePath(projectRoot), baseline);
|
|
108
109
|
}
|
|
@@ -24,6 +24,15 @@ export interface PrepareBuildOptions {
|
|
|
24
24
|
*/
|
|
25
25
|
previousBaseline?: BuildBaseline | undefined;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Describes an exit code in the shell's 128+N signal convention. Truncated
|
|
29
|
+
* configure/build logs with a signal-shaped exit (e.g. 144 = 128+16, SIGURG
|
|
30
|
+
* on macOS) are environmental interruptions, not compiler failures — naming
|
|
31
|
+
* that in the failure text saves the operator a fruitless log hunt
|
|
32
|
+
* (FORGE F16). Returns undefined for codes <= 128 (regular failures) and
|
|
33
|
+
* for codes past the conventional signal range, so callers append nothing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function describeSignalShapedExit(exitCode: number): string | undefined;
|
|
27
36
|
/**
|
|
28
37
|
* Returns true when the file path matches a pattern that forces
|
|
29
38
|
* `mach configure` to regenerate the backend. Exported for testing.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Shared pre-flight logic for build and package commands:
|
|
4
4
|
* story cleanup, branding setup, Furnace component application, and mozconfig generation.
|
|
5
5
|
*/
|
|
6
|
+
import { constants as osConstants } from 'node:os';
|
|
6
7
|
import { BuildError } from '../errors/build.js';
|
|
7
8
|
import { FurnaceError } from '../errors/furnace.js';
|
|
8
9
|
import { toError } from '../utils/errors.js';
|
|
@@ -33,6 +34,27 @@ function extractMachConfigureError(result) {
|
|
|
33
34
|
return '';
|
|
34
35
|
return combined.split('\n').slice(-40).join('\n').trim();
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Describes an exit code in the shell's 128+N signal convention. Truncated
|
|
39
|
+
* configure/build logs with a signal-shaped exit (e.g. 144 = 128+16, SIGURG
|
|
40
|
+
* on macOS) are environmental interruptions, not compiler failures — naming
|
|
41
|
+
* that in the failure text saves the operator a fruitless log hunt
|
|
42
|
+
* (FORGE F16). Returns undefined for codes <= 128 (regular failures) and
|
|
43
|
+
* for codes past the conventional signal range, so callers append nothing.
|
|
44
|
+
*/
|
|
45
|
+
export function describeSignalShapedExit(exitCode) {
|
|
46
|
+
if (exitCode <= 128 || exitCode > 192)
|
|
47
|
+
return undefined;
|
|
48
|
+
const signalNumber = exitCode - 128;
|
|
49
|
+
const names = Object.entries(osConstants.signals)
|
|
50
|
+
.filter(([, value]) => value === signalNumber)
|
|
51
|
+
.map(([name]) => name);
|
|
52
|
+
const label = names.length > 0 ? names.join('/') : `signal ${signalNumber}`;
|
|
53
|
+
return (`Exit ${exitCode} is signal-shaped (${exitCode} - 128 = ${signalNumber}, ${label} on this host): ` +
|
|
54
|
+
'the process was likely interrupted externally (OOM-killer, terminal disconnect, ' +
|
|
55
|
+
'display/session teardown) rather than failing on its own, and the log may be ' +
|
|
56
|
+
'truncated mid-step.');
|
|
57
|
+
}
|
|
36
58
|
/**
|
|
37
59
|
* Builds the `BuildError` for a non-zero auto-configure exit: the output
|
|
38
60
|
* tail (so the underlying mozbuild failure survives), any matched
|
|
@@ -43,8 +65,10 @@ function extractMachConfigureError(result) {
|
|
|
43
65
|
function buildConfigureFailureError(captured) {
|
|
44
66
|
const detail = extractMachConfigureError(captured);
|
|
45
67
|
const hints = explainMachError(`${captured.stderr}\n${captured.stdout}`);
|
|
68
|
+
const signalNote = describeSignalShapedExit(captured.exitCode);
|
|
46
69
|
return new BuildError(`Backend regeneration failed: mach configure exited with code ${captured.exitCode}.` +
|
|
47
70
|
(detail ? `\n\nmach configure output (tail):\n${detail}` : '') +
|
|
71
|
+
(signalNote ? `\n\n${signalNote}` : '') +
|
|
48
72
|
(hints.length > 0 ? `\n\n${hints.map((hint) => `Hint: ${hint}`).join('\n')}` : '') +
|
|
49
73
|
'\n\nBuild stopped because continuing would hide the real configure failure.', 'mach configure');
|
|
50
74
|
}
|
|
@@ -328,6 +328,17 @@ function parsePatchLintBlock(rec) {
|
|
|
328
328
|
if (checkJsExtraShim !== undefined) {
|
|
329
329
|
out.checkJsExtraShim = parseShimPath(checkJsExtraShim, 'patchLint.checkJsExtraShim');
|
|
330
330
|
}
|
|
331
|
+
const checkJsTestFiles = rec.raw('checkJsTestFiles');
|
|
332
|
+
if (checkJsTestFiles !== undefined) {
|
|
333
|
+
if (typeof checkJsTestFiles !== 'boolean') {
|
|
334
|
+
throw new ConfigError('Config field "patchLint.checkJsTestFiles" must be a boolean');
|
|
335
|
+
}
|
|
336
|
+
out.checkJsTestFiles = checkJsTestFiles;
|
|
337
|
+
}
|
|
338
|
+
const checkJsTestShim = rec.raw('checkJsTestShim');
|
|
339
|
+
if (checkJsTestShim !== undefined) {
|
|
340
|
+
out.checkJsTestShim = parseShimPath(checkJsTestShim, 'patchLint.checkJsTestShim');
|
|
341
|
+
}
|
|
331
342
|
const rawColorAllowlist = rec.raw('rawColorAllowlist');
|
|
332
343
|
if (rawColorAllowlist !== undefined) {
|
|
333
344
|
if (!Array.isArray(rawColorAllowlist) ||
|
|
@@ -348,12 +359,22 @@ function parsePatchLintBlock(rec) {
|
|
|
348
359
|
if (chromeScriptJsDoc !== undefined) {
|
|
349
360
|
out.chromeScriptJsDoc = chromeScriptJsDoc;
|
|
350
361
|
}
|
|
362
|
+
const undefinedIdentifiers = parseSeverityGate(rec.raw('undefinedIdentifiers'), 'patchLint.undefinedIdentifiers');
|
|
363
|
+
if (undefinedIdentifiers !== undefined) {
|
|
364
|
+
out.undefinedIdentifiers = undefinedIdentifiers;
|
|
365
|
+
}
|
|
351
366
|
if (out.checkJsStrict === true && out.checkJs !== true) {
|
|
352
367
|
throw new ConfigError('Config field "patchLint.checkJsStrict" requires "patchLint.checkJs": true');
|
|
353
368
|
}
|
|
354
369
|
if (out.checkJsCompilerOptions !== undefined && out.checkJsStrict !== true) {
|
|
355
370
|
throw new ConfigError('Config field "patchLint.checkJsCompilerOptions" requires "patchLint.checkJsStrict": true');
|
|
356
371
|
}
|
|
372
|
+
if (out.checkJsTestFiles === true && out.checkJs !== true) {
|
|
373
|
+
throw new ConfigError('Config field "patchLint.checkJsTestFiles" requires "patchLint.checkJs": true');
|
|
374
|
+
}
|
|
375
|
+
if (out.checkJsTestShim !== undefined && out.checkJsTestFiles !== true) {
|
|
376
|
+
throw new ConfigError('Config field "patchLint.checkJsTestShim" requires "patchLint.checkJsTestFiles": true');
|
|
377
|
+
}
|
|
357
378
|
return out;
|
|
358
379
|
}
|
|
359
380
|
/**
|
|
@@ -413,6 +434,10 @@ function parseTypecheckBlock(rec) {
|
|
|
413
434
|
if (overrides) {
|
|
414
435
|
out.projectOverrides = overrides;
|
|
415
436
|
}
|
|
437
|
+
const undefinedIdentifiers = parseSeverityGate(rec.raw('undefinedIdentifiers'), 'typecheck.undefinedIdentifiers');
|
|
438
|
+
if (undefinedIdentifiers !== undefined) {
|
|
439
|
+
out.undefinedIdentifiers = undefinedIdentifiers;
|
|
440
|
+
}
|
|
416
441
|
return out;
|
|
417
442
|
}
|
|
418
443
|
/**
|