Merge pull request #165 from jrudolph/jr/consistent-outputs

For common operations introduce `asString`, `printToConsole`, and `toFile` subtasks, fixes #164
This commit is contained in:
Johannes Rudolph 2018-09-15 12:43:46 +02:00 committed by GitHub
commit 49ad75aaa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 200 additions and 85 deletions

View File

@ -37,11 +37,30 @@ the notes of version [0.8.2](https://github.com/jrudolph/sbt-dependency-graph/tr
* `ivyReport`: Lets ivy generate the resolution report for you project. Use
`show ivyReport` for the filename of the generated report
The following tasks also support the `toFile` subtask to save the contents to a file:
* `dependencyTree`
* `dependencyList`
* `dependencyStats`
* `dependencyLicenseInfo`
The `toFile` subtask has the following syntax:
```
<config>:<task>::toFile <filename> [-f|--force]
```
Use `-f` to force overwriting an existing file.
E.g. `test:dependencyStats::toFile target/depstats.txt` will write the output of the `dependencyStats` in the `test`
configuration to the file `target/depstats.txt` but would not overwrite an existing file.
All tasks can be scoped to a configuration to get the report for a specific configuration. `test:dependencyGraph`,
for example, prints the dependencies in the `test` configuration. If you don't specify any configuration, `compile` is
assumed as usual.
Note: If you want to run tasks with parameters from outside the sbt shell, make sure to put the whole task invocation in quotes, e.g. `sbt "whatDependsOn <org> <module> <version>"`.
Note: If you want to run tasks with parameters from outside the sbt shell, make sure to put the whole task invocation in
quotes, e.g. `sbt "whatDependsOn <org> <module> <version>"`.
## Configuration settings

View File

@ -19,6 +19,10 @@ package net.virtualvoid.sbt.graph
import sbt._
trait DependencyGraphKeys {
val asString = TaskKey[String]("asString", "Provides the string value for the task it is scoped for")
val printToConsole = TaskKey[Unit]("printToConsole", "Prints the tasks value to the console")
val toFile = InputKey[File]("toFile", "Writes the task value to the given file")
val dependencyGraphMLFile = SettingKey[File](
"dependency-graph-ml-file",
"The location the graphml file should be generated at")

View File

@ -27,6 +27,7 @@ import internal.librarymanagement._
import librarymanagement._
import sbt.dependencygraph.SbtAccess
import sbt.dependencygraph.DependencyGraphSbtCompat.Implicits._
import sbt.complete.Parsers
object DependencyGraphSettings {
import DependencyGraphKeys._
@ -47,70 +48,95 @@ object DependencyGraphSettings {
def reportSettings =
Seq(Compile, Test, IntegrationTest, Runtime, Provided, Optional).flatMap(ivyReportForConfig)
def ivyReportForConfig(config: Configuration) = inConfig(config)(Seq(
ivyReport := { Def.task { ivyReportFunction.value.apply(config.toString) } dependsOn (ignoreMissingUpdate) }.value,
crossProjectId := sbt.CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(projectID.value),
moduleGraphSbt :=
ignoreMissingUpdate.value.configuration(configuration.value).map(report SbtUpdateReport.fromConfigurationReport(report, crossProjectId.value)).getOrElse(ModuleGraph.empty),
moduleGraphIvyReport := IvyReport.fromReportFile(absoluteReportPath(ivyReport.value)),
moduleGraph := {
sbtVersion.value match {
case Version(0, 13, x, _) if x >= 6 moduleGraphSbt.value
case Version(1, _, _, _) moduleGraphSbt.value
}
},
moduleGraph := {
// FIXME: remove busywork
val scalaVersion = Keys.scalaVersion.value
val moduleGraph = DependencyGraphKeys.moduleGraph.value
val renderingAlternatives: Seq[(TaskKey[Unit], ModuleGraph String)] =
Seq(
dependencyTree -> rendering.AsciiTree.asciiTree _,
dependencyList -> rendering.FlatList.render(_.id.idString),
dependencyStats -> rendering.Statistics.renderModuleStatsList _,
licenseInfo -> rendering.LicenseInfo.render _)
if (filterScalaLibrary.value) GraphTransformations.ignoreScalaLibrary(scalaVersion, moduleGraph)
else moduleGraph
},
moduleGraphStore := (moduleGraph storeAs moduleGraphStore triggeredBy moduleGraph).value,
asciiTree := rendering.AsciiTree.asciiTree(moduleGraph.value),
dependencyTree := print(asciiTree).value,
dependencyGraphMLFile := { target.value / "dependencies-%s.graphml".format(config.toString) },
dependencyGraphML := dependencyGraphMLTask.value,
dependencyDotFile := { target.value / "dependencies-%s.dot".format(config.toString) },
dependencyDotString := rendering.DOT.dotGraph(moduleGraph.value, dependencyDotHeader.value, dependencyDotNodeLabel.value, rendering.DOT.AngleBrackets),
dependencyDot := writeToFile(dependencyDotString, dependencyDotFile).value,
dependencyBrowseGraphTarget := { target.value / "browse-dependency-graph" },
dependencyBrowseGraphHTML := browseGraphHTMLTask.value,
dependencyBrowseGraph := openBrowser(dependencyBrowseGraphHTML).value,
dependencyBrowseTreeTarget := { target.value / "browse-dependency-tree" },
dependencyBrowseTreeHTML := browseTreeHTMLTask.value,
dependencyBrowseTree := openBrowser(dependencyBrowseTreeHTML).value,
dependencyList := printFromGraph(rendering.FlatList.render(_, _.id.idString)).value,
dependencyStats := printFromGraph(rendering.Statistics.renderModuleStatsList).value,
dependencyDotHeader :=
"""|digraph "dependency-graph" {
def ivyReportForConfig(config: Configuration) = inConfig(config)(
Seq(
ivyReport := { Def.task { ivyReportFunction.value.apply(config.toString) } dependsOn (ignoreMissingUpdate) }.value,
crossProjectId := sbt.CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(projectID.value),
moduleGraphSbt :=
ignoreMissingUpdate.value.configuration(configuration.value).map(report SbtUpdateReport.fromConfigurationReport(report, crossProjectId.value)).getOrElse(ModuleGraph.empty),
moduleGraphIvyReport := IvyReport.fromReportFile(absoluteReportPath(ivyReport.value)),
moduleGraph := {
sbtVersion.value match {
case Version(0, 13, x, _) if x >= 6 moduleGraphSbt.value
case Version(1, _, _, _) moduleGraphSbt.value
}
},
moduleGraph := {
// FIXME: remove busywork
val scalaVersion = Keys.scalaVersion.value
val moduleGraph = DependencyGraphKeys.moduleGraph.value
if (filterScalaLibrary.value) GraphTransformations.ignoreScalaLibrary(scalaVersion, moduleGraph)
else moduleGraph
},
moduleGraphStore := (moduleGraph storeAs moduleGraphStore triggeredBy moduleGraph).value,
// browse
dependencyBrowseGraphTarget := { target.value / "browse-dependency-graph" },
dependencyBrowseGraphHTML := browseGraphHTMLTask.value,
dependencyBrowseGraph := openBrowser(dependencyBrowseGraphHTML).value,
dependencyBrowseTreeTarget := { target.value / "browse-dependency-tree" },
dependencyBrowseTreeHTML := browseTreeHTMLTask.value,
dependencyBrowseTree := openBrowser(dependencyBrowseTreeHTML).value,
// dot support
dependencyDotFile := { target.value / "dependencies-%s.dot".format(config.toString) },
dependencyDotString := rendering.DOT.dotGraph(moduleGraph.value, dependencyDotHeader.value, dependencyDotNodeLabel.value, rendering.DOT.AngleBrackets),
dependencyDot := writeToFile(dependencyDotString, dependencyDotFile).value,
dependencyDotHeader :=
"""|digraph "dependency-graph" {
| graph[rankdir="LR"]
| edge [
| arrowtail="none"
| ]""".stripMargin,
dependencyDotNodeLabel := { (organisation: String, name: String, version: String)
"""%s<BR/><B>%s</B><BR/>%s""".format(organisation, name, version)
},
whatDependsOn := {
val ArtifactPattern(org, name, versionFilter) = artifactPatternParser.parsed
val graph = moduleGraph.value
val modules =
versionFilter match {
case Some(version) ModuleId(org, name, version) :: Nil
case None graph.nodes.filter(m m.id.organisation == org && m.id.name == name).map(_.id)
}
val output =
modules
.map { module
rendering.AsciiTree.asciiTree(GraphTransformations.reverseGraphStartingAt(graph, module))
}
.mkString("\n")
dependencyDotNodeLabel := { (organisation: String, name: String, version: String)
"""%s<BR/><B>%s</B><BR/>%s""".format(organisation, name, version)
},
streams.value.log.info(output)
output
},
licenseInfo := showLicenseInfo(moduleGraph.value, streams.value)) ++ AsciiGraph.asciiGraphSetttings)
// GraphML support
dependencyGraphMLFile := { target.value / "dependencies-%s.graphml".format(config.toString) },
dependencyGraphML := dependencyGraphMLTask.value,
whatDependsOn := {
val ArtifactPattern(org, name, versionFilter) = artifactPatternParser.parsed
val graph = moduleGraph.value
val modules =
versionFilter match {
case Some(version) ModuleId(org, name, version) :: Nil
case None graph.nodes.filter(m m.id.organisation == org && m.id.name == name).map(_.id)
}
val output =
modules
.map { module
rendering.AsciiTree.asciiTree(GraphTransformations.reverseGraphStartingAt(graph, module))
}
.mkString("\n")
streams.value.log.info(output)
output
},
// deprecated settings
asciiTree := (asString in dependencyTree).value) ++
renderingAlternatives.flatMap((renderingTaskSettings _).tupled) ++
AsciiGraph.asciiGraphSetttings)
def renderingTaskSettings(key: TaskKey[Unit], renderer: ModuleGraph String): Seq[Setting[_]] =
Seq(
asString in key := renderer(moduleGraph.value),
printToConsole in key := streams.value.log.info((asString in key).value),
toFile in key := {
val (targetFile, force) = targetFileAndForceParser.parsed
writeToFile(key.key.label, (asString in key).value, targetFile, force, streams.value)
},
key := (printToConsole in key).value)
def ivyReportFunctionTask = Def.task {
val ivyConfig = Keys.ivyConfiguration.value.asInstanceOf[InlineIvyConfiguration]
@ -157,14 +183,18 @@ object DependencyGraphSettings {
outFile
}
def writeToFile(what: String, data: String, targetFile: File, force: Boolean, streams: TaskStreams): File =
if (targetFile.exists && !force)
throw new RuntimeException(s"Target file for $what already exists at ${targetFile.getAbsolutePath}. Use '-f' to override")
else {
IOUtil.writeToFile(data, targetFile)
streams.log.info(s"Wrote $what to '$targetFile'")
targetFile
}
def absoluteReportPath = (file: File) file.getAbsolutePath
def print(key: TaskKey[String]) =
Def.task { streams.value.log.info(key.value) }
def printFromGraph(f: ModuleGraph String) =
Def.task { streams.value.log.info(f(moduleGraph.value)) }
def openBrowser(uriKey: TaskKey[URI]) =
Def.task {
val uri = uriKey.value
@ -173,33 +203,16 @@ object DependencyGraphSettings {
uri
}
def showLicenseInfo(graph: ModuleGraph, streams: TaskStreams): Unit = {
val output =
graph.nodes.filter(_.isUsed).groupBy(_.license).toSeq.sortBy(_._1).map {
case (license, modules)
license.getOrElse("No license specified") + "\n" +
modules.map(_.id.idString formatted "\t %s").mkString("\n")
}.mkString("\n\n")
streams.log.info(output)
}
import Project._
val shouldForceParser: State Parser[Boolean] = { (state: State)
import sbt.complete.DefaultParsers._
(Space ~> token("--force")).?.map(_.isDefined)
}
case class ArtifactPattern(
organisation: String,
name: String,
version: Option[String])
import sbt.complete.DefaultParsers._
val artifactPatternParser: Def.Initialize[State Parser[ArtifactPattern]] =
resolvedScoped { ctx (state: State)
val graph = loadFromContext(moduleGraphStore, ctx, state) getOrElse ModuleGraph(Nil, Nil)
import sbt.complete.DefaultParsers._
graph.nodes
.map(_.id)
.groupBy(m (m.organisation, m.name))
@ -222,6 +235,10 @@ object DependencyGraphSettings {
}
}
}
val shouldForceParser: Parser[Boolean] = (Space ~> (Parser.literal("-f") | "--force")).?.map(_.isDefined)
val targetFileAndForceParser: Parser[(File, Boolean)] =
Parsers.fileParser(new File(".")) ~ shouldForceParser
// This is to support 0.13.8's InlineConfigurationWithExcludes while not forcing 0.13.8
type HasModule = {
@ -230,7 +247,7 @@ object DependencyGraphSettings {
def crossName(ivyModule: IvySbt#Module) =
ivyModule.moduleSettings match {
case ic: InlineConfiguration ic.module.name
case hm: HasModule if hm.getClass.getName == "sbt.InlineConfigurationWithExcludes" hm.module.name
case hm: HasModule @unchecked if hm.getClass.getName == "sbt.InlineConfigurationWithExcludes" hm.module.name
case _
throw new IllegalStateException("sbt-dependency-graph plugin currently only supports InlineConfiguration of ivy settings (the default in sbt)")
}

View File

@ -18,7 +18,7 @@ package net.virtualvoid.sbt.graph
package rendering
object FlatList {
def render(graph: ModuleGraph, display: Module String): String =
def render(display: Module String)(graph: ModuleGraph): String =
graph.modules.values.toSeq
.distinct
.filterNot(_.isEvicted)

View File

@ -0,0 +1,12 @@
package net.virtualvoid.sbt.graph.rendering
import net.virtualvoid.sbt.graph.ModuleGraph
object LicenseInfo {
def render(graph: ModuleGraph): String =
graph.nodes.filter(_.isUsed).groupBy(_.license).toSeq.sortBy(_._1).map {
case (license, modules)
license.getOrElse("No license specified") + "\n" +
modules.map(_.id.idString formatted "\t %s").mkString("\n")
}.mkString("\n\n")
}

View File

@ -0,0 +1,26 @@
scalaVersion := "2.12.6"
organization := "org.example"
name := "blubber"
version := "0.1"
libraryDependencies ++= Seq(
"com.codahale" % "jerkson_2.9.1" % "0.5.0"
)
TaskKey[Unit]("check") := {
val candidates = "tree list stats licenses".split(' ').map(_.trim)
candidates.foreach { c =>
val expected = new File(s"expected/$c.txt")
val actual = new File(s"target/$c.txt")
import sys.process._
val exit = s"diff -U3 ${expected.getPath} ${actual.getPath}".!
require(exit == 0, s"Diff was non-zero for ${actual.getName}")
}
//require(sanitize(graph) == sanitize(expectedGraph), "Graph for report %s was '\n%s' but should have been '\n%s'" format (report, sanitize(graph), sanitize(expectedGraph)))
()
}

View File

@ -0,0 +1,9 @@
No license specified
org.example:blubber_2.12:0.1
The Apache Software License, Version 2.0
org.codehaus.jackson:jackson-mapper-asl:1.9.11
org.codehaus.jackson:jackson-core-asl:1.9.11
The MIT License
com.codahale:jerkson_2.9.1:0.5.0

View File

@ -0,0 +1,4 @@
com.codahale:jerkson_2.9.1:0.5.0
org.codehaus.jackson:jackson-core-asl:1.9.11
org.codehaus.jackson:jackson-mapper-asl:1.9.11
org.example:blubber_2.12:0.1

View File

@ -0,0 +1,12 @@
TotSize JarSize #TDe #Dep Module
1.754 MB ------- MB 3 1 org.example:blubber_2.12:0.1
1.754 MB 0.741 MB 2 2 com.codahale:jerkson_2.9.1:0.5.0
1.013 MB 0.780 MB 1 1 org.codehaus.jackson:jackson-mapper-asl:1.9.11
0.232 MB 0.232 MB 0 0 org.codehaus.jackson:jackson-core-asl:1.9.11
Columns are
- Jar-Size including dependencies
- Jar-Size
- Number of transitive dependencies
- Number of direct dependencies
- ModuleID

View File

@ -0,0 +1,6 @@
org.example:blubber_2.12:0.1 [S]
+-com.codahale:jerkson_2.9.1:0.5.0 [S]
+-org.codehaus.jackson:jackson-core-asl:1.9.11
+-org.codehaus.jackson:jackson-mapper-asl:1.9.11
+-org.codehaus.jackson:jackson-core-asl:1.9.11

View File

@ -0,0 +1 @@
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % sys.props("project.version"))

View File

@ -0,0 +1,5 @@
> dependencyTree::toFile target/tree.txt
> dependencyList::toFile target/list.txt
> dependencyStats::toFile target/stats.txt
> dependencyLicenseInfo::toFile target/licenses.txt
> check