mirror of https://github.com/sbt/sbt.git
big refactoring: split up the mess in IvyGraphMLDependencies into various subfiles
This commit is contained in:
parent
045aff16f5
commit
7a5126ef87
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package sbt
|
||||
|
||||
/** Accessors to private[sbt] symbols. */
|
||||
object SbtAccess {
|
||||
val unmanagedScalaInstanceOnly = Defaults.unmanagedScalaInstanceOnly
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue