@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,92 +1,290 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Resource-monitor degrade shim for `mach build`.
3
+ * Resource-monitor degrade guard for mach dispatches.
4
4
  *
5
5
  * mozbuild's build resource monitor calls `psutil.virtual_memory()` at
6
- * startup (`start_resource_recording`). On some hosts (psutil vs Darwin 27)
7
- * that raises `RuntimeError: host_statistics64(HOST_VM_INFO64) syscall
8
- * failed`, which aborts `mach build` / `mach build faster` before any work
9
- * happens — so a raw `./mach build faster` dies while the build never
10
- * reaches the compiler. The suite-specific test commands sidestep the
11
- * monitor entirely, but the build commands have no such escape.
6
+ * startup (`start_resource_recording`) and again later from
7
+ * `mozbuild.base._run_make`. On some hosts (psutil vs Darwin 27) those
8
+ * raise `RuntimeError: host_statistics64(HOST_VM_INFO64) syscall failed`,
9
+ * and a `SystemResourceMonitor.__init__` that fails partway leaves an
10
+ * instance without `poll_interval`, so later polling dies with
11
+ * `AttributeError: ... poll_interval`. Any of these aborts `mach build` /
12
+ * `mach build faster` before the compiler ever runs.
12
13
  *
13
- * FireForge writes a tiny `sitecustomize.py` and points the mach
14
- * subprocess's `PYTHONPATH` at it. Python imports `sitecustomize` at
15
- * interpreter startup (before mach runs), so the shim wraps
16
- * `psutil.virtual_memory` / `swap_memory` to downgrade the host syscall
17
- * failure into a non-fatal `UserWarning` and return a zeroed reading. The
18
- * build then proceeds regardless of which mach entry point spawned it. When
19
- * psutil works normally the wrapper is a transparent pass-through.
14
+ * 0.33.0 injected a `sitecustomize.py` via the mach subprocess PYTHONPATH.
15
+ * Field report (0.34.0 cycle): mach re-execs itself into its private
16
+ * virtualenv and drops PYTHONPATH, so the env-var route never loads and
17
+ * the crash family escaped on every protected entry point. FireForge now
18
+ * owns the guard in-process on its mach dispatches by installing a
19
+ * `fireforge_mach_guard.pth` + module pair directly into every discovered
20
+ * mach virtualenv site-packages directory: `.pth` files execute at
21
+ * interpreter startup from the venv's own site-packages, which survives
22
+ * the re-exec. The PYTHONPATH `sitecustomize.py` is retained only as a
23
+ * belt-and-suspenders for the pre-venv bootstrap phase (first build, no
24
+ * `_virtualenvs` on disk yet).
25
+ *
26
+ * The guard itself covers the whole crash family:
27
+ * - wraps `psutil.virtual_memory` / `swap_memory` / `cpu_percent` /
28
+ * `cpu_times` / `disk_io_counters` module-wide, so every call site —
29
+ * including the direct `psutil.virtual_memory()` in
30
+ * `mozbuild.base._run_make` — degrades to a `UserWarning` and a zeroed
31
+ * reading instead of aborting;
32
+ * - guards `SystemResourceMonitor` CONSTRUCTION via an import hook:
33
+ * `poll_interval` is pre-populated before the real `__init__` runs, a
34
+ * failing `__init__` marks the instance degraded instead of raising,
35
+ * and every monitor method no-ops on a degraded instance — so a
36
+ * partially-constructed monitor can never surface the
37
+ * `AttributeError: poll_interval` variant.
20
38
  */
21
- import { tmpdir } from 'node:os';
39
+ import { readdir } from 'node:fs/promises';
40
+ import { homedir, tmpdir } from 'node:os';
22
41
  import { delimiter, join } from 'node:path';
23
- import { ensureDir, writeText } from '../utils/fs.js';
24
- /** Directory name (under the OS temp dir) holding the generated shim. */
42
+ import { toError } from '../utils/errors.js';
43
+ import { ensureDir, pathExists, writeText } from '../utils/fs.js';
44
+ import { verbose } from '../utils/logger.js';
45
+ /** Directory name (under the OS temp dir) holding the PYTHONPATH fallback. */
25
46
  const RESOURCE_SHIM_DIRNAME = 'fireforge-mach-resource-shim';
