@hominis/fireforge 0.34.1 → 0.35.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +17 -4
  3. package/dist/bin/fireforge.d.ts +4 -0
  4. package/dist/bin/fireforge.js +4 -0
  5. package/dist/src/commands/build.js +13 -0
  6. package/dist/src/commands/discard.js +6 -2
  7. package/dist/src/commands/download.js +23 -2
  8. package/dist/src/commands/run.js +14 -2
  9. package/dist/src/commands/test.js +46 -6
  10. package/dist/src/core/build-prepare.js +19 -5
  11. package/dist/src/core/firefox-cache.js +7 -4
  12. package/dist/src/core/firefox-extract.d.ts +21 -1
  13. package/dist/src/core/firefox-extract.js +117 -7
  14. package/dist/src/core/git.js +2 -2
  15. package/dist/src/core/license-headers.d.ts +16 -0
  16. package/dist/src/core/license-headers.js +19 -0
  17. package/dist/src/core/mach-error-hints.js +27 -0
  18. package/dist/src/core/mach-resource-shim.d.ts +42 -7
  19. package/dist/src/core/mach-resource-shim.js +227 -25
  20. package/dist/src/core/patch-apply.js +51 -21
  21. package/dist/src/core/patch-lint.js +12 -1
  22. package/dist/src/core/patch-manifest-io.js +7 -7
  23. package/dist/src/core/patch-manifest-query.d.ts +6 -0
  24. package/dist/src/core/patch-manifest-query.js +9 -2
  25. package/dist/src/core/test-harness-crash.js +12 -0
  26. package/dist/src/core/test-path-scope.d.ts +54 -0
  27. package/dist/src/core/test-path-scope.js +133 -0
  28. package/dist/src/core/toolchain-preflight.d.ts +64 -0
  29. package/dist/src/core/toolchain-preflight.js +195 -0
  30. package/dist/src/core/typecheck-shim.d.ts +4 -1
  31. package/dist/src/core/typecheck-shim.js +48 -17
  32. package/dist/src/errors/download.js +2 -0
  33. package/dist/src/types/commands/options.d.ts +7 -0
  34. package/package.json +1 -1
