@hominis/fireforge 0.32.0 → 0.34.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 +25 -0
- package/dist/src/commands/build.js +9 -1
- package/dist/src/commands/doctor-furnace-jar.d.ts +16 -0
- package/dist/src/commands/doctor-furnace-jar.js +47 -0
- package/dist/src/commands/doctor-furnace.js +2 -0
- package/dist/src/commands/export-flow.js +4 -1
- package/dist/src/commands/export-placement-gate.js +4 -1
- package/dist/src/commands/export.js +61 -4
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +20 -0
- package/dist/src/commands/furnace/chrome-doc-templates.js +52 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +10 -0
- package/dist/src/commands/furnace/chrome-doc.js +31 -4
- package/dist/src/commands/furnace/create-browser-test.d.ts +30 -0
- package/dist/src/commands/furnace/create-browser-test.js +180 -0
- package/dist/src/commands/furnace/create-mochikit.js +11 -4
- package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
- package/dist/src/commands/furnace/create-xpcshell.js +38 -12
- package/dist/src/commands/furnace/create.js +7 -114
- package/dist/src/commands/furnace/index.js +6 -1
- package/dist/src/commands/furnace/scan.d.ts +1 -0
- package/dist/src/commands/furnace/scan.js +57 -27
- package/dist/src/commands/furnace/validate.js +17 -1
- package/dist/src/commands/patch/split-plan.d.ts +18 -2
- package/dist/src/commands/patch/split-plan.js +90 -16
- package/dist/src/commands/patch/split.js +12 -3
- package/dist/src/commands/re-export-options.d.ts +26 -0
- package/dist/src/commands/re-export-options.js +48 -1
- package/dist/src/commands/re-export.js +4 -1
- package/dist/src/commands/register.js +10 -1
- package/dist/src/commands/test-diagnose.js +12 -0
- package/dist/src/commands/test.js +29 -22
- package/dist/src/commands/token.js +12 -1
- package/dist/src/commands/typecheck.js +35 -0
- package/dist/src/core/build-prepare.js +23 -3
- package/dist/src/core/config-validate.js +26 -0
- package/dist/src/core/furnace-apply-dry-run.d.ts +17 -0
- package/dist/src/core/furnace-apply-dry-run.js +105 -0
- package/dist/src/core/furnace-apply-ftl.d.ts +12 -0
- package/dist/src/core/furnace-apply-ftl.js +97 -1
- package/dist/src/core/furnace-apply-helpers.js +10 -80
- package/dist/src/core/furnace-registration.d.ts +24 -0
- package/dist/src/core/furnace-registration.js +56 -3
- package/dist/src/core/furnace-validate-registration.js +18 -0
- package/dist/src/core/mach-resource-shim.d.ts +58 -0
- package/dist/src/core/mach-resource-shim.js +290 -0
- package/dist/src/core/mach.d.ts +51 -10
- package/dist/src/core/mach.js +76 -25
- package/dist/src/core/manifest-helpers.d.ts +13 -0
- package/dist/src/core/manifest-helpers.js +29 -4
- package/dist/src/core/manifest-rules.d.ts +13 -3
- package/dist/src/core/manifest-rules.js +44 -17
- package/dist/src/core/patch-export.d.ts +10 -0
- package/dist/src/core/patch-export.js +23 -2
- package/dist/src/core/patch-lint-cross.d.ts +31 -0
- package/dist/src/core/patch-lint-cross.js +83 -63
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint-reexports.js +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-module.js +15 -2
- package/dist/src/core/register-result.d.ts +9 -0
- package/dist/src/core/register-scaffold.d.ts +55 -0
- package/dist/src/core/register-scaffold.js +146 -0
- package/dist/src/core/register-test-manifest.js +3 -1
- package/dist/src/core/register-xpcshell-test.d.ts +30 -0
- package/dist/src/core/register-xpcshell-test.js +96 -0
- package/dist/src/core/test-harness-crash.d.ts +32 -4
- package/dist/src/core/test-harness-crash.js +152 -14
- package/dist/src/core/token-dark-mode.d.ts +9 -0
- package/dist/src/core/token-dark-mode.js +1 -1
- package/dist/src/core/token-docs.d.ts +32 -0
- package/dist/src/core/token-docs.js +101 -0
- package/dist/src/core/token-manager.d.ts +8 -0
- package/dist/src/core/token-manager.js +77 -95
- package/dist/src/core/token-variant.d.ts +39 -0
- package/dist/src/core/token-variant.js +141 -0
- package/dist/src/core/typecheck.js +56 -28
- package/dist/src/core/xpcshell-appdir.d.ts +7 -0
- package/dist/src/core/xpcshell-appdir.js +12 -3
- package/dist/src/types/commands/options.d.ts +17 -0
- package/dist/src/types/config.d.ts +13 -0
- package/package.json +4 -4
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type ConflictReport } from '../../core/destructive.js';
|
|
9
9
|
import { buildProjectedManifest } from '../../core/patch-policy.js';
|
|
10
|
+
import type { PatchStagedForwardImport } from '../../types/commands/index.js';
|
|
10
11
|
import type { PatchCategory, PatchMetadata } from '../../types/commands/index.js';
|
|
11
12
|
import type { FireForgeConfig } from '../../types/config.js';
|
|
12
13
|
import { type PlacementPlan } from '../export-flow.js';
|
|
@@ -29,6 +30,14 @@ export interface SplitPlan {
|
|
|
29
30
|
description: string;
|
|
30
31
|
/** Patches (by current filename) whose staged-dependency owners re-point to the new patch. */
|
|
31
32
|
ownerRewrites: string[];
|
|
33
|
+
/**
|
|
34
|
+
* Staged forward-import declarations the split introduces, keyed by the
|
|
35
|
+
* importing patch's post-rename filename. These are the new forward edges
|
|
36
|
+
* from existing patches into the freshly-created patch (owner known); they
|
|
37
|
+
* are injected into the projected lint so dry-run matches the real gate,
|
|
38
|
+
* and persisted on commit so the real per-patch gate stays clean.
|
|
39
|
+
*/
|
|
40
|
+
stagedDependencyAdditions: Map<string, PatchStagedForwardImport[]>;
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
34
43
|
*
|
|
@@ -45,12 +54,19 @@ export declare function buildSplitDiff(engineDir: string, files: readonly string
|
|
|
45
54
|
export declare function findOwnerRewriteHolders(patches: readonly PatchMetadata[], sourceFilename: string, movedSet: ReadonlySet<string>): string[];
|
|
46
55
|
/** Rewrites split-affected owners on one manifest row. */
|
|
47
56
|
export declare function rewriteSplitOwners(patch: PatchMetadata, sourceFilename: string, movedSet: ReadonlySet<string>, newFilename: string): PatchMetadata;
|
|
57
|
+
/** Merges `decls` into a patch's `stagedDependencies.forwardImports` (no duplicates). */
|
|
58
|
+
export declare function mergeStagedForwardImports(patch: PatchMetadata, decls: readonly PatchStagedForwardImport[]): PatchMetadata;
|
|
48
59
|
/**
|
|
49
60
|
* Projects the full split (renumber + shrunken source + synthetic new
|
|
50
61
|
* patch + owner rewrites) through cross-patch lint, reporting only the
|
|
51
|
-
* regressions the split itself would introduce.
|
|
62
|
+
* regressions the split itself would introduce. Forward edges into the new
|
|
63
|
+
* patch are auto-declared (and the declarations returned) so the projection
|
|
64
|
+
* matches the real per-patch gate the split leaves behind.
|
|
52
65
|
*/
|
|
53
|
-
export declare function runProjectedSplitLint(patchesDir: string, plan: SplitPlan): Promise<
|
|
66
|
+
export declare function runProjectedSplitLint(patchesDir: string, plan: SplitPlan): Promise<{
|
|
67
|
+
conflicts: ConflictReport | null;
|
|
68
|
+
stagedDependencyAdditions: Map<string, PatchStagedForwardImport[]>;
|
|
69
|
+
}>;
|
|
54
70
|
/** Builds the projected manifest for policy enforcement. */
|
|
55
71
|
export declare function projectSplitManifest(manifest: {
|
|
56
72
|
version: 1;
|
|
@@ -10,7 +10,7 @@ import { join } from 'node:path';
|
|
|
10
10
|
import { getDiffForFilesAgainstHead } from '../../core/git-diff.js';
|
|
11
11
|
import { computeProjectedLintRegressions } from '../../core/lint-projection.js';
|
|
12
12
|
import { extractAffectedFiles } from '../../core/patch-apply.js';
|
|
13
|
-
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFilesInDiff, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
13
|
+
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, collectForwardImportEdges, detectNewFilesInDiff, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
14
14
|
import { rewriteStagedDependencyOwners } from '../../core/patch-manifest.js';
|
|
15
15
|
import { applyRenameMapToManifest, buildProjectedManifest } from '../../core/patch-policy.js';
|
|
16
16
|
import { buildPatchSourceMetadata } from '../../core/patch-source-metadata.js';
|
|
@@ -82,16 +82,12 @@ function buildEntryProjection(diff) {
|
|
|
82
82
|
}
|
|
83
83
|
return { diff, newFiles, modifiedFileAdditions: buildModifiedFileAdditionsFromDiff(diff) };
|
|
84
84
|
}
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
* patch + owner rewrites) through cross-patch lint, reporting only the
|
|
88
|
-
* regressions the split itself would introduce.
|
|
89
|
-
*/
|
|
90
|
-
export async function runProjectedSplitLint(patchesDir, plan) {
|
|
85
|
+
/** Builds the projected post-split queue entries (renumber + shrunken source + new patch). */
|
|
86
|
+
async function buildProjectedSplitEntries(patchesDir, plan) {
|
|
91
87
|
const movedSet = new Set(plan.movedFiles);
|
|
92
88
|
const ownerLookup = (old) => plan.placement.renameMap.get(old)?.newFilename;
|
|
93
89
|
const baseCtx = await buildPatchQueueContext(patchesDir);
|
|
94
|
-
const
|
|
90
|
+
const entries = baseCtx.entries.map((entry) => {
|
|
95
91
|
let metadata = entry.metadata;
|
|
96
92
|
if (metadata) {
|
|
97
93
|
metadata = rewriteStagedDependencyOwners(metadata, ownerLookup);
|
|
@@ -105,13 +101,90 @@ export async function runProjectedSplitLint(patchesDir, plan) {
|
|
|
105
101
|
return base;
|
|
106
102
|
return { ...base, ...buildEntryProjection(plan.remainingDiff) };
|
|
107
103
|
});
|
|
108
|
-
|
|
104
|
+
entries.push({
|
|
109
105
|
filename: plan.placement.newFilename,
|
|
110
106
|
order: plan.placement.insertionOrder,
|
|
111
107
|
metadata: null,
|
|
112
108
|
...buildEntryProjection(plan.movedDiff),
|
|
113
109
|
});
|
|
114
|
-
|
|
110
|
+
entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
111
|
+
return { baseCtx, entries };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Computes the staged forward-import declarations the split introduces:
|
|
115
|
+
* forward edges from existing patches into the freshly-created patch (its
|
|
116
|
+
* `creates` files are the moved files; the owner is the new patch, so it is
|
|
117
|
+
* known). Keyed by the importing patch's projected (post-rename) filename.
|
|
118
|
+
*
|
|
119
|
+
* These edges did not exist before the split (importer and imported file
|
|
120
|
+
* lived in the same patch), so they have no declaration yet — without this
|
|
121
|
+
* the projected lint flags them while the real per-patch gate would resolve
|
|
122
|
+
* them once declared. Auto-declaring keeps the two in lock-step and lets a
|
|
123
|
+
* sound split read as sound.
|
|
124
|
+
*/
|
|
125
|
+
function computeSplitStagedDependencyAdditions(projectedEntries, newFilename) {
|
|
126
|
+
const additions = new Map();
|
|
127
|
+
for (const edge of collectForwardImportEdges({ entries: projectedEntries })) {
|
|
128
|
+
if (edge.owner !== newFilename)
|
|
129
|
+
continue;
|
|
130
|
+
const decl = {
|
|
131
|
+
file: edge.sitePath,
|
|
132
|
+
specifier: edge.specifier,
|
|
133
|
+
creates: edge.creates,
|
|
134
|
+
owner: newFilename,
|
|
135
|
+
};
|
|
136
|
+
const list = additions.get(edge.entry) ?? [];
|
|
137
|
+
const dup = list.some((d) => d.file === decl.file &&
|
|
138
|
+
d.specifier === decl.specifier &&
|
|
139
|
+
d.creates === decl.creates &&
|
|
140
|
+
d.owner === decl.owner);
|
|
141
|
+
if (!dup)
|
|
142
|
+
list.push(decl);
|
|
143
|
+
additions.set(edge.entry, list);
|
|
144
|
+
}
|
|
145
|
+
return additions;
|
|
146
|
+
}
|
|
147
|
+
/** Merges `decls` into a patch's `stagedDependencies.forwardImports` (no duplicates). */
|
|
148
|
+
export function mergeStagedForwardImports(patch, decls) {
|
|
149
|
+
if (decls.length === 0)
|
|
150
|
+
return patch;
|
|
151
|
+
const existing = patch.stagedDependencies?.forwardImports ?? [];
|
|
152
|
+
const merged = [...existing];
|
|
153
|
+
for (const decl of decls) {
|
|
154
|
+
const dup = merged.some((d) => d.file === decl.file &&
|
|
155
|
+
d.specifier === decl.specifier &&
|
|
156
|
+
d.creates === decl.creates &&
|
|
157
|
+
(d.owner ?? '') === (decl.owner ?? ''));
|
|
158
|
+
if (!dup)
|
|
159
|
+
merged.push(decl);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
...patch,
|
|
163
|
+
stagedDependencies: { ...patch.stagedDependencies, forwardImports: merged },
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Injects the computed staged-dependency additions into projected entries' metadata. */
|
|
167
|
+
function injectStagedDependencyAdditions(entries, additions) {
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
const decls = additions.get(entry.filename);
|
|
170
|
+
if (!decls?.length || !entry.metadata)
|
|
171
|
+
continue;
|
|
172
|
+
entry.metadata = mergeStagedForwardImports(entry.metadata, decls);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Projects the full split (renumber + shrunken source + synthetic new
|
|
177
|
+
* patch + owner rewrites) through cross-patch lint, reporting only the
|
|
178
|
+
* regressions the split itself would introduce. Forward edges into the new
|
|
179
|
+
* patch are auto-declared (and the declarations returned) so the projection
|
|
180
|
+
* matches the real per-patch gate the split leaves behind.
|
|
181
|
+
*/
|
|
182
|
+
export async function runProjectedSplitLint(patchesDir, plan) {
|
|
183
|
+
const { baseCtx, entries: projectedEntries } = await buildProjectedSplitEntries(patchesDir, plan);
|
|
184
|
+
// Discover and auto-declare the forward edges this split introduces into
|
|
185
|
+
// the new patch, then inject them before linting so they resolve.
|
|
186
|
+
const stagedDependencyAdditions = computeSplitStagedDependencyAdditions(projectedEntries, plan.placement.newFilename);
|
|
187
|
+
injectStagedDependencyAdditions(projectedEntries, stagedDependencyAdditions);
|
|
115
188
|
const baselineIssues = lintPatchQueue(baseCtx).filter((i) => i.severity === 'error');
|
|
116
189
|
const projectedIssues = lintPatchQueue({ entries: projectedEntries }).filter((i) => i.severity === 'error');
|
|
117
190
|
const regressions = computeProjectedLintRegressions(baselineIssues, projectedIssues);
|
|
@@ -119,12 +192,13 @@ export async function runProjectedSplitLint(patchesDir, plan) {
|
|
|
119
192
|
warn(`Note: projected queue still has ${baselineIssues.length} pre-existing cross-patch ` +
|
|
120
193
|
'error(s) unrelated to this split. Run "fireforge verify" to list them.');
|
|
121
194
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
195
|
+
const conflicts = regressions.length === 0
|
|
196
|
+
? null
|
|
197
|
+
: {
|
|
198
|
+
reason: `split would introduce ${regressions.length} cross-patch lint error(s)`,
|
|
199
|
+
details: regressions.map((i) => `[${i.check}] ${i.file}: ${i.message}`),
|
|
200
|
+
};
|
|
201
|
+
return { conflicts, stagedDependencyAdditions };
|
|
128
202
|
}
|
|
129
203
|
/** Builds the projected manifest for policy enforcement. */
|
|
130
204
|
export function projectSplitManifest(manifest, plan, newMetadata) {
|
|
@@ -32,7 +32,7 @@ import { info, intro, outro, success, warn } from '../../utils/logger.js';
|
|
|
32
32
|
import { pickDefined } from '../../utils/options.js';
|
|
33
33
|
import { placementPlansEqual, resolvePlacementPlan } from '../export-flow.js';
|
|
34
34
|
import { runPatchLint } from '../export-shared.js';
|
|
35
|
-
import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, findOwnerRewriteHolders, projectSplitManifest, rewriteSplitOwners, runProjectedSplitLint, } from './split-plan.js';
|
|
35
|
+
import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, findOwnerRewriteHolders, mergeStagedForwardImports, projectSplitManifest, rewriteSplitOwners, runProjectedSplitLint, } from './split-plan.js';
|
|
36
36
|
/**
|
|
37
37
|
* Commits a confirmed split under the patch directory lock: renumber →
|
|
38
38
|
* write new patch body → write shrunken source body → single manifest
|
|
@@ -74,7 +74,13 @@ async function commitPatchSplit(patchesDir, plan, newMetadata, options) {
|
|
|
74
74
|
if (!fresh)
|
|
75
75
|
throw new GeneralError('Manifest disappeared during split commit.');
|
|
76
76
|
const updatedPatches = fresh.patches.map((patch) => {
|
|
77
|
-
|
|
77
|
+
let withOwners = rewriteSplitOwners(patch, effectiveSourceFilename, movedSet, plan.placement.newFilename);
|
|
78
|
+
// Persist the auto-declared forward edges into the new patch so the
|
|
79
|
+
// real per-patch gate stays clean (keyed by post-rename filename).
|
|
80
|
+
const decls = plan.stagedDependencyAdditions.get(withOwners.filename);
|
|
81
|
+
if (decls?.length) {
|
|
82
|
+
withOwners = mergeStagedForwardImports(withOwners, decls);
|
|
83
|
+
}
|
|
78
84
|
if (patch.filename !== effectiveSourceFilename)
|
|
79
85
|
return withOwners;
|
|
80
86
|
return { ...withOwners, filesAffected: plan.remainingFiles };
|
|
@@ -199,13 +205,16 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
|
|
|
199
205
|
name: options.name,
|
|
200
206
|
description: options.description ?? '',
|
|
201
207
|
ownerRewrites: findOwnerRewriteHolders(manifest.patches, source.filename, movedSet),
|
|
208
|
+
// Populated by runProjectedSplitLint below (forward edges into the new patch).
|
|
209
|
+
stagedDependencyAdditions: new Map(),
|
|
202
210
|
};
|
|
203
211
|
// Per-patch lint both projected bodies, threading the source patch's
|
|
204
212
|
// tier/lintIgnore so an intentional-advisory patch can still split.
|
|
205
213
|
const ignoreChecks = source.lintIgnore ? new Set(source.lintIgnore) : undefined;
|
|
206
214
|
await runPatchLint(paths.engine, remainingFiles, remainingDiff, config, options.skipLint, undefined, ignoreChecks, source.tier);
|
|
207
215
|
await runPatchLint(paths.engine, movedFiles, movedDiff, config, options.skipLint, undefined, ignoreChecks, source.tier);
|
|
208
|
-
const conflicts = await runProjectedSplitLint(paths.patches, plan);
|
|
216
|
+
const { conflicts, stagedDependencyAdditions } = await runProjectedSplitLint(paths.patches, plan);
|
|
217
|
+
plan.stagedDependencyAdditions = stagedDependencyAdditions;
|
|
209
218
|
const newMetadata = buildNewPatchMetadata(plan, config);
|
|
210
219
|
enforcePatchPolicy({
|
|
211
220
|
config,
|
|
@@ -1,3 +1,29 @@
|
|
|
1
1
|
import type { ReExportOptions } from '../types/commands/index.js';
|
|
2
|
+
/** Result of {@link normalizeReExportFilesPositionals}. */
|
|
3
|
+
export interface NormalizedReExportArguments {
|
|
4
|
+
patches: string[];
|
|
5
|
+
options: ReExportOptions;
|
|
6
|
+
/** Extra positionals that were folded into the `--files` list. */
|
|
7
|
+
foldedPaths: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Accepts the `export`-style space-separated path shape for
|
|
11
|
+
* `re-export <patch> --files` (0.34.0 field report): commander's
|
|
12
|
+
* `--files <paths>` consumes one comma-separated value, so
|
|
13
|
+
* `re-export 006-x --files a/b.js c/d.js` used to park `c/d.js` in the
|
|
14
|
+
* positional patches and fail with "--files operates on exactly one
|
|
15
|
+
* target patch" — pointing at the wrong argument. When every positional
|
|
16
|
+
* beyond the first looks like a file path, fold them into the file list.
|
|
17
|
+
*/
|
|
18
|
+
export declare function normalizeReExportFilesPositionals(patches: readonly string[], options: ReExportOptions): NormalizedReExportArguments;
|
|
19
|
+
/**
|
|
20
|
+
* Folding wrapper used by the command layer: applies
|
|
21
|
+
* {@link normalizeReExportFilesPositionals} and logs the folded paths so
|
|
22
|
+
* the operator sees exactly which positionals became file-list entries.
|
|
23
|
+
*/
|
|
24
|
+
export declare function applyReExportFilesPositionalFolding(patches: string[], options: ReExportOptions): {
|
|
25
|
+
patches: string[];
|
|
26
|
+
options: ReExportOptions;
|
|
27
|
+
};
|
|
2
28
|
/** Validates mutually exclusive `re-export` targeting and metadata options. */
|
|
3
29
|
export declare function validateReExportOptionCombinations(patches: readonly string[], options: ReExportOptions): void;
|
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { InvalidArgumentError } from '../errors/base.js';
|
|
3
|
+
import { info } from '../utils/logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Heuristic for `--files` positional folding: an extra positional that
|
|
6
|
+
* contains a path separator and is not a `.patch` filename is an engine
|
|
7
|
+
* file path, not a patch identifier.
|
|
8
|
+
*/
|
|
9
|
+
function looksLikeFilePath(value) {
|
|
10
|
+
return value.includes('/') && !value.endsWith('.patch');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Accepts the `export`-style space-separated path shape for
|
|
14
|
+
* `re-export <patch> --files` (0.34.0 field report): commander's
|
|
15
|
+
* `--files <paths>` consumes one comma-separated value, so
|
|
16
|
+
* `re-export 006-x --files a/b.js c/d.js` used to park `c/d.js` in the
|
|
17
|
+
* positional patches and fail with "--files operates on exactly one
|
|
18
|
+
* target patch" — pointing at the wrong argument. When every positional
|
|
19
|
+
* beyond the first looks like a file path, fold them into the file list.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeReExportFilesPositionals(patches, options) {
|
|
22
|
+
if (options.files === undefined || patches.length <= 1) {
|
|
23
|
+
return { patches: [...patches], options, foldedPaths: [] };
|
|
24
|
+
}
|
|
25
|
+
const [first, ...rest] = patches;
|
|
26
|
+
if (first !== undefined && rest.every(looksLikeFilePath)) {
|
|
27
|
+
return {
|
|
28
|
+
patches: [first],
|
|
29
|
+
options: { ...options, files: [...options.files, ...rest] },
|
|
30
|
+
foldedPaths: rest,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { patches: [...patches], options, foldedPaths: [] };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Folding wrapper used by the command layer: applies
|
|
37
|
+
* {@link normalizeReExportFilesPositionals} and logs the folded paths so
|
|
38
|
+
* the operator sees exactly which positionals became file-list entries.
|
|
39
|
+
*/
|
|
40
|
+
export function applyReExportFilesPositionalFolding(patches, options) {
|
|
41
|
+
const normalized = normalizeReExportFilesPositionals(patches, options);
|
|
42
|
+
if (normalized.foldedPaths.length > 0) {
|
|
43
|
+
info(`--files: treating ${normalized.foldedPaths.length} extra positional path(s) as part ` +
|
|
44
|
+
`of the file list: ${normalized.foldedPaths.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
return { patches: normalized.patches, options: normalized.options };
|
|
47
|
+
}
|
|
3
48
|
/** Validates mutually exclusive `re-export` targeting and metadata options. */
|
|
4
49
|
export function validateReExportOptionCombinations(patches, options) {
|
|
5
50
|
if (options.files !== undefined) {
|
|
@@ -10,7 +55,9 @@ export function validateReExportOptionCombinations(patches, options) {
|
|
|
10
55
|
throw new InvalidArgumentError('--files cannot be combined with --scan-files.', '--files');
|
|
11
56
|
}
|
|
12
57
|
if (patches.length !== 1) {
|
|
13
|
-
throw new InvalidArgumentError('--files operates on exactly one target patch. Pass a single patch identifier.'
|
|
58
|
+
throw new InvalidArgumentError('--files operates on exactly one target patch. Pass a single patch identifier. ' +
|
|
59
|
+
'File paths can be passed space-separated after --files (path-shaped extras are folded ' +
|
|
60
|
+
'into the file list automatically) or as one comma-separated --files value.', '--files');
|
|
14
61
|
}
|
|
15
62
|
}
|
|
16
63
|
if (options.scanFiles !== undefined) {
|
|
@@ -19,7 +19,7 @@ import { pickDefined } from '../utils/options.js';
|
|
|
19
19
|
import { runPatchLint } from './export-shared.js';
|
|
20
20
|
import { loadScanFilesAssignments, withDryRunReExportLock } from './re-export-bulk-scan.js';
|
|
21
21
|
import { reExportFilesInPlace } from './re-export-files.js';
|
|
22
|
-
import { validateReExportOptionCombinations } from './re-export-options.js';
|
|
22
|
+
import { applyReExportFilesPositionalFolding, validateReExportOptionCombinations, } from './re-export-options.js';
|
|
23
23
|
import { assertScanFileAdditionsHaveDiffHunks, confirmBroadScanAdditions, normalizeScanFiles, scanPatchFilesForReExport, } from './re-export-scan.js';
|
|
24
24
|
async function findMissingFiles(engineDir, files) {
|
|
25
25
|
const missingFiles = [];
|
|
@@ -286,6 +286,9 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
286
286
|
}
|
|
287
287
|
const isDryRun = options.dryRun === true;
|
|
288
288
|
intro(isDryRun ? 'FireForge Re-export (dry run)' : 'FireForge Re-export');
|
|
289
|
+
// Accept export-style space-separated paths after --files by folding
|
|
290
|
+
// path-shaped extra positionals into the file list (0.34.0 field report).
|
|
291
|
+
({ patches, options } = applyReExportFilesPositionalFolding(patches, options));
|
|
289
292
|
validateReExportOptionCombinations(patches, options);
|
|
290
293
|
const paths = getProjectPaths(projectRoot);
|
|
291
294
|
// Check if engine exists
|
|
@@ -43,7 +43,9 @@ export async function registerCommand(projectRoot, filePath, options = {}) {
|
|
|
43
43
|
throw new InvalidArgumentError(`File not found in engine: ${engineRelativePath}`, 'path');
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
const result = await registerFile(projectRoot, engineRelativePath, options.dryRun, options.after
|
|
46
|
+
const result = await registerFile(projectRoot, engineRelativePath, options.dryRun, options.after, {
|
|
47
|
+
...(options.createManifest !== undefined ? { createManifest: options.createManifest } : {}),
|
|
48
|
+
});
|
|
47
49
|
if (options.dryRun) {
|
|
48
50
|
// 2026-04-21 eval (Finding #8): dry-run always said "Would
|
|
49
51
|
// register" even when the rule's idempotency check already knew
|
|
@@ -59,6 +61,9 @@ export async function registerCommand(projectRoot, filePath, options = {}) {
|
|
|
59
61
|
info(`[dry-run] Would register ${engineRelativePath}`);
|
|
60
62
|
info(` manifest: ${result.manifest}`);
|
|
61
63
|
info(` entry: ${result.entry}`);
|
|
64
|
+
for (const action of result.scaffoldActions ?? []) {
|
|
65
|
+
info(` scaffold: ${action.manifest} — ${action.change}`);
|
|
66
|
+
}
|
|
62
67
|
if (result.previousEntry) {
|
|
63
68
|
info(` insert after: ${result.previousEntry}`);
|
|
64
69
|
}
|
|
@@ -80,6 +85,9 @@ export async function registerCommand(projectRoot, filePath, options = {}) {
|
|
|
80
85
|
}
|
|
81
86
|
const position = result.previousEntry ? ` (after ${result.previousEntry})` : '';
|
|
82
87
|
success(`Registered ${engineRelativePath} in ${result.manifest}${position}`);
|
|
88
|
+
for (const action of result.scaffoldActions ?? []) {
|
|
89
|
+
info(` scaffold: ${action.manifest} — ${action.change}`);
|
|
90
|
+
}
|
|
83
91
|
info("hint: Run 'fireforge build --ui' to make the new module available at runtime");
|
|
84
92
|
}
|
|
85
93
|
outro('Done');
|
|
@@ -91,6 +99,7 @@ export function registerRegister(program, { getProjectRoot, withErrorHandling })
|
|
|
91
99
|
.description('Register a file in the appropriate build manifest')
|
|
92
100
|
.option('--dry-run', 'Show what would be changed without writing')
|
|
93
101
|
.option('--after <entry>', 'Place entry after line containing this substring (instead of alphabetical)')
|
|
102
|
+
.option('--create-manifest', 'Scaffold a missing manifest (directory moz.build with EXTRA_JS_MODULES / XPCSHELL_TESTS_MANIFESTS, or xpcshell.toml) and wire the parent DIRS chain instead of failing with "Manifest not found"')
|
|
94
103
|
.action(withErrorHandling(async (path, options) => {
|
|
95
104
|
await registerCommand(getProjectRoot(), path, pickDefined(options));
|
|
96
105
|
}));
|
|
@@ -183,6 +183,18 @@ export function finalizeSingleRunOutcome(outcome, normalizedPaths, binaryName, p
|
|
|
183
183
|
if (outcome.verdict.kind === 'harness-crash' && outcome.verdict.signature) {
|
|
184
184
|
throw new GeneralError(buildHarnessCrashMessage(outcome.verdict.signature, outcome.attempts));
|
|
185
185
|
}
|
|
186
|
+
if (outcome.verdict.kind === 'tests-ran-ok') {
|
|
187
|
+
// The verdict is authoritative over the raw exit code: a completed
|
|
188
|
+
// green embedded summary overrides a non-zero exit caused by harness
|
|
189
|
+
// noise (field report: fully green --no-shard runs exited 1, forcing
|
|
190
|
+
// operators to parse embedded summaries by hand).
|
|
191
|
+
if (outcome.verdict.greenSummaryOverride) {
|
|
192
|
+
info(`Note: mach exited ${outcome.result.exitCode}, but the embedded suite summary completed ` +
|
|
193
|
+
'green (Unexpected results: 0). FireForge treats the run as passed; the non-zero exit ' +
|
|
194
|
+
'came from non-fatal harness noise (resource-monitor degradation / telemetry).');
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
186
198
|
if (outcome.verdict.kind === 'no-tests' && outcome.result.exitCode === 0) {
|
|
187
199
|
// The silent false green: exit 0 plus a "Passed: 0"-style summary with
|
|
188
200
|
// zero TEST-START lines must fail, not pass.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
|
+
import { readBuildBaseline } from '../core/build-baseline.js';
|
|
3
4
|
import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
4
5
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
|
-
import { buildArtifactMismatchMessage,
|
|
6
|
+
import { buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, runProtectedMachBuild, withBuildLock, } from '../core/mach.js';
|
|
6
7
|
import { assertMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
|
|
7
8
|
import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
|
|
8
|
-
import { buildHarnessCrashMessage
|
|
9
|
+
import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
|
|
9
10
|
import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
10
11
|
import { checkStaleBuildForTest, formatStaleBuildWarning } from '../core/test-stale-check.js';
|
|
11
12
|
import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
@@ -102,29 +103,35 @@ async function resolveLaunchablePathForTests(engineDir, binaryName, objDir) {
|
|
|
102
103
|
}
|
|
103
104
|
async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries) {
|
|
104
105
|
await withBuildLock(projectRoot, async () => {
|
|
105
|
-
|
|
106
|
+
// Pass the previous baseline exactly like `fireforge build` does, so
|
|
107
|
+
// auto-configure runs under the same conditions on both paths. The
|
|
108
|
+
// pre-test build must never invalidate more of the objdir than a plain
|
|
109
|
+
// `mach build faster` would (field incident: a failed pre-test build
|
|
110
|
+
// was followed by a ~64-minute full rebuild where an incremental one
|
|
111
|
+
// should have sufficed).
|
|
112
|
+
const previousBaseline = await readBuildBaseline(projectRoot);
|
|
113
|
+
await prepareBuildEnvironment(projectRoot, paths, projectConfig, { previousBaseline });
|
|
106
114
|
const s = spinner('Running incremental build...');
|
|
107
|
-
// The pre-test build
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (buildResult.exitCode === 0) {
|
|
116
|
-
s.stop('Build complete');
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
const signature = detectHarnessCrashSignature(`${buildResult.stdout}\n${buildResult.stderr}`);
|
|
120
|
-
if (signature && attempt < maxAttempts) {
|
|
115
|
+
// The pre-test build routes through the same protected mach dispatch as
|
|
116
|
+
// `fireforge build` / `build --ui`: in-venv resource-monitor guard plus
|
|
117
|
+
// the uniform recognized-crash retry budget, with a fresh mach process
|
|
118
|
+
// (and fresh guard install) per attempt. prepareBuildEnvironment runs
|
|
119
|
+
// ONCE, outside the retry loop — retries never re-run mach configure.
|
|
120
|
+
const result = await runProtectedMachBuild('faster', paths.engine, {
|
|
121
|
+
retries: harnessRetries,
|
|
122
|
+
onRetry: (signature, nextAttempt, maxAttempts) => {
|
|
121
123
|
s.message(`Pre-test build hit a harness crash (${signature.reason}); ` +
|
|
122
|
-
`retrying (attempt ${
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
`retrying (attempt ${nextAttempt} of ${maxAttempts})...`);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
if (result.exitCode === 0) {
|
|
128
|
+
s.stop('Build complete');
|
|
129
|
+
return;
|
|
127
130
|
}
|
|
131
|
+
s.error('Pre-test build failed');
|
|
132
|
+
throw new BuildError(result.crashSignature
|
|
133
|
+
? buildHarnessCrashMessage(result.crashSignature, result.attempts, 'mach build faster')
|
|
134
|
+
: 'Pre-test build failed', 'mach build faster');
|
|
128
135
|
});
|
|
129
136
|
}
|
|
130
137
|
function logTestSelection(normalizedPaths) {
|
|
@@ -74,12 +74,18 @@ export async function tokenAddCommand(projectRoot, tokenName, value, options) {
|
|
|
74
74
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
75
75
|
...(options.darkValue !== undefined ? { darkValue: options.darkValue } : {}),
|
|
76
76
|
...(options.createCategory === true ? { createCategory: true } : {}),
|
|
77
|
+
...(options.variant !== undefined ? { variant: options.variant } : {}),
|
|
77
78
|
dryRun: true,
|
|
78
79
|
});
|
|
79
80
|
info('[dry-run] Would add token:');
|
|
80
81
|
info(` Name: ${tokenName}`);
|
|
81
82
|
info(` Value: ${value}`);
|
|
82
|
-
|
|
83
|
+
if (options.variant !== undefined) {
|
|
84
|
+
info(` Variant: :root${options.variant}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
info(` Category: ${options.category}${options.createCategory === true ? ' (created if missing)' : ''}`);
|
|
88
|
+
}
|
|
83
89
|
info(` Mode: ${options.mode}`);
|
|
84
90
|
if (options.description)
|
|
85
91
|
info(` Description: ${options.description}`);
|
|
@@ -96,6 +102,7 @@ export async function tokenAddCommand(projectRoot, tokenName, value, options) {
|
|
|
96
102
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
97
103
|
...(options.darkValue !== undefined ? { darkValue: options.darkValue } : {}),
|
|
98
104
|
...(options.createCategory === true ? { createCategory: true } : {}),
|
|
105
|
+
...(options.variant !== undefined ? { variant: options.variant } : {}),
|
|
99
106
|
});
|
|
100
107
|
if (result.skipped) {
|
|
101
108
|
info(`Token ${tokenName} already exists (skipped)`);
|
|
@@ -151,6 +158,9 @@ export function registerToken(program, { getProjectRoot, withErrorHandling }) {
|
|
|
151
158
|
.makeOptionMandatory(true))
|
|
152
159
|
.option('--description <desc>', 'Comment description for the CSS file')
|
|
153
160
|
.option('--dark-value <val>', 'Dark mode value (required if mode is "override")')
|
|
161
|
+
.option('--variant <selector>', 'Attribute selector fragment routing the declaration into a `:root<selector>` block ' +
|
|
162
|
+
"(e.g. '[data-skin=precision]' or '[data-private]'); creates or updates the block. " +
|
|
163
|
+
'CSS-only — cannot be combined with --mode override.')
|
|
154
164
|
.option('--create-category', 'Declare the category banner in the tokens CSS if it does not exist yet')
|
|
155
165
|
.option('--dry-run', 'Show what would be changed without writing')
|
|
156
166
|
.action(withErrorHandling(async (tokenName, value, options) => {
|
|
@@ -162,6 +172,7 @@ export function registerToken(program, { getProjectRoot, withErrorHandling }) {
|
|
|
162
172
|
darkValue: options.darkValue,
|
|
163
173
|
dryRun: options.dryRun,
|
|
164
174
|
createCategory: options.createCategory,
|
|
175
|
+
variant: options.variant,
|
|
165
176
|
}),
|
|
166
177
|
});
|
|
167
178
|
}));
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* but do not fail. Designed for CI use.
|
|
17
17
|
*/
|
|
18
18
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
19
|
+
import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
|
|
20
|
+
import { findJsconfigPathsDrift, syncFurnaceJsconfigPaths } from '../core/furnace-jsconfig.js';
|
|
19
21
|
import { relativeForDisplay, runTypecheck } from '../core/typecheck.js';
|
|
20
22
|
import { GeneralError } from '../errors/base.js';
|
|
21
23
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
@@ -32,6 +34,11 @@ export function resolveTypecheckProjects(configTypecheck, override) {
|
|
|
32
34
|
return {
|
|
33
35
|
projects: [override],
|
|
34
36
|
...(configTypecheck?.extraShim !== undefined ? { extraShim: configTypecheck.extraShim } : {}),
|
|
37
|
+
// Preserve any per-project override for the targeted path so a one-off
|
|
38
|
+
// `--project` run honours its opt-out / shim override just like a full run.
|
|
39
|
+
...(configTypecheck?.projectOverrides !== undefined
|
|
40
|
+
? { projectOverrides: configTypecheck.projectOverrides }
|
|
41
|
+
: {}),
|
|
35
42
|
};
|
|
36
43
|
}
|
|
37
44
|
if (!configTypecheck) {
|
|
@@ -64,10 +71,38 @@ export async function typecheckCommand(projectRoot, options) {
|
|
|
64
71
|
getProjectPaths(projectRoot);
|
|
65
72
|
const config = await loadConfig(projectRoot);
|
|
66
73
|
const cfg = resolveTypecheckProjects(config.typecheck, options.project);
|
|
74
|
+
// Regenerate a stale Furnace-managed jsconfig before running: the generated
|
|
75
|
+
// `compilerOptions.paths` shim drifts when components are added/renamed
|
|
76
|
+
// without a re-deploy, and a stale shim reports type errors in files the
|
|
77
|
+
// session never touched (e.g. 47 phantom errors from an out-of-date
|
|
78
|
+
// chrome-module mapping). Run the same reconciler `furnace deploy`/`sync`
|
|
79
|
+
// use so typecheck checks against the current workspace.
|
|
80
|
+
await regenerateStaleGeneratedJsconfig(projectRoot);
|
|
67
81
|
info(`Running typecheck across ${String(cfg.projects.length)} project(s): ${cfg.projects.join(', ')}`);
|
|
68
82
|
const results = await runTypecheck(projectRoot, cfg);
|
|
69
83
|
reportResults(projectRoot, results);
|
|
70
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Staleness-checks and regenerates the Furnace-managed jsconfig
|
|
87
|
+
* (`furnace.json` → `typecheckJsconfig`) before typecheck runs. No-op when
|
|
88
|
+
* the project has no furnace.json or no `typecheckJsconfig` is configured.
|
|
89
|
+
* A missing `typecheckJsconfig` file surfaces the reconciler's own clear
|
|
90
|
+
* error rather than producing phantom type diagnostics.
|
|
91
|
+
*/
|
|
92
|
+
async function regenerateStaleGeneratedJsconfig(projectRoot) {
|
|
93
|
+
if (!(await furnaceConfigExists(projectRoot)))
|
|
94
|
+
return;
|
|
95
|
+
const furnaceConfig = await loadFurnaceConfig(projectRoot);
|
|
96
|
+
if (!furnaceConfig.typecheckJsconfig)
|
|
97
|
+
return;
|
|
98
|
+
const drift = await findJsconfigPathsDrift(projectRoot, furnaceConfig);
|
|
99
|
+
if (!drift.changed)
|
|
100
|
+
return;
|
|
101
|
+
info(`Regenerating stale generated jsconfig ${furnaceConfig.typecheckJsconfig} before typecheck ` +
|
|
102
|
+
`(+${String(drift.added.length)} added, ~${String(drift.updated.length)} updated, ` +
|
|
103
|
+
`-${String(drift.pruned.length)} pruned).`);
|
|
104
|
+
await syncFurnaceJsconfigPaths(projectRoot, furnaceConfig);
|
|
105
|
+
}
|
|
71
106
|
/**
|
|
72
107
|
* Prints all issues, computes the per-project + total counts, and
|
|
73
108
|
* throws on errors. Extracted so it can be exercised directly by
|
|
@@ -14,9 +14,23 @@ import { applyAllComponents } from './furnace-apply.js';
|
|
|
14
14
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
|
|
15
15
|
import { runFurnaceMutation } from './furnace-operation.js';
|
|
16
16
|
import { cleanStories } from './furnace-stories.js';
|
|
17
|
-
import { generateMozconfig,
|
|
17
|
+
import { generateMozconfig, runMachCapture } from './mach.js';
|
|
18
18
|
/** Path fragments of files whose edits invalidate the recursive-make backend. */
|
|
19
19
|
const BACKEND_INVALIDATING_SUFFIXES = ['moz.build', 'moz.configure', 'Makefile.in'];
|
|
20
|
+
/**
|
|
21
|
+
* Extracts the tail of captured `mach configure` output so the underlying
|
|
22
|
+
* mozbuild failure (e.g. `mozbuild.util.UnsortedError: ... is not in sorted
|
|
23
|
+
* order`) is carried into the thrown `BuildError` instead of being reduced to
|
|
24
|
+
* a bare exit code. mozbuild writes the error and its traceback to stderr;
|
|
25
|
+
* stdout is included as a fallback for shells that interleave the streams.
|
|
26
|
+
* Returns an empty string when nothing useful was captured.
|
|
27
|
+
*/
|
|
28
|
+
function extractMachConfigureError(result) {
|
|
29
|
+
const combined = `${result.stderr}\n${result.stdout}`.trim();
|
|
30
|
+
if (!combined)
|
|
31
|
+
return '';
|
|
32
|
+
return combined.split('\n').slice(-40).join('\n').trim();
|
|
33
|
+
}
|
|
20
34
|
/**
|
|
21
35
|
* Returns true when the file path matches a pattern that forces
|
|
22
36
|
* `mach configure` to regenerate the backend. Exported for testing.
|
|
@@ -65,10 +79,16 @@ export async function prepareBuildEnvironment(projectRoot, paths, config, option
|
|
|
65
79
|
info(`Backend command: mach configure`);
|
|
66
80
|
const configureSpinner = spinner('Running mach configure...');
|
|
67
81
|
try {
|
|
68
|
-
const
|
|
82
|
+
const captured = await runMachCapture(['configure'], paths.engine);
|
|
83
|
+
const exitCode = captured.exitCode;
|
|
69
84
|
if (exitCode !== 0) {
|
|
70
85
|
configureSpinner.error(`mach configure failed with exit code ${exitCode}`);
|
|
71
|
-
|
|
86
|
+
// Surface the underlying mozbuild error (e.g. UnsortedError) instead
|
|
87
|
+
// of a bare exit code — the generic message hid the actual cause.
|
|
88
|
+
const detail = extractMachConfigureError(captured);
|
|
89
|
+
throw new BuildError(`Backend regeneration failed: mach configure exited with code ${exitCode}.` +
|
|
90
|
+
(detail ? `\n\nmach configure output (tail):\n${detail}` : '') +
|
|
91
|
+
'\n\nBuild stopped because continuing would hide the real configure failure.', 'mach configure');
|
|
72
92
|
}
|
|
73
93
|
else {
|
|
74
94
|
configureSpinner.stop('Backend regenerated successfully (mach configure exit code 0)');
|