@hominis/fireforge 0.33.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 +14 -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/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/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 +53 -16
- package/dist/src/core/mach-resource-shim.js +249 -51
- package/dist/src/core/mach.d.ts +51 -10
- package/dist/src/core/mach.js +76 -32
- package/dist/src/core/manifest-helpers.d.ts +13 -0
- package/dist/src/core/manifest-helpers.js +1 -1
- 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/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 +26 -1
- package/dist/src/core/test-harness-crash.js +121 -11
- 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 +12 -0
- package/package.json +3 -3
|
@@ -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) {
|
|
@@ -71,6 +71,30 @@ export declare function removeLocaleFtlJarMnEntry(engineDir: string, jarMnRelPat
|
|
|
71
71
|
* @param tagName - Custom element tag name whose entries should be removed
|
|
72
72
|
*/
|
|
73
73
|
export declare function removeJarMnEntries(engineDir: string, tagName: string): Promise<void>;
|
|
74
|
+
/** A jar.mn widget registration line whose source file no longer exists. */
|
|
75
|
+
export interface StaleJarMnEntry {
|
|
76
|
+
/** Component tag name from the `(widgets/<tag>/<file>)` source mapping. */
|
|
77
|
+
tagName: string;
|
|
78
|
+
/** File name from the source mapping. */
|
|
79
|
+
fileName: string;
|
|
80
|
+
/** The full (trimmed) jar.mn line. */
|
|
81
|
+
line: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Scans jar.mn for widget registration lines `(widgets/<tag>/<file>)`
|
|
85
|
+
* whose workspace source file no longer exists (0.34.0 field report: a
|
|
86
|
+
* renamed component helper left the old line pointing at a deleted file,
|
|
87
|
+
* and every build failed at packaging). Only tags in `managedTags`
|
|
88
|
+
* (furnace-managed custom components) are inspected so upstream lines are
|
|
89
|
+
* never touched.
|
|
90
|
+
*/
|
|
91
|
+
export declare function findStaleJarMnEntries(engineDir: string, customDir: string, managedTags: readonly string[]): Promise<StaleJarMnEntry[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Removes every stale widget registration line found by
|
|
94
|
+
* {@link findStaleJarMnEntries}. Returns the removed entries. Used by
|
|
95
|
+
* `furnace validate --fix` and `doctor --repair-furnace`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function pruneStaleJarMnEntries(engineDir: string, customDir: string, managedTags: readonly string[]): Promise<StaleJarMnEntry[]>;
|
|
74
98
|
/**
|
|
75
99
|
* Validates that jar.mn entries *could* be added without writing anything.
|
|
76
100
|
* Used by dry-run to surface structural problems early.
|
|
@@ -224,14 +224,67 @@ export async function removeJarMnEntries(engineDir, tagName) {
|
|
|
224
224
|
}
|
|
225
225
|
let content = await readText(filePath);
|
|
226
226
|
const lines = content.split('\n');
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
// Match by the SOURCE MAPPING segment `(widgets/<tagName>/...)` so every
|
|
228
|
+
// line the component registered is removed regardless of the target
|
|
229
|
+
// basename — a helper `.mjs` whose name does not start with the tag
|
|
230
|
+
// (e.g. a renamed `foo-utils.mjs`) used to survive the remove pass and
|
|
231
|
+
// leave a stale registration that broke packaging (0.34.0 field
|
|
232
|
+
// report). The legacy target-path match is kept as an OR for lines
|
|
233
|
+
// written by older FireForge versions without a source mapping. Word
|
|
234
|
+
// boundaries keep "moz-card" from matching "moz-card-group".
|
|
235
|
+
const sourcePattern = new RegExp(`\\(widgets/${escapeForRegex(tagName)}/`);
|
|
236
|
+
const legacyTargetPattern = new RegExp(`content/global/elements/${escapeForRegex(tagName)}\\.`);
|
|
237
|
+
const filtered = lines.filter((line) => !sourcePattern.test(line) && !legacyTargetPattern.test(line));
|
|
230
238
|
if (filtered.length === lines.length)
|
|
231
239
|
return;
|
|
232
240
|
content = filtered.join('\n');
|
|
233
241
|
await writeText(filePath, content);
|
|
234
242
|
}
|
|
243
|
+
const WIDGET_SOURCE_MAPPING_PATTERN = /\(widgets\/([^/)]+)\/([^)]+)\)/;
|
|
244
|
+
/**
|
|
245
|
+
* Scans jar.mn for widget registration lines `(widgets/<tag>/<file>)`
|
|
246
|
+
* whose workspace source file no longer exists (0.34.0 field report: a
|
|
247
|
+
* renamed component helper left the old line pointing at a deleted file,
|
|
248
|
+
* and every build failed at packaging). Only tags in `managedTags`
|
|
249
|
+
* (furnace-managed custom components) are inspected so upstream lines are
|
|
250
|
+
* never touched.
|
|
251
|
+
*/
|
|
252
|
+
export async function findStaleJarMnEntries(engineDir, customDir, managedTags) {
|
|
253
|
+
const filePath = join(engineDir, JAR_MN);
|
|
254
|
+
if (!(await pathExists(filePath)))
|
|
255
|
+
return [];
|
|
256
|
+
const managed = new Set(managedTags);
|
|
257
|
+
const stale = [];
|
|
258
|
+
for (const line of (await readText(filePath)).split('\n')) {
|
|
259
|
+
const match = WIDGET_SOURCE_MAPPING_PATTERN.exec(line);
|
|
260
|
+
if (!match)
|
|
261
|
+
continue;
|
|
262
|
+
const tagName = match[1] ?? '';
|
|
263
|
+
const fileName = match[2] ?? '';
|
|
264
|
+
if (!managed.has(tagName))
|
|
265
|
+
continue;
|
|
266
|
+
if (!(await pathExists(join(customDir, tagName, fileName)))) {
|
|
267
|
+
stale.push({ tagName, fileName, line: line.trim() });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return stale;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Removes every stale widget registration line found by
|
|
274
|
+
* {@link findStaleJarMnEntries}. Returns the removed entries. Used by
|
|
275
|
+
* `furnace validate --fix` and `doctor --repair-furnace`.
|
|
276
|
+
*/
|
|
277
|
+
export async function pruneStaleJarMnEntries(engineDir, customDir, managedTags) {
|
|
278
|
+
const stale = await findStaleJarMnEntries(engineDir, customDir, managedTags);
|
|
279
|
+
if (stale.length === 0)
|
|
280
|
+
return [];
|
|
281
|
+
const filePath = join(engineDir, JAR_MN);
|
|
282
|
+
const staleLines = new Set(stale.map((entry) => entry.line));
|
|
283
|
+
const lines = (await readText(filePath)).split('\n');
|
|
284
|
+
const filtered = lines.filter((line) => !staleLines.has(line.trim()));
|
|
285
|
+
await writeText(filePath, filtered.join('\n'));
|
|
286
|
+
return stale;
|
|
287
|
+
}
|
|
235
288
|
/**
|
|
236
289
|
* Validates that jar.mn entries *could* be added without writing anything.
|
|
237
290
|
* Used by dry-run to surface structural problems early.
|
|
@@ -10,6 +10,7 @@ import { getProjectPaths, loadConfig } from './config.js';
|
|
|
10
10
|
import { getFurnacePaths } from './furnace-config.js';
|
|
11
11
|
import { CUSTOM_ELEMENTS_JS, FTL_DIR, JAR_MN } from './furnace-constants.js';
|
|
12
12
|
import { expandCssFragments, listFragmentIncludes } from './furnace-css-fragments.js';
|
|
13
|
+
import { findStaleJarMnEntries } from './furnace-registration.js';
|
|
13
14
|
import { isTagInCorrectCustomElementsPlacement } from './furnace-registration-validate.js';
|
|
14
15
|
import { getTokensCssPath } from './token-manager.js';
|
|
15
16
|
/**
|
|
@@ -210,6 +211,23 @@ export async function validateJarMnEntries(root, config) {
|
|
|
210
211
|
});
|
|
211
212
|
}
|
|
212
213
|
}
|
|
214
|
+
// Stale registrations: lines pointing at component files that no longer
|
|
215
|
+
// exist in the workspace (left behind by a rename/delete under an older
|
|
216
|
+
// FireForge). These break `mach build` at packaging ("File ... not
|
|
217
|
+
// found"), so they are errors and `--fix` prunes them (0.34.0 field
|
|
218
|
+
// report: both validate --fix and doctor --repair-furnace reported
|
|
219
|
+
// success without pruning).
|
|
220
|
+
const staleEntries = await findStaleJarMnEntries(engineDir, furnacePaths.customDir, Object.keys(config.custom));
|
|
221
|
+
for (const stale of staleEntries) {
|
|
222
|
+
issues.push({
|
|
223
|
+
component: stale.tagName,
|
|
224
|
+
severity: 'error',
|
|
225
|
+
check: 'stale-jar-registration',
|
|
226
|
+
message: `jar.mn registers ${stale.fileName} for ${stale.tagName}, but the source file no longer exists ` +
|
|
227
|
+
`(stale line: "${stale.line}"). Packaging will fail until the line is removed — run ` +
|
|
228
|
+
'"fireforge furnace validate --fix" or "fireforge doctor --repair-furnace" to prune it.',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
213
231
|
return issues;
|
|
214
232
|
}
|
|
215
233
|
/**
|
|
@@ -1,21 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resource-monitor degrade
|
|
2
|
+
* Resource-monitor degrade guard for mach dispatches.
|
|
3
3
|
*
|
|
4
4
|
* mozbuild's build resource monitor calls `psutil.virtual_memory()` at
|
|
5
|
-
* startup (`start_resource_recording`)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* startup (`start_resource_recording`) and again later from
|
|
6
|
+
* `mozbuild.base._run_make`. On some hosts (psutil vs Darwin 27) those
|
|
7
|
+
* raise `RuntimeError: host_statistics64(HOST_VM_INFO64) syscall failed`,
|
|
8
|
+
* and a `SystemResourceMonitor.__init__` that fails partway leaves an
|
|
9
|
+
* instance without `poll_interval`, so later polling dies with
|
|
10
|
+
* `AttributeError: ... poll_interval`. Any of these aborts `mach build` /
|
|
11
|
+
* `mach build faster` before the compiler ever runs.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* 0.33.0 injected a `sitecustomize.py` via the mach subprocess PYTHONPATH.
|
|
14
|
+
* Field report (0.34.0 cycle): mach re-execs itself into its private
|
|
15
|
+
* virtualenv and drops PYTHONPATH, so the env-var route never loads and
|
|
16
|
+
* the crash family escaped on every protected entry point. FireForge now
|
|
17
|
+
* owns the guard in-process on its mach dispatches by installing a
|
|
18
|
+
* `fireforge_mach_guard.pth` + module pair directly into every discovered
|
|
19
|
+
* mach virtualenv site-packages directory: `.pth` files execute at
|
|
20
|
+
* interpreter startup from the venv's own site-packages, which survives
|
|
21
|
+
* the re-exec. The PYTHONPATH `sitecustomize.py` is retained only as a
|
|
22
|
+
* belt-and-suspenders for the pre-venv bootstrap phase (first build, no
|
|
23
|
+
* `_virtualenvs` on disk yet).
|
|
24
|
+
*
|
|
25
|
+
* The guard itself covers the whole crash family:
|
|
26
|
+
* - wraps `psutil.virtual_memory` / `swap_memory` / `cpu_percent` /
|
|
27
|
+
* `cpu_times` / `disk_io_counters` module-wide, so every call site —
|
|
28
|
+
* including the direct `psutil.virtual_memory()` in
|
|
29
|
+
* `mozbuild.base._run_make` — degrades to a `UserWarning` and a zeroed
|
|
30
|
+
* reading instead of aborting;
|
|
31
|
+
* - guards `SystemResourceMonitor` CONSTRUCTION via an import hook:
|
|
32
|
+
* `poll_interval` is pre-populated before the real `__init__` runs, a
|
|
33
|
+
* failing `__init__` marks the instance degraded instead of raising,
|
|
34
|
+
* and every monitor method no-ops on a degraded instance — so a
|
|
35
|
+
* partially-constructed monitor can never surface the
|
|
36
|
+
* `AttributeError: poll_interval` variant.
|
|
37
|
+
*/
|
|
38
|
+
/** Result of installing the mach resource guard before a dispatch. */
|
|
39
|
+
export interface MachResourceGuardInstallation {
|
|
40
|
+
/**
|
|
41
|
+
* Env overlay for the mach subprocess: the PYTHONPATH sitecustomize
|
|
42
|
+
* fallback that covers the pre-venv bootstrap phase. Merged over
|
|
43
|
+
* `process.env` by the exec layer.
|
|
44
|
+
*/
|
|
45
|
+
env: Record<string, string>;
|
|
46
|
+
/** site-packages directories the in-venv guard was written into. */
|
|
47
|
+
sitePackagesDirs: string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Installs the resource-monitor degrade guard for a mach dispatch:
|
|
51
|
+
* `fireforge_mach_guard.pth` + `fireforge_mach_guard.py` into every
|
|
52
|
+
* discovered mach virtualenv site-packages (survives mach's venv re-exec),
|
|
53
|
+
* plus the PYTHONPATH sitecustomize fallback for the pre-venv bootstrap
|
|
54
|
+
* phase. Idempotent and re-run before EVERY protected dispatch (including
|
|
55
|
+
* each retry) so a venv created by a crashed first attempt is guarded on
|
|
56
|
+
* the next one instead of re-dying on the same wedged state.
|
|
19
57
|
*/
|
|
20
|
-
|
|
21
|
-
export declare function resolveMachBuildEnv(): Promise<Record<string, string>>;
|
|
58
|
+
export declare function installMachResourceGuard(engineDir: string): Promise<MachResourceGuardInstallation>;
|