@cdxgen/cdxgen 13.3.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 (255) hide show
  1. package/README.md +9 -6
  2. package/bin/cdxgen.js +90 -26
  3. package/bin/evinse.js +18 -1
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +45 -15
  6. package/bin/tracebom.js +5 -1
  7. package/bin/validate.js +84 -18
  8. package/bin/verify.js +155 -86
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +61 -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/index.js +113 -54
  15. package/lib/cli/jsBom.js +50 -29
  16. package/lib/cli/jvmBom.js +665 -138
  17. package/lib/cli/managedBom.js +320 -200
  18. package/lib/cli/nativeBom.js +139 -30
  19. package/lib/core/activity.js +12 -0
  20. package/lib/core/env.js +17 -9
  21. package/lib/core/fs.js +3 -2
  22. package/lib/core/httpClient.js +159 -24
  23. package/lib/core/pluginRun.js +184 -0
  24. package/lib/core/processTree.js +100 -0
  25. package/lib/core/runState.js +39 -0
  26. package/lib/core/toml.js +72 -0
  27. package/lib/ecosystems/asarutils.js +4 -2
  28. package/lib/ecosystems/bunutils.js +5 -7
  29. package/lib/ecosystems/cargoCache.js +231 -8
  30. package/lib/ecosystems/cmakeResolver.js +10 -21
  31. package/lib/ecosystems/cppBuildContext.js +631 -0
  32. package/lib/ecosystems/cppEvidence.js +337 -22
  33. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  34. package/lib/ecosystems/denoutils.js +93 -66
  35. package/lib/ecosystems/ecosystems.js +1967 -788
  36. package/lib/ecosystems/golem.js +28 -12
  37. package/lib/ecosystems/gradleutils.js +314 -30
  38. package/lib/ecosystems/jsEvidence.js +85 -24
  39. package/lib/ecosystems/kosi.js +21 -4
  40. package/lib/ecosystems/npmutils.js +64 -0
  41. package/lib/ecosystems/parsers-dotnet.js +567 -151
  42. package/lib/ecosystems/parsers-elm.js +46 -28
  43. package/lib/ecosystems/parsers-gleam.js +3 -4
  44. package/lib/ecosystems/parsers-go.js +324 -61
  45. package/lib/ecosystems/parsers-js.js +92 -26
  46. package/lib/ecosystems/parsers-julia.js +2 -3
  47. package/lib/ecosystems/parsers-jvm.js +204 -12
  48. package/lib/ecosystems/parsers-misc.js +182 -55
  49. package/lib/ecosystems/parsers-python.js +141 -10
  50. package/lib/ecosystems/parsers-rust.js +4 -4
  51. package/lib/ecosystems/parsers-terraform.js +1573 -19
  52. package/lib/ecosystems/pythonutils.js +102 -6
  53. package/lib/ecosystems/remote/huggingface.js +3 -0
  54. package/lib/ecosystems/rubyutils.js +67 -17
  55. package/lib/ecosystems/rusi.js +22 -9
  56. package/lib/ecosystems/sbtutils.js +243 -187
  57. package/lib/ecosystems/scalacliutils.js +342 -0
  58. package/lib/ecosystems/scalasem.js +2618 -0
  59. package/lib/ecosystems/terraformRegistry.js +288 -0
  60. package/lib/ecosystems/utils.js +2 -0
  61. package/lib/ecosystems/vendoredCode.js +156 -0
  62. package/lib/evinser/evinser.js +578 -69
  63. package/lib/helpers/agenticToolutils.js +2 -2
  64. package/lib/helpers/bomSigner.js +576 -128
  65. package/lib/inventory/aiCollector.js +4 -1
  66. package/lib/inventory/analyzer.js +110 -127
  67. package/lib/inventory/atomUtils.js +171 -48
  68. package/lib/inventory/auditCategories.js +7 -0
  69. package/lib/inventory/buildToolRateLimit.js +144 -0
  70. package/lib/inventory/cacheCommands.js +2 -1
  71. package/lib/inventory/cmakeBuildDirs.js +254 -0
  72. package/lib/inventory/deps.js +501 -124
  73. package/lib/inventory/dosai.js +248 -44
  74. package/lib/inventory/dosaiParsers.js +310 -15
  75. package/lib/inventory/dynamic.js +52 -6
  76. package/lib/inventory/evidenceUtils.js +154 -109
  77. package/lib/inventory/fetchBatch.js +574 -38
  78. package/lib/inventory/fetchDiskCache.js +597 -0
  79. package/lib/inventory/forgeEnricher.js +14 -5
  80. package/lib/inventory/jvmLocalRepos.js +917 -0
  81. package/lib/inventory/mcpConfigParser.js +2 -3
  82. package/lib/inventory/parentSelection.js +146 -0
  83. package/lib/inventory/rustFormulationParser.js +3 -4
  84. package/lib/inventory/scalaCoords.js +153 -0
  85. package/lib/inventory/source.js +8 -8
  86. package/lib/inventory/spdx.js +101 -0
  87. package/lib/inventory/traceRunner.js +88 -7
  88. package/lib/inventory/tracedCrypto.js +294 -0
  89. package/lib/managers/docker.js +33 -32
  90. package/lib/managers/oci.js +24 -2
  91. package/lib/parsers/cmakeCache.js +69 -0
  92. package/lib/parsers/cmakeLists.js +16 -2
  93. package/lib/parsers/cmakePresets.js +400 -0
  94. package/lib/parsers/compileCommands.js +557 -0
  95. package/lib/parsers/strictJson.js +322 -0
  96. package/lib/parsers/terraformConfig.js +1073 -0
  97. package/lib/parsers/terraformSource.js +652 -0
  98. package/lib/server/server.js +23 -0
  99. package/lib/stages/postgen/introspection/markers.js +1 -1
  100. package/lib/stages/postgen/postgen.js +15 -1
  101. package/lib/stages/postgen/specVersionCompat.js +32 -0
  102. package/lib/validator/index.js +25 -10
  103. package/lib/validator/reporters/json.js +1 -0
  104. package/package.json +21 -21
  105. package/types/lib/audit/index.d.ts.map +1 -1
  106. package/types/lib/cli/index.d.ts.map +1 -1
  107. package/types/lib/cli/jsBom.d.ts.map +1 -1
  108. package/types/lib/cli/jvmBom.d.ts +24 -1
  109. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  110. package/types/lib/cli/managedBom.d.ts +7 -4
  111. package/types/lib/cli/managedBom.d.ts.map +1 -1
  112. package/types/lib/cli/nativeBom.d.ts +3 -1
  113. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  114. package/types/lib/core/activity.d.ts.map +1 -1
  115. package/types/lib/core/env.d.ts +9 -0
  116. package/types/lib/core/env.d.ts.map +1 -1
  117. package/types/lib/core/fs.d.ts.map +1 -1
  118. package/types/lib/core/httpClient.d.ts +13 -8
  119. package/types/lib/core/httpClient.d.ts.map +1 -1
  120. package/types/lib/core/pluginRun.d.ts +97 -0
  121. package/types/lib/core/pluginRun.d.ts.map +1 -0
  122. package/types/lib/core/processTree.d.ts +36 -0
  123. package/types/lib/core/processTree.d.ts.map +1 -0
  124. package/types/lib/core/runState.d.ts +29 -0
  125. package/types/lib/core/runState.d.ts.map +1 -0
  126. package/types/lib/core/toml.d.ts +20 -0
  127. package/types/lib/core/toml.d.ts.map +1 -0
  128. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  129. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  131. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  132. package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
  133. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  134. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  135. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  136. package/types/lib/ecosystems/cppEvidence.d.ts +18 -2
  137. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  139. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  140. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  141. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  143. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  144. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  145. package/types/lib/ecosystems/gradleutils.d.ts +50 -0
  146. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  147. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  148. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  149. package/types/lib/ecosystems/npmutils.d.ts +15 -0
  150. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  151. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  152. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  153. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  154. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  155. package/types/lib/ecosystems/parsers-go.d.ts +44 -2
  156. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  157. package/types/lib/ecosystems/parsers-js.d.ts +6 -1
  158. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  159. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  160. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  161. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  162. package/types/lib/ecosystems/parsers-misc.d.ts +3 -1
  163. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  164. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  165. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  166. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  167. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  168. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  169. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  170. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  171. package/types/lib/ecosystems/rubyutils.d.ts +21 -1
  172. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  173. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  174. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  175. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  176. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  177. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  178. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  179. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  180. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  181. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  182. package/types/lib/ecosystems/utils.d.ts +2 -2
  183. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  184. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  185. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  186. package/types/lib/evinser/evinser.d.ts +75 -8
  187. package/types/lib/evinser/evinser.d.ts.map +1 -1
  188. package/types/lib/helpers/bomSigner.d.ts +80 -4
  189. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  190. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  191. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  192. package/types/lib/inventory/atomUtils.d.ts +82 -10
  193. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  194. package/types/lib/inventory/auditCategories.d.ts +3 -0
  195. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  196. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  197. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  198. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  199. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  200. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  201. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  202. package/types/lib/inventory/deps.d.ts +59 -1
  203. package/types/lib/inventory/deps.d.ts.map +1 -1
  204. package/types/lib/inventory/dosai.d.ts +57 -8
  205. package/types/lib/inventory/dosai.d.ts.map +1 -1
  206. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  207. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  208. package/types/lib/inventory/dynamic.d.ts +10 -0
  209. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  210. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  211. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  212. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  213. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  214. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  215. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  216. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  217. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  218. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  219. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  220. package/types/lib/inventory/parentSelection.d.ts +34 -0
  221. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  222. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  223. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  224. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  225. package/types/lib/inventory/source.d.ts.map +1 -1
  226. package/types/lib/inventory/spdx.d.ts +9 -0
  227. package/types/lib/inventory/spdx.d.ts.map +1 -1
  228. package/types/lib/inventory/traceRunner.d.ts +12 -0
  229. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  230. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  231. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  232. package/types/lib/managers/docker.d.ts.map +1 -1
  233. package/types/lib/managers/oci.d.ts +3 -1
  234. package/types/lib/managers/oci.d.ts.map +1 -1
  235. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  236. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  237. package/types/lib/parsers/cmakeLists.d.ts +2 -1
  238. package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
  239. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  240. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  241. package/types/lib/parsers/compileCommands.d.ts +115 -0
  242. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  243. package/types/lib/parsers/strictJson.d.ts +46 -0
  244. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  245. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  246. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  247. package/types/lib/parsers/terraformSource.d.ts +66 -0
  248. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  249. package/types/lib/server/server.d.ts.map +1 -1
  250. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  252. package/types/lib/validator/index.d.ts +8 -0
  253. package/types/lib/validator/index.d.ts.map +1 -1
  254. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  255. 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
  *
