@hominis/fireforge 0.33.0 → 0.34.1

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 +16 -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 +70 -16
  33. package/dist/src/core/mach-resource-shim.js +310 -52
  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 +149 -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,350 @@
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. Downstream report (0.34.0 cycle):
32
+ * mozsystemmonitor subscripts (`_build_meta` does
33
+ * `psutil.virtual_memory()[0]`) and iterates/unpacks the reading, so the
34
+ * zeroed fallback is a full namedtuple duck type — a zeroed instance of
35
+ * psutil's own result namedtuple where resolvable without touching the
36
+ * failing syscall, else `_DegradedReading` (subscriptable, iterable, with
37
+ * `len()`, `_fields`, `_asdict()`); `cpu_percent` degrades to `0.0`
38
+ * because callers do arithmetic on it;
39
+ * - guards `SystemResourceMonitor` CONSTRUCTION via an import hook:
40
+ * `poll_interval` is pre-populated before the real `__init__` runs, a
41
+ * failing `__init__` marks the instance degraded instead of raising,
42
+ * and every monitor method no-ops on a degraded instance — so a
43
+ * partially-constructed monitor can never surface the
44
+ * `AttributeError: poll_interval` variant.
20
45
  */
21
- import { tmpdir } from 'node:os';
46
+ import { readdir } from 'node:fs/promises';
47
+ import { homedir, tmpdir } from 'node:os';
22
48
  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. */
49
+ import { toError } from '../utils/errors.js';
50
+ import { ensureDir, pathExists, writeText } from '../utils/fs.js';
51
+ import { verbose } from '../utils/logger.js';
52
+ /** Directory name (under the OS temp dir) holding the PYTHONPATH fallback. */
25
53
  const RESOURCE_SHIM_DIRNAME = 'fireforge-mach-resource-shim';
