mirror of https://github.com/sbt/sbt.git
refs #7: add new `what-depends-on` task to find out the reverse dependencies of a module
This commit is contained in:
parent
4d705ed256
commit
5a051f6a3a
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue