diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala index d8ccdf62b..0dcbb0d15 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala @@ -31,6 +31,8 @@ trait DependencyGraphKeys { "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 dependency-graph for a project") + val dependencyDotString = TaskKey[String]("dependency-dot-string", + "Creates a String containing the dependency-graph for a project in dot format") val moduleGraph = TaskKey[ModuleGraph]("module-graph", "The dependency graph for a project") val asciiGraph = TaskKey[String]("dependency-graph-string", diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala index f1ff27d6c..29c8fc306 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala @@ -17,12 +17,12 @@ package net.virtualvoid.sbt.graph import net.virtualvoid.sbt.graph.backend.{ IvyReport, SbtUpdateReport } +import net.virtualvoid.sbt.graph.util.IOUtil import sbt._ import Keys._ import CrossVersion._ -import sbt.complete.DefaultParsers._ import sbt.complete.Parser import org.apache.ivy.core.resolve.ResolveOptions @@ -81,17 +81,15 @@ object DependencyGraphSettings { dependencyGraphMLFile <<= target / "dependencies-%s.graphml".format(config.toString), dependencyGraphML <<= dependencyGraphMLTask, dependencyDotFile <<= target / "dependencies-%s.dot".format(config.toString), - dependencyDot <<= dependencyDotTask, + dependencyDotString <<= dependencyDotStringTask, + dependencyDot <<= writeToFile(dependencyDotString, dependencyDotFile), 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) + """%s
%s
%s""".format(organisation, name, version) }, whatDependsOn <<= InputTask(artifactIdParser) { module ⇒ (module, streams, moduleGraph) map { (module, streams, graph) ⇒ @@ -115,14 +113,19 @@ object DependencyGraphSettings { 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 = rendering.DOT.saveAsDot(graph, dotHead, nodeLabel, outFile) - streams.log.info("Wrote dependency graph to '%s'" format resultFile) - resultFile + def dependencyDotStringTask = + (moduleGraph, dependencyDotHeader, dependencyDotNodeLabel).map { + (graph, dotHead, nodeLabel) ⇒ rendering.DOT.dotGraph(graph, dotHead, nodeLabel) } + + def writeToFile(dataTask: TaskKey[String], fileTask: SettingKey[File]) = + (dataTask, fileTask, streams).map { (data, outFile, streams) ⇒ + IOUtil.writeToFile(data, outFile) + + streams.log.info("Wrote dependency graph to '%s'" format outFile) + outFile + } + def absoluteReportPath = (file: File) ⇒ file.getAbsolutePath def print(key: TaskKey[String]) = diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala index 3eb168e5b..181e8b7eb 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala @@ -14,31 +14,50 @@ * limitations under the License. */ -package net.virtualvoid.sbt.graph.rendering - -import java.io.File - -import net.virtualvoid.sbt.graph.ModuleGraph +package net.virtualvoid.sbt.graph +package rendering object DOT { - def saveAsDot(graph: ModuleGraph, - dotHead: String, - nodeFormation: (String, String, String) ⇒ String, - outputFile: File): File = { + val EvictedStyle = "stroke-dasharray: 5,5" + + def dotGraph(graph: ModuleGraph, + dotHead: String, + nodeFormation: (String, String, String) ⇒ String): String = { val nodes = { - for (n ← graph.nodes) - yield """ "%s"[label=%s]""".format(n.id.idString, - nodeFormation(n.id.organisation, n.id.name, n.id.version)) + for (n ← graph.nodes) yield { + val style = if (n.isEvicted) EvictedStyle else "" + """ "%s"[labelType="html" label="%s" style="%s"]""".format(n.id.idString, + nodeFormation(n.id.organisation, n.id.name, n.id.version), + style) + } }.mkString("\n") + def originWasEvicted(edge: Edge): Boolean = graph.module(edge._1).isEvicted + def targetWasEvicted(edge: Edge): Boolean = graph.module(edge._2).isEvicted + + // add extra edges from evicted to evicted-by module + val evictedByEdges: Seq[Edge] = + graph.nodes.filter(_.isEvicted).map(m ⇒ Edge(m.id, m.id.copy(version = m.evictedByVersion.get))) + + // remove edges to new evicted-by module which is now replaced by a chain + // dependend -> [evicted] -> dependee + val evictionTargetEdges = + graph.edges.filter(targetWasEvicted).map { + case (from, evicted) ⇒ (from, evicted.copy(version = graph.module(evicted).evictedByVersion.get)) + }.toSet + + val filteredEdges = + graph.edges + .filterNot(e ⇒ originWasEvicted(e) || evictionTargetEdges(e)) ++ evictedByEdges + val edges = { - for (e ← graph.edges) - yield """ "%s" -> "%s"""".format(e._1.idString, e._2.idString) + for (e ← filteredEdges) yield { + val extra = if (graph.module(e._1).isEvicted) + s""" [label="Evicted By" style="$EvictedStyle"]""" else "" + """ "%s" -> "%s"%s""".format(e._1.idString, e._2.idString, extra) + } }.mkString("\n") - val dot = "%s\n%s\n%s\n}".format(dotHead, nodes, edges) - - sbt.IO.write(outputFile, dot) - outputFile + "%s\n%s\n%s\n}".format(dotHead, nodes, edges) } } diff --git a/src/main/scala/net/virtualvoid/sbt/graph/util/IOUtil.scala b/src/main/scala/net/virtualvoid/sbt/graph/util/IOUtil.scala new file mode 100644 index 000000000..c9b8ebbf5 --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/util/IOUtil.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2015 Johannes Rudolph + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.virtualvoid.sbt.graph.util + +import java.io.{ OutputStream, InputStream, FileOutputStream, File } +import java.nio.charset.Charset + +import scala.annotation.tailrec + +object IOUtil { + val utf8 = Charset.forName("utf8") + + def writeToFile(string: String, file: File): Unit = + sbt.IO.write(file, string, utf8) + + def saveResource(resourcePath: String, to: File): Unit = { + val is = getClass.getClassLoader.getResourceAsStream(resourcePath) + require(is ne null, s"Couldn't load '$resourcePath' from classpath.") + + val fos = new FileOutputStream(to) + try copy(is, fos) + finally { + is.close() + fos.close() + } + } + + def copy(from: InputStream, to: OutputStream): Unit = { + val buffer = new Array[Byte](65536) + + @tailrec def rec(): Unit = { + val read = from.read(buffer) + if (read > 0) { + to.write(buffer, 0, read) + rec() + } else if (read == 0) + throw new IllegalStateException("InputStream.read returned 0") + } + rec() + } +}