@@ -179,6 +325,8 @@ export async function createJarBom(path, options) {
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,19 +411,28 @@ 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,
@@ -250,13 +441,16 @@ export function discoverSbtProjectsFromCmd(basePath, sbtCmd, env) {
250
441
  env,
251
442
  encoding: "utf-8",
252
443
  },
444
+ true,
253
445
  );
254
446
  if (result.status === 0 && !result.error) {
255
447
  const { projects, root } = parseSbtProjects(result.stdout || "");
256
- // Exclude the aggregating root project - its own dependencyTree is
257
- // usually a subset of the aggregated subprojects and re-resolving it
258
- // only adds noise/time.
259
- 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);
260
454
  if (subprojects.length > 0) {
261
455
  if (DEBUG_MODE) {
262
456
  console.log(
@@ -320,6 +514,8 @@ export async function createJavaBom(path, options) {
320
514
  let tools;
321
515
  let possible_misses = false;
322
516
  let parallelPropTaskOut = "";
517
+ // Mill daemons already running belong to the user and outlive the scan
518
+ const millDaemonsBefore = findMillDaemons(path);
323
519
  try {
324
520
  // war/ear mode
325
521
  if (path.endsWith(".war") || path.endsWith(".jar")) {
@@ -329,8 +525,10 @@ export async function createJavaBom(path, options) {
329
525
  console.log(`Retrieving packages from ${path}`);
330
526
  }
331
527
  const tempDir = safeMkdtempSync(join(getTmpDir(), "war-deps-"));
332
- jarNSMapping = await collectJarNS(tempDir);
333
- 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);
334
532
  if (pkgList.length) {
335
533
  pkgList = await getMvnMetadata(pkgList);
336
534
  }
@@ -456,8 +654,20 @@ export async function createJavaBom(path, options) {
456
654
  }
457
655
  const firstPom = pomFiles.length ? pomFiles[0] : undefined;
458
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();
459
661
  for (const f of pomFiles) {
460
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
+ }
461
671
  if (
462
672
  isQuarkus &&
463
673
  !options.deep &&
@@ -468,15 +678,40 @@ export async function createJavaBom(path, options) {
468
678
  }
469
679
  continue;
470
680
  }
471
- const settingsXml = join(basePath, "settings.xml");
472
- if (safeExistsSync(settingsXml)) {
473
- console.log(
474
- `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}'`,
475
- );
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;
476
696
  }
477
697
  if (mavenCmd?.endsWith("mvn")) {
478
698
  mavenCmd = getMavenCommand(basePath, path, options);
479
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
+ );
714
+ }
480
715
  // Should we attempt to resolve class names
481
716
  if (options.resolveClass || options.deep) {
482
717
  const tmpjarNSMapping = await collectMvnDependencies(
@@ -490,13 +725,18 @@ export async function createJavaBom(path, options) {
490
725
  }
491
726
  }
492
727
  // Use the cyclonedx maven plugin if there is no preference for maven deps tree
493
- if (!useMavenDepsTree) {
728
+ if (!useMavenDepsTree && !isBuildToolRateLimited("maven")) {
494
729
  thoughtLog("The user wants me to use the cyclonedx-maven plugin.");
495
730
  console.log(`Executing '${mavenCmd}' in`, basePath);
496
731
  result = safeSpawnSync(mavenCmd, mvnArgs, {
497
732
  cwd: basePath,
498
733
  shell: isWin,
499
734
  });
735
+ noteBuildToolRateLimit("maven", commandOutputText(result), {
736
+ command: `${basename(mavenCmd)} ${mvnArgs.join(" ")}`,
737
+ exitCode:
738
+ typeof result.status === "number" ? result.status : undefined,
739
+ });
500
740
  // Check if the cyclonedx plugin created the required bom.json file
501
741
  // Sometimes the plugin fails silently for complex maven projects
502
742
  bomJsonFiles = getAllFiles(
@@ -522,8 +762,20 @@ export async function createJavaBom(path, options) {
522
762
  ) {
523
763
  const tempRoot = getTmpDir();
524
764
  const tempDir = safeMkdtempSync(join(tempRoot, "cdxgen-mvn-"));
525
- const tempMvnTree = join(tempDir, "cdxgen-mvn-tree.json");
526
- 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
+ );
527
779
  const tempMvnParentTree = join(
528
780
  tempDir,
529
781
  "cdxgen-mvn-parent-tree.json",
@@ -543,14 +795,11 @@ export async function createJavaBom(path, options) {
543
795
  addArgs = parseMavenArgs(readEnvironmentVariable("MVN_ARGS"));
544
796
  mvnTreeArgs = mvnTreeArgs.concat(addArgs);
545
797
  }
546
- // Automatically use settings.xml to improve the success for fallback
547
- if (safeExistsSync(settingsXml)) {
548
- mvnTreeArgs.push("-s");
549
- mvnTreeArgs.push(settingsXml);
550
- }
798
+ mvnTreeArgs = mvnTreeArgs.concat(projectSettings.args);
551
799
  // For the first pom alone, we need to execute first in non-recursive mode to capture
552
800
  // the parent component. Then, we execute all of them in recursive mode
553
- if (f === firstPom) {
801
+ // A rate limit the --deep copy-dependencies run met ends it too.
802
+ if (f === firstPom && !isBuildToolRateLimited("maven")) {
554
803
  thoughtLog(
555
804
  "What is the parent component here? Let's use maven command to find out.",
556
805
  );
@@ -560,14 +809,19 @@ export async function createJavaBom(path, options) {
560
809
  `-DoutputFile=${tempMvnParentTree}`,
561
810
  "-DoutputType=json",
562
811
  ];
563
- if (addArgs.length) {
564
- findParentComponentArgs =
565
- findParentComponentArgs.concat(addArgs);
566
- }
812
+ findParentComponentArgs = findParentComponentArgs.concat(
813
+ addArgs,
814
+ projectSettings.args,
815
+ );
567
816
  result = safeSpawnSync(mavenCmd, findParentComponentArgs, {
568
817
  cwd: basePath,
569
818
  shell: isWin,
570
819
  });
820
+ noteBuildToolRateLimit("maven", commandOutputText(result), {
821
+ command: `${basename(mavenCmd)} ${findParentComponentArgs.join(" ")}`,
822
+ exitCode:
823
+ typeof result.status === "number" ? result.status : undefined,
824
+ });
571
825
  // If json is empty or unparseable, fallback to text parsing
572
826
  let emptyJson = !safeExistsSync(tempMvnParentTree);
573
827
  if (safeExistsSync(tempMvnParentTree)) {
@@ -583,16 +837,15 @@ export async function createJavaBom(path, options) {
583
837
  }
584
838
  }
585
839
  }
586
- if (result.status !== 0 || result.error || emptyJson) {
840
+ if (
841
+ !isBuildToolRateLimited("maven") &&
842
+ (result.status !== 0 || result.error || emptyJson)
843
+ ) {
587
844
  findParentComponentArgs = [
588
845
  "dependency:tree",
589
846
  "-N",
590
847
  `-DoutputFile=${tempMvnParentTreeText}`,
591
- ];
592
- if (addArgs.length) {
593
- findParentComponentArgs =
594
- findParentComponentArgs.concat(addArgs);
595
- }
848
+ ].concat(addArgs, projectSettings.args);
596
849
  result = safeSpawnSync(mavenCmd, findParentComponentArgs, {
597
850
  cwd: basePath,
598
851
  shell: isWin,
@@ -632,49 +885,60 @@ export async function createJavaBom(path, options) {
632
885
  `Executing '${basename(mavenCmd)} dependency:tree ...' in ${basePath}`,
633
886
  );
634
887
  }
635
- result = safeSpawnSync(mavenCmd, mvnTreeArgs, {
636
- cwd: basePath,
637
- shell: isWin,
638
- });
639
- let emptyJson = !safeExistsSync(tempMvnTree);
640
- if (safeExistsSync(tempMvnTree)) {
641
- const mvnTreeString = readFileSync(tempMvnTree, {
642
- encoding: "utf-8",
643
- });
644
- const parsedList = parseMavenTreeJson(mvnTreeString, f);
645
- if (!parsedList?.pkgList?.length) {
646
- emptyJson = true;
647
- // Remove the invalid json file so text output is preferred below.
648
- if (safeExistsSync(tempMvnTree)) {
649
- safeUnlinkSync(tempMvnTree);
650
- }
651
- }
652
- }
653
- if (result.status !== 0 || result.error || emptyJson) {
654
- mvnTreeArgs = [
655
- "dependency:tree",
656
- `-DoutputFile=${tempMvnTreeText}`,
657
- ];
658
- if (addArgs.length) {
659
- mvnTreeArgs = mvnTreeArgs.concat(addArgs);
660
- }
661
- if (safeExistsSync(settingsXml)) {
662
- mvnTreeArgs.push("-s");
663
- mvnTreeArgs.push(settingsXml);
664
- }
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 {
665
893
  result = safeSpawnSync(mavenCmd, mvnTreeArgs, {
666
894
  cwd: basePath,
667
895
  shell: isWin,
668
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
+ }
669
934
  }
670
935
  if (result.status !== 0 || result.error) {
671
936
  possible_misses = true;
672
937
  // Our approach to recursively invoking the maven plugin for each sub-module is bound to result in failures
673
938
  // These could be due to a range of reasons that are covered below.
674
939
  if (
675
- pomFiles.length === 1 ||
676
- DEBUG_MODE ||
677
- PREFER_MAVEN_DEPS_TREE
940
+ !isBuildToolRateLimited("maven") &&
941
+ (pomFiles.length === 1 || DEBUG_MODE || PREFER_MAVEN_DEPS_TREE)
678
942
  ) {
679
943
  if (result.stdout) {
680
944
  console.log(result.stdout);
@@ -773,16 +1037,18 @@ export async function createJavaBom(path, options) {
773
1037
  pkgList = pkgList.concat(dlist);
774
1038
  }
775
1039
  } else {
776
- const treeFile = safeExistsSync(tempMvnTreeText)
777
- ? tempMvnTreeText
778
- : tempMvnTree;
779
- if (safeExistsSync(treeFile)) {
780
- const mvnTreeString = readFileSync(treeFile, {
781
- encoding: "utf-8",
782
- });
783
- const parsedList = treeFile.endsWith(".json")
784
- ? parseMavenTreeJson(mvnTreeString, f)
785
- : 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);
786
1052
  const dlist = parsedList.pkgList || [];
787
1053
  const tmpParentComponent = dlist.splice(0, 1)[0];
788
1054
  if (tmpParentComponent) {
@@ -1219,6 +1485,14 @@ export async function createJavaBom(path, options) {
1219
1485
  );
1220
1486
  const allOutputs = [];
1221
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
+ }
1222
1496
  if (DEBUG_MODE) {
1223
1497
  console.log(
1224
1498
  `Executing ${gradleCmd} with arguments ${gradleArg.join(" ").substring(0, 150)}... in ${gradleRootPath}`,
@@ -1234,6 +1508,13 @@ export async function createJavaBom(path, options) {
1234
1508
  cwd: gradleRootPath,
1235
1509
  shell: isWin,
1236
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
+ });
1237
1518
  if (sresult.status !== 0 || sresult.error) {
1238
1519
  if (options.failOnError || DEBUG_MODE) {
1239
1520
  console.error(sresult.stdout, sresult.stderr);
@@ -1644,8 +1925,6 @@ export async function createJavaBom(path, options) {
1644
1925
  `${options.multiProject ? "**/" : ""}build.sbt.lock`,
1645
1926
  options,
1646
1927
  );
1647
- const tempCacheDir = safeMkdtempSync(join(getTmpDir(), "sbt-cache-"));
1648
- safeMkdirSync(tempCacheDir, { recursive: true });
1649
1928
  if (
1650
1929
  sbtProjects?.length &&
1651
1930
  isPackageManagerAllowed(
@@ -1711,20 +1990,46 @@ export async function createJavaBom(path, options) {
1711
1990
  }
1712
1991
  }
1713
1992
  safeWriteSync(tempSbtPlugins, sbtPluginDefinition);
1714
- 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.
1715
1996
  const env = { ...process.env };
1716
- // We need to collect the jars from the cache
1717
- if (options.deep) {
1718
- sbtExtraArgs = " updateClassifiers";
1719
- env["COURSIER_CACHE"] = tempCacheDir;
1720
- env["SBT_IVY_HOME"] = tempCacheDir;
1721
- }
1722
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
+ }
1723
2008
  const basePath = sbtProjects[i];
1724
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);
1725
2027
  let sbtArgs = [];
1726
2028
  let pluginFile = null;
2029
+ let pluginAdded = false;
1727
2030
  const subDlFiles = [];
2031
+ const failedSubprojects = [];
2032
+ let subprojects = [];
1728
2033
  if (standalonePluginFile) {
1729
2034
  sbtArgs = [
1730
2035
  `-addPluginSbtFile=${tempSbtPlugins}`,
@@ -1738,15 +2043,23 @@ export async function createJavaBom(path, options) {
1738
2043
  // `dependencyTree` fan-out yields a far more complete tree than a
1739
2044
  // single root resolution (which only captures the root project's own
1740
2045
  // libraryDependencies). See https://github.com/cdxgen/cdxgen/issues/4291
1741
- const subprojects = discoverSbtProjectsFromCmd(
2046
+ subprojects = discoverSbtProjectsFromCmd(
1742
2047
  basePath,
1743
2048
  SBT_CMD,
1744
2049
  env,
2050
+ sbtLauncherArgs,
1745
2051
  );
1746
- if (subprojects.length > 0 && useSlashSyntax) {
1747
- sbtArgs = [
1748
- `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs}`,
1749
- ];
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'"];
1750
2063
  for (const sp of subprojects) {
1751
2064
  const subDlFile = join(tempDir, `dl-${i}-${sp}.tmp`);
1752
2065
  subDlFiles.push(subDlFile);
@@ -1756,21 +2069,27 @@ export async function createJavaBom(path, options) {
1756
2069
  }
1757
2070
  } else if (useSlashSyntax) {
1758
2071
  sbtArgs = [
1759
- `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs} "dependencyTree / toFile ${dlFile} --force"`,
2072
+ `'set ThisBuild / asciiGraphWidth := 800' "dependencyTree / toFile ${dlFile} --force"`,
1760
2073
  ];
1761
2074
  subDlFiles.push(dlFile);
1762
2075
  } else {
1763
2076
  sbtArgs = [
1764
- `'set asciiGraphWidth in ThisBuild := 800'${sbtExtraArgs} "dependencyTree::toFile ${dlFile} --force"`,
2077
+ `'set asciiGraphWidth in ThisBuild := 800' "dependencyTree::toFile ${dlFile} --force"`,
1765
2078
  ];
1766
2079
  subDlFiles.push(dlFile);
1767
2080
  }
1768
- 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
+ }
1769
2088
  }
1770
2089
  // Run sbt in non-interactive batch mode so it never blocks waiting for
1771
2090
  // input on stdin (which can otherwise appear as an indefinite hang) and
1772
2091
  // does not emit ANSI colour codes into the dependency tree output.
1773
- sbtArgs = ["-batch", "-no-colors", ...sbtArgs];
2092
+ sbtArgs = [...sbtLauncherArgs, "-batch", "-no-colors", ...sbtArgs];
1774
2093
  console.log(
1775
2094
  "Executing",
1776
2095
  SBT_CMD,
@@ -1781,16 +2100,31 @@ export async function createJavaBom(path, options) {
1781
2100
  tempSbtgDir,
1782
2101
  );
1783
2102
  // Note that the command has to be invoked with `shell: true` to properly execut sbt
1784
- const result = safeSpawnSync(SBT_CMD, sbtArgs, {
1785
- cwd: basePath,
1786
- shell: true,
1787
- // The sbt commands below embed their own shell quoting, which the
1788
- // shell must strip for sbt to see separate commands.
1789
- cdxgenRawShellArgs: true,
1790
- 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,
1791
2123
  });
1792
2124
  if (result.status !== 0 || result.error) {
1793
- console.error(result.stdout, result.stderr);
2125
+ if (!isBuildToolRateLimited("sbt")) {
2126
+ console.error(result.stdout, result.stderr);
2127
+ }
1794
2128
  recordDegradation("jvm.sbt.no-lockfile", {
1795
2129
  ecosystem: "java",
1796
2130
  tool: "sbt",
@@ -1801,15 +2135,20 @@ export async function createJavaBom(path, options) {
1801
2135
  detail:
1802
2136
  "The sbt dependencyTree command did not succeed, so the declared sbt dependencies are the only evidence available.",
1803
2137
  });
1804
- console.log(
1805
- "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.",
1806
- );
1807
- console.log(
1808
- "2. Check if the plugin net.virtual-void:sbt-dependency-graph 0.10.0-RC1 can be used in the environment",
1809
- );
1810
- console.log(
1811
- "3. Consider creating a lockfile using sbt-dependency-lock plugin. See https://github.com/stringbean/sbt-dependency-lock",
1812
- );
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
+ }
1813
2152
  deferFailOnError(options, {
1814
2153
  ecosystem: "java",
1815
2154
  tool: "sbt",
@@ -1819,13 +2158,95 @@ export async function createJavaBom(path, options) {
1819
2158
  detail: "the sbt dependencyTree command failed",
1820
2159
  });
1821
2160
  }
1822
- 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) {
1823
2235
  cleanupPlugin(basePath, pluginFile);
1824
2236
  }
1825
2237
  const sbtSubprojectRoots = [];
1826
2238
  for (const subDlFile of subDlFiles) {
1827
2239
  if (safeExistsSync(subDlFile)) {
1828
- 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);
1829
2250
  if (retMap.pkgList?.length) {
1830
2251
  const tmpParentComponent = retMap.pkgList.splice(0, 1)[0];
1831
2252
  tmpParentComponent.type = "application";
@@ -1873,19 +2294,38 @@ export async function createJavaBom(path, options) {
1873
2294
  "bom-ref": decodeURIComponent(rootPurl),
1874
2295
  };
1875
2296
  parentComponent = rootComponent;
1876
- dependencies.push({
1877
- ref: decodeURIComponent(rootPurl),
1878
- dependsOn: sbtSubprojectRoots
1879
- .map((c) => c["bom-ref"])
1880
- .filter(Boolean),
1881
- });
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
+ );
1882
2313
  } else if (sbtSubprojectRoots.length > 0 && !parentComponent) {
1883
2314
  parentComponent = sbtSubprojectRoots[0];
1884
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
+ }
1885
2323
  }
1886
2324
 
1887
- // Cleanup
1888
- 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 });
1889
2329
  } // else
1890
2330
 
1891
2331
  if (DEBUG_MODE) {
@@ -1893,7 +2333,10 @@ export async function createJavaBom(path, options) {
1893
2333
  }
1894
2334
  // Should we attempt to resolve class names
1895
2335
  if (options.resolveClass || options.deep) {
1896
- const tmpjarNSMapping = await collectJarNS(tempCacheDir);
2336
+ const tmpjarNSMapping = await collectSbtDependencyJars(
2337
+ pkgList,
2338
+ jarNSMapping,
2339
+ );
1897
2340
  if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
1898
2341
  jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
1899
2342
  }
@@ -1917,12 +2360,6 @@ export async function createJavaBom(path, options) {
1917
2360
  }
1918
2361
  }
1919
2362
  }
1920
- if (tempCacheDir?.startsWith(getTmpDir())) {
1921
- safeRmSync(tempCacheDir, {
1922
- recursive: true,
1923
- force: true,
1924
- });
1925
- }
1926
2363
 
1927
2364
  if (
1928
2365
  millFiles?.length &&
@@ -1940,6 +2377,12 @@ export async function createJavaBom(path, options) {
1940
2377
  options,
1941
2378
  );
1942
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";
1943
2386
  const millCommonArgs = [
1944
2387
  "--color",
1945
2388
  "false",
@@ -1951,7 +2394,7 @@ export async function createJavaBom(path, options) {
1951
2394
  if (!["true", "1"].includes(readEnvironmentVariable("MILL_USE_SERVER"))) {
1952
2395
  millCommonArgs.unshift("--no-server");
1953
2396
  }
1954
- const millArgs = [...millCommonArgs, "__.ivyDepsTree"];
2397
+ const millArgs = [...millCommonArgs, `__.${millTreeTask}`];
1955
2398
  if (DEBUG_MODE) {
1956
2399
  console.log("Executing", millCmd, "in", millRootPath);
1957
2400
  }
@@ -1960,6 +2403,11 @@ export async function createJavaBom(path, options) {
1960
2403
  shell: isWin,
1961
2404
  maxBuffer: MAX_BUFFER * 10,
1962
2405
  });
2406
+ noteBuildToolRateLimit("mill", commandOutputText(sresult), {
2407
+ command: `${basename(millCmd)} ${millArgs.join(" ")}`,
2408
+ exitCode:
2409
+ typeof sresult.status === "number" ? sresult.status : undefined,
2410
+ });
1963
2411
  if (sresult.status !== 0 || sresult.error) {
1964
2412
  if (options.failOnError || DEBUG_MODE) {
1965
2413
  console.error(sresult.stdout, sresult.stderr);
@@ -1969,10 +2417,14 @@ export async function createJavaBom(path, options) {
1969
2417
  tool: "mill",
1970
2418
  exitCode:
1971
2419
  typeof sresult.status === "number" ? sresult.status : undefined,
1972
- detail: "the mill ivyDepsTree command failed",
2420
+ detail: "the mill dependency tree command failed",
1973
2421
  });
1974
2422
  }
1975
- const millResolveArgs = [...millCommonArgs, "resolve", "__.ivyDepsTree"];
2423
+ const millResolveArgs = [
2424
+ ...millCommonArgs,
2425
+ "resolve",
2426
+ `__.${millTreeTask}`,
2427
+ ];
1976
2428
  if (DEBUG_MODE) {
1977
2429
  console.log(
1978
2430
  "Executing",
@@ -1982,6 +2434,9 @@ export async function createJavaBom(path, options) {
1982
2434
  millRootPath,
1983
2435
  );
1984
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.
1985
2440
  sresult = safeSpawnSync(millCmd, millResolveArgs, {
1986
2441
  cwd: millRootPath,
1987
2442
  shell: isWin,
@@ -2004,6 +2459,8 @@ export async function createJavaBom(path, options) {
2004
2459
  const modules = sstdout
2005
2460
  .trim()
2006
2461
  .split("\n")
2462
+ // Mill 1.x prints headers around the module list
2463
+ .filter((a) => a.endsWith(`.${millTreeTask}`))
2007
2464
  .map((a) => a.substring(0, a.lastIndexOf(".")))
2008
2465
  .filter((a) =>
2009
2466
  ["true", "1"].includes(readEnvironmentVariable("MILL_EXCLUDE_TEST"))
@@ -2016,7 +2473,13 @@ export async function createJavaBom(path, options) {
2016
2473
  relations.set(parentComponent["bom-ref"], []);
2017
2474
  for (const module of modules) {
2018
2475
  moduleBomRefs.push(
2019
- parseMillDependency(module, packages, relations, millRootPath),
2476
+ parseMillDependency(
2477
+ module,
2478
+ packages,
2479
+ relations,
2480
+ millRootPath,
2481
+ `${millTreeTask}.log`,
2482
+ ),
2020
2483
  );
2021
2484
  }
2022
2485
  for (const module of moduleBomRefs) {
@@ -2069,6 +2532,34 @@ export async function createJavaBom(path, options) {
2069
2532
  }
2070
2533
  }
2071
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
+ }
2072
2563
  } catch (err) {
2073
2564
  if (!isDeferredFailOnError(err)) {
2074
2565
  throw err;
@@ -2078,11 +2569,47 @@ export async function createJavaBom(path, options) {
2078
2569
  thoughtLog(
2079
2570
  "Completing the java BOM with the components collected before the deferred failure.",
2080
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
+ }
2081
2603
  }
2082
2604
  // Deduplication and metadata belong to whatever was collected, so they run
2083
2605
  // on the deferred path too.
2084
2606
  pkgList = trimComponents(pkgList);
2085
- 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
+ });
2086
2613
  return buildBomNSData(options, pkgList, "maven", {
2087
2614
  src: path,
2088
2615
  nsMapping: jarNSMapping,