@cdxgen/cdxgen 13.1.0 → 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 (186) hide show
  1. package/README.md +8 -7
  2. package/bin/cdxgen.js +16 -3
  3. package/bin/evinse.js +2 -0
  4. package/data/crypto-oid.json +88 -12
  5. package/data/remediations.json +23 -0
  6. package/data/rules/ai-bom.yaml +78 -0
  7. package/data/rules/cargo-rust.yaml +132 -0
  8. package/data/rules/mcp-servers.yaml +122 -0
  9. package/lib/audit/targets.js +6 -1
  10. package/lib/cli/bomAssembly.js +12 -0
  11. package/lib/cli/index.js +192 -1
  12. package/lib/cli/jsBom.js +59 -2
  13. package/lib/cli/jvmBom.js +22 -0
  14. package/lib/cli/managedBom.js +152 -0
  15. package/lib/cli/nativeBom.js +798 -1
  16. package/lib/core/activity.js +120 -2
  17. package/lib/core/env.js +39 -1
  18. package/lib/core/userAgent.js +151 -0
  19. package/lib/ecosystems/cargoCache.js +364 -0
  20. package/lib/ecosystems/caxa.js +13 -5
  21. package/lib/ecosystems/cppEvidence.js +40 -2
  22. package/lib/ecosystems/ecosystems.js +241 -91
  23. package/lib/ecosystems/golem.js +11 -1
  24. package/lib/ecosystems/gradleutils.js +143 -2
  25. package/lib/ecosystems/kosi.js +1256 -0
  26. package/lib/ecosystems/parsers-crystal.js +179 -0
  27. package/lib/ecosystems/parsers-dotnet.js +69 -39
  28. package/lib/ecosystems/parsers-elm.js +816 -0
  29. package/lib/ecosystems/parsers-gleam.js +1 -1
  30. package/lib/ecosystems/parsers-julia.js +268 -0
  31. package/lib/ecosystems/parsers-lua.js +218 -0
  32. package/lib/ecosystems/parsers-misc.js +172 -0
  33. package/lib/ecosystems/parsers-nim.js +221 -0
  34. package/lib/ecosystems/parsers-opam.js +180 -0
  35. package/lib/ecosystems/parsers-perl.js +277 -0
  36. package/lib/ecosystems/parsers-python.js +1 -1
  37. package/lib/ecosystems/parsers-r.js +148 -0
  38. package/lib/ecosystems/parsers-rebar.js +411 -0
  39. package/lib/ecosystems/parsers-rust.js +932 -17
  40. package/lib/ecosystems/parsers-spack.js +204 -0
  41. package/lib/ecosystems/parsers-stack.js +264 -0
  42. package/lib/ecosystems/parsers-terraform.js +194 -0
  43. package/lib/ecosystems/rusi.js +126 -6
  44. package/lib/evinser/evinser.js +111 -1
  45. package/lib/helpers/agenticToolutils.js +925 -0
  46. package/lib/inventory/agentFormulationParser.js +57 -4
  47. package/lib/inventory/aiCollector.js +504 -10
  48. package/lib/inventory/aiInventory.js +12 -0
  49. package/lib/inventory/analyzer.js +488 -30
  50. package/lib/inventory/astWorker.js +6 -0
  51. package/lib/inventory/auditCategories.js +8 -0
  52. package/lib/inventory/cbomutils.js +30 -0
  53. package/lib/inventory/cdxrs.js +7 -2
  54. package/lib/inventory/communityAiConfigParser.js +215 -2
  55. package/lib/inventory/dosai.js +77 -0
  56. package/lib/inventory/envcontext.js +9 -6
  57. package/lib/inventory/evidenceUtils.js +13 -0
  58. package/lib/inventory/fetchBatch.js +74 -2
  59. package/lib/inventory/fetchRate.js +74 -11
  60. package/lib/inventory/forgeEnricher.js +3 -2
  61. package/lib/inventory/jvmToolEnv.js +6 -5
  62. package/lib/inventory/mcpConfigParser.js +166 -2
  63. package/lib/inventory/mcpDiscovery.js +11 -0
  64. package/lib/inventory/mcpServerManifest.js +389 -0
  65. package/lib/inventory/mcpbBundle.js +419 -0
  66. package/lib/inventory/plugins.js +3 -0
  67. package/lib/inventory/protobom.js +54 -88
  68. package/lib/inventory/protobomLoader.js +4 -0
  69. package/lib/inventory/rustFormulationParser.js +1 -1
  70. package/lib/inventory/source.js +10 -4
  71. package/lib/inventory/stylePackageRefs.js +41 -0
  72. package/lib/inventory/svelteUtils.js +524 -0
  73. package/lib/parsers/huggingfaceUtils.js +15 -7
  74. package/lib/parsers/onnx.js +294 -0
  75. package/lib/parsers/safetensors.js +114 -0
  76. package/lib/stages/postgen/introspection/markers.js +29 -6
  77. package/lib/stages/postgen/postgen.js +169 -1
  78. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  79. package/package.json +25 -25
  80. package/types/lib/audit/targets.d.ts.map +1 -1
  81. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  82. package/types/lib/cli/index.d.ts +2 -2
  83. package/types/lib/cli/index.d.ts.map +1 -1
  84. package/types/lib/cli/jsBom.d.ts +23 -0
  85. package/types/lib/cli/jsBom.d.ts.map +1 -1
  86. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  87. package/types/lib/cli/managedBom.d.ts +33 -0
  88. package/types/lib/cli/managedBom.d.ts.map +1 -1
  89. package/types/lib/cli/nativeBom.d.ts +88 -8
  90. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  91. package/types/lib/core/activity.d.ts.map +1 -1
  92. package/types/lib/core/env.d.ts +21 -0
  93. package/types/lib/core/env.d.ts.map +1 -1
  94. package/types/lib/core/userAgent.d.ts +56 -0
  95. package/types/lib/core/userAgent.d.ts.map +1 -0
  96. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  97. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  98. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  99. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  100. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  101. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  102. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  103. package/types/lib/ecosystems/gradleutils.d.ts +20 -0
  104. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  105. package/types/lib/ecosystems/kosi.d.ts +73 -0
  106. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  107. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  108. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  109. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  110. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  111. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  112. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  113. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  114. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  115. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  116. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  117. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  118. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  120. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  121. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  122. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  123. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  124. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  125. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  126. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  127. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  128. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  129. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  130. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  131. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  132. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  133. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  134. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  135. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  136. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  137. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  138. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  139. package/types/lib/evinser/evinser.d.ts +29 -0
  140. package/types/lib/evinser/evinser.d.ts.map +1 -1
  141. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  142. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  143. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  144. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  145. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  146. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  147. package/types/lib/inventory/auditCategories.d.ts +4 -0
  148. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  149. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  150. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  151. package/types/lib/inventory/dosai.d.ts +23 -0
  152. package/types/lib/inventory/dosai.d.ts.map +1 -1
  153. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  154. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  155. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  156. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  157. package/types/lib/inventory/fetchRate.d.ts +9 -6
  158. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  159. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  160. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  161. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  162. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  163. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  164. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  165. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  166. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  167. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  168. package/types/lib/inventory/plugins.d.ts.map +1 -1
  169. package/types/lib/inventory/protobom.d.ts +18 -13
  170. package/types/lib/inventory/protobom.d.ts.map +1 -1
  171. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  172. package/types/lib/inventory/source.d.ts.map +1 -1
  173. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  174. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  175. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  176. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  177. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  178. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  179. package/types/lib/parsers/onnx.d.ts +28 -0
  180. package/types/lib/parsers/onnx.d.ts.map +1 -0
  181. package/types/lib/parsers/safetensors.d.ts +22 -0
  182. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  183. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  184. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  185. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  186. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
