mirror of https://github.com/sbt/sbt.git
Merge pull request #5880 from eed3si9n/wip/dependencygraph
in-sources sbt-dependency-graph
This commit is contained in:
commit
602cf392a6
|
|
@ -15,7 +15,7 @@ env:
|
|||
matrix:
|
||||
- SBT_CMD="mimaReportBinaryIssues ; javafmtCheck ; Test / javafmtCheck; scalafmtCheckAll ; scalafmtSbtCheck; serverTestProj/scalafmtCheckAll; headerCheck ;test:headerCheck ;whitesourceOnPush ;test:compile; publishLocal; test; serverTestProj/test; doc; $UTIL_TESTS; ++$SCALA_213; $UTIL_TESTS"
|
||||
- SBT_CMD="scripted actions/* apiinfo/* compiler-project/* ivy-deps-management/* reporter/* tests/* watch/* classloader-cache/* package/*"
|
||||
- SBT_CMD="scripted dependency-management/* plugins/* project-load/* java/* run/* nio/*"
|
||||
- SBT_CMD="scripted dependency-graph/* dependency-management/* plugins/* project-load/* java/* run/* nio/*"
|
||||
- SBT_CMD="repoOverrideTest:scripted dependency-management/*; scripted source-dependencies/* project/*"
|
||||
|
||||
matrix:
|
||||
|
|
|
|||
|
|
@ -315,6 +315,10 @@ object Terminal {
|
|||
private[this] lazy val isColorEnabledProp: Option[Boolean] =
|
||||
sys.props.get("sbt.color").orElse(sys.props.get("sbt.colour")).flatMap(parseLogOption)
|
||||
|
||||
private[sbt] def red(str: String, doRed: Boolean): String =
|
||||
if (formatEnabledInEnv && doRed) Console.RED + str + Console.RESET
|
||||
else str
|
||||
|
||||
/**
|
||||
*
|
||||
* @param isServer toggles whether or not this is a server of client process
|
||||
|
|
|
|||
|
|
@ -882,7 +882,7 @@ object Defaults extends BuildCommon {
|
|||
Seq(
|
||||
initialCommands :== "",
|
||||
cleanupCommands :== "",
|
||||
asciiGraphWidth :== 40
|
||||
asciiGraphWidth :== 80
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ object PluginDiscovery {
|
|||
"sbt.plugins.SemanticdbPlugin" -> sbt.plugins.SemanticdbPlugin,
|
||||
"sbt.plugins.JUnitXmlReportPlugin" -> sbt.plugins.JUnitXmlReportPlugin,
|
||||
"sbt.plugins.Giter8TemplatePlugin" -> sbt.plugins.Giter8TemplatePlugin,
|
||||
"sbt.plugins.DependencyGraphPlugin" -> sbt.plugins.DependencyGraphPlugin,
|
||||
)
|
||||
val detectedAutoPlugins = discover[AutoPlugin](AutoPlugins)
|
||||
val allAutoPlugins = (defaultAutoPlugins ++ detectedAutoPlugins.modules) map {
|
||||
|
|
|
|||
|
|
@ -81,8 +81,12 @@ object Graph {
|
|||
// [info] | +-baz
|
||||
// [info] |
|
||||
// [info] +-quux
|
||||
def toAscii[A](top: A, children: A => Seq[A], display: A => String, defaultWidth: Int): String = {
|
||||
val maxColumn = math.max(Terminal.get.getWidth, defaultWidth) - 8
|
||||
def toAscii[A](
|
||||
top: A,
|
||||
children: A => Seq[A],
|
||||
display: A => String,
|
||||
maxColumn: Int
|
||||
): String = {
|
||||
val twoSpaces = " " + " " // prevent accidentally being converted into a tab
|
||||
def limitLine(s: String): String =
|
||||
if (s.length > maxColumn) s.slice(0, maxColumn - 2) + ".."
|
||||
|
|
@ -96,21 +100,33 @@ object Graph {
|
|||
}) +
|
||||
s.slice(at + 1, s.length)
|
||||
else s
|
||||
def toAsciiLines(node: A, level: Int): (String, Vector[String]) = {
|
||||
val line = limitLine((twoSpaces * level) + (if (level == 0) "" else "+-") + display(node))
|
||||
val cs = Vector(children(node): _*)
|
||||
val childLines = cs map { toAsciiLines(_, level + 1) }
|
||||
val withBar = childLines.zipWithIndex flatMap {
|
||||
case ((line, withBar), pos) if pos < (cs.size - 1) =>
|
||||
(line +: withBar) map { insertBar(_, 2 * (level + 1)) }
|
||||
case ((line, withBar), _) if withBar.lastOption.getOrElse(line).trim != "" =>
|
||||
(line +: withBar) ++ Vector(twoSpaces * (level + 1))
|
||||
case ((line, withBar), _) => line +: withBar
|
||||
def toAsciiLines(node: A, level: Int, parents: Set[A]): Vector[String] =
|
||||
if (parents contains node) // cycle
|
||||
Vector(limitLine((twoSpaces * level) + "#-" + display(node) + " (cycle)"))
|
||||
else {
|
||||
val line = limitLine((twoSpaces * level) + (if (level == 0) "" else "+-") + display(node))
|
||||
val cs = Vector(children(node): _*)
|
||||
val childLines = cs map {
|
||||
toAsciiLines(_, level + 1, parents + node)
|
||||
}
|
||||
val withBar = childLines.zipWithIndex flatMap {
|
||||
case (lines, pos) if pos < (cs.size - 1) =>
|
||||
lines map {
|
||||
insertBar(_, 2 * (level + 1))
|
||||
}
|
||||
case (lines, pos) =>
|
||||
if (lines.last.trim != "") lines ++ Vector(twoSpaces * (level + 1))
|
||||
else lines
|
||||
}
|
||||
line +: withBar
|
||||
}
|
||||
(line, withBar)
|
||||
}
|
||||
|
||||
val (line, withBar) = toAsciiLines(top, 0)
|
||||
(line +: withBar).mkString("\n")
|
||||
toAsciiLines(top, 0, Set.empty).mkString("\n")
|
||||
}
|
||||
|
||||
def defaultColumnSize: Int = {
|
||||
val termWidth = Terminal.console.getWidth
|
||||
if (termWidth > 20) termWidth - 8
|
||||
else 80 // ignore termWidth
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import sbt.BuildSyntax._
|
||||
import sbt.librarymanagement.{ ModuleID, UpdateReport }
|
||||
|
||||
trait DependencyGraphKeys {
|
||||
val asString = taskKey[String]("Provides the string value for the task it is scoped for")
|
||||
// val printToConsole = TaskKey[Unit]("printToConsole", "Prints the tasks value to the console")
|
||||
val toFile = inputKey[File]("Writes the task value to the given file")
|
||||
|
||||
val dependencyTreeIncludeScalaLibrary = settingKey[Boolean](
|
||||
"Specifies if scala dependency should be included in dependencyTree output"
|
||||
)
|
||||
|
||||
val dependencyGraphMLFile =
|
||||
settingKey[File]("The location the graphml file should be generated at")
|
||||
val dependencyGraphML =
|
||||
taskKey[File]("Creates a graphml file containing the dependency-graph for a project")
|
||||
val dependencyDotFile =
|
||||
settingKey[File]("The location the dot file should be generated at")
|
||||
val dependencyDotNodeLabel = settingKey[(String, String, String) => String](
|
||||
"Returns a formated string of a dependency. Takes organization, name and version as parameters"
|
||||
)
|
||||
val dependencyDotHeader = settingKey[String](
|
||||
"The header of the dot file. (e.g. to set your preferred node shapes)"
|
||||
)
|
||||
val dependencyDot = taskKey[File](
|
||||
"Creates a dot file containing the dependency-graph for a project"
|
||||
)
|
||||
val dependencyDotString = taskKey[String](
|
||||
"Creates a String containing the dependency-graph for a project in dot format"
|
||||
)
|
||||
val dependencyBrowseGraphTarget = settingKey[File](
|
||||
"The location dependency browse graph files should be put."
|
||||
)
|
||||
val dependencyBrowseGraphHTML = taskKey[URI](
|
||||
"Creates an HTML page that can be used to view the graph."
|
||||
)
|
||||
val dependencyBrowseGraph = taskKey[URI](
|
||||
"Opens an HTML page that can be used to view the graph."
|
||||
)
|
||||
val dependencyBrowseTreeTarget = settingKey[File](
|
||||
"The location dependency browse tree files should be put."
|
||||
)
|
||||
val dependencyBrowseTreeHTML = taskKey[URI](
|
||||
"Creates an HTML page that can be used to view the dependency tree"
|
||||
)
|
||||
val dependencyBrowseTree = taskKey[URI](
|
||||
"Opens an HTML page that can be used to view the dependency tree"
|
||||
)
|
||||
val moduleGraph = taskKey[ModuleGraph]("The dependency graph for a project")
|
||||
val moduleGraphIvyReport = taskKey[ModuleGraph](
|
||||
"The dependency graph for a project as generated from an Ivy Report XML"
|
||||
)
|
||||
val moduleGraphSbt = taskKey[ModuleGraph](
|
||||
"The dependency graph for a project as generated from SBT data structures."
|
||||
)
|
||||
val dependencyGraph = inputKey[Unit]("Prints the ascii graph to the console")
|
||||
val dependencyTree = taskKey[Unit]("Prints an ascii tree of all the dependencies to the console")
|
||||
val dependencyList =
|
||||
taskKey[Unit]("Prints a list of all dependencies to the console")
|
||||
val dependencyStats =
|
||||
taskKey[Unit]("Prints statistics for all dependencies to the console")
|
||||
val ivyReportFunction = taskKey[String => File](
|
||||
"A function which returns the file containing the ivy report from the ivy cache for a given configuration"
|
||||
)
|
||||
val ivyReport = taskKey[File](
|
||||
"A task which returns the location of the ivy report file for a given configuration (default `compile`)."
|
||||
)
|
||||
val dependencyLicenseInfo = taskKey[Unit](
|
||||
"Aggregates and shows information about the licenses of dependencies"
|
||||
)
|
||||
|
||||
// internal
|
||||
private[sbt] val ignoreMissingUpdate =
|
||||
TaskKey[UpdateReport]("dependencyUpdate", "sbt-dependency-graph version of update")
|
||||
private[sbt] val moduleGraphStore =
|
||||
TaskKey[ModuleGraph]("module-graph-store", "The stored module-graph from the last run")
|
||||
val whatDependsOn =
|
||||
InputKey[String]("what-depends-on", "Shows information about what depends on the given module")
|
||||
private[sbt] val crossProjectId = SettingKey[ModuleID]("dependency-graph-cross-project-id")
|
||||
}
|
||||
|
||||
object DependencyGraphKeys extends DependencyGraphKeys
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
|
||||
object GraphTransformations {
|
||||
def reverseGraphStartingAt(graph: ModuleGraph, root: GraphModuleId): ModuleGraph = {
|
||||
val deps = graph.reverseDependencyMap
|
||||
|
||||
def visit(
|
||||
module: GraphModuleId,
|
||||
visited: Set[GraphModuleId]
|
||||
): Seq[(GraphModuleId, GraphModuleId)] =
|
||||
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[GraphModuleId])((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: GraphModuleId) =
|
||||
id.organization == "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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
package backend
|
||||
|
||||
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: GraphModuleId, 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(
|
||||
GraphModuleId(infoAttr("organization"), infoAttr("module"), infoAttr("revision"))
|
||||
)
|
||||
|
||||
ModuleGraph(rootModule +: nodes, edges.flatten)
|
||||
}
|
||||
|
||||
private def moduleIdFromElement(element: Node, version: String): GraphModuleId =
|
||||
GraphModuleId(
|
||||
element.attribute("organization").get.text,
|
||||
element.attribute("name").get.text,
|
||||
version
|
||||
)
|
||||
|
||||
private def loadXML(ivyReportFile: String) =
|
||||
ConstructingParser
|
||||
.fromSource(scala.io.Source.fromFile(ivyReportFile), preserveWS = false)
|
||||
.document()
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
package backend
|
||||
|
||||
import scala.language.implicitConversions
|
||||
import scala.language.reflectiveCalls
|
||||
import sbt.librarymanagement.{ ModuleID, ModuleReport, ConfigurationReport }
|
||||
|
||||
object SbtUpdateReport {
|
||||
type OrganizationArtifactReport = {
|
||||
def modules: Seq[ModuleReport]
|
||||
}
|
||||
|
||||
def fromConfigurationReport(report: ConfigurationReport, rootInfo: ModuleID): ModuleGraph = {
|
||||
implicit def id(sbtId: ModuleID): GraphModuleId =
|
||||
GraphModuleId(sbtId.organization, sbtId.name, sbtId.revision)
|
||||
|
||||
def moduleEdges(orgArt: OrganizationArtifactReport): Seq[(Module, Seq[Edge])] = {
|
||||
val chosenVersion = orgArt.modules.find(!_.evicted).map(_.module.revision)
|
||||
orgArt.modules.map(moduleEdge(chosenVersion))
|
||||
}
|
||||
|
||||
def moduleEdge(chosenVersion: Option[String])(report: ModuleReport): (Module, Seq[Edge]) = {
|
||||
val evictedByVersion = if (report.evicted) chosenVersion else None
|
||||
val jarFile = report.artifacts
|
||||
.find(_._1.`type` == "jar")
|
||||
.orElse(report.artifacts.find(_._1.extension == "jar"))
|
||||
.map(_._2)
|
||||
(
|
||||
Module(
|
||||
id = report.module,
|
||||
license = report.licenses.headOption.map(_._1),
|
||||
evictedByVersion = evictedByVersion,
|
||||
jarFile = jarFile,
|
||||
error = report.problem
|
||||
),
|
||||
report.callers.map(caller => Edge(caller.caller, report.module))
|
||||
)
|
||||
}
|
||||
|
||||
val (nodes, edges) = report.details.flatMap(moduleEdges).unzip
|
||||
val root = Module(rootInfo)
|
||||
|
||||
ModuleGraph(root +: nodes, edges.flatten)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
|
||||
import java.io.File
|
||||
import sjsonnew._
|
||||
import scala.collection.mutable.{ HashMap, MultiMap, Set }
|
||||
|
||||
private[sbt] case class GraphModuleId(organization: String, name: String, version: String) {
|
||||
def idString: String = organization + ":" + name + ":" + version
|
||||
}
|
||||
|
||||
private[sbt] object GraphModuleId {
|
||||
import sjsonnew.BasicJsonProtocol.StringJsonFormat
|
||||
implicit val graphModuleIdIso = LList.iso[GraphModuleId, String :*: String :*: String :*: LNil](
|
||||
{ m: GraphModuleId =>
|
||||
("organization", m.organization) :*: ("name", m.name) :*: ("version", m.version) :*: LNil
|
||||
}, {
|
||||
case (_, organization) :*: (_, name) :*: (_, version) :*: LNil =>
|
||||
GraphModuleId(organization, name, version)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private[sbt] case class Module(
|
||||
id: GraphModuleId,
|
||||
license: Option[String] = None,
|
||||
extraInfo: String = "",
|
||||
evictedByVersion: Option[String] = None,
|
||||
jarFile: Option[File] = None,
|
||||
error: Option[String] = None
|
||||
) {
|
||||
def hadError: Boolean = error.isDefined
|
||||
def isUsed: Boolean = !isEvicted
|
||||
def isEvicted: Boolean = evictedByVersion.isDefined
|
||||
}
|
||||
|
||||
private[sbt] object Module {
|
||||
import sjsonnew.BasicJsonProtocol._
|
||||
implicit val moduleIso = LList.iso[Module, GraphModuleId :*: Option[String] :*: String :*: Option[
|
||||
String
|
||||
] :*: Option[File] :*: Option[String] :*: LNil](
|
||||
{ m: Module =>
|
||||
("id", m.id) :*: ("license", m.license) :*: ("extraInfo", m.extraInfo) :*:
|
||||
("evictedByVersion", m.evictedByVersion) :*: (
|
||||
"jarFile",
|
||||
m.jarFile
|
||||
) :*: ("error", m.error) :*: LNil
|
||||
}, {
|
||||
case (_, id) :*: (_, license) :*: (_, extraInfo) :*: (_, evictedByVersion) :*: (_, jarFile) :*: (
|
||||
_,
|
||||
error
|
||||
) :*: LNil =>
|
||||
Module(id, license, extraInfo, evictedByVersion, jarFile, error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private[sbt] case class ModuleGraph(nodes: Seq[Module], edges: Seq[Edge]) {
|
||||
lazy val modules: Map[GraphModuleId, Module] =
|
||||
nodes.map(n => (n.id, n)).toMap
|
||||
|
||||
def module(id: GraphModuleId): Module = modules(id)
|
||||
|
||||
lazy val dependencyMap: Map[GraphModuleId, Seq[Module]] =
|
||||
createMap(identity)
|
||||
|
||||
lazy val reverseDependencyMap: Map[GraphModuleId, Seq[Module]] =
|
||||
createMap { case (a, b) => (b, a) }
|
||||
|
||||
def createMap(
|
||||
bindingFor: ((GraphModuleId, GraphModuleId)) => (GraphModuleId, GraphModuleId)
|
||||
): Map[GraphModuleId, Seq[Module]] = {
|
||||
val m = new HashMap[GraphModuleId, Set[Module]] with MultiMap[GraphModuleId, Module]
|
||||
edges.foreach { entry =>
|
||||
val (f, t) = bindingFor(entry)
|
||||
m.addBinding(f, module(t))
|
||||
}
|
||||
m.toMap.mapValues(_.toSeq.sortBy(_.id.idString)).withDefaultValue(Nil)
|
||||
}
|
||||
|
||||
def roots: Seq[Module] =
|
||||
nodes.filter(n => !edges.exists(_._2 == n.id)).sortBy(_.id.idString)
|
||||
}
|
||||
|
||||
private[sbt] object ModuleGraph {
|
||||
val empty = ModuleGraph(Seq.empty, Seq.empty)
|
||||
|
||||
import BasicJsonProtocol._
|
||||
implicit val moduleGraphIso = LList.iso[ModuleGraph, Vector[Module] :*: Vector[Edge] :*: LNil](
|
||||
{ g: ModuleGraph =>
|
||||
("nodes", g.nodes.toVector) :*: ("edges", g.edges.toVector) :*: LNil
|
||||
}, {
|
||||
case (_, nodes: Vector[Module]) :*: (_, edges: Vector[Edge]) :*: LNil =>
|
||||
ModuleGraph(nodes, edges)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
|
||||
package object graph {
|
||||
type Edge = (GraphModuleId, GraphModuleId)
|
||||
def Edge(from: GraphModuleId, to: GraphModuleId): Edge = from -> to
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
package rendering
|
||||
|
||||
import sbt.internal.util.Terminal.red
|
||||
|
||||
object AsciiTree {
|
||||
def asciiTree(graph: ModuleGraph): String = {
|
||||
val deps = graph.dependencyMap
|
||||
|
||||
// there should only be one root node (the project itself)
|
||||
val roots = graph.roots
|
||||
roots
|
||||
.map { root =>
|
||||
Graph
|
||||
.toAscii[Module](
|
||||
root,
|
||||
node => deps.getOrElse(node.id, Seq.empty[Module]),
|
||||
displayModule,
|
||||
Graph.defaultColumnSize
|
||||
)
|
||||
}
|
||||
.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,89 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
package rendering
|
||||
|
||||
object DOT {
|
||||
val EvictedStyle = "stroke-dasharray: 5,5"
|
||||
|
||||
def dotGraph(
|
||||
graph: ModuleGraph,
|
||||
dotHead: String,
|
||||
nodeFormation: (String, String, String) => String,
|
||||
labelRendering: HTMLLabelRendering
|
||||
): String = {
|
||||
val nodes = {
|
||||
for (n ← graph.nodes) yield {
|
||||
val style = if (n.isEvicted) EvictedStyle else ""
|
||||
val label = nodeFormation(n.id.organization, n.id.name, n.id.version)
|
||||
""" "%s"[%s style="%s"]""".format(
|
||||
n.id.idString,
|
||||
labelRendering.renderLabel(label),
|
||||
style
|
||||
)
|
||||
}
|
||||
}.mkString("\n")
|
||||
|
||||
def originWasEvicted(edge: Edge): Boolean = graph.module(edge._1).isEvicted
|
||||
def targetWasEvicted(edge: Edge): Boolean = graph.module(edge._2).isEvicted
|
||||
|
||||
// add extra edges from evicted to evicted-by module
|
||||
val evictedByEdges: Seq[Edge] =
|
||||
graph.nodes
|
||||
.filter(_.isEvicted)
|
||||
.map(m => Edge(m.id, m.id.copy(version = m.evictedByVersion.get)))
|
||||
|
||||
// remove edges to new evicted-by module which is now replaced by a chain
|
||||
// dependend -> [evicted] -> dependee
|
||||
val evictionTargetEdges =
|
||||
graph.edges
|
||||
.filter(targetWasEvicted)
|
||||
.map {
|
||||
case (from, evicted) =>
|
||||
(from, evicted.copy(version = graph.module(evicted).evictedByVersion.get))
|
||||
}
|
||||
.toSet
|
||||
|
||||
val filteredEdges =
|
||||
graph.edges
|
||||
.filterNot(e => originWasEvicted(e) || evictionTargetEdges(e)) ++ evictedByEdges
|
||||
|
||||
val edges = {
|
||||
for (e ← filteredEdges) yield {
|
||||
val extra =
|
||||
if (graph.module(e._1).isEvicted)
|
||||
s""" [label="Evicted By" style="$EvictedStyle"]"""
|
||||
else ""
|
||||
""" "%s" -> "%s"%s""".format(e._1.idString, e._2.idString, extra)
|
||||
}
|
||||
}.mkString("\n")
|
||||
|
||||
"%s\n%s\n%s\n}".format(dotHead, nodes, edges)
|
||||
}
|
||||
|
||||
sealed trait HTMLLabelRendering {
|
||||
def renderLabel(labelText: String): String
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML labels in Angle brackets as defined at http://graphviz.org/content/node-shapes#html
|
||||
*/
|
||||
case object AngleBrackets extends HTMLLabelRendering {
|
||||
def renderLabel(labelText: String): String = s"label=<$labelText>"
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML labels with `labelType="html"` and label content in double quotes as supported by
|
||||
* dagre-d3
|
||||
*/
|
||||
case object LabelTypeHtml extends HTMLLabelRendering {
|
||||
def renderLabel(labelText: String): String = s"""labelType="html" label="$labelText""""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
package rendering
|
||||
|
||||
import java.io.File
|
||||
import java.net.{ URLEncoder, URI }
|
||||
|
||||
import sbt.io.IO
|
||||
|
||||
object DagreHTML {
|
||||
def createLink(dotGraph: String, targetDirectory: File): URI = {
|
||||
targetDirectory.mkdirs()
|
||||
val graphHTML = new File(targetDirectory, "graph.html")
|
||||
TreeView.saveResource("graph.html", graphHTML)
|
||||
IO.write(new File(targetDirectory, "dependencies.dot"), dotGraph, IO.utf8)
|
||||
|
||||
val graphString =
|
||||
URLEncoder
|
||||
.encode(dotGraph, "utf8")
|
||||
.replaceAllLiterally("+", "%20")
|
||||
|
||||
IO.write(
|
||||
new File(targetDirectory, "dependencies.dot.js"),
|
||||
s"""data = "$graphString";""",
|
||||
IO.utf8
|
||||
)
|
||||
|
||||
new URI(graphHTML.toURI.toString)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
package rendering
|
||||
|
||||
object FlatList {
|
||||
def render(display: Module => String)(graph: ModuleGraph): String =
|
||||
graph.modules.values.toSeq.distinct
|
||||
.filterNot(_.isEvicted)
|
||||
.sortBy(m => (m.id.organization, m.id.name))
|
||||
.map(display)
|
||||
.mkString("\n")
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
package rendering
|
||||
|
||||
import scala.xml.XML
|
||||
|
||||
object GraphML {
|
||||
def saveAsGraphML(graph: ModuleGraph, outputFile: String): Unit = {
|
||||
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,26 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
package rendering
|
||||
|
||||
object LicenseInfo {
|
||||
def render(graph: ModuleGraph): String =
|
||||
graph.nodes
|
||||
.filter(_.isUsed)
|
||||
.groupBy(_.license)
|
||||
.toSeq
|
||||
.sortBy(_._1)
|
||||
.map {
|
||||
case (license, modules) =>
|
||||
license.getOrElse("No license specified") + "\n" +
|
||||
modules.map(_.id.idString formatted "\t %s").mkString("\n")
|
||||
}
|
||||
.mkString("\n\n")
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
package rendering
|
||||
|
||||
object Statistics {
|
||||
def renderModuleStatsList(graph: ModuleGraph): String = {
|
||||
case class ModuleStats(
|
||||
id: GraphModuleId,
|
||||
numDirectDependencies: Int,
|
||||
numTransitiveDependencies: Int,
|
||||
selfSize: Option[Long],
|
||||
transitiveSize: Long,
|
||||
transitiveDependencyStats: Map[GraphModuleId, ModuleStats]
|
||||
) {
|
||||
def transitiveStatsWithSelf: Map[GraphModuleId, ModuleStats] =
|
||||
transitiveDependencyStats + (id -> this)
|
||||
}
|
||||
|
||||
def statsFor(moduleId: GraphModuleId): ModuleStats = {
|
||||
val directDependencies = graph.dependencyMap(moduleId).filterNot(_.isEvicted).map(_.id)
|
||||
val dependencyStats =
|
||||
directDependencies.map(statsFor).flatMap(_.transitiveStatsWithSelf).toMap
|
||||
val selfSize = graph.module(moduleId).jarFile.filter(_.exists).map(_.length)
|
||||
val numDirectDependencies = directDependencies.size
|
||||
val numTransitiveDependencies = dependencyStats.size
|
||||
val transitiveSize = selfSize.getOrElse(0L) + dependencyStats
|
||||
.map(_._2.selfSize.getOrElse(0L))
|
||||
.sum
|
||||
|
||||
ModuleStats(
|
||||
moduleId,
|
||||
numDirectDependencies,
|
||||
numTransitiveDependencies,
|
||||
selfSize,
|
||||
transitiveSize,
|
||||
dependencyStats
|
||||
)
|
||||
}
|
||||
|
||||
def format(stats: ModuleStats): String = {
|
||||
import stats._
|
||||
def mb(bytes: Long): Double = bytes.toDouble / 1000000
|
||||
val selfSize =
|
||||
stats.selfSize match {
|
||||
case Some(size) => f"${mb(size)}%7.3f"
|
||||
case None => "-------"
|
||||
}
|
||||
f"${mb(transitiveSize)}%7.3f MB $selfSize MB $numTransitiveDependencies%4d $numDirectDependencies%4d ${id.idString}%s"
|
||||
}
|
||||
|
||||
val allStats =
|
||||
graph.roots
|
||||
.flatMap(r => statsFor(r.id).transitiveStatsWithSelf)
|
||||
.toMap
|
||||
.values
|
||||
.toSeq
|
||||
.sortBy(s => (-s.transitiveSize, -s.numTransitiveDependencies))
|
||||
|
||||
val header = " TotSize JarSize #TDe #Dep Module\n"
|
||||
|
||||
header +
|
||||
allStats.map(format).mkString("\n") +
|
||||
"""
|
||||
|
|
||||
|Columns are
|
||||
| - Jar-Size including dependencies
|
||||
| - Jar-Size
|
||||
| - Number of transitive dependencies
|
||||
| - Number of direct dependencies
|
||||
| - ModuleID""".stripMargin
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
package graph
|
||||
package rendering
|
||||
|
||||
import java.io.{ OutputStream, InputStream, FileOutputStream, File }
|
||||
import java.net.URI
|
||||
|
||||
import graph.{ Module, ModuleGraph }
|
||||
import sbt.io.IO
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.parsing.json.{ JSONArray, JSONObject }
|
||||
import com.github.ghik.silencer.silent
|
||||
|
||||
@silent object TreeView {
|
||||
def createJson(graph: ModuleGraph): String = {
|
||||
val trees = graph.roots
|
||||
.map(module => processSubtree(graph, module))
|
||||
.toList
|
||||
JSONArray(trees).toString
|
||||
}
|
||||
|
||||
def createLink(graphJson: String, targetDirectory: File): URI = {
|
||||
targetDirectory.mkdirs()
|
||||
val graphHTML = new File(targetDirectory, "tree.html")
|
||||
saveResource("tree.html", graphHTML)
|
||||
IO.write(new File(targetDirectory, "tree.json"), graphJson, IO.utf8)
|
||||
IO.write(new File(targetDirectory, "tree.data.js"), s"tree_data = $graphJson;", IO.utf8)
|
||||
new URI(graphHTML.toURI.toString)
|
||||
}
|
||||
|
||||
private def processSubtree(graph: ModuleGraph, module: Module): JSONObject = {
|
||||
val children = graph.dependencyMap
|
||||
.getOrElse(module.id, List())
|
||||
.map(module => processSubtree(graph, module))
|
||||
.toList
|
||||
moduleAsJson(module, children)
|
||||
}
|
||||
|
||||
private def moduleAsJson(module: Module, children: List[JSONObject]): JSONObject = {
|
||||
val eviction = module.evictedByVersion.map(version => s" (evicted by $version)").getOrElse("")
|
||||
val error = module.error.map(err => s" (errors: $err)").getOrElse("")
|
||||
val text = module.id.idString + eviction + error
|
||||
JSONObject(Map("text" -> text, "children" -> JSONArray(children)))
|
||||
}
|
||||
|
||||
def saveResource(resourcePath: String, to: File): Unit = {
|
||||
val is = getClass.getClassLoader.getResourceAsStream(resourcePath)
|
||||
require(is ne null, s"Couldn't load '$resourcePath' from classpath.")
|
||||
|
||||
val fos = new FileOutputStream(to)
|
||||
try copy(is, fos)
|
||||
finally {
|
||||
is.close()
|
||||
fos.close()
|
||||
}
|
||||
}
|
||||
|
||||
def copy(from: InputStream, to: OutputStream): Unit = {
|
||||
val buffer = new Array[Byte](65536)
|
||||
|
||||
@tailrec def rec(): Unit = {
|
||||
val read = from.read(buffer)
|
||||
if (read > 0) {
|
||||
to.write(buffer, 0, read)
|
||||
rec()
|
||||
} else if (read == 0)
|
||||
throw new IllegalStateException("InputStream.read returned 0")
|
||||
}
|
||||
rec()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package plugins
|
||||
|
||||
import java.io.File
|
||||
|
||||
import sbt.Def._
|
||||
import sbt.Keys._
|
||||
import sbt.SlashSyntax0._
|
||||
import sbt.PluginTrigger.AllRequirements
|
||||
import sbt.Project._
|
||||
import sbt.internal.graph._
|
||||
import sbt.internal.graph.backend.{ IvyReport, SbtUpdateReport }
|
||||
import sbt.internal.graph.rendering.{ DagreHTML, TreeView }
|
||||
import sbt.internal.librarymanagement._
|
||||
import sbt.internal.util.complete.{ Parser, Parsers }
|
||||
import sbt.io.IO
|
||||
import sbt.io.syntax._
|
||||
import sbt.librarymanagement._
|
||||
import sbt.librarymanagement.ivy.InlineIvyConfiguration
|
||||
import sbt.librarymanagement.Configurations.{
|
||||
Compile,
|
||||
IntegrationTest,
|
||||
Optional,
|
||||
Provided,
|
||||
Runtime,
|
||||
Test
|
||||
}
|
||||
// import Keys._
|
||||
|
||||
object DependencyGraphPlugin extends AutoPlugin {
|
||||
import sjsonnew.BasicJsonProtocol._
|
||||
|
||||
object autoImport extends DependencyGraphKeys
|
||||
|
||||
import autoImport._
|
||||
override def trigger: PluginTrigger = AllRequirements
|
||||
override def globalSettings: Seq[Def.Setting[_]] = Seq(
|
||||
dependencyTreeIncludeScalaLibrary := false
|
||||
)
|
||||
override def projectSettings: Seq[Def.Setting[_]] = graphSettings
|
||||
def graphSettings = baseSettings ++ reportSettings
|
||||
|
||||
def baseSettings =
|
||||
Seq(
|
||||
ivyReportFunction := ivyReportFunctionTask.value,
|
||||
// disable the cached resolution engine (exposing a scoped `ivyModule` used directly by `updateTask`), as it
|
||||
// generates artificial module descriptors which are internal to sbt, making it hard to reconstruct the
|
||||
// dependency tree
|
||||
ignoreMissingUpdate / updateOptions := updateOptions.value.withCachedResolution(false),
|
||||
ignoreMissingUpdate / ivyConfiguration := {
|
||||
// inTask will make sure the new definition will pick up `updateOptions in ignoreMissingUpdate`
|
||||
inTask(ignoreMissingUpdate, Classpaths.mkIvyConfiguration).value
|
||||
},
|
||||
ignoreMissingUpdate / ivyModule := {
|
||||
// concatenating & inlining ivySbt & ivyModule default task implementations, as `SbtAccess.inTask` does
|
||||
// NOT correctly force the scope when applied to `TaskKey.toTask` instances (as opposed to raw
|
||||
// implementations like `Classpaths.mkIvyConfiguration` or `Classpaths.updateTask`)
|
||||
val is = new IvySbt((ivyConfiguration in ignoreMissingUpdate).value)
|
||||
new is.Module(moduleSettings.value)
|
||||
},
|
||||
// don't fail on missing dependencies
|
||||
ignoreMissingUpdate / updateConfiguration := updateConfiguration.value.withMissingOk(true),
|
||||
ignoreMissingUpdate := {
|
||||
// inTask will make sure the new definition will pick up `ivyModule/updateConfiguration in ignoreMissingUpdate`
|
||||
inTask(ignoreMissingUpdate, Classpaths.updateTask).value
|
||||
},
|
||||
)
|
||||
|
||||
def reportSettings =
|
||||
Seq(Compile, Test, IntegrationTest, Runtime, Provided, Optional).flatMap(ivyReportForConfig)
|
||||
|
||||
val renderingAlternatives: Seq[(TaskKey[Unit], ModuleGraph => String)] =
|
||||
Seq(
|
||||
dependencyTree -> rendering.AsciiTree.asciiTree _,
|
||||
dependencyList -> rendering.FlatList.render(_.id.idString),
|
||||
dependencyStats -> rendering.Statistics.renderModuleStatsList _,
|
||||
dependencyLicenseInfo -> rendering.LicenseInfo.render _
|
||||
)
|
||||
|
||||
def ivyReportForConfig(config: Configuration) =
|
||||
inConfig(config)(
|
||||
Seq(
|
||||
ivyReport := {
|
||||
Def
|
||||
.task {
|
||||
ivyReportFunction.value.apply(config.toString)
|
||||
}
|
||||
.dependsOn(ignoreMissingUpdate)
|
||||
}.value,
|
||||
crossProjectId := CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(
|
||||
projectID.value
|
||||
),
|
||||
moduleGraphSbt :=
|
||||
ignoreMissingUpdate.value
|
||||
.configuration(configuration.value)
|
||||
.map(report => SbtUpdateReport.fromConfigurationReport(report, crossProjectId.value))
|
||||
.getOrElse(ModuleGraph.empty),
|
||||
moduleGraphIvyReport := IvyReport.fromReportFile(absoluteReportPath(ivyReport.value)),
|
||||
moduleGraph := {
|
||||
sbtVersion.value match {
|
||||
case Version(0, 13, x, _) if x >= 6 => moduleGraphSbt.value
|
||||
case Version(1, _, _, _) => moduleGraphSbt.value
|
||||
}
|
||||
},
|
||||
moduleGraph := {
|
||||
// FIXME: remove busywork
|
||||
val sv = scalaVersion.value
|
||||
val moduleGraph = DependencyGraphKeys.moduleGraph.value
|
||||
if (dependencyTreeIncludeScalaLibrary.value) moduleGraph
|
||||
else GraphTransformations.ignoreScalaLibrary(sv, moduleGraph)
|
||||
},
|
||||
moduleGraphStore := (moduleGraph storeAs moduleGraphStore triggeredBy moduleGraph).value,
|
||||
// browse
|
||||
dependencyBrowseGraphTarget := { target.value / "browse-dependency-graph" },
|
||||
dependencyBrowseGraphHTML := browseGraphHTMLTask.value,
|
||||
dependencyBrowseGraph := openBrowser(dependencyBrowseGraphHTML).value,
|
||||
dependencyBrowseTreeTarget := { target.value / "browse-dependency-tree" },
|
||||
dependencyBrowseTreeHTML := browseTreeHTMLTask.value,
|
||||
dependencyBrowseTree := openBrowser(dependencyBrowseTreeHTML).value,
|
||||
// dot support
|
||||
dependencyDotFile := { target.value / "dependencies-%s.dot".format(config.toString) },
|
||||
dependencyDot / asString := rendering.DOT.dotGraph(
|
||||
moduleGraph.value,
|
||||
dependencyDotHeader.value,
|
||||
dependencyDotNodeLabel.value,
|
||||
rendering.DOT.AngleBrackets
|
||||
),
|
||||
dependencyDot := writeToFile(dependencyDot / asString, dependencyDotFile).value,
|
||||
dependencyDotHeader :=
|
||||
"""|digraph "dependency-graph" {
|
||||
| graph[rankdir="LR"]
|
||||
| edge [
|
||||
| arrowtail="none"
|
||||
| ]""".stripMargin,
|
||||
dependencyDotNodeLabel := { (organization: String, name: String, version: String) =>
|
||||
"""%s<BR/><B>%s</B><BR/>%s""".format(organization, name, version)
|
||||
},
|
||||
// GraphML support
|
||||
dependencyGraphMLFile := {
|
||||
target.value / "dependencies-%s.graphml".format(config.toString)
|
||||
},
|
||||
dependencyGraphML := dependencyGraphMLTask.value,
|
||||
whatDependsOn := {
|
||||
val ArtifactPattern(org, name, versionFilter) = artifactPatternParser.parsed
|
||||
val graph = moduleGraph.value
|
||||
val modules =
|
||||
versionFilter match {
|
||||
case Some(version) => GraphModuleId(org, name, version) :: Nil
|
||||
case None =>
|
||||
graph.nodes.filter(m => m.id.organization == org && m.id.name == name).map(_.id)
|
||||
}
|
||||
val output =
|
||||
modules
|
||||
.map { module =>
|
||||
rendering.AsciiTree
|
||||
.asciiTree(GraphTransformations.reverseGraphStartingAt(graph, module))
|
||||
}
|
||||
.mkString("\n")
|
||||
|
||||
streams.value.log.info(output)
|
||||
output
|
||||
},
|
||||
) ++
|
||||
renderingAlternatives.flatMap((renderingTaskSettings _).tupled)
|
||||
)
|
||||
|
||||
def renderingTaskSettings(key: TaskKey[Unit], renderer: ModuleGraph => String): Seq[Setting[_]] =
|
||||
Seq(
|
||||
key := {
|
||||
val s = streams.value
|
||||
val str = (key / asString).value
|
||||
s.log.info(str)
|
||||
},
|
||||
key / asString := renderer(moduleGraph.value),
|
||||
key / toFile := {
|
||||
val (targetFile, force) = targetFileAndForceParser.parsed
|
||||
writeToFile(key.key.label, (asString in key).value, targetFile, force, streams.value)
|
||||
},
|
||||
)
|
||||
|
||||
def ivyReportFunctionTask = Def.task {
|
||||
val ivyConfig = Keys.ivyConfiguration.value.asInstanceOf[InlineIvyConfiguration]
|
||||
val projectID = Keys.projectID.value
|
||||
val ivyModule = Keys.ivyModule.value
|
||||
|
||||
(config: String) => {
|
||||
val org = projectID.organization
|
||||
val name = crossName(ivyModule)
|
||||
new File(ivyConfig.resolutionCacheDir.get, s"reports/$org-$name-$config.xml")
|
||||
}
|
||||
}
|
||||
|
||||
def dependencyGraphMLTask =
|
||||
Def.task {
|
||||
val resultFile = dependencyGraphMLFile.value
|
||||
rendering.GraphML.saveAsGraphML(moduleGraph.value, resultFile.getAbsolutePath)
|
||||
streams.value.log.info("Wrote dependency graph to '%s'" format resultFile)
|
||||
resultFile
|
||||
}
|
||||
|
||||
def browseGraphHTMLTask =
|
||||
Def.task {
|
||||
val dotGraph = rendering.DOT.dotGraph(
|
||||
moduleGraph.value,
|
||||
dependencyDotHeader.value,
|
||||
dependencyDotNodeLabel.value,
|
||||
rendering.DOT.LabelTypeHtml
|
||||
)
|
||||
val link = DagreHTML.createLink(dotGraph, target.value)
|
||||
streams.value.log.info(s"HTML graph written to $link")
|
||||
link
|
||||
}
|
||||
|
||||
def browseTreeHTMLTask =
|
||||
Def.task {
|
||||
val renderedTree = TreeView.createJson(moduleGraph.value)
|
||||
val link = TreeView.createLink(renderedTree, target.value)
|
||||
streams.value.log.info(s"HTML tree written to $link")
|
||||
link
|
||||
}
|
||||
|
||||
def writeToFile(dataTask: TaskKey[String], fileTask: SettingKey[File]) =
|
||||
Def.task {
|
||||
val outFile = fileTask.value
|
||||
IO.write(outFile, dataTask.value, IO.utf8)
|
||||
|
||||
streams.value.log.info("Wrote dependency graph to '%s'" format outFile)
|
||||
outFile
|
||||
}
|
||||
|
||||
def writeToFile(
|
||||
what: String,
|
||||
data: String,
|
||||
targetFile: File,
|
||||
force: Boolean,
|
||||
streams: TaskStreams
|
||||
): File =
|
||||
if (targetFile.exists && !force)
|
||||
throw new RuntimeException(
|
||||
s"Target file for $what already exists at ${targetFile.getAbsolutePath}. Use '-f' to override"
|
||||
)
|
||||
else {
|
||||
IO.write(targetFile, data, IO.utf8)
|
||||
|
||||
streams.log.info(s"Wrote $what to '$targetFile'")
|
||||
targetFile
|
||||
}
|
||||
|
||||
def absoluteReportPath = (file: File) => file.getAbsolutePath
|
||||
|
||||
def openBrowser(uriKey: TaskKey[URI]) =
|
||||
Def.task {
|
||||
val uri = uriKey.value
|
||||
streams.value.log.info("Opening in browser...")
|
||||
java.awt.Desktop.getDesktop.browse(uri)
|
||||
uri
|
||||
}
|
||||
|
||||
case class ArtifactPattern(organization: String, name: String, version: Option[String])
|
||||
|
||||
import sbt.internal.util.complete.DefaultParsers._
|
||||
val artifactPatternParser: Def.Initialize[State => Parser[ArtifactPattern]] =
|
||||
Keys.resolvedScoped { ctx => (state: State) =>
|
||||
val graph = Defaults.loadFromContext(moduleGraphStore, ctx, state) getOrElse ModuleGraph(
|
||||
Nil,
|
||||
Nil
|
||||
)
|
||||
|
||||
graph.nodes
|
||||
.map(_.id)
|
||||
.groupBy(m => (m.organization, m.name))
|
||||
.map {
|
||||
case ((org, name), modules) =>
|
||||
val versionParsers: Seq[Parser[Option[String]]] =
|
||||
modules.map { id =>
|
||||
token(Space ~> id.version).?
|
||||
}
|
||||
|
||||
(Space ~> token(org) ~ token(Space ~> name) ~ oneOf(versionParsers)).map {
|
||||
case ((org, name), version) => ArtifactPattern(org, name, version)
|
||||
}
|
||||
}
|
||||
.reduceOption(_ | _)
|
||||
.getOrElse {
|
||||
// If the moduleGraphStore couldn't be loaded because no dependency tree command was run before, we should still provide a parser for the command.
|
||||
((Space ~> token(StringBasic, "<organization>")) ~ (Space ~> token(
|
||||
StringBasic,
|
||||
"<module>"
|
||||
)) ~ (Space ~> token(StringBasic, "<version?>")).?).map {
|
||||
case ((org, mod), version) =>
|
||||
ArtifactPattern(org, mod, version)
|
||||
}
|
||||
}
|
||||
}
|
||||
val shouldForceParser: Parser[Boolean] =
|
||||
(Space ~> (Parser.literal("-f") | "--force")).?.map(_.isDefined)
|
||||
|
||||
val targetFileAndForceParser: Parser[(File, Boolean)] =
|
||||
Parsers.fileParser(new File(".")) ~ shouldForceParser
|
||||
|
||||
// This is to support 0.13.8's InlineConfigurationWithExcludes while not forcing 0.13.8
|
||||
type HasModule = {
|
||||
val module: ModuleID
|
||||
}
|
||||
def crossName(ivyModule: IvySbt#Module) =
|
||||
ivyModule.moduleSettings match {
|
||||
case ic: ModuleDescriptorConfiguration => ic.module.name
|
||||
case _ =>
|
||||
throw new IllegalStateException(
|
||||
"sbt-dependency-graph plugin currently only supports ModuleDescriptorConfiguration of ivy settings (the default in sbt)"
|
||||
)
|
||||
}
|
||||
|
||||
val VersionPattern = """(\d+)\.(\d+)\.(\d+)(?:-(.*))?""".r
|
||||
object Version {
|
||||
def unapply(str: String): Option[(Int, Int, Int, Option[String])] = str match {
|
||||
case VersionPattern(major, minor, fix, appendix) =>
|
||||
Some((major.toInt, minor.toInt, fix.toInt, Option(appendix)))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
scalaVersion := "2.12.9"
|
||||
|
||||
libraryDependencies += "org.slf4j" % "slf4j-api" % "1.7.28"
|
||||
updateOptions := updateOptions.value.withCachedResolution(true)
|
||||
|
||||
TaskKey[Unit]("check") := {
|
||||
val report = (Test / ivyReport).value
|
||||
val graph = (Test / dependencyTree / asString).value
|
||||
|
||||
def sanitize(str: String): String = str.split('\n').drop(1).mkString("\n")
|
||||
val expectedGraph =
|
||||
"""default:cachedresolution_2.12:0.1.0-SNAPSHOT
|
||||
| +-org.slf4j:slf4j-api:1.7.28
|
||||
| """.stripMargin
|
||||
require(sanitize(graph) == sanitize(expectedGraph), "Graph for report %s was '\n%s' but should have been '\n%s'" format (report, sanitize(graph), sanitize(expectedGraph)))
|
||||
()
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
> check
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
scalaVersion := "2.9.2"
|
||||
|
||||
libraryDependencies ++= Seq(
|
||||
"org.slf4j" % "slf4j-api" % "1.7.2",
|
||||
"ch.qos.logback" % "logback-classic" % "1.0.7"
|
||||
)
|
||||
|
||||
TaskKey[Unit]("check") := {
|
||||
val report = updateFull.value
|
||||
val graph = (Test / dependencyTree / asString).value
|
||||
def sanitize(str: String): String = str.split('\n').drop(1).map(_.trim).mkString("\n")
|
||||
val expectedGraph =
|
||||
"""default:default-e95e05_2.9.2:0.1-SNAPSHOT [S]
|
||||
| +-ch.qos.logback:logback-classic:1.0.7
|
||||
| | +-ch.qos.logback:logback-core:1.0.7
|
||||
| | +-org.slf4j:slf4j-api:1.6.6 (evicted by: 1.7.2)
|
||||
| | +-org.slf4j:slf4j-api:1.7.2
|
||||
| |
|
||||
| +-org.slf4j:slf4j-api:1.7.2
|
||||
| """.stripMargin
|
||||
IO.writeLines(file("/tmp/blib"), sanitize(graph).split("\n"))
|
||||
IO.writeLines(file("/tmp/blub"), sanitize(expectedGraph).split("\n"))
|
||||
require(sanitize(graph) == sanitize(expectedGraph), "Graph for report %s was '\n%s' but should have been '\n%s'" format (report, sanitize(graph), sanitize(expectedGraph)))
|
||||
()
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
> check
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
scalaVersion := "2.9.2"
|
||||
|
||||
libraryDependencies +=
|
||||
"at.blub" % "blib" % "1.2.3" % "test"
|
||||
|
||||
TaskKey[Unit]("check") := {
|
||||
val report = updateFull.value
|
||||
val graph = (Test / dependencyTree / asString).value
|
||||
|
||||
def sanitize(str: String): String = str.split('\n').drop(1).mkString("\n")
|
||||
val expectedGraph =
|
||||
"""default:default-91180e_2.9.2:0.1-SNAPSHOT
|
||||
| +-%sat.blub:blib:1.2.3 (error: not found)%s
|
||||
| """.stripMargin.format(scala.Console.RED, scala.Console.RESET)
|
||||
require(sanitize(graph) == sanitize(expectedGraph), "Graph for report %s was '\n%s' but should have been '\n%s'" format (report, sanitize(graph), sanitize(expectedGraph)))
|
||||
()
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
> check
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
ThisBuild / scalaVersion := "2.9.2"
|
||||
ThisBuild / version := "0.1-SNAPSHOT"
|
||||
|
||||
lazy val justATransiviteDependencyEndpointProject = project
|
||||
|
||||
lazy val justATransitiveDependencyProject = project
|
||||
.dependsOn(justATransiviteDependencyEndpointProject)
|
||||
|
||||
lazy val justADependencyProject = project
|
||||
|
||||
lazy val test_project = project
|
||||
.dependsOn(justADependencyProject, justATransitiveDependencyProject)
|
||||
.settings(
|
||||
TaskKey[Unit]("check") := {
|
||||
val dotFile = (dependencyDot in Compile).value
|
||||
val expectedGraph =
|
||||
"""digraph "dependency-graph" {
|
||||
| graph[rankdir="LR"]
|
||||
| edge [
|
||||
| arrowtail="none"
|
||||
| ]
|
||||
| "test_project:test_project_2.9.2:0.1-SNAPSHOT"[label=<test_project<BR/><B>test_project_2.9.2</B><BR/>0.1-SNAPSHOT> style=""]
|
||||
| "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT"[label=<justatransitivedependencyproject<BR/><B>justatransitivedependencyproject_2.9.2</B><BR/>0.1-SNAPSHOT> style=""]
|
||||
| "justatransivitedependencyendpointproject:justatransivitedependencyendpointproject_2.9.2:0.1-SNAPSHOT"[label=<justatransivitedependencyendpointproject<BR/><B>justatransivitedependencyendpointproject_2.9.2</B><BR/>0.1-SNAPSHOT> style=""]
|
||||
| "justadependencyproject:justadependencyproject_2.9.2:0.1-SNAPSHOT"[label=<justadependencyproject<BR/><B>justadependencyproject_2.9.2</B><BR/>0.1-SNAPSHOT> style=""]
|
||||
| "test_project:test_project_2.9.2:0.1-SNAPSHOT" -> "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT"
|
||||
| "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT" -> "justatransivitedependencyendpointproject:justatransivitedependencyendpointproject_2.9.2:0.1-SNAPSHOT"
|
||||
| "test_project:test_project_2.9.2:0.1-SNAPSHOT" -> "justadependencyproject:justadependencyproject_2.9.2:0.1-SNAPSHOT"
|
||||
|}
|
||||
""".stripMargin
|
||||
|
||||
val graph : String = scala.io.Source.fromFile(dotFile.getAbsolutePath).mkString
|
||||
val errors = compareByLine(graph, expectedGraph)
|
||||
require(errors.isEmpty , errors.mkString("\n"))
|
||||
()
|
||||
}
|
||||
)
|
||||
|
||||
def compareByLine(got : String, expected : String) : Seq[String] = {
|
||||
val errors = ListBuffer[String]()
|
||||
got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach { case((got_line : String, expected_line : String), i : Int) =>
|
||||
if(got_line != expected_line) {
|
||||
errors.append(
|
||||
"""not matching lines at line %s
|
||||
|expected: %s
|
||||
|got: %s
|
||||
|""".stripMargin.format(i,expected_line, got_line))
|
||||
}
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
> test_project/check
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// ThisBuild / useCoursier := false
|
||||
ThisBuild / scalaVersion := "2.12.6"
|
||||
ThisBuild / organization := "org.example"
|
||||
ThisBuild / version := "0.1"
|
||||
|
||||
name := "blubber"
|
||||
libraryDependencies += "org.typelevel" %% "cats-effect" % "2.2.0"
|
||||
TaskKey[Unit]("check") := {
|
||||
val candidates = "tree list stats licenses".split(' ').map(_.trim)
|
||||
candidates.foreach { c =>
|
||||
val expected = new File(s"expected/$c.txt")
|
||||
val actual = new File(s"target/$c.txt")
|
||||
|
||||
import sys.process._
|
||||
val exit = s"diff -U3 ${expected.getPath} ${actual.getPath}".!
|
||||
require(exit == 0, s"Diff was non-zero for ${actual.getName}")
|
||||
}
|
||||
|
||||
//require(sanitize(graph) == sanitize(expectedGraph), "Graph for report %s was '\n%s' but should have been '\n%s'" format (report, sanitize(graph), sanitize(expectedGraph)))
|
||||
()
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
No license specified
|
||||
org.example:blubber_2.12:0.1
|
||||
|
||||
Apache-2.0
|
||||
org.typelevel:cats-effect_2.12:2.2.0
|
||||
|
||||
MIT
|
||||
org.typelevel:cats-kernel_2.12:2.2.0
|
||||
org.typelevel:cats-core_2.12:2.2.0
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
org.example:blubber_2.12:0.1
|
||||
org.typelevel:cats-core_2.12:2.2.0
|
||||
org.typelevel:cats-effect_2.12:2.2.0
|
||||
org.typelevel:cats-kernel_2.12:2.2.0
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
TotSize JarSize #TDe #Dep Module
|
||||
11.177 MB ------- MB 3 1 org.example:blubber_2.12:0.1
|
||||
11.177 MB 1.185 MB 2 1 org.typelevel:cats-effect_2.12:2.2.0
|
||||
9.992 MB 5.034 MB 1 1 org.typelevel:cats-core_2.12:2.2.0
|
||||
4.958 MB 4.958 MB 0 0 org.typelevel:cats-kernel_2.12:2.2.0
|
||||
|
||||
Columns are
|
||||
- Jar-Size including dependencies
|
||||
- Jar-Size
|
||||
- Number of transitive dependencies
|
||||
- Number of direct dependencies
|
||||
- ModuleID
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
org.example:blubber_2.12:0.1 [S]
|
||||
+-org.typelevel:cats-effect_2.12:2.2.0 [S]
|
||||
+-org.typelevel:cats-core_2.12:2.2.0 [S]
|
||||
+-org.typelevel:cats-kernel_2.12:2.2.0 [S]
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
> dependencyTree/toFile target/tree.txt
|
||||
> dependencyList/toFile target/list.txt
|
||||
> dependencyStats/toFile target/stats.txt
|
||||
> dependencyLicenseInfo/toFile target/licenses.txt
|
||||
> check
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
ThisBuild / useCoursier := false
|
||||
|
||||
version := "0.1.0-SNAPSHOT"
|
||||
|
||||
organization := "default"
|
||||
|
||||
name := "whatDependsOn"
|
||||
|
||||
scalaVersion := "2.9.1"
|
||||
|
||||
resolvers += "typesafe maven" at "https://repo.typesafe.com/typesafe/maven-releases/"
|
||||
|
||||
libraryDependencies ++= Seq(
|
||||
"com.codahale" % "jerkson_2.9.1" % "0.5.0",
|
||||
"org.codehaus.jackson" % "jackson-mapper-asl" % "1.9.10" // as another version of asl
|
||||
)
|
||||
|
||||
val check = TaskKey[Unit]("check")
|
||||
|
||||
check := {
|
||||
def sanitize(str: String): String = str.split('\n').map(_.trim).mkString("\n")
|
||||
def checkOutput(output: String, expected: String): Unit =
|
||||
require(sanitize(expected) == sanitize(output), s"Tree should have been [\n${sanitize(expected)}\n] but was [\n${sanitize(output)}\n]")
|
||||
|
||||
val withVersion =
|
||||
(whatDependsOn in Compile)
|
||||
.toTask(" org.codehaus.jackson jackson-core-asl 1.9.11")
|
||||
.value
|
||||
val expectedGraphWithVersion =
|
||||
"""org.codehaus.jackson:jackson-core-asl:1.9.11
|
||||
| +-com.codahale:jerkson_2.9.1:0.5.0 [S]
|
||||
| | +-default:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
| |
|
||||
| +-org.codehaus.jackson:jackson-mapper-asl:1.9.11
|
||||
| +-com.codahale:jerkson_2.9.1:0.5.0 [S]
|
||||
| | +-default:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
| |
|
||||
| +-default:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
| """.stripMargin
|
||||
|
||||
checkOutput(withVersion, expectedGraphWithVersion)
|
||||
|
||||
val withoutVersion =
|
||||
(whatDependsOn in Compile)
|
||||
.toTask(" org.codehaus.jackson jackson-mapper-asl")
|
||||
.value
|
||||
val expectedGraphWithoutVersion =
|
||||
"""org.codehaus.jackson:jackson-mapper-asl:1.9.11
|
||||
| +-com.codahale:jerkson_2.9.1:0.5.0 [S]
|
||||
| | +-default:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
| |
|
||||
| +-default:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
|
|
||||
|org.codehaus.jackson:jackson-mapper-asl:1.9.10 (evicted by: 1.9.11)
|
||||
| +-default:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
| """.stripMargin
|
||||
checkOutput(withoutVersion, expectedGraphWithoutVersion)
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# same as whatDependsOn test but without the initialization to prime the parser
|
||||
> check
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
ThisBuild / version := "0.1.0-SNAPSHOT"
|
||||
ThisBuild / scalaVersion := "2.9.1"
|
||||
|
||||
name := "whatDependsOn"
|
||||
|
||||
resolvers += "typesafe maven" at "https://repo.typesafe.com/typesafe/maven-releases/"
|
||||
|
||||
libraryDependencies ++= Seq(
|
||||
"com.codahale" % "jerkson_2.9.1" % "0.5.0",
|
||||
"org.codehaus.jackson" % "jackson-mapper-asl" % "1.9.10" // as another version of asl
|
||||
)
|
||||
|
||||
val check = TaskKey[Unit]("check")
|
||||
|
||||
check := {
|
||||
def sanitize(str: String): String = str.split('\n').map(_.trim).mkString("\n")
|
||||
def checkOutput(output: String, expected: String): Unit =
|
||||
require(sanitize(expected) == sanitize(output), s"Tree should have been [\n${sanitize(expected)}\n] but was [\n${sanitize(output)}\n]")
|
||||
|
||||
val withVersion =
|
||||
(whatDependsOn in Compile)
|
||||
.toTask(" org.codehaus.jackson jackson-core-asl 1.9.10")
|
||||
.value
|
||||
val expectedGraphWithVersion =
|
||||
"""org.codehaus.jackson:jackson-core-asl:1.9.10
|
||||
| +-com.codahale:jerkson_2.9.1:0.5.0 [S]
|
||||
| | +-whatdependson:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
| |
|
||||
| +-org.codehaus.jackson:jackson-mapper-asl:1.9.10
|
||||
| +-com.codahale:jerkson_2.9.1:0.5.0 [S]
|
||||
| | +-whatdependson:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
| |
|
||||
| +-whatdependson:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
| """.stripMargin
|
||||
|
||||
checkOutput(withVersion, expectedGraphWithVersion)
|
||||
|
||||
val withoutVersion =
|
||||
(whatDependsOn in Compile)
|
||||
.toTask(" org.codehaus.jackson jackson-mapper-asl")
|
||||
.value
|
||||
val expectedGraphWithoutVersion =
|
||||
"""org.codehaus.jackson:jackson-mapper-asl:1.9.10
|
||||
| +-com.codahale:jerkson_2.9.1:0.5.0 [S]
|
||||
| | +-whatdependson:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
| |
|
||||
| +-whatdependson:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
|
|
||||
|org.codehaus.jackson:jackson-mapper-asl:[1.9.0,2.0.0) (evicted by: 1.9.10)
|
||||
| +-com.codahale:jerkson_2.9.1:0.5.0 [S]
|
||||
| +-whatdependson:whatdependson_2.9.1:0.1.0-SNAPSHOT [S]
|
||||
| """.stripMargin
|
||||
checkOutput(withoutVersion, expectedGraphWithoutVersion)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# to initialize parser with deps
|
||||
> compile:moduleGraph
|
||||
> check
|
||||
Loading…
Reference in New Issue