@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
|
@@ -7,32 +7,105 @@
|
|
|
7
7
|
* Exports and every other mutation stay strictly serial in the primary —
|
|
8
8
|
* a tree's patches/components are snapshots with no merge model, and the
|
|
9
9
|
* tree guard (cli.ts preAction + core/tree-guard.ts) refuses mutating
|
|
10
|
-
* commands inside a tree.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* commands inside a tree. `create --with-objdir` additionally clones the
|
|
11
|
+
* primary's built obj-* directory, rewrites its mozinfo.json to the
|
|
12
|
+
* tree, and runs `mach configure` inside the tree so the remaining
|
|
13
|
+
* configure output (config.status, backend.mk, Makefile,
|
|
14
|
+
* config/autoconf.mk — the verified set) stops naming the primary
|
|
15
|
+
* (fail-closed — mach objdirs embed absolute source paths, and an
|
|
16
|
+
* unrelocated clone would silently operate against the primary), which
|
|
17
|
+
* is what admits build-less `fireforge test` inside that tree.
|
|
13
18
|
*/
|
|
14
19
|
import { spawn } from 'node:child_process';
|
|
15
20
|
import { join } from 'node:path';
|
|
16
21
|
import { Command } from 'commander';
|
|
17
22
|
import { withEngineSessionLock } from '../core/engine-session-lock.js';
|
|
23
|
+
import { hasBuildArtifacts, runMach, withBuildLock } from '../core/mach.js';
|
|
24
|
+
import { assertBuildArtifacts, findObjdirRelocationViolation, } from '../core/mach-build-artifacts.js';
|
|
18
25
|
import { detectCowSupport } from '../core/tree-cow.js';
|
|
19
26
|
import { cloneTree, listTrees, removeTree } from '../core/tree-store.js';
|
|
20
|
-
import { assertValidTreeName, computePrimaryFingerprint, getTreesDir,
|
|
27
|
+
import { assertValidTreeName, computePrimaryFingerprint, getTreeMarkerPath, getTreesDir, readTreeMarkerState, withTreeLifecycleLock, } from '../core/tree-store.js';
|
|
21
28
|
import { GeneralError } from '../errors/base.js';
|
|
29
|
+
import { BuildError } from '../errors/build.js';
|
|
22
30
|
import { pathExists } from '../utils/fs.js';
|
|
23
|
-
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
31
|
+
import { info, intro, outro, setMachineOutputMode, setStdoutSealed, success, warn, } from '../utils/logger.js';
|
|
32
|
+
import { addWaitLockOption, resolveWaitLockSeconds } from '../utils/options.js';
|
|
24
33
|
function assertPosix() {
|
|
25
34
|
if (process.platform === 'win32') {
|
|
26
35
|
throw new GeneralError('fireforge tree is POSIX-only; copy-on-write cloning relies on clonefile/reflink, which are not available on Windows.');
|
|
27
36
|
}
|
|
28
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the primary objdir a `--with-objdir` clone will keep,
|
|
40
|
+
* refusing missing, incomplete, ambiguous, or already-mismatched builds
|
|
41
|
+
* before any copying starts.
|
|
42
|
+
*/
|
|
43
|
+
async function resolveObjdirForClone(projectRoot) {
|
|
44
|
+
const engineDir = join(projectRoot, 'engine');
|
|
45
|
+
const buildCheck = await hasBuildArtifacts(engineDir);
|
|
46
|
+
assertBuildArtifacts(engineDir, buildCheck, {
|
|
47
|
+
label: 'tree create --with-objdir',
|
|
48
|
+
requirement: 'tree create --with-objdir requires a completed primary build.',
|
|
49
|
+
remediation: 'Run "fireforge build" first, or create the tree without --with-objdir.',
|
|
50
|
+
requireExisting: true,
|
|
51
|
+
});
|
|
52
|
+
if (buildCheck.objDir === undefined) {
|
|
53
|
+
throw new GeneralError('tree create --with-objdir could not determine the primary obj-* directory.');
|
|
54
|
+
}
|
|
55
|
+
return buildCheck.objDir;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Runs `mach configure` inside the tree's engine so the configure-generated
|
|
59
|
+
* root files (config.status, backend.mk, Makefile, config/autoconf.mk) are
|
|
60
|
+
* regenerated against the tree's paths — the mozinfo rewrite alone leaves
|
|
61
|
+
* those naming the primary, and a build-less in-tree `test` must never
|
|
62
|
+
* consult primary state. Exit code 0 is not trusted on its own: configure
|
|
63
|
+
* obeys MOZCONFIG / MOZ_OBJDIR, so the postcondition is verified afterwards
|
|
64
|
+
* via `findObjdirRelocationViolation` — the intended objdir was configured,
|
|
65
|
+
* its mozinfo points into the tree, and none of those root files still
|
|
66
|
+
* names the primary engine (nested Makefiles are products of the verified
|
|
67
|
+
* config.status; `.deps` build products are out of scope — see the
|
|
68
|
+
* checker's doc). A failure throws before `cloneTree` writes the marker,
|
|
69
|
+
* so the tree is removed and `clonedObjdir` is never recorded (fail-closed).
|
|
70
|
+
*/
|
|
71
|
+
async function reconfigureClonedObjdir(treeEngineDir, objDir, primaryEngineDir) {
|
|
72
|
+
info(' Running mach configure in the tree so no build metadata retains primary paths...');
|
|
73
|
+
let exitCode;
|
|
74
|
+
try {
|
|
75
|
+
exitCode = await runMach(['configure'], treeEngineDir);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new BuildError('mach configure failed in the cloned tree; the tree was removed. ' +
|
|
79
|
+
'Re-run tree create, or create the tree without --with-objdir.', 'mach configure', error instanceof Error ? error : undefined);
|
|
80
|
+
}
|
|
81
|
+
if (exitCode !== 0) {
|
|
82
|
+
throw new BuildError(`mach configure exited non-zero (${exitCode}) in the cloned tree; the tree was removed. ` +
|
|
83
|
+
'Re-run tree create, or create the tree without --with-objdir.', 'mach configure');
|
|
84
|
+
}
|
|
85
|
+
const violation = await findObjdirRelocationViolation({
|
|
86
|
+
engineDir: treeEngineDir,
|
|
87
|
+
objDir,
|
|
88
|
+
forbiddenDir: primaryEngineDir,
|
|
89
|
+
});
|
|
90
|
+
if (violation !== undefined) {
|
|
91
|
+
throw new BuildError(`mach configure exited 0 in the cloned tree but did not relocate the objdir: ${violation}. ` +
|
|
92
|
+
'The tree was removed. Re-run tree create, or create the tree without --with-objdir.', 'mach configure');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
29
95
|
/** Creates a verification tree under `.fireforge/trees/<name>`. */
|
|
30
96
|
export async function treeCreateCommand(projectRoot, name, options = {}) {
|
|
31
97
|
assertPosix();
|
|
32
98
|
intro('FireForge Tree Create');
|
|
33
99
|
assertValidTreeName(name);
|
|
34
|
-
|
|
35
|
-
|
|
100
|
+
// A marker we cannot parse still means "something claims this is a tree", so
|
|
101
|
+
// it must block nesting too — otherwise a corrupt marker is a licence to
|
|
102
|
+
// clone a snapshot into itself.
|
|
103
|
+
const markerState = await readTreeMarkerState(projectRoot);
|
|
104
|
+
if (markerState.kind !== 'absent') {
|
|
105
|
+
throw new GeneralError(markerState.kind === 'valid'
|
|
106
|
+
? 'Verification trees cannot be nested: run tree create in the primary tree.'
|
|
107
|
+
: `${getTreeMarkerPath(projectRoot)} claims this directory is a verification tree, but ` +
|
|
108
|
+
`${markerState.reason}. Repair or delete the marker before creating a tree here.`);
|
|
36
109
|
}
|
|
37
110
|
const treesDir = getTreesDir(projectRoot);
|
|
38
111
|
const treeRoot = join(treesDir, name);
|
|
@@ -51,28 +124,72 @@ export async function treeCreateCommand(projectRoot, name, options = {}) {
|
|
|
51
124
|
if (capability === 'none') {
|
|
52
125
|
warn('No copy-on-write support detected — performing a full physical copy (--force-copy).');
|
|
53
126
|
}
|
|
127
|
+
// Courtesy fast-fail: refuse obvious no-build cases before waiting on
|
|
128
|
+
// locks. The result is DISCARDED — a build can still start before the
|
|
129
|
+
// locks below are held, so only the re-resolution under the build lock
|
|
130
|
+
// is authoritative for what actually gets cloned.
|
|
131
|
+
if (options.withObjdir === true) {
|
|
132
|
+
await resolveObjdirForClone(projectRoot);
|
|
133
|
+
}
|
|
54
134
|
// Snapshot under the PRIMARY engine-session lock so a mid-mutation
|
|
55
|
-
// state (half-applied export, furnace deploy) is never captured
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
135
|
+
// state (half-applied export, furnace deploy) is never captured; an
|
|
136
|
+
// objdir snapshot additionally holds the primary BUILD lock, under
|
|
137
|
+
// which the objdir is re-validated immediately before cloning so a
|
|
138
|
+
// concurrent `fireforge build` can neither tear the obj-* mid-write
|
|
139
|
+
// nor swap it after preflight. A failed clone (including the
|
|
140
|
+
// fail-closed mozinfo-rewrite refusal and a failed in-tree
|
|
141
|
+
// reconfigure) removes the partial tree so the name is immediately
|
|
142
|
+
// reusable.
|
|
143
|
+
const marker = await withEngineSessionLock(projectRoot, 'tree create', async () => {
|
|
144
|
+
const clone = (objDir) => cloneTree({
|
|
145
|
+
primaryRoot: projectRoot,
|
|
146
|
+
treeRoot,
|
|
147
|
+
name,
|
|
148
|
+
capability,
|
|
149
|
+
createdAt: new Date().toISOString(),
|
|
150
|
+
...(objDir === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: {
|
|
153
|
+
withObjdir: {
|
|
154
|
+
objDir,
|
|
155
|
+
reconfigure: (treeEngineDir) => reconfigureClonedObjdir(treeEngineDir, objDir, join(projectRoot, 'engine')),
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
return options.withObjdir === true
|
|
161
|
+
? await withBuildLock(projectRoot, async () => clone(await resolveObjdirForClone(projectRoot)))
|
|
162
|
+
: await clone();
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
const { rm } = await import('node:fs/promises');
|
|
166
|
+
await rm(treeRoot, { recursive: true, force: true });
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}, { waitLockSeconds: options.waitLockSeconds });
|
|
63
170
|
success(`Created verification tree "${name}" at ${treeRoot}`);
|
|
64
171
|
info(` Engine HEAD: ${marker.engineHead ?? '(no engine)'}`);
|
|
65
|
-
info(' Read-only: status, lint, typecheck, verify, doctor, and export --dry-run run inside it; mutation commands are refused.');
|
|
172
|
+
info(' Read-only: status, lint, typecheck, verify, doctor, and export/re-export --dry-run run inside it; mutation commands are refused.');
|
|
173
|
+
if (marker.clonedObjdir !== undefined) {
|
|
174
|
+
info(` Build cloned: ${marker.clonedObjdir} was kept and rewritten to this tree — build-less "fireforge test" runs inside it.`);
|
|
175
|
+
info(' Browser tests share Marionette port 2828 with the primary; pass --marionette-port for concurrent runs.');
|
|
176
|
+
}
|
|
66
177
|
});
|
|
67
178
|
outro('Tree created');
|
|
68
179
|
}
|
|
69
180
|
/** Prints the tree list with staleness against the current primary state. */
|
|
70
181
|
export async function treeListCommand(projectRoot, options = {}) {
|
|
71
|
-
const trees = await listTrees(projectRoot);
|
|
72
182
|
if (options.json === true) {
|
|
183
|
+
// stdout belongs exclusively to the JSON payload: engage machine mode so
|
|
184
|
+
// any diagnostic — including a listTrees failure rendered later by
|
|
185
|
+
// withErrorHandling, which also resets the mode — routes to stderr.
|
|
186
|
+
setMachineOutputMode(true);
|
|
187
|
+
const trees = await listTrees(projectRoot);
|
|
73
188
|
process.stdout.write(`${JSON.stringify({ schemaVersion: 1, trees }, null, 2)}\n`);
|
|
189
|
+
setMachineOutputMode(false);
|
|
74
190
|
return;
|
|
75
191
|
}
|
|
192
|
+
const trees = await listTrees(projectRoot);
|
|
76
193
|
intro('FireForge Trees');
|
|
77
194
|
if (trees.length === 0) {
|
|
78
195
|
info('No verification trees. Create one with: fireforge tree create <name>');
|
|
@@ -98,7 +215,7 @@ export async function treeRemoveCommand(projectRoot, name, options = {}) {
|
|
|
98
215
|
return;
|
|
99
216
|
}
|
|
100
217
|
for (const target of targets) {
|
|
101
|
-
await removeTree(projectRoot, target);
|
|
218
|
+
await removeTree(projectRoot, target, { force: options.force === true });
|
|
102
219
|
success(`Removed verification tree "${target}"`);
|
|
103
220
|
}
|
|
104
221
|
});
|
|
@@ -109,14 +226,26 @@ async function treeExecCommand(projectRoot, name, args) {
|
|
|
109
226
|
assertPosix();
|
|
110
227
|
assertValidTreeName(name);
|
|
111
228
|
const treeRoot = join(getTreesDir(projectRoot), name);
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
229
|
+
const state = await readTreeMarkerState(treeRoot);
|
|
230
|
+
if (state.kind === 'corrupt') {
|
|
231
|
+
// Collapsing corrupt into "no such tree" misdiagnoses: the directory IS
|
|
232
|
+
// there, claiming to be a tree, and "no tree named X" points the operator
|
|
233
|
+
// at the wrong remediation.
|
|
234
|
+
throw new GeneralError(`Tree "${name}" exists but its marker could not be read: ${state.reason}. ` +
|
|
235
|
+
'Recreate it (fireforge tree remove <name> && fireforge tree create <name>).');
|
|
236
|
+
}
|
|
237
|
+
if (state.kind === 'absent') {
|
|
114
238
|
throw new GeneralError(`No verification tree named "${name}". List trees with: fireforge tree list`);
|
|
115
239
|
}
|
|
240
|
+
const marker = state.marker;
|
|
116
241
|
const current = await computePrimaryFingerprint(projectRoot);
|
|
117
242
|
if (marker.engineHead !== current.engineHead) {
|
|
118
243
|
warn(`Tree "${name}" is stale (primary engine advanced since the snapshot).`);
|
|
119
244
|
}
|
|
245
|
+
else if (marker.engineFingerprint !== undefined &&
|
|
246
|
+
marker.engineFingerprint !== current.engineFingerprint) {
|
|
247
|
+
warn(`Tree "${name}" is stale (primary engine worktree changed since the snapshot).`);
|
|
248
|
+
}
|
|
120
249
|
else if (marker.patchesFingerprint !== current.patchesFingerprint) {
|
|
121
250
|
warn(`Tree "${name}" is stale (primary patches changed since the snapshot).`);
|
|
122
251
|
}
|
|
@@ -133,6 +262,16 @@ async function treeExecCommand(projectRoot, name, args) {
|
|
|
133
262
|
child.on('exit', (code) => {
|
|
134
263
|
resolvePromise(code ?? 1);
|
|
135
264
|
});
|
|
265
|
+
}).finally(() => {
|
|
266
|
+
// FORGE L2: with `stdio: 'inherit'` the child owned stdout — including
|
|
267
|
+
// any FIREFORGE-VERDICT line it emitted as its LAST stdout write. From
|
|
268
|
+
// here on the parent must not write stdout again, or its own refusal
|
|
269
|
+
// text (the GeneralError below rendered by withErrorHandling) would
|
|
270
|
+
// print AFTER the child's verdict and break the "verdict is the run's
|
|
271
|
+
// last stdout line" contract at the tree-exec boundary. Sealing routes
|
|
272
|
+
// the parent's remaining output to stderr; withErrorHandling's finally
|
|
273
|
+
// clears the seal.
|
|
274
|
+
setStdoutSealed(true);
|
|
136
275
|
});
|
|
137
276
|
if (exitCode !== 0) {
|
|
138
277
|
throw new GeneralError(`tree exec: fireforge ${args.join(' ')} exited with code ${String(exitCode)}.`);
|
|
@@ -143,13 +282,19 @@ export function registerTree(program, context) {
|
|
|
143
282
|
const { getProjectRoot, withErrorHandling } = context;
|
|
144
283
|
const tree = program
|
|
145
284
|
.command('tree')
|
|
146
|
-
.description('Manage copy-on-write verification clones of this project for concurrent read-mostly work (lint, typecheck, status, verify, export --dry-run). Mutation commands are refused inside a tree.');
|
|
147
|
-
tree
|
|
285
|
+
.description('Manage copy-on-write verification clones of this project for concurrent read-mostly work (lint, typecheck, status, verify, export/re-export --dry-run; build-less test with create --with-objdir). Mutation commands are refused inside a tree.');
|
|
286
|
+
addWaitLockOption(tree
|
|
148
287
|
.command('create <name>')
|
|
149
288
|
.description('Clone the project (engine included, obj-* excluded) into .fireforge/trees/<name> using filesystem copy-on-write (APFS clonefile / btrfs-XFS reflink). Refuses on filesystems without CoW support unless --force-copy explicitly accepts a full physical copy.')
|
|
150
289
|
.option('--force-copy', 'Allow a full physical copy when the filesystem cannot copy-on-write. This can copy tens of GB; never implied.')
|
|
151
|
-
.action(withErrorHandling(async (name, options) => {
|
|
152
|
-
|
|
290
|
+
.option('--with-objdir', 'Also clone the primary obj-* build and rewrite its mozinfo.json to the tree (fail-closed), enabling build-less "fireforge test" inside the tree. Requires a completed, unambiguous primary build.')).action(withErrorHandling(async (name, options) => {
|
|
291
|
+
// The flag is a CLI-layer concern: the command takes the resolved
|
|
292
|
+
// wait budget, matching how furnace routes its subcommand locks.
|
|
293
|
+
const { waitLock, ...rest } = options;
|
|
294
|
+
await treeCreateCommand(getProjectRoot(), name, {
|
|
295
|
+
...rest,
|
|
296
|
+
waitLockSeconds: resolveWaitLockSeconds(waitLock),
|
|
297
|
+
});
|
|
153
298
|
}));
|
|
154
299
|
tree
|
|
155
300
|
.command('list')
|
|
@@ -160,8 +305,9 @@ export function registerTree(program, context) {
|
|
|
160
305
|
}));
|
|
161
306
|
tree
|
|
162
307
|
.command('remove [name]')
|
|
163
|
-
.description("Delete a verification tree. Refuses while a live process holds the tree's build or engine-session lock.")
|
|
308
|
+
.description("Delete a verification tree. Refuses while a live process holds the tree's build or engine-session lock, and when a lock exists whose owning process cannot be identified.")
|
|
164
309
|
.option('--all', 'Remove every verification tree')
|
|
310
|
+
.option('--force', "Delete even when a lock directory's owner cannot be identified. Only safe once you have confirmed no build or test is running against the tree.")
|
|
165
311
|
.action(withErrorHandling(async (name, options) => {
|
|
166
312
|
await treeRemoveCommand(getProjectRoot(), name, options);
|
|
167
313
|
}));
|
|
@@ -20,7 +20,7 @@ import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
|
20
20
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
21
21
|
import { isGitRepository } from '../core/git.js';
|
|
22
22
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
23
|
-
import { buildPatchQueueContext, lintPatchQueue } from '../core/patch-lint.js';
|
|
23
|
+
import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '../core/patch-lint.js';
|
|
24
24
|
import { loadPatchesManifest, validatePatchesManifestConsistency } from '../core/patch-manifest.js';
|
|
25
25
|
import { evaluatePatchPolicy } from '../core/patch-policy.js';
|
|
26
26
|
import { collectPatchRegistrationReferences } from '../core/patch-registration-refs.js';
|
|
@@ -187,7 +187,7 @@ export async function collectPatchQueueHealth(projectRoot) {
|
|
|
187
187
|
title: `Cross-patch lint issues (${lintIssues.length})`,
|
|
188
188
|
issues: lintIssues.map((issue) => {
|
|
189
189
|
const label = issue.severity === 'error' ? 'ERROR' : issue.severity === 'warning' ? 'WARN' : 'NOTICE';
|
|
190
|
-
return `${label}
|
|
190
|
+
return `${label} ${formatPatchLintIssue(issue)}`;
|
|
191
191
|
}),
|
|
192
192
|
errorCount: lintErrors,
|
|
193
193
|
warningCount: lintWarnings,
|
|
@@ -3,9 +3,10 @@ import { delimiter, dirname } from 'node:path';
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
5
|
import { warnIfFurnaceStale } from '../core/furnace-staleness.js';
|
|
6
|
-
import {
|
|
6
|
+
import { generateMozconfig, hasBuildArtifacts, hasRunnableBundle, watchWithOutput, } from '../core/mach.js';
|
|
7
|
+
import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
|
|
7
8
|
import { GeneralError } from '../errors/base.js';
|
|
8
|
-
import {
|
|
9
|
+
import { BuildError } from '../errors/build.js';
|
|
9
10
|
import { toError } from '../utils/errors.js';
|
|
10
11
|
import { pathExists } from '../utils/fs.js';
|
|
11
12
|
import { info, intro, outro, spinner, verbose } from '../utils/logger.js';
|
|
@@ -123,24 +124,16 @@ export async function watchCommand(projectRoot) {
|
|
|
123
124
|
// instead of an actionable preflight error.
|
|
124
125
|
await probeWatchman();
|
|
125
126
|
// Check for build artifacts before starting watch
|
|
127
|
+
// The mismatch rung rejects copied or relocated obj-* dirs whose mozinfo
|
|
128
|
+
// metadata (topsrcdir, topobjdir, mozconfig) still points at a different
|
|
129
|
+
// source tree; mach watch against stale metadata produces confusing errors.
|
|
126
130
|
const buildCheck = await hasBuildArtifacts(paths.engine);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const mismatchMessage = buildArtifactMismatchMessage(paths.engine, buildCheck, 'Watch mode');
|
|
134
|
-
if (mismatchMessage) {
|
|
135
|
-
throw new GeneralError(mismatchMessage);
|
|
136
|
-
}
|
|
137
|
-
if (!buildCheck.exists) {
|
|
138
|
-
const detail = buildCheck.objDir
|
|
139
|
-
? `Build artifacts incomplete in ${buildCheck.objDir}/`
|
|
140
|
-
: 'No build artifacts found (obj-*/ directory missing)';
|
|
141
|
-
throw new GeneralError(`Watch mode requires a completed build. ${detail}\n\n` +
|
|
142
|
-
"Run 'fireforge build' first to create the initial build, then run 'fireforge watch'.");
|
|
143
|
-
}
|
|
131
|
+
assertBuildArtifacts(paths.engine, buildCheck, {
|
|
132
|
+
label: 'Watch mode',
|
|
133
|
+
requirement: 'Watch mode requires a completed build.',
|
|
134
|
+
remediation: "Run 'fireforge build' first to create the initial build, then run 'fireforge watch'.",
|
|
135
|
+
requireExisting: true,
|
|
136
|
+
});
|
|
144
137
|
// Report bundle state alongside the "Using build artifacts..." banner
|
|
145
138
|
// so an operator watching a mid-build tree can see why `fireforge run`
|
|
146
139
|
// would refuse right now while watch is still going. Watch remains
|
|
@@ -182,10 +175,18 @@ export async function watchCommand(projectRoot) {
|
|
|
182
175
|
const watchmanDir = dirname(watchmanPath);
|
|
183
176
|
const existingPath = process.env['PATH'] ?? '';
|
|
184
177
|
const pathSegments = existingPath.split(delimiter).filter((segment) => segment.length > 0);
|
|
178
|
+
// Both arms filter identically; the cast this replaced laundered
|
|
179
|
+
// `string | undefined` on one arm while the other built the annotated shape
|
|
180
|
+
// honestly.
|
|
181
|
+
const baseEnv = {};
|
|
182
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
183
|
+
if (value !== undefined)
|
|
184
|
+
baseEnv[key] = value;
|
|
185
|
+
}
|
|
185
186
|
const watchmanEnv = pathSegments.includes(watchmanDir)
|
|
186
|
-
?
|
|
187
|
+
? baseEnv
|
|
187
188
|
: {
|
|
188
|
-
...
|
|
189
|
+
...baseEnv,
|
|
189
190
|
PATH: [watchmanDir, ...pathSegments].join(delimiter),
|
|
190
191
|
};
|
|
191
192
|
verbose(`watch: resolved watchman at ${watchmanPath}; forwarding directory in subprocess PATH.`);
|
|
@@ -105,7 +105,7 @@ async function assertDomTargetIsWireable(projectRoot, domFilePath, domTargetPath
|
|
|
105
105
|
await probeDomFragmentInsertionPoint(paths.engine, domFilePath, domTargetPath);
|
|
106
106
|
}
|
|
107
107
|
catch (probeError) {
|
|
108
|
-
throw new InvalidArgumentError(`${probeError
|
|
108
|
+
throw new InvalidArgumentError(`${toError(probeError).message}\n` +
|
|
109
109
|
`The resolved chrome document ${domTargetPath} does not expose an insertion anchor ` +
|
|
110
110
|
'that `fireforge wire` recognises (`#include browser-sets.inc` or `<html:body>`). ' +
|
|
111
111
|
'Add one of those anchors to the chrome doc, or target a document that has them via ' +
|
|
@@ -33,6 +33,17 @@ export declare function parseModule(content: string, onComment?: acorn.Comment[]
|
|
|
33
33
|
/**
|
|
34
34
|
* Convenience cast from `acorn.Node` (or the generic ESTree union returned
|
|
35
35
|
* by estree-walker callbacks) to a positioned, narrowly-typed node.
|
|
36
|
+
*
|
|
37
|
+
* **Caller obligation: you must already have discriminated on `node.type`.**
|
|
38
|
+
* This performs no runtime check — it exists to attach acorn's `start`/`end`
|
|
39
|
+
* offsets to a node the caller has *already* narrowed, typically inside a
|
|
40
|
+
* `if (node.type === 'CallExpression')` branch or an equivalent walker guard.
|
|
41
|
+
* Calling it on an undiscriminated node produces a value whose type is a lie.
|
|
42
|
+
*
|
|
43
|
+
* A runtime `type` assertion was considered and rejected: every call site sits
|
|
44
|
+
* in the hot loop of a lint or registration walker, and all of them are
|
|
45
|
+
* already post-narrowing, so the check would cost traversal time on every node
|
|
46
|
+
* to re-prove something the enclosing branch established.
|
|
36
47
|
*/
|
|
37
48
|
export declare function asEstree<T extends estree.Node>(node: estree.Node): AcornESTreeNode<T>;
|
|
38
49
|
/**
|
|
@@ -40,6 +40,17 @@ export function parseModule(content, onComment) {
|
|
|
40
40
|
/**
|
|
41
41
|
* Convenience cast from `acorn.Node` (or the generic ESTree union returned
|
|
42
42
|
* by estree-walker callbacks) to a positioned, narrowly-typed node.
|
|
43
|
+
*
|
|
44
|
+
* **Caller obligation: you must already have discriminated on `node.type`.**
|
|
45
|
+
* This performs no runtime check — it exists to attach acorn's `start`/`end`
|
|
46
|
+
* offsets to a node the caller has *already* narrowed, typically inside a
|
|
47
|
+
* `if (node.type === 'CallExpression')` branch or an equivalent walker guard.
|
|
48
|
+
* Calling it on an undiscriminated node produces a value whose type is a lie.
|
|
49
|
+
*
|
|
50
|
+
* A runtime `type` assertion was considered and rejected: every call site sits
|
|
51
|
+
* in the hot loop of a lint or registration walker, and all of them are
|
|
52
|
+
* already post-narrowing, so the check would cost traversal time on every node
|
|
53
|
+
* to re-prove something the enclosing branch established.
|
|
43
54
|
*/
|
|
44
55
|
export function asEstree(node) {
|
|
45
56
|
return node;
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import { dirname, join } from 'node:path';
|
|
37
37
|
import { pathExists, readText } from '../utils/fs.js';
|
|
38
38
|
import { getPlatform } from '../utils/platform.js';
|
|
39
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
39
40
|
/**
|
|
40
41
|
* Tokens that uniquely identify a Windows-only `if CONFIG[...]:` block.
|
|
41
42
|
* `MAKENSISU` is the Windows stubinstaller compiler; `OS_TARGET ==
|
|
@@ -96,7 +97,7 @@ async function findOwningMozBuild(engineDir, sourceDir) {
|
|
|
96
97
|
* `"stubinstaller/installing_page.css"`.
|
|
97
98
|
*/
|
|
98
99
|
function matchesQuotedBasename(line, basename) {
|
|
99
|
-
const escaped = basename
|
|
100
|
+
const escaped = escapeRegex(basename);
|
|
100
101
|
return new RegExp(`["'](?:[^"']*\\/)?${escaped}["']`).test(line);
|
|
101
102
|
}
|
|
102
103
|
/**
|
|
@@ -207,6 +208,8 @@ export async function detectPlatformGate(engineDir, sourcePath) {
|
|
|
207
208
|
host = getPlatform();
|
|
208
209
|
}
|
|
209
210
|
catch {
|
|
211
|
+
// Unrecognised host platform. The platform gates below are advisory, so an
|
|
212
|
+
// unknown host disables them rather than failing a warn-only audit.
|
|
210
213
|
host = undefined;
|
|
211
214
|
}
|
|
212
215
|
const sourceDir = dirname(join(engineDir, sourcePath));
|
|
@@ -217,6 +220,8 @@ export async function detectPlatformGate(engineDir, sourcePath) {
|
|
|
217
220
|
content = await readText(mozBuild);
|
|
218
221
|
}
|
|
219
222
|
catch {
|
|
223
|
+
// An unreadable moz.build contributes no platform gates. The audit is
|
|
224
|
+
// warn-only, so a missing input degrades to 'no gate found', not an error.
|
|
220
225
|
content = '';
|
|
221
226
|
}
|
|
222
227
|
const sourceBasename = sourcePath.split('/').pop() ?? '';
|
|
@@ -105,6 +105,8 @@ export async function findRegisteredTarget(engineDir, source) {
|
|
|
105
105
|
content = await readText(jarMn);
|
|
106
106
|
}
|
|
107
107
|
catch {
|
|
108
|
+
// An unreadable jar.mn contributes no registration hints; the resolver falls
|
|
109
|
+
// through to its other strategies.
|
|
108
110
|
content = '';
|
|
109
111
|
}
|
|
110
112
|
const rel = relative(current, sourceAbs).split(sep).join('/');
|
|
@@ -24,6 +24,19 @@ export declare function isTestPath(sourcePath: string): boolean;
|
|
|
24
24
|
* @returns Count of matching trailing segments (basename always counts as 1)
|
|
25
25
|
*/
|
|
26
26
|
export declare function countTrailingSegmentMatches(a: string, b: string): number;
|
|
27
|
+
/**
|
|
28
|
+
* Path segments too common to identify an artifact on their own.
|
|
29
|
+
*
|
|
30
|
+
* Shared deliberately: this set governs two halves of ONE decision, and the
|
|
31
|
+
* halves disagreed before 0.41.0. `scoreCandidate` (selection) used an
|
|
32
|
+
* 11-entry copy while `isConfidentMatch` (confirmation, in `build-audit.ts`)
|
|
33
|
+
* used a 17-entry one, so a source path containing `test`, `tests`, `unit`,
|
|
34
|
+
* `common`, `xpcshell` or `mochitest` earned a +1 selection bonus for a
|
|
35
|
+
* segment that confirmation then rejected as meaningless — the chosen
|
|
36
|
+
* artifact could never be confirmed. The union is authoritative; any addition
|
|
37
|
+
* must apply to both halves, which is now automatic.
|
|
38
|
+
*/
|
|
39
|
+
export declare const GENERIC_PATH_SEGMENTS: ReadonlySet<string>;
|
|
27
40
|
/**
|
|
28
41
|
* Computes a score for `candidatePath` relative to `sourcePath`. Higher
|
|
29
42
|
* scores win. Score = trailing-segment match count, with a bonus when
|
|
@@ -86,6 +86,37 @@ export function countTrailingSegmentMatches(a, b) {
|
|
|
86
86
|
}
|
|
87
87
|
return matches;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Path segments too common to identify an artifact on their own.
|
|
91
|
+
*
|
|
92
|
+
* Shared deliberately: this set governs two halves of ONE decision, and the
|
|
93
|
+
* halves disagreed before 0.41.0. `scoreCandidate` (selection) used an
|
|
94
|
+
* 11-entry copy while `isConfidentMatch` (confirmation, in `build-audit.ts`)
|
|
95
|
+
* used a 17-entry one, so a source path containing `test`, `tests`, `unit`,
|
|
96
|
+
* `common`, `xpcshell` or `mochitest` earned a +1 selection bonus for a
|
|
97
|
+
* segment that confirmation then rejected as meaningless — the chosen
|
|
98
|
+
* artifact could never be confirmed. The union is authoritative; any addition
|
|
99
|
+
* must apply to both halves, which is now automatic.
|
|
100
|
+
*/
|
|
101
|
+
export const GENERIC_PATH_SEGMENTS = new Set([
|
|
102
|
+
'content',
|
|
103
|
+
'chrome',
|
|
104
|
+
'bin',
|
|
105
|
+
'browser',
|
|
106
|
+
'toolkit',
|
|
107
|
+
'modules',
|
|
108
|
+
'base',
|
|
109
|
+
'app',
|
|
110
|
+
'profile',
|
|
111
|
+
'shared',
|
|
112
|
+
'themes',
|
|
113
|
+
'test',
|
|
114
|
+
'tests',
|
|
115
|
+
'unit',
|
|
116
|
+
'common',
|
|
117
|
+
'xpcshell',
|
|
118
|
+
'mochitest',
|
|
119
|
+
]);
|
|
89
120
|
/**
|
|
90
121
|
* Computes a score for `candidatePath` relative to `sourcePath`. Higher
|
|
91
122
|
* scores win. Score = trailing-segment match count, with a bonus when
|
|
@@ -112,19 +143,7 @@ export function scoreCandidate(sourcePath, candidatePath) {
|
|
|
112
143
|
// but are not part of the trailing match. Each unique mid-path hit on
|
|
113
144
|
// a meaningful (>2-char, not generic like 'content'/'chrome'/'bin')
|
|
114
145
|
// segment adds 1 to the score.
|
|
115
|
-
const generic =
|
|
116
|
-
'content',
|
|
117
|
-
'chrome',
|
|
118
|
-
'bin',
|
|
119
|
-
'browser',
|
|
120
|
-
'toolkit',
|
|
121
|
-
'modules',
|
|
122
|
-
'base',
|
|
123
|
-
'app',
|
|
124
|
-
'profile',
|
|
125
|
-
'shared',
|
|
126
|
-
'themes',
|
|
127
|
-
]);
|
|
146
|
+
const generic = GENERIC_PATH_SEGMENTS;
|
|
128
147
|
const trailingSet = new Set(sourceSegs.slice(sourceSegs.length - trailing));
|
|
129
148
|
let bonus = 0;
|
|
130
149
|
for (const seg of sourceSegs) {
|
|
@@ -166,6 +185,8 @@ export async function findAllByBasename(root, name, maxDepth = MAX_SCAN_DEPTH) {
|
|
|
166
185
|
children = await readdir(entry.dir, { withFileTypes: true });
|
|
167
186
|
}
|
|
168
187
|
catch {
|
|
188
|
+
// An unreadable subdirectory yields no candidates. Skip it and keep
|
|
189
|
+
// scoring the rest rather than abandoning the whole resolution.
|
|
169
190
|
continue;
|
|
170
191
|
}
|
|
171
192
|
for (const child of children) {
|
|
@@ -39,7 +39,7 @@ import { pathExists } from '../utils/fs.js';
|
|
|
39
39
|
import { info, verbose, warn } from '../utils/logger.js';
|
|
40
40
|
import { detectPlatformGate } from './build-audit-platform.js';
|
|
41
41
|
import { collectSameBasenameCandidates, findRegisteredTarget, resolveArtifactByRegistration, } from './build-audit-registration.js';
|
|
42
|
-
import { countTrailingSegmentMatches, isTestPath, resolveBestArtifact, } from './build-audit-resolve.js';
|
|
42
|
+
import { countTrailingSegmentMatches, GENERIC_PATH_SEGMENTS, isTestPath, resolveBestArtifact, } from './build-audit-resolve.js';
|
|
43
43
|
import { resolveArtifactByKnownTransform } from './build-audit-transforms.js';
|
|
44
44
|
import { collectChangedEnginePaths } from './engine-changes.js';
|
|
45
45
|
/** Path extensions that are conventionally packaged into the Firefox bundle. */
|
|
@@ -138,6 +138,7 @@ async function resolveDistRoot(engineDir) {
|
|
|
138
138
|
entries = await readdir(engineDir);
|
|
139
139
|
}
|
|
140
140
|
catch {
|
|
141
|
+
// No readable engine directory means no objdir can be identified.
|
|
141
142
|
return undefined;
|
|
142
143
|
}
|
|
143
144
|
const objDirs = entries.filter((e) => e.startsWith('obj-'));
|
|
@@ -161,6 +162,7 @@ async function resolveTestsRoot(engineDir) {
|
|
|
161
162
|
entries = await readdir(engineDir);
|
|
162
163
|
}
|
|
163
164
|
catch {
|
|
165
|
+
// No readable engine directory means no objdir can be identified.
|
|
164
166
|
return undefined;
|
|
165
167
|
}
|
|
166
168
|
const objDirs = entries.filter((e) => e.startsWith('obj-'));
|
|
@@ -236,25 +238,7 @@ function isConfidentMatch(source, candidate) {
|
|
|
236
238
|
}
|
|
237
239
|
const sourceSegs = source.split('/').filter(Boolean);
|
|
238
240
|
const candSegs = candidate.split('/').filter(Boolean);
|
|
239
|
-
const generic =
|
|
240
|
-
'content',
|
|
241
|
-
'chrome',
|
|
242
|
-
'bin',
|
|
243
|
-
'browser',
|
|
244
|
-
'toolkit',
|
|
245
|
-
'modules',
|
|
246
|
-
'base',
|
|
247
|
-
'app',
|
|
248
|
-
'profile',
|
|
249
|
-
'shared',
|
|
250
|
-
'themes',
|
|
251
|
-
'test',
|
|
252
|
-
'tests',
|
|
253
|
-
'unit',
|
|
254
|
-
'common',
|
|
255
|
-
'xpcshell',
|
|
256
|
-
'mochitest',
|
|
257
|
-
]);
|
|
241
|
+
const generic = GENERIC_PATH_SEGMENTS;
|
|
258
242
|
// Skip the basename itself (which trail-matches by definition).
|
|
259
243
|
for (let i = 0; i < sourceSegs.length - 1; i += 1) {
|
|
260
244
|
const seg = sourceSegs[i];
|