47
+ /** Basename (sans extension) of the in-venv guard module. */
48
+ const GUARD_MODULE_NAME = 'fireforge_mach_guard';
26
49
  /**
27
- * `sitecustomize.py` body. Defensive: a missing/broken psutil leaves mach
28
- * untouched, and the wrapper only intercepts the failure path.
50
+ * Python guard body, importable both as the in-venv `fireforge_mach_guard`
51
+ * module (loaded by the sibling `.pth` at interpreter startup) and as the
52
+ * PYTHONPATH `sitecustomize` fallback. Defensive throughout: a missing or
53
+ * broken psutil/mozsystemmonitor leaves mach untouched, and every wrapper
54
+ * only intercepts the failure path.
29
55
  */
30
- const SITECUSTOMIZE_SOURCE = `# Generated by FireForge do not edit.
31
- # Degrades a broken host resource monitor (psutil vs Darwin) from a fatal
32
- # RuntimeError in start_resource_recording into a non-fatal warning, so
33
- # \`mach build\` does not abort before the build runs. Loaded via PYTHONPATH
34
- # so it patches psutil before mach's resource monitor starts.
56
+ const GUARD_PYTHON_SOURCE = `# Generated by FireForge - do not edit.
57
+ # Degrades the broken host resource monitor family (psutil vs Darwin) from
58
+ # fatal startup errors into non-fatal warnings, so mach builds and tests
59
+ # proceed. Installed into the mach virtualenv site-packages via a .pth file
60
+ # (survives mach's venv re-exec, which drops PYTHONPATH).
61
+ import builtins
62
+ import sys
35
63
  import warnings
36
64
 
37
- try:
38
- import psutil
39
- except Exception:
40
- psutil = None
41
65
 
42
- if psutil is not None:
43
- class _DegradedReading(object):
44
- total = available = used = free = active = inactive = wired = 0
45
- percent = 0.0
66
+ def _fireforge_degraded_notice(exc):
67
+ warnings.warn(
68
+ "FireForge: host resource monitor degraded (%s); continuing without resource monitoring." % exc
69
+ )
70
+
71
+
72
+ class _DegradedReading(object):
73
+ total = available = used = free = active = inactive = wired = 0
74
+ percent = 0.0
75
+
76
+ def __getattr__(self, _name):
77
+ return 0
46
78
 
47
- def __getattr__(self, _name):
48
- return 0
49
79
 
50
- def _guard(orig):
80
+ def _guard_psutil():
81
+ try:
82
+ import psutil
83
+ except Exception:
84
+ return
85
+
86
+ def _wrap(orig):
51
87
  def wrapper(*args, **kwargs):
52
88
  try:
53
89
  return orig(*args, **kwargs)
54
90
  except Exception as exc: # noqa: BLE001
55
- warnings.warn(
56
- "FireForge: host resource monitor degraded (%s); build continues." % exc
57
- )
91
+ _fireforge_degraded_notice(exc)
58
92
  return _DegradedReading()
59
93
 
60
94
  return wrapper
61
95
 
62
- for _name in ("virtual_memory", "swap_memory"):
96
+ for _name in ("virtual_memory", "swap_memory", "cpu_percent", "cpu_times", "disk_io_counters"):
63
97
  _orig = getattr(psutil, _name, None)
98
+ if _orig is not None and not getattr(_orig, "_fireforge_guarded", False):
99
+ _wrapped = _wrap(_orig)
100
+ _wrapped._fireforge_guarded = True
101
+ setattr(psutil, _name, _wrapped)
102
+
103
+
104
+ _MONITOR_METHOD_NAMES = (
105
+ "start",
106
+ "stop",
107
+ "record_event",
108
+ "record_marker",
109
+ "begin_phase",
110
+ "finish_phase",
111
+ "aggregate_cpu_percent",
112
+ "aggregate_cpu_times",
113
+ "aggregate_io",
114
+ "min_memory_available",
115
+ "as_dict",
116
+ )
117
+
118
+
119
+ def _patch_monitor_class(cls):
120
+ if cls is None or getattr(cls, "_fireforge_guarded", False):
121
+ return
122
+ orig_init = cls.__init__
123
+
124
+ def guarded_init(self, *args, **kwargs):
125
+ # Pre-populate the attributes a partially-constructed instance is
126
+ # later polled for, so a failing __init__ can never surface
127
+ # AttributeError: poll_interval.
128
+ self.poll_interval = kwargs.get("poll_interval") or 1.0
129
+ self._fireforge_degraded = False
130
+ try:
131
+ orig_init(self, *args, **kwargs)
132
+ except Exception as exc: # noqa: BLE001
133
+ self._fireforge_degraded = True
134
+ _fireforge_degraded_notice(exc)
135
+
136
+ cls.__init__ = guarded_init
137
+
138
+ def _wrap_method(orig):
139
+ def wrapper(self, *args, **kwargs):
140
+ if getattr(self, "_fireforge_degraded", False):
141
+ return None
142
+ try:
143
+ return orig(self, *args, **kwargs)
144
+ except Exception as exc: # noqa: BLE001
145
+ self._fireforge_degraded = True
146
+ _fireforge_degraded_notice(exc)
147
+ return None
148
+
149
+ return wrapper
150
+
151
+ for _name in _MONITOR_METHOD_NAMES:
152
+ _orig = getattr(cls, _name, None)
64
153
  if _orig is not None:
