diff --git a/README.md b/README.md index 404d926c2..67cbf820d 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Tasks & Settings * `dependency-graph`: Shows an ASCII graph of the project's dependencies on the sbt console * `dependency-graph-ml`: Generates a .graphml file with the project's dependencies to `target/dependencies-.graphml`. Use e.g. [yEd](http://www.yworks.com/en/products_yed_about.html) to format the graph to your needs. + * `dependency-dot`: Generates a .dot file with the project's dependencies to `target/dependencies-.dot`. + Use [graphviz](http://www.graphviz.org/) to render it to your preferred graphic format. * `dependency-tree`: Shows an ASCII tree representation of the project's dependencies * `what-depends-on `: Find out what depends on an artifact. Shows a reverse dependency tree for the selected module. @@ -52,6 +54,10 @@ Tasks & Settings If `true`, instead of showing the dependency `"[S]"` is appended to the artifact name. Set to `false` if you want the scala-library dependency to appear in the output. (default: true) * `dependency-graph-ml-file`: a setting which allows configuring the output path of `dependency-graph-ml`. + * `dependency-dot-file`: a setting which allows configuring the output path of `dependency-dot`. + * `dependency-dot-header`: a setting to customize the header of the dot file (e.g. to set your preferred node shapes). + * `dependency-dot-nodes-label`: defines the formation of a node label + (default set to `[organisation]
[name]
[version]`) * `ivy-report`: let's ivy generate the resolution report for you project. Use `show ivy-report` for the filename of the generated report @@ -95,4 +101,4 @@ Copyright (c) 2011, 2012 Johannes Rudolph Published under the [Apache License 2.0](http://en.wikipedia.org/wiki/Apache_license). -[example project]: https://gist.github.com/3106492 \ No newline at end of file +[example project]: https://gist.github.com/3106492 diff --git a/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala b/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala index 8408d03be..88fabf91e 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala @@ -195,6 +195,29 @@ object IvyGraphMLDependencies extends App { XML.save(outputFile, xml) } + def saveAsDot(graph: ModuleGraph, + dotHead: String, + nodeFormation: (String, String, String) => String, + outputFile: File): File = { + val nodes = { + for (n <- graph.nodes) + yield + """ "%s"[label=%s]""".format(n.id.idString, + nodeFormation(n.id.organisation, n.id.name, n.id.version)) + }.mkString("\n") + + val edges = { + for ( e <- graph.edges) + yield + """ "%s" -> "%s"""".format(e._1.idString, e._2.idString) + }.mkString("\n") + + val dot = "%s\n%s\n%s\n}".format(dotHead, nodes, edges) + + sbt.IO.write(outputFile, dot) + outputFile + } + def moduleIdFromElement(element: Node, version: String): ModuleId = ModuleId(element.attribute("organisation").get.text, element.attribute("name").get.text, version) diff --git a/src/main/scala/net/virtualvoid/sbt/graph/Plugin.scala b/src/main/scala/net/virtualvoid/sbt/graph/Plugin.scala index af76d17ca..7b3b0b912 100755 --- a/src/main/scala/net/virtualvoid/sbt/graph/Plugin.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/Plugin.scala @@ -28,6 +28,14 @@ object Plugin extends sbt.Plugin { "The location the graphml file should be generated at") val dependencyGraphML = TaskKey[File]("dependency-graph-ml", "Creates a graphml file containing the dependency-graph for a project") + val dependencyDotFile = SettingKey[File]("dependency-dot-file", + "The location the dot file should be generated at") + val dependencyDotNodeLabel = SettingKey[(String,String,String) => String]("dependency-dot-node-label", + "Returns a formated string of a dependency. Takes organisation, name and version as parameters") + val dependencyDotHeader = SettingKey[String]("dependency-dot-header", + "The header of the dot file. (e.g. to set your preferred node shapes)") + val dependencyDot = TaskKey[File]("dependency-dot", + "Creates a dot file containing the dpendency-graph for a project") val moduleGraph = TaskKey[IvyGraphMLDependencies.ModuleGraph]("module-graph", "The dependency graph for a project") val asciiGraph = TaskKey[String]("dependency-graph-string", @@ -104,6 +112,19 @@ object Plugin extends sbt.Plugin { dependencyTree <<= print(asciiTree), dependencyGraphMLFile <<= target / "dependencies-%s.graphml".format(config.toString), dependencyGraphML <<= dependencyGraphMLTask, + dependencyDotFile <<= target / "dependencies-%s.dot".format(config.toString), + dependencyDot <<= dependencyDotTask, + dependencyDotHeader := """digraph "dependency-graph" { + | graph[rankdir="LR"] + | node [ + | shape="record" + | ] + | edge [ + | arrowtail="none" + | ]""".stripMargin, + dependencyDotNodeLabel := { (organisation: String, name: String, version: String) => + """<%s
%s
%s>""".format(organisation, name, version) + }, whatDependsOn <<= InputTask(artifactIdParser) { module => (module, streams, moduleGraph) map { (module, streams, graph) => streams.log.info(IvyGraphMLDependencies.asciiTree(IvyGraphMLDependencies.reverseGraphStartingAt(graph, module))) @@ -121,7 +142,14 @@ object Plugin extends sbt.Plugin { streams.log.info("Wrote dependency graph to '%s'" format resultFile) resultFile } + def dependencyDotTask = + (moduleGraph, dependencyDotHeader, dependencyDotNodeLabel, dependencyDotFile, streams).map { + (graph, dotHead, nodeLabel, outFile, streams) => + val resultFile = IvyGraphMLDependencies.saveAsDot(graph, dotHead, nodeLabel, outFile) + streams.log.info("Wrote dependency graph to '%s'" format resultFile) + resultFile + } def absoluteReportPath = (file: File) => file.getAbsolutePath def print(key: TaskKey[String]) = diff --git a/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/build.sbt b/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/build.sbt index 283f1ba13..8242b5850 100644 --- a/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/build.sbt +++ b/src/sbt-test/sbt-dependency-graph/ignoreScalaLibrary/build.sbt @@ -6,8 +6,8 @@ scalaVersion := "2.9.2" libraryDependencies ++= Seq( "org.slf4j" % "slf4j-api" % "1.7.2", - "ch.qos.logback" % "logback-classic" % "1.0.7", - "com.typesafe.akka" % "akka-actor" % "2.0.3") + "ch.qos.logback" % "logback-classic" % "1.0.7" +) TaskKey[Unit]("check") <<= (ivyReport in Test, asciiTree in Test) map { (report, graph) => def sanitize(str: String): String = str.split('\n').drop(1).map(_.trim).mkString("\n") @@ -17,9 +17,6 @@ TaskKey[Unit]("check") <<= (ivyReport in Test, asciiTree in Test) map { (report, | | +-ch.qos.logback:logback-core:1.0.7 | | +-org.slf4j:slf4j-api:1.6.6 (evicted by: 1.7.2) | | - | +-com.typesafe.akka:akka-actor:2.0.3 [S] - | | +-com.typesafe:config:0.3.1 - | | | +-org.slf4j:slf4j-api:1.7.2 | """.stripMargin IO.writeLines(file("/tmp/blib"), sanitize(graph).split("\n")) diff --git a/src/sbt-test/sbt-dependency-graph/plugins.sbt b/src/sbt-test/sbt-dependency-graph/plugins.sbt index e25ce12a6..33a40a085 100644 --- a/src/sbt-test/sbt-dependency-graph/plugins.sbt +++ b/src/sbt-test/sbt-dependency-graph/plugins.sbt @@ -1 +1 @@ -addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.7.0-RC3") \ No newline at end of file +addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.7.1-SNAPSHOT") \ No newline at end of file diff --git a/src/sbt-test/sbt-dependency-graph/testDotFileGeneration/project/Build.scala b/src/sbt-test/sbt-dependency-graph/testDotFileGeneration/project/Build.scala new file mode 100644 index 000000000..2becd0917 --- /dev/null +++ b/src/sbt-test/sbt-dependency-graph/testDotFileGeneration/project/Build.scala @@ -0,0 +1,71 @@ +import collection.mutable.ListBuffer +import net.virtualvoid.sbt.graph.Plugin._ + +import sbt._ +import sbt.Keys._ + +object Build extends sbt.Build { + + def defaultSettings = + seq(scalaVersion := "2.9.2") + + lazy val justATransiviteDependencyEndpointProject = + Project("just-a-transitive-dependency-endpoint", file(".")) + .settings(defaultSettings: _*) + + lazy val justATransitiveDependencyProject = + Project("just-a-transitive-dependency", file(".")) + .settings(defaultSettings: _*) + .dependsOn(justATransiviteDependencyEndpointProject) + + lazy val justADependencyProject = + Project("just-a-dependency", file(".")) + .settings(defaultSettings: _*) + + lazy val test_project = + Project("test-dot-file-generation", file(".")) + .settings(graphSettings: _*) + .settings(defaultSettings: _*) + .settings( + TaskKey[Unit]("check") <<= (dependencyDot in Compile) map { (dotFile) => + val expectedGraph = + """digraph "dependency-graph" { + | graph[rankdir="LR"] + | node [ + | shape="record" + | ] + | edge [ + | arrowtail="none" + | ] + | "test-dot-file-generation:test-dot-file-generation_2.9.2:0.1-SNAPSHOT"[label=test-dot-file-generation_2.9.2
0.1-SNAPSHOT>] + | "just-a-transitive-dependency:just-a-transitive-dependency_2.9.2:0.1-SNAPSHOT"[label=just-a-transitive-dependency_2.9.2
0.1-SNAPSHOT>] + | "just-a-transitive-dependency-endpoint:just-a-transitive-dependency-endpoint_2.9.2:0.1-SNAPSHOT"[label=just-a-transitive-dependency-endpoint_2.9.2
0.1-SNAPSHOT>] + | "just-a-dependency:just-a-dependency_2.9.2:0.1-SNAPSHOT"[label=just-a-dependency_2.9.2
0.1-SNAPSHOT>] + | "test-dot-file-generation:test-dot-file-generation_2.9.2:0.1-SNAPSHOT" -> "just-a-transitive-dependency:just-a-transitive-dependency_2.9.2:0.1-SNAPSHOT" + | "just-a-transitive-dependency:just-a-transitive-dependency_2.9.2:0.1-SNAPSHOT" -> "just-a-transitive-dependency-endpoint:just-a-transitive-dependency-endpoint_2.9.2:0.1-SNAPSHOT" + | "test-dot-file-generation:test-dot-file-generation_2.9.2:0.1-SNAPSHOT" -> "just-a-dependency:just-a-dependency_2.9.2:0.1-SNAPSHOT" + |} + """.stripMargin + + val graph : String = scala.io.Source.fromFile(dotFile.getAbsolutePath).mkString + val errors = compareByLine(graph, expectedGraph) + require(errors.isEmpty , errors.mkString("\n")) + () + } + ) + .dependsOn(justADependencyProject, justATransitiveDependencyProject) + + def compareByLine(got : String, expected : String) : Seq[String] = { + val errors = ListBuffer[String]() + got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach { case((got_line : String, expected_line : String), i : Int) => + if(got_line != expected_line) { + errors.append( + """not matching lines at line %s + |expected: %s + |got: %s + |""".stripMargin.format(i,expected_line, got_line)) + } + } + errors + } +} diff --git a/src/sbt-test/sbt-dependency-graph/testDotFileGeneration/project/plugins.sbt b/src/sbt-test/sbt-dependency-graph/testDotFileGeneration/project/plugins.sbt new file mode 120000 index 000000000..0caf1de77 --- /dev/null +++ b/src/sbt-test/sbt-dependency-graph/testDotFileGeneration/project/plugins.sbt @@ -0,0 +1 @@ +../../plugins.sbt \ No newline at end of file diff --git a/src/sbt-test/sbt-dependency-graph/testDotFileGeneration/test b/src/sbt-test/sbt-dependency-graph/testDotFileGeneration/test new file mode 100644 index 000000000..f9fa19e83 --- /dev/null +++ b/src/sbt-test/sbt-dependency-graph/testDotFileGeneration/test @@ -0,0 +1,2 @@ +> project test-dot-file-generation +> check