@cdxgen/cdxgen 13.0.1 → 13.2.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 (286) hide show
  1. package/README.md +30 -16
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +141 -7
  4. package/bin/evinse.js +3 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/crypto-oid.json +88 -12
  7. package/data/frameworks-list.json +86 -1
  8. package/data/remediations.json +1167 -0
  9. package/data/rules/ai-bom.yaml +78 -0
  10. package/data/rules/asar-archives.yaml +1 -1
  11. package/data/rules/build-fidelity.yaml +782 -0
  12. package/data/rules/cargo-rust.yaml +132 -0
  13. package/data/rules/mcp-servers.yaml +122 -0
  14. package/data/rules/package-integrity.yaml +13 -13
  15. package/lib/audit/index.js +48 -4
  16. package/lib/audit/scoring.js +2 -8
  17. package/lib/audit/targets.js +6 -1
  18. package/lib/cli/bomAssembly.js +32 -1
  19. package/lib/cli/cliOptions.js +94 -7
  20. package/lib/cli/index.js +268 -49
  21. package/lib/cli/jsBom.js +91 -5
  22. package/lib/cli/jvmBom.js +1515 -1314
  23. package/lib/cli/managedBom.js +249 -28
  24. package/lib/cli/nativeBom.js +986 -10
  25. package/lib/core/activity.js +120 -2
  26. package/lib/core/buildLedger.js +799 -0
  27. package/lib/core/deferredExit.js +141 -0
  28. package/lib/core/env.js +108 -2
  29. package/lib/core/fs.js +60 -0
  30. package/lib/core/logger.js +12 -99
  31. package/lib/core/severity.js +16 -0
  32. package/lib/core/syncFileWriter.js +106 -0
  33. package/lib/core/userAgent.js +151 -0
  34. package/lib/ecosystems/cargoCache.js +364 -0
  35. package/lib/ecosystems/caxa.js +13 -5
  36. package/lib/ecosystems/cppEvidence.js +40 -2
  37. package/lib/ecosystems/ecosystems.js +250 -91
  38. package/lib/ecosystems/golem.js +11 -1
  39. package/lib/ecosystems/gradleutils.js +498 -17
  40. package/lib/ecosystems/kosi.js +1256 -0
  41. package/lib/ecosystems/parsers-crystal.js +179 -0
  42. package/lib/ecosystems/parsers-dotnet.js +69 -39
  43. package/lib/ecosystems/parsers-elm.js +816 -0
  44. package/lib/ecosystems/parsers-gleam.js +1 -1
  45. package/lib/ecosystems/parsers-js.js +26 -0
  46. package/lib/ecosystems/parsers-julia.js +268 -0
  47. package/lib/ecosystems/parsers-jvm.js +9 -0
  48. package/lib/ecosystems/parsers-lua.js +218 -0
  49. package/lib/ecosystems/parsers-misc.js +188 -2
  50. package/lib/ecosystems/parsers-nim.js +221 -0
  51. package/lib/ecosystems/parsers-opam.js +180 -0
  52. package/lib/ecosystems/parsers-perl.js +277 -0
  53. package/lib/ecosystems/parsers-python.js +172 -36
  54. package/lib/ecosystems/parsers-r.js +148 -0
  55. package/lib/ecosystems/parsers-rebar.js +411 -0
  56. package/lib/ecosystems/parsers-rust.js +944 -19
  57. package/lib/ecosystems/parsers-spack.js +204 -0
  58. package/lib/ecosystems/parsers-stack.js +264 -0
  59. package/lib/ecosystems/parsers-terraform.js +194 -0
  60. package/lib/ecosystems/pypiClassifiers.js +326 -0
  61. package/lib/ecosystems/pythonutils.js +49 -1
  62. package/lib/ecosystems/rubyutils.js +13 -0
  63. package/lib/ecosystems/rusi.js +126 -6
  64. package/lib/evinser/evinser.js +141 -13
  65. package/lib/helpers/agenticToolutils.js +925 -0
  66. package/lib/helpers/remote/dependency-track.js +81 -14
  67. package/lib/inventory/agentFormulationParser.js +57 -4
  68. package/lib/inventory/aiCollector.js +504 -10
  69. package/lib/inventory/aiInventory.js +12 -0
  70. package/lib/inventory/analyzer.js +488 -30
  71. package/lib/inventory/astWorker.js +6 -0
  72. package/lib/inventory/auditCategories.js +17 -0
  73. package/lib/inventory/cbomutils.js +30 -0
  74. package/lib/inventory/cdxrs.js +7 -2
  75. package/lib/inventory/communityAiConfigParser.js +215 -2
  76. package/lib/inventory/depsUtils.js +72 -0
  77. package/lib/inventory/dosai.js +508 -22
  78. package/lib/inventory/envcontext.js +215 -24
  79. package/lib/inventory/evidenceUtils.js +13 -0
  80. package/lib/inventory/fetchBatch.js +131 -2
  81. package/lib/inventory/fetchRate.js +74 -11
  82. package/lib/inventory/forgeEnricher.js +3 -2
  83. package/lib/inventory/jvmToolEnv.js +594 -0
  84. package/lib/inventory/mcpConfigParser.js +166 -2
  85. package/lib/inventory/mcpDiscovery.js +11 -0
  86. package/lib/inventory/mcpServerManifest.js +389 -0
  87. package/lib/inventory/mcpbBundle.js +419 -0
  88. package/lib/inventory/osinfo.js +1 -0
  89. package/lib/inventory/plugins.js +3 -0
  90. package/lib/inventory/protobom.js +54 -88
  91. package/lib/inventory/protobomLoader.js +4 -0
  92. package/lib/inventory/rustFormulationParser.js +1 -1
  93. package/lib/inventory/source.js +10 -4
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/toolRequirements.js +903 -0
  97. package/lib/managers/binary.js +24 -0
  98. package/lib/managers/docker.js +26 -5
  99. package/lib/parsers/huggingfaceUtils.js +15 -7
  100. package/lib/parsers/onnx.js +294 -0
  101. package/lib/parsers/safetensors.js +114 -0
  102. package/lib/server/server.js +1 -1
  103. package/lib/stages/postgen/annotationBuilder.js +97 -0
  104. package/lib/stages/postgen/auditBom.js +76 -79
  105. package/lib/stages/postgen/introspection/annotate.js +320 -0
  106. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  107. package/lib/stages/postgen/introspection/emit.js +192 -0
  108. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  109. package/lib/stages/postgen/introspection/markers.js +250 -0
  110. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  111. package/lib/stages/postgen/introspection/report.js +1208 -0
  112. package/lib/stages/postgen/introspection/score.js +1067 -0
  113. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  114. package/lib/stages/postgen/parentEdge.js +156 -0
  115. package/lib/stages/postgen/postgen.js +284 -3
  116. package/lib/stages/postgen/ruleEngine.js +17 -0
  117. package/lib/stages/pregen/envAudit.js +42 -2
  118. package/lib/stages/pregen/pregen.js +1171 -6
  119. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  120. package/lib/validator/bomValidator.js +11 -2
  121. package/package.json +26 -26
  122. package/types/lib/audit/index.d.ts.map +1 -1
  123. package/types/lib/audit/scoring.d.ts +2 -8
  124. package/types/lib/audit/scoring.d.ts.map +1 -1
  125. package/types/lib/audit/targets.d.ts.map +1 -1
  126. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  127. package/types/lib/cli/cliOptions.d.ts +2 -1
  128. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  129. package/types/lib/cli/index.d.ts +10 -2
  130. package/types/lib/cli/index.d.ts.map +1 -1
  131. package/types/lib/cli/jsBom.d.ts +23 -0
  132. package/types/lib/cli/jsBom.d.ts.map +1 -1
  133. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  134. package/types/lib/cli/managedBom.d.ts +33 -0
  135. package/types/lib/cli/managedBom.d.ts.map +1 -1
  136. package/types/lib/cli/nativeBom.d.ts +88 -8
  137. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  138. package/types/lib/core/activity.d.ts.map +1 -1
  139. package/types/lib/core/buildLedger.d.ts +272 -0
  140. package/types/lib/core/buildLedger.d.ts.map +1 -0
  141. package/types/lib/core/deferredExit.d.ts +84 -0
  142. package/types/lib/core/deferredExit.d.ts.map +1 -0
  143. package/types/lib/core/env.d.ts +43 -1
  144. package/types/lib/core/env.d.ts.map +1 -1
  145. package/types/lib/core/fs.d.ts +21 -0
  146. package/types/lib/core/fs.d.ts.map +1 -1
  147. package/types/lib/core/logger.d.ts +2 -11
  148. package/types/lib/core/logger.d.ts.map +1 -1
  149. package/types/lib/core/severity.d.ts +11 -0
  150. package/types/lib/core/severity.d.ts.map +1 -0
  151. package/types/lib/core/syncFileWriter.d.ts +33 -0
  152. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  153. package/types/lib/core/userAgent.d.ts +56 -0
  154. package/types/lib/core/userAgent.d.ts.map +1 -0
  155. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  156. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  157. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  159. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  160. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  161. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  162. package/types/lib/ecosystems/gradleutils.d.ts +53 -0
  163. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  164. package/types/lib/ecosystems/kosi.d.ts +73 -0
  165. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  166. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  167. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  168. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  169. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  170. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  171. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  172. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  173. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  174. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  176. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  177. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  178. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  179. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  180. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  181. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  182. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  183. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  184. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  185. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  188. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  189. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  190. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  191. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  192. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  193. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  194. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  195. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  196. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  197. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  198. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  199. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  200. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  201. package/types/lib/ecosystems/pythonutils.d.ts +21 -1
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  205. package/types/lib/evinser/evinser.d.ts +34 -1
  206. package/types/lib/evinser/evinser.d.ts.map +1 -1
  207. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  208. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  209. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  210. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  211. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  212. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  213. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  214. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +12 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  219. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  220. package/types/lib/inventory/dosai.d.ts +43 -0
  221. package/types/lib/inventory/dosai.d.ts.map +1 -1
  222. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  223. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  224. package/types/lib/inventory/fetchBatch.d.ts +43 -0
  225. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  226. package/types/lib/inventory/fetchRate.d.ts +9 -6
  227. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  228. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  229. package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
  230. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  231. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  232. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  233. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  234. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  235. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  236. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  237. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  238. package/types/lib/inventory/plugins.d.ts.map +1 -1
  239. package/types/lib/inventory/protobom.d.ts +18 -13
  240. package/types/lib/inventory/protobom.d.ts.map +1 -1
  241. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  242. package/types/lib/inventory/source.d.ts.map +1 -1
  243. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  244. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  245. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  246. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  247. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  248. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  249. package/types/lib/managers/binary.d.ts.map +1 -1
  250. package/types/lib/managers/docker.d.ts.map +1 -1
  251. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  252. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  253. package/types/lib/parsers/onnx.d.ts +28 -0
  254. package/types/lib/parsers/onnx.d.ts.map +1 -0
  255. package/types/lib/parsers/safetensors.d.ts +22 -0
  256. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  257. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  258. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  259. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  260. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  261. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  262. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  263. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  264. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  265. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  266. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  267. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  268. package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
  269. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  270. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  271. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  272. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  273. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  275. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  276. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  277. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  278. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  279. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  280. package/types/lib/stages/postgen/postgen.d.ts +35 -2
  281. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  282. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  283. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  284. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  285. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  286. package/types/lib/validator/bomValidator.d.ts.map +1 -1
package/lib/cli/jvmBom.js CHANGED
@@ -8,6 +8,11 @@ import { build } from "@cdxgen/cdx-purl";
8
8
  import { gte, lte } from "semver";
9
9
 
10
10
  import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
11
+ import { commandOutputText, recordDegradation } from "../core/buildLedger.js";
12
+ import {
13
+ deferFailOnError,
14
+ isDeferredFailOnError,
15
+ } from "../core/deferredExit.js";
11
16
  import {
12
17
  hasAnyProjectType,
13
18
  includeMavenTestScope,
@@ -39,10 +44,13 @@ import {
39
44
  getGradleCommand,
40
45
  getMavenCommand,
41
46
  getMillCommand,
47
+ hasGradleInvocationFailure,
42
48
  parseGradleDep,
43
49
  parseGradleInfoLogsForUrls,
44
50
  parseGradleProperties,
45
51
  parseGradleResolvedDistributions,
52
+ parseGradleVersionCatalog,
53
+ recordGradleInvocationFailure,
46
54
  splitOutputByGradleProjects,
47
55
  } from "../ecosystems/gradleutils.js";
48
56
  import {
@@ -77,6 +85,7 @@ import {
77
85
  attachIdentityTools,
78
86
  extractToolRefs,
79
87
  } from "../inventory/evidenceUtils.js";
88
+ import { readGradleWrapperVersion } from "../inventory/jvmToolEnv.js";
80
89
  import { applyPurl, mavenPurl } from "../inventory/purl.js";
81
90
  import {
82
91
  buildBomNSData,
@@ -263,7 +272,25 @@ export function discoverSbtProjectsFromCmd(basePath, sbtCmd, env) {
263
272
  "sbt projects command did not succeed. Falling back to heuristic project discovery.",
264
273
  );
265
274
  }
275
+ // Reached only when the sbt projects command did not yield usable
276
+ // subprojects; both success paths above returned early.
277
+ recordDegradation("jvm.sbt.project-discovery", {
278
+ ecosystem: "java",
279
+ tool: "sbt",
280
+ impact: "components",
281
+ command: `${basename(sbtCmd)} -batch -no-colors "projects"`,
282
+ detail:
283
+ "The sbt projects command did not yield usable project ids, so project discovery falls back to the build-file heuristic.",
284
+ });
266
285
  } catch (err) {
286
+ recordDegradation("jvm.sbt.project-discovery", {
287
+ ecosystem: "java",
288
+ tool: "sbt",
289
+ kind: "command.failed",
290
+ impact: "components",
291
+ command: `${basename(sbtCmd)} -batch -no-colors "projects"`,
292
+ detail: "The sbt projects command could not be executed.",
293
+ });
267
294
  if (DEBUG_MODE) {
268
295
  console.log("Unable to run sbt projects command.", err);
269
296
  }
@@ -291,269 +318,245 @@ export async function createJavaBom(path, options) {
291
318
  let tools;
292
319
  let possible_misses = false;
293
320
  let parallelPropTaskOut = "";
294
- // war/ear mode
295
- if (path.endsWith(".war") || path.endsWith(".jar")) {
296
- // Check if the file exists
297
- if (safeExistsSync(path)) {
298
- if (DEBUG_MODE) {
299
- console.log(`Retrieving packages from ${path}`);
300
- }
301
- const tempDir = safeMkdtempSync(join(getTmpDir(), "war-deps-"));
302
- jarNSMapping = await collectJarNS(tempDir);
303
- pkgList = await extractJarArchive(path, tempDir, jarNSMapping);
304
- if (pkgList.length) {
305
- pkgList = await getMvnMetadata(pkgList);
306
- }
307
- // Clean up
308
- if (tempDir?.startsWith(getTmpDir())) {
309
- console.log(`Cleaning up ${tempDir}`);
310
- safeRmSync(tempDir, { recursive: true, force: true });
311
- }
312
- } else {
313
- console.log(`${path} doesn't exist`);
314
- }
315
- return buildBomNSData(options, pkgList, "maven", {
316
- src: dirname(path),
317
- filename: path,
318
- nsMapping: jarNSMapping,
319
- dependencies,
320
- parentComponent,
321
- });
322
- }
323
- // -t quarkus is supported
324
- let isQuarkus = options?.projectType?.includes("quarkus");
325
- let useMavenDepsTree = isQuarkus ? false : PREFER_MAVEN_DEPS_TREE;
326
- // Is this a multi-module project
327
- let rootModules;
328
- // maven - pom.xml
329
- const pomFiles = getAllFiles(
330
- path,
331
- `${options.multiProject ? "**/" : ""}pom.xml`,
332
- options,
333
- );
334
- // gradle
335
- const gradleFiles = getAllFiles(
336
- path,
337
- `${options.multiProject ? "**/" : ""}build.gradle*`,
338
- options,
339
- );
340
- // mill
341
- const millFiles = getAllFiles(
342
- path,
343
- `${options.multiProject ? "**/" : ""}build.mill`,
344
- options,
345
- );
346
- let bomJsonFiles = [];
347
- if (
348
- pomFiles?.length &&
349
- isPackageManagerAllowed(
350
- "maven",
351
- ["bazel", "sbt", "gradle", "mill"],
352
- options,
353
- )
354
- ) {
355
- if (gradleFiles.length) {
356
- thoughtLog(
357
- `Is this a Gradle project? I recommend invoking cdxgen with the "-t gradle" option if you're encountering build errors.`,
358
- );
359
- }
360
- if (!isQuarkus) {
361
- // Quarkus projects require special treatment. To detect quarkus, we parse the first 3 maven file to look for a hit
362
- for (const pf of pomFiles.slice(0, 3)) {
363
- const pomMap = parsePom(pf);
364
- if (!rootModules && pomMap?.modules?.length) {
365
- rootModules = pomMap.modules;
321
+ try {
322
+ // war/ear mode
323
+ if (path.endsWith(".war") || path.endsWith(".jar")) {
324
+ // Check if the file exists
325
+ if (safeExistsSync(path)) {
326
+ if (DEBUG_MODE) {
327
+ console.log(`Retrieving packages from ${path}`);
328
+ }
329
+ const tempDir = safeMkdtempSync(join(getTmpDir(), "war-deps-"));
330
+ jarNSMapping = await collectJarNS(tempDir);
331
+ pkgList = await extractJarArchive(path, tempDir, jarNSMapping);
332
+ if (pkgList.length) {
333
+ pkgList = await getMvnMetadata(pkgList);
366
334
  }
367
- // In quarkus mode, we cannot use the maven deps tree
368
- if (pomMap.isQuarkus) {
369
- isQuarkus = true;
370
- useMavenDepsTree = false;
371
- break;
335
+ // Clean up
336
+ if (tempDir?.startsWith(getTmpDir())) {
337
+ console.log(`Cleaning up ${tempDir}`);
338
+ safeRmSync(tempDir, { recursive: true, force: true });
372
339
  }
340
+ } else {
341
+ console.log(`${path} doesn't exist`);
373
342
  }
343
+ return buildBomNSData(options, pkgList, "maven", {
344
+ src: dirname(path),
345
+ filename: path,
346
+ nsMapping: jarNSMapping,
347
+ dependencies,
348
+ parentComponent,
349
+ });
374
350
  }
375
- let result;
376
- let mvnArgs;
377
- // FIXME: How do we motivate everyone to upgrade to 1.7?
378
- const toolsSpecVersion = 1.6;
379
- if (isQuarkus) {
380
- thoughtLog(
381
- "This appears to be a Quarkus project. Let's use the right Maven plugin.",
382
- );
383
- // disable analytics. See: https://quarkus.io/usage/
384
- mvnArgs = [
385
- "-fn",
386
- "quarkus:dependency-sbom",
387
- "-Dquarkus.analytics.disabled=true",
388
- ];
389
- if (options.specVersion >= 1.6) {
390
- mvnArgs = mvnArgs.concat(
391
- `-Dquarkus.dependency.sbom.schema-version=${toolsSpecVersion}`,
351
+ // -t quarkus is supported
352
+ let isQuarkus = options?.projectType?.includes("quarkus");
353
+ let useMavenDepsTree = isQuarkus ? false : PREFER_MAVEN_DEPS_TREE;
354
+ // Is this a multi-module project
355
+ let rootModules;
356
+ // maven - pom.xml
357
+ const pomFiles = getAllFiles(
358
+ path,
359
+ `${options.multiProject ? "**/" : ""}pom.xml`,
360
+ options,
361
+ );
362
+ // gradle
363
+ const gradleFiles = getAllFiles(
364
+ path,
365
+ `${options.multiProject ? "**/" : ""}build.gradle*`,
366
+ options,
367
+ );
368
+ // mill
369
+ const millFiles = getAllFiles(
370
+ path,
371
+ `${options.multiProject ? "**/" : ""}build.mill`,
372
+ options,
373
+ );
374
+ let bomJsonFiles = [];
375
+ if (
376
+ pomFiles?.length &&
377
+ isPackageManagerAllowed(
378
+ "maven",
379
+ ["bazel", "sbt", "gradle", "mill"],
380
+ options,
381
+ )
382
+ ) {
383
+ if (gradleFiles.length) {
384
+ thoughtLog(
385
+ `Is this a Gradle project? I recommend invoking cdxgen with the "-t gradle" option if you're encountering build errors.`,
392
386
  );
393
387
  }
394
- } else {
395
- // FIXME: The last maven plugin release was on November 28th, 2024.
396
- // Should we fork this repo and maintain it ourselves?
397
- const cdxMavenPlugin =
398
- readEnvironmentVariable("CDX_MAVEN_PLUGIN") ||
399
- "org.cyclonedx:cyclonedx-maven-plugin:2.9.1";
400
- const cdxMavenGoal =
401
- readEnvironmentVariable("CDX_MAVEN_GOAL") || "makeAggregateBom";
402
- mvnArgs = [
403
- "-fn",
404
- `${cdxMavenPlugin}:${cdxMavenGoal}`,
405
- "-DoutputName=bom",
406
- ];
407
- if (includeMavenTestScope) {
408
- mvnArgs.push("-DincludeTestScope=true");
409
- }
410
- // By using quiet mode we can reduce the maxBuffer used and avoid crashes
411
- if (!DEBUG_MODE) {
412
- mvnArgs.push("-q");
413
- }
414
- // Support for passing additional settings and profile to maven
415
- if (readEnvironmentVariable("MVN_ARGS")) {
416
- const addArgs = parseMavenArgs(readEnvironmentVariable("MVN_ARGS"));
417
- mvnArgs = mvnArgs.concat(addArgs);
418
- }
419
- // specVersion 1.4 doesn't support externalReferences.type=distribution-intake
420
- // so we need to run the plugin with the correct version
421
- if (options.specVersion >= 1.6) {
422
- mvnArgs = mvnArgs.concat(`-DschemaVersion=${toolsSpecVersion}`);
423
- } else if (options.specVersion > 1.4) {
424
- mvnArgs = mvnArgs.concat(`-DschemaVersion=${options.specVersion}`);
425
- }
426
- }
427
- const firstPom = pomFiles.length ? pomFiles[0] : undefined;
428
- let mavenCmd = getMavenCommand(path, path);
429
- for (const f of pomFiles) {
430
- const basePath = dirname(f);
431
- if (
432
- isQuarkus &&
433
- !options.deep &&
434
- rootModules?.includes(basename(basePath))
435
- ) {
436
- if (DEBUG_MODE) {
437
- console.log("Skipped sub-module", basePath);
388
+ if (!isQuarkus) {
389
+ // Quarkus projects require special treatment. To detect quarkus, we parse the first 3 maven file to look for a hit
390
+ for (const pf of pomFiles.slice(0, 3)) {
391
+ const pomMap = parsePom(pf);
392
+ if (!rootModules && pomMap?.modules?.length) {
393
+ rootModules = pomMap.modules;
394
+ }
395
+ // In quarkus mode, we cannot use the maven deps tree
396
+ if (pomMap.isQuarkus) {
397
+ isQuarkus = true;
398
+ useMavenDepsTree = false;
399
+ break;
400
+ }
438
401
  }
439
- continue;
440
402
  }
441
- const settingsXml = join(basePath, "settings.xml");
442
- if (safeExistsSync(settingsXml)) {
443
- console.log(
444
- `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}'`,
445
- );
446
- }
447
- if (mavenCmd?.endsWith("mvn")) {
448
- mavenCmd = getMavenCommand(basePath, path);
449
- }
450
- // Should we attempt to resolve class names
451
- if (options.resolveClass || options.deep) {
452
- const tmpjarNSMapping = await collectMvnDependencies(
453
- mavenCmd,
454
- basePath,
455
- true,
456
- false,
403
+ let result;
404
+ let mvnArgs;
405
+ // FIXME: How do we motivate everyone to upgrade to 1.7?
406
+ const toolsSpecVersion = 1.6;
407
+ if (isQuarkus) {
408
+ thoughtLog(
409
+ "This appears to be a Quarkus project. Let's use the right Maven plugin.",
457
410
  );
458
- if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
459
- jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
411
+ // disable analytics. See: https://quarkus.io/usage/
412
+ mvnArgs = [
413
+ "-fn",
414
+ "quarkus:dependency-sbom",
415
+ "-Dquarkus.analytics.disabled=true",
416
+ ];
417
+ if (options.specVersion >= 1.6) {
418
+ mvnArgs = mvnArgs.concat(
419
+ `-Dquarkus.dependency.sbom.schema-version=${toolsSpecVersion}`,
420
+ );
421
+ }
422
+ } else {
423
+ // FIXME: The last maven plugin release was on November 28th, 2024.
424
+ // Should we fork this repo and maintain it ourselves?
425
+ const cdxMavenPlugin =
426
+ readEnvironmentVariable("CDX_MAVEN_PLUGIN") ||
427
+ "org.cyclonedx:cyclonedx-maven-plugin:2.9.1";
428
+ const cdxMavenGoal =
429
+ readEnvironmentVariable("CDX_MAVEN_GOAL") || "makeAggregateBom";
430
+ mvnArgs = [
431
+ "-fn",
432
+ `${cdxMavenPlugin}:${cdxMavenGoal}`,
433
+ "-DoutputName=bom",
434
+ ];
435
+ if (includeMavenTestScope) {
436
+ mvnArgs.push("-DincludeTestScope=true");
437
+ }
438
+ // By using quiet mode we can reduce the maxBuffer used and avoid crashes
439
+ if (!DEBUG_MODE) {
440
+ mvnArgs.push("-q");
441
+ }
442
+ // Support for passing additional settings and profile to maven
443
+ if (readEnvironmentVariable("MVN_ARGS")) {
444
+ const addArgs = parseMavenArgs(readEnvironmentVariable("MVN_ARGS"));
445
+ mvnArgs = mvnArgs.concat(addArgs);
446
+ }
447
+ // specVersion 1.4 doesn't support externalReferences.type=distribution-intake
448
+ // so we need to run the plugin with the correct version
449
+ if (options.specVersion >= 1.6) {
450
+ mvnArgs = mvnArgs.concat(`-DschemaVersion=${toolsSpecVersion}`);
451
+ } else if (options.specVersion > 1.4) {
452
+ mvnArgs = mvnArgs.concat(`-DschemaVersion=${options.specVersion}`);
460
453
  }
461
454
  }
462
- // Use the cyclonedx maven plugin if there is no preference for maven deps tree
463
- if (!useMavenDepsTree) {
464
- thoughtLog("The user wants me to use the cyclonedx-maven plugin.");
465
- console.log(`Executing '${mavenCmd}' in`, basePath);
466
- result = safeSpawnSync(mavenCmd, mvnArgs, {
467
- cwd: basePath,
468
- shell: isWin,
469
- });
470
- // Check if the cyclonedx plugin created the required bom.json file
471
- // Sometimes the plugin fails silently for complex maven projects
472
- bomJsonFiles = getAllFiles(
473
- path,
474
- "**/target/*{cdx,bom,cyclonedx}*.json",
475
- options,
476
- );
477
- // Check if the bom json files got created in a directory other than target
478
- if (!bomJsonFiles.length) {
455
+ const firstPom = pomFiles.length ? pomFiles[0] : undefined;
456
+ let mavenCmd = getMavenCommand(path, path);
457
+ for (const f of pomFiles) {
458
+ const basePath = dirname(f);
459
+ if (
460
+ isQuarkus &&
461
+ !options.deep &&
462
+ rootModules?.includes(basename(basePath))
463
+ ) {
464
+ if (DEBUG_MODE) {
465
+ console.log("Skipped sub-module", basePath);
466
+ }
467
+ continue;
468
+ }
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
+ );
474
+ }
475
+ if (mavenCmd?.endsWith("mvn")) {
476
+ mavenCmd = getMavenCommand(basePath, path);
477
+ }
478
+ // Should we attempt to resolve class names
479
+ if (options.resolveClass || options.deep) {
480
+ const tmpjarNSMapping = await collectMvnDependencies(
481
+ mavenCmd,
482
+ basePath,
483
+ true,
484
+ false,
485
+ );
486
+ if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
487
+ jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
488
+ }
489
+ }
490
+ // Use the cyclonedx maven plugin if there is no preference for maven deps tree
491
+ if (!useMavenDepsTree) {
492
+ thoughtLog("The user wants me to use the cyclonedx-maven plugin.");
493
+ console.log(`Executing '${mavenCmd}' in`, basePath);
494
+ result = safeSpawnSync(mavenCmd, mvnArgs, {
495
+ cwd: basePath,
496
+ shell: isWin,
497
+ });
498
+ // Check if the cyclonedx plugin created the required bom.json file
499
+ // Sometimes the plugin fails silently for complex maven projects
479
500
  bomJsonFiles = getAllFiles(
480
501
  path,
481
- "target/**/*{cdx,bom,cyclonedx}*.json",
502
+ "**/target/*{cdx,bom,cyclonedx}*.json",
482
503
  options,
483
504
  );
484
- }
485
- }
486
- // Also check if the user has a preference for maven deps tree command
487
- if (
488
- useMavenDepsTree ||
489
- !bomJsonFiles.length ||
490
- result?.status !== 0 ||
491
- result?.error
492
- ) {
493
- const tempRoot = getTmpDir();
494
- const tempDir = safeMkdtempSync(join(tempRoot, "cdxgen-mvn-"));
495
- const tempMvnTree = join(tempDir, "cdxgen-mvn-tree.json");
496
- const tempMvnTreeText = join(tempDir, "cdxgen-mvn-tree.txt");
497
- const tempMvnParentTree = join(tempDir, "cdxgen-mvn-parent-tree.json");
498
- const tempMvnParentTreeText = join(
499
- tempDir,
500
- "cdxgen-mvn-parent-tree.txt",
501
- );
502
- try {
503
- let mvnTreeArgs = [
504
- "dependency:tree",
505
- `-DoutputFile=${tempMvnTree}`,
506
- "-DoutputType=json",
507
- ];
508
- let addArgs = [];
509
- if (readEnvironmentVariable("MVN_ARGS")) {
510
- addArgs = parseMavenArgs(readEnvironmentVariable("MVN_ARGS"));
511
- mvnTreeArgs = mvnTreeArgs.concat(addArgs);
512
- }
513
- // Automatically use settings.xml to improve the success for fallback
514
- if (safeExistsSync(settingsXml)) {
515
- mvnTreeArgs.push("-s");
516
- mvnTreeArgs.push(settingsXml);
517
- }
518
- // For the first pom alone, we need to execute first in non-recursive mode to capture
519
- // the parent component. Then, we execute all of them in recursive mode
520
- if (f === firstPom) {
521
- thoughtLog(
522
- "What is the parent component here? Let's use maven command to find out.",
505
+ // Check if the bom json files got created in a directory other than target
506
+ if (!bomJsonFiles.length) {
507
+ bomJsonFiles = getAllFiles(
508
+ path,
509
+ "target/**/*{cdx,bom,cyclonedx}*.json",
510
+ options,
523
511
  );
524
- let findParentComponentArgs = [
512
+ }
513
+ }
514
+ // Also check if the user has a preference for maven deps tree command
515
+ if (
516
+ useMavenDepsTree ||
517
+ !bomJsonFiles.length ||
518
+ result?.status !== 0 ||
519
+ result?.error
520
+ ) {
521
+ const tempRoot = getTmpDir();
522
+ 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");
525
+ const tempMvnParentTree = join(
526
+ tempDir,
527
+ "cdxgen-mvn-parent-tree.json",
528
+ );
529
+ const tempMvnParentTreeText = join(
530
+ tempDir,
531
+ "cdxgen-mvn-parent-tree.txt",
532
+ );
533
+ try {
534
+ let mvnTreeArgs = [
525
535
  "dependency:tree",
526
- "-N",
527
- `-DoutputFile=${tempMvnParentTree}`,
536
+ `-DoutputFile=${tempMvnTree}`,
528
537
  "-DoutputType=json",
529
538
  ];
530
- if (addArgs.length) {
531
- findParentComponentArgs = findParentComponentArgs.concat(addArgs);
539
+ let addArgs = [];
540
+ if (readEnvironmentVariable("MVN_ARGS")) {
541
+ addArgs = parseMavenArgs(readEnvironmentVariable("MVN_ARGS"));
542
+ mvnTreeArgs = mvnTreeArgs.concat(addArgs);
532
543
  }
533
- result = safeSpawnSync(mavenCmd, findParentComponentArgs, {
534
- cwd: basePath,
535
- shell: isWin,
536
- });
537
- // If json is empty or unparseable, fallback to text parsing
538
- let emptyJson = !safeExistsSync(tempMvnParentTree);
539
- if (safeExistsSync(tempMvnParentTree)) {
540
- const mvnTreeString = readFileSync(tempMvnParentTree, {
541
- encoding: "utf-8",
542
- });
543
- const parsedList = parseMavenTreeJson(mvnTreeString, f);
544
- if (!parsedList?.pkgList?.length) {
545
- emptyJson = true;
546
- // Remove the invalid json file so text output is preferred below.
547
- if (safeExistsSync(tempMvnParentTree)) {
548
- safeUnlinkSync(tempMvnParentTree);
549
- }
550
- }
544
+ // Automatically use settings.xml to improve the success for fallback
545
+ if (safeExistsSync(settingsXml)) {
546
+ mvnTreeArgs.push("-s");
547
+ mvnTreeArgs.push(settingsXml);
551
548
  }
552
- if (result.status !== 0 || result.error || emptyJson) {
553
- findParentComponentArgs = [
549
+ // For the first pom alone, we need to execute first in non-recursive mode to capture
550
+ // the parent component. Then, we execute all of them in recursive mode
551
+ if (f === firstPom) {
552
+ thoughtLog(
553
+ "What is the parent component here? Let's use maven command to find out.",
554
+ );
555
+ let findParentComponentArgs = [
554
556
  "dependency:tree",
555
557
  "-N",
556
- `-DoutputFile=${tempMvnParentTreeText}`,
558
+ `-DoutputFile=${tempMvnParentTree}`,
559
+ "-DoutputType=json",
557
560
  ];
558
561
  if (addArgs.length) {
559
562
  findParentComponentArgs =
@@ -563,349 +566,407 @@ export async function createJavaBom(path, options) {
563
566
  cwd: basePath,
564
567
  shell: isWin,
565
568
  });
566
- }
567
- if (result.status === 0) {
568
- const parentTreeFile = safeExistsSync(tempMvnParentTreeText)
569
- ? tempMvnParentTreeText
570
- : tempMvnParentTree;
571
- if (safeExistsSync(parentTreeFile)) {
572
- const mvnTreeString = readFileSync(parentTreeFile, {
569
+ // If json is empty or unparseable, fallback to text parsing
570
+ let emptyJson = !safeExistsSync(tempMvnParentTree);
571
+ if (safeExistsSync(tempMvnParentTree)) {
572
+ const mvnTreeString = readFileSync(tempMvnParentTree, {
573
573
  encoding: "utf-8",
574
574
  });
575
- const parsedList = parentTreeFile.endsWith(".json")
576
- ? parseMavenTreeJson(mvnTreeString, f)
577
- : parseMavenTree(mvnTreeString, f);
578
- const dlist = parsedList.pkgList || [];
579
- if (dlist.length) {
580
- const tmpParentComponent = dlist.splice(0, 1)[0];
581
- tmpParentComponent.type = "application";
582
- parentComponent = tmpParentComponent;
583
- parentComponent.components = [];
584
- if (parentComponent.name) {
585
- thoughtLog(
586
- `Parent component is called ${parentComponent.name}!`,
587
- );
575
+ const parsedList = parseMavenTreeJson(mvnTreeString, f);
576
+ if (!parsedList?.pkgList?.length) {
577
+ emptyJson = true;
578
+ // Remove the invalid json file so text output is preferred below.
579
+ if (safeExistsSync(tempMvnParentTree)) {
580
+ safeUnlinkSync(tempMvnParentTree);
588
581
  }
589
582
  }
590
583
  }
591
- }
592
- }
593
- thoughtLog(
594
- `**MAVEN**: Let's use Maven to collect packages from ${basePath}.`,
595
- );
596
- if (DEBUG_MODE) {
597
- console.log(
598
- `Executing '${basename(mavenCmd)} dependency:tree ...' in ${basePath}`,
599
- );
600
- }
601
- result = safeSpawnSync(mavenCmd, mvnTreeArgs, {
602
- cwd: basePath,
603
- shell: isWin,
604
- });
605
- let emptyJson = !safeExistsSync(tempMvnTree);
606
- if (safeExistsSync(tempMvnTree)) {
607
- const mvnTreeString = readFileSync(tempMvnTree, {
608
- encoding: "utf-8",
609
- });
610
- const parsedList = parseMavenTreeJson(mvnTreeString, f);
611
- if (!parsedList?.pkgList?.length) {
612
- emptyJson = true;
613
- // Remove the invalid json file so text output is preferred below.
614
- if (safeExistsSync(tempMvnTree)) {
615
- safeUnlinkSync(tempMvnTree);
584
+ if (result.status !== 0 || result.error || emptyJson) {
585
+ findParentComponentArgs = [
586
+ "dependency:tree",
587
+ "-N",
588
+ `-DoutputFile=${tempMvnParentTreeText}`,
589
+ ];
590
+ if (addArgs.length) {
591
+ findParentComponentArgs =
592
+ findParentComponentArgs.concat(addArgs);
593
+ }
594
+ result = safeSpawnSync(mavenCmd, findParentComponentArgs, {
595
+ cwd: basePath,
596
+ shell: isWin,
597
+ });
598
+ }
599
+ if (result.status === 0) {
600
+ const parentTreeFile = safeExistsSync(tempMvnParentTreeText)
601
+ ? tempMvnParentTreeText
602
+ : tempMvnParentTree;
603
+ if (safeExistsSync(parentTreeFile)) {
604
+ const mvnTreeString = readFileSync(parentTreeFile, {
605
+ encoding: "utf-8",
606
+ });
607
+ const parsedList = parentTreeFile.endsWith(".json")
608
+ ? parseMavenTreeJson(mvnTreeString, f)
609
+ : parseMavenTree(mvnTreeString, f);
610
+ const dlist = parsedList.pkgList || [];
611
+ if (dlist.length) {
612
+ const tmpParentComponent = dlist.splice(0, 1)[0];
613
+ tmpParentComponent.type = "application";
614
+ parentComponent = tmpParentComponent;
615
+ parentComponent.components = [];
616
+ if (parentComponent.name) {
617
+ thoughtLog(
618
+ `Parent component is called ${parentComponent.name}!`,
619
+ );
620
+ }
621
+ }
622
+ }
616
623
  }
617
624
  }
618
- }
619
- if (result.status !== 0 || result.error || emptyJson) {
620
- mvnTreeArgs = [
621
- "dependency:tree",
622
- `-DoutputFile=${tempMvnTreeText}`,
623
- ];
624
- if (addArgs.length) {
625
- mvnTreeArgs = mvnTreeArgs.concat(addArgs);
626
- }
627
- if (safeExistsSync(settingsXml)) {
628
- mvnTreeArgs.push("-s");
629
- mvnTreeArgs.push(settingsXml);
625
+ thoughtLog(
626
+ `**MAVEN**: Let's use Maven to collect packages from ${basePath}.`,
627
+ );
628
+ if (DEBUG_MODE) {
629
+ console.log(
630
+ `Executing '${basename(mavenCmd)} dependency:tree ...' in ${basePath}`,
631
+ );
630
632
  }
631
633
  result = safeSpawnSync(mavenCmd, mvnTreeArgs, {
632
634
  cwd: basePath,
633
635
  shell: isWin,
634
636
  });
635
- }
636
- if (result.status !== 0 || result.error) {
637
- possible_misses = true;
638
- // Our approach to recursively invoking the maven plugin for each sub-module is bound to result in failures
639
- // These could be due to a range of reasons that are covered below.
640
- if (pomFiles.length === 1 || DEBUG_MODE || PREFER_MAVEN_DEPS_TREE) {
641
- if (result.stdout) {
642
- console.log(result.stdout);
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);
643
658
  }
644
- if (result.stderr) {
645
- console.log(result.stderr);
646
- console.log("The above build errors could be due to:\n");
659
+ if (safeExistsSync(settingsXml)) {
660
+ mvnTreeArgs.push("-s");
661
+ mvnTreeArgs.push(settingsXml);
647
662
  }
663
+ result = safeSpawnSync(mavenCmd, mvnTreeArgs, {
664
+ cwd: basePath,
665
+ shell: isWin,
666
+ });
667
+ }
668
+ if (result.status !== 0 || result.error) {
669
+ possible_misses = true;
670
+ // Our approach to recursively invoking the maven plugin for each sub-module is bound to result in failures
671
+ // These could be due to a range of reasons that are covered below.
648
672
  if (
649
- result.stdout &&
650
- (result.stdout.includes("Non-resolvable parent POM") ||
651
- result.stdout.includes("points at wrong local POM"))
652
- ) {
653
- console.log(
654
- "1. Check if the pom.xml contains valid settings for parent and modules. Some projects can be built only from a specific directory.",
655
- );
656
- } else if (
657
- result.stdout &&
658
- (result.stdout.includes("Could not resolve dependencies") ||
659
- result.stdout.includes(
660
- "no dependency information available",
661
- ) ||
662
- result.stdout.includes(
663
- "The following artifacts could not be resolved",
664
- ))
665
- ) {
666
- console.log(
667
- "1. Try building the project with 'mvn package -Dmaven.test.skip=true' using the correct version of Java and maven before invoking cdxgen.",
668
- );
669
- } else if (
670
- result.stdout?.includes(
671
- "Could not resolve target platform specification",
672
- )
673
+ pomFiles.length === 1 ||
674
+ DEBUG_MODE ||
675
+ PREFER_MAVEN_DEPS_TREE
673
676
  ) {
677
+ if (result.stdout) {
678
+ console.log(result.stdout);
679
+ }
680
+ if (result.stderr) {
681
+ console.log(result.stderr);
682
+ console.log("The above build errors could be due to:\n");
683
+ }
684
+ if (
685
+ result.stdout &&
686
+ (result.stdout.includes("Non-resolvable parent POM") ||
687
+ result.stdout.includes("points at wrong local POM"))
688
+ ) {
689
+ console.log(
690
+ "1. Check if the pom.xml contains valid settings for parent and modules. Some projects can be built only from a specific directory.",
691
+ );
692
+ } else if (
693
+ result.stdout &&
694
+ (result.stdout.includes("Could not resolve dependencies") ||
695
+ result.stdout.includes(
696
+ "no dependency information available",
697
+ ) ||
698
+ result.stdout.includes(
699
+ "The following artifacts could not be resolved",
700
+ ))
701
+ ) {
702
+ recordDegradation("jvm.maven.build-first", {
703
+ ecosystem: "java",
704
+ tool: "maven",
705
+ impact: "transitive-deps",
706
+ command: `${basename(mavenCmd)} ${mvnTreeArgs.join(" ")}`,
707
+ exitCode:
708
+ typeof result.status === "number"
709
+ ? result.status
710
+ : undefined,
711
+ detail:
712
+ "Maven could not resolve the project dependencies, so the dependency tree is incomplete.",
713
+ outputExcerpt: commandOutputText(result),
714
+ });
715
+ console.log(
716
+ "1. Try building the project with 'mvn package -Dmaven.test.skip=true' using the correct version of Java and maven before invoking cdxgen.",
717
+ );
718
+ } else if (
719
+ result.stdout?.includes(
720
+ "Could not resolve target platform specification",
721
+ )
722
+ ) {
723
+ console.log(
724
+ "1. Some projects can be built only from the root directory. Invoke cdxgen with --no-recurse option",
725
+ );
726
+ } else {
727
+ console.log(
728
+ "1. Java version requirement: cdxgen container image bundles Java 24 with maven 3.9 which might be incompatible. Try running cdxgen with the custom JDK11-based image `ghcr.io/cdxgen/cdxgen-java11:v13`.",
729
+ );
730
+ }
674
731
  console.log(
675
- "1. Some projects can be built only from the root directory. Invoke cdxgen with --no-recurse option",
732
+ "2. Private dependencies cannot be downloaded: Check if any additional arguments must be passed to maven and set them via MVN_ARGS environment variable.",
676
733
  );
677
- } else {
678
734
  console.log(
679
- "1. Java version requirement: cdxgen container image bundles Java 24 with maven 3.9 which might be incompatible. Try running cdxgen with the custom JDK11-based image `ghcr.io/cdxgen/cdxgen-java11:v13`.",
735
+ "3. Check if all required environment variables including any maven profile arguments are passed correctly to this tool.",
680
736
  );
681
737
  }
738
+ // The degradation is recorded before the fail-on-error handling
739
+ // below, so the run that stops here still reports why maven
740
+ // failed and what fixes it.
741
+ recordDegradation("jvm.maven.manifest-fallback", {
742
+ ecosystem: "java",
743
+ tool: "maven",
744
+ impact: "transitive-deps",
745
+ command: `${basename(mavenCmd)} ${mvnTreeArgs.join(" ")}`,
746
+ exitCode:
747
+ typeof result.status === "number" ? result.status : undefined,
748
+ detail:
749
+ "The maven dependency tree did not succeed, so at best the direct dependencies parsed from pom.xml are available.",
750
+ outputExcerpt: commandOutputText(result),
751
+ });
752
+ // Do not fall back to methods that can produce incomplete results when failOnError is set
753
+ deferFailOnError(options, {
754
+ ecosystem: "java",
755
+ tool: "maven",
756
+ exitCode:
757
+ typeof result.status === "number" ? result.status : undefined,
758
+ command: `${mavenCmd} ${mvnTreeArgs.join(" ")}`,
759
+ outputExcerpt: commandOutputText(result),
760
+ detail: "the maven dependency tree command failed",
761
+ });
682
762
  console.log(
683
- "2. Private dependencies cannot be downloaded: Check if any additional arguments must be passed to maven and set them via MVN_ARGS environment variable.",
763
+ "\nFalling back to parsing pom.xml files. Only direct dependencies would get included!",
684
764
  );
685
- console.log(
686
- "3. Check if all required environment variables including any maven profile arguments are passed correctly to this tool.",
765
+ thoughtLog(
766
+ "**MAVEN**: There appear to be build errors, so the SBOM will be incomplete.",
687
767
  );
688
- }
689
- // Do not fall back to methods that can produce incomplete results when failOnError is set
690
- options.failOnError && process.exit(1);
691
- console.log(
692
- "\nFalling back to parsing pom.xml files. Only direct dependencies would get included!",
693
- );
694
- thoughtLog(
695
- "**MAVEN**: There appear to be build errors, so the SBOM will be incomplete.",
696
- );
697
- const pomMap = parsePom(f);
698
- const dlist = pomMap?.dependencies || [];
699
- if (dlist.length) {
700
- pkgList = pkgList.concat(dlist);
701
- }
702
- } else {
703
- const treeFile = safeExistsSync(tempMvnTreeText)
704
- ? tempMvnTreeText
705
- : tempMvnTree;
706
- if (safeExistsSync(treeFile)) {
707
- const mvnTreeString = readFileSync(treeFile, {
708
- encoding: "utf-8",
709
- });
710
- const parsedList = treeFile.endsWith(".json")
711
- ? parseMavenTreeJson(mvnTreeString, f)
712
- : parseMavenTree(mvnTreeString, f);
713
- const dlist = parsedList.pkgList || [];
714
- const tmpParentComponent = dlist.splice(0, 1)[0];
715
- if (tmpParentComponent) {
716
- tmpParentComponent.type = "application";
717
- }
768
+ const pomMap = parsePom(f);
769
+ const dlist = pomMap?.dependencies || [];
718
770
  if (dlist.length) {
719
771
  pkgList = pkgList.concat(dlist);
720
- if (dlist.length > 1) {
721
- thoughtLog(`Obtained ${dlist.length} components from maven.`);
722
- } else {
772
+ }
773
+ } 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);
784
+ const dlist = parsedList.pkgList || [];
785
+ const tmpParentComponent = dlist.splice(0, 1)[0];
786
+ if (tmpParentComponent) {
787
+ tmpParentComponent.type = "application";
788
+ }
789
+ if (dlist.length) {
790
+ pkgList = pkgList.concat(dlist);
791
+ if (dlist.length > 1) {
792
+ thoughtLog(
793
+ `Obtained ${dlist.length} components from maven.`,
794
+ );
795
+ } else {
796
+ thoughtLog(
797
+ `"Received very few components from the maven dependency tree command for ${basePath}."`,
798
+ );
799
+ }
800
+ }
801
+ // Retain the parent hierarchy
802
+ if (!tmpParentComponent) {
723
803
  thoughtLog(
724
- `"Received very few components from the maven dependency tree command for ${basePath}."`,
804
+ `No parseable components were found after executing '${basename(mavenCmd)}'.`,
725
805
  );
806
+ } else if (!Object.keys(parentComponent).length) {
807
+ parentComponent = tmpParentComponent;
808
+ parentComponent.components = [];
809
+ } else {
810
+ parentComponent.components.push(tmpParentComponent);
726
811
  }
727
- }
728
- // Retain the parent hierarchy
729
- if (!tmpParentComponent) {
730
- thoughtLog(
731
- `No parseable components were found after executing '${basename(mavenCmd)}'.`,
732
- );
733
- } else if (!Object.keys(parentComponent).length) {
734
- parentComponent = tmpParentComponent;
735
- parentComponent.components = [];
736
- } else {
737
- parentComponent.components.push(tmpParentComponent);
738
- }
739
- if (parsedList?.dependenciesList?.length) {
740
- dependencies = mergeDependencies(
741
- dependencies,
742
- parsedList.dependenciesList,
743
- tmpParentComponent,
744
- );
745
- } else {
746
- if (dlist?.length) {
747
- thoughtLog(
748
- `Hmm, I didn't find any dependencies after executing '${basename(mavenCmd)}'. However, I did get ${dlist.length} components, which is confusing.`,
812
+ if (parsedList?.dependenciesList?.length) {
813
+ dependencies = mergeDependencies(
814
+ dependencies,
815
+ parsedList.dependenciesList,
816
+ tmpParentComponent,
749
817
  );
818
+ } else {
819
+ if (dlist?.length) {
820
+ thoughtLog(
821
+ `Hmm, I didn't find any dependencies after executing '${basename(mavenCmd)}'. However, I did get ${dlist.length} components, which is confusing.`,
822
+ );
823
+ }
750
824
  }
751
825
  }
752
826
  }
753
- }
754
- } finally {
755
- if (!DEBUG_MODE && tempDir?.startsWith(tempRoot)) {
756
- safeRmSync(tempDir, { recursive: true, force: true });
827
+ } finally {
828
+ if (!DEBUG_MODE && tempDir?.startsWith(tempRoot)) {
829
+ safeRmSync(tempDir, { recursive: true, force: true });
830
+ }
757
831
  }
758
832
  }
759
- }
760
- } // for
761
- // Locate and parse all bom.json files from the maven plugin
762
- if (!useMavenDepsTree) {
763
- for (const abjson of bomJsonFiles) {
764
- let bomJsonObj;
765
- try {
766
- if (DEBUG_MODE) {
767
- console.log(`Extracting data from generated bom file ${abjson}`);
768
- }
769
- bomJsonObj = JSON.parse(
770
- readFileSync(abjson, {
771
- encoding: "utf-8",
772
- }),
773
- );
774
- if (bomJsonObj) {
775
- if (
776
- !tools &&
777
- bomJsonObj.metadata &&
778
- bomJsonObj.metadata.tools &&
779
- (Array.isArray(bomJsonObj.metadata.tools) ||
780
- bomJsonObj.metadata.tools.components ||
781
- bomJsonObj.metadata.tools.services)
782
- ) {
783
- tools = bomJsonObj.metadata.tools;
833
+ } // for
834
+ // Locate and parse all bom.json files from the maven plugin
835
+ if (!useMavenDepsTree) {
836
+ for (const abjson of bomJsonFiles) {
837
+ let bomJsonObj;
838
+ try {
839
+ if (DEBUG_MODE) {
840
+ console.log(`Extracting data from generated bom file ${abjson}`);
784
841
  }
785
- const toolRefs = extractToolRefs(
786
- bomJsonObj?.metadata?.tools,
787
- (tool) => tool?.name !== "cdxgen",
842
+ bomJsonObj = JSON.parse(
843
+ readFileSync(abjson, {
844
+ encoding: "utf-8",
845
+ }),
788
846
  );
789
- if (
790
- bomJsonObj.metadata?.component &&
791
- !Object.keys(parentComponent).length
792
- ) {
793
- parentComponent = bomJsonObj.metadata.component;
794
- options.parentComponent = parentComponent;
795
- }
796
- if (bomJsonObj.components) {
797
- // Inject evidence into the components. #994
798
- if (options.specVersion >= 1.5) {
799
- // maven would usually generate a target directory closest to the pom.xml
800
- // I am sure there would be cases where this assumption is not true :)
801
- const srcPomFile = join(dirname(abjson), "..", "pom.xml");
802
- for (const acomp of bomJsonObj.components) {
803
- if (!acomp.evidence) {
804
- acomp.evidence = {
805
- identity: {
806
- field: "purl",
807
- confidence: 0.8,
808
- methods: [
809
- {
810
- technique: "manifest-analysis",
811
- confidence: 0.8,
812
- value: srcPomFile,
813
- },
814
- ],
815
- },
816
- };
817
- }
818
- if (!acomp.properties) {
819
- acomp.properties = [];
847
+ if (bomJsonObj) {
848
+ if (
849
+ !tools &&
850
+ bomJsonObj.metadata &&
851
+ bomJsonObj.metadata.tools &&
852
+ (Array.isArray(bomJsonObj.metadata.tools) ||
853
+ bomJsonObj.metadata.tools.components ||
854
+ bomJsonObj.metadata.tools.services)
855
+ ) {
856
+ tools = bomJsonObj.metadata.tools;
857
+ }
858
+ const toolRefs = extractToolRefs(
859
+ bomJsonObj?.metadata?.tools,
860
+ (tool) => tool?.name !== "cdxgen",
861
+ );
862
+ if (
863
+ bomJsonObj.metadata?.component &&
864
+ !Object.keys(parentComponent).length
865
+ ) {
866
+ parentComponent = bomJsonObj.metadata.component;
867
+ options.parentComponent = parentComponent;
868
+ }
869
+ if (bomJsonObj.components) {
870
+ // Inject evidence into the components. #994
871
+ if (options.specVersion >= 1.5) {
872
+ // maven would usually generate a target directory closest to the pom.xml
873
+ // I am sure there would be cases where this assumption is not true :)
874
+ const srcPomFile = join(dirname(abjson), "..", "pom.xml");
875
+ for (const acomp of bomJsonObj.components) {
876
+ if (!acomp.evidence) {
877
+ acomp.evidence = {
878
+ identity: {
879
+ field: "purl",
880
+ confidence: 0.8,
881
+ methods: [
882
+ {
883
+ technique: "manifest-analysis",
884
+ confidence: 0.8,
885
+ value: srcPomFile,
886
+ },
887
+ ],
888
+ },
889
+ };
890
+ }
891
+ if (!acomp.properties) {
892
+ acomp.properties = [];
893
+ }
894
+ acomp.properties.push({
895
+ name: "internal:SrcFile",
896
+ value: srcPomFile,
897
+ });
898
+ attachIdentityTools(acomp, toolRefs);
820
899
  }
821
- acomp.properties.push({
822
- name: "internal:SrcFile",
823
- value: srcPomFile,
824
- });
825
- attachIdentityTools(acomp, toolRefs);
826
900
  }
901
+ pkgList = pkgList.concat(bomJsonObj.components);
902
+ }
903
+ if (bomJsonObj.dependencies) {
904
+ dependencies = mergeDependencies(
905
+ dependencies,
906
+ bomJsonObj.dependencies,
907
+ parentComponent,
908
+ );
827
909
  }
828
- pkgList = pkgList.concat(bomJsonObj.components);
829
910
  }
830
- if (bomJsonObj.dependencies) {
831
- dependencies = mergeDependencies(
832
- dependencies,
833
- bomJsonObj.dependencies,
834
- parentComponent,
835
- );
911
+ } catch (err) {
912
+ if (options.failOnError || DEBUG_MODE) {
913
+ console.log(err);
836
914
  }
837
- }
838
- } catch (err) {
839
- if (options.failOnError || DEBUG_MODE) {
840
- console.log(err);
841
- options.failOnError && process.exit(1);
915
+ deferFailOnError(options, {
916
+ ecosystem: "java",
917
+ tool: "maven",
918
+ detail:
919
+ "reading the BOM generated by the cyclonedx maven plugin failed",
920
+ });
842
921
  }
843
922
  }
844
923
  }
845
- }
846
- if (possible_misses) {
847
- if (gradleFiles.length) {
848
- console.log(
849
- "Is this a gradle project? Try running cdxgen with `-t gradle`.",
850
- );
851
- } else if (!DEBUG_MODE) {
852
- console.warn(
853
- "Multiple errors occurred while building this project with maven. The SBOM is therefore incomplete!",
854
- );
924
+ if (possible_misses) {
925
+ if (gradleFiles.length) {
926
+ console.log(
927
+ "Is this a gradle project? Try running cdxgen with `-t gradle`.",
928
+ );
929
+ } else if (!DEBUG_MODE) {
930
+ console.warn(
931
+ "Multiple errors occurred while building this project with maven. The SBOM is therefore incomplete!",
932
+ );
933
+ }
855
934
  }
856
935
  }
857
- }
858
- const allProjects = [];
859
- const allProjectsAddedPurls = [];
860
- const rootDependsOn = new Set();
861
- const gradleModules = new Map();
862
- // Determine the root path for gradle
863
- // Fixes gradle invocation for microservices-demo
864
- let gradleRootPath = path;
865
- if (
866
- gradleFiles?.length &&
867
- !safeExistsSync(join(path, "settings.gradle")) &&
868
- !safeExistsSync(join(path, "settings.gradle.kts")) &&
869
- !safeExistsSync(join(path, "build.gradle")) &&
870
- !safeExistsSync(join(path, "build.gradle.kts"))
871
- ) {
872
- gradleRootPath = dirname(gradleFiles[0]);
873
- }
874
- if (safeExistsSync(join(gradleRootPath, "gradle.properties"))) {
875
- thoughtLog(
876
- "Hmm, there is a gradle.properties file. Do we need any private modules or custom JVM arguments for this project 🤔?",
877
- );
878
- }
879
- // Execute gradle properties
880
- if (
881
- gradleFiles?.length &&
882
- isPackageManagerAllowed(
883
- "gradle",
884
- ["maven", "bazel", "sbt", "mill"],
885
- options,
886
- )
887
- ) {
888
- const wrapperPropPath = join(
889
- gradleRootPath,
890
- "gradle",
891
- "wrapper",
892
- "gradle-wrapper.properties",
893
- );
894
- if (safeExistsSync(wrapperPropPath)) {
895
- try {
896
- const propContent = readFileSync(wrapperPropPath, "utf-8");
897
- const distUrlLine = propContent
898
- .split("\n")
899
- .find((l) => l.startsWith("distributionUrl="));
900
- if (distUrlLine) {
901
- const distributionUrl = distUrlLine
902
- .split("=")[1]
903
- .trim()
904
- .replace(/\\:/g, ":");
905
- const versionMatch = distributionUrl.match(
906
- /gradle-(.*?)-(bin|all)\.zip/,
907
- );
908
- const gradleVersion = versionMatch ? versionMatch[1] : "unknown";
936
+ const allProjects = [];
937
+ const allProjectsAddedPurls = [];
938
+ const rootDependsOn = new Set();
939
+ const gradleModules = new Map();
940
+ // Determine the root path for gradle
941
+ // Fixes gradle invocation for microservices-demo
942
+ let gradleRootPath = path;
943
+ if (
944
+ gradleFiles?.length &&
945
+ !safeExistsSync(join(path, "settings.gradle")) &&
946
+ !safeExistsSync(join(path, "settings.gradle.kts")) &&
947
+ !safeExistsSync(join(path, "build.gradle")) &&
948
+ !safeExistsSync(join(path, "build.gradle.kts"))
949
+ ) {
950
+ gradleRootPath = dirname(gradleFiles[0]);
951
+ }
952
+ if (safeExistsSync(join(gradleRootPath, "gradle.properties"))) {
953
+ thoughtLog(
954
+ "Hmm, there is a gradle.properties file. Do we need any private modules or custom JVM arguments for this project 🤔?",
955
+ );
956
+ }
957
+ // Execute gradle properties
958
+ if (
959
+ gradleFiles?.length &&
960
+ isPackageManagerAllowed(
961
+ "gradle",
962
+ ["maven", "bazel", "sbt", "mill"],
963
+ options,
964
+ )
965
+ ) {
966
+ const wrapperInfo = readGradleWrapperVersion(gradleRootPath);
967
+ if (wrapperInfo) {
968
+ try {
969
+ const { version: gradleVersion, distributionUrl } = wrapperInfo;
909
970
  thoughtLog(
910
971
  `Found Gradle wrapper with distributionUrl: ${distributionUrl}, version: ${gradleVersion}`,
911
972
  );
@@ -921,20 +982,14 @@ export async function createJavaBom(path, options) {
921
982
  }
922
983
  }
923
984
  if (!toolsArr.find((c) => c.name === "gradle")) {
924
- const distShaLine = propContent
925
- .split("\n")
926
- .find((l) => l.startsWith("distributionSha256Sum="));
927
985
  let hashes;
928
- if (distShaLine) {
929
- const sha256Sum = distShaLine.split("=")[1].trim();
930
- if (sha256Sum) {
931
- hashes = [
932
- {
933
- alg: "SHA-256",
934
- content: sha256Sum,
935
- },
936
- ];
937
- }
986
+ if (wrapperInfo.distributionSha256Sum) {
987
+ hashes = [
988
+ {
989
+ alg: "SHA-256",
990
+ content: wrapperInfo.distributionSha256Sum,
991
+ },
992
+ ];
938
993
  }
939
994
  const gradleComponent = {
940
995
  type: "application",
@@ -965,875 +1020,935 @@ export async function createJavaBom(path, options) {
965
1020
  } else {
966
1021
  tools.components = toolsArr;
967
1022
  }
1023
+ } catch (_err) {
1024
+ // ignore
968
1025
  }
969
- } catch (_err) {
970
- // ignore
971
- }
972
- }
973
- let includedBuilds = [];
974
- let allProjectsStr = [];
975
- if (readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")) {
976
- includedBuilds = readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")
977
- .split(",")
978
- .map((b) => (!b.startsWith(":") ? `:${b}` : b));
979
- }
980
- const resolveGradleDistribution = isFeatureEnabled(
981
- options,
982
- "resolve-gradle-distribution",
983
- );
984
- const gradleInitArgs = readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")
985
- ? []
986
- : ["--init-script", GRADLE_INIT_SCRIPT];
987
- if (resolveGradleDistribution && gradleInitArgs.length) {
988
- gradleInitArgs.push("-PcdxgenResolveDistribution=true");
989
- }
990
- parallelPropTaskOut = executeParallelGradleProperties(
991
- gradleRootPath,
992
- [null].concat(includedBuilds),
993
- gradleInitArgs,
994
- );
995
- if (readEnvironmentVariable("GRADLE_INCLUDED_BUILDS") === undefined) {
996
- const outputLines = parallelPropTaskOut.split("\n");
997
- for (const [_i, line] of outputLines.entries()) {
998
- if (line.startsWith("Root project '") || line.startsWith("Project '")) {
999
- break;
1000
- }
1001
- if (line.startsWith("<CDXGEN:includedBuild>")) {
1002
- const includedBuild = line.split(">");
1003
- if (!includedBuilds.includes(includedBuild[1].trim())) {
1004
- includedBuilds.push(includedBuild[1].trim());
1005
- }
1006
- }
1007
- }
1008
- if (includedBuilds.length > 0) {
1009
- thoughtLog(
1010
- `Wait, this gradle project uses composite builds. I must carefully process these ${includedBuilds.length} projects, in addition to the root.`,
1011
- );
1012
- if (DEBUG_MODE) {
1013
- console.log(`Composite builds: ${includedBuilds.join(" ").trim()}.`);
1014
- }
1015
- parallelPropTaskOut = parallelPropTaskOut.concat(
1016
- "\n",
1017
- executeParallelGradleProperties(gradleRootPath, includedBuilds),
1018
- );
1019
1026
  }
1020
- }
1021
- const splitPropTaskOut = splitOutputByGradleProjects(parallelPropTaskOut, [
1022
- "properties",
1023
- ]);
1024
- for (const [key, propTaskOut] of splitPropTaskOut.entries()) {
1025
- const retMap = parseGradleProperties(propTaskOut);
1026
- const rootProject = retMap.rootProject;
1027
- if (rootProject) {
1028
- const rootComponent = await buildObjectForGradleModule(
1029
- rootProject,
1030
- retMap.metadata,
1031
- );
1032
- if (!includedBuilds.includes(key)) {
1033
- parentComponent = rootComponent;
1034
- }
1035
- gradleModules.set(key, rootComponent);
1036
- if (!allProjectsAddedPurls.includes(rootComponent["purl"])) {
1037
- allProjects.push(rootComponent);
1038
- rootDependsOn.add(rootComponent["bom-ref"]);
1039
- allProjectsAddedPurls.push(rootComponent["purl"]);
1040
- }
1041
- allProjectsStr = allProjectsStr.concat(retMap.projects);
1027
+ let includedBuilds = [];
1028
+ let allProjectsStr = [];
1029
+ if (readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")) {
1030
+ includedBuilds = readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")
1031
+ .split(",")
1032
+ .map((b) => (!b.startsWith(":") ? `:${b}` : b));
1042
1033
  }
1043
- }
1044
- // Get the sub-project properties and set the root dependencies
1045
- if (allProjectsStr?.length) {
1046
- const modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
1047
- ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
1048
- : [];
1049
- if (modulesToSkip.length) {
1050
- thoughtLog(
1051
- `Good news. I know there are ${allProjectsStr.length} gradle modules at ${gradleRootPath}. I must skip ${modulesToSkip.length} out of these.`,
1052
- );
1034
+ const resolveGradleDistribution = isFeatureEnabled(
1035
+ options,
1036
+ "resolve-gradle-distribution",
1037
+ );
1038
+ const gradleInitArgs = readEnvironmentVariable("GRADLE_INCLUDED_BUILDS")
1039
+ ? []
1040
+ : ["--init-script", GRADLE_INIT_SCRIPT];
1041
+ if (resolveGradleDistribution && gradleInitArgs.length) {
1042
+ gradleInitArgs.push("-PcdxgenResolveDistribution=true");
1053
1043
  }
1054
1044
  parallelPropTaskOut = executeParallelGradleProperties(
1055
1045
  gradleRootPath,
1056
- allProjectsStr.filter((module) => !modulesToSkip.includes(module)),
1046
+ [null].concat(includedBuilds),
1047
+ gradleInitArgs,
1057
1048
  );
1049
+ if (readEnvironmentVariable("GRADLE_INCLUDED_BUILDS") === undefined) {
1050
+ const outputLines = parallelPropTaskOut.split("\n");
1051
+ for (const [_i, line] of outputLines.entries()) {
1052
+ if (
1053
+ line.startsWith("Root project '") ||
1054
+ line.startsWith("Project '")
1055
+ ) {
1056
+ break;
1057
+ }
1058
+ if (line.startsWith("<CDXGEN:includedBuild>")) {
1059
+ const includedBuild = line.split(">");
1060
+ if (!includedBuilds.includes(includedBuild[1].trim())) {
1061
+ includedBuilds.push(includedBuild[1].trim());
1062
+ }
1063
+ }
1064
+ }
1065
+ if (includedBuilds.length > 0) {
1066
+ thoughtLog(
1067
+ `Wait, this gradle project uses composite builds. I must carefully process these ${includedBuilds.length} projects, in addition to the root.`,
1068
+ );
1069
+ if (DEBUG_MODE) {
1070
+ console.log(
1071
+ `Composite builds: ${includedBuilds.join(" ").trim()}.`,
1072
+ );
1073
+ }
1074
+ parallelPropTaskOut = parallelPropTaskOut.concat(
1075
+ "\n",
1076
+ executeParallelGradleProperties(gradleRootPath, includedBuilds),
1077
+ );
1078
+ }
1079
+ }
1058
1080
  const splitPropTaskOut = splitOutputByGradleProjects(
1059
1081
  parallelPropTaskOut,
1060
1082
  ["properties"],
1061
1083
  );
1062
-
1063
- for (const subProject of allProjectsStr) {
1064
- const retMap = parseGradleProperties(
1065
- splitPropTaskOut.get(subProject),
1066
- subProject,
1067
- );
1068
- const rootSubProject = retMap.rootProject;
1069
- if (rootSubProject) {
1070
- const rootSubProjectObj = await buildObjectForGradleModule(
1071
- rootSubProject === "root" ? subProject : rootSubProject,
1084
+ for (const [key, propTaskOut] of splitPropTaskOut.entries()) {
1085
+ const retMap = parseGradleProperties(propTaskOut);
1086
+ const rootProject = retMap.rootProject;
1087
+ if (rootProject) {
1088
+ const rootComponent = await buildObjectForGradleModule(
1089
+ rootProject,
1072
1090
  retMap.metadata,
1073
1091
  );
1074
- if (!allProjectsAddedPurls.includes(rootSubProjectObj["purl"])) {
1075
- allProjects.push(rootSubProjectObj);
1076
- rootDependsOn.add(rootSubProjectObj["bom-ref"]);
1077
- allProjectsAddedPurls.push(rootSubProjectObj["purl"]);
1092
+ if (!includedBuilds.includes(key)) {
1093
+ parentComponent = rootComponent;
1094
+ }
1095
+ gradleModules.set(key, rootComponent);
1096
+ if (!allProjectsAddedPurls.includes(rootComponent["purl"])) {
1097
+ allProjects.push(rootComponent);
1098
+ rootDependsOn.add(rootComponent["bom-ref"]);
1099
+ allProjectsAddedPurls.push(rootComponent["purl"]);
1078
1100
  }
1079
- gradleModules.set(subProject, rootSubProjectObj);
1101
+ allProjectsStr = allProjectsStr.concat(retMap.projects);
1080
1102
  }
1081
1103
  }
1082
- // Bug #317 fix
1083
- parentComponent.components = allProjects.flatMap((s) => {
1084
- delete s.qualifiers;
1085
- delete s.evidence;
1086
- return s;
1087
- });
1088
- dependencies.push({
1089
- ref: parentComponent["bom-ref"],
1090
- dependsOn: [...rootDependsOn].sort(),
1091
- });
1092
- }
1093
- }
1094
- if (
1095
- gradleFiles?.length &&
1096
- options.installDeps &&
1097
- isPackageManagerAllowed(
1098
- "gradle",
1099
- ["maven", "bazel", "sbt", "mill"],
1100
- options,
1101
- )
1102
- ) {
1103
- allProjects.push(parentComponent);
1104
- const gradleCmd = getGradleCommand(gradleRootPath, null);
1105
- const gradleDepTask = readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1106
- ? readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1107
- : "dependencies";
1108
-
1109
- const gradleSubCommands = [];
1110
- let modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
1111
- ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
1112
- : [];
1113
- if (readEnvironmentVariable("GRADLE_SKIP_MODULE_DEPENDENCIES")) {
1114
- modulesToSkip = modulesToSkip.concat(
1115
- readEnvironmentVariable("GRADLE_SKIP_MODULE_DEPENDENCIES").split(","),
1116
- );
1117
- }
1118
- if (!modulesToSkip.includes("root")) {
1119
- gradleSubCommands.push(gradleDepTask);
1120
- }
1121
- for (const [key, sp] of gradleModules) {
1122
- //create single command for dependencies tasks on all subprojects
1123
- if (sp.purl !== parentComponent.purl && !modulesToSkip.includes(key)) {
1124
- gradleSubCommands.push(`${key}:${gradleDepTask}`);
1125
- }
1126
- }
1127
- const gradleArguments = buildGradleCommandArguments(
1128
- readEnvironmentVariable("GRADLE_ARGS")
1129
- ? readEnvironmentVariable("GRADLE_ARGS").split(" ")
1130
- : [],
1131
- gradleSubCommands,
1132
- readEnvironmentVariable("GRADLE_ARGS_DEPENDENCIES")
1133
- ? readEnvironmentVariable("GRADLE_ARGS_DEPENDENCIES").split(" ")
1134
- : [],
1135
- gradleCmd.length,
1136
- );
1137
- const allOutputs = [];
1138
- for (const gradleArg of gradleArguments) {
1139
- if (DEBUG_MODE) {
1140
- console.log(
1141
- `Executing ${gradleCmd} with arguments ${gradleArg.join(" ").substring(0, 150)}... in ${gradleRootPath}`,
1142
- );
1143
- }
1144
- thoughtLog(
1145
- `Let's invoke '${basename(gradleCmd)}' with the arguments '${gradleArg.join(" ").substring(0, 100)} ...'.`,
1146
- );
1147
- const finalGradleArg = gradleArg.includes("--info")
1148
- ? gradleArg
1149
- : gradleArg.concat(["--info"]);
1150
- const sresult = safeSpawnSync(gradleCmd, finalGradleArg, {
1151
- cwd: gradleRootPath,
1152
- shell: isWin,
1153
- });
1154
- if (sresult.status !== 0 || sresult.error) {
1155
- if (options.failOnError || DEBUG_MODE) {
1156
- console.error(sresult.stdout, sresult.stderr);
1104
+ // Get the sub-project properties and set the root dependencies
1105
+ if (allProjectsStr?.length) {
1106
+ const modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
1107
+ ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
1108
+ : [];
1109
+ if (modulesToSkip.length) {
1110
+ thoughtLog(
1111
+ `Good news. I know there are ${allProjectsStr.length} gradle modules at ${gradleRootPath}. I must skip ${modulesToSkip.length} out of these.`,
1112
+ );
1157
1113
  }
1158
- options.failOnError && process.exit(1);
1159
- }
1160
- if (sresult.stdout !== null) {
1161
- allOutputs.push(sresult.stdout);
1162
- }
1163
- }
1164
- const sstdout = allOutputs.join("\n");
1165
- if (sstdout) {
1166
- const cmdOutput = Buffer.from(sstdout).toString();
1167
- const perProjectOutput = splitOutputByGradleProjects(cmdOutput, [
1168
- gradleDepTask,
1169
- ]);
1170
- for (const key of gradleModules.keys()) {
1171
- const parsedList = await parseGradleDep(
1172
- perProjectOutput.has(key) ? perProjectOutput.get(key) : "",
1173
- key,
1174
- gradleModules,
1114
+ parallelPropTaskOut = executeParallelGradleProperties(
1175
1115
  gradleRootPath,
1116
+ allProjectsStr.filter((module) => !modulesToSkip.includes(module)),
1117
+ );
1118
+ const splitPropTaskOut = splitOutputByGradleProjects(
1119
+ parallelPropTaskOut,
1120
+ ["properties"],
1176
1121
  );
1177
- const dlist = parsedList.pkgList;
1178
- if (parsedList?.dependenciesList?.length) {
1179
- dependencies = mergeDependencies(
1180
- dependencies,
1181
- parsedList.dependenciesList,
1182
- parentComponent,
1122
+
1123
+ for (const subProject of allProjectsStr) {
1124
+ const retMap = parseGradleProperties(
1125
+ splitPropTaskOut.get(subProject),
1126
+ subProject,
1183
1127
  );
1184
- } else {
1185
- if (dlist?.length) {
1186
- thoughtLog(
1187
- `Hmm, I didn't find any dependencies after executing '${basename(gradleCmd)}' for the project ${key}. However, I did get ${dlist.length} components, which is confusing.`,
1128
+ const rootSubProject = retMap.rootProject;
1129
+ if (rootSubProject) {
1130
+ const rootSubProjectObj = await buildObjectForGradleModule(
1131
+ rootSubProject === "root" ? subProject : rootSubProject,
1132
+ retMap.metadata,
1188
1133
  );
1134
+ if (!allProjectsAddedPurls.includes(rootSubProjectObj["purl"])) {
1135
+ allProjects.push(rootSubProjectObj);
1136
+ rootDependsOn.add(rootSubProjectObj["bom-ref"]);
1137
+ allProjectsAddedPurls.push(rootSubProjectObj["purl"]);
1138
+ }
1139
+ gradleModules.set(subProject, rootSubProjectObj);
1189
1140
  }
1190
1141
  }
1191
- if (dlist?.length) {
1192
- pkgList = pkgList.concat(dlist);
1193
- }
1142
+ // Bug #317 fix
1143
+ parentComponent.components = allProjects.flatMap((s) => {
1144
+ delete s.qualifiers;
1145
+ delete s.evidence;
1146
+ return s;
1147
+ });
1148
+ dependencies.push({
1149
+ ref: parentComponent["bom-ref"],
1150
+ dependsOn: [...rootDependsOn].sort(),
1151
+ });
1194
1152
  }
1195
1153
  }
1196
- if (pkgList.length) {
1197
- const fileToUrlMap = parseGradleInfoLogsForUrls(sstdout);
1198
- // When the `resolve-gradle-distribution` feature flag is enabled, the init script
1199
- // probes each repository for the actual artifact and emits the resolved URL keyed
1200
- // by `group:name:version`. This is more accurate than guessing the first repository.
1201
- const resolvedDistributions =
1202
- parseGradleResolvedDistributions(parallelPropTaskOut);
1154
+ if (
1155
+ gradleFiles?.length &&
1156
+ options.installDeps &&
1157
+ isPackageManagerAllowed(
1158
+ "gradle",
1159
+ ["maven", "bazel", "sbt", "mill"],
1160
+ options,
1161
+ )
1162
+ ) {
1163
+ allProjects.push(parentComponent);
1164
+ const gradleCmd = getGradleCommand(gradleRootPath, null);
1165
+ const gradleDepTask = readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1166
+ ? readEnvironmentVariable("GRADLE_DEPENDENCY_TASK")
1167
+ : "dependencies";
1203
1168
 
1204
- for (const p of pkgList) {
1205
- if (!p.externalReferences) {
1206
- p.externalReferences = [];
1169
+ const gradleSubCommands = [];
1170
+ let modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
1171
+ ? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
1172
+ : [];
1173
+ if (readEnvironmentVariable("GRADLE_SKIP_MODULE_DEPENDENCIES")) {
1174
+ modulesToSkip = modulesToSkip.concat(
1175
+ readEnvironmentVariable("GRADLE_SKIP_MODULE_DEPENDENCIES").split(","),
1176
+ );
1177
+ }
1178
+ if (!modulesToSkip.includes("root")) {
1179
+ gradleSubCommands.push(gradleDepTask);
1180
+ }
1181
+ for (const [key, sp] of gradleModules) {
1182
+ //create single command for dependencies tasks on all subprojects
1183
+ if (sp.purl !== parentComponent.purl && !modulesToSkip.includes(key)) {
1184
+ gradleSubCommands.push(`${key}:${gradleDepTask}`);
1207
1185
  }
1208
- if (!p.externalReferences.find((r) => r.type === "distribution")) {
1209
- const groupPath = p.group.replace(/\./g, "/");
1210
- const matchPath = `${groupPath}/${p.name}/${p.version}/`;
1211
- let matchedUrl = Object.values(fileToUrlMap).find((url) =>
1212
- url.includes(matchPath),
1186
+ }
1187
+ const gradleArguments = buildGradleCommandArguments(
1188
+ readEnvironmentVariable("GRADLE_ARGS")
1189
+ ? readEnvironmentVariable("GRADLE_ARGS").split(" ")
1190
+ : [],
1191
+ gradleSubCommands,
1192
+ readEnvironmentVariable("GRADLE_ARGS_DEPENDENCIES")
1193
+ ? readEnvironmentVariable("GRADLE_ARGS_DEPENDENCIES").split(" ")
1194
+ : [],
1195
+ gradleCmd.length,
1196
+ );
1197
+ const allOutputs = [];
1198
+ for (const gradleArg of gradleArguments) {
1199
+ if (DEBUG_MODE) {
1200
+ console.log(
1201
+ `Executing ${gradleCmd} with arguments ${gradleArg.join(" ").substring(0, 150)}... in ${gradleRootPath}`,
1213
1202
  );
1214
- if (!matchedUrl) {
1215
- matchedUrl =
1216
- resolvedDistributions[`${p.group}:${p.name}:${p.version}`];
1217
- }
1218
- if (matchedUrl) {
1219
- p.externalReferences.push({
1220
- type: "distribution",
1221
- url: matchedUrl,
1222
- });
1203
+ }
1204
+ thoughtLog(
1205
+ `Let's invoke '${basename(gradleCmd)}' with the arguments '${gradleArg.join(" ").substring(0, 100)} ...'.`,
1206
+ );
1207
+ const finalGradleArg = gradleArg.includes("--info")
1208
+ ? gradleArg
1209
+ : gradleArg.concat(["--info"]);
1210
+ const sresult = safeSpawnSync(gradleCmd, finalGradleArg, {
1211
+ cwd: gradleRootPath,
1212
+ shell: isWin,
1213
+ });
1214
+ if (sresult.status !== 0 || sresult.error) {
1215
+ if (options.failOnError || DEBUG_MODE) {
1216
+ console.error(sresult.stdout, sresult.stderr);
1223
1217
  }
1218
+ recordGradleInvocationFailure(sresult, {
1219
+ command: `${gradleCmd} ${finalGradleArg.join(" ")}`,
1220
+ });
1221
+ deferFailOnError(options, {
1222
+ ecosystem: "java",
1223
+ tool: "gradle",
1224
+ exitCode:
1225
+ typeof sresult.status === "number" ? sresult.status : undefined,
1226
+ command: `${gradleCmd} ${finalGradleArg.join(" ")}`,
1227
+ detail: "the gradle dependencies task failed",
1228
+ });
1229
+ }
1230
+ if (sresult.stdout !== null) {
1231
+ allOutputs.push(sresult.stdout);
1224
1232
  }
1225
1233
  }
1226
- if (parentComponent.components?.length) {
1227
- for (const subProj of parentComponent.components) {
1228
- pkgList = pkgList.filter(
1229
- (pkg) =>
1230
- pkg["bom-ref"] !== subProj["bom-ref"] &&
1231
- pkg["bom-ref"] !== parentComponent["bom-ref"],
1234
+ const sstdout = allOutputs.join("\n");
1235
+ if (sstdout) {
1236
+ const cmdOutput = Buffer.from(sstdout).toString();
1237
+ const perProjectOutput = splitOutputByGradleProjects(cmdOutput, [
1238
+ gradleDepTask,
1239
+ ]);
1240
+ for (const key of gradleModules.keys()) {
1241
+ const parsedList = await parseGradleDep(
1242
+ perProjectOutput.has(key) ? perProjectOutput.get(key) : "",
1243
+ key,
1244
+ gradleModules,
1245
+ gradleRootPath,
1232
1246
  );
1247
+ const dlist = parsedList.pkgList;
1248
+ if (parsedList?.dependenciesList?.length) {
1249
+ dependencies = mergeDependencies(
1250
+ dependencies,
1251
+ parsedList.dependenciesList,
1252
+ parentComponent,
1253
+ );
1254
+ } else {
1255
+ if (dlist?.length) {
1256
+ thoughtLog(
1257
+ `Hmm, I didn't find any dependencies after executing '${basename(gradleCmd)}' for the project ${key}. However, I did get ${dlist.length} components, which is confusing.`,
1258
+ );
1259
+ }
1260
+ }
1261
+ if (dlist?.length) {
1262
+ pkgList = pkgList.concat(dlist);
1263
+ }
1233
1264
  }
1234
1265
  }
1235
- thoughtLog(
1236
- `Obtained ${pkgList.length} components by executing the '${basename(gradleCmd)}' command.`,
1237
- );
1238
- if (DEBUG_MODE) {
1239
- console.log("Obtained", pkgList.length, "from this gradle project.");
1240
- }
1241
- } else {
1242
- thoughtLog(
1243
- "**GRADLE:** SBOM is incomplete. I recommend troubleshooting the issue to improve the BOM precision.",
1244
- );
1245
- if (!DEBUG_MODE) {
1246
- console.log(
1247
- "No packages found. Set the environment variable 'CDXGEN_DEBUG_MODE=debug' to troubleshoot any gradle related errors.",
1266
+ if (pkgList.length) {
1267
+ const fileToUrlMap = parseGradleInfoLogsForUrls(sstdout);
1268
+ // When the `resolve-gradle-distribution` feature flag is enabled, the init script
1269
+ // probes each repository for the actual artifact and emits the resolved URL keyed
1270
+ // by `group:name:version`. This is more accurate than guessing the first repository.
1271
+ const resolvedDistributions =
1272
+ parseGradleResolvedDistributions(parallelPropTaskOut);
1273
+
1274
+ for (const p of pkgList) {
1275
+ if (!p.externalReferences) {
1276
+ p.externalReferences = [];
1277
+ }
1278
+ if (!p.externalReferences.find((r) => r.type === "distribution")) {
1279
+ const groupPath = p.group.replace(/\./g, "/");
1280
+ const matchPath = `${groupPath}/${p.name}/${p.version}/`;
1281
+ let matchedUrl = Object.values(fileToUrlMap).find((url) =>
1282
+ url.includes(matchPath),
1283
+ );
1284
+ if (!matchedUrl) {
1285
+ matchedUrl =
1286
+ resolvedDistributions[`${p.group}:${p.name}:${p.version}`];
1287
+ }
1288
+ if (matchedUrl) {
1289
+ p.externalReferences.push({
1290
+ type: "distribution",
1291
+ url: matchedUrl,
1292
+ });
1293
+ }
1294
+ }
1295
+ }
1296
+ if (parentComponent.components?.length) {
1297
+ for (const subProj of parentComponent.components) {
1298
+ pkgList = pkgList.filter(
1299
+ (pkg) =>
1300
+ pkg["bom-ref"] !== subProj["bom-ref"] &&
1301
+ pkg["bom-ref"] !== parentComponent["bom-ref"],
1302
+ );
1303
+ }
1304
+ }
1305
+ thoughtLog(
1306
+ `Obtained ${pkgList.length} components by executing the '${basename(gradleCmd)}' command.`,
1248
1307
  );
1308
+ if (DEBUG_MODE) {
1309
+ console.log("Obtained", pkgList.length, "from this gradle project.");
1310
+ }
1311
+ } else if (hasGradleInvocationFailure()) {
1312
+ // The invocation failure is already ranked, and an empty
1313
+ // configuration is what that failure looks like from here: reporting
1314
+ // both would rank two repairs for one broken toolchain.
1315
+ thoughtLog(
1316
+ "**GRADLE:** No configuration resolved, which the recorded invocation failure already explains.",
1317
+ );
1318
+ } else {
1319
+ recordDegradation("jvm.gradle.unresolved-config", {
1320
+ ecosystem: "java",
1321
+ tool: "gradle",
1322
+ impact: "transitive-deps",
1323
+ command: `${basename(gradleCmd)} ${gradleDepTask}`,
1324
+ detail:
1325
+ "Gradle did not resolve any dependency configuration, so the gradle components carry no resolved dependencies.",
1326
+ });
1327
+ thoughtLog(
1328
+ "**GRADLE:** SBOM is incomplete. I recommend troubleshooting the issue to improve the BOM precision.",
1329
+ );
1330
+ if (!DEBUG_MODE) {
1331
+ console.log(
1332
+ "No packages found. Set the environment variable 'CDXGEN_DEBUG_MODE=debug' to troubleshoot any gradle related errors.",
1333
+ );
1334
+ }
1335
+ deferFailOnError(options, {
1336
+ ecosystem: "java",
1337
+ tool: "gradle",
1338
+ detail: "gradle resolved no dependency configuration",
1339
+ });
1249
1340
  }
1250
- options.failOnError && process.exit(1);
1251
- }
1252
- if (
1253
- (!readEnvironmentVariable("GRADLE_STOP_DAEMON") &&
1254
- (!readEnvironmentVariable("GRADLE_USE_DAEMON") ||
1255
- ["true", "1"].includes(
1256
- readEnvironmentVariable("GRADLE_USE_DAEMON"),
1257
- ))) ||
1258
- ["true", "1"].includes(readEnvironmentVariable("GRADLE_STOP_DAEMON"))
1259
- ) {
1260
- if (DEBUG_MODE) {
1261
- console.log("Stopping gradle daemon...");
1341
+ if (
1342
+ (!readEnvironmentVariable("GRADLE_STOP_DAEMON") &&
1343
+ (!readEnvironmentVariable("GRADLE_USE_DAEMON") ||
1344
+ ["true", "1"].includes(
1345
+ readEnvironmentVariable("GRADLE_USE_DAEMON"),
1346
+ ))) ||
1347
+ ["true", "1"].includes(readEnvironmentVariable("GRADLE_STOP_DAEMON"))
1348
+ ) {
1349
+ if (DEBUG_MODE) {
1350
+ console.log("Stopping gradle daemon...");
1351
+ }
1352
+ const sresult = safeSpawnSync(gradleCmd, ["--stop"], {
1353
+ cwd: gradleRootPath,
1354
+ shell: isWin,
1355
+ });
1356
+ if (sresult.status !== 0 || sresult.error) {
1357
+ if (options.failOnError || DEBUG_MODE) {
1358
+ console.error(sresult.stdout, sresult.stderr);
1359
+ }
1360
+ deferFailOnError(options, {
1361
+ ecosystem: "java",
1362
+ tool: "gradle",
1363
+ exitCode:
1364
+ typeof sresult.status === "number" ? sresult.status : undefined,
1365
+ detail: "stopping the gradle daemon failed",
1366
+ });
1367
+ }
1262
1368
  }
1263
- const sresult = safeSpawnSync(gradleCmd, ["--stop"], {
1264
- cwd: gradleRootPath,
1265
- shell: isWin,
1266
- });
1267
- if (sresult.status !== 0 || sresult.error) {
1268
- if (options.failOnError || DEBUG_MODE) {
1269
- console.error(sresult.stdout, sresult.stderr);
1369
+ // Should we attempt to resolve class names
1370
+ if (options.resolveClass || options.deep) {
1371
+ const tmpjarNSMapping = await collectJarNS(GRADLE_CACHE_DIR);
1372
+ if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
1373
+ jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
1270
1374
  }
1271
- options.failOnError && process.exit(1);
1272
1375
  }
1273
- }
1274
- // Should we attempt to resolve class names
1275
- if (options.resolveClass || options.deep) {
1276
- const tmpjarNSMapping = await collectJarNS(GRADLE_CACHE_DIR);
1277
- if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
1278
- jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
1376
+
1377
+ // A version catalog declares coordinates without resolving them, so it
1378
+ // is used only when Gradle produced no dependency information at all.
1379
+ // The emitted components carry cdx:gradle:catalog=true to mark them as
1380
+ // declared rather than resolved.
1381
+ if (!pkgList.length) {
1382
+ const catalogFiles = getAllFiles(
1383
+ path,
1384
+ `${options.multiProject ? "**/" : ""}gradle/libs.versions.toml`,
1385
+ options,
1386
+ );
1387
+ for (const catalogFile of catalogFiles) {
1388
+ if (DEBUG_MODE) {
1389
+ console.log(`Parsing ${catalogFile}`);
1390
+ }
1391
+ const dlist = parseGradleVersionCatalog(catalogFile);
1392
+ if (dlist?.length) {
1393
+ pkgList = pkgList.concat(dlist);
1394
+ }
1395
+ }
1279
1396
  }
1280
1397
  }
1281
- }
1282
1398
 
1283
- // Bazel
1284
- // Look for the BUILD file only in the root directory
1285
- // NOTE: This can match BUILD files used by perl, so could lead to errors in some projects
1286
- const bazelFiles = getAllFiles(
1287
- path,
1288
- `${options.multiProject ? "**/" : ""}{WORKSPACE{,.bazel},MODULE.bazel}`,
1289
- options,
1290
- );
1291
- if (
1292
- bazelFiles?.length &&
1293
- !hasAnyProjectType(["docker", "oci", "container", "os"], options, false) &&
1294
- isPackageManagerAllowed(
1295
- "bazel",
1296
- ["maven", "gradle", "sbt", "mill"],
1399
+ // Bazel
1400
+ // Look for the BUILD file only in the root directory
1401
+ // NOTE: This can match BUILD files used by perl, so could lead to errors in some projects
1402
+ const bazelFiles = getAllFiles(
1403
+ path,
1404
+ `${options.multiProject ? "**/" : ""}{WORKSPACE{,.bazel},MODULE.bazel}`,
1297
1405
  options,
1298
- )
1299
- ) {
1300
- let BAZEL_CMD = "bazel";
1301
- const bazelHome = readEnvironmentVariable("BAZEL_HOME");
1302
- if (bazelHome) {
1303
- BAZEL_CMD = join(bazelHome, "bin", "bazel");
1304
- }
1305
-
1306
- // bzlmod: parse MODULE.bazel and MODULE.bazel.lock for BCR modules and
1307
- // ecosystem dependencies (Maven, etc.). This works from the manifest and
1308
- // lock alone, so BCR/Maven packages are extracted even when the bazel
1309
- // toolchain is absent. Resolved third-party deps map to their true
1310
- // ecosystem purl (pkg:maven, ...); only BCR modules use pkg:bazel.
1311
- const moduleBazelFiles = bazelFiles.filter(
1312
- (f) => basename(f) === "MODULE.bazel",
1313
1406
  );
1314
- for (const moduleFile of moduleBazelFiles) {
1315
- const baseDir = dirname(moduleFile);
1316
- const moduleResult = parseModuleBazel(moduleFile);
1317
- if (moduleResult.pkgList?.length) {
1318
- pkgList = pkgList.concat(moduleResult.pkgList);
1319
- }
1320
- if (moduleResult.parentComponent?.name) {
1321
- parentComponent = moduleResult.parentComponent;
1407
+ if (
1408
+ bazelFiles?.length &&
1409
+ !hasAnyProjectType(
1410
+ ["docker", "oci", "container", "os"],
1411
+ options,
1412
+ false,
1413
+ ) &&
1414
+ isPackageManagerAllowed(
1415
+ "bazel",
1416
+ ["maven", "gradle", "sbt", "mill"],
1417
+ options,
1418
+ )
1419
+ ) {
1420
+ let BAZEL_CMD = "bazel";
1421
+ const bazelHome = readEnvironmentVariable("BAZEL_HOME");
1422
+ if (bazelHome) {
1423
+ BAZEL_CMD = join(bazelHome, "bin", "bazel");
1322
1424
  }
1323
- const lockFile = join(baseDir, "MODULE.bazel.lock");
1324
- if (safeExistsSync(lockFile)) {
1325
- const lockResult = parseModuleBazelLock(lockFile);
1326
- if (lockResult.pkgList?.length) {
1327
- pkgList = pkgList.concat(lockResult.pkgList);
1425
+
1426
+ // bzlmod: parse MODULE.bazel and MODULE.bazel.lock for BCR modules and
1427
+ // ecosystem dependencies (Maven, etc.). This works from the manifest and
1428
+ // lock alone, so BCR/Maven packages are extracted even when the bazel
1429
+ // toolchain is absent. Resolved third-party deps map to their true
1430
+ // ecosystem purl (pkg:maven, ...); only BCR modules use pkg:bazel.
1431
+ const moduleBazelFiles = bazelFiles.filter(
1432
+ (f) => basename(f) === "MODULE.bazel",
1433
+ );
1434
+ for (const moduleFile of moduleBazelFiles) {
1435
+ const baseDir = dirname(moduleFile);
1436
+ const moduleResult = parseModuleBazel(moduleFile);
1437
+ if (moduleResult.pkgList?.length) {
1438
+ pkgList = pkgList.concat(moduleResult.pkgList);
1328
1439
  }
1329
- if (lockResult.dependencies?.length) {
1330
- dependencies = mergeDependencies(
1331
- dependencies,
1332
- lockResult.dependencies,
1333
- );
1440
+ if (moduleResult.parentComponent?.name) {
1441
+ parentComponent = moduleResult.parentComponent;
1442
+ }
1443
+ const lockFile = join(baseDir, "MODULE.bazel.lock");
1444
+ if (safeExistsSync(lockFile)) {
1445
+ const lockResult = parseModuleBazelLock(lockFile);
1446
+ if (lockResult.pkgList?.length) {
1447
+ pkgList = pkgList.concat(lockResult.pkgList);
1448
+ }
1449
+ if (lockResult.dependencies?.length) {
1450
+ dependencies = mergeDependencies(
1451
+ dependencies,
1452
+ lockResult.dependencies,
1453
+ );
1454
+ }
1334
1455
  }
1335
1456
  }
1336
- }
1337
1457
 
1338
- // Querying bazel means building the project first, which is the most
1339
- // expensive thing cdxgen can do and needs network access. Like every other
1340
- // package-manager invocation it therefore runs only when dependency
1341
- // installation is permitted; `--lifecycle pre-build` and secure mode both
1342
- // turn it off, leaving the manifest and lock as the inventory source.
1343
- for (const f of options.installDeps ? bazelFiles : []) {
1344
- const basePath = dirname(f);
1345
- // Invoke bazel build first
1346
- const bazelTarget = readEnvironmentVariable("BAZEL_TARGET") || "//...";
1347
- let bArgs = [
1348
- ...(readEnvironmentVariable("BAZEL_ARGS")?.split(" ") || []),
1349
- "build",
1350
- bazelTarget,
1351
- ];
1352
- // Automatically load any bazelrc file
1353
- if (
1354
- !readEnvironmentVariable("BAZEL_ARGS") &&
1355
- safeExistsSync(join(basePath, ".bazelrc"))
1356
- ) {
1357
- bArgs = ["--bazelrc=.bazelrc", "build", bazelTarget];
1358
- }
1359
- console.log("Executing", BAZEL_CMD, "in", basePath);
1360
- let result = safeSpawnSync(BAZEL_CMD, bArgs, {
1361
- cwd: basePath,
1362
- shell: isWin,
1363
- });
1364
- if (result.status !== 0 || result.error) {
1365
- if (result.stderr) {
1366
- console.error(result.stdout, result.stderr);
1367
- }
1368
- console.log(
1369
- "1. Check if bazel is installed and available in PATH.\n2. Try building your app with bazel prior to invoking cdxgen",
1370
- );
1371
- options.failOnError && process.exit(1);
1372
- } else {
1373
- const target = readEnvironmentVariable("BAZEL_TARGET") || "//...";
1374
- let query = [
1458
+ // Querying bazel means building the project first, which is the most
1459
+ // expensive thing cdxgen can do and needs network access. Like every other
1460
+ // package-manager invocation it therefore runs only when dependency
1461
+ // installation is permitted; `--lifecycle pre-build` and secure mode both
1462
+ // turn it off, leaving the manifest and lock as the inventory source.
1463
+ for (const f of options.installDeps ? bazelFiles : []) {
1464
+ const basePath = dirname(f);
1465
+ // Invoke bazel build first
1466
+ const bazelTarget = readEnvironmentVariable("BAZEL_TARGET") || "//...";
1467
+ let bArgs = [
1375
1468
  ...(readEnvironmentVariable("BAZEL_ARGS")?.split(" ") || []),
1469
+ "build",
1470
+ bazelTarget,
1376
1471
  ];
1377
- let bazelParser;
1378
1472
  // Automatically load any bazelrc file
1379
1473
  if (
1380
1474
  !readEnvironmentVariable("BAZEL_ARGS") &&
1381
1475
  safeExistsSync(join(basePath, ".bazelrc"))
1382
1476
  ) {
1383
- query = ["--bazelrc=.bazelrc"];
1384
- }
1385
- if (
1386
- ["true", "1"].includes(
1387
- readEnvironmentVariable("BAZEL_USE_ACTION_GRAPH"),
1388
- )
1389
- ) {
1390
- query = query.concat(["query", `deps(${target})`, "--output=label"]);
1391
- bazelParser = parseBazelActionGraph;
1392
- } else {
1393
- query = query.concat([
1394
- "aquery",
1395
- "--output=textproto",
1396
- "--skyframe_state",
1397
- ]);
1398
- bazelParser = parseBazelSkyframe;
1477
+ bArgs = ["--bazelrc=.bazelrc", "build", bazelTarget];
1399
1478
  }
1400
- console.log("Executing", BAZEL_CMD, `${query.join(" ")} in`, basePath);
1401
- result = safeSpawnSync(BAZEL_CMD, query, {
1479
+ console.log("Executing", BAZEL_CMD, "in", basePath);
1480
+ let result = safeSpawnSync(BAZEL_CMD, bArgs, {
1402
1481
  cwd: basePath,
1482
+ shell: isWin,
1403
1483
  });
1404
1484
  if (result.status !== 0 || result.error) {
1405
- console.error(result.stdout, result.stderr);
1406
- options.failOnError && process.exit(1);
1407
- }
1408
- const stdout = result.stdout;
1409
- if (stdout) {
1410
- const cmdOutput = Buffer.from(stdout).toString();
1411
- const dlist = bazelParser(cmdOutput);
1412
- if (dlist?.length) {
1413
- pkgList = pkgList.concat(dlist);
1414
- } else {
1415
- console.log(
1416
- "No packages were detected.\n1. Build your project using bazel build command before running cdxgen\n2. Try running the bazel aquery command manually to see if skyframe state can be retrieved.",
1417
- );
1418
- console.log(
1419
- "If your project requires a different query, please file a bug at cyclonedx/cdxgen repo!",
1420
- );
1421
- options.failOnError && process.exit(1);
1485
+ if (result.stderr) {
1486
+ console.error(result.stdout, result.stderr);
1422
1487
  }
1488
+ console.log(
1489
+ "1. Check if bazel is installed and available in PATH.\n2. Try building your app with bazel prior to invoking cdxgen",
1490
+ );
1491
+ deferFailOnError(options, {
1492
+ ecosystem: "java",
1493
+ tool: "bazel",
1494
+ exitCode:
1495
+ typeof result.status === "number" ? result.status : undefined,
1496
+ detail: "the bazel build failed",
1497
+ });
1423
1498
  } else {
1424
- console.log("Bazel unexpectedly didn't produce any output");
1425
- options.failOnError && process.exit(1);
1499
+ const target = readEnvironmentVariable("BAZEL_TARGET") || "//...";
1500
+ let query = [
1501
+ ...(readEnvironmentVariable("BAZEL_ARGS")?.split(" ") || []),
1502
+ ];
1503
+ let bazelParser;
1504
+ // Automatically load any bazelrc file
1505
+ if (
1506
+ !readEnvironmentVariable("BAZEL_ARGS") &&
1507
+ safeExistsSync(join(basePath, ".bazelrc"))
1508
+ ) {
1509
+ query = ["--bazelrc=.bazelrc"];
1510
+ }
1511
+ if (
1512
+ ["true", "1"].includes(
1513
+ readEnvironmentVariable("BAZEL_USE_ACTION_GRAPH"),
1514
+ )
1515
+ ) {
1516
+ query = query.concat([
1517
+ "query",
1518
+ `deps(${target})`,
1519
+ "--output=label",
1520
+ ]);
1521
+ bazelParser = parseBazelActionGraph;
1522
+ } else {
1523
+ query = query.concat([
1524
+ "aquery",
1525
+ "--output=textproto",
1526
+ "--skyframe_state",
1527
+ ]);
1528
+ bazelParser = parseBazelSkyframe;
1529
+ }
1530
+ console.log(
1531
+ "Executing",
1532
+ BAZEL_CMD,
1533
+ `${query.join(" ")} in`,
1534
+ basePath,
1535
+ );
1536
+ result = safeSpawnSync(BAZEL_CMD, query, {
1537
+ cwd: basePath,
1538
+ });
1539
+ if (result.status !== 0 || result.error) {
1540
+ console.error(result.stdout, result.stderr);
1541
+ deferFailOnError(options, {
1542
+ ecosystem: "java",
1543
+ tool: "bazel",
1544
+ exitCode:
1545
+ typeof result.status === "number" ? result.status : undefined,
1546
+ detail: "the bazel graph query failed",
1547
+ });
1548
+ }
1549
+ const stdout = result.stdout;
1550
+ if (stdout) {
1551
+ const cmdOutput = Buffer.from(stdout).toString();
1552
+ const dlist = bazelParser(cmdOutput);
1553
+ if (dlist?.length) {
1554
+ pkgList = pkgList.concat(dlist);
1555
+ } else {
1556
+ console.log(
1557
+ "No packages were detected.\n1. Build your project using bazel build command before running cdxgen\n2. Try running the bazel aquery command manually to see if skyframe state can be retrieved.",
1558
+ );
1559
+ console.log(
1560
+ "If your project requires a different query, please file a bug at cyclonedx/cdxgen repo!",
1561
+ );
1562
+ deferFailOnError(options, {
1563
+ ecosystem: "java",
1564
+ tool: "bazel",
1565
+ detail: "the bazel graph query produced no packages",
1566
+ });
1567
+ }
1568
+ } else {
1569
+ console.log("Bazel unexpectedly didn't produce any output");
1570
+ deferFailOnError(options, {
1571
+ ecosystem: "java",
1572
+ tool: "bazel",
1573
+ detail: "the bazel graph query unexpectedly produced no output",
1574
+ });
1575
+ }
1426
1576
  }
1427
1577
  }
1428
1578
  }
1429
- }
1430
-
1431
- // scala sbt
1432
- // Identify sbt projects via its `project` directory:
1433
- // - all SBT project _should_ define build.properties file with sbt version info
1434
- // - SBT projects _typically_ have some configs/plugins defined in .sbt files
1435
- // - SBT projects that are still on 0.13.x, can still use the old approach,
1436
- // where configs are defined via Scala files
1437
- // Detecting one of those should be enough to determine an SBT project.
1438
- let sbtProjectFiles = getAllFiles(
1439
- path,
1440
- `${
1441
- options.multiProject ? "**/" : ""
1442
- }project/{build.properties,*.sbt,*.scala}`,
1443
- options,
1444
- );
1445
-
1446
- let sbtProjects = [];
1447
- for (const i in sbtProjectFiles) {
1448
- // parent dir of sbtProjectFile is the `project` directory
1449
- // parent dir of `project` is the sbt root project directory
1450
- const baseDir = dirname(dirname(sbtProjectFiles[i]));
1451
- sbtProjects = sbtProjects.concat(baseDir);
1452
- }
1453
1579
 
1454
- // Fallback in case sbt's project directory is non-existent
1455
- if (!sbtProjects.length) {
1456
- sbtProjectFiles = getAllFiles(
1580
+ // scala sbt
1581
+ // Identify sbt projects via its `project` directory:
1582
+ // - all SBT project _should_ define build.properties file with sbt version info
1583
+ // - SBT projects _typically_ have some configs/plugins defined in .sbt files
1584
+ // - SBT projects that are still on 0.13.x, can still use the old approach,
1585
+ // where configs are defined via Scala files
1586
+ // Detecting one of those should be enough to determine an SBT project.
1587
+ let sbtProjectFiles = getAllFiles(
1457
1588
  path,
1458
- `${options.multiProject ? "**/" : ""}*.sbt`,
1589
+ `${
1590
+ options.multiProject ? "**/" : ""
1591
+ }project/{build.properties,*.sbt,*.scala}`,
1459
1592
  options,
1460
1593
  );
1594
+
1595
+ let sbtProjects = [];
1461
1596
  for (const i in sbtProjectFiles) {
1462
- const baseDir = dirname(sbtProjectFiles[i]);
1597
+ // parent dir of sbtProjectFile is the `project` directory
1598
+ // parent dir of `project` is the sbt root project directory
1599
+ const baseDir = dirname(dirname(sbtProjectFiles[i]));
1463
1600
  sbtProjects = sbtProjects.concat(baseDir);
1464
1601
  }
1465
- }
1466
- // eliminate duplicates and ignore project directories
1467
- sbtProjects = [...new Set(sbtProjects)].filter(
1468
- (p) => !p.endsWith(`${sep}project`) && !p.includes(`target${sep}`),
1469
- );
1470
- const sbtLockFiles = getAllFiles(
1471
- path,
1472
- `${options.multiProject ? "**/" : ""}build.sbt.lock`,
1473
- options,
1474
- );
1475
- const tempCacheDir = safeMkdtempSync(join(getTmpDir(), "sbt-cache-"));
1476
- safeMkdirSync(tempCacheDir, { recursive: true });
1477
- if (
1478
- sbtProjects?.length &&
1479
- isPackageManagerAllowed(
1480
- "sbt",
1481
- ["bazel", "maven", "gradle", "mill"],
1602
+
1603
+ // Fallback in case sbt's project directory is non-existent
1604
+ if (!sbtProjects.length) {
1605
+ sbtProjectFiles = getAllFiles(
1606
+ path,
1607
+ `${options.multiProject ? "**/" : ""}*.sbt`,
1608
+ options,
1609
+ );
1610
+ for (const i in sbtProjectFiles) {
1611
+ const baseDir = dirname(sbtProjectFiles[i]);
1612
+ sbtProjects = sbtProjects.concat(baseDir);
1613
+ }
1614
+ }
1615
+ // eliminate duplicates and ignore project directories
1616
+ sbtProjects = [...new Set(sbtProjects)].filter(
1617
+ (p) => !p.endsWith(`${sep}project`) && !p.includes(`target${sep}`),
1618
+ );
1619
+ const sbtLockFiles = getAllFiles(
1620
+ path,
1621
+ `${options.multiProject ? "**/" : ""}build.sbt.lock`,
1482
1622
  options,
1483
- )
1484
- ) {
1485
- // If the project use sbt lock files
1486
- if (sbtLockFiles?.length) {
1487
- for (const f of sbtLockFiles) {
1488
- const dlist = await parseSbtLock(f);
1489
- if (dlist?.length) {
1490
- pkgList = pkgList.concat(dlist);
1623
+ );
1624
+ const tempCacheDir = safeMkdtempSync(join(getTmpDir(), "sbt-cache-"));
1625
+ safeMkdirSync(tempCacheDir, { recursive: true });
1626
+ if (
1627
+ sbtProjects?.length &&
1628
+ isPackageManagerAllowed(
1629
+ "sbt",
1630
+ ["bazel", "maven", "gradle", "mill"],
1631
+ options,
1632
+ )
1633
+ ) {
1634
+ // If the project use sbt lock files
1635
+ if (sbtLockFiles?.length) {
1636
+ for (const f of sbtLockFiles) {
1637
+ const dlist = await parseSbtLock(f);
1638
+ if (dlist?.length) {
1639
+ pkgList = pkgList.concat(dlist);
1640
+ }
1491
1641
  }
1492
- }
1493
- } else {
1494
- const SBT_CMD = readEnvironmentVariable("SBT_CMD") || "sbt";
1495
- let sbtVersion = determineSbtVersion(path);
1496
- // If can't find sbt version at the root of repository then search in
1497
- // sbt project array too because sometimes the project folder isn't at
1498
- // root of repository
1499
- if (sbtVersion == null) {
1500
- for (const i in sbtProjects) {
1501
- sbtVersion = determineSbtVersion(sbtProjects[i]);
1502
- if (sbtVersion != null) {
1503
- break;
1642
+ } else {
1643
+ const SBT_CMD = readEnvironmentVariable("SBT_CMD") || "sbt";
1644
+ let sbtVersion = determineSbtVersion(path);
1645
+ // If can't find sbt version at the root of repository then search in
1646
+ // sbt project array too because sometimes the project folder isn't at
1647
+ // root of repository
1648
+ if (sbtVersion == null) {
1649
+ for (const i in sbtProjects) {
1650
+ sbtVersion = determineSbtVersion(sbtProjects[i]);
1651
+ if (sbtVersion != null) {
1652
+ break;
1653
+ }
1504
1654
  }
1505
1655
  }
1506
- }
1507
- if (DEBUG_MODE) {
1508
- console.log(`Detected sbt version: ${sbtVersion}`);
1509
- }
1510
- // Introduced in 1.2.0 https://www.scala-sbt.org/1.x/docs/sbt-1.2-Release-Notes.html#addPluginSbtFile+command,
1511
- // however working properly for real only since 1.3.4: https://github.com/sbt/sbt/releases/tag/v1.3.4
1512
- const standalonePluginFile =
1513
- sbtVersion != null &&
1514
- gte(sbtVersion, "1.3.4") &&
1515
- lte(sbtVersion, "1.4.0");
1516
- const useSlashSyntax = !sbtVersion || gte(sbtVersion, "1.5.0");
1517
- const isDependencyTreeBuiltIn =
1518
- sbtVersion != null && gte(sbtVersion, "1.4.0");
1519
- const tempDir = safeMkdtempSync(join(getTmpDir(), "cdxsbt-"));
1520
- const tempSbtgDir = safeMkdtempSync(join(getTmpDir(), "cdxsbtg-"));
1521
- safeMkdirSync(tempSbtgDir, { recursive: true });
1522
- // Create temporary plugins file
1523
- const tempSbtPlugins = join(tempSbtgDir, "dep-plugins.sbt");
1524
-
1525
- // Requires a custom version of `sbt-dependency-graph` that
1526
- // supports `--append` for `toFile` subtask.
1527
- let sbtPluginDefinition = `\naddSbtPlugin("io.shiftleft" % "sbt-dependency-graph" % "0.10.0-append-to-file3")\n`;
1528
- if (isDependencyTreeBuiltIn) {
1529
- sbtPluginDefinition = "\naddDependencyTreePlugin\n";
1530
1656
  if (DEBUG_MODE) {
1531
- console.log("Using addDependencyTreePlugin as the custom plugin");
1657
+ console.log(`Detected sbt version: ${sbtVersion}`);
1532
1658
  }
1533
- }
1534
- safeWriteSync(tempSbtPlugins, sbtPluginDefinition);
1535
- let sbtExtraArgs = "";
1536
- const env = { ...process.env };
1537
- // We need to collect the jars from the cache
1538
- if (options.deep) {
1539
- sbtExtraArgs = " updateClassifiers";
1540
- env["COURSIER_CACHE"] = tempCacheDir;
1541
- env["SBT_IVY_HOME"] = tempCacheDir;
1542
- }
1543
- for (const i in sbtProjects) {
1544
- const basePath = sbtProjects[i];
1545
- const dlFile = join(tempDir, `dl-${i}.tmp`);
1546
- let sbtArgs = [];
1547
- let pluginFile = null;
1548
- const subDlFiles = [];
1549
- if (standalonePluginFile) {
1550
- sbtArgs = [
1551
- `-addPluginSbtFile=${tempSbtPlugins}`,
1552
- `"dependencyList::toFile ${dlFile} --force"`,
1553
- ];
1554
- subDlFiles.push(dlFile);
1555
- } else {
1556
- // write to the existing plugins file
1557
- // Discover the real sbt project ids by asking sbt itself, falling
1558
- // back to the regex-based heuristic if that fails. A per-subproject
1559
- // `dependencyTree` fan-out yields a far more complete tree than a
1560
- // single root resolution (which only captures the root project's own
1561
- // libraryDependencies). See https://github.com/cdxgen/cdxgen/issues/4291
1562
- const subprojects = discoverSbtProjectsFromCmd(
1563
- basePath,
1564
- SBT_CMD,
1565
- env,
1566
- );
1567
- if (subprojects.length > 0 && useSlashSyntax) {
1568
- sbtArgs = [
1569
- `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs}`,
1570
- ];
1571
- for (const sp of subprojects) {
1572
- const subDlFile = join(tempDir, `dl-${i}-${sp}.tmp`);
1573
- subDlFiles.push(subDlFile);
1574
- sbtArgs.push(
1575
- `"${sp}/dependencyTree / toFile ${subDlFile} --force"`,
1576
- );
1577
- }
1578
- } else if (useSlashSyntax) {
1659
+ // Introduced in 1.2.0 https://www.scala-sbt.org/1.x/docs/sbt-1.2-Release-Notes.html#addPluginSbtFile+command,
1660
+ // however working properly for real only since 1.3.4: https://github.com/sbt/sbt/releases/tag/v1.3.4
1661
+ const standalonePluginFile =
1662
+ sbtVersion != null &&
1663
+ gte(sbtVersion, "1.3.4") &&
1664
+ lte(sbtVersion, "1.4.0");
1665
+ const useSlashSyntax = !sbtVersion || gte(sbtVersion, "1.5.0");
1666
+ const isDependencyTreeBuiltIn =
1667
+ sbtVersion != null && gte(sbtVersion, "1.4.0");
1668
+ const tempDir = safeMkdtempSync(join(getTmpDir(), "cdxsbt-"));
1669
+ const tempSbtgDir = safeMkdtempSync(join(getTmpDir(), "cdxsbtg-"));
1670
+ safeMkdirSync(tempSbtgDir, { recursive: true });
1671
+ // Create temporary plugins file
1672
+ const tempSbtPlugins = join(tempSbtgDir, "dep-plugins.sbt");
1673
+
1674
+ // Requires a custom version of `sbt-dependency-graph` that
1675
+ // supports `--append` for `toFile` subtask.
1676
+ let sbtPluginDefinition = `\naddSbtPlugin("io.shiftleft" % "sbt-dependency-graph" % "0.10.0-append-to-file3")\n`;
1677
+ if (isDependencyTreeBuiltIn) {
1678
+ sbtPluginDefinition = "\naddDependencyTreePlugin\n";
1679
+ if (DEBUG_MODE) {
1680
+ console.log("Using addDependencyTreePlugin as the custom plugin");
1681
+ }
1682
+ }
1683
+ safeWriteSync(tempSbtPlugins, sbtPluginDefinition);
1684
+ let sbtExtraArgs = "";
1685
+ 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
+ for (const i in sbtProjects) {
1693
+ const basePath = sbtProjects[i];
1694
+ const dlFile = join(tempDir, `dl-${i}.tmp`);
1695
+ let sbtArgs = [];
1696
+ let pluginFile = null;
1697
+ const subDlFiles = [];
1698
+ if (standalonePluginFile) {
1579
1699
  sbtArgs = [
1580
- `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs} "dependencyTree / toFile ${dlFile} --force"`,
1700
+ `-addPluginSbtFile=${tempSbtPlugins}`,
1701
+ `"dependencyList::toFile ${dlFile} --force"`,
1581
1702
  ];
1582
1703
  subDlFiles.push(dlFile);
1583
1704
  } else {
1584
- sbtArgs = [
1585
- `'set asciiGraphWidth in ThisBuild := 800'${sbtExtraArgs} "dependencyTree::toFile ${dlFile} --force"`,
1586
- ];
1587
- subDlFiles.push(dlFile);
1705
+ // write to the existing plugins file
1706
+ // Discover the real sbt project ids by asking sbt itself, falling
1707
+ // back to the regex-based heuristic if that fails. A per-subproject
1708
+ // `dependencyTree` fan-out yields a far more complete tree than a
1709
+ // single root resolution (which only captures the root project's own
1710
+ // libraryDependencies). See https://github.com/cdxgen/cdxgen/issues/4291
1711
+ const subprojects = discoverSbtProjectsFromCmd(
1712
+ basePath,
1713
+ SBT_CMD,
1714
+ env,
1715
+ );
1716
+ if (subprojects.length > 0 && useSlashSyntax) {
1717
+ sbtArgs = [
1718
+ `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs}`,
1719
+ ];
1720
+ for (const sp of subprojects) {
1721
+ const subDlFile = join(tempDir, `dl-${i}-${sp}.tmp`);
1722
+ subDlFiles.push(subDlFile);
1723
+ sbtArgs.push(
1724
+ `"${sp}/dependencyTree / toFile ${subDlFile} --force"`,
1725
+ );
1726
+ }
1727
+ } else if (useSlashSyntax) {
1728
+ sbtArgs = [
1729
+ `'set ThisBuild / asciiGraphWidth := 800'${sbtExtraArgs} "dependencyTree / toFile ${dlFile} --force"`,
1730
+ ];
1731
+ subDlFiles.push(dlFile);
1732
+ } else {
1733
+ sbtArgs = [
1734
+ `'set asciiGraphWidth in ThisBuild := 800'${sbtExtraArgs} "dependencyTree::toFile ${dlFile} --force"`,
1735
+ ];
1736
+ subDlFiles.push(dlFile);
1737
+ }
1738
+ pluginFile = addPlugin(basePath, sbtPluginDefinition);
1588
1739
  }
1589
- pluginFile = addPlugin(basePath, sbtPluginDefinition);
1590
- }
1591
- // Run sbt in non-interactive batch mode so it never blocks waiting for
1592
- // input on stdin (which can otherwise appear as an indefinite hang) and
1593
- // does not emit ANSI colour codes into the dependency tree output.
1594
- sbtArgs = ["-batch", "-no-colors", ...sbtArgs];
1595
- console.log(
1596
- "Executing",
1597
- SBT_CMD,
1598
- sbtArgs.join(" "),
1599
- "in",
1600
- basePath,
1601
- "using plugins",
1602
- tempSbtgDir,
1603
- );
1604
- // Note that the command has to be invoked with `shell: true` to properly execut sbt
1605
- const result = safeSpawnSync(SBT_CMD, sbtArgs, {
1606
- cwd: basePath,
1607
- shell: true,
1608
- env,
1609
- });
1610
- if (result.status !== 0 || result.error) {
1611
- console.error(result.stdout, result.stderr);
1740
+ // Run sbt in non-interactive batch mode so it never blocks waiting for
1741
+ // input on stdin (which can otherwise appear as an indefinite hang) and
1742
+ // does not emit ANSI colour codes into the dependency tree output.
1743
+ sbtArgs = ["-batch", "-no-colors", ...sbtArgs];
1612
1744
  console.log(
1613
- "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.",
1614
- );
1615
- console.log(
1616
- "2. Check if the plugin net.virtual-void:sbt-dependency-graph 0.10.0-RC1 can be used in the environment",
1617
- );
1618
- console.log(
1619
- "3. Consider creating a lockfile using sbt-dependency-lock plugin. See https://github.com/stringbean/sbt-dependency-lock",
1745
+ "Executing",
1746
+ SBT_CMD,
1747
+ sbtArgs.join(" "),
1748
+ "in",
1749
+ basePath,
1750
+ "using plugins",
1751
+ tempSbtgDir,
1620
1752
  );
1621
- options.failOnError && process.exit(1);
1622
- }
1623
- if (!standalonePluginFile) {
1624
- cleanupPlugin(basePath, pluginFile);
1625
- }
1626
- const sbtSubprojectRoots = [];
1627
- for (const subDlFile of subDlFiles) {
1628
- if (safeExistsSync(subDlFile)) {
1629
- const retMap = await parseSbtTree(subDlFile);
1630
- if (retMap.pkgList?.length) {
1631
- const tmpParentComponent = retMap.pkgList.splice(0, 1)[0];
1632
- tmpParentComponent.type = "application";
1633
- pkgList = pkgList.concat(retMap.pkgList);
1634
- pkgList.push(tmpParentComponent);
1635
- sbtSubprojectRoots.push(tmpParentComponent);
1636
- }
1637
- if (retMap.dependenciesList) {
1638
- dependencies = mergeDependencies(
1639
- dependencies,
1640
- retMap.dependenciesList,
1641
- parentComponent,
1642
- );
1643
- }
1644
- } else {
1645
- if (options.failOnError || DEBUG_MODE) {
1646
- console.log(`sbt dependencyList did not yield ${subDlFile}`);
1753
+ // 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,
1758
+ });
1759
+ if (result.status !== 0 || result.error) {
1760
+ console.error(result.stdout, result.stderr);
1761
+ recordDegradation("jvm.sbt.no-lockfile", {
1762
+ ecosystem: "java",
1763
+ tool: "sbt",
1764
+ impact: "versions",
1765
+ command: `${SBT_CMD} ${sbtArgs.join(" ")}`,
1766
+ exitCode:
1767
+ typeof result.status === "number" ? result.status : undefined,
1768
+ detail:
1769
+ "The sbt dependencyTree command did not succeed, so the declared sbt dependencies are the only evidence available.",
1770
+ });
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
+ );
1780
+ deferFailOnError(options, {
1781
+ ecosystem: "java",
1782
+ tool: "sbt",
1783
+ exitCode:
1784
+ typeof result.status === "number" ? result.status : undefined,
1785
+ command: `${SBT_CMD} ${sbtArgs.join(" ")}`,
1786
+ detail: "the sbt dependencyTree command failed",
1787
+ });
1788
+ }
1789
+ if (!standalonePluginFile) {
1790
+ cleanupPlugin(basePath, pluginFile);
1791
+ }
1792
+ const sbtSubprojectRoots = [];
1793
+ for (const subDlFile of subDlFiles) {
1794
+ if (safeExistsSync(subDlFile)) {
1795
+ const retMap = await parseSbtTree(subDlFile);
1796
+ if (retMap.pkgList?.length) {
1797
+ const tmpParentComponent = retMap.pkgList.splice(0, 1)[0];
1798
+ tmpParentComponent.type = "application";
1799
+ pkgList = pkgList.concat(retMap.pkgList);
1800
+ pkgList.push(tmpParentComponent);
1801
+ sbtSubprojectRoots.push(tmpParentComponent);
1802
+ }
1803
+ if (retMap.dependenciesList) {
1804
+ dependencies = mergeDependencies(
1805
+ dependencies,
1806
+ retMap.dependenciesList,
1807
+ parentComponent,
1808
+ );
1809
+ }
1810
+ } else {
1811
+ if (options.failOnError || DEBUG_MODE) {
1812
+ console.log(`sbt dependencyList did not yield ${subDlFile}`);
1813
+ }
1814
+ if (options.failOnError) {
1815
+ deferFailOnError(options, {
1816
+ ecosystem: "java",
1817
+ tool: "sbt",
1818
+ detail: "the sbt dependencyList output was missing",
1819
+ });
1820
+ }
1647
1821
  }
1648
- options.failOnError && process.exit(1);
1822
+ }
1823
+ // Construct a synthetic root component from build.sbt name/org/version
1824
+ // that aggregates all subproject roots as its direct dependencies.
1825
+ const sbtRoot = parseSbtRootProject(basePath);
1826
+ if (sbtRoot && sbtSubprojectRoots.length > 0) {
1827
+ const rootPurl = build({
1828
+ type: "maven",
1829
+ namespace: sbtRoot.group || null,
1830
+ name: sbtRoot.name,
1831
+ version: sbtRoot.version || null,
1832
+ qualifiers: { type: "jar" } || null,
1833
+ });
1834
+ const rootComponent = {
1835
+ group: sbtRoot.group,
1836
+ name: sbtRoot.name,
1837
+ version: sbtRoot.version,
1838
+ type: "application",
1839
+ purl: rootPurl,
1840
+ "bom-ref": decodeURIComponent(rootPurl),
1841
+ };
1842
+ parentComponent = rootComponent;
1843
+ dependencies.push({
1844
+ ref: decodeURIComponent(rootPurl),
1845
+ dependsOn: sbtSubprojectRoots
1846
+ .map((c) => c["bom-ref"])
1847
+ .filter(Boolean),
1848
+ });
1849
+ } else if (sbtSubprojectRoots.length > 0 && !parentComponent) {
1850
+ parentComponent = sbtSubprojectRoots[0];
1649
1851
  }
1650
1852
  }
1651
- // Construct a synthetic root component from build.sbt name/org/version
1652
- // that aggregates all subproject roots as its direct dependencies.
1653
- const sbtRoot = parseSbtRootProject(basePath);
1654
- if (sbtRoot && sbtSubprojectRoots.length > 0) {
1655
- const rootPurl = build({
1656
- type: "maven",
1657
- namespace: sbtRoot.group || null,
1658
- name: sbtRoot.name,
1659
- version: sbtRoot.version || null,
1660
- qualifiers: { type: "jar" } || null,
1661
- });
1662
- const rootComponent = {
1663
- group: sbtRoot.group,
1664
- name: sbtRoot.name,
1665
- version: sbtRoot.version,
1666
- type: "application",
1667
- purl: rootPurl,
1668
- "bom-ref": decodeURIComponent(rootPurl),
1669
- };
1670
- parentComponent = rootComponent;
1671
- dependencies.push({
1672
- ref: decodeURIComponent(rootPurl),
1673
- dependsOn: sbtSubprojectRoots
1674
- .map((c) => c["bom-ref"])
1675
- .filter(Boolean),
1676
- });
1677
- } else if (sbtSubprojectRoots.length > 0 && !parentComponent) {
1678
- parentComponent = sbtSubprojectRoots[0];
1679
- }
1680
- }
1681
1853
 
1682
- // Cleanup
1683
- safeUnlinkSync(tempSbtPlugins);
1684
- } // else
1854
+ // Cleanup
1855
+ safeUnlinkSync(tempSbtPlugins);
1856
+ } // else
1685
1857
 
1686
- if (DEBUG_MODE) {
1687
- console.log(`Found ${pkgList.length} packages`);
1688
- }
1689
- // Should we attempt to resolve class names
1690
- if (options.resolveClass || options.deep) {
1691
- const tmpjarNSMapping = await collectJarNS(tempCacheDir);
1692
- if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
1693
- jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
1858
+ if (DEBUG_MODE) {
1859
+ console.log(`Found ${pkgList.length} packages`);
1694
1860
  }
1695
- // sbt can store jars in the target directory
1696
- const jarNSData = await createJarBom(path, options);
1697
- if (jarNSData?.bomJson?.components) {
1698
- pkgList = pkgList.concat(jarNSData?.bomJson?.components);
1699
- const targetJarNSMapping = {};
1700
- for (const p of jarNSData.bomJson.components) {
1701
- if (!p?.purl || !p?.properties?.length) {
1702
- continue;
1703
- }
1704
- const nsProp = p.properties.filter(
1705
- (prop) => prop.name === "internal:Namespaces",
1706
- );
1707
- if (nsProp.length) {
1708
- targetJarNSMapping[p.purl] = nsProp[0].value;
1861
+ // Should we attempt to resolve class names
1862
+ if (options.resolveClass || options.deep) {
1863
+ const tmpjarNSMapping = await collectJarNS(tempCacheDir);
1864
+ if (tmpjarNSMapping && Object.keys(tmpjarNSMapping).length) {
1865
+ jarNSMapping = { ...jarNSMapping, ...tmpjarNSMapping };
1866
+ }
1867
+ // sbt can store jars in the target directory
1868
+ const jarNSData = await createJarBom(path, options);
1869
+ if (jarNSData?.bomJson?.components) {
1870
+ pkgList = pkgList.concat(jarNSData?.bomJson?.components);
1871
+ const targetJarNSMapping = {};
1872
+ for (const p of jarNSData.bomJson.components) {
1873
+ if (!p?.purl || !p?.properties?.length) {
1874
+ continue;
1875
+ }
1876
+ const nsProp = p.properties.filter(
1877
+ (prop) => prop.name === "internal:Namespaces",
1878
+ );
1879
+ if (nsProp.length) {
1880
+ targetJarNSMapping[p.purl] = nsProp[0].value;
1881
+ }
1709
1882
  }
1883
+ jarNSMapping = { ...jarNSMapping, ...targetJarNSMapping };
1710
1884
  }
1711
- jarNSMapping = { ...jarNSMapping, ...targetJarNSMapping };
1712
1885
  }
1713
1886
  }
1714
- }
1715
- if (tempCacheDir?.startsWith(getTmpDir())) {
1716
- safeRmSync(tempCacheDir, {
1717
- recursive: true,
1718
- force: true,
1719
- });
1720
- }
1721
-
1722
- if (
1723
- millFiles?.length &&
1724
- isPackageManagerAllowed(
1725
- "mill",
1726
- ["bazel", "sbt", "gradle", "maven"],
1727
- options,
1728
- )
1729
- ) {
1730
- const millRootPath = dirname(millFiles[0]);
1731
- parentComponent = createDefaultParentComponent(
1732
- millRootPath,
1733
- "maven",
1734
- options,
1735
- );
1736
- const millCmd = getMillCommand(millRootPath);
1737
- const millCommonArgs = [
1738
- "--color",
1739
- "false",
1740
- "--disable-callgraph",
1741
- "--disable-prompt",
1742
- "--keep-going",
1743
- "--silent",
1744
- ];
1745
- if (!["true", "1"].includes(readEnvironmentVariable("MILL_USE_SERVER"))) {
1746
- millCommonArgs.unshift("--no-server");
1747
- }
1748
- const millArgs = [...millCommonArgs, "__.ivyDepsTree"];
1749
- if (DEBUG_MODE) {
1750
- console.log("Executing", millCmd, "in", millRootPath);
1751
- }
1752
- let sresult = safeSpawnSync(millCmd, millArgs, {
1753
- cwd: millRootPath,
1754
- shell: isWin,
1755
- maxBuffer: MAX_BUFFER * 10,
1756
- });
1757
- if (sresult.status !== 0 || sresult.error) {
1758
- if (options.failOnError || DEBUG_MODE) {
1759
- console.error(sresult.stdout, sresult.stderr);
1760
- }
1761
- options.failOnError && process.exit(1);
1887
+ if (tempCacheDir?.startsWith(getTmpDir())) {
1888
+ safeRmSync(tempCacheDir, {
1889
+ recursive: true,
1890
+ force: true,
1891
+ });
1762
1892
  }
1763
- const millResolveArgs = [...millCommonArgs, "resolve", "__.ivyDepsTree"];
1764
- if (DEBUG_MODE) {
1765
- console.log(
1766
- "Executing",
1767
- millCmd,
1768
- millResolveArgs.join(" "),
1769
- "in",
1893
+
1894
+ if (
1895
+ millFiles?.length &&
1896
+ isPackageManagerAllowed(
1897
+ "mill",
1898
+ ["bazel", "sbt", "gradle", "maven"],
1899
+ options,
1900
+ )
1901
+ ) {
1902
+ const millRootPath = dirname(millFiles[0]);
1903
+ parentComponent = createDefaultParentComponent(
1770
1904
  millRootPath,
1905
+ "maven",
1906
+ options,
1771
1907
  );
1772
- }
1773
- sresult = safeSpawnSync(millCmd, millResolveArgs, {
1774
- cwd: millRootPath,
1775
- shell: isWin,
1776
- });
1777
- if (sresult.status !== 0 || sresult.error) {
1778
- if (options.failOnError || DEBUG_MODE) {
1779
- console.error(sresult.stdout, sresult.stderr);
1780
- }
1781
- options.failOnError && process.exit(1);
1782
- }
1783
- const sstdout = sresult.stdout;
1784
- if (sstdout) {
1785
- parentComponent.components = [];
1786
- const modules = sstdout
1787
- .trim()
1788
- .split("\n")
1789
- .map((a) => a.substring(0, a.lastIndexOf(".")))
1790
- .filter((a) =>
1791
- ["true", "1"].includes(readEnvironmentVariable("MILL_EXCLUDE_TEST"))
1792
- ? !a.endsWith(".test")
1793
- : true,
1794
- );
1795
- const moduleBomRefs = [];
1796
- const packages = new Map();
1797
- const relations = new Map();
1798
- relations.set(parentComponent["bom-ref"], []);
1799
- for (const module of modules) {
1800
- moduleBomRefs.push(
1801
- parseMillDependency(module, packages, relations, millRootPath),
1802
- );
1908
+ const millCmd = getMillCommand(millRootPath);
1909
+ const millCommonArgs = [
1910
+ "--color",
1911
+ "false",
1912
+ "--disable-callgraph",
1913
+ "--disable-prompt",
1914
+ "--keep-going",
1915
+ "--silent",
1916
+ ];
1917
+ if (!["true", "1"].includes(readEnvironmentVariable("MILL_USE_SERVER"))) {
1918
+ millCommonArgs.unshift("--no-server");
1803
1919
  }
1804
- for (const module of moduleBomRefs) {
1805
- parentComponent.components.push(packages.get(module));
1806
- relations.get(parentComponent["bom-ref"]).push(module);
1807
- packages.delete(module);
1920
+ const millArgs = [...millCommonArgs, "__.ivyDepsTree"];
1921
+ if (DEBUG_MODE) {
1922
+ console.log("Executing", millCmd, "in", millRootPath);
1808
1923
  }
1809
- const newDependencies = [];
1810
- for (const [ref, dependsOn] of relations.entries()) {
1811
- newDependencies.push({
1812
- ref,
1813
- dependsOn,
1924
+ let sresult = safeSpawnSync(millCmd, millArgs, {
1925
+ cwd: millRootPath,
1926
+ shell: isWin,
1927
+ maxBuffer: MAX_BUFFER * 10,
1928
+ });
1929
+ if (sresult.status !== 0 || sresult.error) {
1930
+ if (options.failOnError || DEBUG_MODE) {
1931
+ console.error(sresult.stdout, sresult.stderr);
1932
+ }
1933
+ deferFailOnError(options, {
1934
+ ecosystem: "java",
1935
+ tool: "mill",
1936
+ exitCode:
1937
+ typeof sresult.status === "number" ? sresult.status : undefined,
1938
+ detail: "the mill ivyDepsTree command failed",
1814
1939
  });
1815
1940
  }
1941
+ const millResolveArgs = [...millCommonArgs, "resolve", "__.ivyDepsTree"];
1816
1942
  if (DEBUG_MODE) {
1817
1943
  console.log(
1818
- `Obtained ${packages.size} components and ${relations.size} dependencies from mill.`,
1944
+ "Executing",
1945
+ millCmd,
1946
+ millResolveArgs.join(" "),
1947
+ "in",
1948
+ millRootPath,
1819
1949
  );
1820
1950
  }
1821
- pkgList = pkgList.concat(...packages.values());
1822
- dependencies = mergeDependencies(
1823
- dependencies,
1824
- newDependencies,
1825
- parentComponent,
1826
- );
1827
- }
1828
- if (
1829
- (!readEnvironmentVariable("MILL_SHUTDOWN_SERVER") &&
1830
- ["true", "1"].includes(readEnvironmentVariable("MILL_USE_SERVER"))) ||
1831
- ["true", "1"].includes(readEnvironmentVariable("MILL_SHUTDOWN_SERVER"))
1832
- ) {
1833
- if (DEBUG_MODE) {
1834
- console.log("Shutting down mill server...");
1835
- }
1836
- const sresult = safeSpawnSync(millCmd, ["shutdown"], {
1951
+ sresult = safeSpawnSync(millCmd, millResolveArgs, {
1837
1952
  cwd: millRootPath,
1838
1953
  shell: isWin,
1839
1954
  });
@@ -1841,11 +1956,97 @@ export async function createJavaBom(path, options) {
1841
1956
  if (options.failOnError || DEBUG_MODE) {
1842
1957
  console.error(sresult.stdout, sresult.stderr);
1843
1958
  }
1844
- options.failOnError && process.exit(1);
1959
+ deferFailOnError(options, {
1960
+ ecosystem: "java",
1961
+ tool: "mill",
1962
+ exitCode:
1963
+ typeof sresult.status === "number" ? sresult.status : undefined,
1964
+ detail: "the mill resolve command failed",
1965
+ });
1966
+ }
1967
+ const sstdout = sresult.stdout;
1968
+ if (sstdout) {
1969
+ parentComponent.components = [];
1970
+ const modules = sstdout
1971
+ .trim()
1972
+ .split("\n")
1973
+ .map((a) => a.substring(0, a.lastIndexOf(".")))
1974
+ .filter((a) =>
1975
+ ["true", "1"].includes(readEnvironmentVariable("MILL_EXCLUDE_TEST"))
1976
+ ? !a.endsWith(".test")
1977
+ : true,
1978
+ );
1979
+ const moduleBomRefs = [];
1980
+ const packages = new Map();
1981
+ const relations = new Map();
1982
+ relations.set(parentComponent["bom-ref"], []);
1983
+ for (const module of modules) {
1984
+ moduleBomRefs.push(
1985
+ parseMillDependency(module, packages, relations, millRootPath),
1986
+ );
1987
+ }
1988
+ for (const module of moduleBomRefs) {
1989
+ parentComponent.components.push(packages.get(module));
1990
+ relations.get(parentComponent["bom-ref"]).push(module);
1991
+ packages.delete(module);
1992
+ }
1993
+ const newDependencies = [];
1994
+ for (const [ref, dependsOn] of relations.entries()) {
1995
+ newDependencies.push({
1996
+ ref,
1997
+ dependsOn,
1998
+ });
1999
+ }
2000
+ if (DEBUG_MODE) {
2001
+ console.log(
2002
+ `Obtained ${packages.size} components and ${relations.size} dependencies from mill.`,
2003
+ );
2004
+ }
2005
+ pkgList = pkgList.concat(...packages.values());
2006
+ dependencies = mergeDependencies(
2007
+ dependencies,
2008
+ newDependencies,
2009
+ parentComponent,
2010
+ );
1845
2011
  }
2012
+ if (
2013
+ (!readEnvironmentVariable("MILL_SHUTDOWN_SERVER") &&
2014
+ ["true", "1"].includes(readEnvironmentVariable("MILL_USE_SERVER"))) ||
2015
+ ["true", "1"].includes(readEnvironmentVariable("MILL_SHUTDOWN_SERVER"))
2016
+ ) {
2017
+ if (DEBUG_MODE) {
2018
+ console.log("Shutting down mill server...");
2019
+ }
2020
+ const sresult = safeSpawnSync(millCmd, ["shutdown"], {
2021
+ cwd: millRootPath,
2022
+ shell: isWin,
2023
+ });
2024
+ if (sresult.status !== 0 || sresult.error) {
2025
+ if (options.failOnError || DEBUG_MODE) {
2026
+ console.error(sresult.stdout, sresult.stderr);
2027
+ }
2028
+ deferFailOnError(options, {
2029
+ ecosystem: "java",
2030
+ tool: "mill",
2031
+ exitCode:
2032
+ typeof sresult.status === "number" ? sresult.status : undefined,
2033
+ detail: "shutting down the mill server failed",
2034
+ });
2035
+ }
2036
+ }
2037
+ }
2038
+ } catch (err) {
2039
+ if (!isDeferredFailOnError(err)) {
2040
+ throw err;
1846
2041
  }
2042
+ // The deferred fail-on-error abort kept everything collected so far;
2043
+ // the incomplete-result fallbacks stayed skipped.
2044
+ thoughtLog(
2045
+ "Completing the java BOM with the components collected before the deferred failure.",
2046
+ );
1847
2047
  }
1848
-
2048
+ // Deduplication and metadata belong to whatever was collected, so they run
2049
+ // on the deferred path too.
1849
2050
  pkgList = trimComponents(pkgList);
1850
2051
  pkgList = await getMvnMetadata(pkgList, jarNSMapping, options.deep);
1851
2052
  return buildBomNSData(options, pkgList, "maven", {