@hominis/fireforge 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +25 -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/patch/split-plan.d.ts +18 -2
  24. package/dist/src/commands/patch/split-plan.js +90 -16
  25. package/dist/src/commands/patch/split.js +12 -3
  26. package/dist/src/commands/re-export-options.d.ts +26 -0
  27. package/dist/src/commands/re-export-options.js +48 -1
  28. package/dist/src/commands/re-export.js +4 -1
  29. package/dist/src/commands/register.js +10 -1
  30. package/dist/src/commands/test-diagnose.js +12 -0
  31. package/dist/src/commands/test.js +29 -22
  32. package/dist/src/commands/token.js +12 -1
  33. package/dist/src/commands/typecheck.js +35 -0
  34. package/dist/src/core/build-prepare.js +23 -3
  35. package/dist/src/core/config-validate.js +26 -0
  36. package/dist/src/core/furnace-apply-dry-run.d.ts +17 -0
  37. package/dist/src/core/furnace-apply-dry-run.js +105 -0
  38. package/dist/src/core/furnace-apply-ftl.d.ts +12 -0
  39. package/dist/src/core/furnace-apply-ftl.js +97 -1
  40. package/dist/src/core/furnace-apply-helpers.js +10 -80
  41. package/dist/src/core/furnace-registration.d.ts +24 -0
  42. package/dist/src/core/furnace-registration.js +56 -3
  43. package/dist/src/core/furnace-validate-registration.js +18 -0
  44. package/dist/src/core/mach-resource-shim.d.ts +58 -0
  45. package/dist/src/core/mach-resource-shim.js +290 -0
  46. package/dist/src/core/mach.d.ts +51 -10
  47. package/dist/src/core/mach.js +76 -25
  48. package/dist/src/core/manifest-helpers.d.ts +13 -0
  49. package/dist/src/core/manifest-helpers.js +29 -4
  50. package/dist/src/core/manifest-rules.d.ts +13 -3
  51. package/dist/src/core/manifest-rules.js +44 -17
  52. package/dist/src/core/patch-export.d.ts +10 -0
  53. package/dist/src/core/patch-export.js +23 -2
  54. package/dist/src/core/patch-lint-cross.d.ts +31 -0
  55. package/dist/src/core/patch-lint-cross.js +83 -63
  56. package/dist/src/core/patch-lint-reexports.d.ts +1 -1
  57. package/dist/src/core/patch-lint-reexports.js +1 -1
  58. package/dist/src/core/register-module.d.ts +1 -1
  59. package/dist/src/core/register-module.js +15 -2
  60. package/dist/src/core/register-result.d.ts +9 -0
  61. package/dist/src/core/register-scaffold.d.ts +55 -0
  62. package/dist/src/core/register-scaffold.js +146 -0
  63. package/dist/src/core/register-test-manifest.js +3 -1
  64. package/dist/src/core/register-xpcshell-test.d.ts +30 -0
  65. package/dist/src/core/register-xpcshell-test.js +96 -0
  66. package/dist/src/core/test-harness-crash.d.ts +32 -4
  67. package/dist/src/core/test-harness-crash.js +152 -14
  68. package/dist/src/core/token-dark-mode.d.ts +9 -0
  69. package/dist/src/core/token-dark-mode.js +1 -1
  70. package/dist/src/core/token-docs.d.ts +32 -0
  71. package/dist/src/core/token-docs.js +101 -0
  72. package/dist/src/core/token-manager.d.ts +8 -0
  73. package/dist/src/core/token-manager.js +77 -95
  74. package/dist/src/core/token-variant.d.ts +39 -0
  75. package/dist/src/core/token-variant.js +141 -0
  76. package/dist/src/core/typecheck.js +56 -28
  77. package/dist/src/core/xpcshell-appdir.d.ts +7 -0
  78. package/dist/src/core/xpcshell-appdir.js +12 -3
  79. package/dist/src/types/commands/options.d.ts +17 -0
  80. package/dist/src/types/config.d.ts +13 -0
  81. package/package.json +4 -4
@@ -0,0 +1,290 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Resource-monitor degrade guard for mach dispatches.
4
+ *
5
+ * mozbuild's build resource monitor calls `psutil.virtual_memory()` at
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.
13
+ *
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.
38
+ */
39
+ import { readdir } from 'node:fs/promises';
40
+ import { homedir, tmpdir } from 'node:os';
41
+ import { delimiter, join } from 'node:path';
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. */
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';
49
+ /**
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.
55
+ */
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
63
+ import warnings
64
+
65
+
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
78
+
79
+
80
+ def _guard_psutil():
81
+ try:
82
+ import psutil
83
+ except Exception:
84
+ return
85
+
86
+ def _wrap(orig):
87
+ def wrapper(*args, **kwargs):
88
+ try:
89
+ return orig(*args, **kwargs)
90
+ except Exception as exc: # noqa: BLE001
91
+ _fireforge_degraded_notice(exc)
92
+ return _DegradedReading()
93
+
94
+ return wrapper
95
+
96
+ for _name in ("virtual_memory", "swap_memory", "cpu_percent", "cpu_times", "disk_io_counters"):
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)
153
+ if _orig is not None:
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
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
+ }
205
+ /**
206
+ * Discovers `site-packages` directories of the mach virtualenvs under one
207
+ * `_virtualenvs` root: `<root>/<venv>/lib/python<N.M>/site-packages`.
208
+ */
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() {
250
+ const dir = join(tmpdir(), RESOURCE_SHIM_DIRNAME);
251
+ await ensureDir(dir);
252
+ await writeText(join(dir, 'sitecustomize.py'), GUARD_PYTHON_SOURCE);
253
+ return dir;
254
+ }
255
+ /**
256
+ * Builds the `env` overlay (merged over `process.env` by the exec layer)
257
+ * that prepends the fallback shim directory to `PYTHONPATH` without
258
+ * clobbering an existing `PYTHONPATH`.
259
+ */
260
+ function machResourceShimEnv(shimDir, baseEnv = process.env) {
261
+ const existing = baseEnv['PYTHONPATH'];
262
+ return { PYTHONPATH: existing ? `${shimDir}${delimiter}${existing}` : shimDir };
263
+ }
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 };
289
+ }
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,6 +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 { installMachResourceGuard } from './mach-resource-shim.js';
12
+ import { detectHarnessCrashSignature } from './test-harness-crash.js';
11
13
  // Re-export sub-modules so existing `from './mach.js'` imports keep working.
12
14
  export { attemptMozinfoRewrite, buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, } from './mach-build-artifacts.js';
13
15
  export { generateMozconfig } from './mach-mozconfig.js';
@@ -139,11 +141,63 @@ function surfaceMachErrorHints(result) {
139
141
  }
140
142
  }
141
143
  /**
142
- * Runs a full mach build. On a non-zero exit, any matched error hints are
143
- * surfaced on top of the raw mach output so operators get an actionable
144
- * nudge alongside the cryptic mozbuild traceback. Returns the captured
145
- * result so the caller (e.g. `fireforge build`) can inspect the tail
146
- * 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
147
201
  * successful!" — notably the stale `config.status is out of date`
148
202
  * notice that mach emits when a tool-managed edit landed on
149
203
  * `moz.configure` before the build.
@@ -152,29 +206,17 @@ function surfaceMachErrorHints(result) {
152
206
  * @returns Captured mach result (stdout tail, stderr tail, exit code)
153
207
  */
154
208
  export async function build(engineDir, jobs) {
155
- const args = ['build'];
156
- if (jobs !== undefined) {
157
- args.push('-j', String(jobs));
158
- }
159
- const result = await runMachInheritCapture(args, engineDir);
160
- if (result.exitCode !== 0) {
161
- surfaceMachErrorHints(result);
162
- }
163
- return result;
209
+ return runProtectedMachBuild('full', engineDir, { jobs });
164
210
  }
165
211
  /**
166
- * Runs a fast UI-only build. On a non-zero exit, any matched error hints are
167
- * surfaced on top of the raw mach output. See {@link build} for why the
168
- * 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.
169
215
  * @param engineDir - Path to the engine directory
170
216
  * @returns Captured mach result
171
217
  */
172
218
  export async function buildUI(engineDir) {
173
- const result = await runMachInheritCapture(['build', 'faster'], engineDir);
174
- if (result.exitCode !== 0) {
175
- surfaceMachErrorHints(result);
176
- }
177
- return result;
219
+ return runProtectedMachBuild('faster', engineDir);
178
220
  }
179
221
  /**
180
222
  * Runs an operation while holding a sidecar build lock keyed on the
@@ -318,7 +360,10 @@ export async function test(engineDir, testPaths = [], args = []) {
318
360
  * `fireforge test --perf-samples` to publish the artifact-path contract.
319
361
  */