54
+ /** Basename (sans extension) of the in-venv guard module. */
55
+ const GUARD_MODULE_NAME = 'fireforge_mach_guard';
26
56
  /**
27
- * `sitecustomize.py` body. Defensive: a missing/broken psutil leaves mach
28
- * untouched, and the wrapper only intercepts the failure path.
57
+ * Python guard body, importable both as the in-venv `fireforge_mach_guard`
58
+ * module (loaded by the sibling `.pth` at interpreter startup) and as the
59
+ * PYTHONPATH `sitecustomize` fallback. Defensive throughout: a missing or
60
+ * broken psutil/mozsystemmonitor leaves mach untouched, and every wrapper
61
+ * only intercepts the failure path. Exported for the Python-executing
62
+ * regression test (mach-resource-shim.python.test.ts), which asserts the
63
+ * degraded readings' runtime behavior rather than string-matching the source.
29
64
  */
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.
65
+ export const GUARD_PYTHON_SOURCE = `# Generated by FireForge - do not edit.
66
+ # Degrades the broken host resource monitor family (psutil vs Darwin) from
67
+ # fatal startup errors into non-fatal warnings, so mach builds and tests
68
+ # proceed. Installed into the mach virtualenv site-packages via a .pth file
69
+ # (survives mach's venv re-exec, which drops PYTHONPATH).
70
+ import builtins
71
+ import sys
35
72
  import warnings
36
73
 
37
- try:
38
- import psutil
39
- except Exception:
40
- psutil = None
41
74
 
42
- if psutil is not None:
43
- class _DegradedReading(object):
44
- total = available = used = free = active = inactive = wired = 0
45
- percent = 0.0
75
+ def _fireforge_degraded_notice(exc):
76
+ warnings.warn(
77
+ "FireForge: host resource monitor degraded (%s); continuing without resource monitoring." % exc
78
+ )
79
+
80
+
81
+ class _DegradedReading(object):
82
+ # Duck type of psutil's namedtuple results (svmem field order):
83
+ # mozsystemmonitor subscripts, iterates, and unpacks readings, so the
84
+ # degraded fallback must survive r[0], list(r), len(r), r._fields and
85
+ # r._asdict(), not just attribute access.
86
+ _fields = ("total", "available", "percent", "used", "free", "active", "inactive", "wired")
87
+ total = available = used = free = active = inactive = wired = 0
88
+ percent = 0.0
89
+
90
+ def __getattr__(self, _name):
91
+ return 0
92
+
93
+ def _values(self):
94
+ return tuple(getattr(self, _field) for _field in self._fields)
95
+
96
+ def __getitem__(self, index):
97
+ return self._values()[index]
98
+
99
+ def __iter__(self):
100
+ return iter(self._values())
101
+
102
+ def __len__(self):
103
+ return len(self._fields)
104
+
105
+ def _asdict(self):
106
+ return dict(zip(self._fields, self._values()))
107
+
108
+
109
+ # psutil result namedtuple per wrapped function, resolvable via getattr from
110
+ # the platform module / _common without invoking the failing syscall.
111
+ _PSUTIL_RESULT_CLASSES = {
112
+ "virtual_memory": "svmem",
113
+ "swap_memory": "sswap",
114
+ "cpu_times": "scputimes",
115
+ "disk_io_counters": "sdiskio",
116
+ }
46
117
 
47
- def __getattr__(self, _name):
48
- return 0
49
118
 
50
- def _guard(orig):
119
+ def _degraded_result_factory(psutil, name):
120
+ # cpu_percent returns a plain float callers do arithmetic on.
121
+ if name == "cpu_percent":
122
+ return lambda: 0.0
123
+ _cls_name = _PSUTIL_RESULT_CLASSES.get(name)
124
+ if _cls_name is not None:
125
+ for _mod in (getattr(psutil, "_psplatform", None), getattr(psutil, "_common", None)):
126
+ _cls = getattr(_mod, _cls_name, None) if _mod is not None else None
127
+ _fields = getattr(_cls, "_fields", None)
128
+ if _cls is not None and _fields:
129
+ return lambda _cls=_cls, _n=len(_fields): _cls(*([0] * _n))
130
+ return _DegradedReading
131
+
132
+
133
+ def _guard_psutil():
134
+ try:
135
+ import psutil
136
+ except Exception:
137
+ return
138
+
139
+ def _wrap(orig, fallback):
51
140
  def wrapper(*args, **kwargs):
52
141
  try:
53
142
  return orig(*args, **kwargs)
54
143
  except Exception as exc: # noqa: BLE001
55
- warnings.warn(
56
- "FireForge: host resource monitor degraded (%s); build continues." % exc
57
- )
58
- return _DegradedReading()
144
+ _fireforge_degraded_notice(exc)
145
+ try:
146
+ return fallback()
147
+ except Exception: # noqa: BLE001
148
+ return _DegradedReading()
59
149
 
60
150
  return wrapper
61
151
 
62
- for _name in ("virtual_memory", "swap_memory"):
152
+ for _name in ("virtual_memory", "swap_memory", "cpu_percent", "cpu_times", "disk_io_counters"):
63
153
  _orig = getattr(psutil, _name, None)
154
+ if _orig is not None and not getattr(_orig, "_fireforge_guarded", False):
155
+ try:
156
+ _fallback = _degraded_result_factory(psutil, _name)
157
+ except Exception: # noqa: BLE001
158
+ _fallback = _DegradedReading
159
+ _wrapped = _wrap(_orig, _fallback)
160
+ _wrapped._fireforge_guarded = True
161
+ setattr(psutil, _name, _wrapped)
162
+
163
+
164
+ _MONITOR_METHOD_NAMES = (
165
+ "start",
166
+ "stop",
167
+ "record_event",
168
+ "record_marker",
169
+ "begin_phase",
170
+ "finish_phase",
171
+ "aggregate_cpu_percent",
172
+ "aggregate_cpu_times",
173
+ "aggregate_io",
174
+ "min_memory_available",
175
+ "as_dict",
176
+ )
177
+
178
+
179
+ def _patch_monitor_class(cls):
180
+ if cls is None or getattr(cls, "_fireforge_guarded", False):
181
+ return
182
+ orig_init = cls.__init__
183
+
184
+ def guarded_init(self, *args, **kwargs):
185
+ # Pre-populate the attributes a partially-constructed instance is
186
+ # later polled for, so a failing __init__ can never surface
187
+ # AttributeError: poll_interval.
188
+ self.poll_interval = kwargs.get("poll_interval") or 1.0
189
+ self._fireforge_degraded = False
190
+ try:
191
+ orig_init(self, *args, **kwargs)
192
+ except Exception as exc: # noqa: BLE001
193
+ self._fireforge_degraded = True
194
+ _fireforge_degraded_notice(exc)
195
+
196
+ cls.__init__ = guarded_init
197
+
198
+ def _wrap_method(orig):
199
+ def wrapper(self, *args, **kwargs):
200
+ if getattr(self, "_fireforge_degraded", False):
201
+ return None
202
+ try:
203
+ return orig(self, *args, **kwargs)
204
+ except Exception as exc: # noqa: BLE001
205
+ self._fireforge_degraded = True
206
+ _fireforge_degraded_notice(exc)
207
+ return None
208
+
209
+ return wrapper
210
+
211
+ for _name in _MONITOR_METHOD_NAMES:
212
+ _orig = getattr(cls, _name, None)
64
213
  if _orig is not None:
65
- setattr(psutil, _name, _guard(_orig))
214
+ setattr(cls, _name, _wrap_method(_orig))
215
+ cls._fireforge_guarded = True
216
+
217
+
218
+ _MONITOR_MODULES = ("mozsystemmonitor.resourcemonitor", "mozbuild.resources")
219
+
220
+
221
+ def _patch_loaded_monitor_modules():
222
+ for _mod_name in _MONITOR_MODULES:
223
+ _mod = sys.modules.get(_mod_name)
224
+ if _mod is not None:
225
+ try:
226
+ _patch_monitor_class(getattr(_mod, "SystemResourceMonitor", None))
227
+ except Exception: # noqa: BLE001
228
+ pass
229
+
230
+
231
+ def _install_import_hook():
232
+ _orig_import = builtins.__import__
233
+ if getattr(_orig_import, "_fireforge_guarded", False):
234
+ return
235
+
236
+ def _guarding_import(name, *args, **kwargs):
237
+ _module = _orig_import(name, *args, **kwargs)
238
+ try:
239
+ _patch_loaded_monitor_modules()
240
+ except Exception: # noqa: BLE001
241
+ pass
242
+ return _module
243
+
244
+ _guarding_import._fireforge_guarded = True
245
+ builtins.__import__ = _guarding_import
246
+
247
+
248
+ try:
249
+ _guard_psutil()
250
+ _patch_loaded_monitor_modules()
251
+ _install_import_hook()
252
+ except Exception: # noqa: BLE001
253
+ pass
66
254
  `;
255
+ /** Lists subdirectory names of `dir`, tolerating a missing directory. */
256
+ async function listSubdirs(dir) {
257
+ try {
258
+ const entries = await readdir(dir, { withFileTypes: true });
259
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
260
+ }
261
+ catch {
262
+ return [];
263
+ }
264
+ }
265
+ /**
266
+ * Discovers `site-packages` directories of the mach virtualenvs under one
267
+ * `_virtualenvs` root: `<root>/<venv>/lib/python<N.M>/site-packages`.
268
+ */
269
+ async function discoverVenvSitePackages(virtualenvsDir) {
270
+ const found = [];
271
+ for (const venvName of await listSubdirs(virtualenvsDir)) {
272
+ const libDir = join(virtualenvsDir, venvName, 'lib');
273
+ for (const pythonName of await listSubdirs(libDir)) {
274
+ if (!pythonName.startsWith('python'))
275
+ continue;
276
+ const sitePackages = join(libDir, pythonName, 'site-packages');
277
+ if (await pathExists(sitePackages)) {
278
+ found.push(sitePackages);
279
+ }
280
+ }
281
+ }
282
+ return found;
283
+ }
284
+ /**
285
+ * Discovers every mach virtualenv site-packages dir relevant to an engine
286
+ * checkout: the objdir venvs (under `<engineDir>/obj-&lt;x&gt;/_virtualenvs`)
287
+ * and the mach state-dir venvs (under `$MOZBUILD_STATE_PATH` or
288
+ * `~/.mozbuild`, in `srcdirs/&lt;hash&gt;/_virtualenvs`, where mach keeps the
289
+ * `mach` command venv itself).
290
+ */
291
+ async function discoverMachSitePackages(engineDir) {
292
+ const found = [];
293
+ for (const name of await listSubdirs(engineDir)) {
294
+ if (!name.startsWith('obj-'))
295
+ continue;
296
+ found.push(...(await discoverVenvSitePackages(join(engineDir, name, '_virtualenvs'))));
297
+ }
298
+ const stateDir = process.env['MOZBUILD_STATE_PATH'] ?? join(homedir(), '.mozbuild');
299
+ for (const srcdirName of await listSubdirs(join(stateDir, 'srcdirs'))) {
300
+ found.push(...(await discoverVenvSitePackages(join(stateDir, 'srcdirs', srcdirName, '_virtualenvs'))));
301
+ }
302
+ return found;
303
+ }
67
304
  /**
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.
305
+ * Writes the PYTHONPATH `sitecustomize.py` fallback into a stable
306
+ * FireForge-owned temp directory and returns that directory. Idempotent —
307
+ * overwriting each call keeps it in sync with this source across upgrades.
71
308
  */
