@hominis/fireforge 0.16.5 → 0.18.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 +56 -0
- package/README.md +46 -24
- package/dist/src/commands/build.js +33 -10
- package/dist/src/commands/config.js +32 -20
- package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +18 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +159 -0
- package/dist/src/commands/doctor-furnace.js +2 -0
- package/dist/src/commands/doctor-working-tree.d.ts +29 -0
- package/dist/src/commands/doctor-working-tree.js +93 -0
- package/dist/src/commands/doctor.js +23 -12
- package/dist/src/commands/export-all.js +11 -3
- package/dist/src/commands/export-shared.d.ts +7 -1
- package/dist/src/commands/export-shared.js +21 -3
- package/dist/src/commands/furnace/chrome-doc-tests.js +9 -2
- package/dist/src/commands/furnace/create-templates.d.ts +11 -0
- package/dist/src/commands/furnace/create-templates.js +11 -2
- package/dist/src/commands/furnace/init.js +97 -9
- package/dist/src/commands/furnace/override.js +23 -13
- package/dist/src/commands/furnace/remove.js +8 -0
- package/dist/src/commands/furnace/rename.js +133 -4
- package/dist/src/commands/lint.js +70 -6
- package/dist/src/commands/patch/delete.js +4 -1
- package/dist/src/commands/patch/reorder.js +4 -1
- package/dist/src/commands/re-export-files.js +3 -1
- package/dist/src/commands/re-export.js +4 -1
- package/dist/src/commands/register.js +11 -0
- package/dist/src/commands/resolve.d.ts +25 -1
- package/dist/src/commands/resolve.js +25 -15
- package/dist/src/commands/status.js +100 -122
- package/dist/src/commands/test.js +68 -14
- package/dist/src/commands/token-coverage.js +10 -3
- package/dist/src/commands/wire.js +50 -8
- package/dist/src/core/browser-wire.js +21 -4
- package/dist/src/core/build-audit.js +10 -0
- package/dist/src/core/config.d.ts +33 -0
- package/dist/src/core/config.js +43 -0
- package/dist/src/core/furnace-config.d.ts +23 -2
- package/dist/src/core/furnace-config.js +26 -3
- package/dist/src/core/git-diff.js +21 -2
- package/dist/src/core/mach.d.ts +43 -6
- package/dist/src/core/mach.js +57 -7
- package/dist/src/core/manifest-rules.js +10 -1
- package/dist/src/core/manifest-tokenizers.d.ts +6 -0
- package/dist/src/core/manifest-tokenizers.js +28 -0
- package/dist/src/core/marionette-port.d.ts +50 -0
- package/dist/src/core/marionette-port.js +215 -0
- package/dist/src/core/patch-lint.d.ts +47 -2
- package/dist/src/core/patch-lint.js +89 -14
- package/dist/src/core/patch-manifest-consistency.d.ts +21 -1
- package/dist/src/core/patch-manifest-consistency.js +31 -3
- package/dist/src/core/patch-manifest-io.js +10 -0
- package/dist/src/core/patch-manifest-resolve.d.ts +20 -1
- package/dist/src/core/patch-manifest-resolve.js +29 -2
- package/dist/src/core/patch-manifest-validate.js +25 -1
- package/dist/src/core/status-classify.d.ts +54 -0
- package/dist/src/core/status-classify.js +134 -0
- package/dist/src/core/token-coverage.js +24 -0
- package/dist/src/core/token-dark-mode.d.ts +49 -0
- package/dist/src/core/token-dark-mode.js +182 -0
- package/dist/src/core/token-manager.js +17 -33
- package/dist/src/core/wire-destroy.d.ts +7 -3
- package/dist/src/core/wire-destroy.js +11 -6
- package/dist/src/core/wire-dom-fragment.d.ts +17 -0
- package/dist/src/core/wire-dom-fragment.js +40 -0
- package/dist/src/core/wire-init.d.ts +9 -3
- package/dist/src/core/wire-init.js +18 -6
- package/dist/src/core/wire-subscript.d.ts +7 -3
- package/dist/src/core/wire-subscript.js +11 -4
- package/dist/src/types/commands/patches.d.ts +23 -0
- package/dist/src/types/furnace.d.ts +9 -0
- package/dist/src/utils/parse.d.ts +7 -0
- package/dist/src/utils/parse.js +15 -0
- package/package.json +1 -1
package/dist/src/core/config.js
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* config-mutate.ts — immutable config mutation
|
|
9
9
|
* config-state.ts — state file management
|
|
10
10
|
*/
|
|
11
|
+
import { basename } from 'node:path';
|
|
11
12
|
import { ConfigError, ConfigNotFoundError } from '../errors/config.js';
|
|
12
13
|
import { toError } from '../utils/errors.js';
|
|
13
14
|
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
14
15
|
import { getProjectPaths } from './config-paths.js';
|
|
15
16
|
import { validateConfig } from './config-validate.js';
|
|
17
|
+
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
16
18
|
// ---- re-exports ----
|
|
17
19
|
export { mutateConfig } from './config-mutate.js';
|
|
18
20
|
export { CONFIG_FILENAME, CONFIGS_DIR, ENGINE_DIR, FIREFORGE_DIR, getProjectPaths, PATCHES_DIR, SRC_DIR, STATE_FILENAME, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, } from './config-paths.js';
|
|
@@ -97,9 +99,50 @@ export async function writeConfig(root, config) {
|
|
|
97
99
|
* Writes a raw config document to fireforge.json.
|
|
98
100
|
* This is used by CLI `config --force`, where callers may intentionally write
|
|
99
101
|
* keys or value shapes outside the validated FireForgeConfig schema.
|
|
102
|
+
*
|
|
103
|
+
* Individual writes are atomic via {@link writeJson} (temp file + rename),
|
|
104
|
+
* but atomicity alone does not prevent lost updates across concurrent
|
|
105
|
+
* writers: each writer reads an old copy, mutates its own in-memory view,
|
|
106
|
+
* and writes it back, so the second writer's rename clobbers the first
|
|
107
|
+
* writer's changes. Callers that do read → mutate → write must hold
|
|
108
|
+
* {@link withConfigFileLock} for the full round-trip to serialise
|
|
109
|
+
* against other writers.
|
|
100
110
|
*/
|
|
101
111
|
export async function writeConfigDocument(root, config) {
|
|
102
112
|
const paths = getProjectPaths(root);
|
|
103
113
|
await writeJson(paths.config, config);
|
|
104
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Runs an operation while holding a sidecar lock on `fireforge.json`.
|
|
117
|
+
*
|
|
118
|
+
* Motivating case (2026-04-21 eval): two concurrent `fireforge config
|
|
119
|
+
* <key> <value>` invocations each ran load → mutate → writeJson against
|
|
120
|
+
* the same on-disk fireforge.json. The second rename landed after the
|
|
121
|
+
* first, silently dropping the first writer's key — both commands exited
|
|
122
|
+
* `0`, but only one change survived. This helper turns the same
|
|
123
|
+
* read-modify-write sequence into a serialised operation so a concurrent
|
|
124
|
+
* writer now waits for the lock rather than racing on the document.
|
|
125
|
+
*
|
|
126
|
+
* Reads (`loadConfig`, `loadRawConfigDocument`) stay lock-free: writers
|
|
127
|
+
* always use `writeJson`'s atomic temp-file + rename, so a reader observes
|
|
128
|
+
* either the pre- or post-write document but never a torn file. The lock
|
|
129
|
+
* only serialises writers against other writers.
|
|
130
|
+
*
|
|
131
|
+
* The lock is a sidecar directory `${config}.fireforge-config.lock`, and
|
|
132
|
+
* `withFileLock` handles stale-lock recovery (PID-alive probe, age-based
|
|
133
|
+
* fallback) — a crashed writer does not permanently block future writes.
|
|
134
|
+
*
|
|
135
|
+
* @param root - Root directory of the project
|
|
136
|
+
* @param operation - Async function to run while holding the lock
|
|
137
|
+
* @returns Whatever the operation returns
|
|
138
|
+
*/
|
|
139
|
+
export async function withConfigFileLock(root, operation) {
|
|
140
|
+
const paths = getProjectPaths(root);
|
|
141
|
+
return withFileLock(createSiblingLockPath(paths.config, '.fireforge-config.lock'), operation, {
|
|
142
|
+
onTimeoutMessage: `Timed out waiting to update ${basename(paths.config)}. ` +
|
|
143
|
+
'If no other fireforge process is running, remove the stale lock directory and retry.',
|
|
144
|
+
onStaleLockMessage: (ageMs) => `Removing stale FireForge config lock for ${basename(paths.config)} ` +
|
|
145
|
+
`(age: ${Math.round(ageMs / 1000)}s). A previous fireforge process may have crashed.`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
105
148
|
//# sourceMappingURL=config.js.map
|
|
@@ -111,9 +111,30 @@ export declare function writeFurnaceConfig(root: string, config: FurnaceConfig):
|
|
|
111
111
|
export declare function stampFurnaceOverrideBaseVersions(root: string, version: string): Promise<number>;
|
|
112
112
|
/**
|
|
113
113
|
* Creates a default furnace configuration.
|
|
114
|
-
*
|
|
114
|
+
*
|
|
115
|
+
* When a `binaryName` is provided, the default config carries a
|
|
116
|
+
* `tokenPrefix` derived as `--<binaryName>-`. Without that default,
|
|
117
|
+
* `fireforge token coverage` on a fresh project reports `0 tokens` and
|
|
118
|
+
* labels every custom-property reference as `unknown` — the scan has
|
|
119
|
+
* no prefix to key off. The 2026-04-21 eval walked directly into this
|
|
120
|
+
* state (`furnace init` → `token add` → `token coverage` → zero
|
|
121
|
+
* tokens), and only recovered after hand-editing furnace.json. Deriving
|
|
122
|
+
* the prefix from the binary name matches the convention the scaffolded
|
|
123
|
+
* tokens CSS already uses for its `--<binaryName>-*` declarations.
|
|
124
|
+
*
|
|
125
|
+
* `validateFurnaceConfig` treats `tokenPrefix` as optional, so callers
|
|
126
|
+
* on the legacy no-arg call shape (existing tests, programmatic callers
|
|
127
|
+
* bootstrapping from a not-yet-loaded config) still get a valid config
|
|
128
|
+
* without a prefix; the CLI init path always has a `binaryName` from
|
|
129
|
+
* `fireforge.json` and always sets one.
|
|
130
|
+
*
|
|
131
|
+
* @param options - Optional init context; pass `{ binaryName }` to
|
|
132
|
+
* derive the token prefix.
|
|
133
|
+
* @returns A valid FurnaceConfig
|
|
115
134
|
*/
|
|
116
|
-
export declare function createDefaultFurnaceConfig(
|
|
135
|
+
export declare function createDefaultFurnaceConfig(options?: {
|
|
136
|
+
binaryName?: string;
|
|
137
|
+
}): FurnaceConfig;
|
|
117
138
|
/**
|
|
118
139
|
* Loads furnace config if it exists, or creates and writes a default config.
|
|
119
140
|
* @param root - Root directory of the project
|
|
@@ -460,16 +460,39 @@ export async function stampFurnaceOverrideBaseVersions(root, version) {
|
|
|
460
460
|
}
|
|
461
461
|
/**
|
|
462
462
|
* Creates a default furnace configuration.
|
|
463
|
-
*
|
|
463
|
+
*
|
|
464
|
+
* When a `binaryName` is provided, the default config carries a
|
|
465
|
+
* `tokenPrefix` derived as `--<binaryName>-`. Without that default,
|
|
466
|
+
* `fireforge token coverage` on a fresh project reports `0 tokens` and
|
|
467
|
+
* labels every custom-property reference as `unknown` — the scan has
|
|
468
|
+
* no prefix to key off. The 2026-04-21 eval walked directly into this
|
|
469
|
+
* state (`furnace init` → `token add` → `token coverage` → zero
|
|
470
|
+
* tokens), and only recovered after hand-editing furnace.json. Deriving
|
|
471
|
+
* the prefix from the binary name matches the convention the scaffolded
|
|
472
|
+
* tokens CSS already uses for its `--<binaryName>-*` declarations.
|
|
473
|
+
*
|
|
474
|
+
* `validateFurnaceConfig` treats `tokenPrefix` as optional, so callers
|
|
475
|
+
* on the legacy no-arg call shape (existing tests, programmatic callers
|
|
476
|
+
* bootstrapping from a not-yet-loaded config) still get a valid config
|
|
477
|
+
* without a prefix; the CLI init path always has a `binaryName` from
|
|
478
|
+
* `fireforge.json` and always sets one.
|
|
479
|
+
*
|
|
480
|
+
* @param options - Optional init context; pass `{ binaryName }` to
|
|
481
|
+
* derive the token prefix.
|
|
482
|
+
* @returns A valid FurnaceConfig
|
|
464
483
|
*/
|
|
465
|
-
export function createDefaultFurnaceConfig() {
|
|
466
|
-
|
|
484
|
+
export function createDefaultFurnaceConfig(options = {}) {
|
|
485
|
+
const config = {
|
|
467
486
|
version: 1,
|
|
468
487
|
componentPrefix: 'moz-',
|
|
469
488
|
stock: [],
|
|
470
489
|
overrides: {},
|
|
471
490
|
custom: {},
|
|
472
491
|
};
|
|
492
|
+
if (options.binaryName && options.binaryName.length > 0) {
|
|
493
|
+
config.tokenPrefix = `--${options.binaryName}-`;
|
|
494
|
+
}
|
|
495
|
+
return config;
|
|
473
496
|
}
|
|
474
497
|
/**
|
|
475
498
|
* Loads furnace config if it exists, or creates and writes a default config.
|
|
@@ -9,7 +9,7 @@ import { verbose } from '../utils/logger.js';
|
|
|
9
9
|
import { exec } from '../utils/process.js';
|
|
10
10
|
import { ensureGit, git } from './git-base.js';
|
|
11
11
|
import { fileExistsInHead } from './git-file-ops.js';
|
|
12
|
-
import { getUntrackedFiles } from './git-status.js';
|
|
12
|
+
import { getUntrackedFiles, getUntrackedFilesInDir } from './git-status.js';
|
|
13
13
|
async function execGitWithAllowedExitCodes(repoDir, args, allowedExitCodes = [0]) {
|
|
14
14
|
const result = await exec('git', args, { cwd: repoDir });
|
|
15
15
|
if (allowedExitCodes.includes(result.exitCode)) {
|
|
@@ -183,7 +183,26 @@ export async function getAllDiff(repoDir) {
|
|
|
183
183
|
*/
|
|
184
184
|
export async function getDiffForFilesAgainstHead(repoDir, files) {
|
|
185
185
|
await ensureGit();
|
|
186
|
-
|
|
186
|
+
// Expand any directory entries (paths ending with `/`) into their
|
|
187
|
+
// individual untracked files before diffing. `git status --porcelain=v1`
|
|
188
|
+
// reports collapsed untracked directories as `?? dir/`, and every caller
|
|
189
|
+
// that feeds the aggregate working-tree state into this function must
|
|
190
|
+
// not trigger an EISDIR when the diff pass reads `dir/` as if it were a
|
|
191
|
+
// file. Belt-and-suspenders: the caller-side expansion in `lint.ts`
|
|
192
|
+
// and `export-all.ts` covers the common path, but a single bad call
|
|
193
|
+
// site re-introduced the bug in 0.17.0 — guarding here makes the
|
|
194
|
+
// regression impossible at this layer.
|
|
195
|
+
const expandedFiles = [];
|
|
196
|
+
for (const file of files) {
|
|
197
|
+
if (file.endsWith('/')) {
|
|
198
|
+
const inner = await getUntrackedFilesInDir(repoDir, file);
|
|
199
|
+
for (const entry of inner)
|
|
200
|
+
expandedFiles.push(entry);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
expandedFiles.push(file);
|
|
204
|
+
}
|
|
205
|
+
const uniqueFiles = [...new Set(expandedFiles)].sort();
|
|
187
206
|
const diffs = [];
|
|
188
207
|
for (const file of uniqueFiles) {
|
|
189
208
|
if (await fileExistsInHead(repoDir, file)) {
|
package/dist/src/core/mach.d.ts
CHANGED
|
@@ -60,19 +60,56 @@ export declare function bootstrapWithOutput(engineDir: string): Promise<MachComm
|
|
|
60
60
|
/**
|
|
61
61
|
* Runs a full mach build. On a non-zero exit, any matched error hints are
|
|
62
62
|
* surfaced on top of the raw mach output so operators get an actionable
|
|
63
|
-
* nudge alongside the cryptic mozbuild traceback.
|
|
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
|
|
66
|
+
* successful!" — notably the stale `config.status is out of date`
|
|
67
|
+
* notice that mach emits when a tool-managed edit landed on
|
|
68
|
+
* `moz.configure` before the build.
|
|
64
69
|
* @param engineDir - Path to the engine directory
|
|
65
70
|
* @param jobs - Number of parallel jobs (optional)
|
|
66
|
-
* @returns
|
|
71
|
+
* @returns Captured mach result (stdout tail, stderr tail, exit code)
|
|
67
72
|
*/
|
|
68
|
-
export declare function build(engineDir: string, jobs?: number): Promise<
|
|
73
|
+
export declare function build(engineDir: string, jobs?: number): Promise<MachCommandResult>;
|
|
69
74
|
/**
|
|
70
75
|
* Runs a fast UI-only build. On a non-zero exit, any matched error hints are
|
|
71
|
-
* surfaced on top of the raw mach output.
|
|
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.
|
|
72
78
|
* @param engineDir - Path to the engine directory
|
|
73
|
-
* @returns
|
|
79
|
+
* @returns Captured mach result
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildUI(engineDir: string): Promise<MachCommandResult>;
|
|
82
|
+
/**
|
|
83
|
+
* Runs an operation while holding a sidecar build lock keyed on the
|
|
84
|
+
* project root. Concurrent `fireforge build` / `fireforge build --ui`
|
|
85
|
+
* invocations against the same tree serialise instead of racing through
|
|
86
|
+
* the mach obj-dir.
|
|
87
|
+
*
|
|
88
|
+
* Motivating case (2026-04-21 eval): a `fireforge build --ui` run
|
|
89
|
+
* kicked off while a full `fireforge build` was still in flight against
|
|
90
|
+
* the same engine tree accepted the command and handed off to `mach
|
|
91
|
+
* build faster`, which failed almost immediately with `No rule to make
|
|
92
|
+
* target 'XUL'`. The real problem is that the first build had not yet
|
|
93
|
+
* materialised the full backend; the operator was left staring at a
|
|
94
|
+
* low-level make error with no link to the actual cause (a concurrent
|
|
95
|
+
* build in flight). The lock intercepts the second invocation before
|
|
96
|
+
* it touches mach, and the refusal message names the PID currently
|
|
97
|
+
* holding the lock so the operator can decide whether to wait or
|
|
98
|
+
* investigate a hung process.
|
|
99
|
+
*
|
|
100
|
+
* Stale-lock recovery: the lock stores the owner PID; a crashed build
|
|
101
|
+
* (SIGINT, SIGTERM, or a kernel kill) leaves the lock dir behind but
|
|
102
|
+
* not the owning process, and `withFileLock` removes the lock on the
|
|
103
|
+
* next attempt when `process.kill(pid, 0)` shows the owner is gone.
|
|
104
|
+
*
|
|
105
|
+
* The project-root variant is the right granularity: a single machine
|
|
106
|
+
* may have several FireForge projects side by side, and nothing says
|
|
107
|
+
* they cannot build in parallel. The lock serialises *within* one
|
|
108
|
+
* project, not across unrelated ones.
|
|
109
|
+
*
|
|
110
|
+
* Returns whatever the inner operation returns.
|
|
74
111
|
*/
|
|
75
|
-
export declare function
|
|
112
|
+
export declare function withBuildLock<T>(projectRoot: string, operation: () => Promise<T>): Promise<T>;
|
|
76
113
|
/**
|
|
77
114
|
* Runs the built browser.
|
|
78
115
|
* @param engineDir - Path to the engine directory
|
package/dist/src/core/mach.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
3
|
import { MachNotFoundError } from '../errors/build.js';
|
|
4
4
|
import { pathExists } from '../utils/fs.js';
|
|
5
5
|
import { warn } from '../utils/logger.js';
|
|
6
6
|
import { exec, execInherit, execInheritCapture, execSmokeRun, execStream, } from '../utils/process.js';
|
|
7
|
+
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
7
8
|
import { explainMachError } from './mach-error-hints.js';
|
|
8
9
|
import { getPython } from './mach-python.js';
|
|
9
10
|
// Re-export sub-modules so existing `from './mach.js'` imports keep working.
|
|
@@ -136,10 +137,15 @@ function surfaceMachErrorHints(result) {
|
|
|
136
137
|
/**
|
|
137
138
|
* Runs a full mach build. On a non-zero exit, any matched error hints are
|
|
138
139
|
* surfaced on top of the raw mach output so operators get an actionable
|
|
139
|
-
* nudge alongside the cryptic mozbuild traceback.
|
|
140
|
+
* nudge alongside the cryptic mozbuild traceback. Returns the captured
|
|
141
|
+
* result so the caller (e.g. `fireforge build`) can inspect the tail
|
|
142
|
+
* for post-build diagnostics that mach prints AFTER "Your build was
|
|
143
|
+
* successful!" — notably the stale `config.status is out of date`
|
|
144
|
+
* notice that mach emits when a tool-managed edit landed on
|
|
145
|
+
* `moz.configure` before the build.
|
|
140
146
|
* @param engineDir - Path to the engine directory
|
|
141
147
|
* @param jobs - Number of parallel jobs (optional)
|
|
142
|
-
* @returns
|
|
148
|
+
* @returns Captured mach result (stdout tail, stderr tail, exit code)
|
|
143
149
|
*/
|
|
144
150
|
export async function build(engineDir, jobs) {
|
|
145
151
|
const args = ['build'];
|
|
@@ -150,20 +156,64 @@ export async function build(engineDir, jobs) {
|
|
|
150
156
|
if (result.exitCode !== 0) {
|
|
151
157
|
surfaceMachErrorHints(result);
|
|
152
158
|
}
|
|
153
|
-
return result
|
|
159
|
+
return result;
|
|
154
160
|
}
|
|
155
161
|
/**
|
|
156
162
|
* Runs a fast UI-only build. On a non-zero exit, any matched error hints are
|
|
157
|
-
* surfaced on top of the raw mach output.
|
|
163
|
+
* surfaced on top of the raw mach output. See {@link build} for why the
|
|
164
|
+
* full captured result is returned rather than just the exit code.
|
|
158
165
|
* @param engineDir - Path to the engine directory
|
|
159
|
-
* @returns
|
|
166
|
+
* @returns Captured mach result
|
|
160
167
|
*/
|
|
161
168
|
export async function buildUI(engineDir) {
|
|
162
169
|
const result = await runMachInheritCapture(['build', 'faster'], engineDir);
|
|
163
170
|
if (result.exitCode !== 0) {
|
|
164
171
|
surfaceMachErrorHints(result);
|
|
165
172
|
}
|
|
166
|
-
return result
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Runs an operation while holding a sidecar build lock keyed on the
|
|
177
|
+
* project root. Concurrent `fireforge build` / `fireforge build --ui`
|
|
178
|
+
* invocations against the same tree serialise instead of racing through
|
|
179
|
+
* the mach obj-dir.
|
|
180
|
+
*
|
|
181
|
+
* Motivating case (2026-04-21 eval): a `fireforge build --ui` run
|
|
182
|
+
* kicked off while a full `fireforge build` was still in flight against
|
|
183
|
+
* the same engine tree accepted the command and handed off to `mach
|
|
184
|
+
* build faster`, which failed almost immediately with `No rule to make
|
|
185
|
+
* target 'XUL'`. The real problem is that the first build had not yet
|
|
186
|
+
* materialised the full backend; the operator was left staring at a
|
|
187
|
+
* low-level make error with no link to the actual cause (a concurrent
|
|
188
|
+
* build in flight). The lock intercepts the second invocation before
|
|
189
|
+
* it touches mach, and the refusal message names the PID currently
|
|
190
|
+
* holding the lock so the operator can decide whether to wait or
|
|
191
|
+
* investigate a hung process.
|
|
192
|
+
*
|
|
193
|
+
* Stale-lock recovery: the lock stores the owner PID; a crashed build
|
|
194
|
+
* (SIGINT, SIGTERM, or a kernel kill) leaves the lock dir behind but
|
|
195
|
+
* not the owning process, and `withFileLock` removes the lock on the
|
|
196
|
+
* next attempt when `process.kill(pid, 0)` shows the owner is gone.
|
|
197
|
+
*
|
|
198
|
+
* The project-root variant is the right granularity: a single machine
|
|
199
|
+
* may have several FireForge projects side by side, and nothing says
|
|
200
|
+
* they cannot build in parallel. The lock serialises *within* one
|
|
201
|
+
* project, not across unrelated ones.
|
|
202
|
+
*
|
|
203
|
+
* Returns whatever the inner operation returns.
|
|
204
|
+
*/
|
|
205
|
+
export async function withBuildLock(projectRoot, operation) {
|
|
206
|
+
const lockPath = createSiblingLockPath(join(projectRoot, '.fireforge-build'), '.lock');
|
|
207
|
+
return withFileLock(lockPath, operation, {
|
|
208
|
+
// Default lock timeout is 30s; bump to 24h so a slow full build does
|
|
209
|
+
// not trip the timeout while the second invocation waits. A real
|
|
210
|
+
// operator will ^C long before 24h elapses; the ceiling is there
|
|
211
|
+
// purely so a forgotten lock cannot wedge the command forever.
|
|
212
|
+
timeoutMs: 24 * 60 * 60 * 1000,
|
|
213
|
+
onTimeoutMessage: `Timed out waiting for the FireForge build lock at ${lockPath}. ` +
|
|
214
|
+
'If no other `fireforge build` is running, remove the lock directory and retry.',
|
|
215
|
+
onStaleLockMessage: (ageMs) => `Removing stale FireForge build lock ${basename(lockPath)} (age: ${Math.round(ageMs / 1000)}s). A previous build process may have crashed.`,
|
|
216
|
+
});
|
|
167
217
|
}
|
|
168
218
|
/**
|
|
169
219
|
* Runs the built browser.
|
|
@@ -24,7 +24,16 @@ export function getRules(binaryName) {
|
|
|
24
24
|
// proposed a bogus jar.mn entry. The lookahead blocks the match so
|
|
25
25
|
// `getUnregistrableAdvice` gets a chance to emit the correct
|
|
26
26
|
// guidance for the `.inc.xhtml` case.
|
|
27
|
-
|
|
27
|
+
//
|
|
28
|
+
// Test implementation files under `browser/base/content/test/` are
|
|
29
|
+
// also excluded: they belong in the nearest `browser.toml` manifest,
|
|
30
|
+
// not in jar.mn. 2026-04-23 eval 2: `status --unmanaged` proposed
|
|
31
|
+
// `fireforge register browser/base/content/test/<dir>/browser_*.js`
|
|
32
|
+
// which would have clutter-registered a test file as browser
|
|
33
|
+
// chrome content. The negative lookahead routes those paths to
|
|
34
|
+
// `getUnregistrableAdvice`, which returns the correct
|
|
35
|
+
// browser.toml-centric guidance.
|
|
36
|
+
pattern: /^browser\/base\/content\/(?!.+\.inc\.xhtml$)(?!test\/)(.+\.(?:js|mjs|xhtml|css))$/,
|
|
28
37
|
isRegistered: (engineDir, fileName) => isBrowserContentRegistered(engineDir, fileName),
|
|
29
38
|
register: (engineDir, after, dryRun, fileName) => registerBrowserContent(engineDir, fileName, after, undefined, dryRun),
|
|
30
39
|
extractArgs: (m) => [m[1] ?? ''],
|
|
@@ -26,6 +26,12 @@ export declare function tokenizeJarMn(lines: string[]): JarMnToken[];
|
|
|
26
26
|
/**
|
|
27
27
|
* Tokenizes a moz.build Python list block, returning the tokens and their
|
|
28
28
|
* line range within the file.
|
|
29
|
+
*
|
|
30
|
+
* Supports both multi-line lists (the common shape) and single-line
|
|
31
|
+
* empty lists of the form `EXTRA_JS_MODULES += []` — the eval-2 finding
|
|
32
|
+
* case where a freshly-scaffolded module directory's `moz.build`
|
|
33
|
+
* started with an empty list and the tokenizer returned `null`,
|
|
34
|
+
* leaving `register` unable to add the first entry.
|
|
29
35
|
*/
|
|
30
36
|
export declare function tokenizeMozBuildList(lines: string[], listPattern: RegExp): {
|
|
31
37
|
tokens: MozBuildToken[];
|
|
@@ -44,6 +44,12 @@ export function tokenizeJarMn(lines) {
|
|
|
44
44
|
/**
|
|
45
45
|
* Tokenizes a moz.build Python list block, returning the tokens and their
|
|
46
46
|
* line range within the file.
|
|
47
|
+
*
|
|
48
|
+
* Supports both multi-line lists (the common shape) and single-line
|
|
49
|
+
* empty lists of the form `EXTRA_JS_MODULES += []` — the eval-2 finding
|
|
50
|
+
* case where a freshly-scaffolded module directory's `moz.build`
|
|
51
|
+
* started with an empty list and the tokenizer returned `null`,
|
|
52
|
+
* leaving `register` unable to add the first entry.
|
|
47
53
|
*/
|
|
48
54
|
export function tokenizeMozBuildList(lines, listPattern) {
|
|
49
55
|
const tokens = [];
|
|
@@ -53,6 +59,28 @@ export function tokenizeMozBuildList(lines, listPattern) {
|
|
|
53
59
|
const raw = lines[i] ?? '';
|
|
54
60
|
if (startLine === -1) {
|
|
55
61
|
if (listPattern.test(raw)) {
|
|
62
|
+
// Single-line empty-list handling: a fresh scaffold sometimes
|
|
63
|
+
// writes `EXTRA_JS_MODULES += []` on one line. The pre-fix
|
|
64
|
+
// tokenizer returned `null` because it never saw a line
|
|
65
|
+
// starting with `]`, which stranded `register` with a "Could
|
|
66
|
+
// not find module list section" error against the documented
|
|
67
|
+
// browser/modules/<fork>/ scaffold (eval 2).
|
|
68
|
+
//
|
|
69
|
+
// The in-place split rewrites the single-line form into the
|
|
70
|
+
// canonical multi-line shape so the caller's
|
|
71
|
+
// `lines.splice(insertIndex, 0, entry)` lands inside the list
|
|
72
|
+
// body. The tokens are emitted to mirror the new structure.
|
|
73
|
+
const singleLineMatch = /^([^[]*\[)\s*\]\s*$/.exec(raw);
|
|
74
|
+
if (singleLineMatch) {
|
|
75
|
+
const openPart = singleLineMatch[1] ?? '';
|
|
76
|
+
lines[i] = openPart;
|
|
77
|
+
lines.splice(i + 1, 0, ']');
|
|
78
|
+
startLine = i;
|
|
79
|
+
endLine = i + 1;
|
|
80
|
+
tokens.push({ type: 'list-open', raw: openPart, lineIndex: i });
|
|
81
|
+
tokens.push({ type: 'list-close', raw: ']', lineIndex: i + 1 });
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
56
84
|
startLine = i;
|
|
57
85
|
tokens.push({ type: 'list-open', raw, lineIndex: i });
|
|
58
86
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Default Marionette control port set by `-marionette`. */
|
|
2
|
+
export declare const DEFAULT_MARIONETTE_PORT = 2828;
|
|
3
|
+
/**
|
|
4
|
+
* Information about a process holding the Marionette port.
|
|
5
|
+
*/
|
|
6
|
+
export interface MarionettePortHolder {
|
|
7
|
+
/** OS process ID. */
|
|
8
|
+
pid: number;
|
|
9
|
+
/** Process basename (e.g. `forgefresh`, `firefox`). */
|
|
10
|
+
command: string;
|
|
11
|
+
/**
|
|
12
|
+
* Full command line the holder was launched with, when the probe
|
|
13
|
+
* can recover it. `lsof` by itself only returns the basename, so
|
|
14
|
+
* POSIX callers see `command === commandLine`; Windows callers
|
|
15
|
+
* recover the full command line via `Get-Process`. Used to detect
|
|
16
|
+
* the `-marionette` flag, which positively identifies a stale
|
|
17
|
+
* browser rather than an unrelated listener.
|
|
18
|
+
*/
|
|
19
|
+
commandLine: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Result of a Marionette port probe.
|
|
23
|
+
*/
|
|
24
|
+
export interface MarionettePortProbeResult {
|
|
25
|
+
/** True when something is listening on the probed port. */
|
|
26
|
+
inUse: boolean;
|
|
27
|
+
/** Details about the holder, when the probe recovered them. */
|
|
28
|
+
holder?: MarionettePortHolder;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Probes whether the Marionette port is currently bound by a
|
|
32
|
+
* listener. The probe is best-effort: missing tooling returns
|
|
33
|
+
* `{ inUse: false }` without failing.
|
|
34
|
+
*
|
|
35
|
+
* @param port - Port to probe (default {@link DEFAULT_MARIONETTE_PORT}).
|
|
36
|
+
*/
|
|
37
|
+
export declare function probeMarionettePort(port?: number): Promise<MarionettePortProbeResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Raises a targeted {@link GeneralError} when the Marionette port
|
|
40
|
+
* is held by a browser process; raises a softer warning-shaped
|
|
41
|
+
* error when the holder is unrelated (so the operator still sees
|
|
42
|
+
* a useful signal but can decide whether to wait it out).
|
|
43
|
+
*
|
|
44
|
+
* @param port - Port to probe (default {@link DEFAULT_MARIONETTE_PORT}).
|
|
45
|
+
* @param options - Extra context for the error message (the project's
|
|
46
|
+
* `binaryName` is used to recognise fork-branded browser binaries).
|
|
47
|
+
*/
|
|
48
|
+
export declare function assertMarionettePortAvailable(port?: number, options?: {
|
|
49
|
+
binaryName?: string;
|
|
50
|
+
}): Promise<void>;
|