Merge pull request #5880 from eed3si9n/wip/dependencygraph

in-sources sbt-dependency-graph
This commit is contained in:
eugene yokota 2020-09-21 22:56:10 -04:00 committed by GitHub
commit 602cf392a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1465 additions and 18 deletions

View File

@ -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:

View File

@ -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

View File

@ -882,7 +882,7 @@ object Defaults extends BuildCommon {
Seq(
initialCommands :== "",
cleanupCommands :== "",
asciiGraphWidth :== 40
asciiGraphWidth :== 80
)
)

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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)
}
)
}

View File

@ -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
}

View File

@ -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
)
}

View File

@ -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""""
}
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -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)
}
}

View File

@ -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")
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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
}
}
}

View File

@ -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)))
()
}

View File

@ -0,0 +1 @@
> check

View File

@ -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)))
()
}

View File

@ -0,0 +1 @@
> check

View File

@ -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)))
()
}

View File

@ -0,0 +1 @@
> check

View File

@ -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
}

View File

@ -0,0 +1 @@
> test_project/check

View File

@ -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)))
()
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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)
}

View File

@ -0,0 +1,2 @@
# same as whatDependsOn test but without the initialization to prime the parser
> check

View File

@ -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)
}

View File

@ -0,0 +1,3 @@
# to initialize parser with deps
> compile:moduleGraph
> check