@socketsecurity/cli-with-sentry 1.11.0 → 1.11.1

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 (45) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +5 -5
  3. package/dist/cli.js +930 -398
  4. package/dist/cli.js.map +1 -1
  5. package/dist/constants.js +3 -3
  6. package/dist/constants.js.map +1 -1
  7. package/dist/manifest-scripts/maven-extension/coana-maven-extension.jar +0 -0
  8. package/dist/tsconfig.dts.tsbuildinfo +1 -1
  9. package/dist/types/commands/manifest/bazel/bazel-bin-detect.d.mts +1 -1
  10. package/dist/types/commands/manifest/bazel/bazel-bin-detect.d.mts.map +1 -1
  11. package/dist/types/commands/manifest/bazel/bazel-cleanup.d.mts +8 -0
  12. package/dist/types/commands/manifest/bazel/bazel-cleanup.d.mts.map +1 -0
  13. package/dist/types/commands/manifest/bazel/bazel-cquery.d.mts.map +1 -1
  14. package/dist/types/commands/manifest/bazel/bazel-java-shim.d.mts +1 -9
  15. package/dist/types/commands/manifest/bazel/bazel-java-shim.d.mts.map +1 -1
  16. package/dist/types/commands/manifest/bazel/bazel-process.d.mts +25 -0
  17. package/dist/types/commands/manifest/bazel/bazel-process.d.mts.map +1 -0
  18. package/dist/types/commands/manifest/bazel/bazel-python-shim.d.mts +2 -5
  19. package/dist/types/commands/manifest/bazel/bazel-python-shim.d.mts.map +1 -1
  20. package/dist/types/commands/manifest/bazel/bazel-query-runner.d.mts +1 -9
  21. package/dist/types/commands/manifest/bazel/bazel-query-runner.d.mts.map +1 -1
  22. package/dist/types/commands/manifest/bazel/cmd-manifest-bazel.d.mts.map +1 -1
  23. package/dist/types/commands/manifest/bazel/extract_bazel_to_maven.d.mts.map +1 -1
  24. package/dist/types/commands/manifest/bazel/extract_bazel_to_pypi.d.mts.map +1 -1
  25. package/dist/types/commands/manifest/discover-manifest-roots.d.mts +16 -4
  26. package/dist/types/commands/manifest/discover-manifest-roots.d.mts.map +1 -1
  27. package/dist/types/commands/manifest/enumerate-workspaces.d.mts +3 -1
  28. package/dist/types/commands/manifest/enumerate-workspaces.d.mts.map +1 -1
  29. package/dist/types/commands/manifest/generate-recursive-manifests.d.mts +2 -2
  30. package/dist/types/commands/manifest/generate-recursive-manifests.d.mts.map +1 -1
  31. package/dist/types/commands/manifest/output-manifest-dynamic-sbom-inference.d.mts.map +1 -1
  32. package/dist/types/commands/manifest/run-manifest-facts.d.mts +3 -1
  33. package/dist/types/commands/manifest/run-manifest-facts.d.mts.map +1 -1
  34. package/dist/types/commands/manifest/scripts/build-tool.d.mts +14 -0
  35. package/dist/types/commands/manifest/scripts/build-tool.d.mts.map +1 -1
  36. package/dist/types/commands/manifest/scripts/run.d.mts +2 -0
  37. package/dist/types/commands/manifest/scripts/run.d.mts.map +1 -1
  38. package/dist/types/commands/manifest/setup-recursive-manifest-config.d.mts +14 -16
  39. package/dist/types/commands/manifest/setup-recursive-manifest-config.d.mts.map +1 -1
  40. package/dist/types/utils/config.d.mts +1 -0
  41. package/dist/types/utils/config.d.mts.map +1 -1
  42. package/dist/utils.js +14 -1
  43. package/dist/utils.js.map +1 -1
  44. package/dist/vendor.js +553 -550
  45. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -19,8 +19,9 @@ var prompts = require('../external/@socketsecurity/registry/lib/prompts');
19
19
  var require$$0 = require('node:crypto');
20
20
  var os = require('node:os');
21
21
  var spawn = require('../external/@socketsecurity/registry/lib/spawn');
22
- var bin = require('../external/@socketsecurity/registry/lib/bin');
23
22
  var childProcess = require('node:child_process');
23
+ var bin = require('../external/@socketsecurity/registry/lib/bin');
24
+ var require$$7 = require('node:timers/promises');
24
25
  var fs$1 = require('../external/@socketsecurity/registry/lib/fs');
25
26
  var strings = require('../external/@socketsecurity/registry/lib/strings');
26
27
  var path$1 = require('../external/@socketsecurity/registry/lib/path');
@@ -2081,23 +2082,87 @@ async function performReachabilityAnalysis(options) {
2081
2082
  }
2082
2083
  }
2083
2084
 
2084
- const BUILD_TOOLS = ['gradle', 'maven', 'sbt'];
2085
+ // PATH fallback when no `bin` and no project wrapper.
2086
+ const DEFAULT_BUILD_TOOL_BIN = {
2087
+ __proto__: null,
2088
+ gradle: 'gradle',
2089
+ maven: 'mvn',
2090
+ sbt: 'sbt'
2091
+ };
2085
2092
 
