@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.
Files changed (253) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +16 -10
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +20 -3
  6. package/dist/src/commands/bootstrap.js +5 -1
  7. package/dist/src/commands/discard.js +2 -22
  8. package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
  9. package/dist/src/commands/doctor-check-core.d.ts +20 -0
  10. package/dist/src/commands/doctor-check-core.js +28 -0
  11. package/dist/src/commands/doctor-external-toolchains.js +2 -0
  12. package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
  13. package/dist/src/commands/doctor-furnace.js +13 -11
  14. package/dist/src/commands/doctor-orphaned-harness.js +2 -1
  15. package/dist/src/commands/doctor.js +2 -2
  16. package/dist/src/commands/export-all.js +9 -1
  17. package/dist/src/commands/export-flow.js +13 -3
  18. package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
  19. package/dist/src/commands/export-placement-conflicts.js +74 -0
  20. package/dist/src/commands/export-placement-gate.js +1 -1
  21. package/dist/src/commands/export-shared.d.ts +8 -1
  22. package/dist/src/commands/export-shared.js +16 -7
  23. package/dist/src/commands/export.js +2 -2
  24. package/dist/src/commands/furnace/apply.js +4 -3
  25. package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
  26. package/dist/src/commands/furnace/chrome-doc.js +13 -2
  27. package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
  28. package/dist/src/commands/furnace/create-browser-test.js +5 -0
  29. package/dist/src/commands/furnace/create.js +2 -2
  30. package/dist/src/commands/furnace/diff.js +2 -1
  31. package/dist/src/commands/furnace/init.js +4 -6
  32. package/dist/src/commands/furnace/override.js +2 -2
  33. package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
  34. package/dist/src/commands/furnace/remove-mutations.js +171 -0
  35. package/dist/src/commands/furnace/remove.js +21 -128
  36. package/dist/src/commands/furnace/rename-browser-test.js +54 -5
  37. package/dist/src/commands/furnace/scan.js +27 -36
  38. package/dist/src/commands/furnace/status.js +30 -28
  39. package/dist/src/commands/furnace/validate.js +25 -18
  40. package/dist/src/commands/import.js +6 -20
  41. package/dist/src/commands/lint-per-patch.js +12 -59
  42. package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
  43. package/dist/src/commands/lint-per-run-checkjs.js +102 -0
  44. package/dist/src/commands/lint.js +4 -4
  45. package/dist/src/commands/manifest.js +1 -1
  46. package/dist/src/commands/package.js +9 -16
  47. package/dist/src/commands/patch/compact.js +5 -5
  48. package/dist/src/commands/patch/delete.js +5 -5
  49. package/dist/src/commands/patch/lint-ignore.js +3 -2
  50. package/dist/src/commands/patch/move-files-create.js +22 -4
  51. package/dist/src/commands/patch/move-files-into.js +14 -10
  52. package/dist/src/commands/patch/move-files.js +6 -5
  53. package/dist/src/commands/patch/rename.d.ts +2 -2
  54. package/dist/src/commands/patch/rename.js +151 -50
  55. package/dist/src/commands/patch/reorder.d.ts +9 -0
  56. package/dist/src/commands/patch/reorder.js +14 -10
  57. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  58. package/dist/src/commands/patch/split-plan.js +22 -12
  59. package/dist/src/commands/patch/split.d.ts +1 -1
  60. package/dist/src/commands/patch/split.js +23 -9
  61. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  62. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  63. package/dist/src/commands/patch/staged-dependency.js +10 -0
  64. package/dist/src/commands/re-export-adjacent.js +5 -6
  65. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  66. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  67. package/dist/src/commands/re-export-drift.d.ts +74 -0
  68. package/dist/src/commands/re-export-drift.js +234 -0
  69. package/dist/src/commands/re-export-files.js +5 -3
  70. package/dist/src/commands/re-export-lint.d.ts +97 -0
  71. package/dist/src/commands/re-export-lint.js +210 -0
  72. package/dist/src/commands/re-export-options.js +8 -0
  73. package/dist/src/commands/re-export-register.d.ts +18 -0
  74. package/dist/src/commands/re-export-register.js +61 -0
  75. package/dist/src/commands/re-export-scan.js +13 -7
  76. package/dist/src/commands/re-export-single.d.ts +20 -0
  77. package/dist/src/commands/re-export-single.js +261 -0
  78. package/dist/src/commands/re-export.d.ts +0 -4
  79. package/dist/src/commands/re-export.js +113 -268
  80. package/dist/src/commands/rebase/abort.js +13 -4
  81. package/dist/src/commands/rebase/continue.js +11 -4
  82. package/dist/src/commands/rebase/index.js +19 -29
  83. package/dist/src/commands/rebase/patch-loop.js +12 -0
  84. package/dist/src/commands/reset.js +4 -23
  85. package/dist/src/commands/resolve.js +2 -9
  86. package/dist/src/commands/run.js +11 -21
  87. package/dist/src/commands/source.js +3 -9
  88. package/dist/src/commands/status-check.d.ts +13 -0
  89. package/dist/src/commands/status-check.js +31 -13
  90. package/dist/src/commands/status-json.d.ts +31 -0
  91. package/dist/src/commands/status-json.js +76 -0
  92. package/dist/src/commands/status-output.d.ts +1 -0
  93. package/dist/src/commands/status-output.js +10 -1
  94. package/dist/src/commands/status-ownership.d.ts +52 -0
  95. package/dist/src/commands/status-ownership.js +57 -0
  96. package/dist/src/commands/status.js +184 -175
  97. package/dist/src/commands/test-build-phase.d.ts +19 -0
  98. package/dist/src/commands/test-build-phase.js +168 -0
  99. package/dist/src/commands/test-diagnose.js +30 -2
  100. package/dist/src/commands/test-modes.d.ts +9 -1
  101. package/dist/src/commands/test-modes.js +46 -13
  102. package/dist/src/commands/test-register.js +59 -27
  103. package/dist/src/commands/test-run.d.ts +29 -3
  104. package/dist/src/commands/test-run.js +41 -4
  105. package/dist/src/commands/test-verdict.d.ts +32 -0
  106. package/dist/src/commands/test-verdict.js +48 -0
  107. package/dist/src/commands/test.d.ts +9 -0
  108. package/dist/src/commands/test.js +100 -123
  109. package/dist/src/commands/token-coverage.js +4 -9
  110. package/dist/src/commands/tree.d.ts +3 -0
  111. package/dist/src/commands/tree.js +171 -25
  112. package/dist/src/commands/verify.js +2 -2
  113. package/dist/src/commands/watch.js +22 -21
  114. package/dist/src/commands/wire.js +1 -1
  115. package/dist/src/core/ast-utils.d.ts +11 -0
  116. package/dist/src/core/ast-utils.js +11 -0
  117. package/dist/src/core/build-audit-platform.js +6 -1
  118. package/dist/src/core/build-audit-registration.js +2 -0
  119. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  120. package/dist/src/core/build-audit-resolve.js +34 -13
  121. package/dist/src/core/build-audit.js +4 -20
  122. package/dist/src/core/build-baseline-types.d.ts +29 -8
  123. package/dist/src/core/build-baseline-types.js +2 -1
  124. package/dist/src/core/build-baseline.d.ts +10 -2
  125. package/dist/src/core/build-baseline.js +39 -8
  126. package/dist/src/core/build-prepare.d.ts +4 -0
  127. package/dist/src/core/build-prepare.js +7 -1
  128. package/dist/src/core/config-paths.d.ts +1 -1
  129. package/dist/src/core/config-paths.js +2 -0
  130. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  131. package/dist/src/core/config-validate.js +34 -36
  132. package/dist/src/core/config.js +16 -3
  133. package/dist/src/core/coverage-extend.d.ts +89 -0
  134. package/dist/src/core/coverage-extend.js +188 -0
  135. package/dist/src/core/destructive.js +3 -1
  136. package/dist/src/core/discard-baseline.js +18 -4
  137. package/dist/src/core/engine-precondition.d.ts +35 -0
  138. package/dist/src/core/engine-precondition.js +50 -0
  139. package/dist/src/core/engine-session-lock.d.ts +38 -1
  140. package/dist/src/core/engine-session-lock.js +78 -4
  141. package/dist/src/core/file-lock.d.ts +7 -0
  142. package/dist/src/core/file-lock.js +20 -30
  143. package/dist/src/core/firefox.js +5 -2
  144. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  145. package/dist/src/core/furnace-apply-helpers.js +12 -3
  146. package/dist/src/core/furnace-apply-output.js +9 -0
  147. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  148. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  149. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  150. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  151. package/dist/src/core/furnace-apply.d.ts +16 -4
  152. package/dist/src/core/furnace-apply.js +39 -43
  153. package/dist/src/core/furnace-config.js +2 -0
  154. package/dist/src/core/furnace-css-fragments.js +2 -0
  155. package/dist/src/core/furnace-jsconfig.js +14 -7
  156. package/dist/src/core/furnace-registration-ast.js +8 -8
  157. package/dist/src/core/furnace-registration-remove.js +6 -3
  158. package/dist/src/core/furnace-registration.js +9 -12
  159. package/dist/src/core/furnace-rollback.js +1 -1
  160. package/dist/src/core/furnace-scanner.js +2 -1
  161. package/dist/src/core/furnace-stale-export.js +2 -1
  162. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  163. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  164. package/dist/src/core/furnace-validate-helpers.js +33 -1
  165. package/dist/src/core/furnace-validate-registration.js +16 -0
  166. package/dist/src/core/furnace-validate.js +4 -2
  167. package/dist/src/core/git-diff.js +37 -2
  168. package/dist/src/core/lint-cache.d.ts +1 -1
  169. package/dist/src/core/lint-cache.js +33 -10
  170. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  171. package/dist/src/core/mach-build-artifacts.js +108 -2
  172. package/dist/src/core/mach-resource-shim.js +1 -0
  173. package/dist/src/core/marionette-port.d.ts +16 -0
  174. package/dist/src/core/marionette-port.js +93 -8
  175. package/dist/src/core/marionette-preflight.js +6 -5
  176. package/dist/src/core/patch-apply.d.ts +20 -13
  177. package/dist/src/core/patch-apply.js +32 -33
  178. package/dist/src/core/patch-export.js +3 -5
  179. package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
  180. package/dist/src/core/patch-lint-checkjs.js +78 -7
  181. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  182. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  183. package/dist/src/core/patch-lint-creators.js +45 -0
  184. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  185. package/dist/src/core/patch-lint-cross.js +16 -32
  186. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  187. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  188. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  189. package/dist/src/core/patch-lint-module-registration.js +88 -0
  190. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  191. package/dist/src/core/patch-lint-reexports.d.ts +1 -0
  192. package/dist/src/core/patch-lint-reexports.js +1 -0
  193. package/dist/src/core/patch-lint.d.ts +8 -0
  194. package/dist/src/core/patch-lint.js +84 -6
  195. package/dist/src/core/patch-lock.d.ts +12 -1
  196. package/dist/src/core/patch-lock.js +40 -4
  197. package/dist/src/core/patch-manifest-consistency.js +15 -3
  198. package/dist/src/core/patch-parse.d.ts +8 -0
  199. package/dist/src/core/patch-parse.js +10 -0
  200. package/dist/src/core/patch-policy.d.ts +7 -0
  201. package/dist/src/core/patch-policy.js +4 -1
  202. package/dist/src/core/patch-transform.d.ts +10 -0
  203. package/dist/src/core/patch-transform.js +13 -2
  204. package/dist/src/core/rebase-session.d.ts +44 -2
  205. package/dist/src/core/rebase-session.js +114 -17
  206. package/dist/src/core/register-browser-content.js +2 -1
  207. package/dist/src/core/register-shared-css.js +2 -1
  208. package/dist/src/core/smoke-patterns.d.ts +0 -8
  209. package/dist/src/core/smoke-patterns.js +3 -2
  210. package/dist/src/core/status-binary.d.ts +55 -0
  211. package/dist/src/core/status-binary.js +124 -0
  212. package/dist/src/core/status-classify.d.ts +6 -1
  213. package/dist/src/core/status-classify.js +101 -73
  214. package/dist/src/core/test-harness-crash.d.ts +32 -1
  215. package/dist/src/core/test-harness-crash.js +59 -4
  216. package/dist/src/core/test-path-scope.js +2 -0
  217. package/dist/src/core/test-stale-check.js +1 -20
  218. package/dist/src/core/toolchain-preflight.js +24 -26
  219. package/dist/src/core/tree-guard.d.ts +1 -1
  220. package/dist/src/core/tree-guard.js +67 -12
  221. package/dist/src/core/tree-store.d.ts +86 -10
  222. package/dist/src/core/tree-store.js +310 -72
  223. package/dist/src/core/typecheck.js +2 -1
  224. package/dist/src/core/wire-destroy.js +14 -3
  225. package/dist/src/core/wire-init.js +37 -40
  226. package/dist/src/core/wire-subscript.js +11 -3
  227. package/dist/src/core/wire-utils.js +4 -4
  228. package/dist/src/core/xpcshell-appdir.js +3 -0
  229. package/dist/src/errors/base.d.ts +13 -0
  230. package/dist/src/errors/base.js +13 -0
  231. package/dist/src/errors/rebase.d.ts +13 -0
  232. package/dist/src/errors/rebase.js +17 -0
  233. package/dist/src/types/commands/options.d.ts +86 -1
  234. package/dist/src/types/commands/patches.d.ts +8 -0
  235. package/dist/src/types/furnace.d.ts +7 -0
  236. package/dist/src/utils/build-info.d.ts +31 -0
  237. package/dist/src/utils/build-info.js +98 -0
  238. package/dist/src/utils/concurrency.d.ts +8 -0
  239. package/dist/src/utils/concurrency.js +21 -0
  240. package/dist/src/utils/errors.d.ts +39 -0
  241. package/dist/src/utils/errors.js +55 -0
  242. package/dist/src/utils/fs.js +5 -12
  243. package/dist/src/utils/logger.d.ts +14 -2
  244. package/dist/src/utils/logger.js +43 -13
  245. package/dist/src/utils/options.d.ts +21 -4
  246. package/dist/src/utils/options.js +26 -6
  247. package/dist/src/utils/package-root.js +7 -4
  248. package/dist/src/utils/process-group.js +13 -2
  249. package/dist/src/utils/stdio-drain.d.ts +28 -0
  250. package/dist/src/utils/stdio-drain.js +54 -0
  251. package/dist/src/utils/validation.d.ts +23 -2
  252. package/dist/src/utils/validation.js +29 -3
  253. 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. In-tree `test` and objdir cloning are deferred:
11
- * mach objdirs embed absolute source paths, so a cloned objdir would
12
- * silently operate against the primary tree.
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, readTreeMarker, withTreeLifecycleLock, } from '../core/tree-store.js';
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
- if (await readTreeMarker(projectRoot)) {
35
- throw new GeneralError('Verification trees cannot be nested: run tree create in the primary tree.');
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
- const marker = await withEngineSessionLock(projectRoot, 'tree create', () => cloneTree({
57
- primaryRoot: projectRoot,
58
- treeRoot,
59
- name,
60
- capability,
61
- createdAt: new Date().toISOString(),
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 marker = await readTreeMarker(treeRoot);
113
- if (!marker) {
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
- await treeCreateCommand(getProjectRoot(), name, options);
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} [${issue.check}] ${issue.file}: ${issue.message}`;
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 { buildArtifactMismatchMessage, generateMozconfig, hasBuildArtifacts, hasRunnableBundle, watchWithOutput, } from '../core/mach.js';
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 { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
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
- if (buildCheck.ambiguous && buildCheck.objDirs && buildCheck.objDirs.length > 0) {
128
- throw new AmbiguousBuildArtifactsError(buildCheck.objDirs);
129
- }
130
- // Reject copied or relocated obj-* dirs whose mozinfo metadata (topsrcdir,
131
- // topobjdir, mozconfig) still points at a different source tree. Running mach
132
- // watch against stale metadata produces confusing build errors.
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
- ? { ...process.env }
187
+ ? baseEnv
187
188
  : {
188
- ...process.env,
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 instanceof Error ? probeError.message : String(probeError)}\n` +
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.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
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 = new Set([
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 = new Set([
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];