Skip to content

Commit

Permalink
Merge pull request #109 from matmannion/populate-dependencies
Browse files Browse the repository at this point in the history
Populate dependencies from module graph
  • Loading branch information
raboof authored Jan 13, 2025
2 parents 8e24c7d + d95b03b commit dfce9ff
Show file tree
Hide file tree
Showing 24 changed files with 1,913 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ project/plugins/project/
.history
.cache
.lib/
.bsp/

### Scala template
*.class
Expand Down
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,18 @@ The `listBom` command can be used to generate the contents of the BOM without wr

### configuration

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

Sample configuration:

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

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

### Formatting

Expand Down
63 changes: 58 additions & 5 deletions src/main/scala/com/github/sbt/sbom/BomExtractor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ package com.github.sbt.sbom
import com.github.packageurl.PackageURL
import com.github.sbt.sbom.licenses.LicensesArchive
import org.cyclonedx.Version
import org.cyclonedx.model.{ Bom, Component, ExternalReference, Hash, License, LicenseChoice, Metadata, Tool }
import org.cyclonedx.model.{
Bom,
Component,
Dependency,
ExternalReference,
Hash,
License,
LicenseChoice,
Metadata,
Tool
}
import org.cyclonedx.util.BomUtils
import sbt._
import sbt.librarymanagement.ModuleReport
Expand All @@ -16,7 +26,9 @@ import java.util
import java.util.UUID
import scala.collection.JavaConverters._

class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logger) {
import SbtUpdateReport.ModuleGraph

class BomExtractor(settings: BomExtractorParams, report: UpdateReport, rootModuleID: ModuleID, log: Logger) {
private val serialNumber: String = "urn:uuid:" + UUID.randomUUID.toString

def bom: Bom = {
Expand All @@ -28,6 +40,9 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
bom.setMetadata(metadata)
}
bom.setComponents(components.asJava)
if (settings.includeBomDependencyTree && settings.schemaVersion.getVersion >= Version.VERSION_11.getVersion) {
bom.setDependencies(dependencyTree.asJava)
}
bom
}

Expand Down Expand Up @@ -114,9 +129,7 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
component.setVersion(version)
component.setModified(false)
component.setType(Component.Type.LIBRARY)
component.setPurl(
new PackageURL(PackageURL.StandardTypes.MAVEN, group, name, version, new util.TreeMap(), null).canonicalize()
)
component.setPurl(purl(group, name, version))
if (settings.schemaVersion.getVersion >= Version.VERSION_11.getVersion) {
// component bom-refs must be unique
component.setBomRef(component.getPurl)
Expand Down Expand Up @@ -201,6 +214,46 @@ class BomExtractor(settings: BomExtractorParams, report: UpdateReport, log: Logg
}
}

private def purl(group: String, name: String, version: String): String =
new PackageURL(PackageURL.StandardTypes.MAVEN, group, name, version, new util.TreeMap(), null).canonicalize()

private def dependencyTree: Seq[Dependency] = {
val dependencyTree = configurationsForComponents(settings.configuration).flatMap { configuration =>
dependencyTreeForConfiguration(configuration)
}.distinct // deduplicate dependencies reported by multiple configurations

dependencyTree
}

private def dependencyTreeForConfiguration(configuration: Configuration): Seq[Dependency] = {
report
.configuration(configuration)
.toSeq
.flatMap { configurationReport =>
new DependencyTreeExtractor(configurationReport).dependencyTree
}
}

class DependencyTreeExtractor(configurationReport: ConfigurationReport) {
def dependencyTree: Seq[Dependency] =
moduleGraph.nodes
.sortBy(_.id.idString)
.map { node =>
val bomRef = purl(node.id.organization, node.id.name, node.id.version)

val dependency = new Dependency(bomRef)

val dependsOn = moduleGraph.dependencyMap.getOrElse(node.id, Nil).sortBy(_.id.idString)
dependsOn.foreach { module =>
val bomRef = purl(module.id.organization, module.id.name, module.id.version)
dependency.addDependency(new Dependency(bomRef))
}

dependency
}

private def moduleGraph: ModuleGraph = SbtUpdateReport.fromConfigurationReport(configurationReport, rootModuleID)
}
def logComponent(component: Component): Unit = {
log.info(s""""
|${component.getGroup}" % "${component.getName}" % "${component.getVersion}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ final case class BomExtractorParams(
includeBomHashes: Boolean,
enableBomSha3Hashes: Boolean,
includeBomExternalReferences: Boolean,
includeBomDependencyTree: Boolean,
)
4 changes: 4 additions & 0 deletions src/main/scala/com/github/sbt/sbom/BomSbtPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ object BomSbtPlugin extends AutoPlugin {
lazy val includeBomExternalReferences: SettingKey[Boolean] = settingKey[Boolean](
"should the resulting BOM contain external references? default is true"
)
lazy val includeBomDependencyTree: SettingKey[Boolean] = settingKey[Boolean](
"should the resulting BOM contain the dependency tree? default is true"
)
lazy val makeBom: TaskKey[sbt.File] = taskKey[sbt.File]("Generates bom file")
lazy val listBom: TaskKey[String] = taskKey[String]("Returns the bom")
lazy val components: TaskKey[Component] = taskKey[Component]("Returns the bom")
Expand Down Expand Up @@ -75,6 +78,7 @@ object BomSbtPlugin extends AutoPlugin {
includeBomHashes := true,
enableBomSha3Hashes := true,
includeBomExternalReferences := true,
includeBomDependencyTree := true,
makeBom := Def.taskDyn(BomSbtSettings.makeBomTask(Classpaths.updateTask.value, Compile)).value,
listBom := Def.taskDyn(BomSbtSettings.listBomTask(Classpaths.updateTask.value, Compile)).value,
Test / makeBom := Def.taskDyn(BomSbtSettings.makeBomTask(Classpaths.updateTask.value, Test)).value,
Expand Down
10 changes: 9 additions & 1 deletion src/main/scala/com/github/sbt/sbom/BomSbtSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
package com.github.sbt.sbom

import com.github.sbt.sbom.BomSbtPlugin.autoImport._
import sbt.Keys.{ sLog, target }
import sbt.Keys.{ projectID, sLog, scalaBinaryVersion, scalaVersion, target }
import sbt._

object BomSbtSettings {
Expand All @@ -20,6 +20,9 @@ object BomSbtSettings {
BomTaskProperties(
report,
currentConfiguration,
CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(
projectID.value
),
sLog.value,
bomSchemaVersion.value,
format,
Expand All @@ -29,6 +32,7 @@ object BomSbtSettings {
includeBomHashes.value,
enableBomSha3Hashes.value,
includeBomExternalReferences.value,
includeBomDependencyTree.value,
),
target.value / (currentConfiguration / bomFileName).value
).execute
Expand All @@ -45,6 +49,9 @@ object BomSbtSettings {
BomTaskProperties(
report,
currentConfiguration,
CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(
projectID.value
),
sLog.value,
bomSchemaVersion.value,
format,
Expand All @@ -54,6 +61,7 @@ object BomSbtSettings {
includeBomHashes.value,
enableBomSha3Hashes.value,
includeBomExternalReferences.value,
includeBomDependencyTree.value,
)
).execute
}
Expand Down
9 changes: 8 additions & 1 deletion src/main/scala/com/github/sbt/sbom/BomTask.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import scala.collection.JavaConverters._
final case class BomTaskProperties(
report: UpdateReport,
currentConfiguration: Configuration,
rootModuleID: ModuleID,
log: Logger,
schemaVersion: String,
bomFormat: BomFormat,
Expand All @@ -27,6 +28,7 @@ final case class BomTaskProperties(
includeBomHashes: Boolean,
enableBomSha3Hashes: Boolean,
includeBomExternalReferences: Boolean,
includeBomDependencyTree: Boolean,
)

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

protected def getBomText: String = {
val params: BomExtractorParams = extractorParams(currentConfiguration)
val bom: Bom = new BomExtractor(params, report, log).bom
val bom: Bom = new BomExtractor(params, report, rootModuleID, log).bom
val bomText: String = bomFormat match {
case BomFormat.Json => BomGeneratorFactory.createJson(schemaVersion, bom).toJsonString
case BomFormat.Xml => BomGeneratorFactory.createXml(schemaVersion, bom).toXmlString
Expand Down Expand Up @@ -81,6 +83,7 @@ abstract class BomTask[T](protected val properties: BomTaskProperties) {
includeBomHashes,
enableBomSha3Hashes,
includeBomExternalReferences,
includeBomDependencyTree,
)

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

protected def currentConfiguration: Configuration = properties.currentConfiguration

protected def rootModuleID: ModuleID = properties.rootModuleID

protected def log: Logger = properties.log

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

protected lazy val includeBomExternalReferences: Boolean = properties.includeBomExternalReferences

protected lazy val includeBomDependencyTree: Boolean = properties.includeBomDependencyTree
}
93 changes: 93 additions & 0 deletions src/main/scala/com/github/sbt/sbom/SbtUpdateReport.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: 2023, Scala center, 2011 - 2022, Lightbend, Inc., 2008 - 2010, Mark Harrah
//
// SPDX-License-Identifier: Apache-2.0

package com.github.sbt.sbom

import sbt.librarymanagement.{ ConfigurationReport, ModuleID, ModuleReport }
import sbt.{ File, OrganizationArtifactReport }

import scala.collection.mutable

/*
* taken from sbt at https://github.com/sbt/sbt/blob/1.10.x/main/src/main/scala/sbt/internal/graph/backend/SbtUpdateReport.scala
*
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
object SbtUpdateReport {
case class Module(
id: GraphModuleId,
license: Option[String] = None,
extraInfo: String = "",
evictedByVersion: Option[String] = None,
jarFile: Option[File] = None,
error: Option[String] = None
)

private type Edge = (GraphModuleId, GraphModuleId)
private def Edge(from: GraphModuleId, to: GraphModuleId): Edge = from -> to

case class ModuleGraph(nodes: Seq[Module], edges: Seq[Edge]) {
lazy val modules: Map[GraphModuleId, Module] =
nodes.map(n => (n.id, n)).toMap

def module(id: GraphModuleId): Option[Module] = modules.get(id)

lazy val dependencyMap: Map[GraphModuleId, Seq[Module]] =
createMap(identity)

def createMap(
bindingFor: ((GraphModuleId, GraphModuleId)) => (GraphModuleId, GraphModuleId)
): Map[GraphModuleId, Seq[Module]] = {
val m = new mutable.HashMap[GraphModuleId, mutable.Set[Module]] with mutable.MultiMap[GraphModuleId, Module]
edges.foreach { entry =>
val (f, t) = bindingFor(entry)
module(t).foreach(m.addBinding(f, _))
}
m.toMap.mapValues(_.toSeq.sortBy(_.id.idString)).toMap.withDefaultValue(Nil)
}

def roots: Seq[Module] =
nodes.filter(n => !edges.exists(_._2 == n.id)).sortBy(_.id.idString)
}

case class GraphModuleId(organization: String, name: String, version: String) {
def idString: String = organization + ":" + name + ":" + version
}
object GraphModuleId {
def apply(sbtId: ModuleID): GraphModuleId =
GraphModuleId(sbtId.organization, sbtId.name, sbtId.revision)
}

def fromConfigurationReport(report: ConfigurationReport, rootInfo: ModuleID): ModuleGraph = {
def moduleEdges(orgArt: OrganizationArtifactReport): Seq[(Module, Seq[Edge])] = {
val chosenVersion = orgArt.modules.find(!_.evicted).map(_.module.revision)
orgArt.modules.map(moduleEdge(chosenVersion))
}

def moduleEdge(chosenVersion: Option[String])(report: ModuleReport): (Module, Seq[Edge]) = {
val evictedByVersion = if (report.evicted) chosenVersion else None
val jarFile = report.artifacts
.find(_._1.`type` == "jar")
.orElse(report.artifacts.find(_._1.extension == "jar"))
.map(_._2)
(
Module(
id = GraphModuleId(report.module),
license = report.licenses.headOption.map(_._1),
evictedByVersion = evictedByVersion,
jarFile = jarFile,
error = report.problem
),
report.callers.map(caller => Edge(GraphModuleId(caller.caller), GraphModuleId(report.module)))
)
}
val (nodes, edges) = report.details.flatMap(moduleEdges).unzip
val root = Module(GraphModuleId(rootInfo))

ModuleGraph(root +: nodes, edges.flatten)
}
}
Loading

0 comments on commit dfce9ff

Please sign in to comment.