diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala index 1d8ef8a8a..a33f347f2 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala @@ -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") } diff --git a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala index f943e2d96..9f8bffce5 100644 --- a/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala +++ b/src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala @@ -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) diff --git a/src/main/scala/net/virtualvoid/sbt/graph/GraphTransformations.scala b/src/main/scala/net/virtualvoid/sbt/graph/GraphTransformations.scala new file mode 100644 index 000000000..dc4e976c2 --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/GraphTransformations.scala @@ -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) + } +} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala b/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala deleted file mode 100644 index fd84b63f9..000000000 --- a/src/main/scala/net/virtualvoid/sbt/graph/IvyGraphMLDependencies.scala +++ /dev/null @@ -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 - - - {n.id.idString} - - - - val edgesXml = - for (e <- graph.edges) - yield - - val xml = - - - - {nodesXml} - {edgesXml} - - - - 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: " - - 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) -} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/Main.scala b/src/main/scala/net/virtualvoid/sbt/graph/Main.scala new file mode 100644 index 000000000..497258260 --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/Main.scala @@ -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: " + + 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) +} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/frontend/IvyReport.scala b/src/main/scala/net/virtualvoid/sbt/graph/frontend/IvyReport.scala new file mode 100644 index 000000000..94c9b61e9 --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/frontend/IvyReport.scala @@ -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() +} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/model.scala b/src/main/scala/net/virtualvoid/sbt/graph/model.scala new file mode 100644 index 000000000..9af79d129 --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/model.scala @@ -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) +} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/package.scala b/src/main/scala/net/virtualvoid/sbt/graph/package.scala new file mode 100644 index 000000000..1fe4e5fc1 --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/package.scala @@ -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) +} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiGraph.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiGraph.scala new file mode 100644 index 000000000..f4b21ea95 --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiGraph.scala @@ -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) + } +} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala new file mode 100644 index 000000000..d406a310e --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/AsciiTree.scala @@ -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) +} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala new file mode 100644 index 000000000..e7cb2c3f5 --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/DOT.scala @@ -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 + } +} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/rendering/GraphML.scala b/src/main/scala/net/virtualvoid/sbt/graph/rendering/GraphML.scala new file mode 100644 index 000000000..48bf1ce6b --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/rendering/GraphML.scala @@ -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 + + + {n.id.idString} + + + + val edgesXml = + for (e <- graph.edges) + yield + + val xml = + + + + {nodesXml} + {edgesXml} + + + + XML.save(outputFile, xml) + } +} diff --git a/src/main/scala/net/virtualvoid/sbt/graph/util/ConsoleUtils.scala b/src/main/scala/net/virtualvoid/sbt/graph/util/ConsoleUtils.scala new file mode 100644 index 000000000..ceba7da65 --- /dev/null +++ b/src/main/scala/net/virtualvoid/sbt/graph/util/ConsoleUtils.scala @@ -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 +} diff --git a/src/main/scala/sbt/SbtAccess.scala b/src/main/scala/sbt/SbtAccess.scala index eb9113300..a75a57c31 100644 --- a/src/main/scala/sbt/SbtAccess.scala +++ b/src/main/scala/sbt/SbtAccess.scala @@ -16,6 +16,7 @@ package sbt +/** Accessors to private[sbt] symbols. */ object SbtAccess { val unmanagedScalaInstanceOnly = Defaults.unmanagedScalaInstanceOnly