@hominis/fireforge 0.15.9 → 0.16.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 +73 -0
- package/README.md +2 -0
- package/dist/src/cli.d.ts +4 -1
- package/dist/src/cli.js +6 -3
- package/dist/src/commands/download.js +9 -0
- package/dist/src/commands/export-all.js +46 -0
- package/dist/src/commands/export.js +10 -1
- package/dist/src/commands/furnace/diff.js +22 -2
- package/dist/src/commands/furnace/override.js +35 -12
- package/dist/src/commands/furnace/preview.js +33 -1
- package/dist/src/commands/furnace/rename.js +14 -3
- package/dist/src/commands/lint.js +10 -1
- package/dist/src/commands/package.js +16 -5
- package/dist/src/commands/re-export.js +25 -0
- package/dist/src/commands/register.js +2 -18
- package/dist/src/commands/run.js +23 -2
- package/dist/src/commands/status.js +25 -3
- package/dist/src/commands/test.js +6 -24
- package/dist/src/commands/token.js +14 -1
- package/dist/src/commands/watch.js +14 -2
- package/dist/src/core/branding.d.ts +23 -0
- package/dist/src/core/branding.js +39 -0
- package/dist/src/core/browser-wire.js +68 -23
- package/dist/src/core/mach-build-artifacts.d.ts +41 -0
- package/dist/src/core/mach-build-artifacts.js +70 -0
- package/dist/src/core/mach-error-hints.js +15 -0
- package/dist/src/core/mach-mozconfig.d.ts +25 -0
- package/dist/src/core/mach-mozconfig.js +66 -0
- package/dist/src/core/mach.d.ts +12 -1
- package/dist/src/core/mach.js +14 -1
- package/dist/src/core/manifest-rules.js +22 -1
- package/dist/src/utils/fs.d.ts +12 -0
- package/dist/src/utils/fs.js +12 -0
- package/dist/src/utils/paths.d.ts +19 -0
- package/dist/src/utils/paths.js +33 -0
- package/package.json +1 -1
package/dist/src/commands/run.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { createWriteStream } from 'node:fs';
|
|
3
3
|
import { readdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { getProjectPaths } from '../core/config.js';
|
|
5
|
+
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
6
6
|
import { warnIfFurnaceStale } from '../core/furnace-staleness.js';
|
|
7
|
-
import { buildArtifactMismatchMessage, hasBuildArtifacts, run, runMachSmoke, } from '../core/mach.js';
|
|
7
|
+
import { buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, run, runMachSmoke, } from '../core/mach.js';
|
|
8
8
|
import { compileAllowlistFromFile, compileAllowlistFromStrings, matchesAllowlist, matchesSmokeError, } from '../core/smoke-patterns.js';
|
|
9
9
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
10
10
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
@@ -94,6 +94,27 @@ export async function runCommand(projectRoot, options = {}) {
|
|
|
94
94
|
throw new GeneralError(`Run requires a completed build. ${detail}\n\n` +
|
|
95
95
|
"Run 'fireforge build' first, then rerun 'fireforge run'.");
|
|
96
96
|
}
|
|
97
|
+
// `hasBuildArtifacts` only checks for an `obj-*/dist/` directory; a
|
|
98
|
+
// build that configured but hasn't yet produced the launchable binary
|
|
99
|
+
// (common in a long real Firefox compile that the operator stopped
|
|
100
|
+
// and restarted) passes that check, and `mach run` then fails on the
|
|
101
|
+
// missing binary path. `hasRunnableBundle` narrows the probe to the
|
|
102
|
+
// actual executable so `fireforge run` refuses with a targeted
|
|
103
|
+
// message before handing control to mach. `fireforge watch` stays
|
|
104
|
+
// permissive and instead surfaces the same information as a banner
|
|
105
|
+
// suffix; watch is supposed to drive rebuilds of partially-built
|
|
106
|
+
// trees, so blocking there would defeat the feature.
|
|
107
|
+
if (buildCheck.objDir) {
|
|
108
|
+
const config = await loadConfig(projectRoot);
|
|
109
|
+
const bundleCheck = await hasRunnableBundle(paths.engine, config.binaryName, buildCheck.objDir);
|
|
110
|
+
if (!bundleCheck.runnable) {
|
|
111
|
+
const expected = bundleCheck.expectedPath ?? `dist/bin/${config.binaryName}`;
|
|
112
|
+
throw new GeneralError(`Run requires a completed build that produced the launchable bundle. ` +
|
|
113
|
+
`Build artifacts exist in ${buildCheck.objDir}/ but the expected binary at ${expected} is missing — ` +
|
|
114
|
+
`the build may have aborted or is still in progress.\n\n` +
|
|
115
|
+
"Run 'fireforge build' and wait for it to finish before retrying 'fireforge run'.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
97
118
|
// Warn if Furnace components changed since the last apply
|
|
98
119
|
await warnIfFurnaceStale(projectRoot);
|
|
99
120
|
// Clean stale profile state to prevent silent startup failures
|
|
@@ -12,7 +12,7 @@ import { buildPatchQueueContext, collectNewFileCreatorsByPath } from '../core/pa
|
|
|
12
12
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
13
13
|
import { GeneralError } from '../errors/base.js';
|
|
14
14
|
import { toError } from '../utils/errors.js';
|
|
15
|
-
import { pathExists, readText } from '../utils/fs.js';
|
|
15
|
+
import { FIREFORGE_TMP_PATH_PATTERN, pathExists, readText } from '../utils/fs.js';
|
|
16
16
|
import { info, intro, outro, verbose, warn } from '../utils/logger.js';
|
|
17
17
|
/**
|
|
18
18
|
* Status code descriptions for git status.
|
|
@@ -164,6 +164,21 @@ async function expandDirectoryEntries(files, engineDir) {
|
|
|
164
164
|
}
|
|
165
165
|
return { entries: expanded, truncations };
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Strips entries whose path matches the atomic-temp-file shape
|
|
169
|
+
* FireForge's own `writeText` produces (see
|
|
170
|
+
* {@link import('../utils/fs.js').FIREFORGE_TMP_PATH_PATTERN}). Those
|
|
171
|
+
* files only exist for the duration of a write + rename and should
|
|
172
|
+
* never appear in `status` output; filtering them here keeps every
|
|
173
|
+
* status mode (default, raw, unmanaged, ownership, json) symmetric so
|
|
174
|
+
* the operator never sees a `.mozconfig.fireforge-tmp-<pid>-<uuid>`
|
|
175
|
+
* entry mid-write. Files named for unrelated reasons (e.g. a user's
|
|
176
|
+
* `.bashrc.fireforge-tmp-backup` without the PID+UUID tail) do not
|
|
177
|
+
* match the pattern and pass through unfiltered.
|
|
178
|
+
*/
|
|
179
|
+
function filterFireForgeTempFiles(files) {
|
|
180
|
+
return files.filter((entry) => !FIREFORGE_TMP_PATH_PATTERN.test(entry.file));
|
|
181
|
+
}
|
|
167
182
|
/**
|
|
168
183
|
* Classifies files into patch-backed, unmanaged, or branding buckets.
|
|
169
184
|
*/
|
|
@@ -277,7 +292,11 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
277
292
|
const ownershipExpansion = (await isGitRepository(paths.engine))
|
|
278
293
|
? await expandDirectoryEntries(await getStatusWithCodes(paths.engine), paths.engine)
|
|
279
294
|
: { entries: [], truncations: [] };
|
|
280
|
-
|
|
295
|
+
// Filter atomic-write temp files (Finding #18) so a mid-flight
|
|
296
|
+
// `.fireforge-tmp-<pid>-<uuid>` artefact never shows up in any
|
|
297
|
+
// status mode. The pattern is tight enough to let legitimately
|
|
298
|
+
// similar names through.
|
|
299
|
+
const rawFilesOwnership = filterFireForgeTempFiles(ownershipExpansion.entries);
|
|
281
300
|
renderTruncationBanner(ownershipExpansion.truncations);
|
|
282
301
|
// Only walk the patch bodies when the directory actually exists.
|
|
283
302
|
// Fresh projects with no patch queue yet pass through with an empty
|
|
@@ -313,7 +332,10 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
313
332
|
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
314
333
|
}
|
|
315
334
|
const rawFiles = await getStatusWithCodes(paths.engine);
|
|
316
|
-
const { entries:
|
|
335
|
+
const { entries: expanded, truncations } = await expandDirectoryEntries(rawFiles, paths.engine);
|
|
336
|
+
// Strip atomic-write temp files (Finding #18) before every mode
|
|
337
|
+
// branch so raw / unmanaged / default / json all agree.
|
|
338
|
+
const files = filterFireForgeTempFiles(expanded);
|
|
317
339
|
renderTruncationBanner(truncations);
|
|
318
340
|
if (files.length === 0) {
|
|
319
341
|
info('No modified files');
|
|
@@ -11,28 +11,7 @@ import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
|
11
11
|
import { pathExists } from '../utils/fs.js';
|
|
12
12
|
import { info, intro, spinner, warn } from '../utils/logger.js';
|
|
13
13
|
import { pickDefined } from '../utils/options.js';
|
|
14
|
-
|
|
15
|
-
* Strips a leading "engine/" or "engine\\" prefix from a path if present.
|
|
16
|
-
* Users may specify paths like "engine/browser/modules/..." from the project
|
|
17
|
-
* root, but mach test expects paths relative to the engine directory.
|
|
18
|
-
*
|
|
19
|
-
* The match is case-insensitive because case-insensitive filesystems
|
|
20
|
-
* (default macOS, Windows) treat "Engine/" and "engine/" as the same
|
|
21
|
-
* directory, and a literal lowercase-only check left mach with a
|
|
22
|
-
* non-stripped prefix that resolved to a different path under the engine
|
|
23
|
-
* tree. Tab and other whitespace before the prefix is also ignored.
|
|
24
|
-
*
|
|
25
|
-
* @param testPath - Path as provided by the user
|
|
26
|
-
* @returns Path relative to the engine directory
|
|
27
|
-
*/
|
|
28
|
-
function normalizeTestPath(testPath) {
|
|
29
|
-
const trimmed = testPath.trim();
|
|
30
|
-
const match = /^engine[/\\]/i.exec(trimmed);
|
|
31
|
-
if (match) {
|
|
32
|
-
return trimmed.slice(match[0].length);
|
|
33
|
-
}
|
|
34
|
-
return trimmed;
|
|
35
|
-
}
|
|
14
|
+
import { stripEnginePrefix } from '../utils/paths.js';
|
|
36
15
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
37
16
|
const missingPaths = [];
|
|
38
17
|
for (const testPath of testPaths) {
|
|
@@ -198,8 +177,11 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
198
177
|
throw new GeneralError('Marionette preflight reported FAIL — see output above. Aborting before mach test runs.');
|
|
199
178
|
}
|
|
200
179
|
}
|
|
201
|
-
// Normalize test paths (strip engine/ prefix if present)
|
|
202
|
-
|
|
180
|
+
// Normalize test paths (strip engine/ prefix if present). Uses the
|
|
181
|
+
// shared `stripEnginePrefix` helper so `test`, `register`, `lint`, and
|
|
182
|
+
// `export` all accept the same prefix forms. Also trim to match the
|
|
183
|
+
// previous case-insensitive + leading-whitespace-tolerant contract.
|
|
184
|
+
const normalizedPaths = testPaths.map((p) => stripEnginePrefix(p).trim());
|
|
203
185
|
await assertTestPathsExist(paths.engine, normalizedPaths);
|
|
204
186
|
// Build extra args
|
|
205
187
|
const extraArgs = [];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { Option } from 'commander';
|
|
3
3
|
import { loadConfig } from '../core/config.js';
|
|
4
|
-
import { loadFurnaceConfig } from '../core/furnace-config.js';
|
|
4
|
+
import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
|
|
5
5
|
import { addToken, getTokensCssPath, validateTokenAdd, } from '../core/token-manager.js';
|
|
6
6
|
import { InvalidArgumentError } from '../errors/base.js';
|
|
7
|
+
import { FurnaceError } from '../errors/furnace.js';
|
|
7
8
|
import { toError } from '../utils/errors.js';
|
|
8
9
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
9
10
|
import { pickDefined } from '../utils/options.js';
|
|
@@ -36,6 +37,18 @@ async function normalizeTokenNameForProject(projectRoot, rawTokenName) {
|
|
|
36
37
|
*/
|
|
37
38
|
export async function tokenAddCommand(projectRoot, tokenName, value, options) {
|
|
38
39
|
intro('Token Add');
|
|
40
|
+
// Finding #15: a fresh project without furnace.json failed deep inside
|
|
41
|
+
// the token-manager's `assertTokenCategoryExists` with "Token CSS file
|
|
42
|
+
// not found: browser/themes/shared/<binary>-tokens.css" — technically
|
|
43
|
+
// correct, but the operator's actual next step is to initialize
|
|
44
|
+
// Furnace (which scaffolds the tokens CSS file among other things).
|
|
45
|
+
// Catching the uninitialized case here gives the right guidance up-
|
|
46
|
+
// front before the generic "file not found" error fires.
|
|
47
|
+
if (!(await furnaceConfigExists(projectRoot))) {
|
|
48
|
+
throw new FurnaceError('Token management requires Furnace to be initialized. ' +
|
|
49
|
+
'Tokens live in the Furnace-managed tokens CSS file, which `fireforge furnace init` scaffolds alongside the rest of the Furnace workspace.\n\n' +
|
|
50
|
+
'Run "fireforge furnace init" first, then rerun "fireforge token add ...".');
|
|
51
|
+
}
|
|
39
52
|
// Normalize token name using the configured Furnace token prefix when the
|
|
40
53
|
// user supplied a bare token name like "canvas-gap".
|
|
41
54
|
tokenName = await normalizeTokenNameForProject(projectRoot, tokenName);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
2
2
|
import { warnIfFurnaceStale } from '../core/furnace-staleness.js';
|
|
3
|
-
import { buildArtifactMismatchMessage, generateMozconfig, hasBuildArtifacts, watchWithOutput, } from '../core/mach.js';
|
|
3
|
+
import { buildArtifactMismatchMessage, generateMozconfig, hasBuildArtifacts, hasRunnableBundle, watchWithOutput, } from '../core/mach.js';
|
|
4
4
|
import { GeneralError } from '../errors/base.js';
|
|
5
5
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
6
6
|
import { toError } from '../utils/errors.js';
|
|
@@ -114,7 +114,19 @@ export async function watchCommand(projectRoot) {
|
|
|
114
114
|
throw new GeneralError(`Watch mode requires a completed build. ${detail}\n\n` +
|
|
115
115
|
"Run 'fireforge build' first to create the initial build, then run 'fireforge watch'.");
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
// Report bundle state alongside the "Using build artifacts..." banner
|
|
118
|
+
// so an operator watching a mid-build tree can see why `fireforge run`
|
|
119
|
+
// would refuse right now while watch is still going. Watch remains
|
|
120
|
+
// permissive (it exists to drive rebuilds) — this is informational.
|
|
121
|
+
// The `hasBuildArtifacts` check already passed at this point, so
|
|
122
|
+
// `objDir` is always defined.
|
|
123
|
+
const bundleCheck = buildCheck.objDir
|
|
124
|
+
? await hasRunnableBundle(paths.engine, config.binaryName, buildCheck.objDir)
|
|
125
|
+
: { runnable: false };
|
|
126
|
+
const bundleSuffix = bundleCheck.runnable
|
|
127
|
+
? ' (bundle: runnable)'
|
|
128
|
+
: ' (bundle: pending — watch will rebuild)';
|
|
129
|
+
info(`Using build artifacts from ${buildCheck.objDir}/${bundleSuffix}`);
|
|
118
130
|
// Advisory: warn when Furnace components have drifted since the last
|
|
119
131
|
// apply so the user doesn't launch watch-mode builds with stale
|
|
120
132
|
// components baked in. Mirrors the check in `fireforge run` — without
|
|
@@ -6,6 +6,29 @@ export declare class BrandingError extends FireForgeError {
|
|
|
6
6
|
readonly code: 6;
|
|
7
7
|
get userMessage(): string;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Error thrown when the generated `mozconfig` references a `--with-branding`
|
|
11
|
+
* directory that does not match the branding tree FireForge set up. The
|
|
12
|
+
* mismatch is a silent-corruption hazard — `mach configure` picks the value
|
|
13
|
+
* from mozconfig but the scaffolded branding lives elsewhere, so the build
|
|
14
|
+
* fails deep inside moz.build resolution with a confusing "path does not
|
|
15
|
+
* exist" message. Surface it as an actionable preflight instead.
|
|
16
|
+
*
|
|
17
|
+
* The root cause is that setup renders templates under `configs/` with
|
|
18
|
+
* `${binaryName}` baked in at setup time; a subsequent edit to
|
|
19
|
+
* `fireforge.json`'s `binaryName` (or a re-setup without re-templating)
|
|
20
|
+
* leaves those baked-in names stale while `setupBranding` continues to use
|
|
21
|
+
* the current `config.binaryName`. Both directions (mozconfig ahead of
|
|
22
|
+
* config, config ahead of mozconfig) produce the same class of build break.
|
|
23
|
+
*/
|
|
24
|
+
export declare class BrandingMozconfigMismatchError extends FireForgeError {
|
|
25
|
+
readonly expectedBrandingDir: string;
|
|
26
|
+
readonly mozconfigBrandingDir: string;
|
|
27
|
+
readonly reason: 'mozconfig-missing-branding' | 'name-mismatch' | 'branding-dir-missing';
|
|
28
|
+
readonly code: 6;
|
|
29
|
+
constructor(expectedBrandingDir: string, mozconfigBrandingDir: string, reason: 'mozconfig-missing-branding' | 'name-mismatch' | 'branding-dir-missing');
|
|
30
|
+
get userMessage(): string;
|
|
31
|
+
}
|
|
9
32
|
/**
|
|
10
33
|
* Full branding configuration.
|
|
11
34
|
*/
|
|
@@ -13,6 +13,45 @@ export class BrandingError extends FireForgeError {
|
|
|
13
13
|
return `Branding Error: ${this.message}\n\nBranding is required to set MOZ_APP_VENDOR, MOZ_MACBUNDLE_ID, and other Firefox identity values.`;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Error thrown when the generated `mozconfig` references a `--with-branding`
|
|
18
|
+
* directory that does not match the branding tree FireForge set up. The
|
|
19
|
+
* mismatch is a silent-corruption hazard — `mach configure` picks the value
|
|
20
|
+
* from mozconfig but the scaffolded branding lives elsewhere, so the build
|
|
21
|
+
* fails deep inside moz.build resolution with a confusing "path does not
|
|
22
|
+
* exist" message. Surface it as an actionable preflight instead.
|
|
23
|
+
*
|
|
24
|
+
* The root cause is that setup renders templates under `configs/` with
|
|
25
|
+
* `${binaryName}` baked in at setup time; a subsequent edit to
|
|
26
|
+
* `fireforge.json`'s `binaryName` (or a re-setup without re-templating)
|
|
27
|
+
* leaves those baked-in names stale while `setupBranding` continues to use
|
|
28
|
+
* the current `config.binaryName`. Both directions (mozconfig ahead of
|
|
29
|
+
* config, config ahead of mozconfig) produce the same class of build break.
|
|
30
|
+
*/
|
|
31
|
+
export class BrandingMozconfigMismatchError extends FireForgeError {
|
|
32
|
+
expectedBrandingDir;
|
|
33
|
+
mozconfigBrandingDir;
|
|
34
|
+
reason;
|
|
35
|
+
code = ExitCode.PATCH_ERROR;
|
|
36
|
+
constructor(expectedBrandingDir, mozconfigBrandingDir, reason) {
|
|
37
|
+
super(`Generated mozconfig references "${mozconfigBrandingDir}" but the active branding directory is "${expectedBrandingDir}".`);
|
|
38
|
+
this.expectedBrandingDir = expectedBrandingDir;
|
|
39
|
+
this.mozconfigBrandingDir = mozconfigBrandingDir;
|
|
40
|
+
this.reason = reason;
|
|
41
|
+
}
|
|
42
|
+
get userMessage() {
|
|
43
|
+
const diagnosis = this.reason === 'mozconfig-missing-branding'
|
|
44
|
+
? `The generated mozconfig does not contain a --with-branding directive (found "${this.mozconfigBrandingDir}"). FireForge expected to write one for binaryName "${this.expectedBrandingDir}".`
|
|
45
|
+
: this.reason === 'name-mismatch'
|
|
46
|
+
? `The generated mozconfig sets --with-branding="${this.mozconfigBrandingDir}" but FireForge set up branding under "${this.expectedBrandingDir}".`
|
|
47
|
+
: `The generated mozconfig sets --with-branding="${this.mozconfigBrandingDir}" but no moz.build exists under engine/${this.mozconfigBrandingDir}/.`;
|
|
48
|
+
return (`Branding Error: ${diagnosis}\n\n` +
|
|
49
|
+
'This usually means the rendered configs/ templates drifted from fireforge.json. Fix one of:\n' +
|
|
50
|
+
' 1. Edit configs/common.mozconfig so --with-branding uses ${binaryName} (or the current binaryName), then re-run "fireforge build".\n' +
|
|
51
|
+
' 2. Update fireforge.json so binaryName matches the --with-branding value baked into configs/.\n\n' +
|
|
52
|
+
'The mismatch is caught before mach builds because resolving the build against the wrong branding tree fails deep in moz.build with a confusing "path does not exist" message.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
16
55
|
/**
|
|
17
56
|
* Sets up the custom branding directory for the browser.
|
|
18
57
|
*
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join, relative } from 'node:path';
|
|
3
|
+
import { GeneralError } from '../errors/base.js';
|
|
4
|
+
import { toError } from '../utils/errors.js';
|
|
3
5
|
import { toRootRelativePath } from '../utils/paths.js';
|
|
4
6
|
import { getProjectPaths } from './config.js';
|
|
7
|
+
import { createRollbackJournal, restoreRollbackJournal, snapshotFile } from './furnace-rollback.js';
|
|
5
8
|
import { registerBrowserContent } from './manifest-register.js';
|
|
9
|
+
import { DEFAULT_DOM_TARGET } from './wire-dom-fragment.js';
|
|
6
10
|
import { addDestroyToBrowserInit, addDomFragment, addInitToBrowserInit, addSubscriptToBrowserMain, } from './wire-targets.js';
|
|
7
11
|
export const DEFAULT_BROWSER_SUBSCRIPT_DIR = 'browser/base/content';
|
|
8
12
|
const BROWSER_BASE_DIR = 'browser/base';
|
|
@@ -36,31 +40,72 @@ export async function wireSubscript(root, name, options = {}) {
|
|
|
36
40
|
},
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Snapshot every file the five mutation steps might touch so a mid-sequence
|
|
44
|
+
// failure (most commonly the chrome-document insertion not finding an
|
|
45
|
+
// anchor) does not leave a half-wired browser behind. Before the rollback
|
|
46
|
+
// journal landed here, a failed `wire` would still have written new
|
|
47
|
+
// `loadSubScript` calls into browser-main.js, new init/destroy expressions
|
|
48
|
+
// into browser-init.js, and a new entry into browser/base/jar.mn — the
|
|
49
|
+
// operator then had to grep the engine tree for the partial mutation and
|
|
50
|
+
// hand-revert, or re-download. The snapshots cover the targets on every
|
|
51
|
+
// code path (init/destroy/DOM are conditional, so we snapshot only when
|
|
52
|
+
// the corresponding option would fire a write) plus the two files every
|
|
53
|
+
// wire touches.
|
|
54
|
+
const journal = createRollbackJournal();
|
|
55
|
+
const effectiveDomTargetPath = options.domFilePath
|
|
56
|
+
? toRootRelativePath(engineDir, options.domTargetPath ?? DEFAULT_DOM_TARGET)
|
|
57
|
+
: undefined;
|
|
58
|
+
await snapshotFile(journal, join(engineDir, 'browser/base/content/browser-main.js'));
|
|
59
|
+
if (options.init !== undefined || options.destroy !== undefined) {
|
|
60
|
+
await snapshotFile(journal, join(engineDir, 'browser/base/content/browser-init.js'));
|
|
45
61
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (options.destroy) {
|
|
49
|
-
destroyAdded = await addDestroyToBrowserInit(engineDir, options.destroy);
|
|
62
|
+
if (effectiveDomTargetPath) {
|
|
63
|
+
await snapshotFile(journal, join(engineDir, effectiveDomTargetPath));
|
|
50
64
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
await snapshotFile(journal, join(engineDir, 'browser/base/jar.mn'));
|
|
66
|
+
try {
|
|
67
|
+
// 1. Add subscript to browser-main.js
|
|
68
|
+
const subscriptAdded = await addSubscriptToBrowserMain(engineDir, name);
|
|
69
|
+
// 2. Add init expression to browser-init.js (if provided)
|
|
70
|
+
let initAdded = false;
|
|
71
|
+
if (options.init) {
|
|
72
|
+
initAdded = await addInitToBrowserInit(engineDir, options.init, options.after);
|
|
73
|
+
}
|
|
74
|
+
// 3. Add destroy expression to browser-init.js onUnload() (if provided)
|
|
75
|
+
let destroyAdded = false;
|
|
76
|
+
if (options.destroy) {
|
|
77
|
+
destroyAdded = await addDestroyToBrowserInit(engineDir, options.destroy);
|
|
78
|
+
}
|
|
79
|
+
// 4. Add #include directive to the top-level chrome document (if provided)
|
|
80
|
+
let domInserted = false;
|
|
81
|
+
if (options.domFilePath) {
|
|
82
|
+
domInserted = await addDomFragment(engineDir, toRootRelativePath(engineDir, options.domFilePath), options.domTargetPath);
|
|
83
|
+
}
|
|
84
|
+
// 5. Register in jar.mn
|
|
85
|
+
const jarMnResult = await registerBrowserContent(engineDir, `${name}.js`, undefined, jarMnSourcePath);
|
|
86
|
+
return {
|
|
87
|
+
subscriptAdded,
|
|
88
|
+
initAdded,
|
|
89
|
+
destroyAdded,
|
|
90
|
+
domInserted,
|
|
91
|
+
jarMnResult,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Best-effort rollback: if the restore itself fails, surface both the
|
|
96
|
+
// original wire failure and the rollback failure so the operator knows
|
|
97
|
+
// the engine may be in a partially-wired state that needs manual
|
|
98
|
+
// attention. The original error's message is preserved so the user sees
|
|
99
|
+
// *why* the wire failed (e.g. "Could not find insertion point in chrome
|
|
100
|
+
// document") alongside any rollback diagnosis.
|
|
101
|
+
const originalMessage = toError(error).message;
|
|
102
|
+
try {
|
|
103
|
+
await restoreRollbackJournal(journal);
|
|
104
|
+
}
|
|
105
|
+
catch (rollbackError) {
|
|
106
|
+
throw new GeneralError(`Wire failed: ${originalMessage}. Automatic rollback also failed: ${toError(rollbackError).message}. The engine may contain partially-applied wire mutations; review "git status" under engine/ and revert manually.`);
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
55
109
|
}
|
|
56
|
-
// 5. Register in jar.mn
|
|
57
|
-
const jarMnResult = await registerBrowserContent(engineDir, `${name}.js`, undefined, jarMnSourcePath);
|
|
58
|
-
return {
|
|
59
|
-
subscriptAdded,
|
|
60
|
-
initAdded,
|
|
61
|
-
destroyAdded,
|
|
62
|
-
domInserted,
|
|
63
|
-
jarMnResult,
|
|
64
|
-
};
|
|
65
110
|
}
|
|
66
111
|
//# sourceMappingURL=browser-wire.js.map
|
|
@@ -25,6 +25,47 @@ export interface BuildArtifactCheck {
|
|
|
25
25
|
* @returns Build artifact check result
|
|
26
26
|
*/
|
|
27
27
|
export declare function hasBuildArtifacts(engineDir: string): Promise<BuildArtifactCheck>;
|
|
28
|
+
/**
|
|
29
|
+
* Outcome of the `hasRunnableBundle` probe. Distinguishes "no objdir at
|
|
30
|
+
* all" from "objdir exists but the launchable binary is not yet written"
|
|
31
|
+
* so callers (notably `fireforge run`) can give the operator a specific
|
|
32
|
+
* message instead of the generic build-artifacts-missing line.
|
|
33
|
+
*/
|
|
34
|
+
export interface RunnableBundleCheck {
|
|
35
|
+
/** True when an objdir is present AND the expected binary was found under it. */
|
|
36
|
+
runnable: boolean;
|
|
37
|
+
/** Repo-relative (engine-rooted) path we probed; populated even on failure for error copy. */
|
|
38
|
+
expectedPath?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Checks whether the built browser's launchable binary exists under
|
|
42
|
+
* `<engineDir>/<objDir>/dist/...`. `hasBuildArtifacts` only confirms that
|
|
43
|
+
* an obj tree with a `dist/` subdir exists; a partial or in-progress build
|
|
44
|
+
* can satisfy that check without ever writing the executable, which is the
|
|
45
|
+
* failure mode that makes `fireforge run` throw `mach run` after having
|
|
46
|
+
* reported the build as usable. Separating the probes lets `run` fail fast
|
|
47
|
+
* with a precise message and `watch` stay permissive (it exists to drive
|
|
48
|
+
* rebuilds of incomplete trees) while still reporting the bundle state in
|
|
49
|
+
* its startup banner.
|
|
50
|
+
*
|
|
51
|
+
* Platform layout:
|
|
52
|
+
* - macOS: `<objDir>/dist/*.app/Contents/MacOS/<binaryName>` (the `.app`
|
|
53
|
+
* display casing can differ from `binaryName` — e.g. `Hominis.app` for
|
|
54
|
+
* binary `hominis`, so we enumerate the `*.app` bundles rather than
|
|
55
|
+
* compute the name.
|
|
56
|
+
* - Linux: `<objDir>/dist/bin/<binaryName>`.
|
|
57
|
+
* - Windows: `<objDir>/dist/bin/<binaryName>.exe`.
|
|
58
|
+
*
|
|
59
|
+
* Returns `runnable: false` with no `expectedPath` when the `objDir`
|
|
60
|
+
* itself cannot be scanned — same degraded contract as `hasBuildArtifacts`.
|
|
61
|
+
*
|
|
62
|
+
* @param engineDir Path to the engine directory
|
|
63
|
+
* @param binaryName Lowercase binary name from `fireforge.json`
|
|
64
|
+
* @param objDir The single matching `obj-*` directory name (caller
|
|
65
|
+
* resolves it; typically from `hasBuildArtifacts().objDir`)
|
|
66
|
+
* @returns Structured check result
|
|
67
|
+
*/
|
|
68
|
+
export declare function hasRunnableBundle(engineDir: string, binaryName: string, objDir: string): Promise<RunnableBundleCheck>;
|
|
28
69
|
/** Builds a user-facing explanation when detected build artifacts belong to another workspace. */
|
|
29
70
|
export declare function buildArtifactMismatchMessage(engineDir: string, buildCheck: BuildArtifactCheck, commandName: string): string | undefined;
|
|
30
71
|
/**
|
|
@@ -4,6 +4,7 @@ import { join, relative, resolve, sep } from 'node:path';
|
|
|
4
4
|
import { toError } from '../utils/errors.js';
|
|
5
5
|
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
6
6
|
import { verbose } from '../utils/logger.js';
|
|
7
|
+
import { getPlatform } from '../utils/platform.js';
|
|
7
8
|
import { isObject, isString } from '../utils/validation.js';
|
|
8
9
|
function validateBuildMozinfo(data) {
|
|
9
10
|
if (!isObject(data)) {
|
|
@@ -94,6 +95,75 @@ export async function hasBuildArtifacts(engineDir) {
|
|
|
94
95
|
return { exists: false };
|
|
95
96
|
}
|
|
96
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Checks whether the built browser's launchable binary exists under
|
|
100
|
+
* `<engineDir>/<objDir>/dist/...`. `hasBuildArtifacts` only confirms that
|
|
101
|
+
* an obj tree with a `dist/` subdir exists; a partial or in-progress build
|
|
102
|
+
* can satisfy that check without ever writing the executable, which is the
|
|
103
|
+
* failure mode that makes `fireforge run` throw `mach run` after having
|
|
104
|
+
* reported the build as usable. Separating the probes lets `run` fail fast
|
|
105
|
+
* with a precise message and `watch` stay permissive (it exists to drive
|
|
106
|
+
* rebuilds of incomplete trees) while still reporting the bundle state in
|
|
107
|
+
* its startup banner.
|
|
108
|
+
*
|
|
109
|
+
* Platform layout:
|
|
110
|
+
* - macOS: `<objDir>/dist/*.app/Contents/MacOS/<binaryName>` (the `.app`
|
|
111
|
+
* display casing can differ from `binaryName` — e.g. `Hominis.app` for
|
|
112
|
+
* binary `hominis`, so we enumerate the `*.app` bundles rather than
|
|
113
|
+
* compute the name.
|
|
114
|
+
* - Linux: `<objDir>/dist/bin/<binaryName>`.
|
|
115
|
+
* - Windows: `<objDir>/dist/bin/<binaryName>.exe`.
|
|
116
|
+
*
|
|
117
|
+
* Returns `runnable: false` with no `expectedPath` when the `objDir`
|
|
118
|
+
* itself cannot be scanned — same degraded contract as `hasBuildArtifacts`.
|
|
119
|
+
*
|
|
120
|
+
* @param engineDir Path to the engine directory
|
|
121
|
+
* @param binaryName Lowercase binary name from `fireforge.json`
|
|
122
|
+
* @param objDir The single matching `obj-*` directory name (caller
|
|
123
|
+
* resolves it; typically from `hasBuildArtifacts().objDir`)
|
|
124
|
+
* @returns Structured check result
|
|
125
|
+
*/
|
|
126
|
+
export async function hasRunnableBundle(engineDir, binaryName, objDir) {
|
|
127
|
+
const platform = getPlatform();
|
|
128
|
+
const distDir = join(engineDir, objDir, 'dist');
|
|
129
|
+
if (!(await pathExists(distDir))) {
|
|
130
|
+
return { runnable: false };
|
|
131
|
+
}
|
|
132
|
+
if (platform === 'darwin') {
|
|
133
|
+
let entries;
|
|
134
|
+
try {
|
|
135
|
+
entries = await readdir(distDir, { withFileTypes: true });
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return { runnable: false };
|
|
139
|
+
}
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (!entry.isDirectory())
|
|
142
|
+
continue;
|
|
143
|
+
if (!entry.name.endsWith('.app'))
|
|
144
|
+
continue;
|
|
145
|
+
const candidate = join(distDir, entry.name, 'Contents', 'MacOS', binaryName);
|
|
146
|
+
if (await pathExists(candidate)) {
|
|
147
|
+
return { runnable: true, expectedPath: relative(engineDir, candidate) };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Report an expected-but-missing path rooted at the first .app bundle we
|
|
151
|
+
// can see, or a synthetic path when no bundle exists yet, so the error
|
|
152
|
+
// message names something the operator can look for on disk.
|
|
153
|
+
const firstApp = entries.find((e) => e.isDirectory() && e.name.endsWith('.app'));
|
|
154
|
+
const expected = firstApp
|
|
155
|
+
? relative(engineDir, join(distDir, firstApp.name, 'Contents', 'MacOS', binaryName))
|
|
156
|
+
: relative(engineDir, join(distDir, `<AppName>.app/Contents/MacOS/${binaryName}`));
|
|
157
|
+
return { runnable: false, expectedPath: expected };
|
|
158
|
+
}
|
|
159
|
+
const binaryFile = platform === 'win32' ? `${binaryName}.exe` : binaryName;
|
|
160
|
+
const candidate = join(distDir, 'bin', binaryFile);
|
|
161
|
+
const expectedPath = relative(engineDir, candidate);
|
|
162
|
+
if (await pathExists(candidate)) {
|
|
163
|
+
return { runnable: true, expectedPath };
|
|
164
|
+
}
|
|
165
|
+
return { runnable: false, expectedPath };
|
|
166
|
+
}
|
|
97
167
|
/** Builds a user-facing explanation when detected build artifacts belong to another workspace. */
|
|
98
168
|
export function buildArtifactMismatchMessage(engineDir, buildCheck, commandName) {
|
|
99
169
|
if (!buildCheck.metadataMismatch || !buildCheck.objDir) {
|
|
@@ -19,6 +19,21 @@ export const MACH_ERROR_HINTS = [
|
|
|
19
19
|
hint: 'A file registered under JS_PREFERENCE_PP_FILES contains no preprocessor directives. ' +
|
|
20
20
|
'Use JS_PREFERENCE_FILES instead, or add at least one #filter / #expand directive to the file.',
|
|
21
21
|
},
|
|
22
|
+
{
|
|
23
|
+
// `mach package` inside `packager.py` dereferences a `None` sink when
|
|
24
|
+
// the packaging input set cannot resolve an entry it expected — the
|
|
25
|
+
// most common real-world cause is running `fireforge package` before
|
|
26
|
+
// a full `fireforge build` has finished, so `obj-*/dist/` is missing
|
|
27
|
+
// pieces the packager assumes exist. The hint points at that root
|
|
28
|
+
// cause specifically; the broader "build failed" path has already
|
|
29
|
+
// surfaced the raw traceback above this hint.
|
|
30
|
+
pattern: /packager\.py[\s\S]*?AttributeError: 'NoneType' object has no attribute 'open'|AttributeError: 'NoneType' object has no attribute 'open'[\s\S]*?packager\.py/,
|
|
31
|
+
hint: '`mach package` tripped a `NoneType.open` inside `packager.py`. This is almost always a ' +
|
|
32
|
+
'symptom of the packager being handed an incomplete `obj-*/dist/` tree — e.g. running ' +
|
|
33
|
+
'"fireforge package" before a full "fireforge build" (not --ui) completed, or packaging ' +
|
|
34
|
+
'after a build that failed late. Re-run "fireforge build" to completion, confirm the app ' +
|
|
35
|
+
'bundle exists under `obj-*/dist/`, and rerun "fireforge package".',
|
|
36
|
+
},
|
|
22
37
|
];
|
|
23
38
|
/**
|
|
24
39
|
* Scans captured stderr for known mach errors and returns matching hints.
|
|
@@ -8,6 +8,31 @@ export interface MozconfigVariables {
|
|
|
8
8
|
appId: string;
|
|
9
9
|
binaryName: string;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Extracts the `--with-branding=<path>` value from a rendered mozconfig
|
|
13
|
+
* body. Returns `undefined` when no directive is present — callers treat
|
|
14
|
+
* that as "mozconfig is missing branding", which is itself an actionable
|
|
15
|
+
* configuration error.
|
|
16
|
+
*
|
|
17
|
+
* Exported for testing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractWithBrandingPath(mozconfigContent: string): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Preflights the just-written mozconfig against the branding tree FireForge
|
|
22
|
+
* set up. A drift between the two is silent-corruption territory — the
|
|
23
|
+
* build runs, `mach configure` reads the stale directory name out of
|
|
24
|
+
* mozconfig, and then the recursive make backend errors out with a "path
|
|
25
|
+
* does not exist" message that names the branding dir the mozconfig
|
|
26
|
+
* referenced. By parsing the mozconfig here and comparing to
|
|
27
|
+
* `config.binaryName`, we turn that into a single-line actionable error
|
|
28
|
+
* before `mach` runs.
|
|
29
|
+
*
|
|
30
|
+
* @param engineDir Path to the engine directory (the branding tree lives here)
|
|
31
|
+
* @param mozconfigPath Path to the mozconfig just written
|
|
32
|
+
* @param config FireForge configuration (reads `binaryName`)
|
|
33
|
+
* @throws BrandingMozconfigMismatchError on drift or missing directive
|
|
34
|
+
*/
|
|
35
|
+
export declare function assertBrandingMozconfigAgreement(engineDir: string, mozconfigPath: string, config: FireForgeConfig): Promise<void>;
|
|
11
36
|
/**
|
|
12
37
|
* Generates a mozconfig file from templates.
|
|
13
38
|
* @param configsDir - Path to the configs directory
|