Skip to content

Commit dfce9ff

Browse files
authored
Merge pull request #109 from matmannion/populate-dependencies
Populate dependencies from module graph
2 parents 8e24c7d + d95b03b commit dfce9ff

File tree

24 files changed

+1913
-19
lines changed

24 files changed

+1913
-19
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ project/plugins/project/
1111
.history
1212
.cache
1313
.lib/
14+
.bsp/
1415

1516
### Scala template
1617
*.class

README.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,18 @@ The `listBom` command can be used to generate the contents of the BOM without wr
5151

5252
### configuration
5353

54-
| Setting | Type | Default | Description |
55-
|------------------------------|---------|------------------------------------------------------------------------|----------------------------------------------------------------|
56-
| bomFileName | String | `"${artifactId}-${artifactVersion}.bom.xml"` | bom file name |
57-
| bomFormat | String | `json` or `xml`, defaults to the format of bomFileName or else `json` | bom format |
58-
| bomSchemaVersion | String | `"1.6"` | bom schema version |
59-
| includeBomSerialNumber | Boolean | `false` | include serial number in bom |
60-
| includeBomTimestamp | Boolean | `false` | include timestamp in bom |
61-
| includeBomToolVersion | Boolean | `true` | include tool version in bom |
62-
| includeBomHashes | Boolean | `true` | include artifact hashes in bom |
63-
| enableBomSha3Hashes | Boolean | `true` | enable the generation of sha3 hashes (not available on java 8) |
64-
| includeBomExternalReferences | Boolean | `true` | include external references in bom |
54+
| Setting | Type | Default | Description |
55+
|------------------------------|---------|------------------------------------------------------------------------|-----------------------------------------------------------------|
56+
| bomFileName | String | `"${artifactId}-${artifactVersion}.bom.xml"` | bom file name |
57+
| bomFormat | String | `json` or `xml`, defaults to the format of bomFileName or else `json` | bom format |
58+
| bomSchemaVersion | String | `"1.6"` | bom schema version |
59+
| includeBomSerialNumber | Boolean | `false` | include serial number in bom |
60+
| includeBomTimestamp | Boolean | `false` | include timestamp in bom |
61+
| includeBomToolVersion | Boolean | `true` | include tool version in bom |
62+
| includeBomHashes | Boolean | `true` | include artifact hashes in bom |
63+
| enableBomSha3Hashes | Boolean | `true` | enable the generation of sha3 hashes (not available on java 8) |
64+
| includeBomExternalReferences | Boolean | `true` | include external references in bom |
65+
| includeBomDependencyTree | Boolean | `true` | include dependency tree in bom (bomSchemaVersion 1.1 and later) |
6566

6667
Sample configuration:
6768

@@ -102,7 +103,7 @@ executed.
102103
[Scripted](https://www.scala-sbt.org/1.x/docs/Testing-sbt-plugins.html) is a tool that allow you to test sbt plugins.
103104
For each test it is necessary to create a specially crafted project. These projects are inside src/sbt-test directory.
104105

105-
Scripted tests are run using `scripted` command.
106+
Scripted tests are run using `scripted` command. Note that these fail on JDK 21 due to the old version of sbt.
106107

107108
### Formatting
108109

src/main/scala/com/github/sbt/sbom/BomExtractor.scala

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,17 @@ package com.github.sbt.sbom
77
import com.github.packageurl.PackageURL
88
import com.github.sbt.sbom.licenses.LicensesArchive
99
import org.cyclonedx.Version
10-
import org.cyclonedx.model.{ Bom, Component, ExternalReference, Hash, License, LicenseChoice, Metadata, Tool }
10+
import org.cyclonedx.model.{
11+
Bom,
12+
Component,
13+
Dependency,
14+
ExternalReference,
15+
Hash,
16+
License,
17+
LicenseChoice,
18+
Metadata,
19+
Tool
20+
}
1121
import org.cyclonedx.util.BomUtils
1222
import sbt._
1323
import sbt.librarymanagement.ModuleReport
@@ -16,7 +26,9 @@ import java.util
1626
import java.util.UUID
1727
import scala.collection.JavaConverters._
1828

19-
class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logger) {
29+
import SbtUpdateReport.ModuleGraph
30+
31+
class BomExtractor(settings: BomExtractorParams, report: UpdateReport, rootModuleID: ModuleID, log: Logger) {
2032
private val serialNumber: String = "urn:uuid:" + UUID.randomUUID.toString
2133

2234
def bom: Bom = {
@@ -28,6 +40,9 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
2840
bom.setMetadata(metadata)
2941
}
3042
bom.setComponents(components.asJava)
43+
if (settings.includeBomDependencyTree && settings.schemaVersion.getVersion >= Version.VERSION_11.getVersion) {
44+
bom.setDependencies(dependencyTree.asJava)
45+
}
3146
bom
3247
}
3348

@@ -114,9 +129,7 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
114129
component.setVersion(version)
115130
component.setModified(false)
116131
component.setType(Component.Type.LIBRARY)
117-
component.setPurl(
118-
new PackageURL(PackageURL.StandardTypes.MAVEN, group, name, version, new util.TreeMap(), null).canonicalize()
119-
)
132+
component.setPurl(purl(group, name, version))
120133
if (settings.schemaVersion.getVersion >= Version.VERSION_11.getVersion) {
121134
// component bom-refs must be unique
122135
component.setBomRef(component.getPurl)
@@ -201,6 +214,46 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
201214
}
202215
}
203216