@@ -12,11 +12,13 @@ import { recordDegradation } from "../core/buildLedger.js";
12
12
  import { deferFailOnError } from "../core/deferredExit.js";
13
13
  import {
14
14
  CARGO_CMD,
15
+ CARGO_TARGET,
15
16
  CLJ_CMD,
16
17
  hasAnyProjectType,
17
18
  LEIN_CMD,
18
19
  SWIFT_CMD,
19
20
  shouldFetchLicense,
21
+ shouldFetchPackageMetadata,
20
22
  } from "../core/env.js";
21
23
  import {
22
24
  checksumFile,
@@ -36,6 +38,14 @@ import {
36
38
  getCratesMetadata,
37
39
  getSwiftPackageMetadata,
38
40
  } from "../ecosystems/ecosystems.js";
41
+ import { parseShardsProject } from "../ecosystems/parsers-crystal.js";
42
+ import {
43
+ createElmCacheReader,
44
+ getElmMetadata,
45
+ isElmProjectFile,
46
+ parseElmProject,
47
+ parseLegacyElmProject,
48
+ } from "../ecosystems/parsers-elm.js";
39
49
  import { parseGleamProject } from "../ecosystems/parsers-gleam.js";
40
50
  import {
41
51
  parseGoListDep,
@@ -46,12 +56,17 @@ import {
46
56
  parseGopkgData,
47
57
  parseGosumData,
48
58
  } from "../ecosystems/parsers-go.js";
59
+ import { parseJuliaProject } from "../ecosystems/parsers-julia.js";
49
60
  import {
50
61
  parseCljDep,
51
62
  parseEdnData,
52
63
  parseLeinDep,
53
64
  parseLeiningenData,
54
65
  } from "../ecosystems/parsers-jvm.js";
66
+ import {
67
+ parseLuaRocksLock,
68
+ parseRockspecFile,
69
+ } from "../ecosystems/parsers-lua.js";
55
70
  import {
56
71
  buildObjectForCocoaPod,
57
72
  executeAlpmList,
@@ -78,12 +93,24 @@ import {
78
93
  parseSwiftResolved,
79
94
  prefetchCocoaPodspecs,
80
95
  } from "../ecosystems/parsers-misc.js";
96
+ import { parseNimProject } from "../ecosystems/parsers-nim.js";
97
+ import { parseOpamLockedFile } from "../ecosystems/parsers-opam.js";
98
+ import { parseRebarLock } from "../ecosystems/parsers-rebar.js";
81
99
  import {
100
+ applyCargoDependencyKindMetadata,
101
+ buildCargoLockDependencyGraph,
102
+ buildCargoNativeLibraryComponents,
103
+ cargoComponentKey,
82
104
  parseCargoData,
83
105
  parseCargoDependencyData,
84
106
  parseCargoManifestDependencyData,
107
+ parseCargoManifestDependencyKinds,
108
+ parseCargoMetadataResolve,
85
109
  parseCargoTomlData,
110
+ rollupCargoDependencyKinds,
86
111
  } from "../ecosystems/parsers-rust.js";
112
+ import { parseSpackLock } from "../ecosystems/parsers-spack.js";
113
+ import { parseStackProject } from "../ecosystems/parsers-stack.js";
87
114
  import { resolveZigGraph } from "../ecosystems/zigResolver.js";
88
115
  import { mergeDependencies, trimComponents } from "../inventory/depsUtils.js";
89
116
  import { convertOSQueryResults } from "../inventory/evidenceUtils.js";
@@ -664,6 +691,223 @@ function cargoAnswersVersion(cwd) {
664
691
  * @param {Object} options Parse options from the cli
665
692
  * @returns {Promise<Object|undefined>} Promise resolving to a BOM object or undefined
666
693
  */
694
+ /**
695
+ * Run `cargo metadata` for a manifest and return its parsed resolve graph.
696
+ *
697
+ * The lock file is the union of every dependency kind and every target triple,
698
+ * so it cannot answer which crates are dev-only or platform-gated. Only
699
+ * `cargo metadata` carries that, per dependency edge, in `resolve.nodes[].deps[].dep_kinds`.
700
+ *
701
+ * @param {string} cargoTomlFile Manifest path
702
+ * @param {string} [filterPlatform] Target triple to resolve the graph for
703
+ * @returns {object|undefined} Parsed `cargo metadata` output
704
+ */
705
+ function runCargoMetadata(cargoTomlFile, filterPlatform) {
706
+ const basePath = dirname(cargoTomlFile);
707
+ const platformArgs = filterPlatform
708
+ ? ["--filter-platform", filterPlatform]
709
+ : [];
710
+ // `--locked` keeps the user's lock file untouched; a workspace that is out of
711
+ // date with its lock file needs the retry without it.
712
+ for (const lockArgs of [["--locked"], []]) {
713
+ const cargoArgs = [
714
+ "metadata",
715
+ "--format-version",
716
+ "1",
717
+ "--manifest-path",
718
+ cargoTomlFile,
719
+ ...platformArgs,
720
+ ...lockArgs,
721
+ ];
722
+ if (DEBUG_MODE) {
723
+ console.log("Executing", CARGO_CMD, cargoArgs.join(" "), "in", basePath);
724
+ }
725
+ const result = safeSpawnSync(CARGO_CMD, cargoArgs, {
726
+ cwd: basePath,
727
+ shell: isWin,
728
+ });
729
+ if (result.status === 0 && !result.error && result.stdout) {
730
+ try {
731
+ return JSON.parse(result.stdout);
732
+ } catch (err) {
733
+ if (DEBUG_MODE) {
734
+ console.log(`Unable to parse the cargo metadata output: ${err}`);
735
+ }
736
+ return undefined;
737
+ }
738
+ }
739
+ if (DEBUG_MODE) {
740
+ console.log(result.error, result.stderr);
741
+ }
742
+ }
743
+ return undefined;
744
+ }
745
+
746
+ /**
747
+ * Annotate cargo components with their dependency kind, platform gate and
748
+ * scope.
749
+ *
750
+ * With `--deep`, the edge kinds come from `cargo metadata` and are exact at
751
+ * every depth. Otherwise they are recovered from the manifests, which is exact
752
+ * for the dependencies a workspace member declares and inherited below them.
753
+ *
754
+ * @param {object[]} pkgList Components to annotate
755
+ * @param {object[]} dependencyTree Lockfile dependency relationships
756
+ * @param {string[]} cargoFiles Manifest paths
757
+ * @param {Map<string, string>} manifestRootKeys Manifest path to root component key
758
+ * @param {object} options CLI options
759
+ */
760
+ function annotateCargoDependencyKinds(
761
+ pkgList,
762
+ dependencyTree,
763
+ cargoFiles,
764
+ manifestRootKeys,
765
+ options,
766
+ ) {
767
+ if (!pkgList?.length) {
768
+ return;
769
+ }
770
+ const allEdges = new Map();
771
+ if (options?.deep) {
772
+ const rollups = new Map();
773
+ const packageInfo = new Map();
774
+ const visitedWorkspaceRoots = new Set();
775
+ // The keys that survive resolution for the requested target triple. The
776
+ // unfiltered graph still supplies the kinds, because a crate's kind does
777
+ // not change with the triple.
778
+ const targetIncludedKeys = CARGO_TARGET ? new Set() : undefined;
779
+ let targetFilterApplied = !CARGO_TARGET;
780
+ let anyMetadata = false;
781
+ let everyMetadata = true;
782
+ for (const cargoTomlFile of cargoFiles) {
783
+ const metadata = runCargoMetadata(cargoTomlFile);
784
+ if (!metadata) {
785
+ everyMetadata = false;
786
+ continue;
787
+ }
788
+ // Every member of a workspace answers with the same resolve graph.
789
+ const workspaceRoot = metadata.workspace_root || cargoTomlFile;
790
+ if (visitedWorkspaceRoots.has(workspaceRoot)) {
791
+ anyMetadata = true;
792
+ continue;
793
+ }
794
+ visitedWorkspaceRoots.add(workspaceRoot);
795
+ anyMetadata = true;
796
+ if (CARGO_TARGET) {
797
+ const targetMetadata = runCargoMetadata(cargoTomlFile, CARGO_TARGET);
798
+ if (targetMetadata) {
799
+ for (const key of parseCargoMetadataResolve(targetMetadata)
800
+ .resolvedKeys) {
801
+ targetIncludedKeys.add(key);
802
+ }
803
+ targetFilterApplied = true;
804
+ } else {
805
+ console.warn(
806
+ `cargo metadata could not resolve the dependency graph for the target ${CARGO_TARGET}. The components are not filtered by target.`,
807
+ );
808
+ }
809
+ }
810
+ const graph = parseCargoMetadataResolve(metadata);
811
+ for (const [key, value] of graph.packageInfo) {
812
+ packageInfo.set(key, value);
813
+ }
814
+ for (const [key, value] of graph.edges) {
815
+ allEdges.set(key, [...(allEdges.get(key) || []), ...value]);
816
+ }
817
+ for (const [key, value] of rollupCargoDependencyKinds(graph)) {
818
+ const existing = rollups.get(key);
819
+ if (!existing) {
820
+ rollups.set(key, value);
821
+ continue;
822
+ }
823
+ // A crate that is a runtime dependency of any workspace in the scan is
824
+ // a runtime dependency of the BOM.
825
+ for (const kind of value.kinds) {
826
+ existing.kinds.add(kind);
827
+ }
828
+ for (const target of value.targets) {
829
+ existing.targets.add(target);
830
+ }
831
+ const precedence = ["root", "runtime", "build", "dev"];
832
+ if (
833
+ precedence.indexOf(value.kind) < precedence.indexOf(existing.kind)
834
+ ) {
835
+ existing.kind = value.kind;
836
+ existing.targetGated = value.targetGated;
837
+ } else if (value.kind === existing.kind && !value.targetGated) {
838
+ existing.targetGated = false;
839
+ }
840
+ }
841
+ }
842
+ if (anyMetadata && rollups.size) {
843
+ applyCargoDependencyKindMetadata(pkgList, rollups, packageInfo, {
844
+ resolveIsComplete: everyMetadata,
845
+ filterPlatform: targetFilterApplied ? CARGO_TARGET : undefined,
846
+ targetIncludedKeys: targetFilterApplied
847
+ ? targetIncludedKeys
848
+ : undefined,
849
+ });
850
+ // A `-sys` crate puts a C library into the artifact that the cargo
851
+ // dependency graph itself cannot describe.
852
+ const nativeLibraries = buildCargoNativeLibraryComponents(
853
+ packageInfo,
854
+ allEdges,
855
+ rollups,
856
+ );
857
+ if (nativeLibraries.components.length) {
858
+ pkgList.push(...nativeLibraries.components);
859
+ if (dependencyTree) {
860
+ dependencyTree.push(...nativeLibraries.dependencies);
861
+ }
862
+ }
863
+ return;
864
+ }
865
+ recordDegradation("rust.cargo-metadata-unavailable", {
866
+ ecosystem: "rust",
867
+ tool: "cargo",
868
+ impact: "components",
869
+ command: `${basename(CARGO_CMD)} metadata`,
870
+ detail:
871
+ "cargo metadata could not be executed, so dependency kinds and platform gates were recovered from the manifests alone.",
872
+ });
873
+ }
874
+ if (!dependencyTree?.length || !manifestRootKeys.size) {
875
+ return;
876
+ }
877
+ const declaredEdgesByRoot = new Map();
878
+ for (const [cargoTomlFile, rootKey] of manifestRootKeys) {
879
+ if (!rootKey) {
880
+ continue;
881
+ }
882
+ const declaredEdges = parseCargoManifestDependencyKinds(cargoTomlFile, {
883
+ includeWorkspaceMembers: !options.multiProject,
884
+ });
885
+ const existing = declaredEdgesByRoot.get(rootKey);
886
+ if (!existing) {
887
+ declaredEdgesByRoot.set(rootKey, declaredEdges);
888
+ continue;
889
+ }
890
+ for (const [name, value] of declaredEdges) {
891
+ const knownEdge = existing.get(name);
892
+ if (!knownEdge) {
893
+ existing.set(name, value);
894
+ continue;
895
+ }
896
+ for (const kind of value.kinds) {
897
+ knownEdge.kinds.add(kind);
898
+ }
899
+ for (const target of value.targets) {
900
+ knownEdge.targets.add(target);
901
+ }
902
+ }
903
+ }
904
+ const graph = buildCargoLockDependencyGraph(
905
+ dependencyTree,
906
+ declaredEdgesByRoot,
907
+ );
908
+ applyCargoDependencyKindMetadata(pkgList, rollupCargoDependencyKinds(graph));
909
+ }
910
+
667
911
  export async function createRustBom(path, options) {
668
912
  let pkgList = [];
669
913
  let parentComponent = {};
@@ -769,6 +1013,7 @@ export async function createRustBom(path, options) {
769
1013
  "No Cargo.lock was found beside the manifest, so components carry the declared dependencies without locked versions or the resolved crate graph.",
770
1014
  });
771
1015
  }
1016
+ const manifestRootKeys = new Map();
772
1017
  for (const f of cargoFiles) {
773
1018
  if (DEBUG_MODE) {
774
1019
  console.log(`Parsing ${f}`);
@@ -777,6 +1022,7 @@ export async function createRustBom(path, options) {
777
1022
  includeWorkspaceMembers: !options.multiProject,
778
1023
  });
779
1024
  if (dlist?.length) {
1025
+ manifestRootKeys.set(f, cargoComponentKey(dlist[0]));
780
1026
  if (!cargoLockMode) {
781
1027
  pkgList = pkgList.concat(dlist);
782
1028
  } else {
@@ -832,6 +1078,15 @@ export async function createRustBom(path, options) {
832
1078
  );
833
1079
  }
834
1080
  }
1081
+ if (cargoLockMode) {
1082
+ annotateCargoDependencyKinds(
1083
+ pkgList,
1084
+ dependencyTree,
1085
+ cargoFiles,
1086
+ manifestRootKeys,
1087
+ options,
1088
+ );
1089
+ }
835
1090
  return buildBomNSData(options, pkgList, "cargo", {
836
1091
  src: path,
837
1092
  filename: cargoLockFiles.join(", "),
@@ -1596,6 +1851,44 @@ export function createClojureBom(path, options) {
1596
1851
  return {};
1597
1852
  }
1598
1853
 
1854
+ /**
1855
+ * Locate the Stack lock files under a path.
1856
+ *
1857
+ * Stack names the lock after the configuration file it completes, so the
1858
+ * default `stack.yaml` produces `stack.yaml.lock` and a project with several
1859
+ * configurations produces one lock per configuration. `stack.lock` is also
1860
+ * accepted because tooling and documentation examples have used that name.
1861
+ *
1862
+ * @param {string} path Project path
1863
+ * @param {Object} options Parse options from the cli
1864
+ * @returns {string[]} Lock file paths, deduplicated
1865
+ */
1866
+ export function findStackLockFiles(path, options) {
1867
+ const prefix = options.multiProject ? "**/" : "";
1868
+ const candidates = new Set([
1869
+ ...getAllFiles(path, `${prefix}*.yaml.lock`, options),
1870
+ ...getAllFiles(path, `${prefix}stack.lock`, options),
1871
+ ]);
1872
+ return [...candidates].filter((lockFile) => {
1873
+ if (/(^|[\\/])stack\.(yaml\.)?lock$/u.test(lockFile)) {
1874
+ return true;
1875
+ }
1876
+ // `*.yaml.lock` is not a name Stack owns, so a custom configuration is
1877
+ // accepted only when the file it completes names a snapshot.
1878
+ const configFile = lockFile.replace(/\.lock$/u, "");
1879
+ if (!safeExistsSync(configFile)) {
1880
+ return false;
1881
+ }
1882
+ try {
1883
+ return /^\s*(snapshot|resolver)\s*:/mu.test(
1884
+ readFileSync(configFile, "utf-8"),
1885
+ );
1886
+ } catch {
1887
+ return false;
1888
+ }
1889
+ });
1890
+ }
1891
+
1599
1892
  /**
1600
1893
  * Function to create bom string for Haskell projects
1601
1894
  *
@@ -1626,12 +1919,44 @@ export function createHaskellBom(path, options) {
1626
1919
  filename: cabalFiles.join(", "),
1627
1920
  });
1628
1921
  }
1922
+ // Stack projects keep the resolved extra-deps in a lock file named after
1923
+ // the configuration file, so `stack.yaml` locks to `stack.yaml.lock`.
1924
+ const stackLockFiles = findStackLockFiles(path, options);
1925
+ if (stackLockFiles.length) {
1926
+ const dependencies = [];
1927
+ let parentComponent = {};
1928
+ for (const stackLockFile of stackLockFiles) {
1929
+ if (DEBUG_MODE) {
1930
+ console.log(`Parsing ${stackLockFile}`);
1931
+ }
1932
+ const stackYamlFile = stackLockFile.replace(/\.lock$/u, "");
1933
+ const result = parseStackProject(
1934
+ stackLockFile,
1935
+ safeExistsSync(stackYamlFile) ? stackYamlFile : undefined,
1936
+ );
1937
+ if (result.pkgList?.length) {
1938
+ pkgList = pkgList.concat(result.pkgList);
1939
+ }
1940
+ if (!Object.keys(parentComponent).length) {
1941
+ parentComponent = result.parentComponent;
1942
+ attachRootEdge(result, parentComponent, dependencies);
1943
+ }
1944
+ }
1945
+ if (pkgList.length) {
1946
+ return buildBomNSData(options, pkgList, "hackage", {
1947
+ src: path,
1948
+ filename: stackLockFiles.join(", "),
1949
+ dependencies,
1950
+ parentComponent,
1951
+ });
1952
+ }
1953
+ }
1629
1954
  recordDegradation("haskell.freeze-missing", {
1630
1955
  ecosystem: "haskell",
1631
1956
  tool: "cabal",
1632
1957
  impact: "components",
1633
1958
  detail:
1634
- "No cabal.project.freeze was found, and the freeze file is the source the parser reads for the package set.",
1959
+ "No cabal.project.freeze or stack.yaml.lock was found; those lock files are the sources the parsers read for the package set.",
1635
1960
  });
1636
1961
  return {};
1637
1962
  }
@@ -2302,3 +2627,475 @@ export async function createGleamBom(path, options) {
2302
2627
 
2303
2628
  return {};
2304
2629
  }
2630
+
2631
+ /**
2632
+ * Function to create bom string for Julia projects.
2633
+ *
2634
+ * The resolved `Manifest.toml` is preferred over the declared `Project.toml`
2635
+ * so the BOM carries pinned versions and the package graph.
2636
+ *
2637
+ * @param {string} path to the project
2638
+ * @param {Object} options Parse options from the cli
2639
+ * @returns {Promise<Object>} BOM object
2640
+ */
2641
+ export async function createJuliaBom(path, options) {
2642
+ let pkgList = [];
2643
+ let dependencies = [];
2644
+ let parentComponent = {};
2645
+
2646
+ const projectTomlFiles = getAllFiles(
2647
+ path,
2648
+ `${options.multiProject ? "**/" : ""}Project.toml`,
2649
+ options,
2650
+ );
2651
+ const manifestOnly = !projectTomlFiles.length;
2652
+
2653
+ for (const projectTomlFile of projectTomlFiles) {
2654
+ if (DEBUG_MODE) {
2655
+ console.log(`Parsing ${projectTomlFile}`);
2656
+ }
2657
+ const baseDir = dirname(projectTomlFile);
2658
+ const manifestTomlFile = join(baseDir, "Manifest.toml");
2659
+ const result = parseJuliaProject(
2660
+ projectTomlFile,
2661
+ safeExistsSync(manifestTomlFile) ? manifestTomlFile : undefined,
2662
+ );
2663
+ collectProjectResult(result);
2664
+ }
2665
+ if (manifestOnly) {
2666
+ for (const manifestTomlFile of getAllFiles(
2667
+ path,
2668
+ `${options.multiProject ? "**/" : ""}Manifest.toml`,
2669
+ options,
2670
+ )) {
2671
+ if (DEBUG_MODE) {
2672
+ console.log(`Parsing ${manifestTomlFile}`);
2673
+ }
2674
+ // Gleam also writes a `manifest.toml`; on case-insensitive filesystems
2675
+ // the lookup above can surface it, so only accept files that parse as
2676
+ // a Julia manifest.
2677
+ const parsed = parseJuliaProject(undefined, manifestTomlFile);
2678
+ if (parsed.pkgList.length) {
2679
+ collectProjectResult(parsed);
2680
+ }
2681
+ }
2682
+ }
2683
+
2684
+ function collectProjectResult(result) {
2685
+ if (result.pkgList?.length) {
2686
+ pkgList = pkgList.concat(result.pkgList);
2687
+ }
2688
+ if (result.dependencies?.length) {
2689
+ dependencies = dependencies.concat(result.dependencies);
2690
+ }
2691
+ if (
2692
+ !Object.keys(parentComponent).length &&
2693
+ Object.keys(result.parentComponent).length
2694
+ ) {
2695
+ parentComponent = result.parentComponent;
2696
+ attachRootEdge(result, parentComponent, dependencies);
2697
+ }
2698
+ }
2699
+
2700
+ if (pkgList.length || Object.keys(parentComponent).length) {
2701
+ return buildBomNSData(options, pkgList, "julia", {
2702
+ src: path,
2703
+ filename: projectTomlFiles.join(", "),
2704
+ dependencies,
2705
+ parentComponent,
2706
+ });
2707
+ }
2708
+ return {};
2709
+ }
2710
+
2711
+ /**
2712
+ * Attach a root dependency edge from the parent component to its direct
2713
+ * dependencies, following the shape used by the Gleam and Nix flows.
2714
+ *
2715
+ * @param {{rootInputs?: string[]}} result Parser result with root inputs
2716
+ * @param {Object} parentComponent Parent component to give a bom-ref
2717
+ * @param {Object[]} dependencies Dependency edge list to append to
2718
+ */
2719
+ function attachRootEdge(result, parentComponent, dependencies) {
2720
+ if (result.rootInputs?.length && parentComponent.name) {
2721
+ const rootRef = `application:${parentComponent.name}:${parentComponent.version || "latest"}`;
2722
+ parentComponent["bom-ref"] = rootRef;
2723
+ dependencies.push({
2724
+ ref: rootRef,
2725
+ dependsOn: [...new Set(result.rootInputs)],
2726
+ });
2727
+ }
2728
+ }
2729
+
2730
+ /**
2731
+ * Function to create bom string for Erlang projects using rebar3.
2732
+ *
2733
+ * @param {string} path to the project
2734
+ * @param {Object} options Parse options from the cli
2735
+ * @returns {Promise<Object>} BOM object
2736
+ */
2737
+ export async function createErlangBom(path, options) {
2738
+ let pkgList = [];
2739
+ const rebarLockFiles = getAllFiles(
2740
+ path,
2741
+ `${options.multiProject ? "**/" : ""}rebar.lock`,
2742
+ options,
2743
+ );
2744
+ for (const rebarLockFile of rebarLockFiles) {
2745
+ if (DEBUG_MODE) {
2746
+ console.log(`Parsing ${rebarLockFile}`);
2747
+ }
2748
+ const result = parseRebarLock(rebarLockFile);
2749
+ if (result.pkgList?.length) {
2750
+ pkgList = pkgList.concat(result.pkgList);
2751
+ }
2752
+ }
2753
+ if (pkgList.length) {
2754
+ return buildBomNSData(options, pkgList, "erlang", {
2755
+ src: path,
2756
+ filename: rebarLockFiles.join(", "),
2757
+ });
2758
+ }
2759
+ return {};
2760
+ }
2761
+
2762
+ /**
2763
+ * Function to create bom string for OCaml projects using opam.
2764
+ *
2765
+ * Resolved `.opam.locked` files are preferred; a plain `.opam` file is used
2766
+ * as a declared fallback with unpinned versions.
2767
+ *
2768
+ * @param {string} path to the project
2769
+ * @param {Object} options Parse options from the cli
2770
+ * @returns {Promise<Object>} BOM object
2771
+ */
2772
+ export async function createOCamlBom(path, options) {
2773
+ let pkgList = [];
2774
+ const opamLockedFiles = getAllFiles(
2775
+ path,
2776
+ `${options.multiProject ? "**/" : ""}*.opam.locked`,
2777
+ options,
2778
+ );
2779
+ const opamFiles = opamLockedFiles.length
2780
+ ? []
2781
+ : getAllFiles(path, `${options.multiProject ? "**/" : ""}*.opam`, options);
2782
+ for (const opamFile of [...opamLockedFiles, ...opamFiles]) {
2783
+ if (DEBUG_MODE) {
2784
+ console.log(`Parsing ${opamFile}`);
2785
+ }
2786
+ const result = parseOpamLockedFile(opamFile);
2787
+ if (result.pkgList?.length) {
2788
+ pkgList = pkgList.concat(result.pkgList);
2789
+ }
2790
+ }
2791
+ if (pkgList.length) {
2792
+ return buildBomNSData(options, pkgList, "ocaml", {
2793
+ src: path,
2794
+ filename: [...opamLockedFiles, ...opamFiles].join(", "),
2795
+ });
2796
+ }
2797
+ return {};
2798
+ }
2799
+
2800
+ /**
2801
+ * Function to create bom string for Elm projects.
2802
+ *
2803
+ * @param {string} path to the project
2804
+ * @param {Object} options Parse options from the cli
2805
+ * @returns {Promise<Object>} BOM object
2806
+ */
2807
+ export async function createElmBom(path, options) {
2808
+ let pkgList = [];
2809
+ let dependencies = [];
2810
+ let parentComponent = {};
2811
+
2812
+ const elmJsonFiles = getAllFiles(
2813
+ path,
2814
+ `${options.multiProject ? "**/" : ""}elm.json`,
2815
+ options,
2816
+ ).filter(isElmProjectFile);
2817
+ // A single reader memoises the cache lookups shared by every manifest.
2818
+ const cacheReader = createElmCacheReader();
2819
+ for (const elmJsonFile of elmJsonFiles) {
2820
+ if (DEBUG_MODE) {
2821
+ console.log(`Parsing ${elmJsonFile}`);
2822
+ }
2823
+ const result = parseElmProject(elmJsonFile, cacheReader);
2824
+ if (result.pkgList?.length) {
2825
+ pkgList = pkgList.concat(result.pkgList);
2826
+ }
2827
+ if (result.dependencies?.length) {
2828
+ dependencies = dependencies.concat(result.dependencies);
2829
+ }
2830
+ if (!Object.keys(parentComponent).length) {
2831
+ parentComponent = result.parentComponent;
2832
+ attachRootEdge(result, parentComponent, dependencies);
2833
+ }
2834
+ }
2835
+ const legacyFiles = getAllFiles(
2836
+ path,
2837
+ `${options.multiProject ? "**/" : ""}elm-package.json`,
2838
+ options,
2839
+ );
2840
+ for (const legacyFile of legacyFiles) {
2841
+ if (DEBUG_MODE) {
2842
+ console.log(`Parsing ${legacyFile}`);
2843
+ }
2844
+ const baseDir = dirname(legacyFile);
2845
+ const exactDepsFile = join(baseDir, "elm-stuff", "exact-dependencies.json");
2846
+ const result = parseLegacyElmProject(
2847
+ legacyFile,
2848
+ safeExistsSync(exactDepsFile) ? exactDepsFile : undefined,
2849
+ );
2850
+ if (result.pkgList?.length) {
2851
+ pkgList = pkgList.concat(result.pkgList);
2852
+ }
2853
+ if (result.dependencies?.length) {
2854
+ dependencies = dependencies.concat(result.dependencies);
2855
+ }
2856
+ if (!Object.keys(parentComponent).length) {
2857
+ parentComponent = result.parentComponent;
2858
+ attachRootEdge(result, parentComponent, dependencies);
2859
+ }
2860
+ }
2861
+ if (pkgList.length && shouldFetchPackageMetadata()) {
2862
+ pkgList = await getElmMetadata(pkgList, dependencies);
2863
+ }
2864
+ if (pkgList.length || Object.keys(parentComponent).length) {
2865
+ return buildBomNSData(options, pkgList, "elm", {
2866
+ src: path,
2867
+ filename: [...elmJsonFiles, ...legacyFiles].join(", "),
2868
+ dependencies,
2869
+ parentComponent,
2870
+ });
2871
+ }
2872
+ return {};
2873
+ }
2874
+
2875
+ /**
2876
+ * Function to create bom string for Crystal projects using shards.
2877
+ *
2878
+ * @param {string} path to the project
2879
+ * @param {Object} options Parse options from the cli
2880
+ * @returns {Promise<Object>} BOM object
2881
+ */
2882
+ export async function createCrystalBom(path, options) {
2883
+ let pkgList = [];
2884
+ const dependencies = [];
2885
+ let parentComponent = {};
2886
+
2887
+ const shardYmlFiles = getAllFiles(
2888
+ path,
2889
+ `${options.multiProject ? "**/" : ""}shard.yml`,
2890
+ options,
2891
+ );
2892
+ for (const shardYmlFile of shardYmlFiles) {
2893
+ if (DEBUG_MODE) {
2894
+ console.log(`Parsing ${shardYmlFile}`);
2895
+ }
2896
+ const baseDir = dirname(shardYmlFile);
2897
+ const shardLockFile = join(baseDir, "shard.lock");
2898
+ const result = parseShardsProject(
2899
+ shardYmlFile,
2900
+ safeExistsSync(shardLockFile) ? shardLockFile : undefined,
2901
+ );
2902
+ if (result.pkgList?.length) {
2903
+ pkgList = pkgList.concat(result.pkgList);
2904
+ }
2905
+ if (!Object.keys(parentComponent).length) {
2906
+ parentComponent = result.parentComponent;
2907
+ attachRootEdge(result, parentComponent, dependencies);
2908
+ }
2909
+ }
2910
+ if (pkgList.length || Object.keys(parentComponent).length) {
2911
+ return buildBomNSData(options, pkgList, "crystal", {
2912
+ src: path,
2913
+ filename: shardYmlFiles.join(", "),
2914
+ dependencies,
2915
+ parentComponent,
2916
+ });
2917
+ }
2918
+ return {};
2919
+ }
2920
+
2921
+ /**
2922
+ * Function to create bom string for Nim projects using nimble.
2923
+ *
2924
+ * @param {string} path to the project
2925
+ * @param {Object} options Parse options from the cli
2926
+ * @returns {Promise<Object>} BOM object
2927
+ */
2928
+ export async function createNimBom(path, options) {
2929
+ let pkgList = [];
2930
+ let dependencies = [];
2931
+ let parentComponent = {};
2932
+
2933
+ const nimbleFiles = getAllFiles(
2934
+ path,
2935
+ `${options.multiProject ? "**/" : ""}*.nimble`,
2936
+ options,
2937
+ );
2938
+ for (const nimbleFile of nimbleFiles) {
2939
+ if (DEBUG_MODE) {
2940
+ console.log(`Parsing ${nimbleFile}`);
2941
+ }
2942
+ const baseDir = dirname(nimbleFile);
2943
+ const nimbleLockFile = join(baseDir, "nimble.lock");
2944
+ const result = parseNimProject(
2945
+ nimbleFile,
2946
+ safeExistsSync(nimbleLockFile) ? nimbleLockFile : undefined,
2947
+ );
2948
+ if (result.pkgList?.length) {
2949
+ pkgList = pkgList.concat(result.pkgList);
2950
+ }
2951
+ if (result.dependencies?.length) {
2952
+ dependencies = dependencies.concat(result.dependencies);
2953
+ }
2954
+ if (!Object.keys(parentComponent).length) {
2955
+ parentComponent = result.parentComponent;
2956
+ attachRootEdge(result, parentComponent, dependencies);
2957
+ }
2958
+ }
2959
+ if (pkgList.length || Object.keys(parentComponent).length) {
2960
+ return buildBomNSData(options, pkgList, "nim", {
2961
+ src: path,
2962
+ filename: nimbleFiles.join(", "),
2963
+ dependencies,
2964
+ parentComponent,
2965
+ });
2966
+ }
2967
+ return {};
2968
+ }
2969
+
2970
+ /**
2971
+ * Function to create bom string for Lua projects using LuaRocks.
2972
+ *
2973
+ * When a `luarocks.lock` is present next to a rockspec it supplies the
2974
+ * resolved versions; otherwise the rockspec's declared dependencies are
2975
+ * inventoried without versions.
2976
+ *
2977
+ * @param {string} path to the project
2978
+ * @param {Object} options Parse options from the cli
2979
+ * @returns {Promise<Object>} BOM object
2980
+ */
2981
+ export async function createLuaBom(path, options) {
2982
+ let pkgList = [];
2983
+ let parentComponent = {};
2984
+ const seenLockFiles = new Set();
2985
+ const rockspecFiles = getAllFiles(
2986
+ path,
2987
+ `${options.multiProject ? "**/" : ""}*.rockspec`,
2988
+ options,
2989
+ );
2990
+ for (const rockspecFile of rockspecFiles) {
2991
+ if (DEBUG_MODE) {
2992
+ console.log(`Parsing ${rockspecFile}`);
2993
+ }
2994
+ const baseDir = dirname(rockspecFile);
2995
+ const lockFile = join(baseDir, "luarocks.lock");
2996
+ // The rockspec names the project and its declared set either way; the
2997
+ // lock, when present, supplies the resolved versions.
2998
+ const declared = parseRockspecFile(rockspecFile);
2999
+ if (!Object.keys(parentComponent).length) {
3000
+ parentComponent = declared.parentComponent;
3001
+ }
3002
+ let resolved;
3003
+ if (safeExistsSync(lockFile)) {
3004
+ seenLockFiles.add(lockFile);
3005
+ resolved = parseLuaRocksLock(lockFile, rockspecFile);
3006
+ }
3007
+ const chosen = resolved?.pkgList?.length ? resolved : declared;
3008
+ if (chosen.pkgList?.length) {
3009
+ pkgList = pkgList.concat(chosen.pkgList);
3010
+ }
3011
+ }
3012
+ // A lock can also stand alone, in a project whose rockspec is generated at
3013
+ // release time rather than committed.
3014
+ for (const lockFile of getAllFiles(
3015
+ path,
3016
+ `${options.multiProject ? "**/" : ""}luarocks.lock`,
3017
+ options,
3018
+ )) {
3019
+ if (seenLockFiles.has(lockFile)) {
3020
+ continue;
3021
+ }
3022
+ if (DEBUG_MODE) {
3023
+ console.log(`Parsing ${lockFile}`);
3024
+ }
3025
+ const result = parseLuaRocksLock(lockFile);
3026
+ if (result.pkgList?.length) {
3027
+ pkgList = pkgList.concat(result.pkgList);
3028
+ }
3029
+ }
3030
+ if (pkgList.length) {
3031
+ return buildBomNSData(options, pkgList, "lua", {
3032
+ src: path,
3033
+ filename: rockspecFiles.join(", "),
3034
+ parentComponent,
3035
+ });
3036
+ }
3037
+ return {};
3038
+ }
3039
+
3040
+ /**
3041
+ * Function to create bom string for Spack environments.
3042
+ *
3043
+ * @param {string} path to the project
3044
+ * @param {Object} options Parse options from the cli
3045
+ * @returns {Promise<Object>} BOM object
3046
+ */
3047
+ export async function createSpackBom(path, options) {
3048
+ let pkgList = [];
3049
+ let dependencies = [];
3050
+ let rootInputs = [];
3051
+ const spackLockFiles = getAllFiles(
3052
+ path,
3053
+ `${options.multiProject ? "**/" : ""}spack.lock`,
3054
+ options,
3055
+ );
3056
+ for (const spackLockFile of spackLockFiles) {
3057
+ if (DEBUG_MODE) {
3058
+ console.log(`Parsing ${spackLockFile}`);
3059
+ }
3060
+ const result = parseSpackLock(spackLockFile);
3061
+ if (result.pkgList?.length) {
3062
+ pkgList = pkgList.concat(result.pkgList);
3063
+ }
3064
+ if (result.dependencies?.length) {
3065
+ dependencies = dependencies.concat(result.dependencies);
3066
+ }
3067
+ if (result.rootInputs?.length) {
3068
+ rootInputs = rootInputs.concat(result.rootInputs);
3069
+ }
3070
+ }
3071
+ if (!pkgList.length) {
3072
+ return {};
3073
+ }
3074
+ // The lock names no project component, so the environment directory is the
3075
+ // name the environment is already known by. Its roots are the specs the
3076
+ // lock records as requested, which is why they are linked explicitly rather
3077
+ // than inferred from which nodes have no incoming edge: a build-only tool
3078
+ // such as gmake also has none, and it is not a root.
3079
+ const envName = basename(resolve(path));
3080
+ const parentComponent = {
3081
+ type: "application",
3082
+ name: envName,
3083
+ "bom-ref": `application:${envName}:latest`,
3084
+ description: `Spack environment: ${envName}`,
3085
+ properties: [
3086
+ { name: "internal:SrcFile", value: spackLockFiles.join(", ") },
3087
+ ],
3088
+ };
3089
+ if (rootInputs.length) {
3090
+ dependencies.push({
3091
+ ref: parentComponent["bom-ref"],
3092
+ dependsOn: [...new Set(rootInputs)].sort(),
3093
+ });
3094
+ }
3095
+ return buildBomNSData(options, pkgList, "spack", {
3096
+ src: path,
3097
+ filename: spackLockFiles.join(", "),
3098
+ dependencies,
3099
+ parentComponent,
3100
+ });
3101
+ }