320
362
  export async function testWithOutput(engineDir, testPaths = [], args = [], env) {
321
- 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
+ });
322
367
  }
323
368
  /**
324
369
  * Runs `mach xpcshell-test` (the suite-specific xpcshell command) while
@@ -331,7 +376,10 @@ export async function testWithOutput(engineDir, testPaths = [], args = [], env)
331
376
  * the dispatch path.
332
377
  */
333
378
  export async function xpcshellTestWithOutput(engineDir, testPaths = [], args = [], env) {
334
- 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
+ });
335
383
  }
336
384
  /**
337
385
  * Runs `mach mochitest` (covers browser-chrome / mochitest flavors) while
@@ -339,6 +387,9 @@ export async function xpcshellTestWithOutput(engineDir, testPaths = [], args = [
339
387
  * for non-xpcshell single-suite runs — see {@link xpcshellTestWithOutput}.
340
388
  */
341
389
  export async function mochitestWithOutput(engineDir, testPaths = [], args = [], env) {
342
- 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
+ });
343
394
  }
344
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.
@@ -1,3 +1,28 @@
1
+ /**
2
+ * Ordering comparator matching mozbuild's `StrictOrderingOnAppendList`
3
+ * (`mozbuild.util.UnsortedError`): entries are compared **case-insensitively**,
4
+ * so `HominisAppearanceController` (`appe`) sorts before
5
+ * `HominisAppMenuIntegration` (`appm`) even though a raw byte comparison
6
+ * places the uppercase `M` (0x4D) before the lowercase `e` (0x65).
7
+ *
8
+ * A naive `a > b` insertion landed the new entry in byte order, which
9
+ * `mach configure` then rejected with `UnsortedError`, aborting the build.
10
+ * Ties on the lower-cased key fall back to byte order so the comparison is
11
+ * total and stable.
12
+ */
13
+ export function mozbuildSortCompare(a, b) {
14
+ const la = a.toLowerCase();
15
+ const lb = b.toLowerCase();
16
+ if (la < lb)
17
+ return -1;
18
+ if (la > lb)
19
+ return 1;
20
+ if (a < b)
21
+ return -1;
22
+ if (a > b)
23
+ return 1;
24
+ return 0;
25
+ }
1
26
  /**
2
27
  * Inserts a line into an array of lines in alphabetical order within a
3
28
  * specified range. The comparison key is extracted from each line.
@@ -16,7 +41,7 @@ export function findAlphabeticalPosition(lines, startLine, endLine, newKey, extr
16
41
  existingKeys.push(key);
17
42
  }
18
43
  }
19
- const isSorted = existingKeys.every((k, idx) => idx === 0 || k.localeCompare(existingKeys[idx - 1] ?? '') >= 0);
44
+ const isSorted = existingKeys.every((k, idx) => idx === 0 || mozbuildSortCompare(k, existingKeys[idx - 1] ?? '') >= 0);
20
45
  if (!isSorted) {
21
46
  return { insertIndex: endLine, previousEntry: undefined };
22
47
  }
@@ -29,7 +54,7 @@ export function findAlphabeticalPosition(lines, startLine, endLine, newKey, extr
29
54
  const key = extractKey(line);
30
55
  if (key === undefined)
31
56
  continue;
32
- if (key > newKey) {
57
+ if (mozbuildSortCompare(key, newKey) > 0) {
33
58
  insertIndex = i;
34
59
  break;
35
60
  }
@@ -61,7 +86,7 @@ export function findAlphabeticalTokenPosition(tokens, sectionTargetPattern, newK
61
86
  continue;
62
87
  const match = sectionTargetPattern.exec(entry.parsed.target);
63
88
  const key = match?.[1] ?? entry.parsed.target;
64
- if (key > newKey) {
89
+ if (mozbuildSortCompare(key, newKey) > 0) {
65
90
  insertLineIndex = entry.lineIndex;
66
91
  break;
67
92
  }
@@ -84,7 +109,7 @@ export function findAlphabeticalMozBuildPosition(tokens, newKey) {
84
109
  let previousEntry;
85
110
  for (const item of items) {
86
111
  const key = item.parsed?.value ?? '';
87
- if (key > newKey) {
112
+ if (mozbuildSortCompare(key, newKey) > 0) {
88
113
  insertLineIndex = item.lineIndex;
89
114
  break;
90
115
  }
@@ -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>;