65
- setattr(psutil, _name, _guard(_orig))
154
+ setattr(cls, _name, _wrap_method(_orig))
155
+ cls._fireforge_guarded = True
156
+
157
+
158
+ _MONITOR_MODULES = ("mozsystemmonitor.resourcemonitor", "mozbuild.resources")
159
+
160
+
161
+ def _patch_loaded_monitor_modules():
162
+ for _mod_name in _MONITOR_MODULES:
163
+ _mod = sys.modules.get(_mod_name)
164
+ if _mod is not None:
165
+ try:
166
+ _patch_monitor_class(getattr(_mod, "SystemResourceMonitor", None))
167
+ except Exception: # noqa: BLE001
168
+ pass
169
+
170
+
171
+ def _install_import_hook():
172
+ _orig_import = builtins.__import__
173
+ if getattr(_orig_import, "_fireforge_guarded", False):
174
+ return
175
+
176
+ def _guarding_import(name, *args, **kwargs):
177
+ _module = _orig_import(name, *args, **kwargs)
178
+ try:
179
+ _patch_loaded_monitor_modules()
180
+ except Exception: # noqa: BLE001
181
+ pass
182
+ return _module
183
+
184
+ _guarding_import._fireforge_guarded = True
185
+ builtins.__import__ = _guarding_import
186
+
187
+
188
+ try:
189
+ _guard_psutil()
190
+ _patch_loaded_monitor_modules()
191
+ _install_import_hook()
192
+ except Exception: # noqa: BLE001
193
+ pass
66
194
  `;
195
+ /** Lists subdirectory names of `dir`, tolerating a missing directory. */
196
+ async function listSubdirs(dir) {
197
+ try {
198
+ const entries = await readdir(dir, { withFileTypes: true });
199
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
200
+ }
201
+ catch {
202
+ return [];
203
+ }
204
+ }
67
205
  /**
68
- * Writes the `sitecustomize.py` degrade shim into a stable FireForge-owned
69
- * temp directory and returns that directory. Idempotent — overwriting the
70
- * file each call keeps it in sync with this source if FireForge upgrades.
206
+ * Discovers `site-packages` directories of the mach virtualenvs under one
207
+ * `_virtualenvs` root: `<root>/<venv>/lib/python<N.M>/site-packages`.
71
208
  */
72
- async function ensureMachResourceShim() {
209
+ async function discoverVenvSitePackages(virtualenvsDir) {
210
+ const found = [];
211
+ for (const venvName of await listSubdirs(virtualenvsDir)) {
212
+ const libDir = join(virtualenvsDir, venvName, 'lib');
213
+ for (const pythonName of await listSubdirs(libDir)) {
214
+ if (!pythonName.startsWith('python'))
215
+ continue;
216
+ const sitePackages = join(libDir, pythonName, 'site-packages');
217
+ if (await pathExists(sitePackages)) {
218
+ found.push(sitePackages);
219
+ }
220
+ }
221
+ }
222
+ return found;
223
+ }
224
+ /**
225
+ * Discovers every mach virtualenv site-packages dir relevant to an engine
226
+ * checkout: the objdir venvs (under `<engineDir>/obj-&lt;x&gt;/_virtualenvs`)
227
+ * and the mach state-dir venvs (under `$MOZBUILD_STATE_PATH` or
228
+ * `~/.mozbuild`, in `srcdirs/&lt;hash&gt;/_virtualenvs`, where mach keeps the
229
+ * `mach` command venv itself).
230
+ */
231
+ async function discoverMachSitePackages(engineDir) {
232
+ const found = [];
233
+ for (const name of await listSubdirs(engineDir)) {
234
+ if (!name.startsWith('obj-'))
235
+ continue;
236
+ found.push(...(await discoverVenvSitePackages(join(engineDir, name, '_virtualenvs'))));
237
+ }
238
+ const stateDir = process.env['MOZBUILD_STATE_PATH'] ?? join(homedir(), '.mozbuild');
239
+ for (const srcdirName of await listSubdirs(join(stateDir, 'srcdirs'))) {
240
+ found.push(...(await discoverVenvSitePackages(join(stateDir, 'srcdirs', srcdirName, '_virtualenvs'))));
241
+ }
242
+ return found;
243
+ }
244
+ /**
245
+ * Writes the PYTHONPATH `sitecustomize.py` fallback into a stable
246
+ * FireForge-owned temp directory and returns that directory. Idempotent —
247
+ * overwriting each call keeps it in sync with this source across upgrades.
248
+ */
249
+ async function ensureSitecustomizeFallback() {
73
250
  const dir = join(tmpdir(), RESOURCE_SHIM_DIRNAME);
74
251
  await ensureDir(dir);
75
- await writeText(join(dir, 'sitecustomize.py'), SITECUSTOMIZE_SOURCE);
252
+ await writeText(join(dir, 'sitecustomize.py'), GUARD_PYTHON_SOURCE);
76
253
  return dir;
77
254
  }
78
255
  /**
79
256
  * Builds the `env` overlay (merged over `process.env` by the exec layer)
80
- * that prepends the shim directory to `PYTHONPATH`, so the mach subprocess
81
- * imports the degrade `sitecustomize` without clobbering an existing
82
- * `PYTHONPATH`.
257
+ * that prepends the fallback shim directory to `PYTHONPATH` without
258
+ * clobbering an existing `PYTHONPATH`.
83
259
  */
84
260
  function machResourceShimEnv(shimDir, baseEnv = process.env) {
85
261
  const existing = baseEnv['PYTHONPATH'];
86
262
  return { PYTHONPATH: existing ? `${shimDir}${delimiter}${existing}` : shimDir };
87
263
  }
88
- /** Ensures the shim exists and returns the mach `env` overlay that enables it. */
89
- export async function resolveMachBuildEnv() {
90
- return machResourceShimEnv(await ensureMachResourceShim());
264
+ /**
265
+ * Installs the resource-monitor degrade guard for a mach dispatch:
266
+ * `fireforge_mach_guard.pth` + `fireforge_mach_guard.py` into every
267
+ * discovered mach virtualenv site-packages (survives mach's venv re-exec),
268
+ * plus the PYTHONPATH sitecustomize fallback for the pre-venv bootstrap
269
+ * phase. Idempotent and re-run before EVERY protected dispatch (including
270
+ * each retry) so a venv created by a crashed first attempt is guarded on
271
+ * the next one instead of re-dying on the same wedged state.
272
+ */
273
+ export async function installMachResourceGuard(engineDir) {
274
+ const sitePackagesDirs = await discoverMachSitePackages(engineDir);
275
+ for (const sitePackages of sitePackagesDirs) {
276
+ try {
277
+ await writeText(join(sitePackages, `${GUARD_MODULE_NAME}.py`), GUARD_PYTHON_SOURCE);
278
+ await writeText(join(sitePackages, `${GUARD_MODULE_NAME}.pth`), `import ${GUARD_MODULE_NAME}\n`);
279
+ }
280
+ catch (error) {
281
+ // A read-only or vanished venv must not block the dispatch; the
282
+ // PYTHONPATH fallback still applies and other venvs may have taken
283
+ // the guard.
284
+ verbose(`Could not install mach resource guard into ${sitePackages}: ${toError(error).message}`);
285
+ }
286
+ }
287
+ const env = machResourceShimEnv(await ensureSitecustomizeFallback());
288
+ return { env, sitePackagesDirs };
91
289
  }
92
290
  //# sourceMappingURL=mach-resource-shim.js.map
@@ -1,4 +1,5 @@
1
1
  import { type SmokeLineCallback, type SmokeRunResult } from '../utils/process.js';
2
+ import { type HarnessCrashSignature } from './test-harness-crash.js';
2
3
  export { attemptMozinfoRewrite, buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, } from './mach-build-artifacts.js';
3
4
  export { generateMozconfig } from './mach-mozconfig.js';
4
5
  export { ensurePython, resetResolvedPython } from './mach-python.js';
@@ -57,12 +58,52 @@ export declare function bootstrap(engineDir: string): Promise<number>;
57
58
  * @returns Captured output and exit code
58
59
  */
59
60
  export declare function bootstrapWithOutput(engineDir: string): Promise<MachCommandResult>;
61
+ /** Which mach build entry a protected dispatch runs. */
62
+ export type ProtectedBuildKind = 'full' | 'faster';
63
+ /** Options for {@link runProtectedMachBuild}. */
64
+ export interface ProtectedMachBuildOptions {
65
+ /** Parallel jobs for the full build (ignored for `faster`). */
66
+ jobs?: number | undefined;
67
+ /** Recognized-crash retry budget (0 disables retries). */
68
+ retries?: number | undefined;
69
+ /** Called before each retry with the detected crash signature. */
70
+ onRetry?: (signature: HarnessCrashSignature, nextAttempt: number, maxAttempts: number) => void;
71
+ }
72
+ /** Captured result of a protected (guarded, retried) mach build dispatch. */
73
+ export interface ProtectedMachBuildResult extends MachCommandResult {
74
+ /** How many mach processes were spawned (1 = no retry needed). */
75
+ attempts: number;
76
+ /** Crash signature of the final failed attempt, when recognized. */
77
+ crashSignature?: HarnessCrashSignature;
78
+ }
79
+ /**
80
+ * The single protected path every FireForge mach build dispatch routes
81
+ * through (field report: `build --ui` and the pre-test `--build` step died
82
+ * on the resource-monitor crash family while plain `build` survived,
83
+ * because only some entries were protected and retry budgets differed):
84
+ *
85
+ * 1. installs the resource-monitor degrade guard IN the mach virtualenvs
86
+ * (plus the PYTHONPATH fallback) — re-installed before every attempt,
87
+ * so a venv materialized by a crashed first attempt is guarded on the
88
+ * next one instead of every retry dying on the same wedged state;
89
+ * 2. spawns a fresh mach process per attempt;
90
+ * 3. retries ONLY the recognized harness-crash family (resource monitor /
91
+ * psutil startup tracebacks) up to the uniform budget — an ordinary
92
+ * compile error is never retried.
93
+ *
94
+ * The protected path never runs `mach configure`, never clobbers, and
95
+ * never widens the requested build kind — a `faster` dispatch retries as
96
+ * `mach build faster`, so it cannot invalidate more of the objdir than the
97
+ * command the operator asked for.
98
+ */
99
+ export declare function runProtectedMachBuild(kind: ProtectedBuildKind, engineDir: string, options?: ProtectedMachBuildOptions): Promise<ProtectedMachBuildResult>;
60
100
  /**
61
- * Runs a full mach build. On a non-zero exit, any matched error hints are
62
- * surfaced on top of the raw mach output so operators get an actionable
63
- * nudge alongside the cryptic mozbuild traceback. Returns the captured
64
- * result so the caller (e.g. `fireforge build`) can inspect the tail
65
- * for post-build diagnostics that mach prints AFTER "Your build was
101
+ * Runs a full mach build through the protected dispatch path (resource
102
+ * guard + recognized-crash retries). On a non-zero exit, any matched error
103
+ * hints are surfaced on top of the raw mach output so operators get an
104
+ * actionable nudge alongside the cryptic mozbuild traceback. Returns the
105
+ * captured result so the caller (e.g. `fireforge build`) can inspect the
106
+ * tail for post-build diagnostics that mach prints AFTER "Your build was
66
107
  * successful!" — notably the stale `config.status is out of date`
67
108
  * notice that mach emits when a tool-managed edit landed on
68
109
  * `moz.configure` before the build.
@@ -70,15 +111,15 @@ export declare function bootstrapWithOutput(engineDir: string): Promise<MachComm
70
111
  * @param jobs - Number of parallel jobs (optional)
71
112
  * @returns Captured mach result (stdout tail, stderr tail, exit code)
72
113
  */
73
- export declare function build(engineDir: string, jobs?: number): Promise<MachCommandResult>;
114
+ export declare function build(engineDir: string, jobs?: number): Promise<ProtectedMachBuildResult>;
74
115
  /**
75
- * Runs a fast UI-only build. On a non-zero exit, any matched error hints are
76
- * surfaced on top of the raw mach output. See {@link build} for why the
77
- * full captured result is returned rather than just the exit code.
116
+ * Runs a fast UI-only build through the same protected dispatch path as
117
+ * {@link build}. See {@link build} for why the full captured result is
118
+ * returned rather than just the exit code.
78
119
  * @param engineDir - Path to the engine directory
79
120
  * @returns Captured mach result
80
121
  */
81
- export declare function buildUI(engineDir: string): Promise<MachCommandResult>;
122
+ export declare function buildUI(engineDir: string): Promise<ProtectedMachBuildResult>;
82
123
  /**
83
124
  * Runs an operation while holding a sidecar build lock keyed on the
84
125
  * project root. Concurrent `fireforge build` / `fireforge build --ui`
@@ -8,7 +8,8 @@ import { createSiblingLockPath, withFileLock } from './file-lock.js';
8
8
  import { ensureFirefoxIgnorefileCompatibility } from './firefox-ignorefile.js';
9
9
  import { explainMachError } from './mach-error-hints.js';
10
10
  import { getPython } from './mach-python.js';
11
- import { resolveMachBuildEnv } from './mach-resource-shim.js';
11
+ import { installMachResourceGuard } from './mach-resource-shim.js';
12
+ import { detectHarnessCrashSignature } from './test-harness-crash.js';
12
13
  // Re-export sub-modules so existing `from './mach.js'` imports keep working.
13
14
  export { attemptMozinfoRewrite, buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, } from './mach-build-artifacts.js';
14
15
  export { generateMozconfig } from './mach-mozconfig.js';
@@ -140,11 +141,63 @@ function surfaceMachErrorHints(result) {
140
141
  }
141
142
  }
142
143
  /**
143
- * Runs a full mach build. On a non-zero exit, any matched error hints are
144
- * surfaced on top of the raw mach output so operators get an actionable
145
- * nudge alongside the cryptic mozbuild traceback. Returns the captured
146
- * result so the caller (e.g. `fireforge build`) can inspect the tail
147
- * for post-build diagnostics that mach prints AFTER "Your build was
144
+ * Uniform recognized-crash retry budget for the protected mach build
145
+ * dispatches (`fireforge build`, `build --ui`, and the pre-test `--build`
146
+ * step). Matches the test harness default so every mach dispatch retries
147
+ * the same crash family the same number of times.
148
+ */
149
+ const DEFAULT_BUILD_CRASH_RETRIES = 2;
150
+ /**
151
+ * The single protected path every FireForge mach build dispatch routes
152
+ * through (field report: `build --ui` and the pre-test `--build` step died
153
+ * on the resource-monitor crash family while plain `build` survived,
154
+ * because only some entries were protected and retry budgets differed):
155
+ *
156
+ * 1. installs the resource-monitor degrade guard IN the mach virtualenvs
157
+ * (plus the PYTHONPATH fallback) — re-installed before every attempt,
158
+ * so a venv materialized by a crashed first attempt is guarded on the
159
+ * next one instead of every retry dying on the same wedged state;
160
+ * 2. spawns a fresh mach process per attempt;
161
+ * 3. retries ONLY the recognized harness-crash family (resource monitor /
162
+ * psutil startup tracebacks) up to the uniform budget — an ordinary
163
+ * compile error is never retried.
164
+ *
165
+ * The protected path never runs `mach configure`, never clobbers, and
166
+ * never widens the requested build kind — a `faster` dispatch retries as
167
+ * `mach build faster`, so it cannot invalidate more of the objdir than the
168
+ * command the operator asked for.
169
+ */
170
+ export async function runProtectedMachBuild(kind, engineDir, options = {}) {
171
+ const args = kind === 'faster' ? ['build', 'faster'] : ['build'];
172
+ if (kind === 'full' && options.jobs !== undefined) {
173
+ args.push('-j', String(options.jobs));
174
+ }
175
+ const maxAttempts = Math.max(1, (options.retries ?? DEFAULT_BUILD_CRASH_RETRIES) + 1);
176
+ for (let attempt = 1;; attempt += 1) {
177
+ const { env } = await installMachResourceGuard(engineDir);
178
+ const result = await runMachInheritCapture(args, engineDir, { env });
179
+ if (result.exitCode === 0) {
180
+ return { ...result, attempts: attempt };
181
+ }
182
+ const signature = detectHarnessCrashSignature(`${result.stdout}\n${result.stderr}`);
183
+ if (signature && attempt < maxAttempts) {
184
+ options.onRetry?.(signature, attempt + 1, maxAttempts);
185
+ warn(`mach ${kind === 'faster' ? 'build faster' : 'build'} hit a recognized harness crash ` +
186
+ `(${signature.reason}): ${signature.line}\n` +
187
+ `Retrying with a fresh process (attempt ${attempt + 1} of ${maxAttempts})...`);
188
+ continue;
189
+ }
190
+ surfaceMachErrorHints(result);
191
+ return { ...result, attempts: attempt, ...(signature ? { crashSignature: signature } : {}) };
192
+ }
193
+ }
194
+ /**
195
+ * Runs a full mach build through the protected dispatch path (resource
196
+ * guard + recognized-crash retries). On a non-zero exit, any matched error
197
+ * hints are surfaced on top of the raw mach output so operators get an
198
+ * actionable nudge alongside the cryptic mozbuild traceback. Returns the
199
+ * captured result so the caller (e.g. `fireforge build`) can inspect the
200
+ * tail for post-build diagnostics that mach prints AFTER "Your build was
148
201
  * successful!" — notably the stale `config.status is out of date`
149
202
  * notice that mach emits when a tool-managed edit landed on
150
203
  * `moz.configure` before the build.
@@ -153,35 +206,17 @@ function surfaceMachErrorHints(result) {
153
206
  * @returns Captured mach result (stdout tail, stderr tail, exit code)
154
207
  */
155
208
  export async function build(engineDir, jobs) {
156
- const args = ['build'];
157
- if (jobs !== undefined) {
158
- args.push('-j', String(jobs));
159
- }
160
- // Inject the resource-monitor degrade shim so a host psutil failure does
161
- // not abort the build regardless of which mach entry FireForge spawns.
162
- const env = await resolveMachBuildEnv();
163
- const result = await runMachInheritCapture(args, engineDir, { env });
164
- if (result.exitCode !== 0) {
165
- surfaceMachErrorHints(result);
166
- }
167
- return result;
209
+ return runProtectedMachBuild('full', engineDir, { jobs });
168
210
  }
169
211
  /**
170
- * Runs a fast UI-only build. On a non-zero exit, any matched error hints are
171
- * surfaced on top of the raw mach output. See {@link build} for why the
172
- * full captured result is returned rather than just the exit code.
212
+ * Runs a fast UI-only build through the same protected dispatch path as
213
+ * {@link build}. See {@link build} for why the full captured result is
214
+ * returned rather than just the exit code.
173
215
  * @param engineDir - Path to the engine directory
174
216
  * @returns Captured mach result
175
217
  */
176
218
  export async function buildUI(engineDir) {
177
- // Same resource-monitor degrade shim as the full build: raw
178
- // `./mach build faster` aborts on the host psutil monitor without it.
179
- const env = await resolveMachBuildEnv();
180
- const result = await runMachInheritCapture(['build', 'faster'], engineDir, { env });
181
- if (result.exitCode !== 0) {
182
- surfaceMachErrorHints(result);
183
- }
184
- return result;
219
+ return runProtectedMachBuild('faster', engineDir);
185
220
  }
186
221
  /**
187
222
  * Runs an operation while holding a sidecar build lock keyed on the
@@ -325,7 +360,10 @@ export async function test(engineDir, testPaths = [], args = []) {
325
360
  * `fireforge test --perf-samples` to publish the artifact-path contract.
326
361
  */
327
362
  export async function testWithOutput(engineDir, testPaths = [], args = [], env) {
328
- return runMachCapture(['test', ...testPaths, ...args], engineDir, env ? { env } : {});
363
+ const guard = await installMachResourceGuard(engineDir);
364
+ return runMachCapture(['test', ...testPaths, ...args], engineDir, {
365
+ env: { ...guard.env, ...env },
366
+ });
329
367
  }
330
368
  /**
331
369
  * Runs `mach xpcshell-test` (the suite-specific xpcshell command) while
@@ -338,7 +376,10 @@ export async function testWithOutput(engineDir, testPaths = [], args = [], env)
338
376
  * the dispatch path.
339
377
  */
340
378
  export async function xpcshellTestWithOutput(engineDir, testPaths = [], args = [], env) {
341
- return runMachCapture(['xpcshell-test', ...testPaths, ...args], engineDir, env ? { env } : {});
379
+ const guard = await installMachResourceGuard(engineDir);
380
+ return runMachCapture(['xpcshell-test', ...testPaths, ...args], engineDir, {
381
+ env: { ...guard.env, ...env },
382
+ });
342
383
  }
343
384
  /**
344
385
  * Runs `mach mochitest` (covers browser-chrome / mochitest flavors) while
@@ -346,6 +387,9 @@ export async function xpcshellTestWithOutput(engineDir, testPaths = [], args = [
346
387
  * for non-xpcshell single-suite runs — see {@link xpcshellTestWithOutput}.
347
388
  */
348
389
  export async function mochitestWithOutput(engineDir, testPaths = [], args = [], env) {
349
- return runMachCapture(['mochitest', ...testPaths, ...args], engineDir, env ? { env } : {});
390
+ const guard = await installMachResourceGuard(engineDir);
391
+ return runMachCapture(['mochitest', ...testPaths, ...args], engineDir, {
392
+ env: { ...guard.env, ...env },
393
+ });
350
394
  }
351
395
  //# sourceMappingURL=mach.js.map
@@ -1,4 +1,17 @@
1
1
  import type { JarMnToken, MozBuildToken } from './manifest-tokenizers.js';
2
+ /**
3
+ * Ordering comparator matching mozbuild's `StrictOrderingOnAppendList`
4
+ * (`mozbuild.util.UnsortedError`): entries are compared **case-insensitively**,
5
+ * so `HominisAppearanceController` (`appe`) sorts before
6
+ * `HominisAppMenuIntegration` (`appm`) even though a raw byte comparison
7
+ * places the uppercase `M` (0x4D) before the lowercase `e` (0x65).
8
+ *
9
+ * A naive `a > b` insertion landed the new entry in byte order, which
10
+ * `mach configure` then rejected with `UnsortedError`, aborting the build.
11
+ * Ties on the lower-cased key fall back to byte order so the comparison is
12
+ * total and stable.
13
+ */
14
+ export declare function mozbuildSortCompare(a: string, b: string): number;
2
15
  /**
3
16
  * Inserts a line into an array of lines in alphabetical order within a
4
17
  * specified range. The comparison key is extracted from each line.
@@ -10,7 +10,7 @@
10
10
  * Ties on the lower-cased key fall back to byte order so the comparison is
11
11
  * total and stable.
12
12
  */
13
- function mozbuildSortCompare(a, b) {
13
+ export function mozbuildSortCompare(a, b) {
14
14
  const la = a.toLowerCase();
15
15
  const lb = b.toLowerCase();
16
16
  if (la < lb)
@@ -10,8 +10,14 @@ export interface PatternRule {
10
10
  /** Extract arguments from the regex match */
11
11
  extractArgs: (match: RegExpMatchArray) => string[];
12
12
  }
13
- /** Returns manifest registration rules for the supported engine file patterns. */
14
- export declare function getRules(binaryName: string): PatternRule[];
13
+ /**
14
+ * Returns manifest registration rules for the supported engine file
15
+ * patterns. `createManifest` (from `register --create-manifest`) lets the
16
+ * module and xpcshell-test rules scaffold a missing manifest (directory
17
+ * moz.build / xpcshell.toml) and wire the parent chain instead of failing
18
+ * with "Manifest not found".
19
+ */
20
+ export declare function getRules(binaryName: string, createManifest?: boolean): PatternRule[];
15
21
  /**
16
22
  * Checks if a file path matches any known registrable pattern.
17
23
  * @param filePath - Path relative to engine/
@@ -34,6 +40,10 @@ export declare function isFileRegistered(root: string, filePath: string): Promis
34
40
  * @param filePath - Path relative to engine/
35
41
  * @param dryRun - If true, return what would be done without writing
36
42
  * @param after - Optional substring to place entry after (instead of alphabetical)
43
+ * @param options - `createManifest` scaffolds a missing manifest (directory
44
+ * moz.build / xpcshell.toml) and wires the parent chain instead of failing
37
45
  * @returns Registration result
38
46
  */
39
- export declare function registerFile(root: string, filePath: string, dryRun?: boolean, after?: string): Promise<RegisterResult>;
47
+ export declare function registerFile(root: string, filePath: string, dryRun?: boolean, after?: string, options?: {
48
+ createManifest?: boolean;
49
+ }): Promise<RegisterResult>;