@hominis/fireforge 0.15.8 → 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 +85 -0
- package/README.md +16 -3
- 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-shared.d.ts +6 -1
- package/dist/src/commands/export-shared.js +7 -2
- 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.d.ts +20 -0
- package/dist/src/commands/lint.js +167 -45
- package/dist/src/commands/package.js +16 -5
- package/dist/src/commands/re-export-files.js +6 -2
- package/dist/src/commands/re-export.js +62 -4
- 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/core/patch-lint.d.ts +6 -1
- package/dist/src/core/patch-lint.js +14 -1
- package/dist/src/types/commands/options.d.ts +10 -0
- package/dist/src/types/commands/patches.d.ts +22 -0
- 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
|
@@ -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
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { MozconfigError } from '../errors/build.js';
|
|
4
4
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
5
5
|
import { getPlatform } from '../utils/platform.js';
|
|
6
|
+
import { BrandingMozconfigMismatchError } from './branding.js';
|
|
6
7
|
/**
|
|
7
8
|
* Replaces template variables in a string.
|
|
8
9
|
* @param content - Content with ${variable} placeholders
|
|
@@ -16,6 +17,66 @@ function replaceVariables(content, variables) {
|
|
|
16
17
|
.replace(/\$\{appId\}/g, variables.appId)
|
|
17
18
|
.replace(/\$\{binaryName\}/g, variables.binaryName);
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Matches an `--with-branding=<path>` directive anywhere in a rendered
|
|
22
|
+
* mozconfig. The directive form is the one mach reads; an optional
|
|
23
|
+
* `ac_add_options` prefix is the on-disk convention. `m` flag anchors the
|
|
24
|
+
* search per-line so a multi-line mozconfig with older directives earlier
|
|
25
|
+
* in the file doesn't confuse the extractor. We pick the LAST match
|
|
26
|
+
* because mach itself takes the last-write-wins semantics of shell
|
|
27
|
+
* configuration for overlapping `ac_add_options` calls.
|
|
28
|
+
*/
|
|
29
|
+
const WITH_BRANDING_PATTERN = /^\s*(?:ac_add_options\s+)?--with-branding\s*=\s*(\S+)/gm;
|
|
30
|
+
/**
|
|
31
|
+
* Extracts the `--with-branding=<path>` value from a rendered mozconfig
|
|
32
|
+
* body. Returns `undefined` when no directive is present — callers treat
|
|
33
|
+
* that as "mozconfig is missing branding", which is itself an actionable
|
|
34
|
+
* configuration error.
|
|
35
|
+
*
|
|
36
|
+
* Exported for testing.
|
|
37
|
+
*/
|
|
38
|
+
export function extractWithBrandingPath(mozconfigContent) {
|
|
39
|
+
const matches = [...mozconfigContent.matchAll(WITH_BRANDING_PATTERN)];
|
|
40
|
+
const last = matches.at(-1);
|
|
41
|
+
return last?.[1];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Preflights the just-written mozconfig against the branding tree FireForge
|
|
45
|
+
* set up. A drift between the two is silent-corruption territory — the
|
|
46
|
+
* build runs, `mach configure` reads the stale directory name out of
|
|
47
|
+
* mozconfig, and then the recursive make backend errors out with a "path
|
|
48
|
+
* does not exist" message that names the branding dir the mozconfig
|
|
49
|
+
* referenced. By parsing the mozconfig here and comparing to
|
|
50
|
+
* `config.binaryName`, we turn that into a single-line actionable error
|
|
51
|
+
* before `mach` runs.
|
|
52
|
+
*
|
|
53
|
+
* @param engineDir Path to the engine directory (the branding tree lives here)
|
|
54
|
+
* @param mozconfigPath Path to the mozconfig just written
|
|
55
|
+
* @param config FireForge configuration (reads `binaryName`)
|
|
56
|
+
* @throws BrandingMozconfigMismatchError on drift or missing directive
|
|
57
|
+
*/
|
|
58
|
+
export async function assertBrandingMozconfigAgreement(engineDir, mozconfigPath, config) {
|
|
59
|
+
const mozconfigContent = await readText(mozconfigPath);
|
|
60
|
+
const found = extractWithBrandingPath(mozconfigContent);
|
|
61
|
+
const expected = `browser/branding/${config.binaryName}`;
|
|
62
|
+
if (!found) {
|
|
63
|
+
throw new BrandingMozconfigMismatchError(expected, '(no --with-branding directive)', 'mozconfig-missing-branding');
|
|
64
|
+
}
|
|
65
|
+
// Normalise both sides to forward slashes before compare — Windows-edited
|
|
66
|
+
// configs can carry backslash path separators that the build would treat
|
|
67
|
+
// as literal characters in a repo-relative path.
|
|
68
|
+
const normalizedFound = found.replace(/\\/g, '/');
|
|
69
|
+
if (normalizedFound !== expected) {
|
|
70
|
+
throw new BrandingMozconfigMismatchError(expected, found, 'name-mismatch');
|
|
71
|
+
}
|
|
72
|
+
// Last line of defence: even with matching names, a missing branding tree
|
|
73
|
+
// means the scaffold step hasn't run. Preflight here so the operator
|
|
74
|
+
// doesn't pay for a configure-through-build cycle to discover it.
|
|
75
|
+
const brandingMozBuild = join(engineDir, expected, 'moz.build');
|
|
76
|
+
if (!(await pathExists(brandingMozBuild))) {
|
|
77
|
+
throw new BrandingMozconfigMismatchError(expected, found, 'branding-dir-missing');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
19
80
|
/**
|
|
20
81
|
* Generates a mozconfig file from templates.
|
|
21
82
|
* @param configsDir - Path to the configs directory
|
|
@@ -46,5 +107,10 @@ export async function generateMozconfig(configsDir, engineDir, config) {
|
|
|
46
107
|
const platformContent = await readText(platformPath);
|
|
47
108
|
content += `# Platform configuration (${platform})\n${replaceVariables(platformContent, variables)}`;
|
|
48
109
|
await writeText(outputPath, content);
|
|
110
|
+
// Preflight: the mozconfig we just wrote must reference the branding
|
|
111
|
+
// directory FireForge actually set up. Catching the drift here (after the
|
|
112
|
+
// write, before anything consumes mozconfig) keeps `generateMozconfig`
|
|
113
|
+
// the single source of truth for both the render and the sanity-check.
|
|
114
|
+
await assertBrandingMozconfigAgreement(engineDir, outputPath, config);
|
|
49
115
|
}
|
|
50
116
|
//# sourceMappingURL=mach-mozconfig.js.map
|
package/dist/src/core/mach.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SmokeLineCallback, type SmokeRunResult } from '../utils/process.js';
|
|
2
|
-
export { attemptMozinfoRewrite, type BuildArtifactCheck, buildArtifactMismatchMessage, hasBuildArtifacts, type MozinfoRewriteResult, } from './mach-build-artifacts.js';
|
|
2
|
+
export { attemptMozinfoRewrite, type BuildArtifactCheck, buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, type MozinfoRewriteResult, type RunnableBundleCheck, } from './mach-build-artifacts.js';
|
|
3
3
|
export { generateMozconfig, type MozconfigVariables } from './mach-mozconfig.js';
|
|
4
4
|
export { ensurePython, resetResolvedPython } from './mach-python.js';
|
|
5
5
|
/**
|
|
@@ -111,6 +111,17 @@ export declare function runMachSmoke(args: string[], engineDir: string, options:
|
|
|
111
111
|
* @returns Exit code
|
|
112
112
|
*/
|
|
113
113
|
export declare function machPackage(engineDir: string): Promise<number>;
|
|
114
|
+
/**
|
|
115
|
+
* Creates a distribution package while streaming output to the terminal
|
|
116
|
+
* and capturing the stderr tail for post-run diagnostics. Callers that
|
|
117
|
+
* want to consult {@link explainMachError} on failure should use this
|
|
118
|
+
* variant; the inherit-only `machPackage` above remains for callers that
|
|
119
|
+
* just need an exit code.
|
|
120
|
+
*
|
|
121
|
+
* @param engineDir - Path to the engine directory
|
|
122
|
+
* @returns Captured mach result (stdout tail, stderr tail, exit code)
|
|
123
|
+
*/
|
|
124
|
+
export declare function machPackageCapture(engineDir: string): Promise<MachCommandResult>;
|
|
114
125
|
/**
|
|
115
126
|
* Runs mach watch for auto-rebuilding.
|
|
116
127
|
* @param engineDir - Path to the engine directory
|
package/dist/src/core/mach.js
CHANGED
|
@@ -7,7 +7,7 @@ import { exec, execInherit, execInheritCapture, execSmokeRun, execStream, } from
|
|
|
7
7
|
import { explainMachError } from './mach-error-hints.js';
|
|
8
8
|
import { getPython } from './mach-python.js';
|
|
9
9
|
// Re-export sub-modules so existing `from './mach.js'` imports keep working.
|
|
10
|
-
export { attemptMozinfoRewrite, buildArtifactMismatchMessage, hasBuildArtifacts, } from './mach-build-artifacts.js';
|
|
10
|
+
export { attemptMozinfoRewrite, buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, } from './mach-build-artifacts.js';
|
|
11
11
|
export { generateMozconfig } from './mach-mozconfig.js';
|
|
12
12
|
export { ensurePython, resetResolvedPython } from './mach-python.js';
|
|
13
13
|
/**
|
|
@@ -197,6 +197,19 @@ export async function runMachSmoke(args, engineDir, options) {
|
|
|
197
197
|
export async function machPackage(engineDir) {
|
|
198
198
|
return runMach(['package'], engineDir, { inherit: true });
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Creates a distribution package while streaming output to the terminal
|
|
202
|
+
* and capturing the stderr tail for post-run diagnostics. Callers that
|
|
203
|
+
* want to consult {@link explainMachError} on failure should use this
|
|
204
|
+
* variant; the inherit-only `machPackage` above remains for callers that
|
|
205
|
+
* just need an exit code.
|
|
206
|
+
*
|
|
207
|
+
* @param engineDir - Path to the engine directory
|
|
208
|
+
* @returns Captured mach result (stdout tail, stderr tail, exit code)
|
|
209
|
+
*/
|
|
210
|
+
export async function machPackageCapture(engineDir) {
|
|
211
|
+
return runMachCapture(['package'], engineDir);
|
|
212
|
+
}
|
|
200
213
|
/**
|
|
201
214
|
* Runs mach watch for auto-rebuilding.
|
|
202
215
|
* @param engineDir - Path to the engine directory
|