@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.
- package/CHANGELOG.md +16 -0
- package/dist/src/commands/build.js +9 -1
- package/dist/src/commands/doctor-furnace-jar.d.ts +16 -0
- package/dist/src/commands/doctor-furnace-jar.js +47 -0
- package/dist/src/commands/doctor-furnace.js +2 -0
- package/dist/src/commands/export-flow.js +4 -1
- package/dist/src/commands/export-placement-gate.js +4 -1
- package/dist/src/commands/export.js +61 -4
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +20 -0
- package/dist/src/commands/furnace/chrome-doc-templates.js +52 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +10 -0
- package/dist/src/commands/furnace/chrome-doc.js +31 -4
- package/dist/src/commands/furnace/create-browser-test.d.ts +30 -0
- package/dist/src/commands/furnace/create-browser-test.js +180 -0
- package/dist/src/commands/furnace/create-mochikit.js +11 -4
- package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
- package/dist/src/commands/furnace/create-xpcshell.js +38 -12
- package/dist/src/commands/furnace/create.js +7 -114
- package/dist/src/commands/furnace/index.js +6 -1
- package/dist/src/commands/furnace/scan.d.ts +1 -0
- package/dist/src/commands/furnace/scan.js +57 -27
- package/dist/src/commands/furnace/validate.js +17 -1
- package/dist/src/commands/re-export-options.d.ts +26 -0
- package/dist/src/commands/re-export-options.js +48 -1
- package/dist/src/commands/re-export.js +4 -1
- package/dist/src/commands/register.js +10 -1
- package/dist/src/commands/test-diagnose.js +12 -0
- package/dist/src/commands/test.js +29 -22
- package/dist/src/core/furnace-registration.d.ts +24 -0
- package/dist/src/core/furnace-registration.js +56 -3
- package/dist/src/core/furnace-validate-registration.js +18 -0
- package/dist/src/core/mach-resource-shim.d.ts +70 -16
- package/dist/src/core/mach-resource-shim.js +310 -52
- package/dist/src/core/mach.d.ts +51 -10
- package/dist/src/core/mach.js +76 -32
- package/dist/src/core/manifest-helpers.d.ts +13 -0
- package/dist/src/core/manifest-helpers.js +1 -1
- package/dist/src/core/manifest-rules.d.ts +13 -3
- package/dist/src/core/manifest-rules.js +44 -17
- package/dist/src/core/patch-export.d.ts +10 -0
- package/dist/src/core/patch-export.js +23 -2
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-module.js +15 -2
- package/dist/src/core/register-result.d.ts +9 -0
- package/dist/src/core/register-scaffold.d.ts +55 -0
- package/dist/src/core/register-scaffold.js +146 -0
- package/dist/src/core/register-test-manifest.js +3 -1
- package/dist/src/core/register-xpcshell-test.d.ts +30 -0
- package/dist/src/core/register-xpcshell-test.js +96 -0
- package/dist/src/core/test-harness-crash.d.ts +26 -1
- package/dist/src/core/test-harness-crash.js +149 -11
- package/dist/src/core/xpcshell-appdir.d.ts +7 -0
- package/dist/src/core/xpcshell-appdir.js +12 -3
- package/dist/src/types/commands/options.d.ts +12 -0
- package/package.json +3 -3
|
@@ -1,92 +1,350 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* Resource-monitor degrade
|
|
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`)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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 {
|
|
46
|
+
import { readdir } from 'node:fs/promises';
|
|
47
|
+
import { homedir, tmpdir } from 'node:os';
|
|
22
48
|
import { delimiter, join } from 'node:path';
|
|
23
|
-
import {
|
|
24
|
-
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
|
31
|
-
# Degrades
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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(
|
|
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-<x>/_virtualenvs`)
|
|
287
|
+
* and the mach state-dir venvs (under `$MOZBUILD_STATE_PATH` or
|
|
288
|
+
* `~/.mozbuild`, in `srcdirs/<hash>/_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`
|
|
69
|
-
* temp directory and returns that directory. Idempotent —
|
|
70
|
-
*
|
|
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
|
|
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'),
|
|
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
|
|
81
|
-
*
|
|
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
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
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
|
package/dist/src/core/mach.d.ts
CHANGED
|
@@ -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
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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<
|
|
114
|
+
export declare function build(engineDir: string, jobs?: number): Promise<ProtectedMachBuildResult>;
|
|
74
115
|
/**
|
|
75
|
-
* Runs a fast UI-only build
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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<
|
|
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`
|
package/dist/src/core/mach.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
171
|
-
*
|
|
172
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|