@hominis/fireforge 0.40.0 → 0.41.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 +79 -0
- package/README.md +16 -10
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +20 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +16 -7
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +12 -59
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint.js +4 -4
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +151 -50
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +10 -0
- package/dist/src/commands/re-export-adjacent.js +5 -6
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +8 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +113 -268
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +11 -21
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +13 -0
- package/dist/src/commands/status-check.js +31 -13
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +184 -175
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +41 -4
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/tree.d.ts +3 -0
- package/dist/src/commands/tree.js +171 -25
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +34 -36
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +1 -1
- package/dist/src/core/lint-cache.js +33 -10
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.js +3 -5
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
- package/dist/src/core/patch-lint-checkjs.js +78 -7
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-reexports.d.ts +1 -0
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +8 -0
- package/dist/src/core/patch-lint.js +84 -6
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +0 -8
- package/dist/src/core/smoke-patterns.js +3 -2
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +6 -1
- package/dist/src/core/status-classify.js +101 -73
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-guard.d.ts +1 -1
- package/dist/src/core/tree-guard.js +67 -12
- package/dist/src/core/tree-store.d.ts +86 -10
- package/dist/src/core/tree-store.js +310 -72
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/types/commands/options.d.ts +86 -1
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +23 -2
- package/dist/src/utils/validation.js +29 -3
- package/package.json +9 -9
|
@@ -23,13 +23,11 @@
|
|
|
23
23
|
* plugin, etc.) can legitimately have a fresh `dist/` with no
|
|
24
24
|
* FireForge-recorded baseline update.
|
|
25
25
|
*/
|
|
26
|
-
import { createHash } from 'node:crypto';
|
|
27
|
-
import { readFile } from 'node:fs/promises';
|
|
28
|
-
import { join } from 'node:path';
|
|
29
26
|
import { toError } from '../utils/errors.js';
|
|
30
27
|
import { verbose, warn } from '../utils/logger.js';
|
|
31
28
|
import { isPackageablePath, isXpcomManifestPath } from './build-audit.js';
|
|
32
29
|
import { readBuildBaseline } from './build-baseline.js';
|
|
30
|
+
import { hashEngineFile } from './coverage-extend.js';
|
|
33
31
|
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
34
32
|
export { isXpcomManifestPath };
|
|
35
33
|
/** Cap on the number of changed paths rendered inline. */
|
|
@@ -83,23 +81,6 @@ export async function checkStaleBuildForTest(projectRoot, engineDir) {
|
|
|
83
81
|
const truncated = Math.max(0, packageable.length - head.length);
|
|
84
82
|
return { stale: true, changedPaths: head, truncated, baseline };
|
|
85
83
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Reads a file under the engine directory and returns a hex-encoded
|
|
88
|
-
* SHA-256 of its contents, matching the hash the baseline writer
|
|
89
|
-
* produces. Returns `undefined` on any IO error (missing file,
|
|
90
|
-
* permission denied, etc.) so the caller can treat the path as still
|
|
91
|
-
* stale rather than crashing the preflight.
|
|
92
|
-
*/
|
|
93
|
-
async function hashEngineFile(engineDir, relPath) {
|
|
94
|
-
try {
|
|
95
|
-
const buffer = await readFile(join(engineDir, relPath));
|
|
96
|
-
return createHash('sha256').update(buffer).digest('hex');
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
verbose(`Stale-build preflight: could not hash ${relPath} for baseline comparison — ${toError(error).message}`);
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
84
|
/**
|
|
104
85
|
* Formats a human-readable warning body from a {@link StaleBuildResult}.
|
|
105
86
|
* Kept separate from the probe so test code can assert on the structured
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { execFile } from 'node:child_process';
|
|
31
31
|
import { homedir } from 'node:os';
|
|
32
32
|
import { join } from 'node:path';
|
|
33
|
+
import { toError } from '../utils/errors.js';
|
|
33
34
|
import { pathExists, readText } from '../utils/fs.js';
|
|
34
35
|
import { verbose } from '../utils/logger.js';
|
|
35
36
|
/**
|
|
@@ -133,28 +134,29 @@ export function formatMajorVersionHopNotice(previousVersion, newVersion) {
|
|
|
133
134
|
* yields undefined for that tool — never an error.
|
|
134
135
|
*/
|
|
135
136
|
export async function readDeclaredToolchainMinimums(engineDir) {
|
|
136
|
-
const
|
|
137
|
-
|
|
137
|
+
const entries = Object.entries(MINIMUM_DECLARATIONS);
|
|
138
|
+
const resolved = await Promise.all(entries.map(async ([tool, declaration]) => {
|
|
138
139
|
const filePath = join(engineDir, declaration.relPath);
|
|
139
140
|
if (!(await pathExists(filePath))) {
|
|
140
141
|
verbose(`Toolchain preflight: ${declaration.relPath} not found; skipping ${tool}.`);
|
|
141
|
-
|
|
142
|
+
return undefined;
|
|
142
143
|
}
|
|
143
144
|
try {
|
|
144
145
|
const content = await readText(filePath);
|
|
145
146
|
const match = declaration.pattern.exec(content);
|
|
146
147
|
if (match?.[1]) {
|
|
147
|
-
|
|
148
|
+
return [tool, match[1]];
|
|
148
149
|
}
|
|
149
150
|
else {
|
|
150
151
|
verbose(`Toolchain preflight: no ${tool} minimum declaration recognized in ${declaration.relPath}; skipping.`);
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
catch (error) {
|
|
154
|
-
verbose(`Toolchain preflight: could not read ${declaration.relPath} (${error
|
|
155
|
+
verbose(`Toolchain preflight: could not read ${declaration.relPath} (${toError(error).message}); skipping ${tool}.`);
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
return undefined;
|
|
158
|
+
}));
|
|
159
|
+
return Object.fromEntries(resolved.filter((entry) => entry !== undefined));
|
|
158
160
|
}
|
|
159
161
|
/** Runs `<binary> --version` and parses the leading version number. */
|
|
160
162
|
async function probeBinaryVersion(binary, versionPattern) {
|
|
@@ -185,17 +187,14 @@ async function probeHostToolCandidates(tool) {
|
|
|
185
187
|
const binaries = envOverride
|
|
186
188
|
? [envOverride]
|
|
187
189
|
: [...probe.stateDirRelPaths.map((rel) => join(mozbuildStateDir(), rel)), tool];
|
|
188
|
-
const candidates =
|
|
189
|
-
for (const binary of binaries) {
|
|
190
|
+
const candidates = await Promise.all(binaries.map(async (binary) => {
|
|
190
191
|
const version = await probeBinaryVersion(binary, probe.versionPattern);
|
|
191
|
-
if (version !== undefined)
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
return candidates;
|
|
192
|
+
if (version !== undefined)
|
|
193
|
+
return { binary, version };
|
|
194
|
+
verbose(`Toolchain preflight: "${binary} --version" failed or not found; skipping.`);
|
|
195
|
+
return undefined;
|
|
196
|
+
}));
|
|
197
|
+
return candidates.filter((candidate) => candidate !== undefined);
|
|
199
198
|
}
|
|
200
199
|
/**
|
|
201
200
|
* Compares the tree-declared toolchain minimums against the host binaries
|
|
@@ -210,31 +209,30 @@ async function probeHostToolCandidates(tool) {
|
|
|
210
209
|
*/
|
|
211
210
|
export async function runToolchainPreflight(engineDir) {
|
|
212
211
|
const minimums = await readDeclaredToolchainMinimums(engineDir);
|
|
213
|
-
const
|
|
214
|
-
for (const [tool, minimumVersion] of Object.entries(minimums)) {
|
|
212
|
+
const results = await Promise.all(Object.entries(minimums).map(async ([tool, minimumVersion]) => {
|
|
215
213
|
const minimum = parseVersionComponents(minimumVersion);
|
|
216
214
|
if (!minimum)
|
|
217
|
-
|
|
215
|
+
return undefined;
|
|
218
216
|
const candidates = await probeHostToolCandidates(tool);
|
|
219
217
|
const parsed = candidates.filter((c) => parseVersionComponents(c.version) !== undefined);
|
|
220
218
|
if (parsed.length === 0)
|
|
221
|
-
|
|
219
|
+
return undefined;
|
|
222
220
|
const satisfying = parsed.find((c) => {
|
|
223
221
|
const components = parseVersionComponents(c.version);
|
|
224
222
|
return components !== undefined && !isVersionLower(components, minimum);
|
|
225
223
|
});
|
|
226
224
|
if (satisfying) {
|
|
227
225
|
verbose(`Toolchain preflight: ${tool} ${satisfying.version} (${satisfying.binary}) satisfies minimum ${minimumVersion}.`);
|
|
228
|
-
|
|
226
|
+
return undefined;
|
|
229
227
|
}
|
|
230
|
-
|
|
228
|
+
return {
|
|
231
229
|
tool,
|
|
232
230
|
minimumVersion,
|
|
233
231
|
declaredIn: MINIMUM_DECLARATIONS[tool].relPath,
|
|
234
232
|
candidates: parsed,
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return
|
|
233
|
+
};
|
|
234
|
+
}));
|
|
235
|
+
return results.filter((mismatch) => mismatch !== undefined);
|
|
238
236
|
}
|
|
239
237
|
/**
|
|
240
238
|
* Formats the fail-fast message for definitive preflight mismatches,
|
|
@@ -2,7 +2,7 @@ import { type TreeMarker } from './tree-store.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Per-command verdicts. `allowed` runs unconditionally; `refused` never
|
|
4
4
|
* runs in a tree; `conditional` consults `TREE_CONDITIONAL_CHECKS` with
|
|
5
|
-
* the invoked subcommand chain and
|
|
5
|
+
* the invoked subcommand chain, parsed options, and the tree marker.
|
|
6
6
|
*/
|
|
7
7
|
export declare const TREE_COMMAND_VERDICTS: Readonly<Record<string, 'allowed' | 'refused' | 'conditional'>>;
|
|
8
8
|
interface ConditionalInput {
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
import { existsSync } from 'node:fs';
|
|
16
16
|
import { dirname, join, resolve } from 'node:path';
|
|
17
17
|
import { GeneralError } from '../errors/base.js';
|
|
18
|
-
import {
|
|
18
|
+
import { getTreeMarkerPath, readTreeMarkerState, } from './tree-store.js';
|
|
19
19
|
/**
|
|
20
20
|
* Per-command verdicts. `allowed` runs unconditionally; `refused` never
|
|
21
21
|
* runs in a tree; `conditional` consults `TREE_CONDITIONAL_CHECKS` with
|
|
22
|
-
* the invoked subcommand chain and
|
|
22
|
+
* the invoked subcommand chain, parsed options, and the tree marker.
|
|
23
23
|
*/
|
|
24
24
|
export const TREE_COMMAND_VERDICTS = {
|
|
25
25
|
setup: 'refused',
|
|
@@ -35,13 +35,14 @@ export const TREE_COMMAND_VERDICTS = {
|
|
|
35
35
|
discard: 'refused',
|
|
36
36
|
export: 'conditional',
|
|
37
37
|
'export-all': 'conditional',
|
|
38
|
-
're-export': '
|
|
38
|
+
're-export': 'conditional',
|
|
39
39
|
patch: 'refused',
|
|
40
40
|
rebase: 'refused',
|
|
41
41
|
package: 'refused',
|
|
42
42
|
watch: 'refused',
|
|
43
|
-
//
|
|
44
|
-
|
|
43
|
+
// Build-less runs only, and only in trees whose marker records a cloned
|
|
44
|
+
// (mozinfo-rewritten) objdir — see the conditional check below.
|
|
45
|
+
test: 'conditional',
|
|
45
46
|
config: 'conditional',
|
|
46
47
|
doctor: 'conditional',
|
|
47
48
|
register: 'refused',
|
|
@@ -57,18 +58,44 @@ export const TREE_COMMAND_VERDICTS = {
|
|
|
57
58
|
};
|
|
58
59
|
/** The command set named in refusal messages. */
|
|
59
60
|
const ALLOWED_SUMMARY = 'Verification trees support: status, lint, typecheck, verify, doctor (read-only), ' +
|
|
60
|
-
'config (read), export --dry-run,
|
|
61
|
+
'config (read), export --dry-run, export-all --dry-run, re-export --dry-run, and — ' +
|
|
62
|
+
'in trees created with --with-objdir — build-less test.';
|
|
61
63
|
const TREE_CONDITIONAL_CHECKS = {
|
|
62
64
|
// Exports write patches/ + patches.json; only the dry-run preview is a read.
|
|
63
65
|
export: ({ options }) => options['dryRun'] === true,
|
|
64
66
|
'export-all': ({ options }) => options['dryRun'] === true,
|
|
67
|
+
// Same rule for refreshing existing patches: the dry-run projection is a
|
|
68
|
+
// read (proven side-effect free and runtime-enforced by
|
|
69
|
+
// withDryRunPurityGuard — FORGE H1/H3); a real re-export writes patches/.
|
|
70
|
+
're-export': ({ options }) => options['dryRun'] === true,
|
|
65
71
|
// `config` with no positional value prints; with a value it writes.
|
|
66
72
|
config: ({ args }) => args.filter((a) => typeof a === 'string').length <= 1,
|
|
67
73
|
// Doctor reads unless a repair flag is set.
|
|
68
74
|
doctor: ({ options }) => options['repairFurnace'] !== true && options['fix'] !== true,
|
|
75
|
+
// Build-less test needs the objdir the marker records as cloned,
|
|
76
|
+
// mozinfo-rewritten AND reconfigured in-tree; `test --build` rebuilds
|
|
77
|
+
// the engine and stays primary-only (a tree is a snapshot with no
|
|
78
|
+
// merge-back model, so build outputs mutated in-tree go nowhere).
|
|
79
|
+
test: ({ options }, marker) => marker.clonedObjdir !== undefined && options['build'] !== true,
|
|
69
80
|
// No tree lifecycle inside a tree; `tree list` is the only read.
|
|
70
81
|
tree: ({ subcommands }) => subcommands[0] === 'list',
|
|
71
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* Command-specific refusal hints appended after the generic sentence —
|
|
85
|
+
* for verdicts where "mutates project or engine state" alone would
|
|
86
|
+
* misdiagnose the actual reason.
|
|
87
|
+
*/
|
|
88
|
+
const TREE_REFUSAL_HINTS = {
|
|
89
|
+
test: ({ options }, marker) => {
|
|
90
|
+
if (options['build'] === true) {
|
|
91
|
+
return '"test --build" rebuilds the engine and must run in the primary tree; run build-less "fireforge test" here instead.';
|
|
92
|
+
}
|
|
93
|
+
if (marker.clonedObjdir === undefined) {
|
|
94
|
+
return 'In-tree test requires a tree created with "fireforge tree create <name> --with-objdir".';
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
72
99
|
/**
|
|
73
100
|
* Throws when `commandName` (with its subcommand chain and options) must
|
|
74
101
|
* not run inside the tree described by `marker`. Exported for the
|
|
@@ -79,10 +106,12 @@ export function enforceTreeGuard(marker, commandName, input) {
|
|
|
79
106
|
if (verdict === 'allowed')
|
|
80
107
|
return;
|
|
81
108
|
const conditional = TREE_CONDITIONAL_CHECKS[commandName];
|
|
82
|
-
if (verdict === 'conditional' && conditional && conditional(input))
|
|
109
|
+
if (verdict === 'conditional' && conditional && conditional(input, marker))
|
|
83
110
|
return;
|
|
111
|
+
const hint = TREE_REFUSAL_HINTS[commandName]?.(input, marker);
|
|
84
112
|
throw new GeneralError(`This is a FireForge verification tree ("${marker.name}", created from ${marker.primaryRoot}). ` +
|
|
85
113
|
`"${[commandName, ...input.subcommands].join(' ')}" mutates project or engine state and must run in the primary tree. ` +
|
|
114
|
+
(hint === undefined ? '' : `${hint} `) +
|
|
86
115
|
ALLOWED_SUMMARY);
|
|
87
116
|
}
|
|
88
117
|
/**
|
|
@@ -101,11 +130,12 @@ export async function runTreeGuardHook(rootCommandName, actionCommand) {
|
|
|
101
130
|
const [commandName, ...subcommands] = chain;
|
|
102
131
|
if (commandName === undefined || commandName === rootCommandName)
|
|
103
132
|
return;
|
|
104
|
-
const
|
|
133
|
+
const options = actionCommand.optsWithGlobals();
|
|
134
|
+
const located = await (async () => {
|
|
105
135
|
let current = resolve(process.cwd());
|
|
106
136
|
for (let depth = 0; depth < 50; depth++) {
|
|
107
137
|
if (existsSync(join(current, 'fireforge.json'))) {
|
|
108
|
-
return
|
|
138
|
+
return { root: current, state: await readTreeMarkerState(current) };
|
|
109
139
|
}
|
|
110
140
|
const parent = dirname(current);
|
|
111
141
|
if (parent === current)
|
|
@@ -114,11 +144,36 @@ export async function runTreeGuardHook(rootCommandName, actionCommand) {
|
|
|
114
144
|
}
|
|
115
145
|
return undefined;
|
|
116
146
|
})();
|
|
117
|
-
if (
|
|
147
|
+
if (located === undefined)
|
|
118
148
|
return;
|
|
119
|
-
|
|
149
|
+
const state = located.state;
|
|
150
|
+
if (state.kind === 'absent')
|
|
151
|
+
return;
|
|
152
|
+
// A marker we cannot read is not evidence that this is not a tree — it is
|
|
153
|
+
// evidence that we cannot tell. Treating it as "not a tree" would grant the
|
|
154
|
+
// full mutating command set to a snapshot on the strength of a file that
|
|
155
|
+
// failed to parse, which is exactly backwards for a default-deny guard.
|
|
156
|
+
if (state.kind === 'corrupt') {
|
|
157
|
+
if (options['ignoreCorruptTreeMarker'] === true)
|
|
158
|
+
return;
|
|
159
|
+
// Unconditionally-allowed commands never consult marker fields — their
|
|
160
|
+
// verdict is the same in every tree — so an unreadable marker cannot
|
|
161
|
+
// change their answer. Letting them through keeps read-only diagnostics
|
|
162
|
+
// (status, lint) usable on the very tree the operator needs to inspect.
|
|
163
|
+
// 'conditional' verdicts stay refused: their predicates can write.
|
|
164
|
+
if (TREE_COMMAND_VERDICTS[commandName] === 'allowed')
|
|
165
|
+
return;
|
|
166
|
+
const markerPath = getTreeMarkerPath(located.root);
|
|
167
|
+
throw new GeneralError(`${markerPath} identifies this directory as a FireForge verification tree, but ${state.reason}. ` +
|
|
168
|
+
`Refusing "${[commandName, ...subcommands].join(' ')}" because it cannot be established ` +
|
|
169
|
+
'whether this is a snapshot or the primary tree.\n\n' +
|
|
170
|
+
'Recreate the tree (fireforge tree remove <name> && fireforge tree create <name>), or — if ' +
|
|
171
|
+
'this really is the primary tree — delete the stray marker. ' +
|
|
172
|
+
'Pass --ignore-corrupt-tree-marker to proceed anyway.');
|
|
173
|
+
}
|
|
174
|
+
enforceTreeGuard(state.marker, commandName, {
|
|
120
175
|
subcommands,
|
|
121
|
-
options
|
|
176
|
+
options,
|
|
122
177
|
args: actionCommand.args,
|
|
123
178
|
});
|
|
124
179
|
}
|
|
@@ -6,24 +6,87 @@ export interface TreeMarker {
|
|
|
6
6
|
primaryRoot: string;
|
|
7
7
|
createdAt: string;
|
|
8
8
|
engineHead: string | null;
|
|
9
|
+
/** Hash of the primary engine's tracked + untracked working-tree diff. */
|
|
10
|
+
engineFingerprint?: string | null;
|
|
9
11
|
patchesFingerprint: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Name of the obj-* directory cloned into this tree, written only after
|
|
14
|
+
* its mozinfo.json was successfully rewritten to the tree's paths AND the
|
|
15
|
+
* caller's in-tree reconfigure regenerated the remaining configure output.
|
|
16
|
+
* Its presence is what lets the guard admit build-less `test` in-tree
|
|
17
|
+
* (`tree-guard.ts`); absent on trees created without `--with-objdir`.
|
|
18
|
+
*/
|
|
19
|
+
clonedObjdir?: string;
|
|
10
20
|
}
|
|
11
21
|
/** One row of `tree list`. */
|
|
12
22
|
export interface TreeListEntry {
|
|
13
23
|
name: string;
|
|
14
24
|
path: string;
|
|
15
25
|
createdAt: string;
|
|
16
|
-
staleness: 'fresh' | 'stale (engine advanced)' | 'stale (patches changed)' | 'unknown';
|
|
26
|
+
staleness: 'fresh' | 'stale (engine advanced)' | 'stale (engine worktree changed)' | 'stale (patches changed)' | 'unknown';
|
|
17
27
|
}
|
|
18
28
|
/** Absolute trees directory for a primary root. */
|
|
19
29
|
export declare function getTreesDir(primaryRoot: string): string;
|
|
20
30
|
/** Serialises tree create/remove against each other (primary-side sidecar lock). */
|
|
21
31
|
export declare function withTreeLifecycleLock<T>(primaryRoot: string, operation: () => Promise<T>): Promise<T>;
|
|
22
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Outcome of reading a tree marker.
|
|
34
|
+
*
|
|
35
|
+
* The three states must stay distinct because they carry opposite safety
|
|
36
|
+
* meanings. `absent` means "this directory is not a tree", which is what lets
|
|
37
|
+
* every mutating command run. `corrupt` means "this directory claims to be a
|
|
38
|
+
* tree but we cannot read the claim" — collapsing it into `absent` (as a bare
|
|
39
|
+
* `undefined` return does) hands a snapshot the full mutating command set on
|
|
40
|
+
* the strength of a file we failed to parse, inverting `tree-guard.ts`'s
|
|
41
|
+
* default-deny design. Deleting one field from a marker was enough to run
|
|
42
|
+
* `reset` inside a clone.
|
|
43
|
+
*/
|
|
44
|
+
export type TreeMarkerRead = {
|
|
45
|
+
kind: 'absent';
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'valid';
|
|
48
|
+
marker: TreeMarker;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'corrupt';
|
|
51
|
+
reason: string;
|
|
52
|
+
};
|
|
53
|
+
/** Absolute path of a root's tree marker. */
|
|
54
|
+
export declare function getTreeMarkerPath(root: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Reads the tree marker for a project root, distinguishing "not a tree" from
|
|
57
|
+
* "a tree whose marker we could not read". Callers that make a safety decision
|
|
58
|
+
* (the guard hook, the no-nesting refusal) must use this rather than
|
|
59
|
+
* {@link readTreeMarker}.
|
|
60
|
+
*
|
|
61
|
+
* Only ENOENT/ENOTDIR mean `absent`: any other probe failure (EACCES on the
|
|
62
|
+
* `.fireforge` directory, EIO, …) reports `corrupt`, because "we cannot read
|
|
63
|
+
* the marker" is not evidence that there is no marker — a `pathExists`
|
|
64
|
+
* pre-check here classified a tree with an unreadable `.fireforge` as
|
|
65
|
+
* `absent` and handed it the full mutating command set.
|
|
66
|
+
*/
|
|
67
|
+
export declare function readTreeMarkerState(root: string): Promise<TreeMarkerRead>;
|
|
68
|
+
/**
|
|
69
|
+
* Reads the tree marker for a project root, or undefined when it is absent
|
|
70
|
+
* OR unreadable.
|
|
71
|
+
*
|
|
72
|
+
* For display and best-effort paths only (`tree list` renders an unreadable
|
|
73
|
+
* marker as staleness 'unknown'). Anything that decides whether a mutation may
|
|
74
|
+
* proceed must call {@link readTreeMarkerState} and handle `corrupt`.
|
|
75
|
+
*/
|
|
23
76
|
export declare function readTreeMarker(root: string): Promise<TreeMarker | undefined>;
|
|
24
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Refuses a build-less in-tree `test` when the objdir preflight found is not
|
|
79
|
+
* the one this tree's marker vouched for as rewritten-and-reconfigured. A
|
|
80
|
+
* mismatch means an objdir appeared in the tree through some path other than
|
|
81
|
+
* `tree create --with-objdir` (manual copy, partial restore), so nothing
|
|
82
|
+
* proves it was relocated — running against it could consult primary paths.
|
|
83
|
+
* No-op outside a tree; a corrupt marker is already refused by the guard.
|
|
84
|
+
*/
|
|
85
|
+
export declare function assertObjdirMatchesTreeMarker(projectRoot: string, objDir: string | undefined): Promise<void>;
|
|
86
|
+
/** Current primary-side snapshot identity, including dirty source and patch bodies. */
|
|
25
87
|
export declare function computePrimaryFingerprint(primaryRoot: string): Promise<{
|
|
26
88
|
engineHead: string | null;
|
|
89
|
+
engineFingerprint?: string | null;
|
|
27
90
|
patchesFingerprint: string | null;
|
|
28
91
|
}>;
|
|
29
92
|
/** Validates a tree name: one path segment, no traversal. */
|
|
@@ -31,10 +94,16 @@ export declare function assertValidTreeName(name: string): void;
|
|
|
31
94
|
/**
|
|
32
95
|
* Materialises the tree at `treeRoot` from `primaryRoot` using
|
|
33
96
|
* `capability` ('none' = plain copy, already gated on --force-copy by
|
|
34
|
-
* the caller). The engine
|
|
35
|
-
* directories
|
|
36
|
-
*
|
|
37
|
-
*
|
|
97
|
+
* the caller). The engine is cloned one TOP-LEVEL entry at a time so unwanted
|
|
98
|
+
* `obj-*` directories are never traversed or materialised. That distinction
|
|
99
|
+
* is substantial on Firefox trees: even CoW-cloning and deleting an objdir
|
|
100
|
+
* touches metadata for millions of build outputs. With `withObjdir` exactly
|
|
101
|
+
* that objdir is included — after {@link assertCloneSafeObjdir}
|
|
102
|
+
* refuses symlinked or engine-escaping objdirs before any copying —
|
|
103
|
+
* rewritten to the tree (see below), and
|
|
104
|
+
* reconfigured in-tree via the caller-supplied `reconfigure` hook. A
|
|
105
|
+
* fresh `.fireforge/` is created with only `state.json` (when present),
|
|
106
|
+
* the last-build baseline (withObjdir only), and the tree marker.
|
|
38
107
|
*/
|
|
39
108
|
export declare function cloneTree(args: {
|
|
40
109
|
primaryRoot: string;
|
|
@@ -42,12 +111,19 @@ export declare function cloneTree(args: {
|
|
|
42
111
|
name: string;
|
|
43
112
|
capability: CowCapability;
|
|
44
113
|
createdAt: string;
|
|
114
|
+
withObjdir?: {
|
|
115
|
+
objDir: string;
|
|
116
|
+
reconfigure: (treeEngineDir: string) => Promise<void>;
|
|
117
|
+
};
|
|
45
118
|
}): Promise<TreeMarker>;
|
|
46
119
|
/** Lists trees with staleness against the current primary state. */
|
|
47
120
|
export declare function listTrees(primaryRoot: string): Promise<TreeListEntry[]>;
|
|
48
121
|
/**
|
|
49
122
|
* Removes one tree. Refuses when a live process holds the tree's build
|
|
50
|
-
* or engine-session lock
|
|
51
|
-
*
|
|
123
|
+
* or engine-session lock — or when a lock directory exists whose ownership
|
|
124
|
+
* cannot be established, which `--force` overrides — and never deletes a path
|
|
125
|
+
* outside the trees directory (containment check before `rm -rf`).
|
|
52
126
|
*/
|
|
53
|
-
export declare function removeTree(primaryRoot: string, name: string
|
|
127
|
+
export declare function removeTree(primaryRoot: string, name: string, options?: {
|
|
128
|
+
force?: boolean;
|
|
129
|
+
}): Promise<void>;
|