diff --git a/README.md b/README.md index b22edb2b3..e0e6e1cbf 100644 --- a/README.md +++ b/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: + +``` +:::toFile [-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 "`. +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 "`. ## Configuration settings diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala index 0ca497312..3bb4eab43 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala @@ -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") diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala index 0ec5cab81..2ea3b3cce 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala @@ -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
%s
%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
%s
%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)") } diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/FlatList.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/FlatList.scala index eb26cff87..95f0676d6 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/rendering/FlatList.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/FlatList.scala @@ -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) diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/LicenseInfo.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/LicenseInfo.scala new file mode 100644 index 000000000..cb97d3f0e --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/LicenseInfo.scala @@ -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") +} diff --git a/src/sbt-test/sbt-dependency-graph/toFileSubTask/build.sbt b/src/sbt-test/sbt-dependency-graph/toFileSubTask/build.sbt new file mode 100644 index 000000000..d34ee89e9 --- /dev/null +++ b/src/sbt-test/sbt-dependency-graph/toFileSubTask/build.sbt @@ -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))) + () +} diff --git a/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/licenses.txt b/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/licenses.txt new file mode 100644 index 000000000..826ed0153 --- /dev/null +++ b/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/licenses.txt @@ -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 \ No newline at end of file diff --git a/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/list.txt b/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/list.txt new file mode 100644 index 000000000..4bf401868 --- /dev/null +++ b/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/list.txt @@ -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 \ No newline at end of file diff --git a/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/stats.txt b/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/stats.txt new file mode 100644 index 000000000..6a76e3522 --- /dev/null +++ b/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/stats.txt @@ -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 \ No newline at end of file diff --git a/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/tree.txt b/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/tree.txt new file mode 100644 index 000000000..5b1f1aa5e --- /dev/null +++ b/src/sbt-test/sbt-dependency-graph/toFileSubTask/expected/tree.txt @@ -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 + \ No newline at end of file diff --git a/src/sbt-test/sbt-dependency-graph/toFileSubTask/project/plugins.sbt b/src/sbt-test/sbt-dependency-graph/toFileSubTask/project/plugins.sbt new file mode 100644 index 000000000..6fdebb6d6 --- /dev/null +++ b/src/sbt-test/sbt-dependency-graph/toFileSubTask/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % sys.props("project.version")) diff --git a/src/sbt-test/sbt-dependency-graph/toFileSubTask/test b/src/sbt-test/sbt-dependency-graph/toFileSubTask/test new file mode 100644 index 000000000..83d872e3e --- /dev/null +++ b/src/sbt-test/sbt-dependency-graph/toFileSubTask/test @@ -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