@hominis/fireforge 0.47.1 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/dist/bin/fireforge.js +5 -0
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +16 -28
- package/dist/src/commands/build.js +30 -4
- package/dist/src/commands/doctor-engine-eslint.d.ts +3 -0
- package/dist/src/commands/doctor-engine-eslint.js +12 -0
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +158 -0
- package/dist/src/commands/doctor-source-pin.js +9 -2
- package/dist/src/commands/doctor.js +4 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/furnace/apply.js +10 -4
- package/dist/src/commands/furnace/create-templates.js +0 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +14 -0
- package/dist/src/commands/manifest.d.ts +1 -2
- package/dist/src/commands/patch/delete.js +18 -4
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export-options.js +7 -0
- package/dist/src/commands/re-export-register.js +1 -0
- package/dist/src/commands/re-export-removed.d.ts +4 -0
- package/dist/src/commands/re-export-removed.js +49 -0
- package/dist/src/commands/re-export-single.js +4 -1
- package/dist/src/commands/re-export.js +8 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/test-browser-preflight.d.ts +28 -0
- package/dist/src/commands/test-browser-preflight.js +181 -0
- package/dist/src/commands/test-build-phase.js +30 -7
- package/dist/src/commands/test-checkjs.d.ts +3 -0
- package/dist/src/commands/test-checkjs.js +27 -0
- package/dist/src/commands/test-diagnose.js +3 -0
- package/dist/src/commands/test-harness-env.d.ts +5 -6
- package/dist/src/commands/test-harness-env.js +27 -7
- package/dist/src/commands/test-modes.d.ts +2 -4
- package/dist/src/commands/test-register.js +11 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +21 -1
- package/dist/src/commands/test-verdict.d.ts +2 -2
- package/dist/src/commands/test.js +83 -211
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-baseline-types.d.ts +12 -14
- package/dist/src/core/build-baseline-validate.d.ts +3 -0
- package/dist/src/core/build-baseline-validate.js +53 -0
- package/dist/src/core/build-baseline.d.ts +4 -3
- package/dist/src/core/build-baseline.js +23 -11
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/component-source-guard.d.ts +20 -0
- package/dist/src/core/component-source-guard.js +68 -0
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/coverage-extend.d.ts +5 -34
- package/dist/src/core/coverage-extend.js +26 -37
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/engine-write-boundary.d.ts +5 -0
- package/dist/src/core/engine-write-boundary.js +41 -0
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/furnace-apply-helpers.js +8 -2
- package/dist/src/core/furnace-apply.js +7 -0
- package/dist/src/core/furnace-validate-accessibility.js +9 -12
- package/dist/src/core/furnace-validate.js +0 -43
- package/dist/src/core/gecko-dom-types.d.ts +2 -0
- package/dist/src/core/gecko-dom-types.js +17 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/lit-template-regions.d.ts +4 -0
- package/dist/src/core/lit-template-regions.js +50 -0
- package/dist/src/core/mach-build-artifacts.d.ts +6 -6
- package/dist/src/core/mach-build-artifacts.js +13 -7
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/mach-resource-shim.d.ts +1 -1
- package/dist/src/core/mach-resource-shim.js +2 -0
- package/dist/src/core/marionette-port.d.ts +2 -0
- package/dist/src/core/marionette-port.js +38 -53
- package/dist/src/core/mochitest-server-port.d.ts +1 -1
- package/dist/src/core/mochitest-server-port.js +20 -17
- package/dist/src/core/objdir-maintenance.d.ts +10 -0
- package/dist/src/core/objdir-maintenance.js +151 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +10 -2
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/preflight-wait.d.ts +2 -0
- package/dist/src/core/preflight-wait.js +24 -0
- package/dist/src/core/process-owner.d.ts +9 -0
- package/dist/src/core/process-owner.js +43 -0
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/test-harness-crash.d.ts +1 -1
- package/dist/src/core/test-harness-crash.js +5 -5
- package/dist/src/core/test-host-state.d.ts +14 -0
- package/dist/src/core/test-host-state.js +80 -0
- package/dist/src/core/test-profile-files.d.ts +17 -0
- package/dist/src/core/test-profile-files.js +75 -0
- package/dist/src/core/test-stale-check.d.ts +6 -7
- package/dist/src/core/test-stale-check.js +9 -32
- package/dist/src/core/token-category.d.ts +7 -26
- package/dist/src/core/token-category.js +68 -121
- package/dist/src/core/token-inventory.js +12 -7
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +86 -12
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +29 -3
- package/dist/src/types/config.d.ts +10 -0
- package/dist/src/utils/assert.js +2 -7
- package/dist/src/utils/child-env.d.ts +0 -3
- package/dist/src/utils/child-env.js +0 -3
- package/dist/src/utils/concurrency.d.ts +2 -2
- package/dist/src/utils/concurrency.js +15 -4
- package/dist/src/utils/fs.js +11 -1
- package/dist/src/utils/options.d.ts +16 -1
- package/dist/src/utils/options.js +5 -3
- package/dist/src/utils/paths.js +3 -2
- package/dist/src/utils/process-group.d.ts +1 -2
- package/dist/src/utils/process-group.js +1 -2
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +46 -0
- package/dist/src/utils/process.d.ts +5 -2
- package/dist/src/utils/process.js +21 -8
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +82 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +13 -8
- package/docs/machine-output.md +36 -19
- package/docs/patch-workflow.md +50 -1
- package/docs/testing.md +77 -12
- package/package.json +10 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.49.0
|
|
4
|
+
|
|
5
|
+
### Furnace and builds
|
|
6
|
+
|
|
7
|
+
- Safer Furnace deployment and objdir cloning protect component sources and peer checkouts from shared files and directory aliases. Failed copies finish before rollback; cloned build metadata is relocated and verified.
|
|
8
|
+
- Build and test preflight detect broken partial configuration, stale artifacts and invalid build baselines. Scoped builds retain coverage only while their inputs remain unchanged, and builds remove dangling test links.
|
|
9
|
+
|
|
10
|
+
### Test harness
|
|
11
|
+
|
|
12
|
+
- Added `test --wait-browser [seconds]`, `--wait-port [seconds]` and repeatable `--profile-file source=destination`. Browser ownership checks protect running builds; orphan cleanup waits for process and port release.
|
|
13
|
+
- Test verdicts include host load and perf power-source information. Power changes make perf results inconclusive. Seeded shuffle reproduces shard order; native file shuffle remains unseeded.
|
|
14
|
+
|
|
15
|
+
### Typecheck and lint
|
|
16
|
+
|
|
17
|
+
- Project typecheck uses fresh diagnostics with consistent cached and uncached results. Gecko constructor shims support instance narrowing and pen input; selected test scripts receive configured checkJs checks before pre-test builds.
|
|
18
|
+
- Improved Lit accessibility checks and token category parsing. Doctor checks engine source pins and reports missing engine-local ESLint setup early.
|
|
19
|
+
|
|
20
|
+
### Patch queue
|
|
21
|
+
|
|
22
|
+
- Added `re-export --expect-removed <path>` for safely retiring deleted patch-created files. Patch deletion rechecks the approved queue under lock.
|
|
23
|
+
|
|
24
|
+
### Command reliability
|
|
25
|
+
|
|
26
|
+
- Fixed leaf-command `--wait-lock` handling, JSON error reporting and watch retries. Process tracking and independent cleanup preserve the original run outcome through cancellation and cleanup failures.
|
|
27
|
+
|
|
28
|
+
### Tooling
|
|
29
|
+
|
|
30
|
+
- Full-Firefox integration preserves existing workspace files and Git index state, and fails on incomplete recovery. Build identity includes untracked contents and binary changes. Release checks enforce the pinned toolchain and additional module coverage floors.
|
|
31
|
+
|
|
32
|
+
## 0.48.0
|
|
33
|
+
|
|
34
|
+
### Engine lifecycle
|
|
35
|
+
|
|
36
|
+
- `download --force` asks before it discards work. It replaced `engine/` and deleted the backup without checking for applied patches, unexported edits or local commits. When the engine has changed paths or a HEAD off the recorded base commit, it now names the loss (changed paths, how many match the patch queue) and prompts. A non-interactive run refuses with exit 8 and prints the `--yes` form. A clean engine is replaced silently, as before.
|
|
37
|
+
|
|
38
|
+
### Builds
|
|
39
|
+
|
|
40
|
+
- `fireforge build` regenerates `engine/mozconfig` before its auto-configure, and a mozconfig that differs from the last successful build's is itself a configure trigger. Configure used to read the previous mozconfig, so a fix in `configs/*.mozconfig` (such as pinning `--with-macos-sdk` after the Xcode 27.0 update) could never take effect. The notice names the trigger. A baseline without `mozconfigHash` falls back to whether this run rewrote the file.
|
|
41
|
+
|
|
42
|
+
### Smoke runs
|
|
43
|
+
|
|
44
|
+
- **Default change:** `run --smoke-exit` launches on a fresh temporary profile unless `--profile <path>` names one. It ran against the developer's live profile, so leftover profile state could fail the release gate with nothing wrong in the tree.
|
|
45
|
+
- New `run --profile <path>` and `run --temp-profile`, with or without `--smoke-exit`. FireForge creates the temporary profile, seeds the prefs `mach run` would, and removes it when the run ends, including on failure and SIGINT/SIGTERM. The next `run` sweeps one left by a SIGKILLed run.
|
|
46
|
+
- For the record: mach's own flags are not forwarded. Desktop `mach run` has no `--profile`, and its `--temp-profile` leaks a directory in the objdir on every run.
|
|
47
|
+
|
|
48
|
+
### Rebase
|
|
49
|
+
|
|
50
|
+
- `rebase --dry-run` is real. It printed a patch count and exited 0 without testing a hunk. It now replays the queue onto the engine's HEAD in a throwaway index, walking the real run's context ladder, prints a verdict per patch (clean, reduced context, or reject with the files) and a tally, and exits 6 on any reject. `engine/` is never written.
|
|
51
|
+
- The "from" version is the semantically oldest patch stamp, not the lexically smallest, so `153.10.0esr` no longer counts as older than `153.2.0esr`. A queue with several stamps prints the spread.
|
|
52
|
+
- The header warns when `state.json`'s `downloadedVersion` is not the pinned target, meaning the download step has not run yet.
|
|
53
|
+
|
|
54
|
+
### Performance
|
|
55
|
+
|
|
56
|
+
- The worktree ownership scan behind `status`, `verify`, `doctor`, `import` and `discard` runs four git processes instead of one or more per managed file. On the Hominis tree (1,606 managed files) that was 1,625 spawns. `status --ownership` went from 13 to 16 s down to 5.7 s, and `verify` from 15 s to 7.6 s. Output is byte-identical to 0.47.1.
|
|
57
|
+
- The batched untracked listing uses `ls-files -z`, so a non-ASCII name inside a collapsed directory is no longer expanded to a quoted path that names no file.
|
|
58
|
+
|
|
59
|
+
### Typecheck and checkJs
|
|
60
|
+
|
|
61
|
+
- The per-test-file checkJs programs share one parse of each lib, shim and helper file per run. On the Hominis queue (467 programs) the pass went from 66 to 85 s down to 22 to 25 s, with identical findings. This speeds up `typecheck` and every `lint --per-patch`, `export` or `re-export` that misses the lint cache.
|
|
62
|
+
- `fireforge typecheck` keeps incremental build info per project in `.fireforge/typecheck/`, keyed so a shim, compiler or FireForge change never replays a stale verdict. New `typecheck --no-cache` checks from scratch and writes nothing. Nothing is written under `engine/`.
|
|
63
|
+
- On the Hominis tree `typecheck` went from 93.8 s to 30.2 s.
|
|
64
|
+
- For the record: the projects are not run in a worker pool. They took 6 of the 93.8 s; the per-file checkJs pass was the cost.
|
|
65
|
+
|
|
66
|
+
### Re-export
|
|
67
|
+
|
|
68
|
+
- A one-patch `re-export` roots its checkJs program at the patches it re-exports, as `lint --per-patch --patches` already did, instead of type-checking the whole queue. Cross-patch imports still resolve. On Hominis a cold `re-export <patch> --dry-run` went from 76.7 s to 8.0 s.
|
|
69
|
+
- The queue lint context parses each patch once instead of once per created file: 3 to 4.4 s down to 0.3 s on 401 patches. Shared by `lint --per-patch`, `re-export`, `export` and `typecheck`.
|
|
70
|
+
- `re-export` names its lint scope before the wait and prints a line when it builds a checkJs program. The elapsed time now includes the context build.
|
|
71
|
+
|
|
72
|
+
### Patch queue and lint
|
|
73
|
+
|
|
74
|
+
- New opt-in `patchLint.forwardRegistration: "extended"` widens `forward-registration` beyond test-manifest `support-files` to jar.mn lines, moz.build path tokens, test-manifest sections and `head`, and `customElements.js` chrome URLs. On the Hominis queue it finds 937 edges across 6 patches. The default (`"support-files"`) is unchanged. Each finding prints a `patch staged-dependency --add --kind registration` command, tested to quiet the queue without leaving `staged-dependency-unused` behind.
|
|
75
|
+
- New public `findForwardRegistrations(patchesDir, { scope })` returns the same census as structured records.
|
|
76
|
+
- New `lint --per-patch --notices summary|full`. `summary` prints one line per check instead of every NOTICE row (about 430 on a green Hominis queue). `full` stays the default and is unchanged. Errors, warnings and `--report` JSON are unaffected.
|
|
77
|
+
|
|
78
|
+
### Programmatic API
|
|
79
|
+
|
|
80
|
+
- New public `filesMeasuredByFileTooLarge(patchText)`: the files the `file-too-large` rule measures, which the rule itself now uses. A size-waiver audit can use the rule's own predicate. It covers the JS files a patch creates, not every created file, so `detectNewFilesInDiff` was not exported for this.
|
|
81
|
+
|
|
82
|
+
### Doctor
|
|
83
|
+
|
|
84
|
+
- New macOS `doctor` check: can the bootstrapped clang link against the SDK the build will use? Xcode 27.0 ships a `libSystem.tbd` the Mozilla clang/lld 21.1.8 rejects, and `mach configure` died with exit 5 without naming the cause. The check resolves the SDK from `engine/mozconfig` the way mach does, links a one-line C program, and on failure warns with clang's first error and the installed SDKs. It does nothing off macOS or without a bootstrapped clang.
|
|
85
|
+
|
|
3
86
|
## 0.47.0
|
|
4
87
|
|
|
5
88
|
### Test preflight
|
package/dist/bin/fireforge.js
CHANGED
|
@@ -13,6 +13,7 @@ import { emitKilledVerdict } from '../src/commands/test-verdict.js';
|
|
|
13
13
|
import { forceReleaseHeldLocksForSignal } from '../src/core/file-lock.js';
|
|
14
14
|
import { forceReleaseFurnaceLocksForActiveOperations, isSignalRollbackInFlight, rollbackActiveOperationsForSignal, } from '../src/core/furnace-operation.js';
|
|
15
15
|
import { closeActiveRunLog } from '../src/core/run-log.js';
|
|
16
|
+
import { removeActiveRunProfiles } from '../src/core/run-profile.js';
|
|
16
17
|
import { waitForActiveCriticalSections } from '../src/core/signal-critical.js';
|
|
17
18
|
import { CommandError } from '../src/errors/base.js';
|
|
18
19
|
import { waitForActiveChildShutdown } from '../src/utils/process.js';
|
|
@@ -114,6 +115,10 @@ function installFurnaceSignalHandler(signal, exitCode) {
|
|
|
114
115
|
// loses the race to the exit below. Only a kill FireForge never saw
|
|
115
116
|
// may leave the file behind.
|
|
116
117
|
.then(() => removeActivePgidFile().catch(() => undefined))
|
|
118
|
+
// A `run --temp-profile` (the `--smoke-exit` default) owes the removal
|
|
119
|
+
// of its profile directory; the command's finally loses this race too.
|
|
120
|
+
// Runs after child shutdown, so the browser no longer holds the files.
|
|
121
|
+
.then(() => removeActiveRunProfiles())
|
|
117
122
|
.finally(() => {
|
|
118
123
|
exitAfterStdioFlush(exitCode);
|
|
119
124
|
});
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.
|
|
4
|
-
"commit": "
|
|
5
|
-
"shortCommit": "
|
|
3
|
+
"version": "0.49.0",
|
|
4
|
+
"commit": "09dd0a82fdad367fb2390b4311f785a975995acd",
|
|
5
|
+
"shortCommit": "09dd0a82fdad",
|
|
6
6
|
"dirty": false,
|
|
7
7
|
"dirtyHash": null,
|
|
8
|
-
"buildTime": "2026-
|
|
8
|
+
"buildTime": "2026-10-04T08:17:58.923Z"
|
|
9
9
|
}
|
package/dist/src/cli.js
CHANGED
|
@@ -154,44 +154,32 @@ export function withErrorHandling(handler) {
|
|
|
154
154
|
if (error instanceof CommandError) {
|
|
155
155
|
throw error;
|
|
156
156
|
}
|
|
157
|
+
const normalizedError = toError(error);
|
|
158
|
+
const exitCode = error instanceof FireForgeError ? error.code : ExitCode.GENERAL_ERROR;
|
|
157
159
|
if (error instanceof CancellationError) {
|
|
158
160
|
cancel('Operation cancelled');
|
|
159
|
-
// 130 (128+SIGINT) is the conventional "user interrupted" code, so
|
|
160
|
-
// scripts/CI can distinguish a prompt cancellation from a real
|
|
161
|
-
// failure (which exits 1).
|
|
162
|
-
throw new CommandError(ExitCode.USER_CANCELLED);
|
|
163
161
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// and without the stack there is nothing in it that locates the bug.
|
|
167
|
-
// console.error keeps it on stderr, so a --json payload stays intact.
|
|
168
|
-
if (error instanceof InternalInvariantError) {
|
|
162
|
+
else if (error instanceof InternalInvariantError) {
|
|
163
|
+
// Invariant failures always include their stack for a bug report.
|
|
169
164
|
logError(error.userMessage);
|
|
170
|
-
if (error.stack)
|
|
165
|
+
if (error.stack)
|
|
171
166
|
console.error(error.stack);
|
|
172
|
-
}
|
|
173
|
-
throw new CommandError(error.code);
|
|
174
167
|
}
|
|
175
|
-
if (error instanceof FireForgeError) {
|
|
176
|
-
// In machine mode the payload contract owes the consumer a parseable
|
|
177
|
-
// refusal on stdout, not just a non-zero exit. `logError` has already
|
|
178
|
-
// been routed to stderr by the mode, so the two do not collide.
|
|
168
|
+
else if (error instanceof FireForgeError) {
|
|
179
169
|
logError(error.userMessage);
|
|
180
170
|
printCauseChain(error);
|
|
181
|
-
// Not when a payload already owns stdout: `status --json --fail-on`
|
|
182
|
-
// writes its full document and then refuses, and appending an
|
|
183
|
-
// envelope would make that two JSON documents.
|
|
184
|
-
if (machineOutput && !isStdoutSealed()) {
|
|
185
|
-
emitMachineError(machineErrorCode(error), error.message, error.code);
|
|
186
|
-
}
|
|
187
|
-
throw new CommandError(error.code);
|
|
188
171
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
172
|
+
else {
|
|
173
|
+
logError(`Unexpected error: ${normalizedError.message}`);
|
|
174
|
+
if (normalizedError.stack)
|
|
175
|
+
console.error(normalizedError.stack);
|
|
176
|
+
}
|
|
177
|
+
// One final sink covers every classification. A command that already
|
|
178
|
+
// sealed stdout owns its payload; appending an envelope would corrupt it.
|
|
179
|
+
if (machineOutput && !isStdoutSealed()) {
|
|
180
|
+
emitMachineError(error instanceof FireForgeError ? machineErrorCode(error) : 'unexpected-error', normalizedError.message, exitCode);
|
|
193
181
|
}
|
|
194
|
-
throw new CommandError(
|
|
182
|
+
throw new CommandError(exitCode);
|
|
195
183
|
}
|
|
196
184
|
finally {
|
|
197
185
|
// Central machine-mode and stdout-seal reset. Commands leave both
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { join } from 'node:path';
|
|
2
3
|
import { Command, InvalidArgumentError as CommanderInvalidArgumentError } from 'commander';
|
|
3
4
|
import { validateBrandOverride } from '../core/brand-validation.js';
|
|
4
5
|
import { auditBuildArtifacts } from '../core/build-audit.js';
|
|
@@ -8,6 +9,10 @@ import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
|
8
9
|
import { assertEngineExists } from '../core/engine-precondition.js';
|
|
9
10
|
import { withEngineSessionLock } from '../core/engine-session-lock.js';
|
|
10
11
|
import { attemptMozinfoRewrite, build, buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, hasRunnableBundle, runMach, withBuildLock, } from '../core/mach.js';
|
|
12
|
+
import { findObjdirRelocationViolation } from '../core/mach-build-artifacts.js';
|
|
13
|
+
import { ensureLaunchableBrowserNotRunning } from '../core/marionette-port.js';
|
|
14
|
+
import { assertHealthyPartialConfig, pruneDanglingTestLinks } from '../core/objdir-maintenance.js';
|
|
15
|
+
import { waitForPreflight } from '../core/preflight-wait.js';
|
|
11
16
|
import { closeActiveRunLog, getActiveRunLogPath, openRunLog, setActiveRunLog, } from '../core/run-log.js';
|
|
12
17
|
import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
|
|
13
18
|
import { formatToolchainMismatchMessage, runToolchainPreflight, } from '../core/toolchain-preflight.js';
|
|
@@ -16,8 +21,8 @@ import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
|
16
21
|
import { elapsedSince } from '../utils/elapsed.js';
|
|
17
22
|
import { toError } from '../utils/errors.js';
|
|
18
23
|
import { checkDiskSpace } from '../utils/fs.js';
|
|
19
|
-
import { error, info, intro, outro, spinner, verbose, warn } from '../utils/logger.js';
|
|
20
|
-
import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../utils/options.js';
|
|
24
|
+
import { error, info, intro, notice, outro, spinner, verbose, warn } from '../utils/logger.js';
|
|
25
|
+
import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../utils/options.js';
|
|
21
26
|
import { isPositiveInteger } from '../utils/validation.js';
|
|
22
27
|
function parseJobCount(value) {
|
|
23
28
|
const parsed = Number(value);
|
|
@@ -63,6 +68,13 @@ async function rewriteAndReconfigure(engineDir, objDir, mismatchMessage) {
|
|
|
63
68
|
throw new BuildError(`mach configure exited non-zero (${exitCode}) after mozinfo rewrite; a clean rebuild is required.`, 'mach configure');
|
|
64
69
|
}
|
|
65
70
|
configureSpinner.stop('mach configure regenerated the backend');
|
|
71
|
+
const violation = await findObjdirRelocationViolation({
|
|
72
|
+
engineDir,
|
|
73
|
+
objDir,
|
|
74
|
+
forbiddenDir: rewrite.oldTopsrcdir ?? '',
|
|
75
|
+
});
|
|
76
|
+
if (violation)
|
|
77
|
+
throw new GeneralError(`Relocation incomplete: ${violation}`);
|
|
66
78
|
info('Backend path-rewrite complete; continuing with the build.');
|
|
67
79
|
}
|
|
68
80
|
function resolveJobCount(options, configJobs) {
|
|
@@ -122,8 +134,6 @@ function extractLikelyFailingCommand(captured) {
|
|
|
122
134
|
continue;
|
|
123
135
|
if (isWarningOnlyLine(line))
|
|
124
136
|
continue;
|
|
125
|
-
if (/^make(?:\[\d+\])?:/.test(line))
|
|
126
|
-
continue;
|
|
127
137
|
if (/^g?make(?:\[\d+\])?:/.test(line))
|
|
128
138
|
continue;
|
|
129
139
|
if (/^Error running mach:/.test(line))
|
|
@@ -186,6 +196,20 @@ export async function buildCommand(projectRoot, options) {
|
|
|
186
196
|
info(`Full build output: ${logPath}`);
|
|
187
197
|
}
|
|
188
198
|
}
|
|
199
|
+
/** Checks objdir health and prevents deployments under a running browser. */
|
|
200
|
+
async function preflightBuildObjdir(engineDir, binaryName, objDir, options) {
|
|
201
|
+
if (!objDir)
|
|
202
|
+
return;
|
|
203
|
+
await assertHealthyPartialConfig(engineDir, objDir);
|
|
204
|
+
const bundle = await hasRunnableBundle(engineDir, binaryName, objDir);
|
|
205
|
+
const expectedPath = bundle.expectedPath;
|
|
206
|
+
if (bundle.runnable && expectedPath) {
|
|
207
|
+
await waitForPreflight(() => ensureLaunchableBrowserNotRunning(join(engineDir, expectedPath)), options.waitBrowser === undefined ? undefined : resolveWaitLockSeconds(options.waitBrowser));
|
|
208
|
+
}
|
|
209
|
+
const pruned = await pruneDanglingTestLinks(engineDir, objDir);
|
|
210
|
+
if (pruned > 0)
|
|
211
|
+
notice(`Pruned ${pruned} dangling _tests symlink(s).`);
|
|
212
|
+
}
|
|
189
213
|
async function runBuildCommandBody(projectRoot, options) {
|
|
190
214
|
const buildType = options.ui ? 'UI-only' : 'Full';
|
|
191
215
|
// Load configuration
|
|
@@ -253,6 +277,7 @@ async function runBuildCommandBody(projectRoot, options) {
|
|
|
253
277
|
// auto-configure step there can detect moz.build-family changes since the
|
|
254
278
|
// last successful build. The post-build audit below reuses the same
|
|
255
279
|
// baseline to diff engine changes against dist artifacts.
|
|
280
|
+
await preflightBuildObjdir(paths.engine, config.binaryName, buildCheck.objDir, options);
|
|
256
281
|
const previousBaseline = await readBuildBaseline(projectRoot);
|
|
257
282
|
// Shared pre-flight: branding, Furnace, mozconfig, auto-configure
|
|
258
283
|
await prepareBuildEnvironment(projectRoot, paths, config, {
|
|
@@ -374,6 +399,7 @@ export function registerBuild(program, { getProjectRoot, withErrorHandling }) {
|
|
|
374
399
|
const build = program
|
|
375
400
|
.command('build')
|
|
376
401
|
.description('Build the browser (auto-applies Furnace components first)')
|
|
402
|
+
.option('--wait-browser [seconds]', 'Wait for this objdir browser to exit before changing its build (default 60)', commanderArgParser((raw) => resolveWaitLockSeconds(raw)))
|
|
377
403
|
.option('--ui', 'Fast UI-only rebuild')
|
|
378
404
|
.option('-j, --jobs <n>', 'Number of parallel jobs', parseJobCount)
|
|
379
405
|
.option('--brand <name>', 'Build specific brand')
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathExists } from '../utils/fs.js';
|
|
4
|
+
import { ok, warning } from './doctor-check-core.js';
|
|
5
|
+
/** Surfaces the source-refresh ESLint precondition before an expensive gate/build. */
|
|
6
|
+
export const ENGINE_ESLINT_DOCTOR_CHECK = {
|
|
7
|
+
name: 'Engine ESLint installed',
|
|
8
|
+
skipIf: (ctx) => !ctx.engineExists,
|
|
9
|
+
run: async (ctx) => (await pathExists(join(ctx.paths.engine, 'node_modules/eslint/bin/eslint.js')))
|
|
10
|
+
? ok('Engine ESLint installed')
|
|
11
|
+
: warning('Engine ESLint installed', 'Engine-local ESLint is missing after source extraction; an engine-eslint ratchet cannot run.', 'Run "cd engine && ./mach eslint --setup" after each source refresh, before building or running the gate.'),
|
|
12
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DoctorCheck } from '../types/commands/index.js';
|
|
2
|
+
import type { DoctorCheckDefinition } from './doctor-check-core.js';
|
|
3
|
+
/** The bootstrapped clang mach uses on macOS. */
|
|
4
|
+
export declare function bootstrappedClangPath(): string;
|
|
5
|
+
/** Runs the check for one engine directory. */
|
|
6
|
+
export declare function checkMacosSdkLink(engineDir: string): Promise<DoctorCheck>;
|
|
7
|
+
/** Registry entry for the doctor runner. */
|
|
8
|
+
export declare const MACOS_SDK_LINK_DOCTOR_CHECK: DoctorCheckDefinition;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Doctor check: can the bootstrapped clang link against the macOS SDK the
|
|
4
|
+
* build will use?
|
|
5
|
+
*
|
|
6
|
+
* An Xcode update can ship an SDK whose `.tbd` stubs the pinned toolchain
|
|
7
|
+
* cannot read (Xcode 27.0's `libSystem.tbd` lists `arm64e.x1-macos`
|
|
8
|
+
* targets that clang/lld 21.1.8 rejects as "unknown architecture"). `mach
|
|
9
|
+
* configure` then dies at "checking what kind of list files are supported
|
|
10
|
+
* by the linker" with exit 5, and neither `build` nor `doctor` named the
|
|
11
|
+
* cause: the operator had to read `config.log`. This check links a trivial
|
|
12
|
+
* C program with the bootstrapped clang against the SDK the mozconfig
|
|
13
|
+
* selects, and on failure names the mismatch and lists the SDKs installed.
|
|
14
|
+
*
|
|
15
|
+
* darwin only, and only when a bootstrapped clang exists. A warning, never
|
|
16
|
+
* a failure: the SDK may be one the operator is about to replace. Host SDK
|
|
17
|
+
* and mozconfig paths are built with `posix.join`: they are macOS paths
|
|
18
|
+
* whatever platform the module is loaded on.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
23
|
+
import { join, posix } from 'node:path';
|
|
24
|
+
import { mozbuildStateDir } from '../core/toolchain-preflight.js';
|
|
25
|
+
import { toError } from '../utils/errors.js';
|
|
26
|
+
import { exec } from '../utils/process.js';
|
|
27
|
+
import { ok, warning } from './doctor-check-core.js';
|
|
28
|
+
const CHECK_NAME = 'macOS SDK links with the bootstrapped clang';
|
|
29
|
+
/** The bootstrapped clang mach uses on macOS. */
|
|
30
|
+
export function bootstrappedClangPath() {
|
|
31
|
+
return join(mozbuildStateDir(), 'clang', 'bin', 'clang');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Sources a mozconfig with `ac_add_options` stubbed to print the
|
|
35
|
+
* `--with-macos-sdk=` value, which is how mach reads it. A mozconfig is a
|
|
36
|
+
* shell script: an SDK chosen by shell logic (the first of several
|
|
37
|
+
* candidates that exists) has no literal value to grep.
|
|
38
|
+
*/
|
|
39
|
+
const SDK_FROM_MOZCONFIG_SCRIPT = [
|
|
40
|
+
'ac_add_options() { for a in "$@"; do case "$a" in --with-macos-sdk=*) printf "%s\\n" "${a#--with-macos-sdk=}";; esac; done; }',
|
|
41
|
+
'mk_add_options() { :; }',
|
|
42
|
+
'. "$1"',
|
|
43
|
+
].join('\n');
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the SDK the generated mozconfig selects (`--with-macos-sdk`),
|
|
46
|
+
* falling back to `xcrun --show-sdk-path`, which is what configure
|
|
47
|
+
* auto-detects when the mozconfig names none.
|
|
48
|
+
*/
|
|
49
|
+
async function resolveSelectedSdk(engineDir) {
|
|
50
|
+
const mozconfig = posix.join(engineDir, 'mozconfig');
|
|
51
|
+
if (existsSync(mozconfig)) {
|
|
52
|
+
const sourced = await exec('/bin/sh', ['-c', SDK_FROM_MOZCONFIG_SCRIPT, 'sh', mozconfig], {
|
|
53
|
+
cwd: engineDir,
|
|
54
|
+
env: { topsrcdir: engineDir },
|
|
55
|
+
timeout: 10_000,
|
|
56
|
+
});
|
|
57
|
+
if (sourced.exitCode !== 0) {
|
|
58
|
+
return {
|
|
59
|
+
error: `engine/mozconfig failed while selecting the SDK: ${firstLine(sourced.stderr) ?? `exit ${sourced.exitCode}`}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const selected = sourced.stdout
|
|
63
|
+
.split('\n')
|
|
64
|
+
.map((line) => line.trim())
|
|
65
|
+
.filter((line) => line.length > 0)
|
|
66
|
+
.pop();
|
|
67
|
+
if (selected !== undefined)
|
|
68
|
+
return { sdk: selected, source: 'mozconfig' };
|
|
69
|
+
}
|
|
70
|
+
const xcrun = await exec('xcrun', ['--show-sdk-path'], { timeout: 10_000 });
|
|
71
|
+
const path = xcrun.stdout.trim();
|
|
72
|
+
if (xcrun.exitCode !== 0 || path.length === 0) {
|
|
73
|
+
return { error: `xcrun --show-sdk-path failed: ${firstLine(xcrun.stderr) ?? 'no output'}` };
|
|
74
|
+
}
|
|
75
|
+
return { sdk: path, source: 'xcrun' };
|
|
76
|
+
}
|
|
77
|
+
function firstLine(text) {
|
|
78
|
+
return text
|
|
79
|
+
.split('\n')
|
|
80
|
+
.map((line) => line.trim())
|
|
81
|
+
.find((line) => line.length > 0);
|
|
82
|
+
}
|
|
83
|
+
/** Every macOS SDK directory on the host FireForge knows where to look for. */
|
|
84
|
+
async function listInstalledSdks() {
|
|
85
|
+
const roots = [mozbuildStateDir(), '/Library/Developer/CommandLineTools/SDKs'];
|
|
86
|
+
const developer = await exec('xcode-select', ['-p'], { timeout: 10_000 });
|
|
87
|
+
if (developer.exitCode === 0 && developer.stdout.trim().length > 0) {
|
|
88
|
+
roots.push(posix.join(developer.stdout.trim(), 'Platforms/MacOSX.platform/Developer/SDKs'));
|
|
89
|
+
}
|
|
90
|
+
const found = [];
|
|
91
|
+
for (const root of roots) {
|
|
92
|
+
try {
|
|
93
|
+
for (const entry of await readdir(root)) {
|
|
94
|
+
if (/^MacOSX.*\.sdk$/.test(entry))
|
|
95
|
+
found.push(posix.join(root, entry));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// A root that does not exist lists nothing.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Links `int main(void){return 0;}` with the bootstrapped clang (through
|
|
106
|
+
* its bundled lld, as the build does) against `sdk`, in a temp directory.
|
|
107
|
+
* @returns undefined on success, else the first error line clang printed
|
|
108
|
+
*/
|
|
109
|
+
async function probeSdkLink(clang, sdk) {
|
|
110
|
+
const dir = await mkdtemp(join(tmpdir(), 'fireforge-sdk-probe-'));
|
|
111
|
+
try {
|
|
112
|
+
await writeFile(join(dir, 'probe.c'), 'int main(void) { return 0; }\n');
|
|
113
|
+
const lld = existsSync(join(clang, '..', 'ld64.lld')) ? ['-fuse-ld=lld'] : [];
|
|
114
|
+
const linked = await exec(clang, ['-isysroot', sdk, ...lld, '-o', join(dir, 'probe'), join(dir, 'probe.c')], { timeout: 30_000 });
|
|
115
|
+
if (linked.exitCode === 0)
|
|
116
|
+
return undefined;
|
|
117
|
+
const output = `${linked.stderr}\n${linked.stdout}`;
|
|
118
|
+
return (output
|
|
119
|
+
.split('\n')
|
|
120
|
+
.map((line) => line.trim())
|
|
121
|
+
.find((line) => /error:/.test(line)) ??
|
|
122
|
+
firstLine(output) ??
|
|
123
|
+
`exit ${linked.exitCode}`);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await rm(dir, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Runs the check for one engine directory. */
|
|
130
|
+
export async function checkMacosSdkLink(engineDir) {
|
|
131
|
+
const selected = await resolveSelectedSdk(engineDir);
|
|
132
|
+
const fix = 'Pin an SDK the bootstrapped toolchain can link in configs/darwin.mozconfig ' +
|
|
133
|
+
'(ac_add_options --with-macos-sdk=<path>), then run fireforge build.';
|
|
134
|
+
if ('error' in selected) {
|
|
135
|
+
return warning(CHECK_NAME, selected.error, fix);
|
|
136
|
+
}
|
|
137
|
+
let failure;
|
|
138
|
+
try {
|
|
139
|
+
failure = await probeSdkLink(bootstrappedClangPath(), selected.sdk);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
failure = toError(error).message;
|
|
143
|
+
}
|
|
144
|
+
if (failure === undefined) {
|
|
145
|
+
return ok(CHECK_NAME, `OK (${selected.sdk}, from ${selected.source})`);
|
|
146
|
+
}
|
|
147
|
+
const installed = await listInstalledSdks();
|
|
148
|
+
return warning(CHECK_NAME, `The bootstrapped clang cannot link against ${selected.sdk} (from ${selected.source}): ${failure}. ` +
|
|
149
|
+
`mach configure fails at its linker probe the same way. Installed SDKs: ` +
|
|
150
|
+
(installed.length > 0 ? installed.join(', ') : 'none found') +
|
|
151
|
+
'.', fix);
|
|
152
|
+
}
|
|
153
|
+
/** Registry entry for the doctor runner. */
|
|
154
|
+
export const MACOS_SDK_LINK_DOCTOR_CHECK = {
|
|
155
|
+
name: CHECK_NAME,
|
|
156
|
+
skipIf: (ctx) => process.platform !== 'darwin' || !ctx.engineExists || !existsSync(bootstrappedClangPath()),
|
|
157
|
+
run: (ctx) => checkMacosSdkLink(ctx.paths.engine),
|
|
158
|
+
};
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { getFirefoxVersion } from '../core/firefox.js';
|
|
25
25
|
import { toError } from '../utils/errors.js';
|
|
26
|
+
import { exec } from '../utils/process.js';
|
|
26
27
|
import { ok, warning } from './doctor-check-core.js';
|
|
27
28
|
const CHECK_NAME = 'Source pin matches engine';
|
|
28
29
|
/**
|
|
@@ -42,7 +43,13 @@ async function runSourcePinCheck(ctx) {
|
|
|
42
43
|
// A blank or whitespace-only version.txt (a truncated write, a partial
|
|
43
44
|
// extraction) carries no version to compare, so treat it as absent
|
|
44
45
|
// rather than reporting the engine as being at version "".
|
|
45
|
-
|
|
46
|
+
const pristine = await exec('git', ['show', 'HEAD:browser/config/version.txt'], {
|
|
47
|
+
cwd: ctx.paths.engine,
|
|
48
|
+
}).catch(() => undefined);
|
|
49
|
+
actual =
|
|
50
|
+
(pristine?.exitCode === 0
|
|
51
|
+
? pristine.stdout
|
|
52
|
+
: await getFirefoxVersion(ctx.paths.engine))?.trim() || undefined;
|
|
46
53
|
}
|
|
47
54
|
catch (error) {
|
|
48
55
|
// Best-effort: an unreadable version.txt is not a pin mismatch, and a
|
|
@@ -52,7 +59,7 @@ async function runSourcePinCheck(ctx) {
|
|
|
52
59
|
const downloaded = ctx.state.downloadedVersion;
|
|
53
60
|
const mismatches = [];
|
|
54
61
|
if (actual !== undefined && actual !== pinned) {
|
|
55
|
-
mismatches.push(`engine
|
|
62
|
+
mismatches.push(`engine baseline browser/config/version.txt reads ${actual}`);
|
|
56
63
|
}
|
|
57
64
|
if (downloaded !== undefined && downloaded !== pinned) {
|
|
58
65
|
mismatches.push(`the last download recorded ${downloaded}`);
|
|
@@ -15,8 +15,10 @@ import { error, info, intro, outro, success, warn } from '../utils/logger.js';
|
|
|
15
15
|
import { addWaitLockOption } from '../utils/options.js';
|
|
16
16
|
import { findExecutable } from '../utils/process.js';
|
|
17
17
|
import { failure, ok, warning } from './doctor-check-core.js';
|
|
18
|
+
import { ENGINE_ESLINT_DOCTOR_CHECK } from './doctor-engine-eslint.js';
|
|
18
19
|
import { EXTERNAL_TOOLCHAIN_DOCTOR_CHECK } from './doctor-external-toolchains.js';
|
|
19
20
|
import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
|
|
21
|
+
import { MACOS_SDK_LINK_DOCTOR_CHECK } from './doctor-macos-sdk.js';
|
|
20
22
|
import { ORPHANED_HARNESS_DOCTOR_CHECK } from './doctor-orphaned-harness.js';
|
|
21
23
|
import { PATCH_MANIFEST_CONSISTENCY_CHECK } from './doctor-patch-manifest.js';
|
|
22
24
|
import { POST_REBASE_AUDIT_CHECK } from './doctor-post-rebase-audit.js';
|
|
@@ -210,6 +212,7 @@ const DOCTOR_CHECKS = [
|
|
|
210
212
|
},
|
|
211
213
|
},
|
|
212
214
|
SOURCE_PIN_DOCTOR_CHECK,
|
|
215
|
+
ENGINE_ESLINT_DOCTOR_CHECK,
|
|
213
216
|
{
|
|
214
217
|
name: 'Engine is git repository',
|
|
215
218
|
skipIf: (ctx) => !ctx.engineExists,
|
|
@@ -258,6 +261,7 @@ const DOCTOR_CHECKS = [
|
|
|
258
261
|
},
|
|
259
262
|
},
|
|
260
263
|
ORPHANED_HARNESS_DOCTOR_CHECK,
|
|
264
|
+
MACOS_SDK_LINK_DOCTOR_CHECK,
|
|
261
265
|
{
|
|
262
266
|
name: 'Patches directory exists',
|
|
263
267
|
run: async (ctx) => {
|
|
@@ -9,7 +9,7 @@ import { downloadFirefoxSource, formatBytes, sweepOrphanedEngineWorkDirs, } from
|
|
|
9
9
|
import { clearAppliedFurnaceState } from '../core/furnace-config.js';
|
|
10
10
|
import { getHead, initRepository, isGitRepository, isMissingHeadError, resumeRepository, } from '../core/git.js';
|
|
11
11
|
import { restoreTrackedPath } from '../core/git-file-ops.js';
|
|
12
|
-
import { getDirtyFiles } from '../core/git-status.js';
|
|
12
|
+
import { getDirtyFiles, getWorkingTreeStatus } from '../core/git-status.js';
|
|
13
13
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
14
14
|
import { formatMajorVersionHopNotice } from '../core/toolchain-preflight.js';
|
|
15
15
|
import { EngineExistsError, PartialEngineExistsError } from '../errors/download.js';
|
|
@@ -17,6 +17,7 @@ import { toError } from '../utils/errors.js';
|
|
|
17
17
|
import { checkDiskSpace, ensureDir, pathExists, pathExistsStrict, removeDir } from '../utils/fs.js';
|
|
18
18
|
import { info, intro, outro, spinner, verbose, warn } from '../utils/logger.js';
|
|
19
19
|
import { pickDefined } from '../utils/options.js';
|
|
20
|
+
import { confirmDirtyEngineReset } from './rebase/confirm.js';
|
|
20
21
|
/**
|
|
21
22
|
* Collects the set of patch-touched files from the manifest.
|
|
22
23
|
* Returns an empty set when the patches directory or manifest is absent.
|
|
@@ -35,6 +36,47 @@ async function getPatchTouchedFiles(patchesDir) {
|
|
|
35
36
|
}
|
|
36
37
|
return files;
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Describes what a forced replacement would discard from an existing
|
|
41
|
+
* engine: changed paths against HEAD (tracked edits, applied patches, new
|
|
42
|
+
* untracked files; ignored paths such as the objdir are not work), and a
|
|
43
|
+
* HEAD that moved off the recorded base commit. Returns `undefined` when
|
|
44
|
+
* nothing would be lost, including an engine that is not a git repository
|
|
45
|
+
* or has no commit yet: that is the broken tree `--force` exists to replace.
|
|
46
|
+
*/
|
|
47
|
+
async function describeForcedReplacementLoss(engineDir, patchesDir, baseCommit) {
|
|
48
|
+
if (!(await isGitRepository(engineDir)))
|
|
49
|
+
return undefined;
|
|
50
|
+
let head;
|
|
51
|
+
try {
|
|
52
|
+
head = await getHead(engineDir);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (isMissingHeadError(error))
|
|
56
|
+
return undefined;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const entries = await getWorkingTreeStatus(engineDir);
|
|
60
|
+
const headMoved = baseCommit !== undefined && head !== baseCommit;
|
|
61
|
+
if (entries.length === 0 && !headMoved)
|
|
62
|
+
return undefined;
|
|
63
|
+
const parts = [];
|
|
64
|
+
if (entries.length > 0) {
|
|
65
|
+
const patchFiles = [...(await getPatchTouchedFiles(patchesDir))];
|
|
66
|
+
const claimed = entries.filter((entry) => entry.file.endsWith('/')
|
|
67
|
+
? patchFiles.some((file) => file.startsWith(entry.file))
|
|
68
|
+
: patchFiles.includes(entry.file)).length;
|
|
69
|
+
const unclaimed = entries.length - claimed;
|
|
70
|
+
parts.push(`engine/ has ${entries.length} changed path(s) against HEAD: ` +
|
|
71
|
+
`${claimed} match the patch queue, ${unclaimed} do not` +
|
|
72
|
+
(unclaimed > 0 ? ' (unexported work that exists nowhere else).' : '.'));
|
|
73
|
+
}
|
|
74
|
+
if (headMoved) {
|
|
75
|
+
parts.push(`engine/ HEAD ${head.slice(0, 12)} is not the recorded base commit ${baseCommit.slice(0, 12)}, so commits made in engine/ are discarded too.`);
|
|
76
|
+
}
|
|
77
|
+
parts.push('--force replaces the whole directory.');
|
|
78
|
+
return parts.join(' ');
|
|
79
|
+
}
|
|
38
80
|
/**
|
|
39
81
|
* Restores patch-touched files to their committed (HEAD) state so that a
|
|
40
82
|
* subsequent `fireforge import` does not see spurious uncommitted changes.
|
|
@@ -279,7 +321,8 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
279
321
|
const paths = getProjectPaths(projectRoot);
|
|
280
322
|
// Captured before any state update so the post-download major-hop
|
|
281
323
|
// notice compares against what was actually on disk until now.
|
|
282
|
-
const
|
|
324
|
+
const previousState = await loadState(projectRoot);
|
|
325
|
+
const previousVersion = previousState.downloadedVersion;
|
|
283
326
|
info(`Firefox version: ${version}`);
|
|
284
327
|
await checkDiskSpace(projectRoot, 5 * 1024 * 1024 * 1024, warn);
|
|
285
328
|
// A legitimate holder of this lock runs for 10+ minutes (download +
|
|
@@ -381,6 +424,23 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
381
424
|
}
|
|
382
425
|
throw new EngineExistsError(paths.engine);
|
|
383
426
|
}
|
|
427
|
+
// The replacement deletes the old tree once the new one is active,
|
|
428
|
+
// so anything not in patches/ is gone for good. Ask before fetching.
|
|
429
|
+
const loss = await describeForcedReplacementLoss(paths.engine, paths.patches, previousState.baseCommit);
|
|
430
|
+
if (loss !== undefined &&
|
|
431
|
+
!(await confirmDirtyEngineReset({
|
|
432
|
+
engineDir: paths.engine,
|
|
433
|
+
yes: options.yes ?? false,
|
|
434
|
+
dirty: true,
|
|
435
|
+
refusalDetail: loss,
|
|
436
|
+
nonInteractiveCommand: 'fireforge download --force --yes',
|
|
437
|
+
argumentName: '--yes',
|
|
438
|
+
warningMessage: loss,
|
|
439
|
+
promptMessage: 'Discard the existing engine/ and download a fresh one?',
|
|
440
|
+
cancelMessage: 'Download cancelled; engine/ left untouched',
|
|
441
|
+
}))) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
384
444
|
replacementEngineDir = `${paths.engine}.replacement-${randomUUID()}`;
|
|
385
445
|
backupEngineDir = `${paths.engine}.backup-${randomUUID()}`;
|
|
386
446
|
installEngineDir = replacementEngineDir;
|
|
@@ -436,7 +496,8 @@ export function registerDownload(program, { getProjectRoot, withErrorHandling })
|
|
|
436
496
|
program
|
|
437
497
|
.command('download')
|
|
438
498
|
.description('Download Firefox source')
|
|
439
|
-
.option('-f, --force', 'Force re-download
|
|
499
|
+
.option('-f, --force', 'Force re-download; discards engine/ including applied patches and unexported edits')
|
|
500
|
+
.option('-y, --yes', 'With --force: replace a dirty engine/ without the confirmation prompt')
|
|
440
501
|
.action(withErrorHandling(async (options) => {
|
|
441
502
|
await downloadCommand(getProjectRoot(), pickDefined(options));
|
|
442
503
|
}));
|