@hominis/fireforge 0.47.1 → 0.48.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 +54 -0
- package/dist/bin/fireforge.js +5 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +156 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/download.js +64 -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 +10 -0
- 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.js +4 -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/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- 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/discard-baseline.js +2 -1
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -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/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -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 +8 -1
- 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/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/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 +85 -11
- 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 +15 -0
- package/dist/src/types/config.d.ts +10 -0
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +49 -0
- package/dist/src/utils/process.d.ts +2 -0
- package/dist/src/utils/process.js +2 -1
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +81 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +4 -3
- package/docs/patch-workflow.md +35 -1
- package/package.json +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.48.0
|
|
4
|
+
|
|
5
|
+
### Engine lifecycle
|
|
6
|
+
|
|
7
|
+
- `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.
|
|
8
|
+
|
|
9
|
+
### Builds
|
|
10
|
+
|
|
11
|
+
- `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.
|
|
12
|
+
|
|
13
|
+
### Smoke runs
|
|
14
|
+
|
|
15
|
+
- **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.
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
19
|
+
### Rebase
|
|
20
|
+
|
|
21
|
+
- `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.
|
|
22
|
+
- 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.
|
|
23
|
+
- The header warns when `state.json`'s `downloadedVersion` is not the pinned target, meaning the download step has not run yet.
|
|
24
|
+
|
|
25
|
+
### Performance
|
|
26
|
+
|
|
27
|
+
- 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.
|
|
28
|
+
- 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.
|
|
29
|
+
|
|
30
|
+
### Typecheck and checkJs
|
|
31
|
+
|
|
32
|
+
- 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.
|
|
33
|
+
- `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/`.
|
|
34
|
+
- On the Hominis tree `typecheck` went from 93.8 s to 30.2 s.
|
|
35
|
+
- 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.
|
|
36
|
+
|
|
37
|
+
### Re-export
|
|
38
|
+
|
|
39
|
+
- 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.
|
|
40
|
+
- 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`.
|
|
41
|
+
- `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.
|
|
42
|
+
|
|
43
|
+
### Patch queue and lint
|
|
44
|
+
|
|
45
|
+
- 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.
|
|
46
|
+
- New public `findForwardRegistrations(patchesDir, { scope })` returns the same census as structured records.
|
|
47
|
+
- 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.
|
|
48
|
+
|
|
49
|
+
### Programmatic API
|
|
50
|
+
|
|
51
|
+
- 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.
|
|
52
|
+
|
|
53
|
+
### Doctor
|
|
54
|
+
|
|
55
|
+
- 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.
|
|
56
|
+
|
|
3
57
|
## 0.47.0
|
|
4
58
|
|
|
5
59
|
### 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.48.0",
|
|
4
|
+
"commit": "9215c35e3907f14cc03379fb04c867ebd954085a",
|
|
5
|
+
"shortCommit": "9215c35e3907",
|
|
6
6
|
"dirty": false,
|
|
7
7
|
"dirtyHash": null,
|
|
8
|
-
"buildTime": "2026-09-
|
|
8
|
+
"buildTime": "2026-09-22T21:34:33.874Z"
|
|
9
9
|
}
|
|
@@ -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,156 @@
|
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
import { mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { mozbuildStateDir } from '../core/toolchain-preflight.js';
|
|
23
|
+
import { toError } from '../utils/errors.js';
|
|
24
|
+
import { exec } from '../utils/process.js';
|
|
25
|
+
import { ok, warning } from './doctor-check-core.js';
|
|
26
|
+
const CHECK_NAME = 'macOS SDK links with the bootstrapped clang';
|
|
27
|
+
/** The bootstrapped clang mach uses on macOS. */
|
|
28
|
+
export function bootstrappedClangPath() {
|
|
29
|
+
return join(mozbuildStateDir(), 'clang', 'bin', 'clang');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sources a mozconfig with `ac_add_options` stubbed to print the
|
|
33
|
+
* `--with-macos-sdk=` value, which is how mach reads it. A mozconfig is a
|
|
34
|
+
* shell script: an SDK chosen by shell logic (the first of several
|
|
35
|
+
* candidates that exists) has no literal value to grep.
|
|
36
|
+
*/
|
|
37
|
+
const SDK_FROM_MOZCONFIG_SCRIPT = [
|
|
38
|
+
'ac_add_options() { for a in "$@"; do case "$a" in --with-macos-sdk=*) printf "%s\\n" "${a#--with-macos-sdk=}";; esac; done; }',
|
|
39
|
+
'mk_add_options() { :; }',
|
|
40
|
+
'. "$1"',
|
|
41
|
+
].join('\n');
|
|
42
|
+
/**
|
|
43
|
+
* Resolves the SDK the generated mozconfig selects (`--with-macos-sdk`),
|
|
44
|
+
* falling back to `xcrun --show-sdk-path`, which is what configure
|
|
45
|
+
* auto-detects when the mozconfig names none.
|
|
46
|
+
*/
|
|
47
|
+
async function resolveSelectedSdk(engineDir) {
|
|
48
|
+
const mozconfig = join(engineDir, 'mozconfig');
|
|
49
|
+
if (existsSync(mozconfig)) {
|
|
50
|
+
const sourced = await exec('/bin/sh', ['-c', SDK_FROM_MOZCONFIG_SCRIPT, 'sh', mozconfig], {
|
|
51
|
+
cwd: engineDir,
|
|
52
|
+
env: { topsrcdir: engineDir },
|
|
53
|
+
timeout: 10_000,
|
|
54
|
+
});
|
|
55
|
+
if (sourced.exitCode !== 0) {
|
|
56
|
+
return {
|
|
57
|
+
error: `engine/mozconfig failed while selecting the SDK: ${firstLine(sourced.stderr) ?? `exit ${sourced.exitCode}`}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const selected = sourced.stdout
|
|
61
|
+
.split('\n')
|
|
62
|
+
.map((line) => line.trim())
|
|
63
|
+
.filter((line) => line.length > 0)
|
|
64
|
+
.pop();
|
|
65
|
+
if (selected !== undefined)
|
|
66
|
+
return { sdk: selected, source: 'mozconfig' };
|
|
67
|
+
}
|
|
68
|
+
const xcrun = await exec('xcrun', ['--show-sdk-path'], { timeout: 10_000 });
|
|
69
|
+
const path = xcrun.stdout.trim();
|
|
70
|
+
if (xcrun.exitCode !== 0 || path.length === 0) {
|
|
71
|
+
return { error: `xcrun --show-sdk-path failed: ${firstLine(xcrun.stderr) ?? 'no output'}` };
|
|
72
|
+
}
|
|
73
|
+
return { sdk: path, source: 'xcrun' };
|
|
74
|
+
}
|
|
75
|
+
function firstLine(text) {
|
|
76
|
+
return text
|
|
77
|
+
.split('\n')
|
|
78
|
+
.map((line) => line.trim())
|
|
79
|
+
.find((line) => line.length > 0);
|
|
80
|
+
}
|
|
81
|
+
/** Every macOS SDK directory on the host FireForge knows where to look for. */
|
|
82
|
+
async function listInstalledSdks() {
|
|
83
|
+
const roots = [mozbuildStateDir(), '/Library/Developer/CommandLineTools/SDKs'];
|
|
84
|
+
const developer = await exec('xcode-select', ['-p'], { timeout: 10_000 });
|
|
85
|
+
if (developer.exitCode === 0 && developer.stdout.trim().length > 0) {
|
|
86
|
+
roots.push(join(developer.stdout.trim(), 'Platforms/MacOSX.platform/Developer/SDKs'));
|
|
87
|
+
}
|
|
88
|
+
const found = [];
|
|
89
|
+
for (const root of roots) {
|
|
90
|
+
try {
|
|
91
|
+
for (const entry of await readdir(root)) {
|
|
92
|
+
if (/^MacOSX.*\.sdk$/.test(entry))
|
|
93
|
+
found.push(join(root, entry));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// A root that does not exist lists nothing.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Links `int main(void){return 0;}` with the bootstrapped clang (through
|
|
104
|
+
* its bundled lld, as the build does) against `sdk`, in a temp directory.
|
|
105
|
+
* @returns undefined on success, else the first error line clang printed
|
|
106
|
+
*/
|
|
107
|
+
async function probeSdkLink(clang, sdk) {
|
|
108
|
+
const dir = await mkdtemp(join(tmpdir(), 'fireforge-sdk-probe-'));
|
|
109
|
+
try {
|
|
110
|
+
await writeFile(join(dir, 'probe.c'), 'int main(void) { return 0; }\n');
|
|
111
|
+
const lld = existsSync(join(clang, '..', 'ld64.lld')) ? ['-fuse-ld=lld'] : [];
|
|
112
|
+
const linked = await exec(clang, ['-isysroot', sdk, ...lld, '-o', join(dir, 'probe'), join(dir, 'probe.c')], { timeout: 30_000 });
|
|
113
|
+
if (linked.exitCode === 0)
|
|
114
|
+
return undefined;
|
|
115
|
+
const output = `${linked.stderr}\n${linked.stdout}`;
|
|
116
|
+
return (output
|
|
117
|
+
.split('\n')
|
|
118
|
+
.map((line) => line.trim())
|
|
119
|
+
.find((line) => /error:/.test(line)) ??
|
|
120
|
+
firstLine(output) ??
|
|
121
|
+
`exit ${linked.exitCode}`);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await rm(dir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Runs the check for one engine directory. */
|
|
128
|
+
export async function checkMacosSdkLink(engineDir) {
|
|
129
|
+
const selected = await resolveSelectedSdk(engineDir);
|
|
130
|
+
const fix = 'Pin an SDK the bootstrapped toolchain can link in configs/darwin.mozconfig ' +
|
|
131
|
+
'(ac_add_options --with-macos-sdk=<path>), then run fireforge build.';
|
|
132
|
+
if ('error' in selected) {
|
|
133
|
+
return warning(CHECK_NAME, selected.error, fix);
|
|
134
|
+
}
|
|
135
|
+
let failure;
|
|
136
|
+
try {
|
|
137
|
+
failure = await probeSdkLink(bootstrappedClangPath(), selected.sdk);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
failure = toError(error).message;
|
|
141
|
+
}
|
|
142
|
+
if (failure === undefined) {
|
|
143
|
+
return ok(CHECK_NAME, `OK (${selected.sdk}, from ${selected.source})`);
|
|
144
|
+
}
|
|
145
|
+
const installed = await listInstalledSdks();
|
|
146
|
+
return warning(CHECK_NAME, `The bootstrapped clang cannot link against ${selected.sdk} (from ${selected.source}): ${failure}. ` +
|
|
147
|
+
`mach configure fails at its linker probe the same way. Installed SDKs: ` +
|
|
148
|
+
(installed.length > 0 ? installed.join(', ') : 'none found') +
|
|
149
|
+
'.', fix);
|
|
150
|
+
}
|
|
151
|
+
/** Registry entry for the doctor runner. */
|
|
152
|
+
export const MACOS_SDK_LINK_DOCTOR_CHECK = {
|
|
153
|
+
name: CHECK_NAME,
|
|
154
|
+
skipIf: (ctx) => process.platform !== 'darwin' || !ctx.engineExists || !existsSync(bootstrappedClangPath()),
|
|
155
|
+
run: (ctx) => checkMacosSdkLink(ctx.paths.engine),
|
|
156
|
+
};
|
|
@@ -17,6 +17,7 @@ import { findExecutable } from '../utils/process.js';
|
|
|
17
17
|
import { failure, ok, warning } from './doctor-check-core.js';
|
|
18
18
|
import { EXTERNAL_TOOLCHAIN_DOCTOR_CHECK } from './doctor-external-toolchains.js';
|
|
19
19
|
import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
|
|
20
|
+
import { MACOS_SDK_LINK_DOCTOR_CHECK } from './doctor-macos-sdk.js';
|
|
20
21
|
import { ORPHANED_HARNESS_DOCTOR_CHECK } from './doctor-orphaned-harness.js';
|
|
21
22
|
import { PATCH_MANIFEST_CONSISTENCY_CHECK } from './doctor-patch-manifest.js';
|
|
22
23
|
import { POST_REBASE_AUDIT_CHECK } from './doctor-post-rebase-audit.js';
|
|
@@ -258,6 +259,7 @@ const DOCTOR_CHECKS = [
|
|
|
258
259
|
},
|
|
259
260
|
},
|
|
260
261
|
ORPHANED_HARNESS_DOCTOR_CHECK,
|
|
262
|
+
MACOS_SDK_LINK_DOCTOR_CHECK,
|
|
261
263
|
{
|
|
262
264
|
name: 'Patches directory exists',
|
|
263
265
|
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
|
}));
|
|
@@ -30,13 +30,16 @@ function isSafeIoFallback(error) {
|
|
|
30
30
|
const code = getNodeErrorCode(error);
|
|
31
31
|
return code !== undefined && SAFE_IO_FALLBACK_CODES.has(code);
|
|
32
32
|
}
|
|
33
|
-
/** Concurrency bound for per-file classification (
|
|
33
|
+
/** Concurrency bound for per-file classification (reads and patch replays). */
|
|
34
34
|
const UNMANAGED_CLASSIFY_CONCURRENCY = 8;
|
|
35
35
|
async function getUnmanagedDirtyFiles(engineDir, patchesDir, dirtyFiles) {
|
|
36
36
|
// One manifest+patch-discovery load for the whole batch (the per-call
|
|
37
37
|
// computePatchedContent re-read everything for every file), and bounded
|
|
38
38
|
// concurrency instead of an unbounded Promise.all over git spawns.
|
|
39
|
-
const { computePatched: computeExpected } = await createPatchedContentContext(patchesDir, engineDir);
|
|
39
|
+
const { computePatched: computeExpected, prefetchBase } = await createPatchedContentContext(patchesDir, engineDir);
|
|
40
|
+
// One `git cat-file --batch` for every HEAD blob instead of a `git show`
|
|
41
|
+
// per dirty file.
|
|
42
|
+
await prefetchBase(dirtyFiles);
|
|
40
43
|
const classifications = await mapWithConcurrency(dirtyFiles, UNMANAGED_CLASSIFY_CONCURRENCY, async (file) => {
|
|
41
44
|
try {
|
|
42
45
|
const [expected, exists] = await Promise.all([
|
|
@@ -138,7 +138,7 @@ const SUPPRESSED_SIZE_CHECKS = new Set([
|
|
|
138
138
|
* filename-prefixed issue rows, and the cache writes, all in the same
|
|
139
139
|
* sequence a serial run produced. Returns the run tallies.
|
|
140
140
|
*/
|
|
141
|
-
async function applyPerPatchResults(subset, results, issues, checkJs, cache) {
|
|
141
|
+
async function applyPerPatchResults(subset, results, issues, checkJs, cache, heldNotices) {
|
|
142
142
|
const totals = {
|
|
143
143
|
linted: 0,
|
|
144
144
|
skipped: 0,
|
|
@@ -190,6 +190,10 @@ async function applyPerPatchResults(subset, results, issues, checkJs, cache) {
|
|
|
190
190
|
for (const suppressedIssue of result.suppressedIssues) {
|
|
191
191
|
if (!SUPPRESSED_SIZE_CHECKS.has(suppressedIssue.check))
|
|
192
192
|
continue;
|
|
193
|
+
if (heldNotices !== undefined) {
|
|
194
|
+
heldNotices.push({ check: suppressedIssue.check, patch: patch.filename, suppressed: true });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
193
197
|
info(`NOTICE [${suppressedIssue.check}] ${patch.filename}: suppressed by lintIgnore — ${suppressedIssue.message}`);
|
|
194
198
|
}
|
|
195
199
|
totals.suppressed += result.suppressedIssues.length;
|
|
@@ -232,7 +236,10 @@ async function lintSubsetConcurrently(subset, lintCtx) {
|
|
|
232
236
|
* prints the per-patch summary, and applies the failure criteria
|
|
233
237
|
* (errors, `--max-warnings`) by throwing GeneralError.
|
|
234
238
|
*/
|
|
235
|
-
function reportPerPatchOutcome(issues, linted, skipped, options) {
|
|
239
|
+
function reportPerPatchOutcome(issues, linted, skipped, options, heldNotices) {
|
|
240
|
+
if (heldNotices !== undefined) {
|
|
241
|
+
reportNoticeSummary(heldNotices, issues);
|
|
242
|
+
}
|
|
236
243
|
if (issues.length === 0) {
|
|
237
244
|
if (linted === 0 && skipped > 0) {
|
|
238
245
|
info(`No patches in the queue have been applied to engine/. Run "fireforge import" first if you want lint findings against the staged hunks; otherwise this is expected.`);
|
|
@@ -247,8 +254,10 @@ function reportPerPatchOutcome(issues, linted, skipped, options) {
|
|
|
247
254
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
248
255
|
const warnings = issues.filter((i) => i.severity === 'warning');
|
|
249
256
|
const notices = issues.filter((i) => i.severity === 'notice');
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
if (heldNotices === undefined) {
|
|
258
|
+
for (const issue of notices)
|
|
259
|
+
info(`NOTICE ${formatPatchLintIssue(issue)}`);
|
|
260
|
+
}
|
|
252
261
|
for (const issue of warnings)
|
|
253
262
|
warn(formatPatchLintIssue(issue));
|
|
254
263
|
for (const issue of errors)
|
|
@@ -272,6 +281,34 @@ function reportPerPatchOutcome(issues, linted, skipped, options) {
|
|
|
272
281
|
outro('Lint passed');
|
|
273
282
|
}
|
|
274
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* `--notices summary`: one line per check instead of one per finding, for
|
|
286
|
+
* a green queue whose hundreds of NOTICE lines are reviewed waivers and
|
|
287
|
+
* under-threshold measurements. The full list stays in `--report`.
|
|
288
|
+
* Errors and warnings are never summarised.
|
|
289
|
+
*/
|
|
290
|
+
function reportNoticeSummary(heldNotices, issues) {
|
|
291
|
+
const byCheck = new Map();
|
|
292
|
+
const tally = (check, patch, suppressed) => {
|
|
293
|
+
const entry = byCheck.get(check) ?? { count: 0, suppressed: 0, patches: new Set() };
|
|
294
|
+
entry.count += 1;
|
|
295
|
+
if (suppressed)
|
|
296
|
+
entry.suppressed += 1;
|
|
297
|
+
entry.patches.add(patch);
|
|
298
|
+
byCheck.set(check, entry);
|
|
299
|
+
};
|
|
300
|
+
for (const held of heldNotices)
|
|
301
|
+
tally(held.check, held.patch, held.suppressed);
|
|
302
|
+
for (const issue of issues) {
|
|
303
|
+
if (issue.severity !== 'notice')
|
|
304
|
+
continue;
|
|
305
|
+
tally(issue.check, issue.patches?.[0] ?? issue.file.split(' :: ')[0] ?? issue.file, false);
|
|
306
|
+
}
|
|
307
|
+
for (const [check, entry] of [...byCheck].sort(([a], [b]) => a.localeCompare(b))) {
|
|
308
|
+
const suppressed = entry.suppressed > 0 ? `, ${entry.suppressed} of them suppressed by lintIgnore` : '';
|
|
309
|
+
info(`NOTICE [${check}]: ${entry.count} across ${entry.patches.size} patch(es)${suppressed} (see --report)`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
275
312
|
/**
|
|
276
313
|
* Resolves the `--patches <name…>` subset filter against the manifest,
|
|
277
314
|
* matching each requested name tolerantly (exact filename, filename ±
|
|
@@ -394,7 +431,8 @@ async function lintPerPatchInner(projectRoot, paths, options = {}) {
|
|
|
394
431
|
engineHeadSha,
|
|
395
432
|
checkJs,
|
|
396
433
|
});
|
|
397
|
-
const
|
|
434
|
+
const heldNotices = options.notices === 'summary' ? [] : undefined;
|
|
435
|
+
const { linted, skipped, cacheDirty, reusedCacheEntries, suppressed } = await applyPerPatchResults(subset, results, issues, checkJs, cache, heldNotices);
|
|
398
436
|
for (const issue of lintPatchQueue(ctx)) {
|
|
399
437
|
if (isSubset && !subsetTouchedFiles.has(issue.file))
|
|
400
438
|
continue;
|
|
@@ -411,5 +449,5 @@ async function lintPerPatchInner(projectRoot, paths, options = {}) {
|
|
|
411
449
|
if (options.report !== undefined) {
|
|
412
450
|
await writePerPatchLintReport(options.report, subset, results);
|
|
413
451
|
}
|
|
414
|
-
reportPerPatchOutcome(issues, linted, skipped, options);
|
|
452
|
+
reportPerPatchOutcome(issues, linted, skipped, options, heldNotices);
|
|
415
453
|
}
|
|
@@ -52,6 +52,9 @@ export interface PerRunCheckJs {
|
|
|
52
52
|
/** True when this file kind participates in the run's checkJs surface
|
|
53
53
|
* (`.sys.mjs`, plus test scripts when `checkJsTestFiles` is on). */
|
|
54
54
|
isRelevant: (file: string) => boolean;
|
|
55
|
+
/** The program's roots under `rootScopePatches`, or undefined for the
|
|
56
|
+
* whole queue. A file outside it is resolvable but never checked. */
|
|
57
|
+
rootScope: ReadonlySet<string> | undefined;
|
|
55
58
|
}
|
|
56
59
|
/**
|
|
57
60
|
* Builds the per-run checkJs program controller when `patchLint.checkJs` is
|
|
@@ -236,6 +236,9 @@ function validateLintFlags(options) {
|
|
|
236
236
|
if (options.report !== undefined && !options.perPatch) {
|
|
237
237
|
throw new InvalidArgumentError('--report requires --per-patch.', '--report');
|
|
238
238
|
}
|
|
239
|
+
if (options.notices !== undefined && !options.perPatch) {
|
|
240
|
+
throw new InvalidArgumentError('--notices requires --per-patch.', '--notices');
|
|
241
|
+
}
|
|
239
242
|
}
|
|
240
243
|
/**
|
|
241
244
|
* Aggregate-mode patch-size softening: when the linted diff is every
|
|
@@ -499,6 +502,7 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
499
502
|
.option('--max-warnings <n>', 'Fail when lint reports more than <n> warning(s); use 0 for warning-clean release gates.')
|
|
500
503
|
.option('--no-cache', 'Bypass per-patch lint result cache reads and writes.')
|
|
501
504
|
.option('--report <path>', 'With --per-patch, write a machine-readable JSON report (per-patch size metrics, tier, thresholds, issues, and lintIgnore-suppressed issues) to <path>.')
|
|
505
|
+
.option('--notices <mode>', 'With --per-patch: "full" (default) prints every NOTICE line; "summary" prints one line per check with its count and patch count. The full list stays in --report. Errors and warnings always print in full.')
|
|
502
506
|
.action(withErrorHandling(async (paths, options) => {
|
|
503
507
|
const lintOptions = {};
|
|
504
508
|
if (options.since !== undefined) {
|
|
@@ -526,6 +530,12 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
526
530
|
if (options.report !== undefined) {
|
|
527
531
|
lintOptions.report = options.report;
|
|
528
532
|
}
|
|
533
|
+
if (options.notices !== undefined) {
|
|
534
|
+
if (options.notices !== 'summary' && options.notices !== 'full') {
|
|
535
|
+
throw new InvalidArgumentError(`--notices must be "summary" or "full" (got "${options.notices}").`, '--notices');
|
|
536
|
+
}
|
|
537
|
+
lintOptions.notices = options.notices;
|
|
538
|
+
}
|
|
529
539
|
await lintCommand(getProjectRoot(), paths, lintOptions);
|
|
530
540
|
}));
|
|
531
541
|
const lintCache = lint
|
|
@@ -44,8 +44,23 @@ export interface ReExportLintContext {
|
|
|
44
44
|
/** The pre-existing-queue-errors note is printed once per invocation. */
|
|
45
45
|
queueBaselineWarned: boolean;
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Builds the once-per-invocation lint context.
|
|
49
|
+
*
|
|
50
|
+
* The checkJs program is rooted at the patches being re-exported, exactly as
|
|
51
|
+
* `lint --per-patch --patches` roots it at its subset: the rest of the
|
|
52
|
+
* queue stays resolvable, so cross-patch imports type-check identically,
|
|
53
|
+
* but nothing else is parsed or checked. Unscoped, a one-patch re-export
|
|
54
|
+
* that missed the lint cache type-checked the whole queue (76 s on a
|
|
55
|
+
* 401-patch queue) to report on one patch.
|
|
56
|
+
*
|
|
57
|
+
* @param projectRoot - FireForge project root
|
|
58
|
+
* @param paths - Resolved project paths
|
|
59
|
+
* @param config - Loaded configuration
|
|
60
|
+
* @param noCache - Skip the per-patch lint result cache (`--no-cache`)
|
|
61
|
+
* @param scopePatches - Filenames of the patches this run re-exports
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildReExportLintContext(projectRoot: string, paths: ProjectPaths, config: FireForgeConfig, noCache: boolean, scopePatches: ReadonlySet<string>): Promise<ReExportLintContext>;
|
|
49
64
|
/** Fresh lint payload carried to the post-write cache store. */
|
|
50
65
|
export interface ReExportLintResult {
|
|
51
66
|
issues: PatchLintIssue[];
|
|
@@ -32,15 +32,39 @@ import { reportPatchLintOutcome } from './export-shared.js';
|
|
|
32
32
|
import { buildPerRunCheckJs } from './lint-per-run-checkjs.js';
|
|
33
33
|
import { projectEntryBody } from './patch/entry-projection.js';
|
|
34
34
|
import { formatForwardImportRemedy } from './re-export-scan.js';
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Builds the once-per-invocation lint context.
|
|
37
|
+
*
|
|
38
|
+
* The checkJs program is rooted at the patches being re-exported, exactly as
|
|
39
|
+
* `lint --per-patch --patches` roots it at its subset: the rest of the
|
|
40
|
+
* queue stays resolvable, so cross-patch imports type-check identically,
|
|
41
|
+
* but nothing else is parsed or checked. Unscoped, a one-patch re-export
|
|
42
|
+
* that missed the lint cache type-checked the whole queue (76 s on a
|
|
43
|
+
* 401-patch queue) to report on one patch.
|
|
44
|
+
*
|
|
45
|
+
* @param projectRoot - FireForge project root
|
|
46
|
+
* @param paths - Resolved project paths
|
|
47
|
+
* @param config - Loaded configuration
|
|
48
|
+
* @param noCache - Skip the per-patch lint result cache (`--no-cache`)
|
|
49
|
+
* @param scopePatches - Filenames of the patches this run re-exports
|
|
50
|
+
*/
|
|
51
|
+
export async function buildReExportLintContext(projectRoot, paths, config, noCache, scopePatches) {
|
|
37
52
|
const patchQueueCtx = (await pathExists(paths.patches))
|
|
38
53
|
? await buildPatchQueueContext(paths.patches, config)
|
|
39
54
|
: undefined;
|
|
40
55
|
const checkJs = patchQueueCtx
|
|
41
|
-
? buildPerRunCheckJs(projectRoot, paths, config, patchQueueCtx)
|
|
56
|
+
? buildPerRunCheckJs(projectRoot, paths, config, patchQueueCtx, scopePatches)
|
|
42
57
|
: undefined;
|
|
43
58
|
const cache = noCache ? undefined : await loadPerPatchLintCache(projectRoot);
|
|
59
|
+
if (patchQueueCtx) {
|
|
60
|
+
// The queue rules and the projection below read every patch, so the
|
|
61
|
+
// wait is named rather than left behind one spinner line.
|
|
62
|
+
info(`Lint scope: queue rules over all ${patchQueueCtx.entries.length} patch(es); ` +
|
|
63
|
+
`checkJs rooted at the ${scopePatches.size} re-exported patch(es); ` +
|
|
64
|
+
(cache === undefined
|
|
65
|
+
? 'lint cache off (--no-cache).'
|
|
66
|
+
: `lint cache ${Object.keys(cache.entries).length === 0 ? 'empty' : 'loaded'}.`));
|
|
67
|
+
}
|
|
44
68
|
const engineHeadSha = cache ? await getPerPatchLintCacheHeadSha(paths.engine) : undefined;
|
|
45
69
|
return {
|
|
46
70
|
projectRoot,
|
|
@@ -123,7 +147,12 @@ export async function lintReExportedPatch(args) {
|
|
|
123
147
|
lintOptions.onSuppressed = (suppressed) => suppressedIssues.push(...suppressed);
|
|
124
148
|
if (checkJs) {
|
|
125
149
|
const created = checkJsFilesInDiff(diffContent, checkJs);
|
|
126
|
-
|
|
150
|
+
if (created.size > 0) {
|
|
151
|
+
info(`checkJs: type-checking ${created.size} file(s) of ${patch.filename} (lint cache ${cache ? 'miss' : 'off'}).`);
|
|
152
|
+
}
|
|
153
|
+
// A file the scoped program does not root at would be resolvable but
|
|
154
|
+
// never checked, so it takes the per-patch build too.
|
|
155
|
+
const allKnown = [...created].every((f) => checkJs.resolutionSet.has(f) && (checkJs.rootScope?.has(f) ?? true));
|
|
127
156
|
if (allKnown) {
|
|
128
157
|
// Slice this patch's findings out of the one queue-wide program.
|
|
129
158
|
lintOptions.precomputedCheckJs = await checkJs.sliceFor(created);
|