refs #7: add new `what-depends-on` task to find out the reverse dependencies of a module

This commit is contained in:
Johannes Rudolph 2012-10-22 09:28:32 +02:00
parent 4d705ed256
commit 5a051f6a3a
2 changed files with 87 additions and 8 deletions

View File

@ -25,13 +25,31 @@ import sbt.Graph
import xml.{Document, XML, Node}
import com.github.mdr.ascii.layout
import layout._
import sbinary.{CollectionTypes, Format, DefaultProtocol}
object IvyGraphMLDependencies extends App {
case class Module(organisation: String, name: String, version: String, error: Option[String] = None) {
def id: String = organisation+":"+name+":"+version
override def hashCode(): Int = id.hashCode
override def equals(p1: Any): Boolean = p1 match {
case m: Module => id == m.id
case _ => false
}
}
case class ModuleGraph(nodes: Seq[Module], edges: Seq[(Module, Module)])
case class ModuleGraph(nodes: Seq[Module], edges: Seq[(Module, Module)]) {
lazy val dependencyMap: Map[Module, Seq[Module]] = {
val m = new HashMap[Module, MSet[Module]] with MultiMap[Module, Module]
edges.foreach { case (from, to) => m.addBinding(from, to) }
m.toMap.mapValues(_.toSeq.sortBy(_.id))
}
lazy val reverseDependencyMap: Map[Module, Seq[Module]] = {
val m = new HashMap[Module, MSet[Module]] with MultiMap[Module, Module]
edges.foreach { case (from, to) => m.addBinding(to, from) }
m.toMap.mapValues(_.toSeq.sortBy(_.id))
}
}
def graph(ivyReportFile: String): ModuleGraph =
buildGraph(buildDoc(ivyReportFile))
@ -49,19 +67,36 @@ object IvyGraphMLDependencies extends App {
ModuleGraph(nodes, edges)
}
def reverseGraphStartingAt(graph: ModuleGraph, root: Module): ModuleGraph = {
val deps = graph.reverseDependencyMap
def visit(module: Module, visited: Set[Module]): Seq[(Module, Module)] =
if (visited(module))
Nil
else
deps.get(module) match {
case Some(deps) =>
deps.flatMap { to =>
(module, to) +: visit(to, visited + module)
}
case None => Nil
}
val edges = visit(root, Set.empty)
val nodes = edges.foldLeft(Set.empty[Module])((set, edge) => set + edge._1 + edge._2)
ModuleGraph(nodes.toSeq, edges)
}
def asciiGraph(graph: ModuleGraph): String =
Layouter.renderGraph(buildAsciiGraph(graph))
def asciiTree(graph: ModuleGraph): String = {
val deps = {
val m = new HashMap[String, MSet[Module]] with MultiMap[String, Module]
graph.edges.foreach { case (from, to) => m.addBinding(from.id, to) }
m.toMap.mapValues(_.toSeq.sortBy(_.id))
}
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)).sortBy(_.id)
roots.map { root =>
Graph.toAscii[Module](root, node => deps.getOrElse(node.id, Seq.empty[Module]), x => x.id + x.error.map(" (error: "+_+")").getOrElse(""))
Graph.toAscii[Module](root, node => deps.getOrElse(node, Seq.empty[Module]), x => x.id + x.error.map(" (error: "+_+")").getOrElse(""))
}.mkString("\n")
}
@ -117,4 +152,12 @@ object IvyGraphMLDependencies extends App {
val file = args.lift(0).filter(f => new File(f).exists).getOrElse(die(usage))
val inputFile = args.lift(1).getOrElse(die(usage))
saveAsGraphML(graph(file), inputFile)
}
}
object ModuleGraphProtocol extends DefaultProtocol {
import IvyGraphMLDependencies._
implicit def seqFormat[T: Format]: Format[Seq[T]] = wrap[Seq[T], List[T]](_.toList, _.toSeq)
implicit val ModuleFormat: Format[Module] = asProduct4(Module)(Module.unapply(_).get)
implicit val ModuleGraphFormat: Format[ModuleGraph] = asProduct2(ModuleGraph)(ModuleGraph.unapply(_).get)
}

View File

@ -21,6 +21,7 @@ import Keys._
import complete.Parser
import org.apache.ivy.core.resolve.ResolveOptions
import net.virtualvoid.sbt.graph.IvyGraphMLDependencies.ModuleGraph
object Plugin extends sbt.Plugin {
val dependencyGraphMLFile = SettingKey[File]("dependency-graph-ml-file",
@ -44,6 +45,12 @@ object Plugin extends sbt.Plugin {
val ignoreMissingUpdate = TaskKey[UpdateReport]("update-ignore-missing",
"A copy of the update task which ignores missing artifacts")
// internal
import ModuleGraphProtocol._
val moduleGraphStore = TaskKey[IvyGraphMLDependencies.ModuleGraph]("module-graph-store", "The stored module-graph from the last run")
val whatDependsOn = InputKey[Unit]("what-depends-on", "Shows information about what depends on the given module")
def graphSettings = seq(
ivyReportFunction <<= (sbtVersion, target, projectID, ivyModule, appConfiguration, streams) map { (sbtV, target, projectID, ivyModule, config, streams) =>
sbtV match {
@ -62,6 +69,7 @@ object Plugin extends sbt.Plugin {
def ivyReportForConfig(config: Configuration) = inConfig(config)(seq(
ivyReport <<= ivyReportFunction map (_(config.toString)) dependsOn(ignoreMissingUpdate),
moduleGraph <<= ivyReport map (absoluteReportPath.andThen(IvyGraphMLDependencies.graph)),
moduleGraphStore <<= moduleGraph storeAs moduleGraphStore triggeredBy moduleGraph,
asciiGraph <<= moduleGraph map IvyGraphMLDependencies.asciiGraph,
dependencyGraph <<= InputTask(shouldForceParser) { force =>
(force, moduleGraph, streams) map { (force, graph, streams) =>
@ -83,6 +91,11 @@ object Plugin extends sbt.Plugin {
dependencyTree <<= print(asciiTree),
dependencyGraphMLFile <<= target / "dependencies-%s.graphml".format(config.toString),
dependencyGraphML <<= dependencyGraphMLTask,
whatDependsOn <<= InputTask(artifactIdParser) { module =>
(module, streams, moduleGraph) map { (module, streams, graph) =>
streams.log.info(IvyGraphMLDependencies.asciiTree(IvyGraphMLDependencies.reverseGraphStartingAt(graph, module)))
}
},
Compat.ignoreMissingUpdateT
))
@ -108,6 +121,29 @@ object Plugin extends sbt.Plugin {
(Space ~> token("--force")).?.map(_.isDefined)
}
import IvyGraphMLDependencies.Module
val artifactIdParser: Initialize[State => Parser[Module]] =
resolvedScoped { ctx => (state: State) =>
val graph = loadFromContext(moduleGraphStore, ctx, state) getOrElse ModuleGraph(Nil, Nil)
import complete.DefaultParsers._
def moduleFrom(modules: Seq[Module]) =
modules.map { m =>
(token(m.name) ~ Space ~ token(m.version)).map(_ => m)
}.reduce(_ | _)
graph.nodes.groupBy(_.organisation).map {
case (org, modules) =>
Space ~ token(org) ~ Space ~> moduleFrom(modules)
}.reduceOption(_ | _).getOrElse {
(Space ~> token(StringBasic, "organization") ~ Space ~ token(StringBasic, "module") ~ Space ~ token(StringBasic, "version") ).map { case ((((org, _), mod), _), version) =>
Module(org, mod, version)
}
}
}
def crossName(ivyModule: IvySbt#Module) =
ivyModule.moduleSettings match {
case ic: InlineConfiguration => ic.module.name