217+
private def purl(group: String, name: String, version: String): String =
218+
new PackageURL(PackageURL.StandardTypes.MAVEN, group, name, version, new util.TreeMap(), null).canonicalize()
219+
220+
private def dependencyTree: Seq[Dependency] = {
221+
val dependencyTree = configurationsForComponents(settings.configuration).flatMap { configuration =>
222+
dependencyTreeForConfiguration(configuration)
223+
}.distinct // deduplicate dependencies reported by multiple configurations
224+
225+
dependencyTree
226+
}
227+
228+
private def dependencyTreeForConfiguration(configuration: Configuration): Seq[Dependency] = {
229+
report
230+
.configuration(configuration)
231+
.toSeq
232+
.flatMap { configurationReport =>
233+
new DependencyTreeExtractor(configurationReport).dependencyTree
234+
}
235+
}
236+
237+
class DependencyTreeExtractor(configurationReport: ConfigurationReport) {
238+
def dependencyTree: Seq[Dependency] =
239+
moduleGraph.nodes
240+
.sortBy(_.id.idString)
241+
.map { node =>
242+
val bomRef = purl(node.id.organization, node.id.name, node.id.version)
243+
244+
val dependency = new Dependency(bomRef)
245+
246+
val dependsOn = moduleGraph.dependencyMap.getOrElse(node.id, Nil).sortBy(_.id.idString)
247+
dependsOn.foreach { module =>
248+
val bomRef = purl(module.id.organization, module.id.name, module.id.version)
249+
dependency.addDependency(new Dependency(bomRef))
250+
}
251+
252+
dependency
253+
}
254+
255+
private def moduleGraph: ModuleGraph = SbtUpdateReport.fromConfigurationReport(configurationReport, rootModuleID)
256+
}
204257
def logComponent(component: Component): Unit = {
205258
log.info(s""""
206259
|${component.getGroup}" % "${component.getName}" % "${component.getVersion}",

src/main/scala/com/github/sbt/sbom/BomExtractorParams.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ final case class BomExtractorParams(
1616
includeBomHashes: Boolean,
1717
enableBomSha3Hashes: Boolean,
1818
includeBomExternalReferences: Boolean,
19+
includeBomDependencyTree: Boolean,
1920
)

src/main/scala/com/github/sbt/sbom/BomSbtPlugin.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ object BomSbtPlugin extends AutoPlugin {
4747
lazy val includeBomExternalReferences: SettingKey[Boolean] = settingKey[Boolean](
4848
"should the resulting BOM contain external references? default is true"
4949
)
50+
lazy val includeBomDependencyTree: SettingKey[Boolean] = settingKey[Boolean](
51+
"should the resulting BOM contain the dependency tree? default is true"
52+
)
5053
lazy val makeBom: TaskKey[sbt.File] = taskKey[sbt.File]("Generates bom file")
5154
lazy val listBom: TaskKey[String] = taskKey[String]("Returns the bom")
5255
lazy val components: TaskKey[Component] = taskKey[Component]("Returns the bom")
@@ -75,6 +78,7 @@ object BomSbtPlugin extends AutoPlugin {
7578
includeBomHashes := true,
7679
enableBomSha3Hashes := true,
7780
includeBomExternalReferences := true,
81+
includeBomDependencyTree := true,
7882
makeBom := Def.taskDyn(BomSbtSettings.makeBomTask(Classpaths.updateTask.value, Compile)).value,
7983
listBom := Def.taskDyn(BomSbtSettings.listBomTask(Classpaths.updateTask.value, Compile)).value,
8084
Test / makeBom := Def.taskDyn(BomSbtSettings.makeBomTask(Classpaths.updateTask.value, Test)).value,

src/main/scala/com/github/sbt/sbom/BomSbtSettings.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
package com.github.sbt.sbom
66

77
import com.github.sbt.sbom.BomSbtPlugin.autoImport._
8-
import sbt.Keys.{ sLog, target }
8+
import sbt.Keys.{ projectID, sLog, scalaBinaryVersion, scalaVersion, target }
99
import sbt._
1010

1111
object BomSbtSettings {
@@ -20,6 +20,9 @@ object BomSbtSettings {
2020
BomTaskProperties(
2121
report,
2222
currentConfiguration,
23+
CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(
24+
projectID.value
25+
),
2326
sLog.value,
2427
bomSchemaVersion.value,
2528
format,
@@ -29,6 +32,7 @@ object BomSbtSettings {
2932
includeBomHashes.value,
3033
enableBomSha3Hashes.value,
3134
includeBomExternalReferences.value,
35+
includeBomDependencyTree.value,
3236
),
3337
target.value / (currentConfiguration / bomFileName).value
3438
).execute
@@ -45,6 +49,9 @@ object BomSbtSettings {
4549
BomTaskProperties(
4650
report,
4751
currentConfiguration,
52+
CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(
53+
projectID.value
54+
),
4855
sLog.value,
4956
bomSchemaVersion.value,
5057
format,
@@ -54,6 +61,7 @@ object BomSbtSettings {
5461
includeBomHashes.value,
5562
enableBomSha3Hashes.value,
5663
includeBomExternalReferences.value,
64+
includeBomDependencyTree.value,
5765
)
5866
).execute
5967
}

src/main/scala/com/github/sbt/sbom/BomTask.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import scala.collection.JavaConverters._
1818
final case class BomTaskProperties(
1919
report: UpdateReport,
2020
currentConfiguration: Configuration,
21+
rootModuleID: ModuleID,
2122
log: Logger,
2223
schemaVersion: String,
2324
bomFormat: BomFormat,
@@ -27,6 +28,7 @@ final case class BomTaskProperties(
2728
includeBomHashes: Boolean,
2829
enableBomSha3Hashes: Boolean,
2930
includeBomExternalReferences: Boolean,
31+
includeBomDependencyTree: Boolean,
3032
)
3133

3234
abstract class BomTask[T](protected val properties: BomTaskProperties) {
@@ -35,7 +37,7 @@ abstract class BomTask[T](protected val properties: BomTaskProperties) {
3537

3638
protected def getBomText: String = {
3739
val params: BomExtractorParams = extractorParams(currentConfiguration)
38-
val bom: Bom = new BomExtractor(params, report, log).bom
40+
val bom: Bom = new BomExtractor(params, report, rootModuleID, log).bom
3941
val bomText: String = bomFormat match {
4042
case BomFormat.Json => BomGeneratorFactory.createJson(schemaVersion, bom).toJsonString
4143
case BomFormat.Xml => BomGeneratorFactory.createXml(schemaVersion, bom).toXmlString
@@ -81,6 +83,7 @@ abstract class BomTask[T](protected val properties: BomTaskProperties) {
8183
includeBomHashes,
8284
enableBomSha3Hashes,
8385
includeBomExternalReferences,
86+
includeBomDependencyTree,
8487
)
8588

8689
protected def logBomInfo(params: BomExtractorParams, bom: Bom): Unit = {
@@ -93,6 +96,8 @@ abstract class BomTask[T](protected val properties: BomTaskProperties) {
9396

9497
protected def currentConfiguration: Configuration = properties.currentConfiguration
9598

99+
protected def rootModuleID: ModuleID = properties.rootModuleID
100+
96101
protected def log: Logger = properties.log
97102

98103
protected lazy val schemaVersion: Version =
@@ -117,4 +122,6 @@ abstract class BomTask[T](protected val properties: BomTaskProperties) {
117122
protected lazy val enableBomSha3Hashes: Boolean = properties.enableBomSha3Hashes
118123

119124
protected lazy val includeBomExternalReferences: Boolean = properties.includeBomExternalReferences
125+
126+
protected lazy val includeBomDependencyTree: Boolean = properties.includeBomDependencyTree
120127
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// SPDX-FileCopyrightText: 2023, Scala center, 2011 - 2022, Lightbend, Inc., 2008 - 2010, Mark Harrah
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package com.github.sbt.sbom
6+
7+
import sbt.librarymanagement.{ ConfigurationReport, ModuleID, ModuleReport }
8+
import sbt.{ File, OrganizationArtifactReport }
9+
10+
import scala.collection.mutable
11+
12+
/*
13+
* taken from sbt at https://github.com/sbt/sbt/blob/1.10.x/main/src/main/scala/sbt/internal/graph/backend/SbtUpdateReport.scala
14+
*
15+
* Copyright 2023, Scala center
16+
* Copyright 2011 - 2022, Lightbend, Inc.
17+
* Copyright 2008 - 2010, Mark Harrah
18+
* Licensed under Apache License 2.0 (see LICENSE)
19+
*/
20+
object SbtUpdateReport {
21+
case class Module(
22+
id: GraphModuleId,
23+
license: Option[String] = None,
24+
extraInfo: String = "",
25+
evictedByVersion: Option[String] = None,
26+
jarFile: Option[File] = None,
27+
error: Option[String] = None
28+
)
29+
30+
private type Edge = (GraphModuleId, GraphModuleId)
31+
private def Edge(from: GraphModuleId, to: GraphModuleId): Edge = from -> to
32+
33+
case class ModuleGraph(nodes: Seq[Module], edges: Seq[Edge]) {
34+
lazy val modules: Map[GraphModuleId, Module] =
35+
nodes.map(n => (n.id, n)).toMap
36+
37+
def module(id: GraphModuleId): Option[Module] = modules.get(id)
38+
39+
lazy val dependencyMap: Map[GraphModuleId, Seq[Module]] =
40+
createMap(identity)
41+
42+
def createMap(
43+
bindingFor: ((GraphModuleId, GraphModuleId)) => (GraphModuleId, GraphModuleId)
44+
): Map[GraphModuleId, Seq[Module]] = {
45+
val m = new mutable.HashMap[GraphModuleId, mutable.Set[Module]] with mutable.MultiMap[GraphModuleId, Module]
46+
edges.foreach { entry =>
47+
val (f, t) = bindingFor(entry)
48+
module(t).foreach(m.addBinding(f, _))
49+
}
50+
m.toMap.mapValues(_.toSeq.sortBy(_.id.idString)).toMap.withDefaultValue(Nil)
51+
}
52+
53+
def roots: Seq[Module] =
54+
nodes.filter(n => !edges.exists(_._2 == n.id)).sortBy(_.id.idString)
55+
}
56+
57+
case class GraphModuleId(organization: String, name: String, version: String) {
58+
def idString: String = organization + ":" + name + ":" + version
59+
}
60+
object GraphModuleId {
61+
def apply(sbtId: ModuleID): GraphModuleId =
62+
GraphModuleId(sbtId.organization, sbtId.name, sbtId.revision)
63+
}
64+
65+
def fromConfigurationReport(report: ConfigurationReport, rootInfo: ModuleID): ModuleGraph = {
66+
def moduleEdges(orgArt: OrganizationArtifactReport): Seq[(Module, Seq[Edge])] = {
67+
val chosenVersion = orgArt.modules.find(!_.evicted).map(_.module.revision)
68+
orgArt.modules.map(moduleEdge(chosenVersion))
69+
}
70+
71+
def moduleEdge(chosenVersion: Option[String])(report: ModuleReport): (Module, Seq[Edge]) = {
72+
val evictedByVersion = if (report.evicted) chosenVersion else None
73+
val jarFile = report.artifacts
74+
.find(_._1.`type` == "jar")
75+
.orElse(report.artifacts.find(_._1.extension == "jar"))
76+
.map(_._2)
77+
(
78+
Module(
79+
id = GraphModuleId(report.module),
80+
license = report.licenses.headOption.map(_._1),
81+
evictedByVersion = evictedByVersion,
82+
jarFile = jarFile,
83+
error = report.problem
84+
),
85+
report.callers.map(caller => Edge(GraphModuleId(caller.caller), GraphModuleId(report.module)))
86+
)
87+
}
88+
val (nodes, edges) = report.details.flatMap(moduleEdges).unzip
89+
val root = Module(GraphModuleId(rootInfo))
90+
91+
ModuleGraph(root +: nodes, edges.flatten)
92+
}
93+
}

0 commit comments

Comments
 (0)