2086
- // One glob-suffix set per ecosystem's build-descriptor marker(s); `settings.gradle(.kts)`
2087
- // covers Kotlin-DSL roots that have no root `build.gradle`.
2088
- const MARKERS_BY_TOOL = {
2093
+ // Project-local wrapper, preferred because it pins the expected build-tool
2094
+ // version. sbt has no wrapper convention. POSIX names only (no win32 target).
2095
+ const BUILD_TOOL_WRAPPER = {
2089
2096
  __proto__: null,
2090
- gradle: ['build.gradle', 'build.gradle.kts', 'settings.gradle', 'settings.gradle.kts'],
2091
- maven: ['pom.xml'],
2092
- sbt: ['build.sbt']
2097
+ gradle: 'gradlew',
2098
+ maven: 'mvnw'
2093
2099
  };
2094
- const TOOL_BY_MARKER = {
2095
- __proto__: null
2100
+
2101
+ // Maven builds by file (`mvn --file`), so a directory can hold several: the
2102
+ // POMs a scan uploads, matched case-insensitively. `walkGlob` is a
2103
+ // case-sensitive superset of `nocaseGlob`, for the walk.
2104
+
2105
+ // `settings.gradle(.kts)` covers Kotlin-DSL roots that have no root
2106
+ // `build.gradle`.
2107
+ const BUILD_FILES = {
2108
+ __proto__: null,
2109
+ gradle: {
2110
+ byFile: false,
2111
+ names: ['build.gradle', 'build.gradle.kts', 'settings.gradle', 'settings.gradle.kts']
2112
+ },
2113
+ maven: {
2114
+ byFile: true,
2115
+ nocaseGlob: '{*-*.,}pom{.xml,}',
2116
+ walkGlob: '*[pP][oO][mM]*'
2117
+ },
2118
+ sbt: {
2119
+ byFile: false,
2120
+ names: ['build.sbt']
2121
+ }
2096
2122
  };
2097
- for (const tool of BUILD_TOOLS) {
2098
- for (const marker of MARKERS_BY_TOOL[tool]) {
2099
- TOOL_BY_MARKER[marker] = tool;
2123
+
2124
+ // sbt happily runs in any directory, synthesizing a default project from its
2125
+ // name, so an sbt run outside a build yields a plausible but bogus SBOM. Maven
2126
+ // and Gradle refuse such a directory themselves.
2127
+ function looksLikeSbtBuild(projectDir) {
2128
+ return u.existsSync(path.resolve(projectDir, 'build.sbt')) || u.existsSync(path.resolve(projectDir, 'project'));
2129
+ }
2130
+ function resolveBuildToolBin(tool, projectDir, bin) {
2131
+ if (bin) {
2132
+ return bin;
2133
+ }
2134
+ const wrapperName = BUILD_TOOL_WRAPPER[tool];
2135
+ if (wrapperName && u.existsSync(path.resolve(projectDir, wrapperName))) {
2136
+ return `./${wrapperName}`;
2100
2137
  }
2138
+ return DEFAULT_BUILD_TOOL_BIN[tool];
2139
+ }
2140
+
2141
+ // Distinct for every build sharing a directory.
2142
+ function socketFactsFileName(tool, entryFile) {
2143
+ if (!BUILD_FILES[tool].byFile) {
2144
+ return `${tool}${constants.default.DOT_SOCKET_DOT_FACTS_JSON}`;
2145
+ }
2146
+ return entryFile ? `${path.basename(entryFile)}${constants.default.DOT_SOCKET_DOT_FACTS_JSON}` : undefined;
2147
+ }
2148
+
2149
+ const BUILD_TOOLS = ['gradle', 'maven', 'sbt'];
2150
+ const BUILD_FILE_MATCHERS = new Map(BUILD_TOOLS.map(tool => {
2151
+ const files = BUILD_FILES[tool];
2152
+ return [tool, files.byFile ? vendor.micromatchExports.matcher(files.nocaseGlob, {
2153
+ dot: true,
2154
+ nocase: true
2155
+ }) : basename => files.names.includes(basename)];
2156
+ }));
2157
+
2158
+ // `entryFile` (a basename in `dir`) is set only for a tool that addresses its
2159
+ // builds by file, where one directory can hold several builds.
2160
+
2161
+ function buildRootPath(root) {
2162
+ return path.join(root.dir, root.entryFile ?? '');
2163
+ }
2164
+ function buildFileTool(tools, basename) {
2165
+ return tools.find(tool => BUILD_FILE_MATCHERS.get(tool)?.(basename));
2101
2166
  }
2102
2167
 
2103
2168
  // A disabled ecosystem is skipped entirely by the glob scan below, so a
@@ -2148,20 +2213,24 @@ async function addCoveredBuildFiles(covered, buildRoot, projects) {
2148
2213
  }
2149
2214
  }
2150
2215
 
2151
- // `dir` must be realpath-resolved, as findBuildToolCandidates returns it.
2152
- function isBuildRootCovered(covered, tool, dir) {
2153
- return MARKERS_BY_TOOL[tool].some(marker => covered.has(path.join(dir, marker)));
2216
+ // `root.dir` must be realpath-resolved, as findBuildToolCandidates returns it.
2217
+ function isBuildRootCovered(covered, tool, root) {
2218
+ const files = BUILD_FILES[tool];
2219
+ if (files.byFile) {
2220
+ return !!root.entryFile && covered.has(buildRootPath(root));
2221
+ }
2222
+ return files.names.some(name => covered.has(path.join(root.dir, name)));
2223
+ }
2224
+ function compareStrings(a, b) {
2225
+ return a < b ? -1 : a > b ? 1 : 0;
2154
2226
  }
2155
- function sortByDepthThenPath(dirs, cwd) {
2156
- return [...dirs].sort((a, b) => {
2157
- const relA = path.relative(cwd, a);
2158
- const relB = path.relative(cwd, b);
2227
+ function sortByDepthThenPath(roots, cwd) {
2228
+ return [...roots].sort((a, b) => {
2229
+ const relA = path.relative(cwd, a.dir);
2230
+ const relB = path.relative(cwd, b.dir);
2159
2231
  const depthA = relA === '' ? 0 : relA.split(path.sep).length;
2160
2232
  const depthB = relB === '' ? 0 : relB.split(path.sep).length;
2161
- if (depthA !== depthB) {
2162
- return depthA - depthB;
2163
- }
2164
- return relA < relB ? -1 : relA > relB ? 1 : 0;
2233
+ return depthA - depthB || compareStrings(relA, relB) || compareStrings(a.entryFile ?? '', b.entryFile ?? '');
2165
2234
  });
2166
2235
  }
2167
2236
 
@@ -2170,36 +2239,56 @@ function sortByDepthThenPath(dirs, cwd) {
2170
2239
  async function findBuildToolCandidates({
2171
2240
  cwd,
2172
2241
  excludePaths,
2173
- sockJson
2242
+ sockJson,
2243
+ socketConfig
2174
2244
  }) {
2175
2245
  const enabledTools = BUILD_TOOLS.filter(tool => !sockJson.defaults?.manifest?.[tool]?.disabled);
2176
2246
  const result = new Map();
2177
2247
  if (!enabledTools.length) {
2178
2248
  return result;
2179
2249
  }
2180
- const patterns = enabledTools.flatMap(tool => MARKERS_BY_TOOL[tool].map(marker => `**/${marker}`));
2181
2250
  const additionalIgnores = (excludePaths ?? []).flatMap(excludePathToScanIgnores);
2182
- const hits = await utils.globWithGitIgnore(patterns, {
2251
+ // Build files are matched after the walk, as a scan matches them after its
2252
+ // ignores, so --exclude-paths and .gitignore stay case-sensitive.
2253
+ const hits = await utils.globWithGitIgnore(enabledTools.flatMap(tool => {
2254
+ const files = BUILD_FILES[tool];
2255
+ return (files.byFile ? [files.walkGlob] : files.names).map(glob => `**/${glob}`);
2256
+ }), {
2183
2257
  absolute: true,
2184
2258
  additionalIgnores,
2185
- cwd
2186
- });
2187
- const dirSetsByTool = new Map(enabledTools.map(tool => [tool, new Set()]));
2188
- const resolvedHits = await Promise.all(hits.map(async hit => ({
2189
- dir: await utils.realpathOrResolved(path.dirname(hit)),
2190
- tool: TOOL_BY_MARKER[path.basename(hit)]
2259
+ cwd,
2260
+ socketConfig
2261
+ });
2262
+ const rootsByTool = new Map(enabledTools.map(tool => [tool, new Map()]));
2263
+ const matched = hits.flatMap(hit => {
2264
+ const basename = path.basename(hit);
2265
+ const tool = buildFileTool(enabledTools, basename);
2266
+ return tool ? [{
2267
+ basename,
2268
+ hit,
2269
+ tool
2270
+ }] : [];
2271
+ });
2272
+ const roots = await Promise.all(matched.map(async ({
2273
+ basename,
2274
+ hit,
2275
+ tool
2276
+ }) => ({
2277
+ root: {
2278
+ dir: await utils.realpathOrResolved(path.dirname(hit)),
2279
+ entryFile: BUILD_FILES[tool].byFile ? basename : undefined
2280
+ },
2281
+ tool
2191
2282
  })));
2192
2283
  for (const {
2193
- dir,
2284
+ root,
2194
2285
  tool
2195
- } of resolvedHits) {
2196
- if (tool) {
2197
- dirSetsByTool.get(tool)?.add(dir);
2198
- }
2286
+ } of roots) {
2287
+ rootsByTool.get(tool)?.set(buildRootPath(root), root);
2199
2288
  }
2200
2289
  const realCwd = await utils.realpathOrResolved(cwd);
2201
2290
  for (const tool of enabledTools) {
2202
- result.set(tool, sortByDepthThenPath([...(dirSetsByTool.get(tool) ?? [])], realCwd));
2291
+ result.set(tool, sortByDepthThenPath([...(rootsByTool.get(tool)?.values() ?? [])], realCwd));
2203
2292
  }
2204
2293
  return result;
2205
2294
  }
@@ -2292,56 +2381,6 @@ function formatMissingEnvVarRefs(missing) {
2292
2381
  return `references ${names}, which ${verb} not set in this environment`;
2293
2382
  }
2294
2383
 
2295
- // PATH fallback when no `bin` and no project wrapper.
2296
- const DEFAULT_BUILD_TOOL_BIN = {
2297
- __proto__: null,
2298
- gradle: 'gradle',
2299
- maven: 'mvn',
2300
- sbt: 'sbt'
2301
- };
2302
-
2303
- // Project-local wrapper, preferred because it pins the expected build-tool
2304
- // version. sbt has no wrapper convention. POSIX names only (no win32 target).
2305
- const BUILD_TOOL_WRAPPER = {
2306
- __proto__: null,
2307
- gradle: 'gradlew',
2308
- maven: 'mvnw'
2309
- };
2310
-
2311
- // Gradle (8+) and sbt hold one build per directory; Maven builds are addressed
2312
- // by POM file, so `mvn -f other-pom.xml` puts a second build in the directory.
2313
- const ADDRESSED_BY_FILE = {
2314
- __proto__: null,
2315
- gradle: false,
2316
- maven: true,
2317
- sbt: false
2318
- };
2319
-
2320
- // sbt happily runs in any directory, synthesizing a default project from its
2321
- // name, so an sbt run outside a build yields a plausible but bogus SBOM. Maven
2322
- // and Gradle refuse such a directory themselves.
2323
- function looksLikeSbtBuild(projectDir) {
2324
- return u.existsSync(path.resolve(projectDir, 'build.sbt')) || u.existsSync(path.resolve(projectDir, 'project'));
2325
- }
2326
- function resolveBuildToolBin(tool, projectDir, bin) {
2327
- if (bin) {
2328
- return bin;
2329
- }
2330
- const wrapperName = BUILD_TOOL_WRAPPER[tool];
2331
- if (wrapperName && u.existsSync(path.resolve(projectDir, wrapperName))) {
2332
- return `./${wrapperName}`;
2333
- }
2334
- return DEFAULT_BUILD_TOOL_BIN[tool];
2335
- }
2336
-
2337
- // Distinct for every build sharing a directory.
2338
- function socketFactsFileName(tool, entryFile) {
2339
- if (!ADDRESSED_BY_FILE[tool]) {
2340
- return `${tool}${constants.default.DOT_SOCKET_DOT_FACTS_JSON}`;
2341
- }
2342
- return entryFile ? `${path.basename(entryFile)}${constants.default.DOT_SOCKET_DOT_FACTS_JSON}` : undefined;
2343
- }
2344
-
2345
2384
  // Gradle's variant-aware resolver: distinct exceptions give mutually-exclusive
2346
2385
  // phrasing, so most-specific-first substring checks classify reliably.
2347
2386
  function classifyGradleFailure(detail) {
@@ -3805,7 +3844,8 @@ async function invokeMaven(opts, coanaTask, tmpDirPrefix) {
3805
3844
  // `validate` is the cheapest phase that triggers the extension; no compile
3806
3845
  // needed (analysis uses configured paths, not classes).
3807
3846
  const props = [`-Dmaven.ext.class.path=${jarPath}`, `-Dcoana.task=${coanaTask}`, `-Dsocket.recordsFile=${recordsFile}`, ...commonProps(opts, '-D')];
3808
- const args = [...props, ...(opts.toolOpts ?? []), '--batch-mode', 'validate'];
3847
+ const fileOpts = opts.entryFile ? ['--file', opts.entryFile] : [];
3848
+ const args = [...props, ...fileOpts, ...(opts.toolOpts ?? []), '--batch-mode', 'validate'];
3809
3849
  const out = await runNeverThrow(bin, args, opts);
3810
3850
  return await assembleFromRecords('maven', out, recordsFile);
3811
3851
  });
@@ -3841,6 +3881,7 @@ async function runManifestFacts({
3841
3881
  buildOpts,
3842
3882
  cwd,
3843
3883
  ecosystem,
3884
+ entryFile,
3844
3885
  excludeConfigs,
3845
3886
  excludePaths,
3846
3887
  hints = FLAG_HINTS,
@@ -3867,9 +3908,14 @@ async function runManifestFacts({
3867
3908
  logger.logger.fail(`No sbt build found at \`${cwd}\` (expected a build.sbt file or a project directory).`);
3868
3909
  return null;
3869
3910
  }
3870
- logger.logger.info(`Generating Socket facts for the ${ecosystem} project at \`${cwd}\` ...`);
3911
+ const location = buildRootPath({
3912
+ dir: cwd,
3913
+ entryFile
3914
+ });
3915
+ logger.logger.info(`Generating Socket facts for the ${ecosystem} project at \`${location}\` ...`);
3871
3916
  const scriptOpts = {
3872
3917
  bin: bin || undefined,
3918
+ entryFile,
3873
3919
  excludeConfigs: excludeConfigs || undefined,
3874
3920
  excludePaths: excludePaths?.length ? excludePaths : undefined,
3875
3921
  // `env` replaces the spawned process's whole environment, not just JAVA_HOME.
@@ -3969,7 +4015,7 @@ async function runManifestFacts({
3969
4015
  // regardless of `ignoreUnresolved` — that flag only tolerates dependencies a
3970
4016
  // successful run couldn't resolve.
3971
4017
  process.exitCode = 1;
3972
- logger.logger.fail(`The ${ecosystem} build failed (exit code ${code}) before producing any Socket facts.`);
4018
+ logger.logger.fail(`The ${ecosystem} build at \`${location}\` failed (exit code ${code}) before producing any Socket facts.`);
3973
4019
  return null;
3974
4020
  }
3975
4021
 
@@ -4032,13 +4078,40 @@ function getSkipReason(ecosystem, disabled, facts) {
4032
4078
  }
4033
4079
  return undefined;
4034
4080
  }
4035
- // Nearest already-confirmed-disabled ancestor of `dir`, if any - lets the
4081
+ // Maven 3's options starting with `f`; its GnuParser takes a long name after
4082
+ // one dash too, and splits `-fX` and `-f=X` into `-f X`.
4083
+ const MAVEN_FILE_OPTS = new Set(['f', 'file']);
4084
+ const MAVEN_OTHER_F_OPTS = new Set(['fae', 'fail-at-end', 'fail-fast', 'fail-never', 'ff', 'fn']);
4085
+
4086
+ // Each POM is built by its own `--file`, so a user one is dropped.
4087
+ function withoutMavenFileOpts(dir, opts) {
4088
+ const kept = [];
4089
+ for (let i = 0, {
4090
+ length
4091
+ } = opts; i < length; i += 1) {
4092
+ const opt = opts[i];
4093
+ const name = opt.replace(/^--?/, '');
4094
+ if (opt === name || MAVEN_OTHER_F_OPTS.has(name)) {
4095
+ kept.push(opt);
4096
+ } else if (MAVEN_FILE_OPTS.has(name)) {
4097
+ i += 1;
4098
+ } else if (!MAVEN_FILE_OPTS.has(name.split('=')[0]) && !(opt.startsWith('-f') && !opt.startsWith('--'))) {
4099
+ kept.push(opt);
4100
+ }
4101
+ }
4102
+ if (kept.length !== opts.length) {
4103
+ logger.logger.warn(`Ignoring --file in mavenOpts for ${dir}: each POM there is built by its own file.`);
4104
+ }
4105
+ return kept;
4106
+ }
4107
+
4108
+ // Nearest already-confirmed-disabled `dir` or ancestor of it, if any - lets the
4036
4109
  // caller shorten `readSocketJsonCascade`'s walk to start there instead of
4037
4110
  // all the way back at `cwd`, without skipping any nested override.
4038
4111
  function nearestDisabledRoot(dir, disabledRoots) {
4039
4112
  let nearest;
4040
4113
  for (const root of disabledRoots) {
4041
- if (dir.startsWith(`${root.dir}${path.sep}`) && (!nearest || root.dir.length > nearest.dir.length)) {
4114
+ if ((dir === root.dir || dir.startsWith(`${root.dir}${path.sep}`)) && (!nearest || root.dir.length > nearest.dir.length)) {
4042
4115
  nearest = root;
4043
4116
  }
4044
4117
  }
@@ -4058,54 +4131,71 @@ async function runEcosystemCandidates({
4058
4131
  }) {
4059
4132
  const outcomes = [];
4060
4133
  const covered = new Set();
4061
- for (const [ecosystem, dirs] of candidatesByTool) {
4134
+ for (const [ecosystem, roots] of candidatesByTool) {
4062
4135
  const disabledRoots = [];
4063
- for (let dirIndex = 0; dirIndex < dirs.length; dirIndex += 1) {
4064
- const dir = dirs[dirIndex];
4065
- if (isBuildRootCovered(covered, ecosystem, dir)) {
4136
+ const configByDir = new Map();
4137
+ for (const {
4138
+ 0: rootIndex,
4139
+ 1: root
4140
+ } of roots.entries()) {
4141
+ const {
4142
+ dir,
4143
+ entryFile
4144
+ } = root;
4145
+ if (isBuildRootCovered(covered, ecosystem, root)) {
4066
4146
  outcomes.push({
4067
- dir,
4147
+ ...root,
4068
4148
  ecosystem,
4069
4149
  status: 'skippedCovered'
4070
4150
  });
4071
4151
  continue;
4072
4152
  }
4073
- const nearestRoot = nearestDisabledRoot(dir, disabledRoots);
4074
- const sockJson = nearestRoot ? utils.readSocketJsonCascade(dir, nearestRoot.dir, nearestRoot.sockJson) : utils.readSocketJsonCascade(dir, realCwd, rootSockJson);
4153
+ let dirConfig = configByDir.get(dir);
4154
+ if (!dirConfig) {
4155
+ const nearestRoot = nearestDisabledRoot(dir, disabledRoots);
4156
+ const sockJson = nearestRoot ? utils.readSocketJsonCascade(dir, nearestRoot.dir, nearestRoot.sockJson) : utils.readSocketJsonCascade(dir, realCwd, rootSockJson);
4157
+ dirConfig = {
4158
+ ...resolveEcosystemConfig(ecosystem, dir, sockJson),
4159
+ excludePaths: projectIgnorePathsToReachExcludePaths(excludePaths, {
4160
+ cwd: realCwd,
4161
+ target: dir
4162
+ })
4163
+ };
4164
+ configByDir.set(dir, dirConfig);
4165
+ if (dirConfig.skipReason) {
4166
+ // Only warn for a genuinely new disabled root, not one already
4167
+ // covered by an ancestor's warning above - otherwise a big disabled
4168
+ // reactor with hundreds of nested poms would spam one warning line
4169
+ // per pom for what's really a single root cause. The aggregate
4170
+ // count still shows up in the final summary either way.
4171
+ if (!nearestRoot) {
4172
+ logger.logger.warn(`Skipping ${dir} (${ecosystem}): ${dirConfig.skipReason}.`);
4173
+ }
4174
+ disabledRoots.push({
4175
+ dir,
4176
+ sockJson
4177
+ });
4178
+ }
4179
+ }
4075
4180
  const {
4076
4181
  bin,
4077
4182
  buildOpts,
4078
4183
  excludeConfigs,
4184
+ excludePaths: excludePathsForRoot,
4079
4185
  ignoreUnresolved,
4080
4186
  includeConfigs,
4081
4187
  javaHome,
4082
4188
  skipReason,
4083
4189
  verbose: configVerbose
4084
- } = resolveEcosystemConfig(ecosystem, dir, sockJson);
4190
+ } = dirConfig;
4085
4191
  if (skipReason) {
4086
- // Only warn for a genuinely new disabled root, not one already
4087
- // covered by an ancestor's warning above - otherwise a big disabled
4088
- // reactor with hundreds of nested poms would spam one warning line
4089
- // per pom for what's really a single root cause. The aggregate
4090
- // count still shows up in the final summary either way.
4091
- if (!nearestRoot) {
4092
- logger.logger.warn(`Skipping ${dir} (${ecosystem}): ${skipReason}.`);
4093
- }
4094
4192
  outcomes.push({
4095
- dir,
4193
+ ...root,
4096
4194
  ecosystem,
4097
4195
  status: 'skippedDisabled'
4098
4196
  });
4099
- disabledRoots.push({
4100
- dir,
4101
- sockJson
4102
- });
4103
4197
  continue;
4104
4198
  }
4105
- const excludePathsForRoot = projectIgnorePathsToReachExcludePaths(excludePaths, {
4106
- cwd: realCwd,
4107
- target: dir
4108
- });
4109
4199
 
4110
4200
  // eslint-disable-next-line no-await-in-loop
4111
4201
  const result = await runManifestFacts({
@@ -4113,6 +4203,7 @@ async function runEcosystemCandidates({
4113
4203
  buildOpts,
4114
4204
  cwd: dir,
4115
4205
  ecosystem,
4206
+ entryFile,
4116
4207
  excludeConfigs,
4117
4208
  excludePaths: excludePathsForRoot,
4118
4209
  hints: {
@@ -4131,17 +4222,17 @@ async function runEcosystemCandidates({
4131
4222
  });
4132
4223
  if (result === null) {
4133
4224
  outcomes.push({
4134
- dir,
4225
+ ...root,
4135
4226
  ecosystem,
4136
4227
  status: 'failed'
4137
4228
  });
4138
- const abortedDirs = dirs.slice(dirIndex + 1);
4139
- if (abortedDirs.length) {
4140
- logger.logger.warn(`Aborting ${ecosystem} discovery: ${dir} failed to generate Socket facts, and without them the remaining build roots cannot be safely classified as covered or independent.`);
4229
+ const abortedRoots = roots.slice(rootIndex + 1);
4230
+ if (abortedRoots.length) {
4231
+ logger.logger.warn(`Aborting ${ecosystem} discovery: ${buildRootPath(root)} failed to generate Socket facts, and without them the remaining build roots cannot be safely classified as covered or independent.`);
4141
4232
  }
4142
- for (const abortedDir of abortedDirs) {
4233
+ for (const aborted of abortedRoots) {
4143
4234
  outcomes.push({
4144
- dir: abortedDir,
4235
+ ...aborted,
4145
4236
  ecosystem,
4146
4237
  status: 'aborted'
4147
4238
  });
@@ -4150,18 +4241,18 @@ async function runEcosystemCandidates({
4150
4241
  }
4151
4242
  if (!result) {
4152
4243
  outcomes.push({
4153
- dir,
4244
+ ...root,
4154
4245
  ecosystem,
4155
4246
  status: 'empty'
4156
4247
  });
4157
4248
  continue;
4158
4249
  }
4159
4250
 
4160
- // `-f`/`-p` can root the reactor away from `dir`.
4251
+ // Gradle's `-p` can root the build away from `dir`.
4161
4252
  // eslint-disable-next-line no-await-in-loop
4162
4253
  await addCoveredBuildFiles(covered, path.dirname(result.factsPath), result.projects);
4163
4254
  outcomes.push({
4164
- dir,
4255
+ ...root,
4165
4256
  ecosystem,
4166
4257
  factsPath: result.factsPath,
4167
4258
  status: 'generated'
@@ -4194,6 +4285,7 @@ async function generateRecursiveManifests({
4194
4285
  const candidatesByTool = await findBuildToolCandidates({
4195
4286
  cwd,
4196
4287
  excludePaths,
4288
+ socketConfig: utils.loadSocketConfig(cwd),
4197
4289
  sockJson: withoutDisabledFlags(rootSockJson)
4198
4290
  });
4199
4291
  const runAll = resolvedSbtTmpDir => runEcosystemCandidates({
@@ -4259,7 +4351,7 @@ function resolveEcosystemConfig(ecosystem, dir, sockJson) {
4259
4351
  const bin = config?.bin ?? undefined;
4260
4352
  return {
4261
4353
  bin: bin ?? resolveBuildToolBin('maven', dir),
4262
- buildOpts: parseBuildToolOpts(config?.mavenOpts ?? undefined),
4354
+ buildOpts: withoutMavenFileOpts(dir, parseBuildToolOpts(config?.mavenOpts ?? undefined)),
4263
4355
  excludeConfigs: config?.excludeConfigs ?? '',
4264
4356
  ignoreUnresolved: Boolean(config?.ignoreUnresolved),
4265
4357
  includeConfigs: config?.includeConfigs ?? '',
@@ -4377,6 +4469,234 @@ sockJson, cwd = process.cwd()) {
4377
4469
  return output;
4378
4470
  }
4379
4471
 
4472
+ function findEnvironmentKey(env, name, platform = process.platform) {
4473
+ if (platform !== 'win32') {
4474
+ return name;
4475
+ }
4476
+ const lower = name.toLowerCase();
4477
+ return Object.keys(env).find(key => key.toLowerCase() === lower) ?? name;
4478
+ }
4479
+ function getBazelEnvironment(overrides = {}, platform = process.platform) {
4480
+ const env = {
4481
+ ...process.env
4482
+ };
4483
+ for (const [key, value] of Object.entries(overrides)) {
4484
+ env[findEnvironmentKey(env, key, platform)] = value;
4485
+ }
4486
+ return env;
4487
+ }
4488
+ function getBazelEnvironmentValue(env, name) {
4489
+ return env[findEnvironmentKey(env, name)];
4490
+ }
4491
+ function prependBazelPath(env, dir) {
4492
+ const key = findEnvironmentKey(env, 'PATH');
4493
+ return {
4494
+ ...env,
4495
+ [key]: `${dir}${path.delimiter}${env[key] ?? ''}`
4496
+ };
4497
+ }
4498
+ async function findBazelExecutable(name, env, cwd) {
4499
+ const searchPath = (getBazelEnvironmentValue(env, 'PATH') ?? '').split(path.delimiter).map(entry => path.resolve(cwd, entry.replace(/^"(.*)"$/, '$1'))).join(path.delimiter);
4500
+ const found = await vendor.libExports$1(name, {
4501
+ nothrow: true,
4502
+ path: searchPath,
4503
+ pathExt: getBazelEnvironmentValue(env, 'PATHEXT')
4504
+ });
4505
+ return found ? resolveBazelExecutable(found) : undefined;
4506
+ }
4507
+ function resolveBazelExecutable(file) {
4508
+ const resolved = u.realpathSync(file);
4509
+ const extension = path.extname(resolved).toLowerCase();
4510
+ if (process.platform !== 'win32' || !['', '.cmd', '.ps1'].includes(extension)) {
4511
+ return resolved;
4512
+ }
4513
+ try {
4514
+ return bin.resolveBinPathSync(resolved);
4515
+ } catch (e) {
4516
+ if (extension === '.ps1') {
4517
+ throw new utils.InputError('This PowerShell Bazel launcher could not be resolved to a Node entry point. Pass a native .exe, a .cmd or .bat wrapper, or a JavaScript entry point with --bazel.');
4518
+ }
4519
+ if (utils.isErrnoException(e) && e.code === 'ENOENT') {
4520
+ return resolved;
4521
+ }
4522
+ throw e;
4523
+ }
4524
+ }
4525
+ function isNodeScript(bin) {
4526
+ if (/\.[cm]?js$/i.test(bin)) {
4527
+ return true;
4528
+ }
4529
+ if (path.extname(bin) || process.platform !== 'win32' || !u.existsSync(bin)) {
4530
+ return false;
4531
+ }
4532
+ const fd = u.openSync(bin, 'r');
4533
+ try {
4534
+ const header = Buffer.alloc(256);
4535
+ const length = u.readSync(fd, header);
4536
+ return /^#![^\r\n]*\bnode(?:\s|$)/.test(header.toString('utf8', 0, length));
4537
+ } finally {
4538
+ u.closeSync(fd);
4539
+ }
4540
+ }
4541
+ const BATCH_METACHARACTERS = /[ !%^&()<>|"]/g;
4542
+ function escapeBatchMetacharacters(value) {
4543
+ return value.replace(BATCH_METACHARACTERS, '^$&');
4544
+ }
4545
+ function escapeBatchArgument(value) {
4546
+ if (/[\r\n\0]/.test(value)) {
4547
+ throw new utils.InputError('Bazel batch launchers cannot accept arguments containing newlines or NUL characters. Use a native .exe launcher.');
4548
+ }
4549
+ const quoted = `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
4550
+ // Batch forwarding parses arguments twice, so protect each shell metacharacter twice.
4551
+ return escapeBatchMetacharacters(escapeBatchMetacharacters(quoted));
4552
+ }
4553
+ function buildBazelProcessInvocation(bin, args, env) {
4554
+ if (isNodeScript(bin)) {
4555
+ return {
4556
+ command: process.execPath,
4557
+ args: [bin, ...args],
4558
+ windowsVerbatimArguments: false
4559
+ };
4560
+ }
4561
+ if (process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(bin)) {
4562
+ const script = [escapeBatchMetacharacters(bin), ...args.map(escapeBatchArgument)].join(' ');
4563
+ return {
4564
+ command: getBazelEnvironmentValue(env, 'ComSpec') ?? 'cmd.exe',
4565
+ args: ['/d', '/v:off', '/s', '/c', `"${script}"`],
4566
+ windowsVerbatimArguments: true
4567
+ };
4568
+ }
4569
+ return {
4570
+ command: bin,
4571
+ args,
4572
+ windowsVerbatimArguments: false
4573
+ };
4574
+ }
4575
+ class BazelTimeoutError extends Error {
4576
+ constructor(timeout, cause) {
4577
+ super(`Bazel did not finish within ${timeout}ms`, {
4578
+ cause
4579
+ });
4580
+ this.stdout = readOutput(cause, 'stdout');
4581
+ this.stderr = readOutput(cause, 'stderr');
4582
+ }
4583
+ }
4584
+ function readOutput(error, key) {
4585
+ if (error && typeof error === 'object' && key in error) {
4586
+ const value = Reflect.get(error, key);
4587
+ return typeof value === 'string' ? value : '';
4588
+ }
4589
+ return '';
4590
+ }
4591
+ function makeAbortError(reason) {
4592
+ const error = new Error('The operation was aborted', {
4593
+ cause: reason
4594
+ });
4595
+ error.name = 'AbortError';
4596
+ return error;
4597
+ }
4598
+
4599
+ // Launchers can leave Bazel holding the output pipes after the launcher is killed.
4600
+ const PIPE_RELEASE_GRACE_MS = 5_000;
4601
+ async function killProcessTree(child) {
4602
+ const {
4603
+ pid
4604
+ } = child;
4605
+ if (pid === undefined) {
4606
+ return;
4607
+ }
4608
+ if (process.platform === 'win32') {
4609
+ await new Promise(resolve => {
4610
+ childProcess.execFile('taskkill', ['/pid', String(pid), '/t', '/f'], () => resolve());
4611
+ });
4612
+ return;
4613
+ }
4614
+ try {
4615
+ process.kill(-pid, 'SIGTERM');
4616
+ } catch (e) {
4617
+ if (!utils.isErrnoException(e) || e.code !== 'ESRCH') {
4618
+ throw e;
4619
+ }
4620
+ }
4621
+ }
4622
+ async function spawnBazel(bin, args, {
4623
+ signal = constants.default.abortSignal,
4624
+ timeout,
4625
+ ...options
4626
+ }) {
4627
+ const env = getBazelEnvironment(options.env);
4628
+ const invocation = buildBazelProcessInvocation(bin, args, env);
4629
+ const controller = new AbortController();
4630
+ const pending = spawn.spawn(invocation.command, invocation.args, {
4631
+ ...options,
4632
+ // A process group of its own lets a timeout or abort reach everything the launcher started.
4633
+ detached: process.platform !== 'win32',
4634
+ env,
4635
+ shell: false,
4636
+ signal: controller.signal,
4637
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
4638
+ });
4639
+ let timedOut = false;
4640
+ let releaseTimer;
4641
+ const stop = () => {
4642
+ releaseTimer ??= setTimeout(() => {
4643
+ pending.process.stdout?.destroy();
4644
+ pending.process.stderr?.destroy();
4645
+ }, PIPE_RELEASE_GRACE_MS);
4646
+ return killProcessTree(pending.process);
4647
+ };
4648
+ const onAbort = () => {
4649
+ // taskkill finds descendants through the launcher, so the launcher must outlive the tree kill.
4650
+ void stop().finally(() => controller.abort(signal.reason));
4651
+ };
4652
+ if (signal.aborted) {
4653
+ onAbort();
4654
+ } else {
4655
+ signal.addEventListener('abort', onAbort, {
4656
+ once: true
4657
+ });
4658
+ }
4659
+ const timer = timeout ? setTimeout(() => {
4660
+ timedOut = true;
4661
+ void stop();
4662
+ }, timeout) : undefined;
4663
+ let result;
4664
+ try {
4665
+ result = await pending;
4666
+ } catch (e) {
4667
+ if (timedOut && timeout) {
4668
+ throw new BazelTimeoutError(timeout, e);
4669
+ }
4670
+ // On Windows the tree kill ends the launcher first, so the spawn rejects with its exit code.
4671
+ if (signal.aborted && !(e instanceof Error && e.name === 'AbortError')) {
4672
+ throw makeAbortError(signal.reason);
4673
+ }
4674
+ throw e;
4675
+ } finally {
4676
+ clearTimeout(timer);
4677
+ clearTimeout(releaseTimer);
4678
+ signal.removeEventListener('abort', onAbort);
4679
+ }
4680
+ // Output is incomplete when the pipes were released after a timeout.
4681
+ if (timedOut && timeout) {
4682
+ throw new BazelTimeoutError(timeout, result);
4683
+ }
4684
+ return result;
4685
+ }
4686
+ function buildBazelStartupFlags(opts) {
4687
+ const startup = [];
4688
+ if (opts.bazelRc) {
4689
+ startup.push(`--bazelrc=${opts.bazelRc}`);
4690
+ }
4691
+ if (opts.outputUserRoot) {
4692
+ startup.push(`--output_user_root=${opts.outputUserRoot}`);
4693
+ }
4694
+ if (opts.bazelOutputBase) {
4695
+ startup.push(`--output_base=${opts.bazelOutputBase}`);
4696
+ }
4697
+ return startup;
4698
+ }
4699
+
4380
4700
  /**
4381
4701
  * Resolve the bazel binary to invoke for `socket manifest bazel`.
4382
4702
  *
@@ -4386,70 +4706,285 @@ sockJson, cwd = process.cwd()) {
4386
4706
  * 3. Fall back to `bazel` on PATH.
4387
4707
  * 4. If neither is found, throw InputError with install instructions.
4388
4708
  */
4389
- async function resolveBazelBinary(explicit) {
4709
+ async function resolveBazelBinary(explicit, env, cwd) {
4390
4710
  if (explicit) {
4391
- if (!u.existsSync(explicit)) {
4392
- throw new utils.InputError(`--bazel path does not exist: ${explicit}. Install bazelisk or bazel, or pass an existing path via --bazel.`);
4711
+ const resolved = path.resolve(cwd, explicit);
4712
+ if (!u.existsSync(resolved)) {
4713
+ throw new utils.InputError(`--bazel path does not exist: ${resolved}. Install bazelisk or bazel, or pass an existing path via --bazel.`);
4393
4714
  }
4394
- return explicit;
4715
+ return resolveBazelExecutable(resolved);
4395
4716
  }
4396
4717
  // Prefer bazelisk: respects .bazelversion in the workspace.
4397
- const bazelisk = await bin.whichBin('bazelisk', {
4398
- nothrow: true
4399
- });
4718
+ const bazelisk = await findBazelExecutable('bazelisk', env, cwd);
4400
4719
  if (bazelisk) {
4401
4720
  return bazelisk;
4402
4721
  }
4403
- const bazel = await bin.whichBin('bazel', {
4404
- nothrow: true
4405
- });
4722
+ const bazel = await findBazelExecutable('bazel', env, cwd);
4406
4723
  if (bazel) {
4407
4724
  return bazel;
4408
4725
  }
4409
4726
  throw new utils.InputError('Could not find bazelisk or bazel on PATH. ' + 'Install bazelisk (recommended; https://github.com/bazelbuild/bazelisk) ' + 'or bazel, or pass --bazel <path>.');
4410
4727
  }
4411
4728
 
4729
+ const BAZEL_OUTPUT_ROOT_PREFIX = 'socket-bazel-';
4730
+ function makeBazelOutputUserRoot(env, cwd) {
4731
+ const parent = getBazelEnvironmentValue(env, 'SOCKET_BAZEL_OUTPUT_ROOT');
4732
+ const resolved = parent ? path.resolve(cwd, parent) : os.tmpdir();
4733
+ u.mkdirSync(resolved, {
4734
+ recursive: true
4735
+ });
4736
+ return u.mkdtempSync(path.join(resolved, BAZEL_OUTPUT_ROOT_PREFIX));
4737
+ }
4738
+ const RM_OPTIONS = {
4739
+ recursive: true,
4740
+ force: true,
4741
+ maxRetries: 5,
4742
+ retryDelay: 200
4743
+ };
4744
+ async function removeBazelTempdir(dir) {
4745
+ try {
4746
+ try {
4747
+ await u.promises.rm(dir, RM_OPTIONS);
4748
+ } catch (e) {
4749
+ if (!utils.isErrnoException(e) || !['EACCES', 'EPERM'].includes(e.code ?? '')) {
4750
+ throw e;
4751
+ }
4752
+ await makeDirectoriesWritable(dir);
4753
+ await u.promises.rm(dir, RM_OPTIONS);
4754
+ }
4755
+ return true;
4756
+ } catch (error) {
4757
+ logger.logger.warn(`Bazel temporary directory could not be removed: ${dir}. ${utils.getErrorCause(error)}`);
4758
+ return false;
4759
+ }
4760
+ }
4761
+ async function makeDirectoriesWritable(root) {
4762
+ const pending = [root];
4763
+ while (pending.length) {
4764
+ const directory = pending.pop();
4765
+ if (!directory) {
4766
+ continue;
4767
+ }
4768
+ try {
4769
+ // Repository rules can unpack read-only directories. Keep links to user files untouched.
4770
+ // eslint-disable-next-line no-await-in-loop
4771
+ const stat = await u.promises.lstat(directory);
4772
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
4773
+ continue;
4774
+ }
4775
+ // eslint-disable-next-line no-await-in-loop
4776
+ await u.promises.chmod(directory, 0o700);
4777
+ // eslint-disable-next-line no-await-in-loop
4778
+ const entries = await u.promises.readdir(directory, {
4779
+ withFileTypes: true
4780
+ });
4781
+ for (const entry of entries) {
4782
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
4783
+ pending.push(path.join(directory, entry.name));
4784
+ }
4785
+ }
4786
+ } catch (e) {
4787
+ if (!utils.isErrnoException(e) || e.code !== 'ENOENT') {
4788
+ throw e;
4789
+ }
4790
+ }
4791
+ }
4792
+ }
4793
+ const SHUTDOWN_TIMEOUT_MS = 10_000;
4794
+ const SERVER_EXIT_TIMEOUT_MS = 5_000;
4795
+ async function cleanUpBazelOutputRoot(root, contexts) {
4796
+ // Ctrl-C aborts the scan's signal while the CLI keeps running, and cleanup must still run.
4797
+ const signal = AbortSignal.timeout(3 * SHUTDOWN_TIMEOUT_MS);
4798
+ let listed;
4799
+ try {
4800
+ listed = findServerPids(root);
4801
+ } catch (e) {
4802
+ logger.logger.warn(`Bazel output root kept because its servers could not be checked: ${root}. ${utils.getErrorCause(e)}`);
4803
+ }
4804
+ const servers = new Set(listed);
4805
+ const shutDown = await Promise.all(Array.from(contexts.values(), opts => shutDownBazelServer(opts, signal)));
4806
+ for (const pid of shutDown) {
4807
+ if (pid !== undefined) {
4808
+ servers.add(pid);
4809
+ }
4810
+ }
4811
+ const stopped = await Promise.all(Array.from(servers, stopBazelServer));
4812
+ if (!stopped.every(Boolean)) {
4813
+ logger.logger.warn(`Bazel output root kept because its server is still running: ${root}`);
4814
+ return false;
4815
+ }
4816
+ if (!listed) {
4817
+ return false;
4818
+ }
4819
+ return await removeBazelTempdir(root);
4820
+ }
4821
+
4822
+ // Returns the pid of a server this scan owns, read before shutdown removes its pid file.
4823
+ async function shutDownBazelServer(opts, signal) {
4824
+ const root = opts.outputUserRoot;
4825
+ if (!root) {
4826
+ return undefined;
4827
+ }
4828
+ const startupFlags = buildBazelStartupFlags(opts);
4829
+ const spawnOptions = {
4830
+ cwd: opts.cwd,
4831
+ ...(opts.env ? {
4832
+ env: opts.env
4833
+ } : {}),
4834
+ signal,
4835
+ timeout: SHUTDOWN_TIMEOUT_MS
4836
+ };
4837
+ let pid;
4838
+ try {
4839
+ // Only this scan can have started a server whose install or output base is in its fresh root.
4840
+ // An rc file can point the scan at a caller's install and output base.
4841
+ const info = await spawnBazel(opts.bin, [...startupFlags, 'info', 'install_base', 'output_base'], spawnOptions);
4842
+ const bases = parseBazelInfo(info.stdout);
4843
+ const outputBase = bases.get('output_base');
4844
+ if (!outputBase || ![bases.get('install_base'), outputBase].some(dir => dir && isPathInside(dir, root))) {
4845
+ return undefined;
4846
+ }
4847
+ pid = readServerPid(outputBase);
4848
+ await spawnBazel(opts.bin, [...startupFlags, 'shutdown'], spawnOptions);
4849
+ } catch (error) {
4850
+ logger.logger.warn(`Bazel shutdown failed in ${opts.cwd}. ${utils.getErrorCause(error)}`);
4851
+ }
4852
+ return pid;
4853
+ }
4854
+ function findServerPids(root) {
4855
+ if (!u.existsSync(root)) {
4856
+ return [];
4857
+ }
4858
+ const pids = [];
4859
+ for (const entry of u.readdirSync(root, {
4860
+ withFileTypes: true
4861
+ })) {
4862
+ const pid = entry.isDirectory() ? readServerPid(path.join(root, entry.name)) : undefined;
4863
+ if (pid !== undefined) {
4864
+ pids.push(pid);
4865
+ }
4866
+ }
4867
+ return pids;
4868
+ }
4869
+ function readServerPid(outputBase) {
4870
+ let contents;
4871
+ try {
4872
+ contents = u.readFileSync(path.join(outputBase, 'server', 'server.pid.txt'), 'utf8');
4873
+ } catch (e) {
4874
+ if (utils.isErrnoException(e) && e.code === 'ENOENT') {
4875
+ return undefined;
4876
+ }
4877
+ throw e;
4878
+ }
4879
+ const pid = Number(contents.trim());
4880
+ return Number.isSafeInteger(pid) && pid > 0 && isProcessAlive(pid) ? pid : undefined;
4881
+ }
4882
+ function isProcessAlive(pid) {
4883
+ try {
4884
+ process.kill(pid, 0);
4885
+ return true;
4886
+ } catch (e) {
4887
+ return utils.isErrnoException(e) && e.code === 'EPERM';
4888
+ }
4889
+ }
4890
+ async function waitForProcessExit(pid) {
4891
+ const deadline = Date.now() + SERVER_EXIT_TIMEOUT_MS;
4892
+ while (isProcessAlive(pid)) {
4893
+ if (Date.now() >= deadline) {
4894
+ return false;
4895
+ }
4896
+ // eslint-disable-next-line no-await-in-loop
4897
+ await require$$7.setTimeout(100);
4898
+ }
4899
+ return true;
4900
+ }
4901
+
4902
+ // A shutdown that exits successfully can still leave the server running, for example with
4903
+ // `shutdown --iff_heap_size_greater_than` in an rc file.
4904
+ async function stopBazelServer(pid) {
4905
+ if (await waitForProcessExit(pid)) {
4906
+ return true;
4907
+ }
4908
+ try {
4909
+ process.kill(pid, 'SIGKILL');
4910
+ } catch (e) {
4911
+ if (!utils.isErrnoException(e) || e.code !== 'ESRCH') {
4912
+ logger.logger.warn(`Bazel server ${pid} could not be stopped. ${utils.getErrorCause(e)}`);
4913
+ return false;
4914
+ }
4915
+ }
4916
+ return await waitForProcessExit(pid);
4917
+ }
4918
+ function parseBazelInfo(stdout) {
4919
+ const values = new Map();
4920
+ for (const line of stdout.split(/\r?\n/)) {
4921
+ const separator = line.indexOf(': ');
4922
+ if (separator > 0) {
4923
+ values.set(line.slice(0, separator), line.slice(separator + 2).trim());
4924
+ }
4925
+ }
4926
+ return values;
4927
+ }
4928
+ function isPathInside(child, parent) {
4929
+ const relative = path.relative(u.realpathSync.native(parent), u.realpathSync.native(child));
4930
+ return relative !== '' && relative.split(path.sep)[0] !== '..' && !path.isAbsolute(relative);
4931
+ }
4932
+
4412
4933
  // Default per-invocation timeout for bazel queries. Bazel cold-cache starts
4413
4934
  // can take several minutes; 10 minutes is generous while still bounding CI hangs.
4414
4935
  const BAZEL_QUERY_TIMEOUT_MS = 600_000;
4415
4936
  const STDERR_TAIL_BYTES = 4_096;
4416
4937
  const STDOUT_EXCERPT_BYTES = 1_024;
4417
-
4418
- // Splits the user-supplied --bazel-flags string on whitespace.
4419
- // Empty / undefined returns []. No shell parsing — quoted args with embedded
4420
- // whitespace are not supported (documented limitation; same trust model as
4421
- // gradleOpts).
4422
4938
  function splitBazelFlags(flags) {
4423
4939
  if (!flags) {
4424
4940
  return [];
4425
4941
  }
4426
- return flags.split(/\s+/).filter(Boolean);
4427
- }
4428
-
4429
- // Build the shared startup-flag prefix for any bazel invocation. Centralised
4430
- // so `--output_user_root` propagates to every spawn — principle 7 of the
4431
- // Maven design requires per-invocation server isolation across query,
4432
- // cquery, and `bazel mod` commands alike.
4433
- function buildStartupFlags(opts) {
4434
- const startup = [];
4435
- if (opts.bazelRc) {
4436
- startup.push(`--bazelrc=${opts.bazelRc}`);
4942
+ const args = [];
4943
+ let argument = '';
4944
+ let quote;
4945
+ let started = false;
4946
+ for (let index = 0; index < flags.length; index += 1) {
4947
+ const character = flags.charAt(index);
4948
+ if (quote) {
4949
+ if (character === quote) {
4950
+ if (flags.charAt(index + 1) === quote) {
4951
+ argument += quote;
4952
+ index += 1;
4953
+ } else {
4954
+ quote = undefined;
4955
+ }
4956
+ } else {
4957
+ argument += character;
4958
+ }
4959
+ } else if ((character === '"' || character === "'") && (!started || argument.endsWith('='))) {
4960
+ quote = character;
4961
+ started = true;
4962
+ } else if (/\s/.test(character)) {
4963
+ if (started) {
4964
+ args.push(argument);
4965
+ argument = '';
4966
+ started = false;
4967
+ }
4968
+ } else {
4969
+ argument += character;
4970
+ started = true;
4971
+ }
4437
4972
  }
4438
- if (opts.outputUserRoot) {
4439
- startup.push(`--output_user_root=${opts.outputUserRoot}`);
4973
+ if (quote) {
4974
+ throw new utils.InputError('--bazel-flags contains an unterminated quote. Close each quoted value. Backslashes are preserved literally.');
4440
4975
  }
4441
- if (opts.bazelOutputBase) {
4442
- startup.push(`--output_base=${opts.bazelOutputBase}`);
4976
+ if (started) {
4977
+ args.push(argument);
4443
4978
  }
4444
- return startup;
4979
+ return args;
4445
4980
  }
4446
4981
  function buildBazelModShowVisibleReposArgv(opts) {
4447
4982
  const userFlags = splitBazelFlags(opts.bazelFlags);
4448
- return [...buildStartupFlags(opts), 'mod', 'dump_repo_mapping', '', '--output=json', ...userFlags];
4983
+ return [...buildBazelStartupFlags(opts), 'mod', 'dump_repo_mapping', '', '--output=json', ...userFlags];
4449
4984
  }
4450
4985
  function buildBazelModShowMavenExtensionArgv(opts) {
4451
4986
  const userFlags = splitBazelFlags(opts.bazelFlags);
4452
- return [...buildStartupFlags(opts), 'mod', 'show_extension', '@rules_jvm_external//:extensions.bzl%maven',
4987
+ return [...buildBazelStartupFlags(opts), 'mod', 'show_extension', '@rules_jvm_external//:extensions.bzl%maven',
4453
4988
  // A read-only scan must never rewrite the user's MODULE.bazel.lock; pin
4454
4989
  // the lockfile read-only before user flags, mirroring the query/cquery
4455
4990
  // argv builders.
@@ -4462,7 +4997,7 @@ function buildBazelModShowMavenExtensionArgv(opts) {
4462
4997
  }
4463
4998
  function buildBazelModShowPipExtensionArgv(opts) {
4464
4999
  const userFlags = splitBazelFlags(opts.bazelFlags);
4465
- return [...buildStartupFlags(opts), 'mod', 'show_extension', '@rules_python//python/extensions:pip.bzl%pip',
5000
+ return [...buildBazelStartupFlags(opts), 'mod', 'show_extension', '@rules_python//python/extensions:pip.bzl%pip',
4466
5001
  // A read-only scan must never rewrite the user's MODULE.bazel.lock; pin
4467
5002
  // the lockfile read-only before user flags, mirroring the query/cquery
4468
5003
  // argv builders.
@@ -4474,7 +5009,7 @@ function buildBazelArgv(queryStr, opts, output = 'build') {
4474
5009
  // Keep query output stable and avoid updating Bazel lockfiles while extracting.
4475
5010
  const queryFlags = ['--lockfile_mode=off', '--noshow_progress'];
4476
5011
  const userFlags = splitBazelFlags(opts.bazelFlags);
4477
- return [...buildStartupFlags(opts), 'query', ...queryFlags, ...opts.invocationFlags, queryStr, `--output=${output}`, ...userFlags];
5012
+ return [...buildBazelStartupFlags(opts), 'query', ...queryFlags, ...opts.invocationFlags, queryStr, `--output=${output}`, ...userFlags];
4478
5013
  }
4479
5014
 
4480
5015
  // Lightweight presence-check cquery used by the tri-state probe classifier.
@@ -4484,7 +5019,7 @@ function buildBazelArgv(queryStr, opts, output = 'build') {
4484
5019
  // `bazel-cquery.mts` needs but the probe does not).
4485
5020
  function buildBazelProbeCqueryArgv(repoName, opts) {
4486
5021
  const userFlags = splitBazelFlags(opts.bazelFlags);
4487
- return [...buildStartupFlags(opts), 'cquery', '--lockfile_mode=off', '--noshow_progress', ...opts.invocationFlags, `@${repoName}//...`, '--output=label', '--keep_going', ...userFlags];
5022
+ return [...buildBazelStartupFlags(opts), 'cquery', '--lockfile_mode=off', '--noshow_progress', ...opts.invocationFlags, `@${repoName}//...`, '--output=label', '--keep_going', ...userFlags];
4488
5023
  }
4489
5024
  function stringField(value) {
4490
5025
  return typeof value === 'string' ? value : '';
@@ -4559,7 +5094,7 @@ async function runBazelQuery(queryStr, opts, output) {
4559
5094
  let result;
4560
5095
  try {
4561
5096
  spinner.start(`Running bazel query (${queryStr.slice(0, 80)})...`);
4562
- const output = await spawn.spawn(opts.bin, argv, {
5097
+ const output = await spawnBazel(opts.bin, argv, {
4563
5098
  cwd: opts.cwd,
4564
5099
  timeout: BAZEL_QUERY_TIMEOUT_MS,
4565
5100
  ...(opts.env ? {
@@ -4605,7 +5140,7 @@ async function runBazelOneShot(argv, opts, step) {
4605
5140
  const startedAt = Date.now();
4606
5141
  let result;
4607
5142
  try {
4608
- const output = await spawn.spawn(opts.bin, argv, {
5143
+ const output = await spawnBazel(opts.bin, argv, {
4609
5144
  cwd: opts.cwd,
4610
5145
  timeout: BAZEL_QUERY_TIMEOUT_MS,
4611
5146
  ...(opts.env ? {
@@ -4770,16 +5305,7 @@ function buildMetadataCqueryExpr(repoName) {
4770
5305
  // Build the full cquery argv for a per-repo metadata cquery. Exposed for
4771
5306
  // argv-shape unit tests without touching `spawn`.
4772
5307
  function buildMetadataCqueryArgv(repoName, opts) {
4773
- const startup = [];
4774
- if (opts.bazelRc) {
4775
- startup.push(`--bazelrc=${opts.bazelRc}`);
4776
- }
4777
- if (opts.outputUserRoot) {
4778
- startup.push(`--output_user_root=${opts.outputUserRoot}`);
4779
- }
4780
- if (opts.bazelOutputBase) {
4781
- startup.push(`--output_base=${opts.bazelOutputBase}`);
4782
- }
5308
+ const startup = buildBazelStartupFlags(opts);
4783
5309
  const userFlags = splitBazelFlags(opts.bazelFlags);
4784
5310
  return [...startup, 'cquery', '--lockfile_mode=off', '--noshow_progress', ...opts.invocationFlags, buildMetadataCqueryExpr(repoName), '--output=jsonproto', '--proto:output_rule_attrs=tags,maven_coordinates,deps,exports,runtime_deps', '--keep_going', ...userFlags];
4785
5311
  }
@@ -5117,7 +5643,7 @@ async function runMetadataCqueryForRepo(args) {
5117
5643
  const argv = buildMetadataCqueryArgv(repoName, opts);
5118
5644
  const startedAt = Date.now();
5119
5645
  try {
5120
- const result = await spawn.spawn(opts.bin, argv, {
5646
+ const result = await spawnBazel(opts.bin, argv, {
5121
5647
  cwd: workspaceRoot,
5122
5648
  timeout: timeoutMs,
5123
5649
  ...(opts.env ? {
@@ -5146,10 +5672,7 @@ async function runMetadataCqueryForRepo(args) {
5146
5672
  const err = e;
5147
5673
  const stdout = typeof err.stdout === 'string' ? err.stdout : '';
5148
5674
  const stderr = typeof err.stderr === 'string' ? err.stderr : '';
5149
- // On a `timeout`, the registry spawn kills the child, so Node sets
5150
- // `killed: true` and `signal: 'SIGTERM'` (or `SIGKILL`). There is no
5151
- // `timedOut` flag on the real rejection, so do not test for one.
5152
- const timedOut = err.killed === true || err.signal === 'SIGTERM' || err.signal === 'SIGKILL';
5675
+ const timedOut = e instanceof BazelTimeoutError;
5153
5676
  const {
5154
5677
  artifacts,
5155
5678
  unresolvedLabels
@@ -5176,25 +5699,19 @@ async function runMetadataCqueryForRepo(args) {
5176
5699
  }
5177
5700
  }
5178
5701
 
5179
- let probed = false;
5180
-
5181
- // Verifies `java` is functional in the current execution environment. Bazel
5182
- // JVM manifest extraction (rules_jvm_external → Coursier) requires a real
5183
- // JDK; the CLI does not attempt to discover Homebrew installs or mutate the
5184
- // caller's PATH/JAVA_HOME. If `java -version` fails we throw with an
5185
- // actionable message so the surfaced error names the prerequisite directly
5186
- // instead of relying on Bazel's downstream diagnostic.
5187
- function ensureJavaOnPath() {
5188
- if (probed) {
5189
- return;
5190
- }
5702
+ function ensureJavaOnPath(env, cwd) {
5703
+ const javaHome = getBazelEnvironmentValue(env, 'JAVA_HOME');
5704
+ const executable = process.platform === 'win32' ? 'java.exe' : 'java';
5705
+ const java = javaHome ? path.resolve(cwd, javaHome, 'bin', executable) : executable;
5191
5706
  try {
5192
- childProcess.execSync('java -version', {
5193
- stdio: 'ignore'
5707
+ childProcess.execFileSync(java, ['-version'], {
5708
+ cwd,
5709
+ env,
5710
+ stdio: 'ignore',
5711
+ timeout: 10_000
5194
5712
  });
5195
- probed = true;
5196
5713
  } catch {
5197
- throw new Error('Java is required for Bazel JVM manifest extraction ' + '(rules_jvm_external invokes Coursier, which needs a JDK). ' + 'Install a JDK (e.g. Temurin or OpenJDK) and ensure `java` is on PATH.');
5714
+ throw new utils.InputError('Java is required for Bazel JVM manifest extraction. Install a JDK and ensure JAVA_HOME points to it or java is on PATH.');
5198
5715
  }
5199
5716
  }
5200
5717
 
@@ -5233,75 +5750,55 @@ function validateOutputBase(outputBase, cwd) {
5233
5750
  }
5234
5751
  }
5235
5752
 
5236
- // Stable shim dir name — same process will get the same dir; concurrent
5237
- // socket-cli invocations on the same machine share it. The symlink target
5238
- // is whatever python3 resolves to NOW; if PATH changes between invocations
5239
- // we replace the symlink.
5240
- const SHIM_SUBDIR = 'socket-cli-bazel-python-shim';
5241
-
5242
- // Cache the result for the lifetime of this process.
5243
- let cached = null;
5244
-
5245
- // Safe wrapper around whichBin that returns null instead of throwing when
5246
- // nothrow semantics are broken in older registry versions (realpath 'null' bug).
5247
- async function safeWhichBin(name) {
5753
+ function isPython3(executable, env, cwd) {
5248
5754
  try {
5249
- return (await bin.whichBin(name, {
5250
- nothrow: true
5251
- })) ?? null;
5755
+ const version = childProcess.execFileSync(executable, ['-c', 'import sys\nprint(sys.version_info.major)'], {
5756
+ cwd,
5757
+ env,
5758
+ encoding: 'utf8',
5759
+ stdio: ['ignore', 'pipe', 'pipe'],
5760
+ timeout: 10_000
5761
+ });
5762
+ return version.trim() === '3';
5252
5763
  } catch {
5253
- return null;
5764
+ // Windows application aliases can resolve without a working interpreter.
5765
+ return false;
5254
5766
  }
5255
5767
  }
5256
- async function provisionPythonShim() {
5257
- if (cached) {
5258
- return cached;
5259
- }
5260
- const pythonOnPath = await safeWhichBin('python');
5261
- if (pythonOnPath) {
5262
- cached = {
5263
- augmentedEnv: undefined,
5264
- shimDir: undefined
5265
- };
5266
- return cached;
5267
- }
5268
- const python3OnPath = await safeWhichBin('python3');
5269
- if (!python3OnPath) {
5270
- throw new utils.InputError('Neither `python` nor `python3` found on PATH. Older versions of ' + 'rules_jvm_external require a `python` interpreter for repository ' + 'rules. Install Python 3 and ensure it is on PATH, then retry.');
5271
- }
5272
- const shimDir = path.join(os.tmpdir(), SHIM_SUBDIR);
5273
- u.mkdirSync(shimDir, {
5274
- recursive: true
5275
- });
5276
- const linkPath = path.join(shimDir, 'python');
5277
- // Replace the symlink defensively in case python3's resolved path moved.
5278
- if (u.existsSync(linkPath)) {
5279
- try {
5280
- u.unlinkSync(linkPath);
5281
- } catch {
5282
- // Tolerate races; the next symlinkSync may still succeed.
5768
+ function notPython3Error(executable) {
5769
+ return new utils.InputError(`Python 3 is required for Bazel manifest extraction. ${executable} did not run as Python 3. Install Python 3 and ensure python${process.platform === 'win32' ? '.exe' : ''} is on PATH.`);
5770
+ }
5771
+ async function provisionPythonShim(env, cwd) {
5772
+ const python = await findBazelExecutable('python', env, cwd);
5773
+ if (python) {
5774
+ if (process.platform === 'win32' && path.extname(python).toLowerCase() !== '.exe') {
5775
+ throw new utils.InputError('Bazel on Windows requires a working Python 3 python.exe on PATH. Python batch launchers are not supported.');
5283
5776
  }
5284
- }
5285
- // The shim dir is process-shared (os.tmpdir()/socket-cli-bazel-python-shim),
5286
- // so a concurrent socket-cli invocation may re-create the link between our
5287
- // unlinkSync and symlinkSync. Tolerate EEXIST when the link is back: the
5288
- // other process won the race and left a usable shim in place.
5289
- try {
5290
- u.symlinkSync(python3OnPath, linkPath);
5291
- } catch (e) {
5292
- if (e.code === 'EEXIST' && u.existsSync(linkPath)) ; else {
5293
- throw e;
5777
+ if (isPython3(python, env, cwd)) {
5778
+ return {
5779
+ env,
5780
+ shimDir: undefined
5781
+ };
5782
+ }
5783
+ if (process.platform === 'win32') {
5784
+ throw notPython3Error(python);
5294
5785
  }
5786
+ } else if (process.platform === 'win32') {
5787
+ throw new utils.InputError('Bazel on Windows requires a working Python 3 python.exe on PATH. Install Python 3 with the PATH option enabled, then retry.');
5295
5788
  }
5296
- const augmentedEnv = {
5297
- ...process.env,
5298
- PATH: `${shimDir}${path.delimiter}${process.env['PATH'] ?? ''}`
5299
- };
5300
- cached = {
5301
- augmentedEnv,
5789
+ const python3 = await findBazelExecutable('python3', env, cwd);
5790
+ if (!python3) {
5791
+ throw python ? notPython3Error(python) : new utils.InputError('Neither python nor python3 was found on PATH. Install Python 3 and ensure it is on PATH, then retry.');
5792
+ }
5793
+ if (!isPython3(python3, env, cwd)) {
5794
+ throw notPython3Error(python3);
5795
+ }
5796
+ const shimDir = u.mkdtempSync(path.join(os.tmpdir(), 'socket-bazel-python-'));
5797
+ u.symlinkSync(python3, path.join(shimDir, 'python'));
5798
+ return {
5799
+ env: prependBazelPath(env, shimDir),
5302
5800
  shimDir
5303
5801
  };
5304
- return cached;
5305
5802
  }
5306
5803
 
5307
5804
  /**
@@ -5761,7 +6258,6 @@ function findWorkspaceRoots(opts) {
5761
6258
  // hard failure when nothing else was analyzable, and to a partial otherwise.
5762
6259
 
5763
6260
  const DEFAULT_PER_REPO_TIMEOUT_MS = 60_000;
5764
- const REAP_TIMEOUT_MS = 10_000;
5765
6261
 
5766
6262
  // Machine-readable completeness signal emitted alongside the synthetic
5767
6263
  // manifests. A `complete: false` summary tells a downstream consumer (e.g.
@@ -6156,39 +6652,6 @@ async function discoverCandidatesForWorkspace(workspaceRoot, mode, queryOpts, ve
6156
6652
  };
6157
6653
  }
6158
6654
 
6159
- // Best-effort reap of a Bazel server. Spawned with a short timeout so
6160
- // a wedged server can't itself hang the cleanup; failures are swallowed
6161
- // because the caller will `rm -rf` the output_user_root regardless.
6162
- async function reapBazelServer(bin, outputUserRoot, verbose) {
6163
- try {
6164
- await spawn.spawn(bin, [`--output_user_root=${outputUserRoot}`, 'shutdown'], {
6165
- timeout: REAP_TIMEOUT_MS
6166
- });
6167
- } catch (e) {
6168
- // Server may already be dead, or shutdown itself timed out — the
6169
- // tempdir removal below is sufficient cleanup.
6170
- if (verbose) {
6171
- logger.logger.log(`[VERBOSE] reapBazelServer: shutdown failed for ${outputUserRoot} (${utils.getErrorCause(e)}); tempdir removal will still run`);
6172
- }
6173
- }
6174
- }
6175
- async function removeTempdir(dir, verbose) {
6176
- try {
6177
- await u.promises.rm(dir, {
6178
- recursive: true,
6179
- force: true
6180
- });
6181
- } catch (e) {
6182
- // Best effort. The next CLI invocation lands a fresh tempdir.
6183
- if (verbose) {
6184
- logger.logger.log(`[VERBOSE] removeTempdir: ${dir} not fully removed (${utils.getErrorCause(e)}); a stale dir may linger until the next OS tempdir sweep`);
6185
- }
6186
- }
6187
- }
6188
- function makeOutputUserRoot() {
6189
- return u.mkdtempSync(path.join(os.tmpdir(), 'socket-bazel-'));
6190
- }
6191
-
6192
6655
  // Construct the BazelQueryOptions shape used for a single workspace's
6193
6656
  // queries. Lifted to module scope (out of the per-workspace loop) so
6194
6657
  // ESLint's consistent-function-scoping is happy; takes everything it
@@ -6209,17 +6672,15 @@ function buildQueryOpts(args) {
6209
6672
  invocationFlags,
6210
6673
  outputUserRoot,
6211
6674
  ...(opts.bazelRc ? {
6212
- bazelRc: opts.bazelRc
6675
+ bazelRc: path.resolve(opts.cwd, opts.bazelRc)
6213
6676
  } : {}),
6214
6677
  ...(opts.bazelFlags ? {
6215
6678
  bazelFlags: opts.bazelFlags
6216
6679
  } : {}),
6217
6680
  ...(opts.bazelOutputBase ? {
6218
- bazelOutputBase: opts.bazelOutputBase
6219
- } : {}),
6220
- ...(baseEnv ? {
6221
- env: baseEnv
6681
+ bazelOutputBase: path.resolve(opts.cwd, opts.bazelOutputBase)
6222
6682
  } : {}),
6683
+ env: baseEnv,
6223
6684
  verbose
6224
6685
  };
6225
6686
  }
@@ -6240,16 +6701,23 @@ async function extractBazelToMaven(opts) {
6240
6701
 
6241
6702
  // Validate config + ensure toolchains BEFORE we mint a tempdir.
6242
6703
  let bin;
6243
- let baseEnv;
6704
+ let baseEnv = getBazelEnvironment(opts.env);
6705
+ let pythonShimDir;
6706
+ let outputUserRoot;
6244
6707
  try {
6245
6708
  if (opts.bazelOutputBase) {
6246
6709
  validateOutputBase(opts.bazelOutputBase, opts.cwd);
6247
6710
  }
6248
- ensureJavaOnPath();
6249
- const shim = await provisionPythonShim();
6250
- baseEnv = shim.augmentedEnv ?? opts.env;
6251
- bin = await resolveBazelBinary(opts.bin);
6711
+ ensureJavaOnPath(baseEnv, cwd);
6712
+ bin = await resolveBazelBinary(opts.bin, baseEnv, cwd);
6713
+ const shim = await provisionPythonShim(baseEnv, cwd);
6714
+ pythonShimDir = shim.shimDir;
6715
+ baseEnv = shim.env;
6716
+ outputUserRoot = makeBazelOutputUserRoot(baseEnv, cwd);
6252
6717
  } catch (e) {
6718
+ if (pythonShimDir) {
6719
+ await removeBazelTempdir(pythonShimDir);
6720
+ }
6253
6721
  logger.logger.fail(`Unexpected error in bazel2maven: ${utils.getErrorCause(e)}`);
6254
6722
  if (verbose) {
6255
6723
  logger.logger.group('[VERBOSE] error:');
@@ -6268,8 +6736,7 @@ async function extractBazelToMaven(opts) {
6268
6736
 
6269
6737
  // Track every output_user_root we mint so we can reap them all in
6270
6738
  // the cleanup pass, even if a per-repo timeout forced a re-mint.
6271
- let outputUserRoot = makeOutputUserRoot();
6272
- const mintedRoots = [outputUserRoot];
6739
+ const mintedRoots = new Map([[outputUserRoot, new Map()]]);
6273
6740
  if (verbose) {
6274
6741
  logger.logger.log(`[VERBOSE] initial --output_user_root=${outputUserRoot} (will be reaped on completion)`);
6275
6742
  }
@@ -6296,7 +6763,7 @@ async function extractBazelToMaven(opts) {
6296
6763
  // Always apply the default prune policy so no caller can forget it;
6297
6764
  // callers EXTEND it via ignoreDirNames/ignoreDirPrefixes.
6298
6765
  const ignoreDirNames = new Set([...DEFAULT_BAZEL_WALKER_IGNORE_DIR_NAMES, ...(opts.ignoreDirNames ?? [])]);
6299
- const ignoreDirPrefixes = [...DEFAULT_BAZEL_WALKER_IGNORE_DIR_PREFIXES, ...(opts.ignoreDirPrefixes ?? [])];
6766
+ const ignoreDirPrefixes = [...DEFAULT_BAZEL_WALKER_IGNORE_DIR_PREFIXES, BAZEL_OUTPUT_ROOT_PREFIX, ...(opts.ignoreDirPrefixes ?? [])];
6300
6767
  const workspaceRoots = findWorkspaceRoots({
6301
6768
  cwd,
6302
6769
  ignoreDirNames,
@@ -6343,15 +6810,19 @@ async function extractBazelToMaven(opts) {
6343
6810
  }
6344
6811
  logger.logger.info(`Workspace ${relPath || '.'}: bzlmod=${mode.bzlmod} workspace=${mode.workspace}`);
6345
6812
  const invocationFlags = getBazelInvocationFlags(mode);
6346
- const queryOptsFor = userRoot => buildQueryOpts({
6347
- baseEnv,
6348
- bin,
6349
- invocationFlags,
6350
- opts,
6351
- outputUserRoot: userRoot,
6352
- spawnCwd: workspaceRoot,
6353
- verbose
6354
- });
6813
+ const queryOptsFor = userRoot => {
6814
+ const queryOpts = buildQueryOpts({
6815
+ baseEnv,
6816
+ bin,
6817
+ invocationFlags,
6818
+ opts,
6819
+ outputUserRoot: userRoot,
6820
+ spawnCwd: workspaceRoot,
6821
+ verbose
6822
+ });
6823
+ mintedRoots.get(userRoot)?.set(workspaceRoot, queryOpts);
6824
+ return queryOpts;
6825
+ };
6355
6826
  const {
6356
6827
  candidates,
6357
6828
  discoveryIndeterminate,
@@ -6430,11 +6901,12 @@ async function extractBazelToMaven(opts) {
6430
6901
  state: 'failed'
6431
6902
  });
6432
6903
  // eslint-disable-next-line no-await-in-loop
6433
- await reapBazelServer(bin, outputUserRoot, verbose);
6434
- // eslint-disable-next-line no-await-in-loop
6435
- await removeTempdir(outputUserRoot, verbose);
6436
- outputUserRoot = makeOutputUserRoot();
6437
- mintedRoots.push(outputUserRoot);
6904
+ const cleaned = await cleanUpBazelOutputRoot(outputUserRoot, mintedRoots.get(outputUserRoot) ?? new Map());
6905
+ if (cleaned) {
6906
+ mintedRoots.delete(outputUserRoot);
6907
+ }
6908
+ outputUserRoot = makeBazelOutputUserRoot(baseEnv, cwd);
6909
+ mintedRoots.set(outputUserRoot, new Map());
6438
6910
  if (verbose) {
6439
6911
  logger.logger.log(`[VERBOSE] minted fresh --output_user_root=${outputUserRoot} after timeout`);
6440
6912
  }
@@ -6663,11 +7135,9 @@ async function extractBazelToMaven(opts) {
6663
7135
  workspaceOutcomes
6664
7136
  };
6665
7137
  } finally {
6666
- for (const dir of mintedRoots) {
6667
- // eslint-disable-next-line no-await-in-loop
6668
- await reapBazelServer(bin, dir, verbose);
6669
- // eslint-disable-next-line no-await-in-loop
6670
- await removeTempdir(dir, verbose);
7138
+ await Promise.all(Array.from(mintedRoots, ([root, contexts]) => cleanUpBazelOutputRoot(root, contexts)));
7139
+ if (pythonShimDir) {
7140
+ await removeBazelTempdir(pythonShimDir);
6671
7141
  }
6672
7142
  }
6673
7143
  }
@@ -12227,14 +12697,20 @@ async function extractBazelToPypi(opts) {
12227
12697
  logger.logger.warn(`Warning: cwd does not exist: ${cwd}`);
12228
12698
  }
12229
12699
  logger.logger.groupEnd();
12700
+ let pythonShimDir;
12230
12701
  try {
12702
+ if (process.platform === 'win32') {
12703
+ throw new utils.InputError('Bazel PyPI extraction on Windows is not supported yet. Platform-specific dependency selection still needs verification. Use --ecosystem maven for Windows SCA scans.');
12704
+ }
12231
12705
  // Validate caller-provided Bazel filesystem settings before invoking Bazel.
12232
12706
  if (opts.bazelOutputBase) {
12233
12707
  validateOutputBase(opts.bazelOutputBase, opts.cwd);
12234
12708
  }
12235
12709
  // Python shim (for rules_python workspace discovery).
12236
- const shim = await provisionPythonShim();
12237
- const baseEnv = shim.augmentedEnv ?? opts.env;
12710
+ const env = getBazelEnvironment(opts.env);
12711
+ const shim = await provisionPythonShim(env, cwd);
12712
+ pythonShimDir = shim.shimDir;
12713
+ const baseEnv = shim.env;
12238
12714
 
12239
12715
  // Step 1: workspace detection.
12240
12716
  const mode = detectWorkspaceMode(cwd);
@@ -12242,7 +12718,7 @@ async function extractBazelToPypi(opts) {
12242
12718
  const invocationFlags = getBazelInvocationFlags(mode);
12243
12719
 
12244
12720
  // Step 2: bazel binary resolution.
12245
- const bin = await resolveBazelBinary(opts.bin);
12721
+ const bin = await resolveBazelBinary(opts.bin, baseEnv, cwd);
12246
12722
  logger.logger.info(`Using bazel: ${bin}`);
12247
12723
  if (verbose) {
12248
12724
  logger.logger.log('[VERBOSE] resolved options:', {
@@ -12268,9 +12744,7 @@ async function extractBazelToPypi(opts) {
12268
12744
  ...(opts.bazelOutputBase ? {
12269
12745
  bazelOutputBase: opts.bazelOutputBase
12270
12746
  } : {}),
12271
- ...(baseEnv ? {
12272
- env: baseEnv
12273
- } : {}),
12747
+ env: baseEnv,
12274
12748
  verbose
12275
12749
  };
12276
12750
 
@@ -12410,6 +12884,10 @@ async function extractBazelToPypi(opts) {
12410
12884
  artifactCount: 0,
12411
12885
  ok: false
12412
12886
  };
12887
+ } finally {
12888
+ if (pythonShimDir) {
12889
+ await removeBazelTempdir(pythonShimDir);
12890
+ }
12413
12891
  }
12414
12892
  }
12415
12893
 
@@ -12495,7 +12973,7 @@ const config$g = {
12495
12973
  },
12496
12974
  bazelFlags: {
12497
12975
  type: 'string',
12498
- description: 'Flags forwarded to every bazel invocation (single quoted string)'
12976
+ description: 'Bazel command flags, with quoted values and literal backslashes'
12499
12977
  },
12500
12978
  bazelOutputBase: {
12501
12979
  type: 'string',
@@ -12542,6 +13020,20 @@ const config$g = {
12542
13020
  Note: this command generates dependency manifests for Bazel workspaces.
12543
13021
  It does not run reachability analysis.
12544
13022
 
13023
+ Native Windows support currently covers Maven. Use PowerShell or
13024
+ Command Prompt with a working Python 3 python.exe and a JDK on PATH.
13025
+ PyPI extraction on Windows is not supported yet.
13026
+
13027
+ Set SOCKET_BAZEL_OUTPUT_ROOT to a short, writable parent directory,
13028
+ such as C:/sb, to shorten connector-owned Bazel output paths. Each
13029
+ invocation gets a separate directory. An explicit --bazel-output-base
13030
+ takes precedence for Bazel's cache and is kept after the scan. The
13031
+ scan only stops Bazel servers it started.
13032
+
13033
+ In --bazel-flags, single or double quotes at the start of a value or
13034
+ after = group values containing spaces. Backslashes stay literal. Double a matching quote to include
13035
+ it within a quoted value. Shell variables are not expanded.
13036
+
12545
13037
  Maven hub discovery: under Bzlmod, hubs are enumerated from
12546
13038
  \`bazel mod show_extension\` and filtered to the root module's own hubs.
12547
13039
  Under legacy WORKSPACE mode (no \`show_extension\`), only conventionally
@@ -12690,7 +13182,9 @@ async function run$H(argv, importMeta, {
12690
13182
  let perRepoTimeout = cli.flags['perRepoTimeout'];
12691
13183
 
12692
13184
  // Set defaults for any flag/arg that is not given. Check socket.json first.
12693
- if (!bazel) {
13185
+ if (bazel) {
13186
+ bazel = path.resolve(process.cwd(), String(bazel));
13187
+ } else {
12694
13188
  const defaultBazel = sockJson.defaults?.manifest?.bazel?.bazel ?? sockJson.defaults?.manifest?.bazel?.bin;
12695
13189
  if (defaultBazel) {
12696
13190
  bazel = defaultBazel;
@@ -12769,7 +13263,7 @@ async function run$H(argv, importMeta, {
12769
13263
  logger.logger.groupEnd();
12770
13264
  }
12771
13265
  if (dryRun) {
12772
- logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
13266
+ logger.logger.log(constants.DRY_RUN_BAILING_NOW);
12773
13267
  return;
12774
13268
  }
12775
13269
 
@@ -13091,7 +13585,7 @@ async function run$F(argv, importMeta, {
13091
13585
 
13092
13586
  function renderTable(outcomes) {
13093
13587
  // A covered reactor member is implied by its parent's line above it.
13094
- return outcomes.filter(o => o.status !== 'skippedCovered').map(o => `- ${o.dir} (${o.ecosystem}): ${o.status}${o.factsPath ? ` -> ${o.factsPath}` : ''}`).join('\n');
13588
+ return outcomes.filter(o => o.status !== 'skippedCovered').map(o => `- ${buildRootPath(o)} (${o.ecosystem}): ${o.status}${o.factsPath ? ` -> ${o.factsPath}` : ''}`).join('\n');
13095
13589
  }
13096
13590
  function summarize(outcomes) {
13097
13591
  const generated = outcomes.filter(o => o.status === 'generated').length;
@@ -14905,6 +15399,7 @@ async function enumerateWorkspaces({
14905
15399
  buildOpts,
14906
15400
  cwd,
14907
15401
  ecosystem,
15402
+ entryFile,
14908
15403
  excludePaths,
14909
15404
  javaHome,
14910
15405
  verbose
@@ -14921,6 +15416,7 @@ async function enumerateWorkspaces({
14921
15416
  }
14922
15417
  const scriptOpts = {
14923
15418
  bin: bin || undefined,
15419
+ entryFile,
14924
15420
  excludePaths: excludePaths?.length ? excludePaths : undefined,
14925
15421
  // `env` replaces the spawned process's whole environment, not just JAVA_HOME.
14926
15422
  env: resolvedJavaHome ? {
@@ -14955,8 +15451,8 @@ async function enumerateWorkspaces({
14955
15451
  };
14956
15452
  }
14957
15453
 
14958
- // A single discovered build root - a directory with its own gradle/sbt/maven
14959
- // marker file.
15454
+ // One prompt per directory, as settings are per directory; Maven may have
15455
+ // several POMs, so several roots, there.
14960
15456
 
14961
15457
  const ROOT_ECOSYSTEMS = ['maven', 'gradle', 'sbt'];
14962
15458
  const ECOSYSTEM_LABELS = {
@@ -14991,23 +15487,6 @@ function notCanceled$1() {
14991
15487
  function getEcosystemSection(sockJson, ecosystem) {
14992
15488
  return sockJson.defaults?.manifest?.[ecosystem] ?? {};
14993
15489
  }
14994
-
14995
- // Depth-then-path sort: for exclusion roots it's just stable log ordering;
14996
- // for candidates it's load-bearing, since a parent must be prompted before
14997
- // its children so a child's shown default reflects a parent's just-written
14998
- // cascade instead of its pre-run value.
14999
- function sortCandidatesForDisplay(candidates, cwd) {
15000
- return [...candidates].sort((a, b) => {
15001
- const relA = path.relative(cwd, a.dir);
15002
- const relB = path.relative(cwd, b.dir);
15003
- const depthA = relA.split(path.sep).length;
15004
- const depthB = relB.split(path.sep).length;
15005
- if (depthA !== depthB) {
15006
- return depthA - depthB;
15007
- }
15008
- return relA < relB ? -1 : relA > relB ? 1 : 0;
15009
- });
15010
- }
15011
15490
  function toPosixRelative(cwd, dir) {
15012
15491
  return path.relative(cwd, dir).split(path.sep).join('/');
15013
15492
  }
@@ -15035,12 +15514,15 @@ async function scanBuildRoots({
15035
15514
  excludePaths,
15036
15515
  sockJson
15037
15516
  }) {
15517
+ const socketConfig = utils.loadSocketConfig(cwd);
15038
15518
  const [fullByTool, includedByTool] = await Promise.all([findBuildToolCandidates({
15039
15519
  cwd,
15520
+ socketConfig,
15040
15521
  sockJson
15041
15522
  }), findBuildToolCandidates({
15042
15523
  cwd,
15043
15524
  excludePaths,
15525
+ socketConfig,
15044
15526
  sockJson
15045
15527
  })]);
15046
15528
  return {
@@ -15063,20 +15545,47 @@ async function discoverBuildRoots({
15063
15545
  const included = [];
15064
15546
  const ignorePatterns = (excludePaths ?? []).flatMap(excludePathToScanIgnores);
15065
15547
  const ecosystemsByRoot = new Map();
15066
- for (const [ecosystem, fullDirs] of fullByTool) {
15067
- const includedDirs = new Set(includedByTool.get(ecosystem) ?? []);
15068
- for (const dir of fullDirs) {
15069
- if (dir === realCwd) {
15548
+ for (const [ecosystem, fullRoots] of fullByTool) {
15549
+ const includedPaths = new Set((includedByTool.get(ecosystem) ?? []).map(buildRootPath));
15550
+ const rootsByDir = new Map();
15551
+ for (const root of fullRoots) {
15552
+ if (root.dir === realCwd) {
15070
15553
  continue;
15071
15554
  }
15072
- if (includedDirs.has(dir)) {
15555
+ const roots = rootsByDir.get(root.dir);
15556
+ if (roots) {
15557
+ roots.push(root);
15558
+ } else {
15559
+ rootsByDir.set(root.dir, [root]);
15560
+ }
15561
+ }
15562
+ for (const {
15563
+ 0: dir,
15564
+ 1: roots
15565
+ } of rootsByDir) {
15566
+ const relDir = toPosixRelative(realCwd, dir);
15567
+ const includedRoots = [];
15568
+ const excludedRoots = [];
15569
+ for (const root of roots) {
15570
+ if (includedPaths.has(buildRootPath(root))) {
15571
+ includedRoots.push(root);
15572
+ } else {
15573
+ excludedRoots.push(root);
15574
+ }
15575
+ }
15576
+ if (includedRoots.length) {
15073
15577
  included.push({
15074
15578
  dir,
15075
- ecosystem
15579
+ ecosystem,
15580
+ roots: includedRoots
15076
15581
  });
15582
+ // `disabled` is per directory, so it cannot exclude one build file
15583
+ // without its included siblings.
15584
+ for (const root of excludedRoots) {
15585
+ logger.logger.warn(`Not disabling ${ecosystem} in ${relDir}: --exclude-paths matches ${buildRootPath(root)} but not the other build files there. Pass the same --exclude-paths to the scan instead.`);
15586
+ }
15077
15587
  continue;
15078
15588
  }
15079
- const relDir = toPosixRelative(realCwd, dir);
15080
15589
  const rootRelDir = findExclusionRoot(relDir, ignorePatterns);
15081
15590
  const rootDir = path.join(realCwd, rootRelDir);
15082
15591
  const ecosystems = ecosystemsByRoot.get(rootDir) ?? new Set();
@@ -15094,48 +15603,65 @@ async function discoverBuildRoots({
15094
15603
  }
15095
15604
 
15096
15605
  // Fails closed: without the members' build files, covered and independent
15097
- // candidates can't be told apart.
15606
+ // candidates can't be told apart. In order, as a root's enumeration can cover
15607
+ // the roots after it.
15098
15608
  async function markWorkspaceCoverage({
15099
- candidate,
15100
15609
  covered,
15101
15610
  cwd,
15611
+ dir,
15612
+ ecosystem,
15102
15613
  excludePaths,
15103
- rootSockJson
15614
+ rootSockJson,
15615
+ roots
15104
15616
  }) {
15105
- const cascade = utils.readSocketJsonCascade(candidate.dir, cwd, rootSockJson);
15617
+ if (roots.every(root => isBuildRootCovered(covered, ecosystem, root))) {
15618
+ return {
15619
+ ok: true,
15620
+ data: undefined
15621
+ };
15622
+ }
15623
+ const cascade = utils.readSocketJsonCascade(dir, cwd, rootSockJson);
15106
15624
  const {
15107
15625
  bin,
15108
15626
  buildOpts,
15109
15627
  javaHome,
15110
15628
  skipReason
15111
- } = resolveEcosystemConfig(candidate.ecosystem, candidate.dir, cascade);
15629
+ } = resolveEcosystemConfig(ecosystem, dir, cascade);
15112
15630
  if (skipReason) {
15113
15631
  return {
15114
15632
  ok: true,
15115
15633
  data: undefined
15116
15634
  };
15117
15635
  }
15118
- const excludePathsForCandidate = projectIgnorePathsToReachExcludePaths(excludePaths, {
15636
+ const excludePathsForDir = projectIgnorePathsToReachExcludePaths(excludePaths, {
15119
15637
  cwd,
15120
- target: candidate.dir
15121
- });
15122
- const enumResult = await enumerateWorkspaces({
15123
- bin,
15124
- buildOpts,
15125
- cwd: candidate.dir,
15126
- ecosystem: candidate.ecosystem,
15127
- excludePaths: excludePathsForCandidate,
15128
- javaHome,
15129
- verbose: false
15638
+ target: dir
15130
15639
  });
15131
- if (!enumResult) {
15132
- const relDir = path.relative(cwd, candidate.dir) || '.';
15133
- return {
15134
- ok: false,
15135
- message: `Could not determine ${candidate.ecosystem} workspace layout for ${relDir}; aborting rather than risk misclassifying its members.`
15136
- };
15640
+ for (const root of roots) {
15641
+ if (isBuildRootCovered(covered, ecosystem, root)) {
15642
+ continue;
15643
+ }
15644
+ // eslint-disable-next-line no-await-in-loop
15645
+ const enumResult = await enumerateWorkspaces({
15646
+ bin,
15647
+ buildOpts,
15648
+ cwd: dir,
15649
+ ecosystem,
15650
+ entryFile: root.entryFile,
15651
+ excludePaths: excludePathsForDir,
15652
+ javaHome,
15653
+ verbose: false
15654
+ });
15655
+ if (!enumResult) {
15656
+ const relRoot = path.relative(cwd, buildRootPath(root)) || '.';
15657
+ return {
15658
+ ok: false,
15659
+ message: `Could not determine ${ecosystem} workspace layout for ${relRoot}; aborting rather than risk misclassifying its members.`
15660
+ };
15661
+ }
15662
+ // eslint-disable-next-line no-await-in-loop
15663
+ await addCoveredBuildFiles(covered, enumResult.buildRoot, enumResult.projects);
15137
15664
  }
15138
- await addCoveredBuildFiles(covered, enumResult.buildRoot, enumResult.projects);
15139
15665
  return {
15140
15666
  ok: true,
15141
15667
  data: undefined
@@ -15545,7 +16071,7 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
15545
16071
  logger.logger.log('');
15546
16072
  logger.logger.log(`Configuring ${cwd} ...`);
15547
16073
  const initialSockJson = utils.readOrDefaultSocketJson(cwd);
15548
- // Resolved once here for sortCandidatesForDisplay/disableExclusionRoot's
16074
+ // Resolved once here for sortByDepthThenPath/disableExclusionRoot's
15549
16075
  // relative-path math, consistent with discoverBuildRoots' own internal
15550
16076
  // resolution (see its comment for why this matters).
15551
16077
  const realCwd = await utils.realpathOrResolved(cwd);
@@ -15584,7 +16110,7 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
15584
16110
 
15585
16111
  // Which detected ecosystems cwd is itself a build root for - see
15586
16112
  // setupRecursiveRootDefaults' wording split.
15587
- const ecosystemsAtCwd = ROOT_ECOSYSTEMS.filter(ecosystem => (fullByTool.get(ecosystem) ?? []).includes(realCwd));
16113
+ const ecosystemsAtCwd = ROOT_ECOSYSTEMS.filter(ecosystem => (fullByTool.get(ecosystem) ?? []).some(root => root.dir === realCwd));
15588
16114
  const rootResult = await setupRecursiveRootDefaults(cwd, defaultOnReadError, detectedEcosystems, ecosystemsAtCwd);
15589
16115
  if (!rootResult.ok) {
15590
16116
  return rootResult;
@@ -15611,7 +16137,7 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
15611
16137
  if (!excluded.length) {
15612
16138
  logger.logger.log('None matched --exclude-paths; nothing disabled.');
15613
16139
  } else {
15614
- const orderedExcluded = sortCandidatesForDisplay(excluded, realCwd);
16140
+ const orderedExcluded = sortByDepthThenPath(excluded, realCwd);
15615
16141
  for (const candidate of orderedExcluded) {
15616
16142
  // eslint-disable-next-line no-await-in-loop
15617
16143
  const result = await disableExclusionRoot({
@@ -15649,17 +16175,16 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
15649
16175
  // cwd never appears in `included`, but a reactor root often sits exactly
15650
16176
  // at cwd - seed coverage from it first so its declared members still get
15651
16177
  // recognized.
15652
- for (const ecosystem of ecosystemsAtCwd) {
16178
+ for (const ecosystem of ROOT_ECOSYSTEMS) {
15653
16179
  // eslint-disable-next-line no-await-in-loop
15654
16180
  const coverageResult = await markWorkspaceCoverage({
15655
- candidate: {
15656
- dir: realCwd,
15657
- ecosystem
15658
- },
15659
16181
  covered,
15660
16182
  cwd: realCwd,
16183
+ dir: realCwd,
16184
+ ecosystem,
15661
16185
  excludePaths,
15662
- rootSockJson
16186
+ rootSockJson,
16187
+ roots: (includedByTool.get(ecosystem) ?? []).filter(root => root.dir === realCwd)
15663
16188
  });
15664
16189
  if (!coverageResult.ok) {
15665
16190
  return coverageResult;
@@ -15672,9 +16197,14 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
15672
16197
  reenabled: 0,
15673
16198
  skipped: 0
15674
16199
  };
15675
- const orderedIncluded = sortCandidatesForDisplay(included, realCwd);
16200
+ // A parent is prompted before its children, so a child's shown default
16201
+ // reflects the parent's just-written cascade.
16202
+ const orderedIncluded = sortByDepthThenPath(included, realCwd);
15676
16203
  for (const candidate of orderedIncluded) {
15677
- if (isBuildRootCovered(covered, candidate.ecosystem, candidate.dir)) {
16204
+ const {
16205
+ ecosystem
16206
+ } = candidate;
16207
+ if (candidate.roots.every(root => isBuildRootCovered(covered, ecosystem, root))) {
15678
16208
  counts.covered += 1;
15679
16209
  continue;
15680
16210
  }
@@ -15683,7 +16213,7 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
15683
16213
  const result = await processCandidate({
15684
16214
  cwd: realCwd,
15685
16215
  dir: candidate.dir,
15686
- ecosystem: candidate.ecosystem,
16216
+ ecosystem,
15687
16217
  rootSockJson
15688
16218
  });
15689
16219
  if (!result.ok) {
@@ -15710,11 +16240,13 @@ async function setupRecursiveManifestConfig(cwd, defaultOnReadError, excludePath
15710
16240
  // build-tool invocation only runs for a project already prompted about.
15711
16241
  // eslint-disable-next-line no-await-in-loop
15712
16242
  const coverageResult = await markWorkspaceCoverage({
15713
- candidate,
15714
16243
  covered,
15715
16244
  cwd: realCwd,
16245
+ dir: candidate.dir,
16246
+ ecosystem,
15716
16247
  excludePaths,
15717
- rootSockJson
16248
+ rootSockJson,
16249
+ roots: candidate.roots
15718
16250
  });
15719
16251
  if (!coverageResult.ok) {
15720
16252
  return coverageResult;
@@ -24795,5 +25327,5 @@ process.on('unhandledRejection', async (reason, promise) => {
24795
25327
  // eslint-disable-next-line n/no-process-exit
24796
25328
  process.exit(1);
24797
25329
  });
24798
- //# debugId=5d7a85dd-501f-4419-b550-bef90180bd6a
25330
+ //# debugId=b77ced41-382f-4a5e-a03f-154a33fa6ec
24799
25331
  //# sourceMappingURL=cli.js.map