@hominis/fireforge 0.27.1 → 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 +6 -0
- package/README.md +5 -5
- package/dist/src/cli.js +5 -1
- package/dist/src/commands/build.js +61 -1
- package/dist/src/commands/doctor-working-tree.js +5 -1
- package/dist/src/commands/download.js +41 -45
- 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/re-export.js +6 -6
- 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 +5 -5
- package/dist/src/commands/rebase/summary.js +7 -2
- package/dist/src/commands/resolve.js +2 -1
- 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/firefox-cache.d.ts +1 -1
- package/dist/src/core/firefox-cache.js +10 -3
- package/dist/src/core/firefox.js +1 -1
- package/dist/src/core/git.js +7 -2
- package/dist/src/core/ownership-table.d.ts +3 -1
- package/dist/src/core/ownership-table.js +31 -7
- package/dist/src/core/patch-export.d.ts +4 -0
- package/dist/src/core/patch-export.js +4 -0
- 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/patches.d.ts +9 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
## 0.27.0
|
|
4
4
|
|
|
5
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.
|
|
6
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.
|
|
7
13
|
- Added elapsed progress for extraction, initial source commits, and rebase/re-export patch refreshes.
|
|
8
14
|
- Added `re-export --files --allow-shrink` so patch ownership shrinkage is refused unless explicitly acknowledged, with clearer dry-run previews.
|
|
9
15
|
- Surfaced likely new sibling files during plain re-export and aligned verify/status ownership reporting for unowned worktree changes.
|
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,9 +63,9 @@ 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
71
|
npx fireforge source set --version 145.0.0esr --product firefox-esr --sha256 <archive-sha256>
|
|
@@ -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
|
|
@@ -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 {
|
|
@@ -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,46 +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, (message) => {
|
|
253
|
-
if (phaseState.value === 'extract')
|
|
254
|
-
s.message(message);
|
|
255
|
-
});
|
|
256
|
-
if (phaseState.value === 'extract') {
|
|
257
|
-
s.stop(`Firefox ${version} extracted`);
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
s.stop(`Firefox ${version} downloaded`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
s.error(phaseState.value === 'extract' ? 'Extraction failed' : 'Download failed');
|
|
265
|
-
throw error;
|
|
266
|
-
}
|
|
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
|
+
});
|
|
267
263
|
// Finding #17: the git indexing phase of `download` can block for
|
|
268
264
|
// minutes on a ~600 MB Firefox tree — the spinner updates less often
|
|
269
265
|
// than operators expect during the monolithic `git add -A` pass, and
|
|
@@ -273,7 +269,7 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
273
269
|
// still fire as usual; this is an additional up-front signal, not a
|
|
274
270
|
// replacement.
|
|
275
271
|
info('Indexing downloaded source into git (one-time; typically 3–5 minutes on a ~600 MB Firefox tree)...');
|
|
276
|
-
|
|
272
|
+
info('Git phase: initializing/resetting source repository metadata.');
|
|
277
273
|
const gitSpinner = spinner('Initializing git repository (this may take a few minutes)...');
|
|
278
274
|
let baseCommit;
|
|
279
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
|
}
|
|
@@ -370,13 +370,13 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
370
370
|
// which we refuse to version-stamp through.
|
|
371
371
|
const shouldStamp = options.stamp === true && !isDryRun && reExported > 0 && reExported === selectedPatches.length;
|
|
372
372
|
if (shouldStamp) {
|
|
373
|
-
await stampPatchVersions(paths.patches, reExportedFilenames, config.firefox.version);
|
|
373
|
+
await stampPatchVersions(paths.patches, reExportedFilenames, config.firefox.version, config.firefox.product);
|
|
374
374
|
}
|
|
375
375
|
if (isDryRun) {
|
|
376
376
|
progress.stop('Dry run complete');
|
|
377
377
|
success(`[dry-run] Would re-export ${reExported} of ${selectedPatches.length} patch(es)`);
|
|
378
378
|
if (options.stamp === true) {
|
|
379
|
-
info(`[dry-run] Would stamp
|
|
379
|
+
info(`[dry-run] Would stamp sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${reExported} patch(es)`);
|
|
380
380
|
}
|
|
381
381
|
outro('Dry run complete');
|
|
382
382
|
}
|
|
@@ -384,7 +384,7 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
384
384
|
progress.stop('Re-export complete');
|
|
385
385
|
success(`Re-exported ${reExported} of ${selectedPatches.length} patch(es)`);
|
|
386
386
|
if (shouldStamp) {
|
|
387
|
-
success(`Stamped
|
|
387
|
+
success(`Stamped sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${reExportedFilenames.length} patch(es)`);
|
|
388
388
|
}
|
|
389
389
|
else if (options.stamp === true && reExported !== selectedPatches.length) {
|
|
390
390
|
warn('--stamp was requested but some patches failed or were skipped; refusing to stamp a partial set.');
|
|
@@ -397,8 +397,8 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
397
397
|
program
|
|
398
398
|
.command('re-export [patches...]')
|
|
399
399
|
.description('Refresh existing patch bodies (and filesAffected with --scan) from the current engine ' +
|
|
400
|
-
'state. Does NOT change
|
|
401
|
-
'
|
|
400
|
+
'state. Does NOT change sourceVersion/sourceProduct by default — use --stamp or run ' +
|
|
401
|
+
'rebase for source metadata stamping.')
|
|
402
402
|
.option('-a, --all', 'Re-export all patches')
|
|
403
403
|
.option('-s, --scan', 'Scan directories for new/removed files and update filesAffected')
|
|
404
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], [])
|
|
@@ -411,7 +411,7 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
411
411
|
.option('--allow-shrink', 'Allow --files to remove paths currently owned by the patch. Required before --yes can bypass the shrink confirmation.')
|
|
412
412
|
.option('-y, --yes', 'Skip confirmation prompts (required for non-TTY destructive writes)')
|
|
413
413
|
.option('--force-unsafe', 'Bypass cross-patch lint refusal when --files shrinks a patch')
|
|
414
|
-
.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.")
|
|
415
415
|
.addOption(new Option('--tier <tier>', 'Force a tier override on the selected patch (only "branding" recognised). Mutually exclusive with --all.').choices(['branding']))
|
|
416
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], [])
|
|
417
417
|
.action(withErrorHandling(async (patches, options) => {
|
|
@@ -73,6 +73,8 @@ export async function handleContinue(projectRoot, maxFuzz) {
|
|
|
73
73
|
// v0.14.0 resolve.ts fix.
|
|
74
74
|
await updatePatchAndMetadata(paths.patches, currentPatch.filename, diffContent, {
|
|
75
75
|
sourceEsrVersion: session.toVersion,
|
|
76
|
+
sourceVersion: session.toVersion,
|
|
77
|
+
...(session.toProduct !== undefined ? { sourceProduct: session.toProduct } : {}),
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
finally {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `fireforge rebase` — semi-automated
|
|
2
|
+
* `fireforge rebase` — semi-automated Firefox source version upgrade.
|
|
3
3
|
*
|
|
4
4
|
* Orchestrates the full patch-rebase workflow:
|
|
5
5
|
* 1. Reset engine to baseline
|
|
@@ -13,7 +13,7 @@ import { Command } from 'commander';
|
|
|
13
13
|
import type { CommandContext } from '../../types/cli.js';
|
|
14
14
|
import type { RebaseOptions } from '../../types/commands/index.js';
|
|
15
15
|
/**
|
|
16
|
-
* Runs the rebase command to orchestrate
|
|
16
|
+
* Runs the rebase command to orchestrate a Firefox source version upgrade.
|
|
17
17
|
* @param projectRoot - Root directory of the project
|
|
18
18
|
* @param options - Rebase options
|
|
19
19
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* `fireforge rebase` — semi-automated
|
|
3
|
+
* `fireforge rebase` — semi-automated Firefox source version upgrade.
|
|
4
4
|
*
|
|
5
5
|
* Orchestrates the full patch-rebase workflow:
|
|
6
6
|
* 1. Reset engine to baseline
|
|
@@ -15,6 +15,7 @@ import { getFurnacePaths, updateFurnaceState } from '../../core/furnace-config.j
|
|
|
15
15
|
import { getHead, isGitRepository, isMissingHeadError, resetChanges } from '../../core/git.js';
|
|
16
16
|
import { discoverPatches } from '../../core/patch-files.js';
|
|
17
17
|
import { loadPatchesManifest } from '../../core/patch-manifest.js';
|
|
18
|
+
import { getPatchSourceProduct, getPatchSourceVersion } from '../../core/patch-source-metadata.js';
|
|
18
19
|
import { hasActiveRebaseSession, saveRebaseSession } from '../../core/rebase-session.js';
|
|
19
20
|
import { GeneralError } from '../../errors/base.js';
|
|
20
21
|
import { RebaseSessionExistsError } from '../../errors/rebase.js';
|
|
@@ -65,9 +66,11 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
65
66
|
throw new GeneralError('No patches found in manifest. Nothing to rebase.');
|
|
66
67
|
}
|
|
67
68
|
// Determine the "from" version from the patches
|
|
68
|
-
const patchVersions = new Set(manifest.patches.map((p) => p
|
|
69
|
+
const patchVersions = new Set(manifest.patches.map((p) => getPatchSourceVersion(p)));
|
|
70
|
+
const patchProducts = new Set(manifest.patches.map((p) => getPatchSourceProduct(p)).filter(Boolean));
|
|
69
71
|
const sortedVersions = [...patchVersions].sort();
|
|
70
72
|
const fromVersion = sortedVersions[0] ?? currentVersion;
|
|
73
|
+
const fromProduct = [...patchProducts].sort()[0] ?? config.firefox.product;
|
|
71
74
|
if (patchVersions.size === 1 && fromVersion === currentVersion) {
|
|
72
75
|
info('All patches already match the current Firefox version. Nothing to rebase.');
|
|
73
76
|
outro('Rebase not needed');
|
|
@@ -110,6 +113,8 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
110
113
|
const allPatches = await discoverPatches(paths.patches);
|
|
111
114
|
const session = {
|
|
112
115
|
startedAt: new Date().toISOString(),
|
|
116
|
+
fromProduct,
|
|
117
|
+
toProduct: config.firefox.product,
|
|
113
118
|
fromVersion,
|
|
114
119
|
toVersion: currentVersion,
|
|
115
120
|
preRebaseCommit,
|
|
@@ -125,7 +130,7 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
125
130
|
}
|
|
126
131
|
// ── Public API ──
|
|
127
132
|
/**
|
|
128
|
-
* Runs the rebase command to orchestrate
|
|
133
|
+
* Runs the rebase command to orchestrate a Firefox source version upgrade.
|
|
129
134
|
* @param projectRoot - Root directory of the project
|
|
130
135
|
* @param options - Rebase options
|
|
131
136
|
*/
|
|
@@ -142,7 +147,7 @@ export async function rebaseCommand(projectRoot, options = {}) {
|
|
|
142
147
|
export function registerRebase(program, { getProjectRoot, withErrorHandling }) {
|
|
143
148
|
program
|
|
144
149
|
.command('rebase')
|
|
145
|
-
.description('Semi-automated
|
|
150
|
+
.description('Semi-automated Firefox source version upgrade — apply patches with fuzz and re-export')
|
|
146
151
|
.option('--continue', 'Resume after manually resolving a failed patch')
|
|
147
152
|
.option('--abort', 'Cancel the rebase and restore engine to pre-rebase state')
|
|
148
153
|
.option('--dry-run', 'Show what would happen without modifying anything')
|
|
@@ -143,16 +143,16 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
|
|
|
143
143
|
.filter((p) => p.status === 'applied-clean' || p.status === 'applied-fuzz' || p.status === 'resolved')
|
|
144
144
|
.map((p) => p.filename);
|
|
145
145
|
if (appliedFilenames.length > 0) {
|
|
146
|
-
await stampPatchVersions(paths.patches, appliedFilenames, session.toVersion);
|
|
146
|
+
await stampPatchVersions(paths.patches, appliedFilenames, session.toVersion, session.toProduct);
|
|
147
147
|
}
|
|
148
148
|
// Stamp every Furnace override's `baseVersion` to match the rebased
|
|
149
|
-
// Firefox version. Before this stamp, a successful
|
|
149
|
+
// Firefox source version. Before this stamp, a successful source bump left
|
|
150
150
|
// overrides in a doctor-failing drift state (each override still
|
|
151
|
-
// claimed the pre-rebase
|
|
151
|
+
// claimed the pre-rebase source as its baseline) and every subsequent
|
|
152
152
|
// `fireforge doctor` failed `Furnace component validation`. The
|
|
153
153
|
// stamp is unconditional per the helper's contract: rebase already
|
|
154
154
|
// succeeded on the patch side, so the operator is committing to the
|
|
155
|
-
// new
|
|
155
|
+
// new source baseline; per-component health checking stays with
|
|
156
156
|
// `fireforge furnace validate` / `doctor --repair-furnace`.
|
|
157
157
|
try {
|
|
158
158
|
const overridesStamped = await stampFurnaceOverrideBaseVersions(projectRoot, session.toVersion);
|
|
@@ -176,7 +176,7 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
|
|
|
176
176
|
return next;
|
|
177
177
|
});
|
|
178
178
|
info('');
|
|
179
|
-
success(`All patches re-exported with
|
|
179
|
+
success(`All patches re-exported with sourceVersion=${session.toVersion}`);
|
|
180
180
|
outro('Rebase complete!');
|
|
181
181
|
}
|
|
182
182
|
async function reExportAppliedPatches(session, paths) {
|
|
@@ -27,7 +27,11 @@ export function statusLabel(status, fuzzFactor) {
|
|
|
27
27
|
*/
|
|
28
28
|
export function printSummary(session) {
|
|
29
29
|
info('');
|
|
30
|
-
|
|
30
|
+
const from = session.fromProduct
|
|
31
|
+
? `${session.fromProduct} ${session.fromVersion}`
|
|
32
|
+
: session.fromVersion;
|
|
33
|
+
const to = session.toProduct ? `${session.toProduct} ${session.toVersion}` : session.toVersion;
|
|
34
|
+
info(`Source Rebase Summary: ${from} → ${to}`);
|
|
31
35
|
info('='.repeat(55));
|
|
32
36
|
for (const patch of session.patches) {
|
|
33
37
|
const label = statusLabel(patch.status, patch.fuzzFactor);
|
|
@@ -37,7 +41,8 @@ export function printSummary(session) {
|
|
|
37
41
|
const fuzz = session.patches.filter((p) => p.status === 'applied-fuzz').length;
|
|
38
42
|
const resolved = session.patches.filter((p) => p.status === 'resolved').length;
|
|
39
43
|
const failed = session.patches.filter((p) => p.status === 'failed').length;
|
|
44
|
+
const total = session.patches.length;
|
|
40
45
|
info('');
|
|
41
|
-
info(`Results: ${clean} clean, ${fuzz} fuzz-applied, ${resolved} manually resolved, ${failed} failed`);
|
|
46
|
+
info(`Results: ${total} total: ${clean} clean, ${fuzz} fuzz-applied, ${resolved} manually resolved, ${failed} failed`);
|
|
42
47
|
}
|
|
43
48
|
//# sourceMappingURL=summary.js.map
|
|
@@ -8,6 +8,7 @@ import { stageFiles, unstageFiles } from '../core/git-file-ops.js';
|
|
|
8
8
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
9
9
|
import { updatePatchAndMetadata } from '../core/patch-export.js';
|
|
10
10
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
11
|
+
import { buildPatchSourceMetadata } from '../core/patch-source-metadata.js';
|
|
11
12
|
import { GeneralError, ResolutionError } from '../errors/base.js';
|
|
12
13
|
import { toError } from '../utils/errors.js';
|
|
13
14
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -132,7 +133,7 @@ export async function resolveCommand(projectRoot, options = {}) {
|
|
|
132
133
|
const config = await loadConfig(projectRoot);
|
|
133
134
|
await updatePatchAndMetadata(paths.patches, patchFilename, diffContent, {
|
|
134
135
|
filesAffected: diffFilesAffected,
|
|
135
|
-
|
|
136
|
+
...buildPatchSourceMetadata(config.firefox),
|
|
136
137
|
});
|
|
137
138
|
// Cleanup: Clear pendingResolution from state.json transactionally so
|
|
138
139
|
// we don't clobber concurrent updates to unrelated keys (e.g. another
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ClassifiedFile } from '../core/status-classify.js';
|
|
2
|
+
export interface ClassifiedBuckets {
|
|
3
|
+
conflict: ClassifiedFile[];
|
|
4
|
+
unmanaged: ClassifiedFile[];
|
|
5
|
+
patchBacked: ClassifiedFile[];
|
|
6
|
+
patchOwnedDrift: ClassifiedFile[];
|
|
7
|
+
branding: ClassifiedFile[];
|
|
8
|
+
furnace: ClassifiedFile[];
|
|
9
|
+
}
|
|
10
|
+
/** Renders the unmanaged-only status view and registration hints. */
|
|
11
|
+
export declare function renderUnmanagedOnly(unmanagedFiles: ClassifiedFile[], totalModified: number, projectRoot: string, binaryName: string): Promise<void>;
|
|
12
|
+
/** Renders the default classified status buckets. */
|
|
13
|
+
export declare function renderDefaultStatus(totalModified: number, buckets: ClassifiedBuckets, projectRoot: string, binaryName: string): Promise<void>;
|