72
- async function ensureMachResourceShim() {
309
+ async function ensureSitecustomizeFallback() {
73
310
  const dir = join(tmpdir(), RESOURCE_SHIM_DIRNAME);
74
311
  await ensureDir(dir);
75
- await writeText(join(dir, 'sitecustomize.py'), SITECUSTOMIZE_SOURCE);
312
+ await writeText(join(dir, 'sitecustomize.py'), GUARD_PYTHON_SOURCE);
76
313
  return dir;
77
314
  }
78
315
  /**
79
316
  * 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`.
317
+ * that prepends the fallback shim directory to `PYTHONPATH` without
318
+ * clobbering an existing `PYTHONPATH`.
83
319
  */
84
320
  function machResourceShimEnv(shimDir, baseEnv = process.env) {
85
321
  const existing = baseEnv['PYTHONPATH'];
86
322
  return { PYTHONPATH: existing ? `${shimDir}${delimiter}${existing}` : shimDir };
87
323
  }
88
- /** Ensures the shim exists and returns the mach `env` overlay that enables it. */
89
- export async function resolveMachBuildEnv() {
90
- return machResourceShimEnv(await ensureMachResourceShim());
324
+ /**
325
+ * Installs the resource-monitor degrade guard for a mach dispatch:
326
+ * `fireforge_mach_guard.pth` + `fireforge_mach_guard.py` into every
327
+ * discovered mach virtualenv site-packages (survives mach's venv re-exec),
328
+ * plus the PYTHONPATH sitecustomize fallback for the pre-venv bootstrap
329
+ * phase. Idempotent and re-run before EVERY protected dispatch (including
330
+ * each retry) so a venv created by a crashed first attempt is guarded on
331
+ * the next one instead of re-dying on the same wedged state.
332
+ */
333
+ export async function installMachResourceGuard(engineDir) {
334
+ const sitePackagesDirs = await discoverMachSitePackages(engineDir);
335
+ for (const sitePackages of sitePackagesDirs) {
336
+ try {
337
+ await writeText(join(sitePackages, `${GUARD_MODULE_NAME}.py`), GUARD_PYTHON_SOURCE);
338
+ await writeText(join(sitePackages, `${GUARD_MODULE_NAME}.pth`), `import ${GUARD_MODULE_NAME}\n`);
339
+ }
340
+ catch (error) {
341
+ // A read-only or vanished venv must not block the dispatch; the
342
+ // PYTHONPATH fallback still applies and other venvs may have taken
343
+ // the guard.
344
+ verbose(`Could not install mach resource guard into ${sitePackages}: ${toError(error).message}`);
345
+ }
346
+ }
347
+ const env = machResourceShimEnv(await ensureSitecustomizeFallback());
348
+ return { env, sitePackagesDirs };
91
349
  }
92
350
  //# 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.