@hominis/fireforge 0.27.0 → 0.27.2
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 +11 -1
- package/README.md +6 -6
- package/dist/src/cli.js +5 -1
- package/dist/src/commands/build.js +61 -1
- package/dist/src/commands/doctor/post-rebase-audit.d.ts +2 -0
- package/dist/src/commands/doctor/post-rebase-audit.js +86 -0
- package/dist/src/commands/doctor-working-tree.js +5 -1
- package/dist/src/commands/doctor.js +3 -0
- package/dist/src/commands/download.js +41 -42
- package/dist/src/commands/export-all.js +3 -2
- package/dist/src/commands/export-flow.d.ts +2 -0
- package/dist/src/commands/export-flow.js +2 -0
- package/dist/src/commands/export.js +5 -4
- package/dist/src/commands/import.js +2 -1
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/re-export.js +10 -8
- package/dist/src/commands/rebase/conflict-summary.d.ts +12 -0
- package/dist/src/commands/rebase/conflict-summary.js +38 -0
- package/dist/src/commands/rebase/continue.js +2 -0
- package/dist/src/commands/rebase/index.d.ts +2 -2
- package/dist/src/commands/rebase/index.js +9 -4
- package/dist/src/commands/rebase/patch-loop.js +29 -11
- package/dist/src/commands/rebase/summary.js +7 -2
- package/dist/src/commands/resolve.js +2 -1
- package/dist/src/commands/setup-support.js +6 -2
- package/dist/src/commands/setup.js +1 -0
- package/dist/src/commands/source.d.ts +9 -0
- package/dist/src/commands/source.js +92 -0
- package/dist/src/commands/status-output.d.ts +13 -0
- package/dist/src/commands/status-output.js +186 -0
- package/dist/src/commands/status.js +4 -247
- package/dist/src/commands/verify.js +32 -16
- package/dist/src/core/build-prepare.js +12 -4
- package/dist/src/core/config-validate.js +1 -1
- package/dist/src/core/firefox-cache.d.ts +1 -1
- package/dist/src/core/firefox-cache.js +10 -3
- package/dist/src/core/firefox-extract.d.ts +1 -1
- package/dist/src/core/firefox-extract.js +13 -1
- package/dist/src/core/firefox.d.ts +2 -1
- package/dist/src/core/firefox.js +3 -3
- package/dist/src/core/furnace-registration-validate.d.ts +7 -0
- package/dist/src/core/furnace-registration-validate.js +29 -12
- package/dist/src/core/furnace-validate-registration.js +5 -37
- package/dist/src/core/git.js +25 -5
- package/dist/src/core/ownership-table.d.ts +3 -1
- package/dist/src/core/ownership-table.js +31 -7
- package/dist/src/core/patch-artifact-normalize.d.ts +9 -0
- package/dist/src/core/patch-artifact-normalize.js +13 -0
- package/dist/src/core/patch-export-update.js +2 -1
- package/dist/src/core/patch-export.d.ts +4 -0
- package/dist/src/core/patch-export.js +7 -2
- package/dist/src/core/patch-manifest-consistency.d.ts +1 -1
- package/dist/src/core/patch-manifest-consistency.js +4 -2
- package/dist/src/core/patch-manifest-query.d.ts +4 -3
- package/dist/src/core/patch-manifest-query.js +12 -4
- package/dist/src/core/patch-manifest-validate.js +22 -4
- package/dist/src/core/patch-source-metadata.d.ts +8 -0
- package/dist/src/core/patch-source-metadata.js +17 -0
- package/dist/src/core/rebase-session.d.ts +8 -3
- package/dist/src/core/rebase-session.js +1 -1
- package/dist/src/core/status-classify.d.ts +4 -1
- package/dist/src/core/status-classify.js +4 -5
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +16 -1
- package/dist/src/types/commands/patches.d.ts +9 -1
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/utils/elapsed.d.ts +4 -0
- package/dist/src/utils/elapsed.js +15 -0
- package/dist/src/utils/validation.d.ts +2 -2
- package/dist/src/utils/validation.js +5 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## 0.27.0
|
|
4
4
|
|
|
5
|
+
- Added first-class `firefox-devedition` source support and atomic `fireforge source set`.
|
|
6
|
+
- Fixed `source set --version` so the subcommand accepts both space and equals forms without colliding with the root CLI version flag.
|
|
7
|
+
- Added `sourceProduct` and `sourceVersion` patch metadata while preserving `sourceEsrVersion` as a deprecated compatibility alias.
|
|
8
|
+
- Renamed source-rebase reporting away from ESR-only wording and clarified summaries with total patch counts.
|
|
9
|
+
- Unified status, ownership, doctor, and verify worktree classification, including an explained patch-owned drift state for manually resolved or re-exported files.
|
|
10
|
+
- Hardened build diagnostics so backend regeneration success/failure and failed make/mach commands include exit codes, tails, log hints, and verbose rerun suggestions.
|
|
5
11
|
- Improved `download --force` git indexing progress with phase, count, and heartbeat output.
|
|
12
|
+
- Added cache metadata progress for archive validation, SHA-256 calculation, and sidecar JSON writes.
|
|
13
|
+
- Added elapsed progress for extraction, initial source commits, and rebase/re-export patch refreshes.
|
|
6
14
|
- Added `re-export --files --allow-shrink` so patch ownership shrinkage is refused unless explicitly acknowledged, with clearer dry-run previews.
|
|
7
15
|
- Surfaced likely new sibling files during plain re-export and aligned verify/status ownership reporting for unowned worktree changes.
|
|
8
16
|
- Preserved patch-owned branding `configure.sh` settings during build preflight.
|
|
9
|
-
- Added custom element registration support for Furnace validate/apply.
|
|
17
|
+
- Added custom element registration support for Furnace validate/apply and Firefox 152-style array-backed ESM registrations.
|
|
18
|
+
- Normalized generated patch artifacts so blank context lines do not trip raw whitespace checks.
|
|
19
|
+
- Improved rebase conflict summaries and added `doctor --post-rebase-audit` for common registration surfaces.
|
|
10
20
|
|
|
11
21
|
## 0.26.0
|
|
12
22
|
|
package/README.md
CHANGED
|
@@ -11,12 +11,12 @@ Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon
|
|
|
11
11
|
## What It Does
|
|
12
12
|
|
|
13
13
|
- **Patch based** Edit Firefox inside the `engine/` directory, then export changes into `.patch` files with manifest metadata.
|
|
14
|
-
- **
|
|
14
|
+
- **Source rebasing** Reapply your patches onto newer Firefox source trees, including ESR, Beta, and Developer Edition archives, resolve rejects and re-export the queue against the new baseline, hopefully...
|
|
15
15
|
- **Firefox source and build helpers** Download, bootstrap, build, run, test, package, smoke-check, etc.
|
|
16
16
|
- **Wiring and registration** Add chrome scripts, DOM fragments, modules, styles, tests and manifest entries through commands built by learning from existing Firefox conventions.
|
|
17
17
|
- **Furnace components** Create or override `MozLitElement` widgets easily to add new or adapt existing UI components to your needs.
|
|
18
18
|
- **Quality** `lint`, `typecheck`, `verify` and `doctor` catch common issues early.
|
|
19
|
-
- **Tests** Fireforge was build by taking apart and applying patches of all sorts to original Firefox
|
|
19
|
+
- **Tests** Fireforge was build by taking apart and applying patches of all sorts to original Firefox source code across different versions and products, learning what works vs doesn't and creating some quite extensive tests based on that covering all manner of scenarios. Yes, we mock quite a bit, but when building a tool that modifies a separate code base, I think it's a solid compromise for the time being. Full end-to-end runs are currently run locally on my MacBook, as they require about 30 GB of disk and significant compute for multiple full builds. Full end-to-end via Actions will be added soonishlyTM but might need a different runner...
|
|
20
20
|
|
|
21
21
|
## Requirements
|
|
22
22
|
|
|
@@ -63,12 +63,12 @@ npx fireforge test browser/base/content/test/browser/
|
|
|
63
63
|
|
|
64
64
|
Use `fireforge --help` for the full set of commands.
|
|
65
65
|
|
|
66
|
-
## Rebasing Firefox
|
|
66
|
+
## Rebasing Firefox Source
|
|
67
67
|
|
|
68
|
-
When Mozilla publishes a new
|
|
68
|
+
When Mozilla publishes a new Firefox source release you need to update the configured version/product, download the new source code and reapply the patches:
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
npx fireforge
|
|
71
|
+
npx fireforge source set --version 145.0.0esr --product firefox-esr --sha256 <archive-sha256>
|
|
72
72
|
npx fireforge download --force
|
|
73
73
|
npx fireforge rebase
|
|
74
74
|
```
|
|
@@ -95,7 +95,7 @@ Use `fireforge furnace --help` for the full set of component commands.
|
|
|
95
95
|
- **Docker builds** Reproducible builds using Docker containers.
|
|
96
96
|
- **CI mode** Automated setup for continuous integration pipelines.
|
|
97
97
|
- **Update manifests** Generate update server manifests for auto-updates.
|
|
98
|
-
- **Nightly support** Requires implementing `hg clone` support via mozilla-central.
|
|
98
|
+
- **Nightly source support** Requires implementing `hg clone` support via mozilla-central. ESR, Beta, and Developer Edition source archives are supported through `fireforge source set`.
|
|
99
99
|
- **E2E Github Actions** Requires either a higher tier of Githubs offering, an external VPS or another provider entirely. In any case, full end-to-end testing is currently run solely locally.
|
|
100
100
|
|
|
101
101
|
## Licence
|
package/dist/src/cli.js
CHANGED
|
@@ -154,6 +154,7 @@ function buildGroupedHelpFormatter(manifest) {
|
|
|
154
154
|
const desc = helper.optionDescription(opt);
|
|
155
155
|
return formatHelpLine(term, desc, termWidth, helpWidth);
|
|
156
156
|
});
|
|
157
|
+
optionLines.unshift(formatHelpLine('-V, --version', 'output the version number', termWidth, helpWidth));
|
|
157
158
|
if (optionLines.length > 0) {
|
|
158
159
|
output.push('Options:');
|
|
159
160
|
output.push(...optionLines);
|
|
@@ -234,7 +235,6 @@ export function createProgram() {
|
|
|
234
235
|
program
|
|
235
236
|
.name('fireforge')
|
|
236
237
|
.description('A build tool for customizing Firefox')
|
|
237
|
-
.version(getPackageVersion())
|
|
238
238
|
.option('-v, --verbose', 'Enable debug output')
|
|
239
239
|
.hook('preAction', (thisCommand) => {
|
|
240
240
|
const opts = thisCommand.opts();
|
|
@@ -256,6 +256,10 @@ export function createProgram() {
|
|
|
256
256
|
* Main CLI entry point.
|
|
257
257
|
*/
|
|
258
258
|
export async function main() {
|
|
259
|
+
if (process.argv.length === 3 && (process.argv[2] === '--version' || process.argv[2] === '-V')) {
|
|
260
|
+
process.stdout.write(`${getPackageVersion()}\n`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
259
263
|
const program = createProgram();
|
|
260
264
|
await program.parseAsync(process.argv);
|
|
261
265
|
}
|
|
@@ -69,6 +69,65 @@ function resolveJobCount(options, configJobs) {
|
|
|
69
69
|
}
|
|
70
70
|
return jobs;
|
|
71
71
|
}
|
|
72
|
+
function tailLines(text, maxLines) {
|
|
73
|
+
const lines = text
|
|
74
|
+
.split(/\r?\n/)
|
|
75
|
+
.map((line) => line.trimEnd())
|
|
76
|
+
.filter((line) => line.length > 0);
|
|
77
|
+
return lines.slice(-maxLines).join('\n');
|
|
78
|
+
}
|
|
79
|
+
function extractLastMakeError(captured) {
|
|
80
|
+
const lines = captured.split(/\r?\n/).filter((line) => /\bmake(?:\[\d+\])?: \*\*\*/.test(line));
|
|
81
|
+
return lines.at(-1)?.trim();
|
|
82
|
+
}
|
|
83
|
+
function extractLikelyFailingCommand(captured) {
|
|
84
|
+
const lines = captured
|
|
85
|
+
.split(/\r?\n/)
|
|
86
|
+
.map((line) => line.trim())
|
|
87
|
+
.filter((line) => line.length > 0);
|
|
88
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
89
|
+
const line = lines[index];
|
|
90
|
+
if (!line)
|
|
91
|
+
continue;
|
|
92
|
+
if (/^make(?:\[\d+\])?:/.test(line))
|
|
93
|
+
continue;
|
|
94
|
+
if (/^g?make(?:\[\d+\])?:/.test(line))
|
|
95
|
+
continue;
|
|
96
|
+
if (/^Error running mach:/.test(line))
|
|
97
|
+
continue;
|
|
98
|
+
if (/^\d+:\d+\.\d+\s+/.test(line))
|
|
99
|
+
continue;
|
|
100
|
+
if (/\b(?:cp|clang|clang\+\+|rustc|python|node|make|install_name_tool)\b/.test(line)) {
|
|
101
|
+
return line;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
function buildFailureDiagnostics(result, engineDir, objDir, machCommand) {
|
|
107
|
+
const captured = `${result.stderr}\n${result.stdout}`;
|
|
108
|
+
const stderrTail = tailLines(result.stderr, 20);
|
|
109
|
+
const combinedTail = tailLines(captured, 30);
|
|
110
|
+
const makeError = extractLastMakeError(captured);
|
|
111
|
+
const failingCommand = extractLikelyFailingCommand(captured);
|
|
112
|
+
const logHint = objDir
|
|
113
|
+
? `engine/${objDir}/ (inspect build logs, warnings, and generated make targets under this objdir)`
|
|
114
|
+
: 'engine/obj-* (inspect the active objdir for build logs, warnings, and generated make targets)';
|
|
115
|
+
const verboseRerun = objDir
|
|
116
|
+
? `cd ${engineDir} && ./mach build -v; if a make target is named above, retry it with: make -C ${objDir} <target> V=1`
|
|
117
|
+
: `cd ${engineDir} && ./mach build -v`;
|
|
118
|
+
return [
|
|
119
|
+
`Build failed with exit code ${result.exitCode}.`,
|
|
120
|
+
`Mach phase: ${machCommand}`,
|
|
121
|
+
makeError ? `Last make error: ${makeError}` : undefined,
|
|
122
|
+
failingCommand ? `Final failing command/error line: ${failingCommand}` : undefined,
|
|
123
|
+
stderrTail ? `Captured stderr tail:\n${stderrTail}` : undefined,
|
|
124
|
+
`Captured output tail:\n${combinedTail}`,
|
|
125
|
+
`Logs/profile/warnings: ${logHint}`,
|
|
126
|
+
`Verbose rerun: ${verboseRerun}`,
|
|
127
|
+
]
|
|
128
|
+
.filter((part) => part !== undefined && part.length > 0)
|
|
129
|
+
.join('\n\n');
|
|
130
|
+
}
|
|
72
131
|
/**
|
|
73
132
|
* Runs the build command.
|
|
74
133
|
* @param projectRoot - Root directory of the project
|
|
@@ -172,7 +231,8 @@ export async function buildCommand(projectRoot, options) {
|
|
|
172
231
|
const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
173
232
|
if (result.exitCode !== 0) {
|
|
174
233
|
error(`Build failed after ${timeStr}`);
|
|
175
|
-
|
|
234
|
+
const machCommand = options.ui ? 'mach build faster' : 'mach build';
|
|
235
|
+
throw new BuildError(buildFailureDiagnostics(result, paths.engine, buildCheck.objDir, machCommand), machCommand);
|
|
176
236
|
}
|
|
177
237
|
// Tool-managed branding edits that land on `browser/moz.configure`
|
|
178
238
|
// before the build cause mach's post-build guard to print one of two
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { pathExists, readText } from '../../utils/fs.js';
|
|
5
|
+
import { ok, warning } from '../doctor-check-core.js';
|
|
6
|
+
async function readEngineText(engineDir, relativePath) {
|
|
7
|
+
const fullPath = join(engineDir, relativePath);
|
|
8
|
+
if (!(await pathExists(fullPath)))
|
|
9
|
+
return null;
|
|
10
|
+
return readText(fullPath);
|
|
11
|
+
}
|
|
12
|
+
async function collectBrowserTomlFiles(root) {
|
|
13
|
+
const testRoot = join(root, 'browser/base/content/test');
|
|
14
|
+
if (!(await pathExists(testRoot)))
|
|
15
|
+
return [];
|
|
16
|
+
const result = [];
|
|
17
|
+
async function walk(absDir, relDir) {
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = await readdir(absDir, { withFileTypes: true });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
27
|
+
const absPath = join(absDir, entry.name);
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
await walk(absPath, relPath);
|
|
30
|
+
}
|
|
31
|
+
else if (entry.isFile() && entry.name === 'browser.toml') {
|
|
32
|
+
result.push(`browser/base/content/test/${relPath}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
await walk(testRoot, '');
|
|
37
|
+
return result.sort();
|
|
38
|
+
}
|
|
39
|
+
async function runPostRebaseAudit(ctx) {
|
|
40
|
+
const issues = [];
|
|
41
|
+
const engineDir = ctx.paths.engine;
|
|
42
|
+
const mozConfigure = await readEngineText(engineDir, 'browser/moz.configure');
|
|
43
|
+
if (mozConfigure === null) {
|
|
44
|
+
issues.push('browser/moz.configure is missing');
|
|
45
|
+
}
|
|
46
|
+
else if (!mozConfigure.includes('BROWSER_CHROME_URL')) {
|
|
47
|
+
issues.push('browser/moz.configure does not mention BROWSER_CHROME_URL');
|
|
48
|
+
}
|
|
49
|
+
const browserJar = await readEngineText(engineDir, 'browser/base/jar.mn');
|
|
50
|
+
if (browserJar === null) {
|
|
51
|
+
issues.push('browser/base/jar.mn is missing');
|
|
52
|
+
}
|
|
53
|
+
else if (!/\.xhtml\b/.test(browserJar)) {
|
|
54
|
+
issues.push('browser/base/jar.mn has no chrome document .xhtml entries');
|
|
55
|
+
}
|
|
56
|
+
const customElements = await readEngineText(engineDir, 'toolkit/content/customElements.js');
|
|
57
|
+
if (customElements === null) {
|
|
58
|
+
issues.push('toolkit/content/customElements.js is missing');
|
|
59
|
+
}
|
|
60
|
+
else if (!customElements.includes('customElements')) {
|
|
61
|
+
issues.push('toolkit/content/customElements.js does not contain customElements registrations');
|
|
62
|
+
}
|
|
63
|
+
const toolkitJar = await readEngineText(engineDir, 'toolkit/content/jar.mn');
|
|
64
|
+
if (toolkitJar === null) {
|
|
65
|
+
issues.push('toolkit/content/jar.mn is missing');
|
|
66
|
+
}
|
|
67
|
+
else if (!toolkitJar.includes('content/global/widgets/') &&
|
|
68
|
+
!toolkitJar.includes('content/global/elements/')) {
|
|
69
|
+
issues.push('toolkit/content/jar.mn has no widget/element exposure entries');
|
|
70
|
+
}
|
|
71
|
+
const browserTomls = await collectBrowserTomlFiles(engineDir);
|
|
72
|
+
if (browserTomls.length === 0) {
|
|
73
|
+
issues.push('no browser.toml files found under browser/base/content/test');
|
|
74
|
+
}
|
|
75
|
+
if (issues.length === 0) {
|
|
76
|
+
return ok('Post-rebase registration audit');
|
|
77
|
+
}
|
|
78
|
+
return warning('Post-rebase registration audit', `${issues.length} suspicious registration surface${issues.length === 1 ? '' : 's'}: ${issues.join('; ')}.`, 'Inspect the named engine paths, refresh any drifted registration patches, then re-run "fireforge doctor --post-rebase-audit".');
|
|
79
|
+
}
|
|
80
|
+
export const POST_REBASE_AUDIT_CHECK = {
|
|
81
|
+
name: 'Post-rebase registration audit',
|
|
82
|
+
skipIf: (ctx) => !ctx.options.postRebaseAudit || !ctx.engineExists,
|
|
83
|
+
dependsOn: ['fireforge.json is valid'],
|
|
84
|
+
run: runPostRebaseAudit,
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=post-rebase-audit.js.map
|
|
@@ -20,6 +20,7 @@ function summarizeWorkingTreeChangeCount(changeCount) {
|
|
|
20
20
|
function formatManagedDetail(counts) {
|
|
21
21
|
return [
|
|
22
22
|
counts.patchBacked > 0 ? `${counts.patchBacked} patch-backed` : null,
|
|
23
|
+
counts.patchOwnedDrift > 0 ? `${counts.patchOwnedDrift} patch-owned drift` : null,
|
|
23
24
|
counts.branding > 0 ? `${counts.branding} branding` : null,
|
|
24
25
|
counts.furnace > 0 ? `${counts.furnace} furnace` : null,
|
|
25
26
|
]
|
|
@@ -57,6 +58,7 @@ export async function inspectEngineWorkingTree(ctx) {
|
|
|
57
58
|
branding: 0,
|
|
58
59
|
furnace: 0,
|
|
59
60
|
patchBacked: 0,
|
|
61
|
+
patchOwnedDrift: 0,
|
|
60
62
|
conflict: 0,
|
|
61
63
|
unmanaged: 0,
|
|
62
64
|
};
|
|
@@ -67,6 +69,8 @@ export async function inspectEngineWorkingTree(ctx) {
|
|
|
67
69
|
counts.furnace++;
|
|
68
70
|
else if (entry.classification === 'patch-backed')
|
|
69
71
|
counts.patchBacked++;
|
|
72
|
+
else if (entry.classification === 'patch-owned-drift')
|
|
73
|
+
counts.patchOwnedDrift++;
|
|
70
74
|
else if (entry.classification === 'conflict')
|
|
71
75
|
counts.conflict++;
|
|
72
76
|
else
|
|
@@ -75,7 +79,7 @@ export async function inspectEngineWorkingTree(ctx) {
|
|
|
75
79
|
if (counts.conflict > 0) {
|
|
76
80
|
return warning('Engine working tree', `Engine working tree has ${counts.conflict} cross-patch ownership conflict${counts.conflict === 1 ? '' : 's'}. Multiple patches in patches.json claim the same file.`, 'Run "fireforge status --ownership" to see the conflicting patches, then run "fireforge verify" and resolve the overlap.');
|
|
77
81
|
}
|
|
78
|
-
const managedTotal = counts.branding + counts.furnace + counts.patchBacked;
|
|
82
|
+
const managedTotal = counts.branding + counts.furnace + counts.patchBacked + counts.patchOwnedDrift;
|
|
79
83
|
if (counts.unmanaged === 0) {
|
|
80
84
|
const managedDetail = formatManagedDetail(counts);
|
|
81
85
|
return {
|
|
@@ -10,6 +10,7 @@ import { toError } from '../utils/errors.js';
|
|
|
10
10
|
import { pathExists } from '../utils/fs.js';
|
|
11
11
|
import { error, info, intro, outro, success, warn } from '../utils/logger.js';
|
|
12
12
|
import { findExecutable } from '../utils/process.js';
|
|
13
|
+
import { POST_REBASE_AUDIT_CHECK } from './doctor/post-rebase-audit.js';
|
|
13
14
|
import { failure, ok, warning } from './doctor-check-core.js';
|
|
14
15
|
import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
|
|
15
16
|
import { inspectEngineWorkingTree } from './doctor-working-tree.js';
|
|
@@ -358,6 +359,7 @@ const DOCTOR_CHECKS = [
|
|
|
358
359
|
},
|
|
359
360
|
fix: 'Re-export affected files with "fireforge export <paths...>" to create full-file patches',
|
|
360
361
|
},
|
|
362
|
+
POST_REBASE_AUDIT_CHECK,
|
|
361
363
|
// Furnace checks live in a sibling module so this file stays under the
|
|
362
364
|
// max-lines threshold. Splicing them in as an array preserves the
|
|
363
365
|
// declarative registry contract — each entry remains a single
|
|
@@ -466,6 +468,7 @@ export function registerDoctor(program, { getProjectRoot, withErrorHandling }) {
|
|
|
466
468
|
.option('--repair-patches-manifest', 'Rebuild patches/patches.json from the current patch files before reporting results')
|
|
467
469
|
.option('--repair-furnace', 'Reconcile furnace state: clear stale furnace-state.json entries, re-run furnace apply to fix engine drift, and clear the pending-repair marker set by a failed preview teardown')
|
|
468
470
|
.option('--clear-resolution', 'Clear stale pendingResolution state after the patch queue health check reports no errors')
|
|
471
|
+
.option('--post-rebase-audit', 'Check common registration surfaces after a Firefox source rebase')
|
|
469
472
|
.action(withErrorHandling(async (options) => {
|
|
470
473
|
const result = await doctorCommand(getProjectRoot(), options);
|
|
471
474
|
if (result.exitCode !== 0) {
|
|
@@ -111,6 +111,38 @@ function closeRestoreSpinner(restoreSpinner, result) {
|
|
|
111
111
|
}
|
|
112
112
|
restoreSpinner.stop('Patch-touched files restored');
|
|
113
113
|
}
|
|
114
|
+
async function downloadAndExtractFirefox(args) {
|
|
115
|
+
const { version, product, engineDir, cacheDir, sha256 } = args;
|
|
116
|
+
let s = spinner(`Downloading Firefox ${version}...`);
|
|
117
|
+
let lastPercent = 0;
|
|
118
|
+
const phaseState = { value: 'download' };
|
|
119
|
+
try {
|
|
120
|
+
await downloadFirefoxSource(version, product, engineDir, cacheDir, (downloaded, total) => {
|
|
121
|
+
if (total <= 0)
|
|
122
|
+
return;
|
|
123
|
+
const percent = Math.floor((downloaded / total) * 100);
|
|
124
|
+
if (percent !== lastPercent && percent % 5 === 0) {
|
|
125
|
+
s.message(`Downloading Firefox ${version}... ${percent}% (${formatBytes(downloaded)} / ${formatBytes(total)})`);
|
|
126
|
+
lastPercent = percent;
|
|
127
|
+
}
|
|
128
|
+
}, (phase) => {
|
|
129
|
+
if (phase === 'extract' && phaseState.value === 'download') {
|
|
130
|
+
s.stop(`Firefox ${version} downloaded`);
|
|
131
|
+
phaseState.value = 'extract';
|
|
132
|
+
s = spinner(`Extracting Firefox ${version}... (decompressing ~600 MB of source; typically 30–90s)`);
|
|
133
|
+
}
|
|
134
|
+
}, sha256, (message) => {
|
|
135
|
+
s.message(message);
|
|
136
|
+
});
|
|
137
|
+
s.stop(phaseState.value === 'extract'
|
|
138
|
+
? `Firefox ${version} extracted`
|
|
139
|
+
: `Firefox ${version} downloaded`);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
s.error(phaseState.value === 'extract' ? 'Extraction failed' : 'Download failed');
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
114
146
|
/**
|
|
115
147
|
* Runs the download command.
|
|
116
148
|
* @param projectRoot - Root directory of the project
|
|
@@ -118,12 +150,9 @@ function closeRestoreSpinner(restoreSpinner, result) {
|
|
|
118
150
|
*/
|
|
119
151
|
export async function downloadCommand(projectRoot, options) {
|
|
120
152
|
intro('FireForge Download');
|
|
121
|
-
|
|
122
|
-
const config = await loadConfig(projectRoot);
|
|
153
|
+
const config = await loadConfig(projectRoot), version = config.firefox.version;
|
|
123
154
|
const paths = getProjectPaths(projectRoot);
|
|
124
|
-
const version = config.firefox.version;
|
|
125
155
|
info(`Firefox version: ${version}`);
|
|
126
|
-
// Disk space pre-flight: Firefox source is ~5 GB
|
|
127
156
|
await checkDiskSpace(projectRoot, 5 * 1024 * 1024 * 1024, warn);
|
|
128
157
|
await withFileLock(join(paths.fireforgeDir, 'download.fireforge.lock'), async () => {
|
|
129
158
|
// Check if engine already exists
|
|
@@ -224,43 +253,13 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
224
253
|
// Ensure cache directory exists
|
|
225
254
|
const cacheDir = join(paths.fireforgeDir, 'cache');
|
|
226
255
|
await ensureDir(cacheDir);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
let s = spinner(`Downloading Firefox ${version}...`);
|
|
235
|
-
let lastPercent = 0;
|
|
236
|
-
const phaseState = { value: 'download' };
|
|
237
|
-
try {
|
|
238
|
-
await downloadFirefoxSource(version, config.firefox.product, paths.engine, cacheDir, (downloaded, total) => {
|
|
239
|
-
if (total <= 0)
|
|
240
|
-
return;
|
|
241
|
-
const percent = Math.floor((downloaded / total) * 100);
|
|
242
|
-
if (percent !== lastPercent && percent % 5 === 0) {
|
|
243
|
-
s.message(`Downloading Firefox ${version}... ${percent}% (${formatBytes(downloaded)} / ${formatBytes(total)})`);
|
|
244
|
-
lastPercent = percent;
|
|
245
|
-
}
|
|
246
|
-
}, (phase) => {
|
|
247
|
-
if (phase === 'extract' && phaseState.value === 'download') {
|
|
248
|
-
s.stop(`Firefox ${version} downloaded`);
|
|
249
|
-
phaseState.value = 'extract';
|
|
250
|
-
s = spinner(`Extracting Firefox ${version}... (decompressing ~600 MB of source; typically 30–90s)`);
|
|
251
|
-
}
|
|
252
|
-
}, config.firefox.sha256);
|
|
253
|
-
if (phaseState.value === 'extract') {
|
|
254
|
-
s.stop(`Firefox ${version} extracted`);
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
s.stop(`Firefox ${version} downloaded`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
catch (error) {
|
|
261
|
-
s.error(phaseState.value === 'extract' ? 'Extraction failed' : 'Download failed');
|
|
262
|
-
throw error;
|
|
263
|
-
}
|
|
256
|
+
await downloadAndExtractFirefox({
|
|
257
|
+
version,
|
|
258
|
+
product: config.firefox.product,
|
|
259
|
+
engineDir: paths.engine,
|
|
260
|
+
cacheDir,
|
|
261
|
+
...(config.firefox.sha256 !== undefined ? { sha256: config.firefox.sha256 } : {}),
|
|
262
|
+
});
|
|
264
263
|
// Finding #17: the git indexing phase of `download` can block for
|
|
265
264
|
// minutes on a ~600 MB Firefox tree — the spinner updates less often
|
|
266
265
|
// than operators expect during the monolithic `git add -A` pass, and
|
|
@@ -270,7 +269,7 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
270
269
|
// still fire as usual; this is an additional up-front signal, not a
|
|
271
270
|
// replacement.
|
|
272
271
|
info('Indexing downloaded source into git (one-time; typically 3–5 minutes on a ~600 MB Firefox tree)...');
|
|
273
|
-
|
|
272
|
+
info('Git phase: initializing/resetting source repository metadata.');
|
|
274
273
|
const gitSpinner = spinner('Initializing git repository (this may take a few minutes)...');
|
|
275
274
|
let baseCommit;
|
|
276
275
|
try {
|
|
@@ -8,6 +8,7 @@ import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
|
8
8
|
import { commitExportedPatch, findAllPatchesForFiles } from '../core/patch-export.js';
|
|
9
9
|
import { buildPatchQueueContext, collectNewFileCreatorsByPath, detectNewFilesInDiff, } from '../core/patch-lint.js';
|
|
10
10
|
import { collectPatchRegistrationReferences } from '../core/patch-registration-refs.js';
|
|
11
|
+
import { buildPatchSourceMetadata } from '../core/patch-source-metadata.js';
|
|
11
12
|
import { GeneralError } from '../errors/base.js';
|
|
12
13
|
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
13
14
|
import { info, intro, outro, spinner } from '../utils/logger.js';
|
|
@@ -262,7 +263,7 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
262
263
|
name: patchName,
|
|
263
264
|
description,
|
|
264
265
|
filesAffected,
|
|
265
|
-
|
|
266
|
+
...buildPatchSourceMetadata(config.firefox),
|
|
266
267
|
explicitSupersede: options.supersede === true,
|
|
267
268
|
allowOverlap: options.allowOverlap === true,
|
|
268
269
|
config,
|
|
@@ -299,7 +300,7 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
299
300
|
description,
|
|
300
301
|
diff,
|
|
301
302
|
filesAffected,
|
|
302
|
-
|
|
303
|
+
...buildPatchSourceMetadata(config.firefox),
|
|
303
304
|
config,
|
|
304
305
|
policyCommand: 'export-all',
|
|
305
306
|
forceUnsafe: options.forceUnsafe === true,
|
|
@@ -93,6 +93,8 @@ export interface DryRunPreviewInput {
|
|
|
93
93
|
description: string;
|
|
94
94
|
filesAffected: string[];
|
|
95
95
|
sourceEsrVersion: string;
|
|
96
|
+
sourceProduct?: FireForgeConfig['firefox']['product'];
|
|
97
|
+
sourceVersion?: string;
|
|
96
98
|
explicitSupersede: boolean;
|
|
97
99
|
allowOverlap: boolean;
|
|
98
100
|
/** Optional `PatchMetadata.tier` opt-in carried from the CLI. */
|
|
@@ -354,6 +354,8 @@ export async function renderDryRunPreview(input) {
|
|
|
354
354
|
description: input.description,
|
|
355
355
|
filesAffected: input.filesAffected,
|
|
356
356
|
sourceEsrVersion: input.sourceEsrVersion,
|
|
357
|
+
...(input.sourceProduct !== undefined ? { sourceProduct: input.sourceProduct } : {}),
|
|
358
|
+
...(input.sourceVersion !== undefined ? { sourceVersion: input.sourceVersion } : {}),
|
|
357
359
|
...(input.tier !== undefined ? { tier: input.tier } : {}),
|
|
358
360
|
...(input.lintIgnore !== undefined ? { lintIgnore: input.lintIgnore } : {}),
|
|
359
361
|
...(input.config !== undefined ? { config: input.config } : {}),
|
|
@@ -13,6 +13,7 @@ import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
|
13
13
|
import { commitExportedPatch, findAllPatchesForFiles } from '../core/patch-export.js';
|
|
14
14
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
15
15
|
import { applyRenameMapToManifest, buildProjectedManifest, enforcePatchPolicy, } from '../core/patch-policy.js';
|
|
16
|
+
import { buildPatchSourceMetadata } from '../core/patch-source-metadata.js';
|
|
16
17
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
17
18
|
import { toError } from '../utils/errors.js';
|
|
18
19
|
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
@@ -211,7 +212,7 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
211
212
|
name: patchName,
|
|
212
213
|
description,
|
|
213
214
|
createdAt: new Date().toISOString(),
|
|
214
|
-
|
|
215
|
+
...buildPatchSourceMetadata(config.firefox),
|
|
215
216
|
filesAffected,
|
|
216
217
|
...(options.tier !== undefined ? { tier: options.tier } : {}),
|
|
217
218
|
...(options.lintIgnore !== undefined && options.lintIgnore.length > 0
|
|
@@ -263,7 +264,7 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
263
264
|
name: patchName,
|
|
264
265
|
description,
|
|
265
266
|
filesAffected,
|
|
266
|
-
|
|
267
|
+
...buildPatchSourceMetadata(config.firefox),
|
|
267
268
|
explicitSupersede: options.supersede === true,
|
|
268
269
|
allowOverlap: options.allowOverlap === true,
|
|
269
270
|
...(options.tier !== undefined ? { tier: options.tier } : {}),
|
|
@@ -288,7 +289,7 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
288
289
|
name: patchName,
|
|
289
290
|
description,
|
|
290
291
|
createdAt: new Date().toISOString(),
|
|
291
|
-
|
|
292
|
+
...buildPatchSourceMetadata(config.firefox),
|
|
292
293
|
filesAffected,
|
|
293
294
|
...(options.tier !== undefined ? { tier: options.tier } : {}),
|
|
294
295
|
...(options.lintIgnore !== undefined && options.lintIgnore.length > 0
|
|
@@ -366,7 +367,7 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
366
367
|
description,
|
|
367
368
|
diff,
|
|
368
369
|
filesAffected,
|
|
369
|
-
|
|
370
|
+
...buildPatchSourceMetadata(config.firefox),
|
|
370
371
|
...(options.tier !== undefined ? { tier: options.tier } : {}),
|
|
371
372
|
...(options.lintIgnore !== undefined && options.lintIgnore.length > 0
|
|
372
373
|
? { lintIgnore: options.lintIgnore }
|
|
@@ -6,6 +6,7 @@ import { getHead } from '../core/git.js';
|
|
|
6
6
|
import { getDirtyFiles } from '../core/git-status.js';
|
|
7
7
|
import { applyPatchesWithContinue, computePatchedContent, countPatches, discoverPatches, extractAffectedFiles, PatchError, } from '../core/patch-apply.js';
|
|
8
8
|
import { checkVersionCompatibility, loadPatchesManifest, validatePatchesManifestConsistency, validatePatchIntegrity, } from '../core/patch-manifest.js';
|
|
9
|
+
import { getPatchSourceVersion } from '../core/patch-source-metadata.js';
|
|
9
10
|
import { GeneralError } from '../errors/base.js';
|
|
10
11
|
import { toError } from '../utils/errors.js';
|
|
11
12
|
import { pathExists, readText } from '../utils/fs.js';
|
|
@@ -269,7 +270,7 @@ export async function importCommand(projectRoot, options = {}) {
|
|
|
269
270
|
// doesn't need to see version warnings for patches outside the range.
|
|
270
271
|
if (options.until !== undefined && !untilFilenameSet.has(patch.filename))
|
|
271
272
|
continue;
|
|
272
|
-
const warning = checkVersionCompatibility(patch
|
|
273
|
+
const warning = checkVersionCompatibility(getPatchSourceVersion(patch), currentVersion);
|
|
273
274
|
if (warning) {
|
|
274
275
|
warn(`${patch.filename}: ${warning}`);
|
|
275
276
|
}
|
|
@@ -18,6 +18,7 @@ import { registerReset } from './reset.js';
|
|
|
18
18
|
import { registerResolve } from './resolve.js';
|
|
19
19
|
import { registerRun } from './run.js';
|
|
20
20
|
import { registerSetup } from './setup.js';
|
|
21
|
+
import { registerSource } from './source.js';
|
|
21
22
|
import { registerStatus } from './status.js';
|
|
22
23
|
import { registerTest } from './test.js';
|
|
23
24
|
import { registerToken } from './token.js';
|
|
@@ -31,6 +32,7 @@ import { registerWire } from './wire.js';
|
|
|
31
32
|
*/
|
|
32
33
|
export const COMMAND_MANIFEST = [
|
|
33
34
|
{ name: 'setup', group: 'project', register: registerSetup },
|
|
35
|
+
{ name: 'source', group: 'project', register: registerSource },
|
|
34
36
|
{ name: 'download', group: 'engine', register: registerDownload },
|
|
35
37
|
{ name: 'bootstrap', group: 'engine', register: registerBootstrap },
|
|
36
38
|
{ name: 'import', group: 'workflow', register: registerImport },
|
|
@@ -10,6 +10,7 @@ import { updatePatchAndMetadata } from '../core/patch-export.js';
|
|
|
10
10
|
import { getClaimedFiles, loadPatchesManifest, resolvePatchIdentifier, stampPatchVersions, } from '../core/patch-manifest.js';
|
|
11
11
|
import { buildProjectedManifest, enforcePatchPolicy } from '../core/patch-policy.js';
|
|
12
12
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
13
|
+
import { elapsedSince } from '../utils/elapsed.js';
|
|
13
14
|
import { toError } from '../utils/errors.js';
|
|
14
15
|
import { pathExists } from '../utils/fs.js';
|
|
15
16
|
import { cancel, info, intro, isCancel, outro, spinner, success, warn } from '../utils/logger.js';
|
|
@@ -341,8 +342,9 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
341
342
|
let reExported = 0;
|
|
342
343
|
const reExportedFilenames = [];
|
|
343
344
|
const progress = spinner('Preparing re-export...');
|
|
344
|
-
|
|
345
|
-
|
|
345
|
+
const startedAt = Date.now();
|
|
346
|
+
for (const [index, patch] of selectedPatches.entries()) {
|
|
347
|
+
progress.message(`Re-exporting ${index + 1}/${selectedPatches.length}: ${patch.filename} (${patch.filesAffected.length} file(s), ${elapsedSince(startedAt)} elapsed)...`);
|
|
346
348
|
try {
|
|
347
349
|
const exported = await reExportSinglePatch(patch, paths, manifest, options, isDryRun, config);
|
|
348
350
|
if (exported) {
|
|
@@ -368,13 +370,13 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
368
370
|
// which we refuse to version-stamp through.
|
|
369
371
|
const shouldStamp = options.stamp === true && !isDryRun && reExported > 0 && reExported === selectedPatches.length;
|
|
370
372
|
if (shouldStamp) {
|
|
371
|
-
await stampPatchVersions(paths.patches, reExportedFilenames, config.firefox.version);
|
|
373
|
+
await stampPatchVersions(paths.patches, reExportedFilenames, config.firefox.version, config.firefox.product);
|
|
372
374
|
}
|
|
373
375
|
if (isDryRun) {
|
|
374
376
|
progress.stop('Dry run complete');
|
|
375
377
|
success(`[dry-run] Would re-export ${reExported} of ${selectedPatches.length} patch(es)`);
|
|
376
378
|
if (options.stamp === true) {
|
|
377
|
-
info(`[dry-run] Would stamp
|
|
379
|
+
info(`[dry-run] Would stamp sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${reExported} patch(es)`);
|
|
378
380
|
}
|
|
379
381
|
outro('Dry run complete');
|
|
380
382
|
}
|
|
@@ -382,7 +384,7 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
382
384
|
progress.stop('Re-export complete');
|
|
383
385
|
success(`Re-exported ${reExported} of ${selectedPatches.length} patch(es)`);
|
|
384
386
|
if (shouldStamp) {
|
|
385
|
-
success(`Stamped
|
|
387
|
+
success(`Stamped sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${reExportedFilenames.length} patch(es)`);
|
|
386
388
|
}
|
|
387
389
|
else if (options.stamp === true && reExported !== selectedPatches.length) {
|
|
388
390
|
warn('--stamp was requested but some patches failed or were skipped; refusing to stamp a partial set.');
|
|
@@ -395,8 +397,8 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
395
397
|
program
|
|
396
398
|
.command('re-export [patches...]')
|
|
397
399
|
.description('Refresh existing patch bodies (and filesAffected with --scan) from the current engine ' +
|
|
398
|
-
'state. Does NOT change
|
|
399
|
-
'
|
|
400
|
+
'state. Does NOT change sourceVersion/sourceProduct by default — use --stamp or run ' +
|
|
401
|
+
'rebase for source metadata stamping.')
|
|
400
402
|
.option('-a, --all', 'Re-export all patches')
|
|
401
403
|
.option('-s, --scan', 'Scan directories for new/removed files and update filesAffected')
|
|
402
404
|
.option('--scan-file <path>', 'With --scan, add this explicit engine-relative file to one target patch without collecting adjacent files. Repeatable.', (value, prev) => [...prev, value], [])
|
|
@@ -409,7 +411,7 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
409
411
|
.option('--allow-shrink', 'Allow --files to remove paths currently owned by the patch. Required before --yes can bypass the shrink confirmation.')
|
|
410
412
|
.option('-y, --yes', 'Skip confirmation prompts (required for non-TTY destructive writes)')
|
|
411
413
|
.option('--force-unsafe', 'Bypass cross-patch lint refusal when --files shrinks a patch')
|
|
412
|
-
.option('--stamp', "After every selected patch refreshes cleanly, stamp each re-exported patch's
|
|
414
|
+
.option('--stamp', "After every selected patch refreshes cleanly, stamp each re-exported patch's sourceVersion/sourceProduct in patches.json to firefox.version/firefox.product from fireforge.json. No effect on a partial run.")
|
|
413
415
|
.addOption(new Option('--tier <tier>', 'Force a tier override on the selected patch (only "branding" recognised). Mutually exclusive with --all.').choices(['branding']))
|
|
414
416
|
.option('--lint-ignore <check-id>', 'Append a lint check ID to the patch\'s PatchMetadata.lintIgnore (union, de-duped, repeatable). Mutually exclusive with --all. Use "fireforge patch lint-ignore" for --remove / --clear.', (value, prev) => [...prev, value], [])
|
|
415
417
|
.action(withErrorHandling(async (patches, options) => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface RebaseConflictSummary {
|
|
2
|
+
patchFilename: string;
|
|
3
|
+
failedFiles: string[];
|
|
4
|
+
category: string;
|
|
5
|
+
nextCommands: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Builds a concise operator-facing summary for a failed rebase patch. */
|
|
8
|
+
export declare function buildRebaseConflictSummary(args: {
|
|
9
|
+
patchFilename: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
rejectFiles?: string[];
|
|
12
|
+
}): RebaseConflictSummary;
|