@@ -0,0 +1,195 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Toolchain-minimum awareness for Firefox source hops.
4
+ *
5
+ * When `fireforge download --force` moves the engine to a new Firefox
6
+ * MAJOR version, the tree's declared toolchain minimums (cbindgen, Rust)
7
+ * frequently move with it — and the first `fireforge build` then dies a
8
+ * few seconds into `mach configure` with a message whose remediation
9
+ * text names `./mach bootstrap`, the wrong tool for a FireForge-managed
10
+ * repo (152.0b7 → 153.0b8 source-refresh drill: `ERROR: cbindgen version
11
+ * 0.29.1 is too old. At least version 0.29.4 is required.`).
12
+ *
13
+ * Two layers:
14
+ * 1. {@link formatMajorVersionHopNotice} — a post-download nudge when
15
+ * the downloaded major differs from the previously downloaded one.
16
+ * 2. {@link runToolchainPreflight} — a cheap pre-build probe comparing
17
+ * the minimums the tree itself declares against the host binaries
18
+ * `mach configure` will resolve. Deliberately FAIL-SOFT: it reports
19
+ * a mismatch only when a minimum was positively parsed AND the host
20
+ * binary was found AND its version is definitively lower. Any
21
+ * uncertainty (file moved upstream, unparseable output, binary not
22
+ * on PATH) skips silently — the mach-error-hints translator still
23
+ * catches the real configure failure downstream.
24
+ */
25
+ import { execFile } from 'node:child_process';
26
+ import { join } from 'node:path';
27
+ import { pathExists, readText } from '../utils/fs.js';
28
+ import { verbose } from '../utils/logger.js';
29
+ /** Where each tool's minimum is declared inside the Firefox tree. */
30
+ const MINIMUM_DECLARATIONS = {
31
+ // build/moz.configure/bindgen.configure:
32
+ // cbindgen_min_version = Version("0.27.0")
33
+ cbindgen: {
34
+ relPath: 'build/moz.configure/bindgen.configure',
35
+ pattern: /cbindgen_min_version\s*=\s*Version\(\s*["']([\d.]+)["']\s*\)/,
36
+ },
37
+ // python/mozboot/mozboot/util.py:
38
+ // MINIMUM_RUST_VERSION = "1.82.0"
39
+ // (build/moz.configure/rust.configure imports this constant, so the
40
+ // mozboot file is the single authority in the tree.)
41
+ rustc: {
42
+ relPath: 'python/mozboot/mozboot/util.py',
43
+ pattern: /MINIMUM_RUST_VERSION\s*=\s*["']([\d.]+)["']/,
44
+ },
45
+ };
46
+ /** How each tool's host binary is resolved and its version parsed. */
47
+ const HOST_PROBES = {
48
+ // `mach configure` honours the CBINDGEN env option (bindgen.configure's
49
+ // `option(env="CBINDGEN", ...)`), so the probe must too — otherwise the
50
+ // preflight could veto a build configure would have accepted.
51
+ cbindgen: { envVar: 'CBINDGEN', versionPattern: /cbindgen\s+(\d+(?:\.\d+)*)/ },
52
+ rustc: { envVar: 'RUSTC', versionPattern: /rustc\s+(\d+(?:\.\d+)*)/ },
53
+ };
54
+ /**
55
+ * Parses a dotted version string into numeric components. Trailing
56
+ * non-numeric suffixes are ignored; returns undefined when the string
57
+ * does not start with a number. (Same posture as the furnace version
58
+ * drift classifier, kept local so the two modules stay decoupled.)
59
+ */
60
+ function parseVersionComponents(version) {
61
+ const match = /^(\d+(?:\.\d+)*)/.exec(version.trim());
62
+ if (!match?.[1])
63
+ return undefined;
64
+ return match[1].split('.').map(Number);
65
+ }
66
+ /** Component-wise comparison; missing components count as 0. */
67
+ function isVersionLower(candidate, minimum) {
68
+ const length = Math.max(candidate.length, minimum.length);
69
+ for (let i = 0; i < length; i += 1) {
70
+ const a = candidate[i] ?? 0;
71
+ const b = minimum[i] ?? 0;
72
+ if (a !== b)
73
+ return a < b;
74
+ }
75
+ return false;
76
+ }
77
+ /**
78
+ * Returns the one-line notice to print after a download that hopped the
79
+ * Firefox MAJOR version, or undefined when no notice is warranted (first
80
+ * download, same major, or unparseable versions — an unparseable version
81
+ * must not spam every download with a false hint).
82
+ *
83
+ * @param previousVersion - `downloadedVersion` recorded in state before this download
84
+ * @param newVersion - Version that was just downloaded
85
+ */
86
+ export function formatMajorVersionHopNotice(previousVersion, newVersion) {
87
+ if (!previousVersion)
88
+ return undefined;
89
+ const previousMajor = parseVersionComponents(previousVersion)?.[0];
90
+ const newMajor = parseVersionComponents(newVersion)?.[0];
91
+ if (previousMajor === undefined || newMajor === undefined)
92
+ return undefined;
93
+ if (previousMajor === newMajor)
94
+ return undefined;
95
+ return (`Firefox major version changed (${String(previousMajor)} → ${String(newMajor)}): ` +
96
+ 'upstream toolchain minimums (cbindgen, Rust, …) may have moved with it. ' +
97
+ 'Consider running "fireforge bootstrap" before the next build.');
98
+ }
99
+ /**
100
+ * Reads the toolchain minimums the engine tree itself declares. Any file
101
+ * that is missing or no longer matches the expected declaration shape
102
+ * yields undefined for that tool — never an error.
103
+ */
104
+ export async function readDeclaredToolchainMinimums(engineDir) {
105
+ const minimums = {};
106
+ for (const [tool, declaration] of Object.entries(MINIMUM_DECLARATIONS)) {
107
+ const filePath = join(engineDir, declaration.relPath);
108
+ if (!(await pathExists(filePath))) {
109
+ verbose(`Toolchain preflight: ${declaration.relPath} not found; skipping ${tool}.`);
110
+ continue;
111
+ }
112
+ try {
113
+ const content = await readText(filePath);
114
+ const match = declaration.pattern.exec(content);
115
+ if (match?.[1]) {
116
+ minimums[tool] = match[1];
117
+ }
118
+ else {
119
+ verbose(`Toolchain preflight: no ${tool} minimum declaration recognized in ${declaration.relPath}; skipping.`);
120
+ }
121
+ }
122
+ catch (error) {
123
+ verbose(`Toolchain preflight: could not read ${declaration.relPath} (${error instanceof Error ? error.message : String(error)}); skipping ${tool}.`);
124
+ }
125
+ }
126
+ return minimums;
127
+ }
128
+ /** Runs `<binary> --version` and parses the leading version number. */
129
+ async function probeHostToolVersion(tool) {
130
+ const probe = HOST_PROBES[tool];
131
+ const binary = process.env[probe.envVar] ?? tool;
132
+ const output = await new Promise((resolvePromise) => {
133
+ execFile(binary, ['--version'], { timeout: 10_000 }, (err, stdout) => {
134
+ resolvePromise(err ? undefined : stdout);
135
+ });
136
+ });
137
+ if (output === undefined) {
138
+ verbose(`Toolchain preflight: "${binary} --version" failed or not found; skipping ${tool}.`);
139
+ return undefined;
140
+ }
141
+ const match = probe.versionPattern.exec(output);
142
+ if (!match?.[1]) {
143
+ verbose(`Toolchain preflight: could not parse ${tool} version from "${output.trim()}".`);
144
+ return undefined;
145
+ }
146
+ return match[1];
147
+ }
148
+ /**
149
+ * Compares the tree-declared toolchain minimums against the host binaries
150
+ * `mach configure` will resolve. Returns only DEFINITIVE mismatches; every
151
+ * uncertain probe passes silently (see module header for the rationale).
152
+ *
153
+ * @param engineDir - Path to the engine directory
154
+ */
155
+ export async function runToolchainPreflight(engineDir) {
156
+ const minimums = await readDeclaredToolchainMinimums(engineDir);
157
+ const mismatches = [];
158
+ for (const [tool, minimumVersion] of Object.entries(minimums)) {
159
+ const minimum = parseVersionComponents(minimumVersion);
160
+ if (!minimum)
161
+ continue;
162
+ const hostVersion = await probeHostToolVersion(tool);
163
+ if (hostVersion === undefined)
164
+ continue;
165
+ const host = parseVersionComponents(hostVersion);
166
+ if (!host)
167
+ continue;
168
+ if (isVersionLower(host, minimum)) {
169
+ mismatches.push({
170
+ tool,
171
+ hostVersion,
172
+ minimumVersion,
173
+ declaredIn: MINIMUM_DECLARATIONS[tool].relPath,
174
+ });
175
+ }
176
+ else {
177
+ verbose(`Toolchain preflight: ${tool} ${hostVersion} satisfies minimum ${minimumVersion}.`);
178
+ }
179
+ }
180
+ return mismatches;
181
+ }
182
+ /**
183
+ * Formats the fail-fast message for definitive preflight mismatches,
184
+ * naming `fireforge bootstrap` as the remedy (mach's own configure error
185
+ * suggests `./mach bootstrap`, which is the wrong entry point for a
186
+ * FireForge-managed repo).
187
+ */
188
+ export function formatToolchainMismatchMessage(mismatches) {
189
+ const lines = mismatches.map((m) => ` - ${m.tool} ${m.hostVersion} is older than the minimum ${m.minimumVersion} declared by this Firefox source (engine/${m.declaredIn})`);
190
+ return ('Toolchain preflight found host tools older than what this Firefox source requires:\n' +
191
+ `${lines.join('\n')}\n\n` +
192
+ 'This typically happens after "fireforge download --force" moved the engine to a new ' +
193
+ 'Firefox major version. Run "fireforge bootstrap" to update the toolchain, then retry the build.');
194
+ }
195
+ //# sourceMappingURL=toolchain-preflight.js.map
@@ -40,7 +40,10 @@ export interface ComposedShim {
40
40
  * direction is intentional (declarations later in concat order
41
41
  * augment earlier ones), so a project that wants to refine `Services`
42
42
  * with a more specific type can do so by declaring it in the extra
43
- * shim. Any triple-slash `/// <reference path="…">` directives inside the
43
+ * shim, and members can be ADDED to the structured globals by merging
44
+ * their interfaces (`interface ChromeUtilsShim { newMember(): any }` —
45
+ * see the module doc comment). Any triple-slash
46
+ * `/// <reference path="…">` directives inside the
44
47
  * extra shim are inlined (resolved against the extra shim's own directory)
45
48
  * so they are not silently dropped at the synthetic shim path.
46
49
  *
@@ -22,6 +22,23 @@ export const SHIM_FILENAME = '__fireforge_firefox_globals.d.ts';
22
22
  * for the most common Mozilla APIs. Types are intentionally loose
23
23
  * (`any`) because full Firefox type coverage is out of scope.
24
24
  *
25
+ * Structured globals (`ChromeUtils`, `Localization`) are declared via
26
+ * named global interfaces (`ChromeUtilsShim`, `LocalizationShim`, …)
27
+ * rather than closed object-literal types, so a project's extra shim
28
+ * (`patchLint.checkJsExtraShim` / `typecheck.extraShim`) can ADD members
29
+ * through TypeScript interface merging:
30
+ *
31
+ * // my-extra-shim.d.ts
32
+ * interface ChromeUtilsShim {
33
+ * someNewApi(arg: string): unknown;
34
+ * }
35
+ *
36
+ * (A second `declare var ChromeUtils` in the extra shim remains a
37
+ * duplicate-identifier error by design — merge the interface instead.)
38
+ * The member lists track upstream WebIDL additions per Firefox release
39
+ * (`dom/chrome-webidl/ChromeUtils.webidl` for ChromeUtils); when a new
40
+ * release adds a commonly-patched member, add a loose signature here.
41
+ *
25
42
  * Notable patterns that require shimming:
26
43
  * - `const lazy = {};` + `ChromeUtils.defineESModuleGetters(lazy, { ... })`
27
44
  * populates `lazy` at runtime; we declare it as `Record<string, any>`.
@@ -37,7 +54,9 @@ export const SHIM_FILENAME = '__fireforge_firefox_globals.d.ts';
37
54
  */
38
55
  const FIREFOX_GLOBALS_SHIM = `
39
56
  declare var Services: any;
40
- declare var ChromeUtils: {
57
+ // Extensible via interface merging from a project extra shim — see the
58
+ // module doc comment in typecheck-shim.ts.
59
+ interface ChromeUtilsShim {
41
60
  defineESModuleGetters(target: any, modules: Record<string, string>): void;
42
61
  importESModule(specifier: string): any;
43
62
  import(specifier: string): any;
@@ -46,7 +65,12 @@ declare var ChromeUtils: {
46
65
  generateQI(interfaces: any[]): Function;
47
66
  getClassName(obj: any, unwrap?: boolean): string;
48
67
  isClassInfo(obj: any): boolean;
49
- };
68
+ // Firefox 153, dom/chrome-webidl/ChromeUtils.webidl: two overloads
69
+ // (URI string / nsIURI) with a PredictRemoteTypeOptions dictionary —
70
+ // collapsed into one loose signature per the shim's pragmatic posture.
71
+ predictRemoteTypeForURI(uri: string | object | null, options?: object): string | null;
72
+ }
73
+ declare var ChromeUtils: ChromeUtilsShim;
50
74
  declare var Cu: any;
51
75
  declare var Ci: any;
52
76
  declare var Cc: any;
@@ -63,23 +87,27 @@ declare var gNavigatorBundle: any;
63
87
  declare var AppConstants: any;
64
88
  // Fluent localization — a stable chrome global. Members stay loose (any),
65
89
  // but the constructor shape is declared so "new Localization([...])" and
66
- // "new Localization([...], true)" typecheck without a local cast.
67
- declare var Localization: {
90
+ // "new Localization([...], true)" typecheck without a local cast. Both
91
+ // interfaces are extensible via interface merging from a project extra
92
+ // shim, same as ChromeUtilsShim.
93
+ interface LocalizationInstanceShim {
94
+ formatValue(id: string, args?: Record<string, unknown>): any;
95
+ formatValues(keys: any[]): any;
96
+ formatMessages(keys: any[]): any;
97
+ formatValueSync(id: string, args?: Record<string, unknown>): any;
98
+ formatValuesSync(keys: any[]): any;
99
+ formatMessagesSync(keys: any[]): any;
100
+ addResourceIds(ids: Array<string | { path: string; optional?: boolean }>): void;
101
+ removeResourceIds(ids: string[]): number;
102
+ setAsync(): void;
103
+ }
104
+ interface LocalizationShim {
68
105
  new (
69
106
  resourceIds: Array<string | { path: string; optional?: boolean }>,
70
107
  sync?: boolean
71
- ): {
72
- formatValue(id: string, args?: Record<string, unknown>): any;
73
- formatValues(keys: any[]): any;
74
- formatMessages(keys: any[]): any;
75
- formatValueSync(id: string, args?: Record<string, unknown>): any;
76
- formatValuesSync(keys: any[]): any;
77
- formatMessagesSync(keys: any[]): any;
78
- addResourceIds(ids: Array<string | { path: string; optional?: boolean }>): void;
79
- removeResourceIds(ids: string[]): number;
80
- setAsync(): void;
81
- };
82
- };
108
+ ): LocalizationInstanceShim;
109
+ }
110
+ declare var Localization: LocalizationShim;
83
111
 
84
112
  // Shorthand ambient modules — exports from matching URL imports are loosely typed,
85
113
  // avoiding noResolve empty-graph namespaces. (Named member access broke when we tried
@@ -154,7 +182,10 @@ async function inlineTripleSlashReferences(source, baseDir, seen) {
154
182
  * direction is intentional (declarations later in concat order
155
183
  * augment earlier ones), so a project that wants to refine `Services`
156
184
  * with a more specific type can do so by declaring it in the extra
157
- * shim. Any triple-slash `/// <reference path="…">` directives inside the
185
+ * shim, and members can be ADDED to the structured globals by merging
186
+ * their interfaces (`interface ChromeUtilsShim { newMember(): any }` —
187
+ * see the module doc comment). Any triple-slash
188
+ * `/// <reference path="…">` directives inside the
158
189
  * extra shim are inlined (resolved against the extra shim's own directory)
159
190
  * so they are not silently dropped at the synthetic shim path.
160
191
  *
@@ -65,8 +65,10 @@ export class ExtractionError extends DownloadError {
65
65
  this.archivePath = archivePath;
66
66
  }
67
67
  get userMessage() {
68
+ const reason = this.cause instanceof Error ? `Reason: ${this.cause.message}\n\n` : '';
68
69
  return (`Extraction Error: Failed to extract Firefox source archive.\n\n` +
69
70
  `Archive: ${this.archivePath}\n\n` +
71
+ reason +
70
72
  'To fix this:\n' +
71
73
  ' 1. Delete the corrupted archive and try again\n' +
72
74
  ' 2. Ensure you have enough disk space\n' +
@@ -389,6 +389,13 @@ export interface RunOptions {
389
389
  * inspect the raw stream after smoke-exit returns.
390
390
  */
391
391
  captureConsole?: string;
392
+ /**
393
+ * Launch the browser with `--headless` (forwarded to the binary via
394
+ * `mach run`). Primarily for smoke mode on shared desktops: a headed
395
+ * smoke window absorbs live keyboard/mouse input, and the resulting
396
+ * console errors contaminate the capture and can fail the run.
397
+ */
398
+ headless?: boolean;
392
399
  }
393
400
  /**
394
401
  * Options for the test command.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.34.1",
3
+ "version": "0.35.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",