@hominis/fireforge 0.47.0 → 0.48.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.
- package/CHANGELOG.md +65 -0
- package/dist/bin/fireforge.js +11 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +156 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +10 -0
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export.js +4 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/test-harness-teardown.d.ts +62 -0
- package/dist/src/commands/test-harness-teardown.js +128 -0
- package/dist/src/commands/test-register.js +2 -1
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +36 -7
- package/dist/src/commands/test-verdict.d.ts +19 -4
- package/dist/src/commands/test-verdict.js +22 -4
- package/dist/src/commands/test.js +18 -10
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +7 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/file-lock.js +16 -9
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/harness-helper-pids.d.ts +98 -0
- package/dist/src/core/harness-helper-pids.js +240 -0
- package/dist/src/core/harness-orphans.d.ts +53 -8
- package/dist/src/core/harness-orphans.js +125 -44
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/mach.d.ts +16 -0
- package/dist/src/core/mach.js +6 -1
- package/dist/src/core/marionette-preflight.js +9 -3
- package/dist/src/core/parent-exit-watchdog.d.ts +62 -0
- package/dist/src/core/parent-exit-watchdog.js +69 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +8 -1
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +85 -11
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +26 -1
- package/dist/src/types/config.d.ts +17 -0
- package/dist/src/utils/process-group.d.ts +5 -1
- package/dist/src/utils/process-group.js +7 -3
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +49 -0
- package/dist/src/utils/process.d.ts +20 -0
- package/dist/src/utils/process.js +16 -3
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +88 -1
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +21 -4
- package/docs/machine-output.md +22 -9
- package/docs/patch-workflow.md +35 -1
- package/docs/testing.md +68 -4
- package/package.json +7 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Stdin-fed command execution with byte-exact stdout.
|
|
4
|
+
*
|
|
5
|
+
* Its own module rather than a function in `process.ts` only because that
|
|
6
|
+
* file sits on a 500-line budget. The logic belongs to the exec layer.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { buildChildEnv } from './child-env.js';
|
|
10
|
+
import { exitCodeFromClose } from './process.js';
|
|
11
|
+
/**
|
|
12
|
+
* Runs a command, writes `input` to its stdin, and returns stdout as raw
|
|
13
|
+
* bytes. For batch protocols (`git cat-file --batch`) whose framing counts
|
|
14
|
+
* bytes: the decoded, 50 MB-capped collector `exec` uses would corrupt
|
|
15
|
+
* both. The caller bounds the input, and so the output.
|
|
16
|
+
* @param command - Command to execute
|
|
17
|
+
* @param args - Command arguments
|
|
18
|
+
* @param input - Bytes written to stdin, which is then closed
|
|
19
|
+
* @param options - Execution options
|
|
20
|
+
*/
|
|
21
|
+
export async function execWithInput(command, args, input, options = {}) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const child = spawn(command, args, {
|
|
24
|
+
cwd: options.cwd,
|
|
25
|
+
env: buildChildEnv(options),
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
});
|
|
28
|
+
const out = [];
|
|
29
|
+
const err = [];
|
|
30
|
+
child.stdout.on('data', (chunk) => {
|
|
31
|
+
out.push(chunk);
|
|
32
|
+
});
|
|
33
|
+
child.stderr.on('data', (chunk) => {
|
|
34
|
+
err.push(chunk);
|
|
35
|
+
});
|
|
36
|
+
// A child that exits before reading all of stdin (a bad ref, say) makes
|
|
37
|
+
// the write fail with EPIPE. The close handler reports the real outcome.
|
|
38
|
+
child.stdin.on('error', () => undefined);
|
|
39
|
+
child.stdin.end(input);
|
|
40
|
+
child.on('error', reject);
|
|
41
|
+
child.on('close', (code, signal) => {
|
|
42
|
+
resolve({
|
|
43
|
+
stdout: Buffer.concat(out),
|
|
44
|
+
stderr: Buffer.concat(err).toString('utf8'),
|
|
45
|
+
exitCode: exitCodeFromClose(code, signal),
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -85,7 +85,27 @@ export interface ProcessGroupOptions {
|
|
|
85
85
|
* whenever this option is set.
|
|
86
86
|
*/
|
|
87
87
|
processGroup?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Receives the process-group id (the detached child's pid) right after a
|
|
90
|
+
* group-leader spawn. Lets a caller expose the group to a supervisor that
|
|
91
|
+
* cannot signal FireForge itself gracefully (`fireforge test --pgid-file`),
|
|
92
|
+
* or hold it for a teardown of its own. Not called when no group was
|
|
93
|
+
* created (`processGroup` unset, or win32).
|
|
94
|
+
*/
|
|
95
|
+
onProcessGroup?: (pgid: number) => void;
|
|
96
|
+
/**
|
|
97
|
+
* Extra teardown the caller owns, awaited after the child's `close` and
|
|
98
|
+
* the post-run group sweep, before the child's closure settles. Placed
|
|
99
|
+
* here rather than after the wrapper resolves so the bin signal handler's
|
|
100
|
+
* `waitForActiveChildShutdown` covers it too: a reap that ran after the
|
|
101
|
+
* wrapper resolved would race `process.exit` on a forwarded SIGTERM. Must
|
|
102
|
+
* never throw; a rejection is swallowed so it cannot mask the child's
|
|
103
|
+
* result.
|
|
104
|
+
*/
|
|
105
|
+
postCloseSweep?: () => Promise<void>;
|
|
88
106
|
}
|
|
107
|
+
/** A child's exit code, with a signal death mapped to 128 + its number. */
|
|
108
|
+
export declare function exitCodeFromClose(code: number | null, signal: NodeJS.Signals | null): number;
|
|
89
109
|
/**
|
|
90
110
|
* Executes a command and returns its output.
|
|
91
111
|
* @param command - Command to execute
|
|
@@ -77,7 +77,8 @@ function collectedOutput(out, err) {
|
|
|
77
77
|
truncated: out.wasTruncated() || err.wasTruncated(),
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
/** A child's exit code, with a signal death mapped to 128 + its number. */
|
|
81
|
+
export function exitCodeFromClose(code, signal) {
|
|
81
82
|
if (code !== null) {
|
|
82
83
|
return code;
|
|
83
84
|
}
|
|
@@ -154,6 +155,8 @@ async function spawnTracked(spec) {
|
|
|
154
155
|
detached: usesProcessGroup,
|
|
155
156
|
});
|
|
156
157
|
const groupPid = usesProcessGroup ? child.pid : undefined;
|
|
158
|
+
if (groupPid !== undefined)
|
|
159
|
+
options.onProcessGroup?.(groupPid);
|
|
157
160
|
spec.attach?.(child);
|
|
158
161
|
// Group-aware kills route to the whole tree. A non-group child is killed
|
|
159
162
|
// directly by the forwarder's own `child.kill`.
|
|
@@ -183,11 +186,21 @@ async function spawnTracked(spec) {
|
|
|
183
186
|
closure.settle();
|
|
184
187
|
resolve(spec.result(code, signal));
|
|
185
188
|
};
|
|
189
|
+
// Group sweep first (it takes the helpers that stayed in the group),
|
|
190
|
+
// then the caller's own sweep (the ones that did not), then settle.
|
|
191
|
+
const callerSweep = () => {
|
|
192
|
+
const sweep = options.postCloseSweep;
|
|
193
|
+
if (sweep === undefined) {
|
|
194
|
+
finish();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
sweep().then(finish, finish);
|
|
198
|
+
};
|
|
186
199
|
if (groupPid !== undefined) {
|
|
187
|
-
sweepProcessGroup(groupPid).then(
|
|
200
|
+
sweepProcessGroup(groupPid).then(callerSweep, callerSweep);
|
|
188
201
|
}
|
|
189
202
|
else {
|
|
190
|
-
|
|
203
|
+
callerSweep();
|
|
191
204
|
}
|
|
192
205
|
});
|
|
193
206
|
});
|
|
@@ -68,6 +68,17 @@ export declare function isJsonObject(value: JsonValue | undefined): value is Jso
|
|
|
68
68
|
* Accepts formats like "140.9.0", "140.9.1", "140.9.0esr", "147.0b1"
|
|
69
69
|
*/
|
|
70
70
|
export declare function isValidFirefoxVersion(version: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Orders two Firefox version strings semantically: major, minor and patch
|
|
73
|
+
* numerically, with a beta (`153.0b7`) before the release it precedes
|
|
74
|
+
* (`153.0`). An `esr` suffix does not change the order: `153.3.0esr` and
|
|
75
|
+
* `153.3.0` compare equal, since the product says which line it is. A
|
|
76
|
+
* lexical sort, which this replaces in `rebase`, put `153.10.0esr` before
|
|
77
|
+
* `153.2.0esr` and `99.0` after `140.0esr`. A string that is not a Firefox
|
|
78
|
+
* version sorts after every one that is, then lexically.
|
|
79
|
+
* @returns Negative when `a` is older, positive when newer, 0 when equal
|
|
80
|
+
*/
|
|
81
|
+
export declare function compareFirefoxVersions(a: string, b: string): number;
|
|
71
82
|
/**
|
|
72
83
|
* Validates a release-candidate build directory name.
|
|
73
84
|
* Accepts formats like "build1", "build2", "build12" (no leading zero).
|
|
@@ -100,6 +100,49 @@ export function isValidFirefoxVersion(version) {
|
|
|
100
100
|
// Beta: 147.0b1, 147.0b2
|
|
101
101
|
return /^[1-9]\d{0,2}\.\d+(?:b[1-9]\d*|\.\d+(?:esr)?|esr)?$/.test(version);
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Orders two Firefox version strings semantically: major, minor and patch
|
|
105
|
+
* numerically, with a beta (`153.0b7`) before the release it precedes
|
|
106
|
+
* (`153.0`). An `esr` suffix does not change the order: `153.3.0esr` and
|
|
107
|
+
* `153.3.0` compare equal, since the product says which line it is. A
|
|
108
|
+
* lexical sort, which this replaces in `rebase`, put `153.10.0esr` before
|
|
109
|
+
* `153.2.0esr` and `99.0` after `140.0esr`. A string that is not a Firefox
|
|
110
|
+
* version sorts after every one that is, then lexically.
|
|
111
|
+
* @returns Negative when `a` is older, positive when newer, 0 when equal
|
|
112
|
+
*/
|
|
113
|
+
export function compareFirefoxVersions(a, b) {
|
|
114
|
+
const left = parseFirefoxVersionOrder(a);
|
|
115
|
+
const right = parseFirefoxVersionOrder(b);
|
|
116
|
+
if (left === undefined || right === undefined) {
|
|
117
|
+
if (left !== undefined)
|
|
118
|
+
return -1;
|
|
119
|
+
if (right !== undefined)
|
|
120
|
+
return 1;
|
|
121
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
122
|
+
}
|
|
123
|
+
for (let i = 0; i < left.length; i++) {
|
|
124
|
+
const delta = (left[i] ?? 0) - (right[i] ?? 0);
|
|
125
|
+
if (delta !== 0)
|
|
126
|
+
return delta;
|
|
127
|
+
}
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* `[major, minor, patch, betaRank]`, where a release ranks above every beta
|
|
132
|
+
* of the same version.
|
|
133
|
+
*/
|
|
134
|
+
function parseFirefoxVersionOrder(version) {
|
|
135
|
+
const match = /^(\d+)\.(\d+)(?:\.(\d+))?(?:b(\d+))?(?:esr)?$/i.exec(version.trim());
|
|
136
|
+
if (!match)
|
|
137
|
+
return undefined;
|
|
138
|
+
const beta = match[4];
|
|
139
|
+
return [
|
|
140
|
+
Number(match[1]),
|
|
141
|
+
Number(match[2]),
|
|
142
|
+
Number(match[3] ?? 0),
|
|
143
|
+
beta === undefined ? Number.MAX_SAFE_INTEGER : Number(beta),
|
|
144
|
+
];
|
|
145
|
+
}
|
|
103
146
|
/**
|
|
104
147
|
* Validates a release-candidate build directory name.
|
|
105
148
|
* Accepts formats like "build1", "build2", "build12" (no leading zero).
|
package/docs/configuration.md
CHANGED
|
@@ -59,6 +59,19 @@ tool at
|
|
|
59
59
|
`/Applications/Xcode.app/Contents/Applications/Icon Composer.app/Contents/Executables/ictool`,
|
|
60
60
|
plus `actool` via `xcrun`, `sips`, and `iconutil`.
|
|
61
61
|
|
|
62
|
+
On macOS, `fireforge doctor` also checks that the bootstrapped clang
|
|
63
|
+
(`~/.mozbuild/clang`, or under `MOZBUILD_STATE_PATH`) can link against the
|
|
64
|
+
SDK the build will use. That is the SDK `engine/mozconfig` selects with
|
|
65
|
+
`--with-macos-sdk`, resolved by sourcing the mozconfig the way mach does,
|
|
66
|
+
or `xcrun --show-sdk-path` when it names none. The check links a trivial C
|
|
67
|
+
program through the toolchain's lld in a temp directory. When that fails, it
|
|
68
|
+
reports a warning with clang's first error and lists the SDKs installed
|
|
69
|
+
under the mozbuild state directory, the Command Line Tools and the active
|
|
70
|
+
Xcode. An Xcode update can ship an SDK the pinned toolchain cannot read, and
|
|
71
|
+
`mach configure` then dies at its linker probe with nothing but `config.log`
|
|
72
|
+
saying why. The check does nothing on other platforms and when no clang has
|
|
73
|
+
been bootstrapped.
|
|
74
|
+
|
|
62
75
|
## `patchLint`
|
|
63
76
|
|
|
64
77
|
Per-patch lint type-checks plain Firefox JS against a built-in
|
|
@@ -84,6 +97,28 @@ Firefox-globals shim that tracks upstream WebIDL additions per release.
|
|
|
84
97
|
project's, then `npx --no-install`. Left at `'off'`, formatting is out of
|
|
85
98
|
scope for the per-patch tier, and no other tier checks `.sys.mjs`
|
|
86
99
|
formatting either.
|
|
100
|
+
- `patchLint.forwardRegistration` (`'support-files'` or `'extended'`,
|
|
101
|
+
default `'support-files'`) picks which carriers the `forward-registration`
|
|
102
|
+
queue rule checks for a line naming a file that only a later patch
|
|
103
|
+
creates. The default checks test-manifest `support-files`. `'extended'`
|
|
104
|
+
also checks:
|
|
105
|
+
- jar.mn pair lines (the source in parentheses, relative to the jar.mn);
|
|
106
|
+
- moz.build path tokens (a quoted string with a `.` or `/`, relative to
|
|
107
|
+
the moz.build);
|
|
108
|
+
- test-manifest section headers and `head`;
|
|
109
|
+
- `customElements.js` `chrome://<pkg>/content/…` URLs, resolved through
|
|
110
|
+
the jar.mn lines the same queue adds.
|
|
111
|
+
|
|
112
|
+
A line whose target cannot be resolved without the build system is left
|
|
113
|
+
alone: a `%` or `@AB_CD@` substitution, `**`, `!`, a path that leaves the
|
|
114
|
+
tree, or a chrome URL no queue jar.mn line packages. Each finding prints
|
|
115
|
+
the same paste-and-run `patch staged-dependency --add --kind registration`
|
|
116
|
+
command as the `support-files` arm. The setting is opt-in because a queue
|
|
117
|
+
that predates it can carry many such edges, and a new error would red a
|
|
118
|
+
`--max-warnings 0` gate on install. The same census is on the public API as
|
|
119
|
+
`findForwardRegistrations(patchesDir, { scope })`, which defaults to
|
|
120
|
+
`'extended'`, for a consumer that keeps its own ratchet over these edges.
|
|
121
|
+
|
|
87
122
|
- `patchLint.fileSizeThresholds` tunes the `file-too-large` line counts per
|
|
88
123
|
file class:
|
|
89
124
|
|
|
@@ -105,6 +140,27 @@ Firefox-globals shim that tracks upstream WebIDL additions per release.
|
|
|
105
140
|
is a hard failure rather than a soft limit, which is what these thresholds
|
|
106
141
|
exist to let you move.
|
|
107
142
|
|
|
143
|
+
## `typecheck`
|
|
144
|
+
|
|
145
|
+
`typecheck.projects` lists the jsconfig files `fireforge typecheck` checks
|
|
146
|
+
as whole projects. `typecheck.extraShim` and `typecheck.projectOverrides`
|
|
147
|
+
add declarations to the Firefox-globals shim, for every project or for one.
|
|
148
|
+
When `patchLint.checkJs` is on, the same command also runs the per-patch
|
|
149
|
+
checkJs pass that `export` enforces, so a green `typecheck` means
|
|
150
|
+
export-clean types.
|
|
151
|
+
|
|
152
|
+
Each project keeps incremental build info in `.fireforge/typecheck/`, one
|
|
153
|
+
file per project. A warm run re-checks only the files whose content changed
|
|
154
|
+
since the last run. The file name carries a key over everything outside the
|
|
155
|
+
program's own files that can change a verdict: the composed shim, the
|
|
156
|
+
TypeScript version, the compiler options, `typecheck.undefinedIdentifiers`
|
|
157
|
+
and the FireForge version. A change to any of them selects a new file, so a
|
|
158
|
+
stale verdict is never replayed, and the project's other build-info files are
|
|
159
|
+
deleted. `fireforge typecheck --no-cache` checks every project from scratch
|
|
160
|
+
and neither reads nor writes the build info. A jsconfig's own `incremental`
|
|
161
|
+
and `tsBuildInfoFile` are still ignored, so nothing is written under
|
|
162
|
+
`engine/`.
|
|
163
|
+
|
|
108
164
|
## `patchPolicy`
|
|
109
165
|
|
|
110
166
|
`patchPolicy.ranges` makes `patch compact` range-aware, so closing ordinal
|
|
@@ -113,7 +169,13 @@ gaps does not move a patch out of the band its category owns.
|
|
|
113
169
|
## `test`
|
|
114
170
|
|
|
115
171
|
`test.canaryPath` and `test.canaryTimeoutSeconds` supply the defaults for
|
|
116
|
-
`fireforge test --canary`.
|
|
172
|
+
`fireforge test --canary`.
|
|
173
|
+
|
|
174
|
+
`test.reapOrphans` is `"report"` (default) or `"reap"`: what the preflight
|
|
175
|
+
census does with a harness helper that survived an earlier run in this
|
|
176
|
+
objdir. `"reap"` terminates it and stamps `orphans-reaped=<n>` on the verdict
|
|
177
|
+
line; `--reap-orphans` does the same for one invocation. The objdir anchor
|
|
178
|
+
rule is the same under both. See [`testing.md`](testing.md).
|
|
117
179
|
|
|
118
180
|
## `buildAudit`
|
|
119
181
|
|
|
@@ -219,6 +281,31 @@ non-CI launches print a warning saying so. Exit codes 12 and 13 separate a
|
|
|
219
281
|
console regression from a launch failure. See
|
|
220
282
|
[`exit-codes.md`](exit-codes.md).
|
|
221
283
|
|
|
284
|
+
A smoke run starts on a fresh temporary profile by default (since 0.48.0).
|
|
285
|
+
The step exists to prove what the build does on a clean start. The
|
|
286
|
+
developer's `obj-*/tmp/profile-default` carries whatever the last session
|
|
287
|
+
left (a pre-reset database, a restored hand-made applet), and those reds
|
|
288
|
+
say nothing about the tree. FireForge creates the directory under the OS
|
|
289
|
+
temp dir, seeds the two prefs `mach run` writes into profiles it makes
|
|
290
|
+
itself (`browser.shell.checkDefaultBrowser` and
|
|
291
|
+
`browser.aboutConfig.showWarning`), and passes it to Firefox as
|
|
292
|
+
`-profile`. The directory is removed when the run ends. That includes a
|
|
293
|
+
failed run and a SIGINT/SIGTERM, which the signal handler removes after
|
|
294
|
+
the browser tree has shut down. A FireForge that was SIGKILLed cannot clean
|
|
295
|
+
up after itself, so the next `run` removes any temporary profile whose
|
|
296
|
+
owning process is gone.
|
|
297
|
+
|
|
298
|
+
- `--profile <path>` launches with a named profile directory and leaves it
|
|
299
|
+
in place. Under `--smoke-exit` it replaces the fresh default.
|
|
300
|
+
- `--temp-profile` gives a plain `run` the same fresh, removed-afterwards
|
|
301
|
+
profile.
|
|
302
|
+
|
|
303
|
+
A plain `run` without either flag keeps mach's `profile-default`. `mach run`'s
|
|
304
|
+
desktop parser has no `--profile` option (its `--profile/-P` belongs to the
|
|
305
|
+
Android parser), so the profile is always passed as a program parameter.
|
|
306
|
+
With an explicit profile, mach neither writes its `runprefs` from
|
|
307
|
+
`~/.mozbuild/machrc` nor accepts `--setpref`.
|
|
308
|
+
|
|
222
309
|
The allowlist (`--console-allow-file`) is count-blind by default: a line that
|
|
223
310
|
matches an entry never counts, however often it fires. A `# max-hits: N`
|
|
224
311
|
comment directly above an entry (other comments may sit between) caps how
|
package/docs/exit-codes.md
CHANGED
|
@@ -15,7 +15,7 @@ which error class produces each one.
|
|
|
15
15
|
| 3 | `DOWNLOAD_ERROR` | Could not download or extract Firefox source. | `DownloadError` and subclasses |
|
|
16
16
|
| 4 | `GIT_ERROR` | A git operation failed. | `GitError` and subclasses, except `GitNotFoundError` |
|
|
17
17
|
| 5 | `BUILD_ERROR` | `mach build` failed, or a test suite went red. | `BuildError` and subclasses, `TestFailureError` |
|
|
18
|
-
| 6 | `PATCH_ERROR` | A patch failed to apply, or the patch queue could not be mutated.
|
|
18
|
+
| 6 | `PATCH_ERROR` | A patch failed to apply (including a patch `rebase --dry-run` predicts will reject), or the patch queue could not be mutated. | `PatchError`, `PatchManifestCorruptError`, `RebaseError` and subclasses |
|
|
19
19
|
| 7 | `MISSING_DEPENDENCY` | A required tool (python3, git, tar) was not found. | `PythonNotFoundError`, `GitNotFoundError`, `MachNotFoundError` |
|
|
20
20
|
| 8 | `INVALID_ARGUMENT` | The flags were wrong. A usage problem, not an environment one. | `InvalidArgumentError`, plus commander usage errors (unknown command or option, missing argument) |
|
|
21
21
|
| 9 | `FURNACE_ERROR` | A Furnace component operation failed. | `FurnaceError` |
|
|
@@ -190,9 +190,10 @@ command, read the decision table at the bottom first.
|
|
|
190
190
|
stages and unstages for real. Each of those commands therefore runs its
|
|
191
191
|
whole body inside `withPrivateGitIndex`, which seeds a temp
|
|
192
192
|
`GIT_INDEX_FILE` from the repository's own index so that every refresh
|
|
193
|
-
lands there and is discarded. `typecheck` additionally
|
|
194
|
-
`incremental
|
|
195
|
-
under `engine/`.
|
|
193
|
+
lands there and is discarded. `typecheck` additionally ignores a
|
|
194
|
+
jsconfig's own `incremental` and `tsBuildInfoFile`, so no sidecar is
|
|
195
|
+
written under `engine/`. Its incremental build info (0.48.0) lives in the
|
|
196
|
+
project's `.fireforge/typecheck/`, outside the engine. The scope fails open, meaning an unresolvable git dir
|
|
196
197
|
simply does not install it, and it is not reentrant. This exists so that
|
|
197
198
|
invariant 8 stays evidential: a concurrent `fireforge test` must not be
|
|
198
199
|
invalidated by FireForge's own read-only lane. Enforced by
|
|
@@ -284,7 +285,10 @@ Wait ergonomics are opt-in: `pollMaxMs` (the poll interval doubles from
|
|
|
284
285
|
`pollMs` up to the cap) and `waitProgressMs` with `onWaitProgress` (a periodic
|
|
285
286
|
progress callback carrying the holder's PID, liveness, and owner-metadata
|
|
286
287
|
lines from the pid file). All three are inert when unset, so existing callers
|
|
287
|
-
keep byte-identical behavior.
|
|
288
|
+
keep byte-identical behavior. Within one poll iteration a due progress probe
|
|
289
|
+
runs before the deadline check, so a wait that times out has always reported
|
|
290
|
+
the holder at least once when its budget exceeds `waitProgressMs`, however
|
|
291
|
+
slow the host (`file-lock.test.ts`, "budget shorter than one iteration").
|
|
288
292
|
|
|
289
293
|
### `src/core/file-lock-owner.ts`
|
|
290
294
|
|
|
@@ -430,10 +434,23 @@ On SIGINT or SIGTERM, `bin/fireforge.ts`:
|
|
|
430
434
|
- `waitForActiveCriticalSections(5 s)`, which holds the exit for in-flight
|
|
431
435
|
"apply plus persist" pairs (invariant 4). It is bounded so that a stuck
|
|
432
436
|
write cannot postpone the exit the user asked for.
|
|
437
|
+
- `waitForActiveChildShutdown(12 s)`, which holds the exit until every
|
|
438
|
+
tracked child has closed. For a process-group dispatch (`mach` under
|
|
439
|
+
`test`, `build`, `run`) "closed" includes the exec layer's post-close
|
|
440
|
+
work: the group sweep and the caller's `postCloseSweep` (the harness
|
|
441
|
+
helper reap in `fireforge test`). Both run before the child's closure
|
|
442
|
+
settles, so a forwarded SIGTERM cannot race `process.exit` past them.
|
|
433
443
|
3. Force-releases the furnace lock directories of active operations, since
|
|
434
444
|
`withFileLock`'s `finally` will never run past `process.exit`.
|
|
435
445
|
4. Calls `process.exit(130 | 143)`.
|
|
436
446
|
|
|
447
|
+
A parent that dies without signalling FireForge (SIGKILL on the `npm` above
|
|
448
|
+
it) enters no handler here. `fireforge test` covers that case itself with
|
|
449
|
+
the parent-exit watchdog (`src/core/parent-exit-watchdog.ts`): it writes the
|
|
450
|
+
killed verdict, reaps the mach group and the tracked helpers, and lets the
|
|
451
|
+
dispatch loop end the run; the process then exits through the ordinary
|
|
452
|
+
error path, not through this pipeline.
|
|
453
|
+
|
|
437
454
|
## Choosing a primitive for a new mutation
|
|
438
455
|
|
|
439
456
|
| Your operation | Use |
|
package/docs/machine-output.md
CHANGED
|
@@ -86,15 +86,15 @@ moved.
|
|
|
86
86
|
`FIREFORGE-VERDICT: FAIL reason=<reason>` uses a closed set
|
|
87
87
|
(`FireforgeVerdictReason`, `src/commands/test-verdict.ts`):
|
|
88
88
|
|
|
89
|
-
| Reason | Meaning
|
|
90
|
-
| --------------- |
|
|
91
|
-
| `crash` | The harness or the browser died, so there is no trustworthy suite result.
|
|
92
|
-
| `no-tests` | The run dispatched but nothing ran.
|
|
93
|
-
| `test-failures` | The suite ran and reported unexpected results.
|
|
94
|
-
| `preflight` | The run was refused before the harness was reached.
|
|
95
|
-
| `inconclusive` | A result exists, but `engine/` moved under it, so the result was discarded.
|
|
96
|
-
| `lock-timeout` | The run never started: the engine session lock stayed contended.
|
|
97
|
-
| `killed` | A signal terminated the run. Written from the signal handler so that a log tail always describes itself. A killed run must never be silent, or "killed", "still running" and "never started" all look alike. |
|
|
89
|
+
| Reason | Meaning |
|
|
90
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
91
|
+
| `crash` | The harness or the browser died, so there is no trustworthy suite result. |
|
|
92
|
+
| `no-tests` | The run dispatched but nothing ran. |
|
|
93
|
+
| `test-failures` | The suite ran and reported unexpected results. |
|
|
94
|
+
| `preflight` | The run was refused before the harness was reached. |
|
|
95
|
+
| `inconclusive` | A result exists, but `engine/` moved under it, so the result was discarded. |
|
|
96
|
+
| `lock-timeout` | The run never started: the engine session lock stayed contended. |
|
|
97
|
+
| `killed` | A signal terminated the run, or FireForge ended it because its parent process vanished. Written from the signal handler (or the parent-exit watchdog) so that a log tail always describes itself. A killed run must never be silent, or "killed", "still running" and "never started" all look alike. `signal=` names the cause: a signal name, or `parent-exit`. |
|
|
98
98
|
|
|
99
99
|
### The additive `note=` key
|
|
100
100
|
|
|
@@ -124,6 +124,19 @@ tasks _inside_ a file is harness code that reads the exported seed; FireForge
|
|
|
124
124
|
owns the seed, the plumbing and the record. Absent when the run did not
|
|
125
125
|
shuffle.
|
|
126
126
|
|
|
127
|
+
### The additive `orphans-reaped=` key
|
|
128
|
+
|
|
129
|
+
How many harness processes FireForge terminated itself during the run: at
|
|
130
|
+
preflight, survivors of an earlier run in this objdir under `--reap-orphans`
|
|
131
|
+
or `test.reapOrphans: "reap"`; at teardown, helpers this run's harness
|
|
132
|
+
launched (httpd, websocket server, ssltunnel, process bridge, the browser,
|
|
133
|
+
`moz-http2`) that were still alive after mach exited. Summed across the census, every
|
|
134
|
+
retry attempt and every shard. Absent when nothing was reaped, so a green on
|
|
135
|
+
a quiet machine prints the line it always did, and a green after a reap is
|
|
136
|
+
distinguishable from it. A run ended by SIGTERM or by the parent-exit
|
|
137
|
+
watchdog writes its verdict before the teardown reap runs, so that line never
|
|
138
|
+
carries the key; the reap is reported on stderr instead.
|
|
139
|
+
|
|
127
140
|
A preflight refusal's own text is written to stdout before the verdict line,
|
|
128
141
|
and into the run log, so the verdict stays the last stdout write while the
|
|
129
142
|
reason survives both a redirect and a `tail`. See
|
package/docs/patch-workflow.md
CHANGED
|
@@ -81,11 +81,23 @@ slugs, and bare slugs.
|
|
|
81
81
|
`lint --per-patch --max-warnings 0` is the warning-clean form for a release
|
|
82
82
|
gate.
|
|
83
83
|
|
|
84
|
+
`lint --per-patch --notices summary` prints one line per check in place of
|
|
85
|
+
every NOTICE row, for example `NOTICE [large-patch-lines]: 180 across 172
|
|
86
|
+
patch(es), 150 of them suppressed by lintIgnore (see --report)`. On a green
|
|
87
|
+
queue those rows are reviewed waivers and under-threshold measurements, and
|
|
88
|
+
there can be hundreds of them. The default, `--notices full`, prints them
|
|
89
|
+
as before. Errors and warnings always print in full, and the `--report`
|
|
90
|
+
JSON is the same in both modes.
|
|
91
|
+
|
|
84
92
|
`lint --per-patch --report <path>` writes a machine-readable JSON report with
|
|
85
93
|
each patch's line count, tier, active size thresholds, issues, and issues
|
|
86
94
|
suppressed by `lintIgnore`. The size metrics (`countNonBinaryDiffLines`,
|
|
87
95
|
`resolvePatchSizeTier`, `getPatchSizeThresholds`) are also exported on the
|
|
88
|
-
programmatic API.
|
|
96
|
+
programmatic API. So is `filesMeasuredByFileTooLarge(patchText)`, which
|
|
97
|
+
returns the files `file-too-large` measures in a patch: the JS files it
|
|
98
|
+
creates, and never a modified file or a new file of another kind. The rule
|
|
99
|
+
selects its files through that same function, so an audit that uses it
|
|
100
|
+
cannot drift from the rule.
|
|
89
101
|
|
|
90
102
|
Per-patch lint type-checks plain Firefox JS against a built-in
|
|
91
103
|
Firefox-globals shim that tracks upstream WebIDL additions per release. See
|
|
@@ -116,6 +128,28 @@ npx fireforge download --force
|
|
|
116
128
|
npx fireforge rebase
|
|
117
129
|
```
|
|
118
130
|
|
|
131
|
+
`download --force` is the destructive step of the three, not `rebase`. It
|
|
132
|
+
replaces `engine/` wholesale, so applied patches, unexported edits and
|
|
133
|
+
commits made in `engine/` are gone once the new tree is active. When the
|
|
134
|
+
engine it is about to replace has changed paths against HEAD, or HEAD has
|
|
135
|
+
moved off the recorded base commit, it names the loss (how many changed
|
|
136
|
+
paths match the patch queue and how many do not) and asks first. A
|
|
137
|
+
non-interactive run refuses unless `--yes` is given. A clean engine is
|
|
138
|
+
replaced without a prompt.
|
|
139
|
+
|
|
140
|
+
`npx fireforge rebase --dry-run` replays the queue onto the engine's HEAD
|
|
141
|
+
without touching `engine/`. It seeds a private index from HEAD and applies
|
|
142
|
+
each patch there with `git apply --cached`, in queue order, walking the same
|
|
143
|
+
context-reduction ladder as the real run (exact, then `-C2`, `-C1`, `-C0`,
|
|
144
|
+
bounded by `--max-fuzz`). Each patch is reported as `clean`, `reduced
|
|
145
|
+
context` (with the step it needed) or `reject` (with the files git named).
|
|
146
|
+
It exits 6 when anything rejects. A reject after an earlier one is marked
|
|
147
|
+
as possibly cascading from it, since the replay continues without the
|
|
148
|
+
rejected patch. Run it after the download, when HEAD is the new source. The
|
|
149
|
+
header names the oldest patch stamp as "from", compared as versions rather
|
|
150
|
+
than strings, prints the spread when the queue carries several stamps, and
|
|
151
|
+
warns when `engine/` is not yet on the pinned version.
|
|
152
|
+
|
|
119
153
|
If a patch fails, fix the reject inside `engine/`, then run
|
|
120
154
|
`npx fireforge rebase --continue`. A rebase leaves the engine mutated on
|
|
121
155
|
purpose, so its apply-and-persist pairs are held across a signal rather than
|
package/docs/testing.md
CHANGED
|
@@ -87,21 +87,85 @@ a slow build for a silently wrong test.
|
|
|
87
87
|
earlier run in this project's objdir. It runs at preflight, before this run
|
|
88
88
|
spawns anything, so every hit is necessarily a survivor. A match has to be
|
|
89
89
|
objdir-anchored, since `xpcshell` and `server.js` are far too generic to
|
|
90
|
-
report on their own
|
|
90
|
+
report on their own, and structural: the process's executable is a helper
|
|
91
|
+
binary, or an interpreter whose script argument is a helper script. A
|
|
92
|
+
process that merely mentions such a path (an editor, a `grep`, the shell
|
|
93
|
+
FireForge runs under) is not a helper, and FireForge's own ancestors are
|
|
94
|
+
never candidates. Survivors slow every later run without appearing in
|
|
91
95
|
its output. The usual symptom is a three-second suite taking minutes of
|
|
92
|
-
wall clock. The census is report-only by default
|
|
93
|
-
|
|
96
|
+
wall clock. The census is report-only by default; `--reap-orphans`, or
|
|
97
|
+
`test.reapOrphans: "reap"` in `fireforge.json`, terminates what it finds
|
|
98
|
+
(SIGTERM, a 500 ms grace, SIGKILL). It never refuses a run. Every reap is
|
|
99
|
+
stamped on the verdict line as `orphans-reaped=<n>`. A different shape,
|
|
94
100
|
reparented Python `multiprocessing` workers, is covered by the
|
|
95
101
|
`Orphaned harness workers` doctor check.
|
|
102
|
+
- Every dispatch owns its own kill path. The harness announces what it
|
|
103
|
+
launches (`runtests.py | Server pid: <n>`, the websocket server, the SSL
|
|
104
|
+
tunnel, the websocket/process bridge, and the browser as `Application
|
|
105
|
+
pid`); FireForge tracks those pids and, after mach has exited for any
|
|
106
|
+
reason (a clean finish, a harness crash before a retry, a no-output
|
|
107
|
+
timeout, a forwarded SIGTERM, the parent-exit watchdog below), terminates
|
|
108
|
+
any of them still alive. A tracked pid is re-read from `ps` first and must
|
|
109
|
+
still be anchored to this objdir and still look like the thing that was
|
|
110
|
+
launched, so a recycled pid is never signalled. One helper is never
|
|
111
|
+
announced: mozserve starts `moz-http2` in a session of its own and logs no
|
|
112
|
+
pid, so it is in neither the group nor the announcements. For it, FireForge
|
|
113
|
+
snapshots the same-objdir helpers before the dispatch spawns and, after
|
|
114
|
+
mach exits, reaps any helper-shaped process under this checkout's absolute
|
|
115
|
+
objdir that is new since that snapshot and reparented to launchd. An
|
|
116
|
+
earlier run's survivor is in the snapshot and stays the census's call. This
|
|
117
|
+
reap runs inside the exec layer's close path, so the signal handler's
|
|
118
|
+
bounded child-shutdown wait covers it. The count joins `orphans-reaped=`.
|
|
119
|
+
- FireForge watches its own parent while a dispatch runs. A supervisor that
|
|
120
|
+
SIGKILLs the process above `fireforge test` (an `npm run` step killed at
|
|
121
|
+
its bound) signals nothing below it; FireForge notices the reparenting
|
|
122
|
+
within about two seconds, writes `FIREFORGE-VERDICT: FAIL reason=killed
|
|
123
|
+
signal=parent-exit`, reaps the harness process group and the tracked
|
|
124
|
+
helpers, and ends the run without classifying or retrying.
|
|
96
125
|
- `--perf-samples <path>` publishes a perf-sample artifact path to the
|
|
97
126
|
harness (exported as `<BINARYNAME>_PERF_SAMPLE_JSON`).
|
|
98
127
|
|
|
128
|
+
## Process groups and supervisors
|
|
129
|
+
|
|
130
|
+
`fireforge test` spawns mach as the leader of its **own** process group
|
|
131
|
+
(`detached: true`), so that FireForge can signal the whole harness tree with
|
|
132
|
+
one negative-pid kill and sweep the group after mach exits. Two consequences
|
|
133
|
+
for anything that supervises `fireforge test`:
|
|
134
|
+
|
|
135
|
+
- A supervisor that kills _its own_ process group (`kill -- -<npm pid>`) does
|
|
136
|
+
not reach mach: mach is not in that group. Killing the `npm`/`node` chain
|
|
137
|
+
above FireForge leaves the harness tree running.
|
|
138
|
+
- SIGTERM to FireForge is the right signal. FireForge forwards it to the
|
|
139
|
+
mach group, escalates to SIGKILL after a grace period, sweeps the group,
|
|
140
|
+
reaps the tracked helpers, and writes `FAIL reason=killed signal=SIGTERM`.
|
|
141
|
+
(That line, like the watchdog's `signal=parent-exit` line, is written
|
|
142
|
+
before the teardown reap runs, so it cannot carry `orphans-reaped=`; the
|
|
143
|
+
reap reports to stderr.)
|
|
144
|
+
- A supervisor that has to SIGKILL FireForge should pass
|
|
145
|
+
`--pgid-file <path>`. FireForge rewrites the file with the harness group id
|
|
146
|
+
on every mach spawn (each retry attempt and shard) and removes it when the
|
|
147
|
+
run ends under its own control. After a SIGKILL the file is still there:
|
|
148
|
+
`kill -TERM -- -$(cat <path>)`, then `-KILL`, takes mach and every helper
|
|
149
|
+
that shares its group. Two processes are outside it: the browser
|
|
150
|
+
(mozprocess puts it in a group of its own) and `moz-http2` (mozserve
|
|
151
|
+
starts it in its own session). A SIGTERM to the group gives `runtests.py`
|
|
152
|
+
the chance to run its own cleanup, which normally takes the browser with
|
|
153
|
+
it; nothing takes `moz-http2`, so after a SIGKILL of FireForge it stays
|
|
154
|
+
until the next run's census, which is why that run should carry the reap
|
|
155
|
+
posture. A browser that does survive is the Marionette preflight's and
|
|
156
|
+
`--kill-stale-marionette`'s to remove.
|
|
157
|
+
|
|
158
|
+
The mochitest helpers (httpd, websocket server, ssltunnel, process bridge)
|
|
159
|
+
are plain children of `runtests.py` and share mach's group. The browser and
|
|
160
|
+
`moz-http2` do not, which is why the teardown reap above exists alongside the
|
|
161
|
+
group sweep.
|
|
162
|
+
|
|
99
163
|
## The verdict line
|
|
100
164
|
|
|
101
165
|
Every test run ends with one machine-readable line:
|
|
102
166
|
|
|
103
167
|
```
|
|
104
|
-
FIREFORGE-VERDICT: PASS|FAIL reason=… [note=<class>] [shuffle=<seed>] [log=<path>]
|
|
168
|
+
FIREFORGE-VERDICT: PASS|FAIL reason=… [note=<class>] [shuffle=<seed>] [orphans-reaped=<n>] [log=<path>]
|
|
105
169
|
```
|
|
106
170
|
|
|
107
171
|
Automation should branch on this line rather than on the raw process code,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hominis/fireforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "FireForge — a build tool for customizing Firefox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -75,9 +75,9 @@
|
|
|
75
75
|
"eslint-plugin-simple-import-sort": "^14.0.0",
|
|
76
76
|
"fast-check": "^4.6.0",
|
|
77
77
|
"husky": "^9.1.7",
|
|
78
|
-
"knip": "6.
|
|
78
|
+
"knip": "6.37.0",
|
|
79
79
|
"lint-staged": "^17.0.4",
|
|
80
|
-
"prettier": "3.9.
|
|
80
|
+
"prettier": "3.9.8",
|
|
81
81
|
"tsx": "^4.7.0",
|
|
82
82
|
"typescript": "~6.0.0",
|
|
83
83
|
"typescript-eslint": "^8.0.0",
|
|
@@ -108,5 +108,9 @@
|
|
|
108
108
|
"publishConfig": {
|
|
109
109
|
"access": "public",
|
|
110
110
|
"provenance": true
|
|
111
|
+
},
|
|
112
|
+
"allowScripts": {
|
|
113
|
+
"esbuild@0.28.1": true,
|
|
114
|
+
"fsevents@2.3.3": true
|
|
111
115
|
}
|
|
112
116
|
}
|