big refactoring: split up the mess in IvyGraphMLDependencies into various subfiles

This commit is contained in:
Johannes Rudolph 2015-11-14 17:14:47 +01:00
parent 045aff16f5
commit 7a5126ef87
14 changed files with 450 additions and 267 deletions

View File

@ -31,7 +31,7 @@ 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 dpendency-graph for a project")
val moduleGraph = TaskKey[IvyGraphMLDependencies.ModuleGraph]("module-graph",
val moduleGraph = TaskKey[ModuleGraph]("module-graph",
"The dependency graph for a project")
val asciiGraph = TaskKey[String]("dependency-graph-string",
"Returns a string containing the ascii representation of the dependency graph for a project")
@ -54,7 +54,7 @@ trait DependencyGraphKeys {
"Aggregates and shows information about the licenses of dependencies")
// internal
private[graph] val moduleGraphStore = TaskKey[IvyGraphMLDependencies.ModuleGraph]("module-graph-store", "The stored module-graph from the last run")
private[graph] val moduleGraphStore = TaskKey[ModuleGraph]("module-graph-store", "The stored module-graph from the last run")
private[graph] val whatDependsOn = InputKey[Unit]("what-depends-on", "Shows information about what depends on the given module")
}

View File

@ -26,8 +26,6 @@ import sbt.complete.Parser
import org.apache.ivy.core.resolve.ResolveOptions
import IvyGraphMLDependencies.ModuleGraph
object DependencyGraphSettings {
import DependencyGraphKeys._
import ModuleGraphProtocol._
@ -54,23 +52,23 @@ object DependencyGraphSettings {
def ivyReportForConfig(config: Configuration) = inConfig(config)(Seq(
ivyReport <<= ivyReportFunction map (_(config.toString)) dependsOn(ignoreMissingUpdate),
moduleGraph <<= ivyReport map (absoluteReportPath.andThen(IvyGraphMLDependencies.graph)),
moduleGraph <<= ivyReport map (absoluteReportPath.andThen(frontend.IvyReport.fromReportFile)),
moduleGraph <<= (scalaVersion, moduleGraph, filterScalaLibrary) map { (scalaV, graph, filter) =>
if (filter)
IvyGraphMLDependencies.ignoreScalaLibrary(scalaV, graph)
GraphTransformations.ignoreScalaLibrary(scalaV, graph)
else
graph
},
moduleGraphStore <<= moduleGraph storeAs moduleGraphStore triggeredBy moduleGraph,
asciiGraph <<= moduleGraph map IvyGraphMLDependencies.asciiGraph,
asciiGraph <<= moduleGraph map rendering.AsciiGraph.asciiGraph,
dependencyGraph <<= InputTask(shouldForceParser) { force =>
(force, moduleGraph, streams) map { (force, graph, streams) =>
if (force || graph.nodes.size < 15) {
streams.log.info(IvyGraphMLDependencies.asciiGraph(graph))
streams.log.info(rendering.AsciiGraph.asciiGraph(graph))
streams.log.info("\n\n")
streams.log.info("Note: The old tree layout is still available by using `dependency-tree`")
} else {
streams.log.info(IvyGraphMLDependencies.asciiTree(graph))
streams.log.info(rendering.AsciiTree.asciiTree(graph))
if (!force) {
streams.log.info("\n")
@ -79,7 +77,7 @@ object DependencyGraphSettings {
}
}
},
asciiTree <<= moduleGraph map IvyGraphMLDependencies.asciiTree,
asciiTree <<= moduleGraph map rendering.AsciiTree.asciiTree,
dependencyTree <<= print(asciiTree),
dependencyGraphMLFile <<= target / "dependencies-%s.graphml".format(config.toString),
dependencyGraphML <<= dependencyGraphMLTask,
@ -98,7 +96,7 @@ object DependencyGraphSettings {
},
whatDependsOn <<= InputTask(artifactIdParser) { module =>
(module, streams, moduleGraph) map { (module, streams, graph) =>
streams.log.info(IvyGraphMLDependencies.asciiTree(IvyGraphMLDependencies.reverseGraphStartingAt(graph, module)))
streams.log.info(rendering.AsciiTree.asciiTree(GraphTransformations.reverseGraphStartingAt(graph, module)))
}
},
licenseInfo <<= (moduleGraph, streams) map showLicenseInfo
@ -109,7 +107,7 @@ object DependencyGraphSettings {
def dependencyGraphMLTask =
(moduleGraph, dependencyGraphMLFile, streams) map { (graph, resultFile, streams) =>
IvyGraphMLDependencies.saveAsGraphML(graph, resultFile.getAbsolutePath)
rendering.GraphML.saveAsGraphML(graph, resultFile.getAbsolutePath)
streams.log.info("Wrote dependency graph to '%s'" format resultFile)
resultFile
}
@ -117,7 +115,7 @@ object DependencyGraphSettings {
(moduleGraph, dependencyDotHeader, dependencyDotNodeLabel, dependencyDotFile, streams).map {
(graph, dotHead, nodeLabel, outFile, streams) =>
val resultFile = IvyGraphMLDependencies.saveAsDot(graph, dotHead, nodeLabel, outFile)
val resultFile = rendering.DOT.saveAsDot(graph, dotHead, nodeLabel, outFile)
streams.log.info("Wrote dependency graph to '%s'" format resultFile)
resultFile
}
@ -143,8 +141,6 @@ object DependencyGraphSettings {
(Space ~> token("--force")).?.map(_.isDefined)
}
import IvyGraphMLDependencies.ModuleId
val artifactIdParser: Initialize[State => Parser[ModuleId]] =
resolvedScoped { ctx => (state: State) =>
val graph = loadFromContext(moduleGraphStore, ctx, state) getOrElse ModuleGraph(Nil, Nil)

View File

@ -0,0 +1,58 @@
/*
* Copyright 2011, 2012 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
object GraphTransformations {
def reverseGraphStartingAt(graph: ModuleGraph, root: ModuleId): ModuleGraph = {
val deps = graph.reverseDependencyMap
def visit(module: ModuleId, visited: Set[ModuleId]): Seq[(ModuleId, ModuleId)] =
if (visited(module))
Nil
else
deps.get(module) match {
case Some(deps) =>
deps.flatMap { to =>
(module, to.id) +: visit(to.id, visited + module)
}
case None => Nil
}
val edges = visit(root, Set.empty)
val nodes = edges.foldLeft(Set.empty[ModuleId])((set, edge) => set + edge._1 + edge._2).map(graph.module)
ModuleGraph(nodes.toSeq, edges)
}
def ignoreScalaLibrary(scalaVersion: String, graph: ModuleGraph): ModuleGraph = {
def isScalaLibrary(m: Module) = isScalaLibraryId(m.id)
def isScalaLibraryId(id: ModuleId) = id.organisation == "org.scala-lang" && id.name == "scala-library"
def dependsOnScalaLibrary(m: Module): Boolean =
graph.dependencyMap(m.id).exists(isScalaLibrary)
def addScalaLibraryAnnotation(m: Module): Module = {
if (dependsOnScalaLibrary(m))
m.copy(extraInfo = m.extraInfo + " [S]")
else
m
}
val newNodes = graph.nodes.map(addScalaLibraryAnnotation).filterNot(isScalaLibrary)
val newEdges = graph.edges.filterNot(e => isScalaLibraryId(e._2))
ModuleGraph(newNodes, newEdges)
}
}

View File

@ -1,252 +0,0 @@
/*
* Copyright 2011, 2012 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
import xml.parsing.ConstructingParser
import java.io.File
import collection.mutable.HashMap
import collection.mutable.MultiMap
import collection.mutable.{Set => MSet}
import sbt.ConsoleLogger
import xml.{NodeSeq, Document, XML, Node}
import com.github.mdr.ascii.layout
import layout._
import sbinary.{Format, DefaultProtocol}
object IvyGraphMLDependencies extends App {
case class ModuleId(organisation: String,
name: String,
version: String) {
def idString: String = organisation+":"+name+":"+version
}
case class Module(id: ModuleId,
license: Option[String] = None,
extraInfo: String = "",
evictedByVersion: Option[String] = None,
error: Option[String] = None) {
def hadError: Boolean = error.isDefined
def isUsed: Boolean = !evictedByVersion.isDefined
}
type Edge = (ModuleId, ModuleId)
case class ModuleGraph(nodes: Seq[Module], edges: Seq[Edge]) {
lazy val modules: Map[ModuleId, Module] =
nodes.map(n => (n.id, n)).toMap
def module(id: ModuleId): Module = modules(id)
lazy val dependencyMap: Map[ModuleId, Seq[Module]] =
createMap(identity)
lazy val reverseDependencyMap: Map[ModuleId, Seq[Module]] =
createMap { case (a, b) => (b, a) }
def createMap(bindingFor: ((ModuleId, ModuleId)) => (ModuleId, ModuleId)): Map[ModuleId, Seq[Module]] = {
val m = new HashMap[ModuleId, MSet[Module]] with MultiMap[ModuleId, Module]
edges.foreach { entry =>
val (f, t) = bindingFor(entry)
m.addBinding(f, module(t))
}
m.toMap.mapValues(_.toSeq.sortBy(_.id.idString)).withDefaultValue(Nil)
}
}
def graph(ivyReportFile: String): ModuleGraph =
buildGraph(buildDoc(ivyReportFile))
def buildGraph(doc: Document): ModuleGraph = {
def edgesForModule(id: ModuleId, revision: NodeSeq): Seq[Edge] =
for {
caller <- revision \ "caller"
callerModule = moduleIdFromElement(caller, caller.attribute("callerrev").get.text)
} yield (moduleIdFromElement(caller, caller.attribute("callerrev").get.text), id)
val moduleEdges: Seq[(Module, Seq[Edge])] = for {
mod <- doc \ "dependencies" \ "module"
revision <- mod \ "revision"
rev = revision.attribute("name").get.text
moduleId = moduleIdFromElement(mod, rev)
module = Module(moduleId,
(revision \ "license").headOption.flatMap(_.attribute("name")).map(_.text),
evictedByVersion = (revision \ "evicted-by").headOption.flatMap(_.attribute("rev").map(_.text)),
error = revision.attribute("error").map(_.text))
} yield (module, edgesForModule(moduleId, revision))
val (nodes, edges) = moduleEdges.unzip
val info = (doc \ "info").head
def infoAttr(name: String): String =
info.attribute(name).getOrElse(throw new IllegalArgumentException("Missing attribute "+name)).text
val rootModule = Module(ModuleId(infoAttr("organisation"), infoAttr("module"), infoAttr("revision")))
ModuleGraph(rootModule +: nodes, edges.flatten)
}
def reverseGraphStartingAt(graph: ModuleGraph, root: ModuleId): ModuleGraph = {
val deps = graph.reverseDependencyMap
def visit(module: ModuleId, visited: Set[ModuleId]): Seq[(ModuleId, ModuleId)] =
if (visited(module))
Nil
else
deps.get(module) match {
case Some(deps) =>
deps.flatMap { to =>
(module, to.id) +: visit(to.id, visited + module)
}
case None => Nil
}
val edges = visit(root, Set.empty)
val nodes = edges.foldLeft(Set.empty[ModuleId])((set, edge) => set + edge._1 + edge._2).map(graph.module)
ModuleGraph(nodes.toSeq, edges)
}
def ignoreScalaLibrary(scalaVersion: String, graph: ModuleGraph): ModuleGraph = {
def isScalaLibrary(m: Module) = isScalaLibraryId(m.id)
def isScalaLibraryId(id: ModuleId) = id.organisation == "org.scala-lang" && id.name == "scala-library"
def dependsOnScalaLibrary(m: Module): Boolean =
graph.dependencyMap(m.id).exists(isScalaLibrary)
def addScalaLibraryAnnotation(m: Module): Module = {
if (dependsOnScalaLibrary(m))
m.copy(extraInfo = m.extraInfo + " [S]")
else
m
}
val newNodes = graph.nodes.map(addScalaLibraryAnnotation).filterNot(isScalaLibrary)
val newEdges = graph.edges.filterNot(e => isScalaLibraryId(e._2))
ModuleGraph(newNodes, newEdges)
}
def asciiGraph(graph: ModuleGraph): String =
Layouter.renderGraph(buildAsciiGraph(graph))
def asciiTree(graph: ModuleGraph): String = {
val deps = graph.dependencyMap
// there should only be one root node (the project itself)
val roots = graph.nodes.filter(n => !graph.edges.exists(_._2 == n.id)).sortBy(_.id.idString)
roots.map { root =>
util.AsciiTreeLayout.toAscii[Module](root, node => deps.getOrElse(node.id, Seq.empty[Module]), displayModule)
}.mkString("\n")
}
def displayModule(module: Module): String =
red(module.id.idString +
module.extraInfo +
module.error.map(" (error: "+_+")").getOrElse("") +
module.evictedByVersion.map(_ formatted " (evicted by: %s)").getOrElse(""), module.hadError)
private def buildAsciiGraph(moduleGraph: ModuleGraph): layout.Graph[String] = {
def renderVertex(module: Module): String =
module.id.name + module.extraInfo + "\n" +
module.id.organisation + "\n" +
module.id.version +
module.error.map("\nerror: "+_).getOrElse("") +
module.evictedByVersion.map(_ formatted "\nevicted by: %s").getOrElse("")
val vertices = moduleGraph.nodes.map(renderVertex).toList
val edges = moduleGraph.edges.toList.map { case (from, to) (renderVertex(moduleGraph.module(from)), renderVertex(moduleGraph.module(to))) }
layout.Graph(vertices, edges)
}
def saveAsGraphML(graph: ModuleGraph, outputFile: String) {
val nodesXml =
for (n <- graph.nodes)
yield
<node id={n.id.idString}><data key="d0">
<y:ShapeNode>
<y:NodeLabel>{n.id.idString}</y:NodeLabel>
</y:ShapeNode>
</data></node>
val edgesXml =
for (e <- graph.edges)
yield <edge source={e._1.idString} target={e._2.idString} />
val xml =
<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:y="http://www.yworks.com/xml/graphml"
xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
<key for="node" id="d0" yfiles.type="nodegraphics"/>
<graph id="Graph" edgedefault="undirected">
{nodesXml}
{edgesXml}
</graph>
</graphml>
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)
private def buildDoc(ivyReportFile: String) = ConstructingParser.fromSource(io.Source.fromFile(ivyReportFile), false).document
def red(str: String, doRed: Boolean): String =
if (ConsoleLogger.formatEnabled && doRed)
Console.RED + str + Console.RESET
else
str
def die(msg: String): Nothing = {
println(msg)
sys.exit(1)
}
def usage: String =
"Usage: <ivy-report-file> <output-file>"
val reportFile = args.lift(0).filter(f => new File(f).exists).getOrElse(die(usage))
val outputFile = args.lift(1).getOrElse(die(usage))
saveAsGraphML(graph(reportFile), outputFile)
}
object ModuleGraphProtocol extends DefaultProtocol {
import IvyGraphMLDependencies._
implicit def seqFormat[T: Format]: Format[Seq[T]] = wrap[Seq[T], List[T]](_.toList, _.toSeq)
implicit val ModuleIdFormat: Format[ModuleId] = asProduct3(ModuleId)(ModuleId.unapply(_).get)
implicit val ModuleFormat: Format[Module] = asProduct5(Module)(Module.unapply(_).get)
implicit val ModuleGraphFormat: Format[ModuleGraph] = asProduct2(ModuleGraph)(ModuleGraph.unapply(_).get)
}

View File

@ -0,0 +1,33 @@
/*
* 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
import java.io.File
object Main extends App {
def die(msg: String): Nothing = {
println(msg)
sys.exit(1)
}
def usage: String =
"Usage: <ivy-report-file> <output-file>"
val reportFile = args.lift(0).filter(f => new File(f).exists).getOrElse(die(usage))
val outputFile = args.lift(1).getOrElse(die(usage))
val graph = frontend.IvyReport.fromReportFile(reportFile)
rendering.GraphML.saveAsGraphML(graph, outputFile)
}

View File

@ -0,0 +1,61 @@
/*
* 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.frontend
import net.virtualvoid.sbt.graph._
import scala.xml.{NodeSeq, Document, Node}
import scala.xml.parsing.ConstructingParser
object IvyReport {
def fromReportFile(ivyReportFile: String): ModuleGraph =
fromReportXML(loadXML(ivyReportFile))
def fromReportXML(doc: Document): ModuleGraph = {
def edgesForModule(id: ModuleId, revision: NodeSeq): Seq[Edge] =
for {
caller <- revision \ "caller"
callerModule = moduleIdFromElement(caller, caller.attribute("callerrev").get.text)
} yield (moduleIdFromElement(caller, caller.attribute("callerrev").get.text), id)
val moduleEdges: Seq[(Module, Seq[Edge])] = for {
mod <- doc \ "dependencies" \ "module"
revision <- mod \ "revision"
rev = revision.attribute("name").get.text
moduleId = moduleIdFromElement(mod, rev)
module = Module(moduleId,
(revision \ "license").headOption.flatMap(_.attribute("name")).map(_.text),
evictedByVersion = (revision \ "evicted-by").headOption.flatMap(_.attribute("rev").map(_.text)),
error = revision.attribute("error").map(_.text))
} yield (module, edgesForModule(moduleId, revision))
val (nodes, edges) = moduleEdges.unzip
val info = (doc \ "info").head
def infoAttr(name: String): String =
info.attribute(name).getOrElse(throw new IllegalArgumentException("Missing attribute "+name)).text
val rootModule = Module(ModuleId(infoAttr("organisation"), infoAttr("module"), infoAttr("revision")))
ModuleGraph(rootModule +: nodes, edges.flatten)
}
private def moduleIdFromElement(element: Node, version: String): ModuleId =
ModuleId(element.attribute("organisation").get.text, element.attribute("name").get.text, version)
private def loadXML(ivyReportFile: String) =
ConstructingParser.fromSource(io.Source.fromFile(ivyReportFile), preserveWS = false).document()
}

View File

@ -0,0 +1,63 @@
/*
* 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
import scala.collection.mutable.{MultiMap, HashMap, Set}
case class ModuleId(organisation: String,
name: String,
version: String) {
def idString: String = organisation+":"+name+":"+version
}
case class Module(id: ModuleId,
license: Option[String] = None,
extraInfo: String = "",
evictedByVersion: Option[String] = None,
error: Option[String] = None) {
def hadError: Boolean = error.isDefined
def isUsed: Boolean = !evictedByVersion.isDefined
}
case class ModuleGraph(nodes: Seq[Module], edges: Seq[Edge]) {
lazy val modules: Map[ModuleId, Module] =
nodes.map(n => (n.id, n)).toMap
def module(id: ModuleId): Module = modules(id)
lazy val dependencyMap: Map[ModuleId, Seq[Module]] =
createMap(identity)
lazy val reverseDependencyMap: Map[ModuleId, Seq[Module]] =
createMap { case (a, b) => (b, a) }
def createMap(bindingFor: ((ModuleId, ModuleId)) => (ModuleId, ModuleId)): Map[ModuleId, Seq[Module]] = {
val m = new HashMap[ModuleId, Set[Module]] with MultiMap[ModuleId, Module]
edges.foreach { entry =>
val (f, t) = bindingFor(entry)
m.addBinding(f, module(t))
}
m.toMap.mapValues(_.toSeq.sortBy(_.id.idString)).withDefaultValue(Nil)
}
}
import sbinary.{Format, DefaultProtocol}
object ModuleGraphProtocol extends DefaultProtocol {
implicit def seqFormat[T: Format]: Format[Seq[T]] = wrap[Seq[T], List[T]](_.toList, _.toSeq)
implicit val ModuleIdFormat: Format[ModuleId] = asProduct3(ModuleId)(ModuleId.unapply(_).get)
implicit val ModuleFormat: Format[Module] = asProduct5(Module)(Module.unapply(_).get)
implicit val ModuleGraphFormat: Format[ModuleGraph] = asProduct2(ModuleGraph.apply _)(ModuleGraph.unapply(_).get)
}

View File

@ -0,0 +1,21 @@
/*
* 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
package object graph {
type Edge = (ModuleId, ModuleId)
}

View File

@ -0,0 +1,38 @@
/*
* 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
package rendering
import com.github.mdr.ascii.layout._
object AsciiGraph {
def asciiGraph(graph: ModuleGraph): String =
Layouter.renderGraph(buildAsciiGraph(graph))
private def buildAsciiGraph(moduleGraph: ModuleGraph): Graph[String] = {
def renderVertex(module: Module): String =
module.id.name + module.extraInfo + "\n" +
module.id.organisation + "\n" +
module.id.version +
module.error.map("\nerror: "+_).getOrElse("") +
module.evictedByVersion.map(_ formatted "\nevicted by: %s").getOrElse("")
val vertices = moduleGraph.nodes.map(renderVertex).toList
val edges = moduleGraph.edges.toList.map { case (from, to) (renderVertex(moduleGraph.module(from)), renderVertex(moduleGraph.module(to))) }
Graph(vertices, edges)
}
}

View File

@ -0,0 +1,39 @@
/*
* 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
package rendering
import util.AsciiTreeLayout
import util.ConsoleUtils._
object AsciiTree {
def asciiTree(graph: ModuleGraph): String = {
val deps = graph.dependencyMap
// there should only be one root node (the project itself)
val roots = graph.nodes.filter(n => !graph.edges.exists(_._2 == n.id)).sortBy(_.id.idString)
roots.map { root =>
AsciiTreeLayout.toAscii[Module](root, node => deps.getOrElse(node.id, Seq.empty[Module]), displayModule)
}.mkString("\n")
}
def displayModule(module: Module): String =
red(module.id.idString +
module.extraInfo +
module.error.map(" (error: "+_+")").getOrElse("") +
module.evictedByVersion.map(_ formatted " (evicted by: %s)").getOrElse(""), module.hadError)
}

View File

@ -0,0 +1,46 @@
/*
* 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.rendering
import java.io.File
import net.virtualvoid.sbt.graph.ModuleGraph
object DOT {
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
}
}

View File

@ -0,0 +1,52 @@
/*
* 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.rendering
import net.virtualvoid.sbt.graph.ModuleGraph
import scala.xml.XML
object GraphML {
def saveAsGraphML(graph: ModuleGraph, outputFile: String) {
val nodesXml =
for (n <- graph.nodes)
yield
<node id={n.id.idString}><data key="d0">
<y:ShapeNode>
<y:NodeLabel>{n.id.idString}</y:NodeLabel>
</y:ShapeNode>
</data></node>
val edgesXml =
for (e <- graph.edges)
yield <edge source={e._1.idString} target={e._2.idString} />
val xml =
<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:y="http://www.yworks.com/xml/graphml"
xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
<key for="node" id="d0" yfiles.type="nodegraphics"/>
<graph id="Graph" edgedefault="undirected">
{nodesXml}
{edgesXml}
</graph>
</graphml>
XML.save(outputFile, xml)
}
}

View File

@ -0,0 +1,27 @@
/*
* 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 sbt.ConsoleLogger
object ConsoleUtils {
def red(str: String, doRed: Boolean): String =
if (ConsoleLogger.formatEnabled && doRed)
Console.RED + str + Console.RESET
else
str
}

View File

@ -16,6 +16,7 @@
package sbt
/** Accessors to private[sbt] symbols. */
object SbtAccess {
val unmanagedScalaInstanceOnly = Defaults.unmanagedScalaInstanceOnly