@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/src/commands/build.js +9 -1
  3. package/dist/src/commands/doctor-furnace-jar.d.ts +16 -0
  4. package/dist/src/commands/doctor-furnace-jar.js +47 -0
  5. package/dist/src/commands/doctor-furnace.js +2 -0
  6. package/dist/src/commands/export-flow.js +4 -1
  7. package/dist/src/commands/export-placement-gate.js +4 -1
  8. package/dist/src/commands/export.js +61 -4
  9. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +20 -0
  10. package/dist/src/commands/furnace/chrome-doc-templates.js +52 -0
  11. package/dist/src/commands/furnace/chrome-doc.d.ts +10 -0
  12. package/dist/src/commands/furnace/chrome-doc.js +31 -4
  13. package/dist/src/commands/furnace/create-browser-test.d.ts +30 -0
  14. package/dist/src/commands/furnace/create-browser-test.js +180 -0
  15. package/dist/src/commands/furnace/create-mochikit.js +11 -4
  16. package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
  17. package/dist/src/commands/furnace/create-xpcshell.js +38 -12
  18. package/dist/src/commands/furnace/create.js +7 -114
  19. package/dist/src/commands/furnace/index.js +6 -1
  20. package/dist/src/commands/furnace/scan.d.ts +1 -0
  21. package/dist/src/commands/furnace/scan.js +57 -27
  22. package/dist/src/commands/furnace/validate.js +17 -1
  23. package/dist/src/commands/re-export-options.d.ts +26 -0
  24. package/dist/src/commands/re-export-options.js +48 -1
  25. package/dist/src/commands/re-export.js +4 -1
  26. package/dist/src/commands/register.js +10 -1
  27. package/dist/src/commands/test-diagnose.js +12 -0
  28. package/dist/src/commands/test.js +29 -22
  29. package/dist/src/core/furnace-registration.d.ts +24 -0
  30. package/dist/src/core/furnace-registration.js +56 -3
  31. package/dist/src/core/furnace-validate-registration.js +18 -0
  32. package/dist/src/core/mach-resource-shim.d.ts +53 -16
  33. package/dist/src/core/mach-resource-shim.js +249 -51
  34. package/dist/src/core/mach.d.ts +51 -10
  35. package/dist/src/core/mach.js +76 -32
  36. package/dist/src/core/manifest-helpers.d.ts +13 -0
  37. package/dist/src/core/manifest-helpers.js +1 -1
  38. package/dist/src/core/manifest-rules.d.ts +13 -3
  39. package/dist/src/core/manifest-rules.js +44 -17
  40. package/dist/src/core/patch-export.d.ts +10 -0
  41. package/dist/src/core/patch-export.js +23 -2
  42. package/dist/src/core/register-module.d.ts +1 -1
  43. package/dist/src/core/register-module.js +15 -2
  44. package/dist/src/core/register-result.d.ts +9 -0
  45. package/dist/src/core/register-scaffold.d.ts +55 -0
  46. package/dist/src/core/register-scaffold.js +146 -0
  47. package/dist/src/core/register-test-manifest.js +3 -1
  48. package/dist/src/core/register-xpcshell-test.d.ts +30 -0
  49. package/dist/src/core/register-xpcshell-test.js +96 -0
  50. package/dist/src/core/test-harness-crash.d.ts +26 -1
  51. package/dist/src/core/test-harness-crash.js +121 -11
  52. package/dist/src/core/xpcshell-appdir.d.ts +7 -0
  53. package/dist/src/core/xpcshell-appdir.js +12 -3
  54. package/dist/src/types/commands/options.d.ts +12 -0
  55. 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.', '--files');
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, buildUI, hasBuildArtifacts, hasRunnableBundle, withBuildLock, } from '../core/mach.js';
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, detectHarnessCrashSignature, } from '../core/test-harness-crash.js';
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
- await prepareBuildEnvironment(projectRoot, paths, projectConfig);
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 runs through mach too, so the same resource-monitor
108
- // startup crash that aborts `mach test` can abort `mach build faster`.
109
- // Run the harness-crash classifier over the build output and retry within
110
- // the same `--harness-retries` budget rather than hard-failing with a
111
- // bare "Pre-test build failed" (field report E2).
112
- const maxAttempts = Math.max(1, harnessRetries + 1);
113
- for (let attempt = 1;; attempt += 1) {
114
- const buildResult = await buildUI(paths.engine);
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 ${attempt + 1} of ${maxAttempts})...`);
123
- continue;
124
- }
125
- s.error('Pre-test build failed');
126
- throw new BuildError(signature ? buildHarnessCrashMessage(signature, attempt) : 'Pre-test build failed', 'mach build faster');
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
- // Use a regex with word boundary so "moz-card" does not match "moz-card-group".
228
- const pattern = new RegExp(`content/global/elements/${escapeForRegex(tagName)}\\.`);
229
- const filtered = lines.filter((line) => !pattern.test(line));
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 shim for `mach build`.
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`). On some hosts (psutil vs Darwin 27)
6
- * that raises `RuntimeError: host_statistics64(HOST_VM_INFO64) syscall
7
- * failed`, which aborts `mach build` / `mach build faster` before any work
8
- * happens — so a raw `./mach build faster` dies while the build never
9
- * reaches the compiler. The suite-specific test commands sidestep the
10
- * monitor entirely, but the build commands have no such escape.
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
- * FireForge writes a tiny `sitecustomize.py` and points the mach
13
- * subprocess's `PYTHONPATH` at it. Python imports `sitecustomize` at
14
- * interpreter startup (before mach runs), so the shim wraps
15
- * `psutil.virtual_memory` / `swap_memory` to downgrade the host syscall
16
- * failure into a non-fatal `UserWarning` and return a zeroed reading. The
17
- * build then proceeds regardless of which mach entry point spawned it. When
18
- * psutil works normally the wrapper is a transparent pass-through.
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
- /** Ensures the shim exists and returns the mach `env` overlay that enables it. */
21
- export declare function resolveMachBuildEnv(): Promise<Record<string, string>>;
58
+ export declare function installMachResourceGuard(engineDir: string): Promise<MachResourceGuardInstallation>;