mirror of https://github.com/sbt/sbt.git
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:
commit
49ad75aaa5
21
README.md
21
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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)))
|
||||
()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % sys.props("project.version"))
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue