@hominis/fireforge 0.15.9 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/README.md +6 -2
- package/dist/src/cli.d.ts +4 -1
- package/dist/src/cli.js +6 -3
- package/dist/src/commands/config.js +16 -5
- package/dist/src/commands/download.js +31 -4
- package/dist/src/commands/export-all.js +96 -9
- package/dist/src/commands/export.js +10 -1
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +11 -1
- package/dist/src/commands/furnace/chrome-doc-templates.js +12 -2
- package/dist/src/commands/furnace/create.js +21 -3
- package/dist/src/commands/furnace/diff.js +22 -2
- package/dist/src/commands/furnace/index.js +1 -0
- package/dist/src/commands/furnace/init.js +76 -2
- package/dist/src/commands/furnace/override.js +35 -12
- package/dist/src/commands/furnace/preview.js +46 -1
- package/dist/src/commands/furnace/rename.js +14 -3
- package/dist/src/commands/lint.js +26 -2
- package/dist/src/commands/package.js +16 -5
- package/dist/src/commands/re-export.js +25 -0
- package/dist/src/commands/rebase/patch-loop.js +19 -0
- package/dist/src/commands/register.js +2 -18
- package/dist/src/commands/run.js +23 -2
- package/dist/src/commands/status.js +42 -8
- 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/commands/wire.js +35 -9
- 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/build-baseline.d.ts +14 -0
- package/dist/src/core/build-baseline.js +61 -1
- package/dist/src/core/config-mutate.d.ts +1 -1
- package/dist/src/core/config.d.ts +17 -0
- package/dist/src/core/config.js +35 -0
- package/dist/src/core/firefox.d.ts +16 -2
- package/dist/src/core/firefox.js +7 -2
- package/dist/src/core/furnace-config.d.ts +23 -0
- package/dist/src/core/furnace-config.js +38 -0
- 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 +38 -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.js +43 -20
- package/dist/src/core/test-stale-check.js +46 -1
- package/dist/src/core/token-manager.js +57 -4
- package/dist/src/core/token-scaffold.d.ts +36 -0
- package/dist/src/core/token-scaffold.js +74 -0
- package/dist/src/types/commands/options.d.ts +10 -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
|
@@ -23,12 +23,26 @@ export declare function getDownloadUrl(version: string, product?: FirefoxProduct
|
|
|
23
23
|
* @returns Tarball filename
|
|
24
24
|
*/
|
|
25
25
|
export declare function getTarballFilename(version: string, product?: FirefoxProduct): string;
|
|
26
|
+
/**
|
|
27
|
+
* Lifecycle phase reported by {@link downloadFirefoxSource}. The download
|
|
28
|
+
* CLI command uses this to swap spinners between the bytes-on-the-wire
|
|
29
|
+
* phase and the silent tar-xz decompression phase that follows — before
|
|
30
|
+
* this, a single spinner stuck at "Downloading Firefox … 100%" covered
|
|
31
|
+
* both phases, making the first-run setup look hung precisely when the
|
|
32
|
+
* archive was already on disk and `tar` was the long pole.
|
|
33
|
+
*/
|
|
34
|
+
export type FirefoxSourcePhase = 'download' | 'extract';
|
|
35
|
+
/** Callback fired at phase transitions during {@link downloadFirefoxSource}. */
|
|
36
|
+
export type FirefoxSourcePhaseCallback = (phase: FirefoxSourcePhase) => void;
|
|
26
37
|
/**
|
|
27
38
|
* Downloads and extracts Firefox source.
|
|
28
39
|
* @param version - Firefox version to download
|
|
29
40
|
* @param product - Firefox product type
|
|
30
41
|
* @param destDir - Destination directory for extracted source
|
|
31
42
|
* @param cacheDir - Directory to store downloaded tarball
|
|
32
|
-
* @param onProgress - Optional progress callback
|
|
43
|
+
* @param onProgress - Optional progress callback for the download byte stream
|
|
44
|
+
* @param onPhase - Optional callback fired when the function transitions
|
|
45
|
+
* between phases (`'download'` → `'extract'`). Fires exactly once per
|
|
46
|
+
* phase even if the cached archive path skips the wire entirely.
|
|
33
47
|
*/
|
|
34
|
-
export declare function downloadFirefoxSource(version: string, product: FirefoxProduct, destDir: string, cacheDir: string, onProgress?: ProgressCallback): Promise<void>;
|
|
48
|
+
export declare function downloadFirefoxSource(version: string, product: FirefoxProduct, destDir: string, cacheDir: string, onProgress?: ProgressCallback, onPhase?: FirefoxSourcePhaseCallback): Promise<void>;
|
package/dist/src/core/firefox.js
CHANGED
|
@@ -39,16 +39,21 @@ export function getTarballFilename(version, product = 'firefox') {
|
|
|
39
39
|
* @param product - Firefox product type
|
|
40
40
|
* @param destDir - Destination directory for extracted source
|
|
41
41
|
* @param cacheDir - Directory to store downloaded tarball
|
|
42
|
-
* @param onProgress - Optional progress callback
|
|
42
|
+
* @param onProgress - Optional progress callback for the download byte stream
|
|
43
|
+
* @param onPhase - Optional callback fired when the function transitions
|
|
44
|
+
* between phases (`'download'` → `'extract'`). Fires exactly once per
|
|
45
|
+
* phase even if the cached archive path skips the wire entirely.
|
|
43
46
|
*/
|
|
44
|
-
export async function downloadFirefoxSource(version, product, destDir, cacheDir, onProgress) {
|
|
47
|
+
export async function downloadFirefoxSource(version, product, destDir, cacheDir, onProgress, onPhase) {
|
|
45
48
|
const archive = resolveArchive(version, product);
|
|
46
49
|
const tarballPath = join(cacheDir, archive.filename);
|
|
47
50
|
// Ensure cache directory exists
|
|
48
51
|
await ensureDir(cacheDir);
|
|
52
|
+
onPhase?.('download');
|
|
49
53
|
await ensureCachedArchive(archive, cacheDir, onProgress);
|
|
50
54
|
// Extract to a unique temporary directory so concurrent downloads for
|
|
51
55
|
// the same destination do not clobber each other.
|
|
56
|
+
onPhase?.('extract');
|
|
52
57
|
const tempDir = `${destDir}.tmp-${randomUUID()}`;
|
|
53
58
|
try {
|
|
54
59
|
await extractTarXz(tarballPath, tempDir);
|
|
@@ -86,6 +86,29 @@ export declare function loadFurnaceConfig(root: string): Promise<FurnaceConfig>;
|
|
|
86
86
|
* @param config - Configuration to write
|
|
87
87
|
*/
|
|
88
88
|
export declare function writeFurnaceConfig(root: string, config: FurnaceConfig): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Stamps every override's `baseVersion` to the supplied version. Used by
|
|
91
|
+
* `fireforge rebase` after a successful patch re-export so a successful
|
|
92
|
+
* ESR bump does not leave Furnace overrides in a doctor-failing drift
|
|
93
|
+
* state. Returns the number of overrides stamped (zero if furnace.json
|
|
94
|
+
* has no overrides, or if the file is missing).
|
|
95
|
+
*
|
|
96
|
+
* Motivating case: a 140.9.0esr → 140.9.1esr rebase stamps patch
|
|
97
|
+
* `sourceEsrVersion` via `stampPatchVersions`, but before 0.16.0 no
|
|
98
|
+
* equivalent stamping ran for Furnace override `baseVersion`. `doctor`
|
|
99
|
+
* then immediately failed Furnace component validation on every
|
|
100
|
+
* override. The stamp is deliberately unconditional — `fireforge
|
|
101
|
+
* furnace validate` is the right tool for "does this override still
|
|
102
|
+
* apply", and rebase already attested that the patch layer re-validated
|
|
103
|
+
* against the new ESR; the per-override health check belongs in a
|
|
104
|
+
* separate pass, not inline with the stamp.
|
|
105
|
+
*
|
|
106
|
+
* @param root - Root directory of the project
|
|
107
|
+
* @param version - Firefox version string to stamp onto every override
|
|
108
|
+
* @returns Number of overrides whose `baseVersion` was updated (either
|
|
109
|
+
* because it was missing or because it differed from `version`).
|
|
110
|
+
*/
|
|
111
|
+
export declare function stampFurnaceOverrideBaseVersions(root: string, version: string): Promise<number>;
|
|
89
112
|
/**
|
|
90
113
|
* Creates a default furnace configuration.
|
|
91
114
|
* @returns A valid empty FurnaceConfig
|
|
@@ -420,6 +420,44 @@ export async function writeFurnaceConfig(root, config) {
|
|
|
420
420
|
const paths = getFurnacePaths(root);
|
|
421
421
|
await writeJson(paths.furnaceConfig, config);
|
|
422
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Stamps every override's `baseVersion` to the supplied version. Used by
|
|
425
|
+
* `fireforge rebase` after a successful patch re-export so a successful
|
|
426
|
+
* ESR bump does not leave Furnace overrides in a doctor-failing drift
|
|
427
|
+
* state. Returns the number of overrides stamped (zero if furnace.json
|
|
428
|
+
* has no overrides, or if the file is missing).
|
|
429
|
+
*
|
|
430
|
+
* Motivating case: a 140.9.0esr → 140.9.1esr rebase stamps patch
|
|
431
|
+
* `sourceEsrVersion` via `stampPatchVersions`, but before 0.16.0 no
|
|
432
|
+
* equivalent stamping ran for Furnace override `baseVersion`. `doctor`
|
|
433
|
+
* then immediately failed Furnace component validation on every
|
|
434
|
+
* override. The stamp is deliberately unconditional — `fireforge
|
|
435
|
+
* furnace validate` is the right tool for "does this override still
|
|
436
|
+
* apply", and rebase already attested that the patch layer re-validated
|
|
437
|
+
* against the new ESR; the per-override health check belongs in a
|
|
438
|
+
* separate pass, not inline with the stamp.
|
|
439
|
+
*
|
|
440
|
+
* @param root - Root directory of the project
|
|
441
|
+
* @param version - Firefox version string to stamp onto every override
|
|
442
|
+
* @returns Number of overrides whose `baseVersion` was updated (either
|
|
443
|
+
* because it was missing or because it differed from `version`).
|
|
444
|
+
*/
|
|
445
|
+
export async function stampFurnaceOverrideBaseVersions(root, version) {
|
|
446
|
+
if (!(await furnaceConfigExists(root)))
|
|
447
|
+
return 0;
|
|
448
|
+
const config = await loadFurnaceConfig(root);
|
|
449
|
+
let changed = 0;
|
|
450
|
+
for (const override of Object.values(config.overrides)) {
|
|
451
|
+
if (override.baseVersion !== version) {
|
|
452
|
+
override.baseVersion = version;
|
|
453
|
+
changed++;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (changed > 0) {
|
|
457
|
+
await writeFurnaceConfig(root, config);
|
|
458
|
+
}
|
|
459
|
+
return changed;
|
|
460
|
+
}
|
|
423
461
|
/**
|
|
424
462
|
* Creates a default furnace configuration.
|
|
425
463
|
* @returns A valid empty FurnaceConfig
|
|
@@ -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,44 @@ 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
|
+
},
|
|
37
|
+
{
|
|
38
|
+
// Upstream bindgen on some macOS libc++ SDK versions emits
|
|
39
|
+
// `pub type basic_string___self_view = root::std::__1::basic_string_view<_CharT>;`
|
|
40
|
+
// inside gecko-profiler's generated `bindings.rs`, but `_CharT` is
|
|
41
|
+
// not in scope where the alias lands — so the Rust compile fails
|
|
42
|
+
// with "cannot find type `_CharT`". The symptom is obscure and the
|
|
43
|
+
// fix is external: Hominis ships
|
|
44
|
+
// `990-infra-bindgen-basic-string-workaround.patch` in its patch
|
|
45
|
+
// queue, which strips the offending alias line post-generation.
|
|
46
|
+
// This hint surfaces the workaround pointer alongside the raw
|
|
47
|
+
// bindgen output so operators don't have to reverse-engineer the
|
|
48
|
+
// failure.
|
|
49
|
+
pattern: /cannot find type `_CharT` in this scope[\s\S]*?gecko-profiler-|gecko-profiler-[\s\S]*?cannot find type `_CharT` in this scope/,
|
|
50
|
+
hint: 'The Rust compile failed on a bindgen-generated `basic_string___self_view` alias in ' +
|
|
51
|
+
'gecko-profiler/bindings.rs. This is an upstream bindgen output bug against some ' +
|
|
52
|
+
'macOS libc++ SDK versions and needs a post-generation patch to strip the alias. ' +
|
|
53
|
+
'The known-working workaround is the `990-infra-bindgen-basic-string-workaround.patch` ' +
|
|
54
|
+
"Hominis ships in its patch queue — import the equivalent into your fork's patches/, " +
|
|
55
|
+
'then re-run "fireforge import" + "fireforge build". If you do not use Hominis\' queue, ' +
|
|
56
|
+
'apply the following post-process to the generated file before the Rust compile: ' +
|
|
57
|
+
'remove any `pub type basic_string___self_view = …<_CharT>;` line from ' +
|
|
58
|
+
'`<objdir>/release/build/gecko-profiler-*/out/gecko/bindings.rs`.',
|
|
59
|
+
},
|
|
22
60
|
];
|
|
23
61
|
/**
|
|
24
62
|
* 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
|
|
@@ -16,7 +16,15 @@ export function getRules(binaryName) {
|
|
|
16
16
|
extractArgs: (m) => [m[1] ?? ''],
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
|
|
19
|
+
// `.inc.xhtml` fragments under browser/base/content/ are deliberately
|
|
20
|
+
// excluded: they are consumed via `#include` from a registered chrome
|
|
21
|
+
// document (typically browser.xhtml) and do not get their own
|
|
22
|
+
// packaged chrome URI. Before this carve-out, `status` flagged every
|
|
23
|
+
// wired fragment as "potentially unregistered" and `register --dry-run`
|
|
24
|
+
// proposed a bogus jar.mn entry. The lookahead blocks the match so
|
|
25
|
+
// `getUnregistrableAdvice` gets a chance to emit the correct
|
|
26
|
+
// guidance for the `.inc.xhtml` case.
|
|
27
|
+
pattern: /^browser\/base\/content\/(?!.+\.inc\.xhtml$)(.+\.(?:js|mjs|xhtml|css))$/,
|
|
20
28
|
isRegistered: (engineDir, fileName) => isBrowserContentRegistered(engineDir, fileName),
|
|
21
29
|
register: (engineDir, after, dryRun, fileName) => registerBrowserContent(engineDir, fileName, after, undefined, dryRun),
|
|
22
30
|
extractArgs: (m) => [m[1] ?? ''],
|
|
@@ -98,6 +106,19 @@ function getUnregistrableAdvice(filePath) {
|
|
|
98
106
|
if (filePath.endsWith('.ftl')) {
|
|
99
107
|
return "FTL locale files are auto-discovered via jar.mn glob patterns and don't need manual registration.";
|
|
100
108
|
}
|
|
109
|
+
// `.inc.xhtml` fragments live under browser/base/content/ but are
|
|
110
|
+
// consumed via `#include` from a registered chrome document (browser.xhtml
|
|
111
|
+
// by default; a fork's custom top-level doc when `wire --dom-target` is
|
|
112
|
+
// set). The preprocessor resolves the include at packaging time, so the
|
|
113
|
+
// fragment never needs its own chrome URI entry in jar.mn. Give the
|
|
114
|
+
// operator the actionable `wire` path instead of letting the generic
|
|
115
|
+
// "unknown file pattern" message above fire.
|
|
116
|
+
if (/^browser\/base\/content\/.+\.inc\.xhtml$/.test(filePath)) {
|
|
117
|
+
return ('`.inc.xhtml` fragments are consumed via `#include` from a registered chrome document ' +
|
|
118
|
+
'(e.g. browser.xhtml). They do not need an independent jar.mn entry — run ' +
|
|
119
|
+
'"fireforge wire <name> --dom <path>" to insert the #include, or add the directive manually ' +
|
|
120
|
+
'in the top-level chrome document.');
|
|
121
|
+
}
|
|
101
122
|
const testMatch = filePath.match(/^browser\/base\/content\/test\/([^/]+)\/(?!browser\.toml$).+$/);
|
|
102
123
|
if (testMatch) {
|
|
103
124
|
const dir = testMatch[1];
|
|
@@ -159,6 +159,17 @@ export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config
|
|
|
159
159
|
// Check for non-tokenized custom properties. A variable that is both
|
|
160
160
|
// declared and consumed inside the same file is auto-exempted as a
|
|
161
161
|
// runtime state channel (see furnace.json → runtimeVariables).
|
|
162
|
+
//
|
|
163
|
+
// When diff context is available, scope the `var(...)` scan to
|
|
164
|
+
// added/modified lines only. `cssContent` (full-file) is still the
|
|
165
|
+
// source of `localDeclarations` so vars declared anywhere in the file
|
|
166
|
+
// are recognised as same-file refs regardless of where the consuming
|
|
167
|
+
// `var(...)` appears. Before this scoping change, a small edit to a
|
|
168
|
+
// Furnace override of a stock component (e.g. moz-card) produced a
|
|
169
|
+
// `token-prefix-violation` for every stock `var(--moz-card-*)` the
|
|
170
|
+
// upstream file already carried, because the scanner saw the full
|
|
171
|
+
// applied file and flagged each inherited reference as if the fork
|
|
172
|
+
// had introduced it.
|
|
162
173
|
if (tokenPrefix) {
|
|
163
174
|
const declarationPattern = /(?:^|[{;,\s])(--[\w-]+)\s*:/g;
|
|
164
175
|
const localDeclarations = new Set();
|
|
@@ -168,26 +179,38 @@ export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config
|
|
|
168
179
|
if (name)
|
|
169
180
|
localDeclarations.add(name);
|
|
170
181
|
}
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
182
|
+
const prefixScanSource = addedLinesByFile
|
|
183
|
+
? (addedLinesByFile.get(file) ?? []).join('\n').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
184
|
+
: cssContent;
|
|
185
|
+
if (prefixScanSource.length > 0) {
|
|
186
|
+
const varPattern = /var\(\s*(--[\w-]+)/g;
|
|
187
|
+
const flaggedProps = new Set();
|
|
188
|
+
let match;
|
|
189
|
+
while ((match = varPattern.exec(prefixScanSource)) !== null) {
|
|
190
|
+
const prop = match[1];
|
|
191
|
+
if (!prop)
|
|
192
|
+
continue;
|
|
193
|
+
if (prop.startsWith(tokenPrefix))
|
|
194
|
+
continue;
|
|
195
|
+
if (tokenAllowlist?.has(prop))
|
|
196
|
+
continue;
|
|
197
|
+
if (runtimeVariables?.has(prop))
|
|
198
|
+
continue;
|
|
199
|
+
if (localDeclarations.has(prop))
|
|
200
|
+
continue;
|
|
201
|
+
// De-duplicate per (file, prop) pair so the same introduced var
|
|
202
|
+
// used five times in the added hunk doesn't produce five
|
|
203
|
+
// identical issue entries.
|
|
204
|
+
if (flaggedProps.has(prop))
|
|
205
|
+
continue;
|
|
206
|
+
flaggedProps.add(prop);
|
|
207
|
+
issues.push({
|
|
208
|
+
file,
|
|
209
|
+
check: 'token-prefix-violation',
|
|
210
|
+
message: `CSS references var(${prop}) which does not match the required token prefix "${tokenPrefix}". Use a design token, add to tokenAllowlist, or (for runtime state channels) list the variable in runtimeVariables.`,
|
|
211
|
+
severity: 'error',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
191
214
|
}
|
|
192
215
|
}
|
|
193
216
|
}
|