@cdxgen/cdxgen 13.2.0 → 13.4.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 (311) hide show
  1. package/README.md +33 -14
  2. package/bin/cdxgen.js +310 -138
  3. package/bin/evinse.js +22 -0
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +119 -15
  6. package/bin/tracebom.js +69 -5
  7. package/bin/validate.js +107 -19
  8. package/bin/verify.js +172 -63
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +85 -0
  11. package/data/rules/dependency-sources.yaml +67 -0
  12. package/data/rules/scala-evidence.yaml +115 -0
  13. package/lib/audit/index.js +38 -6
  14. package/lib/cli/bomAssembly.js +105 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +156 -58
  17. package/lib/cli/jsBom.js +99 -53
  18. package/lib/cli/jvmBom.js +708 -147
  19. package/lib/cli/managedBom.js +415 -224
  20. package/lib/cli/nativeBom.js +264 -44
  21. package/lib/core/activity.js +12 -0
  22. package/lib/core/env.js +22 -15
  23. package/lib/core/fs.js +176 -7
  24. package/lib/core/httpClient.js +159 -24
  25. package/lib/core/metadataProperties.js +97 -0
  26. package/lib/core/pluginRun.js +184 -0
  27. package/lib/core/processTree.js +100 -0
  28. package/lib/core/projectConfig.js +31 -4
  29. package/lib/core/runState.js +39 -0
  30. package/lib/core/toml.js +72 -0
  31. package/lib/ecosystems/asarutils.js +4 -2
  32. package/lib/ecosystems/bunutils.js +274 -108
  33. package/lib/ecosystems/cargoCache.js +231 -8
  34. package/lib/ecosystems/caxa.js +434 -2
  35. package/lib/ecosystems/cmakeResolver.js +76 -21
  36. package/lib/ecosystems/cppBuildContext.js +631 -0
  37. package/lib/ecosystems/cppEvidence.js +399 -51
  38. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  39. package/lib/ecosystems/denoutils.js +93 -66
  40. package/lib/ecosystems/ecosystems.js +1983 -793
  41. package/lib/ecosystems/golem.js +28 -12
  42. package/lib/ecosystems/gradleutils.js +356 -37
  43. package/lib/ecosystems/jsEvidence.js +104 -44
  44. package/lib/ecosystems/kosi.js +31 -7
  45. package/lib/ecosystems/npmutils.js +174 -9
  46. package/lib/ecosystems/parsers-dotnet.js +616 -159
  47. package/lib/ecosystems/parsers-elm.js +46 -28
  48. package/lib/ecosystems/parsers-gleam.js +3 -4
  49. package/lib/ecosystems/parsers-go.js +371 -70
  50. package/lib/ecosystems/parsers-js.js +263 -76
  51. package/lib/ecosystems/parsers-julia.js +2 -3
  52. package/lib/ecosystems/parsers-jvm.js +204 -12
  53. package/lib/ecosystems/parsers-misc.js +929 -216
  54. package/lib/ecosystems/parsers-python.js +167 -24
  55. package/lib/ecosystems/parsers-rust.js +28 -22
  56. package/lib/ecosystems/parsers-terraform.js +1573 -19
  57. package/lib/ecosystems/pythonutils.js +102 -6
  58. package/lib/ecosystems/remote/huggingface.js +3 -0
  59. package/lib/ecosystems/rubyutils.js +1492 -182
  60. package/lib/ecosystems/rusi.js +54 -10
  61. package/lib/ecosystems/sbtutils.js +243 -187
  62. package/lib/ecosystems/scalacliutils.js +342 -0
  63. package/lib/ecosystems/scalasem.js +2618 -0
  64. package/lib/ecosystems/terraformRegistry.js +288 -0
  65. package/lib/ecosystems/utils.js +2 -0
  66. package/lib/ecosystems/vendoredCode.js +156 -0
  67. package/lib/evinser/evinser.js +1008 -191
  68. package/lib/evinser/swiftsem.js +1914 -302
  69. package/lib/helpers/agenticToolutils.js +2 -2
  70. package/lib/helpers/bomSigner.js +1101 -222
  71. package/lib/inventory/aiCollector.js +4 -1
  72. package/lib/inventory/analyzer.js +110 -127
  73. package/lib/inventory/atomUtils.js +703 -43
  74. package/lib/inventory/auditCategories.js +7 -0
  75. package/lib/inventory/buildToolRateLimit.js +144 -0
  76. package/lib/inventory/cacheCommands.js +2 -1
  77. package/lib/inventory/cdxrs.js +103 -12
  78. package/lib/inventory/cmakeBuildDirs.js +254 -0
  79. package/lib/inventory/deps.js +501 -124
  80. package/lib/inventory/depsUtils.js +183 -11
  81. package/lib/inventory/display.js +4 -5
  82. package/lib/inventory/dosai.js +798 -57
  83. package/lib/inventory/dosaiParsers.js +310 -15
  84. package/lib/inventory/dynamic.js +90 -6
  85. package/lib/inventory/evidenceUtils.js +166 -113
  86. package/lib/inventory/fetchBatch.js +690 -55
  87. package/lib/inventory/fetchDiskCache.js +597 -0
  88. package/lib/inventory/forgeEnricher.js +14 -5
  89. package/lib/inventory/jvmLocalRepos.js +917 -0
  90. package/lib/inventory/mcpConfigParser.js +2 -3
  91. package/lib/inventory/parentSelection.js +146 -0
  92. package/lib/inventory/protobom.js +8 -1
  93. package/lib/inventory/purl.js +152 -3
  94. package/lib/inventory/rustFormulationParser.js +3 -4
  95. package/lib/inventory/sbomDocument.js +2 -70
  96. package/lib/inventory/scalaCoords.js +153 -0
  97. package/lib/inventory/source.js +45 -27
  98. package/lib/inventory/spdx.js +101 -0
  99. package/lib/inventory/traceRunner.js +295 -25
  100. package/lib/inventory/tracedCrypto.js +294 -0
  101. package/lib/managers/binary.js +115 -24
  102. package/lib/managers/docker.js +467 -46
  103. package/lib/managers/oci.js +63 -3
  104. package/lib/parsers/cmakeCache.js +69 -0
  105. package/lib/parsers/cmakeLists.js +466 -0
  106. package/lib/parsers/cmakePresets.js +400 -0
  107. package/lib/parsers/compileCommands.js +557 -0
  108. package/lib/parsers/largeJson.js +454 -0
  109. package/lib/parsers/npmrc.js +19 -1
  110. package/lib/parsers/strictJson.js +322 -0
  111. package/lib/parsers/terraformConfig.js +1073 -0
  112. package/lib/parsers/terraformSource.js +652 -0
  113. package/lib/server/server.js +23 -0
  114. package/lib/stages/postgen/introspection/markers.js +1 -1
  115. package/lib/stages/postgen/postgen.js +83 -16
  116. package/lib/stages/postgen/specVersionCompat.js +32 -0
  117. package/lib/validator/bomValidator.js +6 -16
  118. package/lib/validator/index.js +64 -11
  119. package/lib/validator/reporters/console.js +3 -0
  120. package/lib/validator/reporters/json.js +2 -0
  121. package/lib/validator/reporters/sarif.js +7 -0
  122. package/package.json +23 -23
  123. package/types/lib/audit/index.d.ts.map +1 -1
  124. package/types/lib/cli/bomAssembly.d.ts +2 -1
  125. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  126. package/types/lib/cli/cliOptions.d.ts +33 -1
  127. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  128. package/types/lib/cli/index.d.ts.map +1 -1
  129. package/types/lib/cli/jsBom.d.ts.map +1 -1
  130. package/types/lib/cli/jvmBom.d.ts +24 -1
  131. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  132. package/types/lib/cli/managedBom.d.ts +7 -4
  133. package/types/lib/cli/managedBom.d.ts.map +1 -1
  134. package/types/lib/cli/nativeBom.d.ts +3 -1
  135. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  136. package/types/lib/core/activity.d.ts.map +1 -1
  137. package/types/lib/core/env.d.ts +9 -0
  138. package/types/lib/core/env.d.ts.map +1 -1
  139. package/types/lib/core/fs.d.ts +55 -0
  140. package/types/lib/core/fs.d.ts.map +1 -1
  141. package/types/lib/core/httpClient.d.ts +13 -8
  142. package/types/lib/core/httpClient.d.ts.map +1 -1
  143. package/types/lib/core/metadataProperties.d.ts +39 -0
  144. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  145. package/types/lib/core/pluginRun.d.ts +97 -0
  146. package/types/lib/core/pluginRun.d.ts.map +1 -0
  147. package/types/lib/core/processTree.d.ts +36 -0
  148. package/types/lib/core/processTree.d.ts.map +1 -0
  149. package/types/lib/core/projectConfig.d.ts +6 -2
  150. package/types/lib/core/projectConfig.d.ts.map +1 -1
  151. package/types/lib/core/runState.d.ts +29 -0
  152. package/types/lib/core/runState.d.ts.map +1 -0
  153. package/types/lib/core/toml.d.ts +20 -0
  154. package/types/lib/core/toml.d.ts.map +1 -0
  155. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  156. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  157. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  159. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  160. package/types/lib/ecosystems/caxa.d.ts +14 -0
  161. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  162. package/types/lib/ecosystems/cmakeResolver.d.ts +28 -3
  163. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  164. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  165. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  166. package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
  167. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  168. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  169. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  170. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  171. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  172. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  173. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  174. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  175. package/types/lib/ecosystems/gradleutils.d.ts +70 -4
  176. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  177. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  178. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  179. package/types/lib/ecosystems/kosi.d.ts +13 -0
  180. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  181. package/types/lib/ecosystems/npmutils.d.ts +57 -0
  182. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  183. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  184. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-go.d.ts +54 -2
  188. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  189. package/types/lib/ecosystems/parsers-js.d.ts +16 -5
  190. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  191. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  199. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  200. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  201. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rubyutils.d.ts +164 -3
  205. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  206. package/types/lib/ecosystems/rusi.d.ts +13 -0
  207. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  208. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  209. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  210. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  211. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  212. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  213. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  214. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  215. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  216. package/types/lib/ecosystems/utils.d.ts +2 -2
  217. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  218. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  219. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  220. package/types/lib/evinser/evinser.d.ts +132 -15
  221. package/types/lib/evinser/evinser.d.ts.map +1 -1
  222. package/types/lib/evinser/swiftsem.d.ts +280 -15
  223. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  224. package/types/lib/helpers/bomSigner.d.ts +182 -13
  225. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  226. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  227. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  228. package/types/lib/inventory/atomUtils.d.ts +219 -5
  229. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  230. package/types/lib/inventory/auditCategories.d.ts +3 -0
  231. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  232. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  233. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  234. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  235. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  236. package/types/lib/inventory/cdxrs.d.ts +8 -3
  237. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  238. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  239. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  240. package/types/lib/inventory/deps.d.ts +59 -1
  241. package/types/lib/inventory/deps.d.ts.map +1 -1
  242. package/types/lib/inventory/depsUtils.d.ts +15 -0
  243. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  244. package/types/lib/inventory/display.d.ts.map +1 -1
  245. package/types/lib/inventory/dosai.d.ts +121 -9
  246. package/types/lib/inventory/dosai.d.ts.map +1 -1
  247. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  248. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  249. package/types/lib/inventory/dynamic.d.ts +10 -0
  250. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  251. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  252. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  253. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  254. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  255. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  256. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  257. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  258. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  259. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  260. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  261. package/types/lib/inventory/parentSelection.d.ts +34 -0
  262. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  263. package/types/lib/inventory/plugins.d.ts.map +1 -1
  264. package/types/lib/inventory/protobom.d.ts.map +1 -1
  265. package/types/lib/inventory/purl.d.ts +71 -2
  266. package/types/lib/inventory/purl.d.ts.map +1 -1
  267. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  268. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  269. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  270. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  271. package/types/lib/inventory/source.d.ts.map +1 -1
  272. package/types/lib/inventory/spdx.d.ts +9 -0
  273. package/types/lib/inventory/spdx.d.ts.map +1 -1
  274. package/types/lib/inventory/traceRunner.d.ts +59 -1
  275. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  276. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  277. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  278. package/types/lib/managers/binary.d.ts +30 -1
  279. package/types/lib/managers/binary.d.ts.map +1 -1
  280. package/types/lib/managers/docker.d.ts +29 -0
  281. package/types/lib/managers/docker.d.ts.map +1 -1
  282. package/types/lib/managers/oci.d.ts +17 -1
  283. package/types/lib/managers/oci.d.ts.map +1 -1
  284. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  285. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  286. package/types/lib/parsers/cmakeLists.d.ts +77 -0
  287. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  288. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  289. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  290. package/types/lib/parsers/compileCommands.d.ts +115 -0
  291. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  292. package/types/lib/parsers/largeJson.d.ts +52 -0
  293. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  294. package/types/lib/parsers/npmrc.d.ts +5 -0
  295. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  296. package/types/lib/parsers/strictJson.d.ts +46 -0
  297. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  298. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  299. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  300. package/types/lib/parsers/terraformSource.d.ts +66 -0
  301. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  302. package/types/lib/server/server.d.ts.map +1 -1
  303. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  304. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  306. package/types/lib/validator/index.d.ts +18 -1
  307. package/types/lib/validator/index.d.ts.map +1 -1
  308. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  309. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  310. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
  311. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
package/lib/cli/jvmBom.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { readFileSync } from "node:fs";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { basename, dirname, join, resolve, sep } from "node:path";
5
5
  import process from "node:process";
@@ -7,7 +7,11 @@ import process from "node:process";
7
7
  import { build } from "@cdxgen/cdx-purl";
8
8
  import { gte, lte } from "semver";
9
9
 
10
- import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
10
+ import {
11
+ DEBUG_MODE,
12
+ isSecureMode,
13
+ readEnvironmentVariable,
14
+ } from "../core/activity.js";
11
15
  import { commandOutputText, recordDegradation } from "../core/buildLedger.js";
12
16
  import {
13
17
  deferFailOnError,
@@ -41,9 +45,11 @@ import {
41
45
  buildObjectForGradleModule,
42
46
  collectGradleDependencies,
43
47
  executeParallelGradleProperties,
48
+ findMillDaemons,
44
49
  getGradleCommand,
45
50
  getMavenCommand,
46
51
  getMillCommand,
52
+ getMillVersion,
47
53
  hasGradleInvocationFailure,
48
54
  parseGradleDep,
49
55
  parseGradleInfoLogsForUrls,
@@ -52,29 +58,41 @@ import {
52
58
  parseGradleVersionCatalog,
53
59
  recordGradleInvocationFailure,
54
60
  splitOutputByGradleProjects,
61
+ stopMillDaemons,
55
62
  } from "../ecosystems/gradleutils.js";
56
63
  import {
57
64
  parseModuleBazel,
58
65
  parseModuleBazelLock,
59
66
  } from "../ecosystems/parsers-bazel.js";
60
67
  import {
68
+ MAVEN_MODULE_TREE_STEM,
61
69
  parseBazelActionGraph,
62
70
  parseBazelSkyframe,
63
71
  parseMavenTree,
64
72
  parseMavenTreeJson,
65
73
  parseMillDependency,
66
74
  parsePom,
75
+ readMavenModuleTrees,
67
76
  } from "../ecosystems/parsers-jvm.js";
68
77
  import {
69
78
  addPlugin,
70
79
  cleanupPlugin,
71
80
  determineSbtVersion,
72
81
  discoverSbtProjects,
82
+ isSbt2Version,
73
83
  parseSbtLock,
74
84
  parseSbtProjects,
75
85
  parseSbtRootProject,
76
86
  parseSbtTree,
87
+ sbt2DependencyTreeCommand,
88
+ sbtSpawnSync,
77
89
  } from "../ecosystems/sbtutils.js";
90
+ import { collectScalaCliComponents } from "../ecosystems/scalacliutils.js";
91
+ import { collectScalaJsNpmComponents } from "../ecosystems/scalasem.js";
92
+ import {
93
+ isBuildToolRateLimited,
94
+ noteBuildToolRateLimit,
95
+ } from "../inventory/buildToolRateLimit.js";
78
96
  import {
79
97
  collectJarNS,
80
98
  collectMvnDependencies,
@@ -85,8 +103,15 @@ import {
85
103
  attachIdentityTools,
86
104
  extractToolRefs,
87
105
  } from "../inventory/evidenceUtils.js";
106
+ import {
107
+ coursierCacheDir,
108
+ findLocalMavenArtifact,
109
+ locateInCoursierCache,
110
+ projectSettingsArgs,
111
+ } from "../inventory/jvmLocalRepos.js";
88
112
  import { readGradleWrapperVersion } from "../inventory/jvmToolEnv.js";
89
113
  import { applyPurl, mavenPurl } from "../inventory/purl.js";
114
+ import { publishedArtifactId } from "../inventory/scalaCoords.js";
90
115
  import {
91
116
  buildBomNSData,
92
117
  createDefaultParentComponent,
@@ -136,6 +161,127 @@ export const GRADLE_INIT_SCRIPT = resolve(
136
161
  export const SBT_CACHE_DIR =
137
162
  readEnvironmentVariable("SBT_CACHE_DIR") || join(homedir(), ".ivy2", "cache");
138
163
 
164
+ /**
165
+ * Collect the jars in the caches sbt uses: the Coursier cache, which sbt 1.3
166
+ * and later resolve into, and the Ivy cache named by SBT_CACHE_DIR, which
167
+ * older builds used.
168
+ *
169
+ * @returns {Promise<Object>} Jar namespace mapping, as collectJarNS returns.
170
+ */
171
+ async function collectSbtCacheDependencies() {
172
+ let nsMapping = {};
173
+ for (const cacheDir of [coursierCacheDir(), SBT_CACHE_DIR]) {
174
+ if (cacheDir && safeExistsSync(cacheDir)) {
175
+ nsMapping = { ...nsMapping, ...(await collectJarNS(cacheDir)) };
176
+ }
177
+ }
178
+ return nsMapping;
179
+ }
180
+
181
+ /**
182
+ * The jar of a resolved sbt dependency in the user's own caches: the Coursier
183
+ * cache sbt 1.3 and later resolve into, the other local Maven caches, and the
184
+ * Ivy cache older builds used.
185
+ *
186
+ * @param {Object} pkg Package with group, name and version.
187
+ * @returns {string|undefined} Path of the jar.
188
+ */
189
+ function localSbtDependencyJar(pkg) {
190
+ // Scala components are named without their binary suffix; the jar has it.
191
+ const name = publishedArtifactId(pkg);
192
+ const coursierDir = locateInCoursierCache(pkg.group, name, pkg.version)?.dir;
193
+ const coursierJar =
194
+ coursierDir && join(coursierDir, `${name}-${pkg.version}.jar`);
195
+ if (coursierJar && safeExistsSync(coursierJar)) {
196
+ return coursierJar;
197
+ }
198
+ const jarPath = findLocalMavenArtifact(pkg.group, name, pkg.version)?.jarPath;
199
+ if (jarPath) {
200
+ return jarPath;
201
+ }
202
+ for (const typeDir of ["jars", "bundles"]) {
203
+ const ivyJar = join(
204
+ SBT_CACHE_DIR,
205
+ pkg.group,
206
+ name,
207
+ typeDir,
208
+ `${name}-${pkg.version}.jar`,
209
+ );
210
+ if (safeExistsSync(ivyJar)) {
211
+ return ivyJar;
212
+ }
213
+ }
214
+ return undefined;
215
+ }
216
+
217
+ /**
218
+ * Collect the namespaces of the jars of resolved sbt dependencies. sbt has
219
+ * just downloaded them into the user's caches, so each is read from there;
220
+ * the rest of the cache is never walked.
221
+ *
222
+ * @param {Object[]} pkgList Packages from the sbt dependency trees.
223
+ * @param {Object} [known] Jar namespace mapping already collected. Packages
224
+ * it covers are skipped.
225
+ * @returns {Promise<Object>} Jar namespace mapping keyed by package purl.
226
+ */
227
+ export async function collectSbtDependencyJars(pkgList, known = {}) {
228
+ const nsMapping = {};
229
+ for (const pkg of pkgList || []) {
230
+ // Applications are the build's own projects, which no cache holds.
231
+ if (
232
+ !pkg?.purl ||
233
+ !pkg.group ||
234
+ !pkg.name ||
235
+ !pkg.version ||
236
+ pkg.type === "application" ||
237
+ known[pkg.purl] ||
238
+ nsMapping[pkg.purl]
239
+ ) {
240
+ continue;
241
+ }
242
+ const jarPath = localSbtDependencyJar(pkg);
243
+ if (!jarPath) {
244
+ continue;
245
+ }
246
+ const [entry] = Object.values(await collectJarNS(jarPath));
247
+ if (entry) {
248
+ nsMapping[pkg.purl] = entry;
249
+ }
250
+ }
251
+ return nsMapping;
252
+ }
253
+
254
+ /**
255
+ * The purls and bom-refs of a parent component and the module components
256
+ * nested under it.
257
+ *
258
+ * @param {Object} parentComponent Parent component.
259
+ * @returns {Set<string>} Purls and bom-refs.
260
+ */
261
+ export function ownComponentRefs(parentComponent) {
262
+ const refs = new Set();
263
+ // The Gradle path nests modules that can point back at the parent, so the
264
+ // walk remembers what it has seen.
265
+ const seen = new Set();
266
+ const stack = parentComponent ? [parentComponent] : [];
267
+ while (stack.length) {
268
+ const component = stack.pop();
269
+ if (!component || typeof component !== "object" || seen.has(component)) {
270
+ continue;
271
+ }
272
+ seen.add(component);
273
+ for (const ref of [component.purl, component["bom-ref"]]) {
274
+ if (ref) {
275
+ refs.add(ref);
276
+ }
277
+ }
278
+ if (Array.isArray(component.components)) {
279
+ stack.push(...component.components);
280
+ }
281
+ }
282
+ return refs;
283
+ }
284
+
139
285
  /**
140
286
  * Function to create bom string for Java jars
141
287
  *
@@ -167,18 +313,20 @@ export async function createJarBom(path, options) {
167
313
  const parentComponent = createDefaultParentComponent(path, "maven", options);
168
314
  if (options.useGradleCache) {
169
315
  nsMapping = await collectGradleDependencies(
170
- getGradleCommand(path, null),
316
+ getGradleCommand(path, null, options),
171
317
  path,
172
318
  false,
173
319
  true,
174
320
  );
175
321
  } else if (options.useMavenCache) {
176
322
  nsMapping = await collectMvnDependencies(
177
- getMavenCommand(path, null),
323
+ getMavenCommand(path, null, options),
178
324
  null,
179
325
  false,
180
326
  true,
181
327
  );
328
+ } else if (options.useSbtCache) {
329
+ nsMapping = await collectSbtCacheDependencies();
182
330
  }
183
331
  if (path.endsWith(".jar")) {
184
332
  jarFiles = [resolve(path)];
@@ -207,14 +355,17 @@ export async function createJarBom(path, options) {
207
355
  if (dlist?.length) {
208
356
  pkgList = pkgList.concat(dlist);
209
357
  }
210
- if (pkgList.length) {
211
- pkgList = await getMvnMetadata(pkgList);
212
- }
213
358
  // Clean up
214
359
  if (tempDir?.startsWith(getTmpDir())) {
215
360
  safeRmSync(tempDir, { recursive: true, force: true });
216
361
  }
217
362
  }
363
+ // Metadata is resolved once for the whole scan. Inside the loop every jar
364
+ // re-resolved the packages of all the jars before it, and lookups that had
365
+ // failed were sent again each time.
366
+ if (pkgList.length) {
367
+ pkgList = await getMvnMetadata(pkgList);
368
+ }
218
369
  pkgList = pkgList.concat(await convertJarNSToPackages(nsMapping));
219
370
  return buildBomNSData(options, pkgList, "maven", {
220
371
  src: path,
@@ -222,6 +373,37 @@ export async function createJarBom(path, options) {
222
373
  });
223
374
  }
224
375
 
376
+ /**
377
+ * Whether a project keeps source directories of its own. Used to keep the
378
+ * root project of an sbt build: a root with sources is usually the
379
+ * application itself and merely aggregates the modules for convenience,
380
+ * while a root without sources is a pure aggregator whose own resolution
381
+ * adds nothing.
382
+ *
383
+ * @param {string} basePath Directory of the sbt build
384
+ * @returns {boolean} True when the build root has main sources
385
+ */
386
+ function sbtRootHasOwnSources(basePath) {
387
+ for (const language of ["scala", "java", "kotlin"]) {
388
+ if (safeExistsSync(join(basePath, "src", "main", language))) {
389
+ return true;
390
+ }
391
+ }
392
+ // A Play application keeps its sources in app/ instead of src/main
393
+ const appDir = join(basePath, "app");
394
+ if (safeExistsSync(appDir)) {
395
+ try {
396
+ const entries = readdirSync(appDir, { withFileTypes: true });
397
+ return entries.some(
398
+ (entry) => entry.isDirectory() || entry.name.endsWith(".scala"),
399
+ );
400
+ } catch (_err) {
401
+ return false;
402
+ }
403
+ }
404
+ return false;
405
+ }
406
+
225
407
  /**
226
408
  * Discover the real sbt project ids for a build by invoking sbt's own
227
409
  * `projects` command. This is more reliable than scraping the build files with
@@ -229,32 +411,46 @@ export async function createJarBom(path, options) {
229
411
  * (commented-out code, examples, values that merely resemble project defs) that
230
412
  * can lead to hangs when those bogus scopes are later passed to `dependencyTree`.
231
413
  *
414
+ * The root project is kept when it has sources of its own, since it is then
415
+ * the application rather than a pure aggregator.
416
+ *
232
417
  * Falls back to the regex-based {@link discoverSbtProjects} heuristic when the
233
418
  * sbt invocation fails or yields nothing useful.
234
419
  *
235
420
  * @param {string} basePath Directory of the sbt build
236
421
  * @param {string} sbtCmd sbt executable
237
422
  * @param {Object} env Environment for the spawned process
423
+ * @param {string[]} [launcherArgs] Launcher options placed before the sbt arguments
238
424
  * @returns {string[]} List of sbt project ids
239
425
  */
240
- export function discoverSbtProjectsFromCmd(basePath, sbtCmd, env) {
426
+ export function discoverSbtProjectsFromCmd(
427
+ basePath,
428
+ sbtCmd,
429
+ env,
430
+ launcherArgs = [],
431
+ ) {
241
432
  try {
242
- const result = safeSpawnSync(
433
+ const result = sbtSpawnSync(
243
434
  sbtCmd,
244
- ["-batch", "-no-colors", '"projects"'],
435
+ [...launcherArgs, "-batch", "-no-colors", '"projects"'],
245
436
  {
246
437
  cwd: basePath,
247
438
  shell: true,
439
+ // The quotes around the sbt command are for the shell to strip.
440
+ cdxgenRawShellArgs: true,
248
441
  env,
249
442
  encoding: "utf-8",
250
443
  },
444
+ true,
251
445
  );
252
446
  if (result.status === 0 && !result.error) {
253
447
  const { projects, root } = parseSbtProjects(result.stdout || "");
254
- // Exclude the aggregating root project - its own dependencyTree is
255
- // usually a subset of the aggregated subprojects and re-resolving it
256
- // only adds noise/time.
257
- const subprojects = projects.filter((p) => p !== root);
448
+ // Exclude the aggregating root project unless it has sources of its
449
+ // own: its dependencyTree is then the application's own dependencies
450
+ // and not a subset of the aggregated subprojects.
451
+ const subprojects = sbtRootHasOwnSources(basePath)
452
+ ? projects
453
+ : projects.filter((p) => p !== root);
258
454
  if (subprojects.length > 0) {
259
455
  if (DEBUG_MODE) {
260
456
  console.log(
@@ -318,6 +514,8 @@ export async function createJavaBom(path, options) {
318
514
  let tools;
319
515
  let possible_misses = false;
320
516
  let parallelPropTaskOut = "";
517
+ // Mill daemons already running belong to the user and outlive the scan
518
+ const millDaemonsBefore = findMillDaemons(path);
321
519
  try {
322
520
  // war/ear mode
323
521
  if (path.endsWith(".war") || path.endsWith(".jar")) {
@@ -327,8 +525,10 @@ export async function createJavaBom(path, options) {
327
525
  console.log(`Retrieving packages from ${path}`);
328
526
  }
329
527
  const tempDir = safeMkdtempSync(join(getTmpDir(), "war-deps-"));
330
- jarNSMapping = await collectJarNS(tempDir);
331
- pkgList = await extractJarArchive(path, tempDir, jarNSMapping);
528
+ // The nested jars are read from the extraction directory and their
529
+ // namespaces attached to each component there; the directory is
530
+ // removed afterwards, so no jar paths are kept for evinse.
531
+ pkgList = await extractJarArchive(path, tempDir);
332
532
  if (pkgList.length) {
333
533
  pkgList = await getMvnMetadata(pkgList);
334
534
  }
@@ -453,9 +653,21 @@ export async function createJavaBom(path, options) {
453
653
  }
454
654
  }
455
655
  const firstPom = pomFiles.length ? pomFiles[0] : undefined;
456
- let mavenCmd = getMavenCommand(path, path);
656
+ let mavenCmd = getMavenCommand(path, path, options);
657
+ // Pom files whose own tree a recursive run of their aggregator already
658
+ // returned. Each is skipped when its turn comes, so a reactor is run
659
+ // once from its aggregator instead of once per member.
660
+ const coveredPomFiles = new Set();
457
661
  for (const f of pomFiles) {
458
662
  const basePath = dirname(f);
663
+ if (coveredPomFiles.has(f)) {
664
+ if (DEBUG_MODE) {
665
+ console.log(
666
+ `Skipped ${basePath}: the recursive dependency:tree run of its aggregator already resolved it.`,
667
+ );
668
+ }
669
+ continue;
670
+ }
459
671
  if (
460
672
  isQuarkus &&
461
673
  !options.deep &&
@@ -466,14 +678,39 @@ export async function createJavaBom(path, options) {
466
678
  }
467
679
  continue;
468
680
  }
469
- const settingsXml = join(basePath, "settings.xml");
470
- if (safeExistsSync(settingsXml)) {
471
- console.log(
472
- `maven settings.xml found in ${basePath}. Please set the MVN_ARGS environment variable based on the full mvn build command used for this project.\nExample: MVN_ARGS='--settings ${settingsXml}'`,
473
- );
681
+ if (isBuildToolRateLimited("maven")) {
682
+ // Every further Maven run in this scan would pay the same back-off
683
+ // for the same empty answer, so Maven is not asked again. The
684
+ // direct dependencies the pom declares are kept, as for a pom whose
685
+ // run failed.
686
+ if (DEBUG_MODE) {
687
+ console.log(
688
+ `Parsing ${f} without Maven: the repository rate limited Maven in this scan.`,
689
+ );
690
+ }
691
+ const dlist = parsePom(f)?.dependencies || [];
692
+ if (dlist.length) {
693
+ pkgList = pkgList.concat(dlist);
694
+ }
695
+ continue;
474
696
  }
475
697
  if (mavenCmd?.endsWith("mvn")) {
476
- mavenCmd = getMavenCommand(basePath, path);
698
+ mavenCmd = getMavenCommand(basePath, path, options);
699
+ }
700
+ // A settings.xml beside the pom is passed as global settings, so the
701
+ // user's own settings.xml, with its mirrors and credentials, still
702
+ // applies on top of it.
703
+ const projectSettings = projectSettingsArgs(basePath, {
704
+ secureMode: isSecureMode,
705
+ });
706
+ if (projectSettings.args.length) {
707
+ console.log(
708
+ `Passing ${projectSettings.settingsFile} to Maven as global settings (-gs), so your own settings.xml still applies. Name a settings file in MVN_ARGS to change this.`,
709
+ );
710
+ } else if (projectSettings.skipped === "secure-mode") {
711
+ console.log(
712
+ `Ignoring ${projectSettings.settingsFile} in secure mode. If you trust it, pass it with MVN_ARGS='--global-settings ${projectSettings.settingsFile}'.`,
713
+ );
477
714
  }
478
715
  // Should we attempt to resolve class names
479
716
  if (options.resolveClass || options.deep) {
@@ -488,13 +725,18 @@ export async function createJavaBom(path, options) {
488
725
  }
489
726
  }
490
727
  // Use the cyclonedx maven plugin if there is no preference for maven deps tree
491
- if (!useMavenDepsTree) {
728
+ if (!useMavenDepsTree && !isBuildToolRateLimited("maven")) {
492
729
  thoughtLog("The user wants me to use the cyclonedx-maven plugin.");
493
730
  console.log(`Executing '${mavenCmd}' in`, basePath);
494
731
  result = safeSpawnSync(mavenCmd, mvnArgs, {
495
732
  cwd: basePath,
496
733
  shell: isWin,
497
734
  });
735
+ noteBuildToolRateLimit("maven", commandOutputText(result), {
736
+ command: `${basename(mavenCmd)} ${mvnArgs.join(" ")}`,
737
+ exitCode:
738
+ typeof result.status === "number" ? result.status : undefined,
739
+ });
498
740
  // Check if the cyclonedx plugin created the required bom.json file
499
741
  // Sometimes the plugin fails silently for complex maven projects
500
742
  bomJsonFiles = getAllFiles(
@@ -520,8 +762,20 @@ export async function createJavaBom(path, options) {
520
762
  ) {
521
763
  const tempRoot = getTmpDir();
522
764
  const tempDir = safeMkdtempSync(join(tempRoot, "cdxgen-mvn-"));
523
- const tempMvnTree = join(tempDir, "cdxgen-mvn-tree.json");
524
- const tempMvnTreeText = join(tempDir, "cdxgen-mvn-tree.txt");
765
+ // The recursive run writes one tree per reactor module into these
766
+ // directories, so the aggregator's run returns every module.
767
+ const treeJsonDir = join(tempDir, "tree-json");
768
+ const treeTextDir = join(tempDir, "tree-text");
769
+ safeMkdirSync(treeJsonDir);
770
+ safeMkdirSync(treeTextDir);
771
+ const tempMvnTree = join(
772
+ treeJsonDir,
773
+ `${MAVEN_MODULE_TREE_STEM}.json`,
774
+ );
775
+ const tempMvnTreeText = join(
776
+ treeTextDir,
777
+ `${MAVEN_MODULE_TREE_STEM}.txt`,
778
+ );
525
779
  const tempMvnParentTree = join(
526
780
  tempDir,
527
781
  "cdxgen-mvn-parent-tree.json",
@@ -541,14 +795,11 @@ export async function createJavaBom(path, options) {
541
795
  addArgs = parseMavenArgs(readEnvironmentVariable("MVN_ARGS"));
542
796
  mvnTreeArgs = mvnTreeArgs.concat(addArgs);
543
797
  }
544
- // Automatically use settings.xml to improve the success for fallback
545
- if (safeExistsSync(settingsXml)) {
546
- mvnTreeArgs.push("-s");
547
- mvnTreeArgs.push(settingsXml);
548
- }
798
+ mvnTreeArgs = mvnTreeArgs.concat(projectSettings.args);
549
799
  // For the first pom alone, we need to execute first in non-recursive mode to capture
550
800
  // the parent component. Then, we execute all of them in recursive mode
551
- if (f === firstPom) {
801
+ // A rate limit the --deep copy-dependencies run met ends it too.
802
+ if (f === firstPom && !isBuildToolRateLimited("maven")) {
552
803
  thoughtLog(
553
804
  "What is the parent component here? Let's use maven command to find out.",
554
805
  );
@@ -558,14 +809,19 @@ export async function createJavaBom(path, options) {
558
809
  `-DoutputFile=${tempMvnParentTree}`,
559
810
  "-DoutputType=json",
560
811
  ];
561
- if (addArgs.length) {
562
- findParentComponentArgs =
563
- findParentComponentArgs.concat(addArgs);
564
- }
812
+ findParentComponentArgs = findParentComponentArgs.concat(
813
+ addArgs,
814
+ projectSettings.args,
815
+ );
565
816
  result = safeSpawnSync(mavenCmd, findParentComponentArgs, {
566
817
  cwd: basePath,
567
818
  shell: isWin,
568
819
  });
820
+ noteBuildToolRateLimit("maven", commandOutputText(result), {
821
+ command: `${basename(mavenCmd)} ${findParentComponentArgs.join(" ")}`,
822
+ exitCode:
823
+ typeof result.status === "number" ? result.status : undefined,
824
+ });
569
825
  // If json is empty or unparseable, fallback to text parsing
570
826
  let emptyJson = !safeExistsSync(tempMvnParentTree);
571
827
  if (safeExistsSync(tempMvnParentTree)) {
@@ -581,16 +837,15 @@ export async function createJavaBom(path, options) {
581
837
  }
582
838
  }
583
839
  }
584
- if (result.status !== 0 || result.error || emptyJson) {
840
+ if (
841
+ !isBuildToolRateLimited("maven") &&
842
+ (result.status !== 0 || result.error || emptyJson)
843
+ ) {
585
844
  findParentComponentArgs = [
586
845
  "dependency:tree",
587
846
  "-N",
588
847
  `-DoutputFile=${tempMvnParentTreeText}`,
589
- ];
590
- if (addArgs.length) {
591
- findParentComponentArgs =
592
- findParentComponentArgs.concat(addArgs);
593
- }
848
+ ].concat(addArgs, projectSettings.args);
594
849
  result = safeSpawnSync(mavenCmd, findParentComponentArgs, {
595
850
  cwd: basePath,
596
851
  shell: isWin,
@@ -630,49 +885,60 @@ export async function createJavaBom(path, options) {
630
885
  `Executing '${basename(mavenCmd)} dependency:tree ...' in ${basePath}`,
631
886
  );
632
887
  }
633
- result = safeSpawnSync(mavenCmd, mvnTreeArgs, {
634
- cwd: basePath,
635
- shell: isWin,
636
- });
637
- let emptyJson = !safeExistsSync(tempMvnTree);
638
- if (safeExistsSync(tempMvnTree)) {
639
- const mvnTreeString = readFileSync(tempMvnTree, {
640
- encoding: "utf-8",
641
- });
642
- const parsedList = parseMavenTreeJson(mvnTreeString, f);
643
- if (!parsedList?.pkgList?.length) {
644
- emptyJson = true;
645
- // Remove the invalid json file so text output is preferred below.
646
- if (safeExistsSync(tempMvnTree)) {
647
- safeUnlinkSync(tempMvnTree);
648
- }
649
- }
650
- }
651
- if (result.status !== 0 || result.error || emptyJson) {
652
- mvnTreeArgs = [
653
- "dependency:tree",
654
- `-DoutputFile=${tempMvnTreeText}`,
655
- ];
656
- if (addArgs.length) {
657
- mvnTreeArgs = mvnTreeArgs.concat(addArgs);
658
- }
659
- if (safeExistsSync(settingsXml)) {
660
- mvnTreeArgs.push("-s");
661
- mvnTreeArgs.push(settingsXml);
662
- }
888
+ if (isBuildToolRateLimited("maven")) {
889
+ // The parent or plugin run already met the rate limit, so the
890
+ // tree run is skipped rather than repeated for the same answer.
891
+ result = { status: 1 };
892
+ } else {
663
893
  result = safeSpawnSync(mavenCmd, mvnTreeArgs, {
664
894
  cwd: basePath,
665
895
  shell: isWin,
666
896
  });
897
+ noteBuildToolRateLimit("maven", commandOutputText(result), {
898
+ command: `${basename(mavenCmd)} ${mvnTreeArgs.join(" ")}`,
899
+ exitCode:
900
+ typeof result.status === "number" ? result.status : undefined,
901
+ });
902
+ const emptyJson = !readMavenModuleTrees(
903
+ treeJsonDir,
904
+ f,
905
+ ).trees.some(
906
+ (tree) =>
907
+ parseMavenTreeJson(tree.text, tree.pomFile)?.pkgList?.length,
908
+ );
909
+ if (emptyJson) {
910
+ // Remove the invalid json trees so the text output is
911
+ // preferred below.
912
+ safeRmSync(treeJsonDir, { recursive: true, force: true });
913
+ }
914
+ if (
915
+ !isBuildToolRateLimited("maven") &&
916
+ (result.status !== 0 || result.error || emptyJson)
917
+ ) {
918
+ mvnTreeArgs = [
919
+ "dependency:tree",
920
+ `-DoutputFile=${tempMvnTreeText}`,
921
+ ].concat(addArgs, projectSettings.args);
922
+ result = safeSpawnSync(mavenCmd, mvnTreeArgs, {
923
+ cwd: basePath,
924
+ shell: isWin,
925
+ });
926
+ noteBuildToolRateLimit("maven", commandOutputText(result), {
927
+ command: `${basename(mavenCmd)} ${mvnTreeArgs.join(" ")}`,
928
+ exitCode:
929
+ typeof result.status === "number"
930
+ ? result.status
931
+ : undefined,
932
+ });
933
+ }
667
934
  }
668
935
  if (result.status !== 0 || result.error) {
669
936
  possible_misses = true;
670
937
  // Our approach to recursively invoking the maven plugin for each sub-module is bound to result in failures
671
938
  // These could be due to a range of reasons that are covered below.
672
939
  if (
673
- pomFiles.length === 1 ||
674
- DEBUG_MODE ||
675
- PREFER_MAVEN_DEPS_TREE
940
+ !isBuildToolRateLimited("maven") &&
941
+ (pomFiles.length === 1 || DEBUG_MODE || PREFER_MAVEN_DEPS_TREE)
676
942
  ) {
677
943
  if (result.stdout) {
678
944
  console.log(result.stdout);
@@ -771,16 +1037,18 @@ export async function createJavaBom(path, options) {
771
1037
  pkgList = pkgList.concat(dlist);
772
1038
  }
773
1039
  } else {
774
- const treeFile = safeExistsSync(tempMvnTreeText)
775
- ? tempMvnTreeText
776
- : tempMvnTree;
777
- if (safeExistsSync(treeFile)) {
778
- const mvnTreeString = readFileSync(treeFile, {
779
- encoding: "utf-8",
780
- });
781
- const parsedList = treeFile.endsWith(".json")
782
- ? parseMavenTreeJson(mvnTreeString, f)
783
- : parseMavenTree(mvnTreeString, f);
1040
+ // The text trees exist only when the JSON run failed and was
1041
+ // retried. Each reactor module has its own tree, and a member
1042
+ // whose tree is here is not run again on its own.
1043
+ let moduleTrees = readMavenModuleTrees(treeTextDir, f);
1044
+ if (!moduleTrees.trees.length) {
1045
+ moduleTrees = readMavenModuleTrees(treeJsonDir, f);
1046
+ }
1047
+ for (const modulePom of moduleTrees.covered) {
1048
+ coveredPomFiles.add(modulePom);
1049
+ }
1050
+ for (const tree of moduleTrees.trees) {
1051
+ const parsedList = parseMavenTree(tree.text, tree.pomFile);
784
1052
  const dlist = parsedList.pkgList || [];
785
1053
  const tmpParentComponent = dlist.splice(0, 1)[0];
786
1054
  if (tmpParentComponent) {
@@ -1041,11 +1309,23 @@ export async function createJavaBom(path, options) {
1041
1309
  if (resolveGradleDistribution && gradleInitArgs.length) {
1042
1310
  gradleInitArgs.push("-PcdxgenResolveDistribution=true");
1043
1311
  }
1044
- parallelPropTaskOut = executeParallelGradleProperties(
1045
- gradleRootPath,
1046
- [null].concat(includedBuilds),
1047
- gradleInitArgs,
1048
- );
1312
+ // `gradle properties` evaluates the project's build scripts, so it is
1313
+ // part of the dependency-tooling surface gated by --no-install-deps
1314
+ // (and the pre-build lifecycle).
1315
+ const gradleToolingAllowed =
1316
+ options.installDeps !== false || options.introspect === true;
1317
+ if (gradleToolingAllowed) {
1318
+ parallelPropTaskOut = executeParallelGradleProperties(
1319
+ gradleRootPath,
1320
+ [null].concat(includedBuilds),
1321
+ gradleInitArgs,
1322
+ options,
1323
+ );
1324
+ } else {
1325
+ console.log(
1326
+ "Skipping 'gradle properties' since dependency installation is disabled. Only statically parsed Gradle metadata will be used.",
1327
+ );
1328
+ }
1049
1329
  if (readEnvironmentVariable("GRADLE_INCLUDED_BUILDS") === undefined) {
1050
1330
  const outputLines = parallelPropTaskOut.split("\n");
1051
1331
  for (const [_i, line] of outputLines.entries()) {
@@ -1073,7 +1353,14 @@ export async function createJavaBom(path, options) {
1073
1353
  }
1074
1354
  parallelPropTaskOut = parallelPropTaskOut.concat(
1075
1355
  "\n",
1076
- executeParallelGradleProperties(gradleRootPath, includedBuilds),
1356
+ gradleToolingAllowed
1357
+ ? executeParallelGradleProperties(
1358
+ gradleRootPath,
1359
+ includedBuilds,
1360
+ [],
1361
+ options,
1362
+ )
1363
+ : "",
1077
1364
  );
1078
1365
  }
1079
1366
  }
@@ -1114,6 +1401,8 @@ export async function createJavaBom(path, options) {
1114
1401
  parallelPropTaskOut = executeParallelGradleProperties(
1115
1402
  gradleRootPath,
1116
1403
  allProjectsStr.filter((module) => !modulesToSkip.includes(module)),
1404
+ [],
1405
+ options,
1117
1406
  );
1118
1407
  const splitPropTaskOut = splitOutputByGradleProjects(
1119
1408
  parallelPropTaskOut,
@@ -1161,7 +1450,7 @@ export async function createJavaBom(path, options) {
1161
1450
  )
1162
1451
  ) {
1163
1452
  allProjects.push(parentComponent);
1164
- const gradleCmd = getGradleCommand(gradleRootPath, null);
1453
+ const gradleCmd = getGradleCommand(gradleRootPath, null, options);
1165
1454
  const gradleDepTask = readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1166
1455
  ? readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1167
1456
  : "dependencies";
@@ -1196,6 +1485,14 @@ export async function createJavaBom(path, options) {
1196
1485
  );
1197
1486
  const allOutputs = [];
1198
1487
  for (const gradleArg of gradleArguments) {
1488
+ if (isBuildToolRateLimited("gradle")) {
1489
+ if (DEBUG_MODE) {
1490
+ console.log(
1491
+ "Skipping the remaining gradle dependency tasks: the repository rate limited gradle in this scan.",
1492
+ );
1493
+ }
1494
+ break;
1495
+ }
1199
1496
  if (DEBUG_MODE) {
1200
1497
  console.log(
1201
1498
  `Executing ${gradleCmd} with arguments ${gradleArg.join(" ").substring(0, 150)}... in ${gradleRootPath}`,
@@ -1211,6 +1508,13 @@ export async function createJavaBom(path, options) {
1211
1508
  cwd: gradleRootPath,
1212
1509
  shell: isWin,
1213
1510
  });
1511
+ // The dependencies task exits 0 even when resolution failed under a
1512
+ // rate limit, so the output decides, not the status.
1513
+ noteBuildToolRateLimit("gradle", commandOutputText(sresult), {
1514
+ command: `${basename(gradleCmd)} ${finalGradleArg.join(" ")}`,
1515
+ exitCode:
1516
+ typeof sresult.status === "number" ? sresult.status : undefined,
1517
+ });
1214
1518
  if (sresult.status !== 0 || sresult.error) {
1215
1519
  if (options.failOnError || DEBUG_MODE) {
1216
1520
  console.error(sresult.stdout, sresult.stderr);
@@ -1621,8 +1925,6 @@ export async function createJavaBom(path, options) {
1621
1925
  `${options.multiProject ? "**/" : ""}build.sbt.lock`,
1622
1926
  options,
1623
1927
  );
1624
- const tempCacheDir = safeMkdtempSync(join(getTmpDir(), "sbt-cache-"));
1625
- safeMkdirSync(tempCacheDir, { recursive: true });
1626
1928
  if (
1627
1929
  sbtProjects?.length &&
1628
1930
  isPackageManagerAllowed(
@@ -1639,6 +1941,13 @@ export async function createJavaBom(path, options) {
1639
1941
  pkgList = pkgList.concat(dlist);
1640
1942
  }
1641
1943
  }
1944
+ } else if (options.installDeps === false && !options.introspect) {
1945
+ // Executing sbt evaluates the project's build definition (arbitrary
1946
+ // Scala), so it belongs to the dependency-tooling surface disabled
1947
+ // by --no-install-deps and the pre-build lifecycle.
1948
+ console.log(
1949
+ "Skipping sbt dependencyTree since dependency installation is disabled. Only statically parsed sbt metadata will be used.",
1950
+ );
1642
1951
  } else {
1643
1952
  const SBT_CMD = readEnvironmentVariable("SBT_CMD") || "sbt";
1644
1953
  let sbtVersion = determineSbtVersion(path);
@@ -1681,20 +1990,46 @@ export async function createJavaBom(path, options) {
1681
1990
  }
1682
1991
  }
1683
1992
  safeWriteSync(tempSbtPlugins, sbtPluginDefinition);
1684
- let sbtExtraArgs = "";
1993
+ // sbt resolves into the user's own caches, so a project that has been
1994
+ // built resolves without the network. The dependency trees download
1995
+ // the jar of every dependency they list, which is all --deep reads.
1685
1996
  const env = { ...process.env };
1686
- // We need to collect the jars from the cache
1687
- if (options.deep) {
1688
- sbtExtraArgs = " updateClassifiers";
1689
- env["COURSIER_CACHE"] = tempCacheDir;
1690
- env["SBT_IVY_HOME"] = tempCacheDir;
1691
- }
1692
1997
  for (const i in sbtProjects) {
1998
+ if (isBuildToolRateLimited("sbt")) {
1999
+ // Every remaining sbt build in this scan resolves from the same
2000
+ // rate limited repository.
2001
+ if (DEBUG_MODE) {
2002
+ console.log(
2003
+ `Skipping ${sbtProjects[i]}: the repository rate limited sbt in this scan.`,
2004
+ );
2005
+ }
2006
+ break;
2007
+ }
1693
2008
  const basePath = sbtProjects[i];
1694
2009
  const dlFile = join(tempDir, `dl-${i}.tmp`);
2010
+ // The sbt version of the project itself decides the invocation
2011
+ // form: sbt 2 both joins separate command-line arguments into a
2012
+ // single command line and no longer offers `dependencyTree / toFile`.
2013
+ const projectSbtVersion = determineSbtVersion(basePath) || sbtVersion;
2014
+ const isSbt2 = isSbt2Version(projectSbtVersion);
2015
+ // The sbt 2 launcher runs a thin client by default. When a server is
2016
+ // already running for the build, an IDE's for example, the client
2017
+ // prints neither the project list nor the trees, so sbt runs
2018
+ // in-process instead. sbt.bat has no `--server` option.
2019
+ const sbtLauncherArgs = isSbt2 && !isWin ? ["--server"] : [];
2020
+ const sbtServerFile = join(
2021
+ basePath,
2022
+ "project",
2023
+ "target",
2024
+ "active.json",
2025
+ );
2026
+ const sbtServerWasRunning = isSbt2 && safeExistsSync(sbtServerFile);
1695
2027
  let sbtArgs = [];
1696
2028
  let pluginFile = null;
2029
+ let pluginAdded = false;
1697
2030
  const subDlFiles = [];
2031
+ const failedSubprojects = [];
2032
+ let subprojects = [];
1698
2033
  if (standalonePluginFile) {
1699
2034
  sbtArgs = [
1700
2035
  `-addPluginSbtFile=${tempSbtPlugins}`,
@@ -1708,15 +2043,23 @@ export async function createJavaBom(path, options) {
1708
2043
  // `dependencyTree` fan-out yields a far more complete tree than a
1709
2044
  // single root resolution (which only captures the root project's own
1710
2045
  // libraryDependencies). See https://github.com/cdxgen/cdxgen/issues/4291
1711
- const subprojects = discoverSbtProjectsFromCmd(
2046
+ subprojects = discoverSbtProjectsFromCmd(
1712
2047
  basePath,
1713
2048
  SBT_CMD,
1714
2049
  env,
2050
+ sbtLauncherArgs,
1715
2051
  );
1716
- if (subprojects.length > 0 && useSlashSyntax) {
1717
- sbtArgs = [
1718
- `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs}`,
1719
- ];
2052
+ if (isSbt2) {
2053
+ const trees = subprojects.length
2054
+ ? subprojects.map((sp) => ({
2055
+ project: sp,
2056
+ file: join(tempDir, `dl-${i}-${sp}.tmp`),
2057
+ }))
2058
+ : [{ file: dlFile }];
2059
+ subDlFiles.push(...trees.map((t) => t.file));
2060
+ sbtArgs = [sbt2DependencyTreeCommand(trees)];
2061
+ } else if (subprojects.length > 0 && useSlashSyntax) {
2062
+ sbtArgs = ["'set ThisBuild / asciiGraphWidth := 800'"];
1720
2063
  for (const sp of subprojects) {
1721
2064
  const subDlFile = join(tempDir, `dl-${i}-${sp}.tmp`);
1722
2065
  subDlFiles.push(subDlFile);
@@ -1726,21 +2069,27 @@ export async function createJavaBom(path, options) {
1726
2069
  }
1727
2070
  } else if (useSlashSyntax) {
1728
2071
  sbtArgs = [
1729
- `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs} "dependencyTree / toFile ${dlFile} --force"`,
2072
+ `'set ThisBuild / asciiGraphWidth := 800' "dependencyTree / toFile ${dlFile} --force"`,
1730
2073
  ];
1731
2074
  subDlFiles.push(dlFile);
1732
2075
  } else {
1733
2076
  sbtArgs = [
1734
- `'set asciiGraphWidth in ThisBuild := 800'${sbtExtraArgs} "dependencyTree::toFile ${dlFile} --force"`,
2077
+ `'set asciiGraphWidth in ThisBuild := 800' "dependencyTree::toFile ${dlFile} --force"`,
1735
2078
  ];
1736
2079
  subDlFiles.push(dlFile);
1737
2080
  }
1738
- pluginFile = addPlugin(basePath, sbtPluginDefinition);
2081
+ // sbt 2 enables its dependency tree plugin by default, so the
2082
+ // build's plugins.sbt stays untouched and the build definition is
2083
+ // not recompiled for the scan.
2084
+ if (!isSbt2) {
2085
+ pluginFile = addPlugin(basePath, sbtPluginDefinition);
2086
+ pluginAdded = true;
2087
+ }
1739
2088
  }
1740
2089
  // Run sbt in non-interactive batch mode so it never blocks waiting for
1741
2090
  // input on stdin (which can otherwise appear as an indefinite hang) and
1742
2091
  // does not emit ANSI colour codes into the dependency tree output.
1743
- sbtArgs = ["-batch", "-no-colors", ...sbtArgs];
2092
+ sbtArgs = [...sbtLauncherArgs, "-batch", "-no-colors", ...sbtArgs];
1744
2093
  console.log(
1745
2094
  "Executing",
1746
2095
  SBT_CMD,
@@ -1751,13 +2100,31 @@ export async function createJavaBom(path, options) {
1751
2100
  tempSbtgDir,
1752
2101
  );
1753
2102
  // Note that the command has to be invoked with `shell: true` to properly execut sbt
1754
- const result = safeSpawnSync(SBT_CMD, sbtArgs, {
1755
- cwd: basePath,
1756
- shell: true,
1757
- env,
2103
+ // sbt 2 is the exception: its ;-joined command argument contains a
2104
+ // semicolon, so on POSIX the launcher is spawned directly and the
2105
+ // argument reaches sbt verbatim.
2106
+ const result = sbtSpawnSync(
2107
+ SBT_CMD,
2108
+ sbtArgs,
2109
+ {
2110
+ cwd: basePath,
2111
+ shell: isWin || !isSbt2 || standalonePluginFile,
2112
+ // The sbt commands below embed their own shell quoting, which the
2113
+ // shell must strip for sbt to see separate commands.
2114
+ cdxgenRawShellArgs: true,
2115
+ env,
2116
+ },
2117
+ !isSbt2 || standalonePluginFile,
2118
+ );
2119
+ noteBuildToolRateLimit("sbt", commandOutputText(result), {
2120
+ command: `${SBT_CMD} ${sbtArgs.join(" ")}`,
2121
+ exitCode:
2122
+ typeof result.status === "number" ? result.status : undefined,
1758
2123
  });
1759
2124
  if (result.status !== 0 || result.error) {
1760
- console.error(result.stdout, result.stderr);
2125
+ if (!isBuildToolRateLimited("sbt")) {
2126
+ console.error(result.stdout, result.stderr);
2127
+ }
1761
2128
  recordDegradation("jvm.sbt.no-lockfile", {
1762
2129
  ecosystem: "java",
1763
2130
  tool: "sbt",
@@ -1768,15 +2135,20 @@ export async function createJavaBom(path, options) {
1768
2135
  detail:
1769
2136
  "The sbt dependencyTree command did not succeed, so the declared sbt dependencies are the only evidence available.",
1770
2137
  });
1771
- console.log(
1772
- "1. Check if scala and sbt is installed and available in PATH. Only scala 2.10 + sbt 0.13.6+ and 2.12 + sbt 1.0+ is supported for now.",
1773
- );
1774
- console.log(
1775
- "2. Check if the plugin net.virtual-void:sbt-dependency-graph 0.10.0-RC1 can be used in the environment",
1776
- );
1777
- console.log(
1778
- "3. Consider creating a lockfile using sbt-dependency-lock plugin. See https://github.com/stringbean/sbt-dependency-lock",
1779
- );
2138
+ if (!isBuildToolRateLimited("sbt")) {
2139
+ console.log(
2140
+ "1. Check if scala and sbt is installed and available in PATH. Only scala 2.10 + sbt 0.13.6+, 2.12 + sbt 1.0+ and sbt 2.x are supported for now.",
2141
+ );
2142
+ // sbt 2 brings its own dependency tree, so no plugin is added.
2143
+ if (!isSbt2) {
2144
+ console.log(
2145
+ "2. Check if the plugin net.virtual-void:sbt-dependency-graph 0.10.0-RC1 can be used in the environment",
2146
+ );
2147
+ }
2148
+ console.log(
2149
+ `${isSbt2 ? 2 : 3}. Consider creating a lockfile using sbt-dependency-lock plugin. See https://github.com/stringbean/sbt-dependency-lock`,
2150
+ );
2151
+ }
1780
2152
  deferFailOnError(options, {
1781
2153
  ecosystem: "java",
1782
2154
  tool: "sbt",
@@ -1786,13 +2158,95 @@ export async function createJavaBom(path, options) {
1786
2158
  detail: "the sbt dependencyTree command failed",
1787
2159
  });
1788
2160
  }
1789
- if (!standalonePluginFile) {
2161
+ // One failing subproject (for example an unresolvable SNAPSHOT)
2162
+ // fails the combined command and would zero the whole SBOM. Retry
2163
+ // each subproject on its own and keep the trees that succeed. A rate
2164
+ // limited repository ends the retries as well: each would download
2165
+ // the same artifacts against the same 429.
2166
+ if (
2167
+ !standalonePluginFile &&
2168
+ subprojects.length > 0 &&
2169
+ (result.status !== 0 || result.error) &&
2170
+ !isBuildToolRateLimited("sbt")
2171
+ ) {
2172
+ for (const sp of subprojects) {
2173
+ const subDlFile = join(tempDir, `dl-${i}-${sp}.tmp`);
2174
+ if (safeExistsSync(subDlFile)) {
2175
+ // The combined run produced this tree before it failed.
2176
+ continue;
2177
+ }
2178
+ if (DEBUG_MODE) {
2179
+ console.log(`Retrying the sbt tree of ${sp} on its own.`);
2180
+ }
2181
+ let retryArgs;
2182
+ if (isSbt2) {
2183
+ retryArgs = [
2184
+ ...sbtLauncherArgs,
2185
+ "-batch",
2186
+ "-no-colors",
2187
+ sbt2DependencyTreeCommand([{ project: sp, file: subDlFile }]),
2188
+ ];
2189
+ } else if (useSlashSyntax) {
2190
+ retryArgs = [
2191
+ "-batch",
2192
+ "-no-colors",
2193
+ "'set ThisBuild / asciiGraphWidth := 800'",
2194
+ `"${sp}/dependencyTree / toFile ${subDlFile} --force"`,
2195
+ ];
2196
+ } else {
2197
+ continue;
2198
+ }
2199
+ sbtSpawnSync(
2200
+ SBT_CMD,
2201
+ retryArgs,
2202
+ {
2203
+ cwd: basePath,
2204
+ shell: isWin || !isSbt2,
2205
+ cdxgenRawShellArgs: true,
2206
+ env,
2207
+ },
2208
+ !isSbt2,
2209
+ );
2210
+ // The tree file is already listed for parsing, so a retry only
2211
+ // has to produce it.
2212
+ if (!safeExistsSync(subDlFile)) {
2213
+ failedSubprojects.push(sp);
2214
+ }
2215
+ }
2216
+ if (failedSubprojects.length) {
2217
+ console.log(
2218
+ `The sbt dependency trees of ${failedSubprojects.join(", ")} could not be resolved.`,
2219
+ );
2220
+ }
2221
+ }
2222
+ // Where the thin client still runs, it leaves the server it started
2223
+ // behind. A server that was running before the scan is not ours to
2224
+ // stop.
2225
+ if (isSbt2 && !sbtServerWasRunning && safeExistsSync(sbtServerFile)) {
2226
+ sbtSpawnSync(SBT_CMD, ["-batch", "-no-colors", "shutdown"], {
2227
+ cwd: basePath,
2228
+ shell: isWin,
2229
+ env,
2230
+ });
2231
+ }
2232
+ // Only a plugins.sbt cdxgen amended is restored: without a backup,
2233
+ // the cleanup removes the file as one cdxgen created.
2234
+ if (pluginAdded) {
1790
2235
  cleanupPlugin(basePath, pluginFile);
1791
2236
  }
1792
2237
  const sbtSubprojectRoots = [];
1793
2238
  for (const subDlFile of subDlFiles) {
1794
2239
  if (safeExistsSync(subDlFile)) {
1795
- const retMap = await parseSbtTree(subDlFile);
2240
+ // The trees are temporary files that go away with the scan, so
2241
+ // the components name the build definition they were resolved
2242
+ // from instead: build.sbt, or for a build written only in
2243
+ // project/*.scala, the build.properties beside it.
2244
+ const sbtBuildFile =
2245
+ [
2246
+ join(basePath, "build.sbt"),
2247
+ join(basePath, "project", "build.properties"),
2248
+ ].find((f) => safeExistsSync(f)) || basePath;
2249
+ const retMap = await parseSbtTree(subDlFile, sbtBuildFile);
1796
2250
  if (retMap.pkgList?.length) {
1797
2251
  const tmpParentComponent = retMap.pkgList.splice(0, 1)[0];
1798
2252
  tmpParentComponent.type = "application";
@@ -1840,19 +2294,38 @@ export async function createJavaBom(path, options) {
1840
2294
  "bom-ref": decodeURIComponent(rootPurl),
1841
2295
  };
1842
2296
  parentComponent = rootComponent;
1843
- dependencies.push({
1844
- ref: decodeURIComponent(rootPurl),
1845
- dependsOn: sbtSubprojectRoots
1846
- .map((c) => c["bom-ref"])
1847
- .filter(Boolean),
1848
- });
2297
+ // Merged rather than appended, so the root lists every subproject
2298
+ // once, and a root project with sources of its own, whose tree is
2299
+ // the synthetic root itself, neither depends on itself nor gets a
2300
+ // second dependency entry.
2301
+ dependencies = mergeDependencies(
2302
+ dependencies,
2303
+ [
2304
+ {
2305
+ ref: rootComponent["bom-ref"],
2306
+ dependsOn: sbtSubprojectRoots
2307
+ .map((c) => c["bom-ref"])
2308
+ .filter(Boolean),
2309
+ },
2310
+ ],
2311
+ rootComponent,
2312
+ );
1849
2313
  } else if (sbtSubprojectRoots.length > 0 && !parentComponent) {
1850
2314
  parentComponent = sbtSubprojectRoots[0];
1851
2315
  }
2316
+ if (failedSubprojects.length && parentComponent?.name) {
2317
+ parentComponent.properties ??= [];
2318
+ parentComponent.properties.push({
2319
+ name: "cdx:sbt:failedProjects",
2320
+ value: failedSubprojects.join(", "),
2321
+ });
2322
+ }
1852
2323
  }
1853
2324
 
1854
- // Cleanup
1855
- safeUnlinkSync(tempSbtPlugins);
2325
+ // The trees were parsed inside the loop, so neither directory is
2326
+ // needed any more.
2327
+ safeRmSync(tempDir, { recursive: true, force: true });
2328
+ safeRmSync(tempSbtgDir, { recursive: true, force: true });
1856
2329
  } // else
1857
2330
 
1858
2331
  if (DEBUG_MODE) {
@@ -1860,7 +2333,10 @@ export async function createJavaBom(path, options) {
1860
2333
  }
1861
2334
  // Should we attempt to resolve class names
1862
2335
  if (options.resolveClass || options.deep) {
1863
- const tmpjarNSMapping = await collectJarNS(tempCacheDir);
2336
+ const tmpjarNSMapping = await collectSbtDependencyJars(
2337
+ pkgList,
2338
+ jarNSMapping,
2339
+ );
1864
2340
  if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
1865
2341
  jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
1866
2342
  }
@@ -1884,15 +2360,10 @@ export async function createJavaBom(path, options) {
1884
2360
  }
1885
2361
  }
1886
2362
  }
1887
- if (tempCacheDir?.startsWith(getTmpDir())) {
1888
- safeRmSync(tempCacheDir, {
1889
- recursive: true,
1890
- force: true,
1891
- });
1892
- }
1893
2363
 
1894
2364
  if (
1895
2365
  millFiles?.length &&
2366
+ (options.installDeps !== false || options.introspect === true) &&
1896
2367
  isPackageManagerAllowed(
1897
2368
  "mill",
1898
2369
  ["bazel", "sbt", "gradle", "maven"],
@@ -1905,7 +2376,13 @@ export async function createJavaBom(path, options) {
1905
2376
  "maven",
1906
2377
  options,
1907
2378
  );
1908
- const millCmd = getMillCommand(millRootPath);
2379
+ const millCmd = getMillCommand(millRootPath, options);
2380
+ // Mill 1.0 replaced the ivyDepsTree task with showMvnDepsTree
2381
+ const millVersion = getMillVersion(millRootPath, millCmd);
2382
+ const millTreeTask =
2383
+ millVersion != null && gte(millVersion, "1.0.0")
2384
+ ? "showMvnDepsTree"
2385
+ : "ivyDepsTree";
1909
2386
  const millCommonArgs = [
1910
2387
  "--color",
1911
2388
  "false",
@@ -1917,7 +2394,7 @@ export async function createJavaBom(path, options) {
1917
2394
  if (!["true", "1"].includes(readEnvironmentVariable("MILL_USE_SERVER"))) {
1918
2395
  millCommonArgs.unshift("--no-server");
1919
2396
  }
1920
- const millArgs = [...millCommonArgs, "__.ivyDepsTree"];
2397
+ const millArgs = [...millCommonArgs, `__.${millTreeTask}`];
1921
2398
  if (DEBUG_MODE) {
1922
2399
  console.log("Executing", millCmd, "in", millRootPath);
1923
2400
  }
@@ -1926,6 +2403,11 @@ export async function createJavaBom(path, options) {
1926
2403
  shell: isWin,
1927
2404
  maxBuffer: MAX_BUFFER * 10,
1928
2405
  });
2406
+ noteBuildToolRateLimit("mill", commandOutputText(sresult), {
2407
+ command: `${basename(millCmd)} ${millArgs.join(" ")}`,
2408
+ exitCode:
2409
+ typeof sresult.status === "number" ? sresult.status : undefined,
2410
+ });
1929
2411
  if (sresult.status !== 0 || sresult.error) {
1930
2412
  if (options.failOnError || DEBUG_MODE) {
1931
2413
  console.error(sresult.stdout, sresult.stderr);
@@ -1935,10 +2417,14 @@ export async function createJavaBom(path, options) {
1935
2417
  tool: "mill",
1936
2418
  exitCode:
1937
2419
  typeof sresult.status === "number" ? sresult.status : undefined,
1938
- detail: "the mill ivyDepsTree command failed",
2420
+ detail: "the mill dependency tree command failed",
1939
2421
  });
1940
2422
  }
1941
- const millResolveArgs = [...millCommonArgs, "resolve", "__.ivyDepsTree"];
2423
+ const millResolveArgs = [
2424
+ ...millCommonArgs,
2425
+ "resolve",
2426
+ `__.${millTreeTask}`,
2427
+ ];
1942
2428
  if (DEBUG_MODE) {
1943
2429
  console.log(
1944
2430
  "Executing",
@@ -1948,6 +2434,9 @@ export async function createJavaBom(path, options) {
1948
2434
  millRootPath,
1949
2435
  );
1950
2436
  }
2437
+ // The resolve run only lists the build's tasks and downloads nothing,
2438
+ // so it runs even after a rate limit: the trees of the modules that did
2439
+ // resolve under --keep-going are read through it.
1951
2440
  sresult = safeSpawnSync(millCmd, millResolveArgs, {
1952
2441
  cwd: millRootPath,
1953
2442
  shell: isWin,
@@ -1970,6 +2459,8 @@ export async function createJavaBom(path, options) {
1970
2459
  const modules = sstdout
1971
2460
  .trim()
1972
2461
  .split("\n")
2462
+ // Mill 1.x prints headers around the module list
2463
+ .filter((a) => a.endsWith(`.${millTreeTask}`))
1973
2464
  .map((a) => a.substring(0, a.lastIndexOf(".")))
1974
2465
  .filter((a) =>
1975
2466
  ["true", "1"].includes(readEnvironmentVariable("MILL_EXCLUDE_TEST"))
@@ -1982,7 +2473,13 @@ export async function createJavaBom(path, options) {
1982
2473
  relations.set(parentComponent["bom-ref"], []);
1983
2474
  for (const module of modules) {
1984
2475
  moduleBomRefs.push(
1985
- parseMillDependency(module, packages, relations, millRootPath),
2476
+ parseMillDependency(
2477
+ module,
2478
+ packages,
2479
+ relations,
2480
+ millRootPath,
2481
+ `${millTreeTask}.log`,
2482
+ ),
1986
2483
  );
1987
2484
  }
1988
2485
  for (const module of moduleBomRefs) {
@@ -2035,6 +2532,34 @@ export async function createJavaBom(path, options) {
2035
2532
  }
2036
2533
  }
2037
2534
  }
2535
+ // scala-cli projects declare everything with //> using directives in
2536
+ // their sources and have none of the build files handled above.
2537
+ if (
2538
+ !pomFiles?.length &&
2539
+ !gradleFiles?.length &&
2540
+ !millFiles?.length &&
2541
+ !sbtProjects.length &&
2542
+ isPackageManagerAllowed(
2543
+ "scala-cli",
2544
+ ["bazel", "sbt", "gradle", "maven", "mill"],
2545
+ options,
2546
+ )
2547
+ ) {
2548
+ const dlist = collectScalaCliComponents(path, options);
2549
+ if (dlist?.length) {
2550
+ thoughtLog(
2551
+ `**SCALA-CLI**: Found ${dlist.length} declared dependencies.`,
2552
+ );
2553
+ pkgList = pkgList.concat(dlist);
2554
+ if (!parentComponent?.name) {
2555
+ parentComponent = createDefaultParentComponent(
2556
+ path,
2557
+ "maven",
2558
+ options,
2559
+ );
2560
+ }
2561
+ }
2562
+ }
2038
2563
  } catch (err) {
2039
2564
  if (!isDeferredFailOnError(err)) {
2040
2565
  throw err;
@@ -2044,11 +2569,47 @@ export async function createJavaBom(path, options) {
2044
2569
  thoughtLog(
2045
2570
  "Completing the java BOM with the components collected before the deferred failure.",
2046
2571
  );
2572
+ } finally {
2573
+ // A build this scan ran can have started a Mill daemon of its own
2574
+ // without --no-server; stop those even when a later step failed.
2575
+ stopMillDaemons(path, millDaemonsBefore);
2576
+ }
2577
+ // A Scala.js build bundles npm packages: scalajs-bundler installs them
2578
+ // under target, and a bundler workspace keeps its lock file beside the
2579
+ // build. Their npm components join the BOM so module imports can be
2580
+ // attributed to them, and the project depends on the workspaces' direct
2581
+ // dependencies.
2582
+ try {
2583
+ const scalajsNpm = await collectScalaJsNpmComponents(
2584
+ path,
2585
+ pkgList,
2586
+ options,
2587
+ );
2588
+ if (scalajsNpm.components.length) {
2589
+ pkgList = pkgList.concat(scalajsNpm.components);
2590
+ const npmEdges = [...scalajsNpm.dependencies];
2591
+ if (parentComponent?.["bom-ref"] && scalajsNpm.roots.length) {
2592
+ npmEdges.push({
2593
+ ref: parentComponent["bom-ref"],
2594
+ dependsOn: scalajsNpm.roots,
2595
+ });
2596
+ }
2597
+ dependencies = mergeDependencies(dependencies, npmEdges, parentComponent);
2598
+ }
2599
+ } catch (err) {
2600
+ if (DEBUG_MODE) {
2601
+ console.log("Unable to collect the Scala.js npm dependencies", err);
2602
+ }
2047
2603
  }
2048
2604
  // Deduplication and metadata belong to whatever was collected, so they run
2049
2605
  // on the deferred path too.
2050
2606
  pkgList = trimComponents(pkgList);
2051
- pkgList = await getMvnMetadata(pkgList, jarNSMapping, options.deep);
2607
+ // --deep collects namespaces; it does not opt in to remote licence lookups,
2608
+ // which FETCH_LICENSE controls. The project's own modules are never looked
2609
+ // up remotely: they are not published.
2610
+ pkgList = await getMvnMetadata(pkgList, jarNSMapping, false, {
2611
+ skipPurls: ownComponentRefs(parentComponent),
2612
+ });
2052
2613
  return buildBomNSData(options, pkgList, "maven", {
2053
2614
  src: path,
2054
2615
  nsMapping: jarNSMapping,