refactor: Unify to dependency-tree

**Problem**
While sbt-dependency-graph is useful, not just for the basic ASCII graph,
but for DOT file generation etc, it adds a large number of settings and
tasks for combination of formats and actions to the point that
we actually disable most of them by default.

**Solution*
I've had an idea for a while that dependencyTree can be implemented
as a inputTask that accepts its own subcommands and options,
and this implements that.

For example, to open the browser that hosts a DOT file, now you can write
    dependencyTree dot --browse
This commit is contained in:
Eugene Yokota (eed3si9n) 2025-08-09 14:15:27 -04:00 committed by Eugene Yokota
parent 09b9a97437
commit 61fe604519
34 changed files with 354 additions and 357 deletions

View File

@ -157,7 +157,7 @@ jobs:
if: ${{ matrix.jobtype == 3 }}
shell: bash
run: |
# ./sbt -v "dependencyTreeProj/publishLocal; scripted dependency-graph/*"
./sbt -v --client "scripted dependency-graph/*"
./sbt -v --client "scripted dependency-management/* project-load/* project-matrix/* java/* run/*"
# ./sbt -v --client "scripted plugins/*"
# ./sbt -v --client "scripted nio/*"

View File

@ -628,19 +628,6 @@ lazy val scriptedSbtProj = (project in file("scripted-sbt"))
.dependsOn(lmCore)
.configure(addSbtIO, addSbtCompilerInterface)
lazy val dependencyTreeProj = (project in file("dependency-tree"))
.dependsOn(sbtProj)
.settings(
sbtPlugin := true,
baseSettings,
name := "sbt-dependency-tree",
pluginCrossBuild / sbtVersion := version.value,
publishMavenStyle := true,
sbtPluginPublishLegacyMavenStyle := false,
// mimaSettings,
mimaPreviousArtifacts := Set.empty,
)
lazy val remoteCacheProj = (project in file("sbt-remote-cache"))
.dependsOn(sbtProj)
.settings(
@ -1212,7 +1199,6 @@ def allProjects =
stdTaskProj,
runProj,
scriptedSbtProj,
dependencyTreeProj,
protocolProj,
actionsProj,
commandProj,

View File

@ -1,30 +0,0 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package plugins
import scala.annotation.nowarn
object DependencyTreePlugin extends AutoPlugin {
object autoImport extends DependencyTreeKeys
override def trigger = AllRequirements
override def requires = MiniDependencyTreePlugin
@nowarn
val configurations = Vector(Compile, Test, Runtime, Provided, Optional)
// MiniDependencyTreePlugin provides baseBasicReportingSettings for Compile and Test
override lazy val projectSettings: Seq[Def.Setting[?]] =
configurations.diff(Vector(Compile, Test)).flatMap { config =>
inConfig(config)(DependencyTreeSettings.baseBasicReportingSettings)
} ++
configurations.flatMap { config =>
inConfig(config)(DependencyTreeSettings.baseFullReportingSettings)
}
}

View File

@ -4442,16 +4442,6 @@ trait BuildExtra extends BuildCommon with DefExtra {
Seq(compose(onLoad, add), compose(onUnload, remove))
}
/**
* Adds Dependency tree plugin.
*/
def addDependencyTreePlugin: Setting[Seq[ModuleID]] =
libraryDependencies += sbtPluginExtra(
ModuleID("org.scala-sbt", "sbt-dependency-tree", sbtVersion.value),
sbtBinaryVersion.value,
scalaBinaryVersion.value
)
/**
* Adds Maven resolver plugin.
*/

View File

@ -53,7 +53,7 @@ object PluginDiscovery:
"sbt.plugins.SemanticdbPlugin" -> sbt.plugins.SemanticdbPlugin,
"sbt.plugins.JUnitXmlReportPlugin" -> sbt.plugins.JUnitXmlReportPlugin,
"sbt.plugins.Giter8TemplatePlugin" -> sbt.plugins.Giter8TemplatePlugin,
"sbt.plugins.MiniDependencyTreePlugin" -> sbt.plugins.MiniDependencyTreePlugin,
"sbt.plugins.DependencyTreePlugin" -> sbt.plugins.DependencyTreePlugin,
)
val detectedAutoPlugins = discover[AutoPlugin](AutoPlugins)
val allAutoPlugins = (defaultAutoPlugins ++ detectedAutoPlugins.modules) map { (name, value) =>

View File

@ -18,6 +18,11 @@ import sbt.io.IO
object DagreHTML {
def createLink(dotGraph: String, targetDirectory: File): URI = {
val graphHTML = createFile(dotGraph, targetDirectory)
new URI(graphHTML.toURI.toString)
}
def createFile(dotGraph: String, targetDirectory: File): File = {
targetDirectory.mkdirs()
val graphHTML = new File(targetDirectory, "graph.html")
TreeView.saveResource("graph.html", graphHTML)
@ -33,7 +38,6 @@ object DagreHTML {
s"""data = "$graphString";""",
IO.utf8
)
new URI(graphHTML.toURI.toString)
graphHTML
}
}

View File

@ -11,23 +11,25 @@ package internal
package graph
package rendering
import scala.xml.XML
import java.io.StringWriter
import scala.xml.{ Elem, XML }
object GraphML {
def saveAsGraphML(graph: ModuleGraph, outputFile: String): Unit = {
object GraphML:
def graphML(graph: ModuleGraph): Elem =
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>
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}/>
for e <- graph.edges
yield <edge source={e._1.idString} target={e._2.idString}/>
val xml =
val r =
<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">
@ -35,7 +37,13 @@ object GraphML {
{edgesXml}
</graph>
</graphml>
r
XML.save(outputFile, xml)
}
}
def graphMLAsString(graph: ModuleGraph): String =
val w = StringWriter()
XML.write(w, graphML(graph), "UTF-8", false, None.orNull)
w.toString()
def saveAsGraphML(graph: ModuleGraph, outputFile: String): Unit =
XML.save(outputFile, graphML(graph))
end GraphML

View File

@ -28,12 +28,17 @@ object TreeView {
}
def createLink(graphJson: String, targetDirectory: File): URI = {
val graphHTML = createFile(graphJson, targetDirectory)
new URI(graphHTML.toURI.toString)
}
def createFile(graphJson: String, targetDirectory: File): File = {
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)
graphHTML
}
private[rendering] def processSubtree(

View File

@ -9,39 +9,15 @@
package sbt
package plugins
import java.io.File
import java.net.URI
import sbt.internal.graph.*
import sbt.Def.*
import sbt.librarymanagement.{ ModuleID, UpdateReport }
trait MiniDependencyTreeKeys {
abstract class DependencyTreeKeys:
val dependencyTree = inputKey[String]("Displays dependencies in ascii tree and other formats")
val dependencyTreeIncludeScalaLibrary = settingKey[Boolean](
"Specifies if scala dependency should be included in dependencyTree output"
)
val dependencyTree = taskKey[Unit]("Prints an ascii tree of all the dependencies to the console")
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")
// internal
private[sbt] val dependencyTreeIgnoreMissingUpdate =
taskKey[UpdateReport]("update used for dependencyTree task")
private[sbt] val dependencyTreeModuleGraphStore =
taskKey[ModuleGraph]("The stored module-graph from the last run")
val whatDependsOn = inputKey[String]("Shows information about what depends on the given module")
private[sbt] val dependencyTreeCrossProjectId = settingKey[ModuleID]("")
}
object MiniDependencyTreeKeys extends MiniDependencyTreeKeys
abstract class DependencyTreeKeys {
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 dependencyDotNodeColors = settingKey[Boolean](
"The boxes of nodes are painted with colors. Otherwise they're black."
)
@ -51,41 +27,18 @@ abstract class DependencyTreeKeys {
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"
)
// internal
private[sbt] val dependencyTreeIgnoreMissingUpdate =
taskKey[UpdateReport]("update used for dependencyTree task")
private[sbt] val dependencyTreeModuleGraphStore =
taskKey[ModuleGraph]("The stored module-graph from the last run")
val whatDependsOn = inputKey[String]("Shows information about what depends on the given module")
private[sbt] val dependencyTreeCrossProjectId = settingKey[ModuleID]("")
// 0 was added to avoid conflict with sbt-dependency-tree
private[sbt] val dependencyTreeModuleGraph0 =
taskKey[ModuleGraph]("The dependency graph for a project")
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 dependencyLicenseInfo = taskKey[Unit](
"Aggregates and shows information about the licenses of dependencies"
)
}
end DependencyTreeKeys
object DependencyTreeKeys extends DependencyTreeKeys

View File

@ -0,0 +1,42 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package plugins
import sbt.PluginTrigger.AllRequirements
import sbt.ProjectExtra.*
import sbt.librarymanagement.Configurations.{ Compile, Test }
object DependencyTreePlugin extends AutoPlugin {
object autoImport extends DependencyTreeKeys
private val defaultDependencyDotHeader =
"""|digraph "dependency-graph" {
| graph[rankdir="LR"; splines=polyline]
| edge [
| arrowtail="none"
| ]""".stripMargin
private val defaultDependencyDotNodeLabel =
(organization: String, name: String, version: String) =>
s"""${organization}<BR/><B>${name}</B><BR/>${version}"""
import autoImport.*
override def trigger: PluginTrigger = AllRequirements
override def globalSettings: Seq[Def.Setting[?]] = Seq(
dependencyTreeIncludeScalaLibrary :== false,
dependencyDotNodeColors :== true,
dependencyDotHeader := defaultDependencyDotHeader,
dependencyDotNodeLabel := defaultDependencyDotNodeLabel,
)
override lazy val projectSettings: Seq[Def.Setting[?]] =
DependencyTreeSettings.coreSettings ++
inConfig(Compile)(DependencyTreeSettings.baseSettings) ++
inConfig(Test)(DependencyTreeSettings.baseSettings)
}

View File

@ -10,6 +10,7 @@ package sbt
package plugins
import java.io.File
import java.util.Locale
import sbt.Def.*
import sbt.Keys.*
@ -19,15 +20,88 @@ import sbt.internal.graph.backend.SbtUpdateReport
import sbt.internal.graph.rendering.{ DagreHTML, TreeView }
import sbt.internal.librarymanagement.*
import sbt.internal.util.complete.{ Parser, Parsers }
import sbt.internal.util.complete.DefaultParsers.*
import sbt.io.IO
import sbt.io.syntax.*
import sbt.librarymanagement.*
import sbt.util.Logger
import scala.Console
object DependencyTreeSettings {
private[sbt] object DependencyTreeSettings:
import sjsonnew.BasicJsonProtocol.*
import MiniDependencyTreeKeys.*
import DependencyTreeKeys.*
enum Arg:
case Help
case Quiet
case Format(format: Fmt)
case Out(out: String)
case Browse
enum Fmt:
case Tree
case List
case Stats
case Json
case Graph
case HtmlGraph
case Html
case Xml
// Parser for the supported formats
lazy val FmtParser: Parser[Fmt] =
(("tree" ^^^ Fmt.Tree)
| ("list" ^^^ Fmt.List)
| ("stats" ^^^ Fmt.Stats)
| ("json" ^^^ Fmt.Json)
| ("dot" ^^^ Fmt.Graph)
| ("graph" ^^^ Fmt.Graph)
| ("html-graph" ^^^ Fmt.HtmlGraph)
| ("html" ^^^ Fmt.Html)
| ("xml" ^^^ Fmt.Xml))
lazy val ArgParser: Parser[Arg] =
Space ~> (("help" ^^^ Arg.Help)
| ("--help" ^^^ Arg.Help)
| FmtParser.map(fmt => Arg.Format(fmt)))
lazy val ArgOptionParser: Parser[Arg] =
Space ~> (("--quiet" ^^^ Arg.Quiet)
| ("--browse" ^^^ Arg.Browse)
| ("--out" ~> Space ~> StringBasic)
.map(Arg.Out(_))
.examples("--out /tmp/deps.txt"))
// You can have zero-or-one format and options afterwards
lazy val ArgsParser: Parser[Seq[Arg]] =
(ArgParser.? ~ ArgOptionParser.*).map:
case (a, opts) => a.toList ::: opts.toList
def usageText: String =
s"""dependencyTree task displays the dependency graph.
USAGE
dependencyTree [subcommand] [options]
SUBCOMMAND
tree Prints ascii tree (default)
list Prints list of all dependencies
graph Prints GraphViz DOT file
dot Same as graph
html Creates HTML page
html-graph Creates HTML page with GraphViz DOT file
json Prints JSON
xml Prints GraphML
stats Prints statistics for all dependencies
help Prints this help
OPTIONS
--quiet Returns the output as task value, replacing asString
--out <file> Writes the output to the specified file;
The file extension will influence the default subcommand
--browse Opens the browser when combined with graph or html subcommand
"""
/**
* Core settings needed for any graphing tasks.
*/
@ -59,10 +133,10 @@ object DependencyTreeSettings {
)
/**
* MiniDependencyTreePlugin includes these settings for Compile and Test scopes
* DependencyTreePlugin includes these settings for Compile and Test scopes
* to provide dependencyTree task.
*/
lazy val baseBasicReportingSettings: Seq[Def.Setting[?]] =
lazy val baseSettings: Seq[Def.Setting[?]] =
Seq(
dependencyTreeCrossProjectId := CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(
projectID.value
@ -82,57 +156,81 @@ object DependencyTreeSettings {
.storeAs(dependencyTreeModuleGraphStore)
.triggeredBy(dependencyTreeModuleGraph0)
.value,
) ++ {
renderingTaskSettings(dependencyTree) :+ {
dependencyTree / asString := {
rendering.AsciiTree.asciiTree(dependencyTreeModuleGraph0.value, asciiGraphWidth.value)
}
}
}
/**
* This is the maximum strength settings for DependencyTreePlugin.
*/
lazy val baseFullReportingSettings: Seq[Def.Setting[?]] =
Seq(
// 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 := {
val config = configuration.value
target.value / s"dependencies-${config.toString}.dot"
},
dependencyDot / asString := Def.uncached(
rendering.DOT.dotGraph(
dependencyTreeModuleGraph0.value,
dependencyDotHeader.value,
dependencyDotNodeLabel.value,
rendering.DOT.HTMLLabelRendering.AngleBrackets,
dependencyDotNodeColors.value
)
),
dependencyDot := writeToFile(dependencyDot / asString, dependencyDotFile).value,
dependencyDotHeader :=
"""|digraph "dependency-graph" {
| graph[rankdir="LR"; splines=polyline]
| edge [
| arrowtail="none"
| ]""".stripMargin,
dependencyDotNodeColors := true,
dependencyDotNodeLabel := { (organization: String, name: String, version: String) =>
s"""${organization}<BR/><B>${name}</B><BR/>${version}"""
},
// GraphML support
dependencyGraphMLFile := {
val config = configuration.value
target.value / s"dependencies-${config.toString}.graphml"
},
dependencyGraphML := dependencyGraphMLTask.value,
dependencyTree := (Def.inputTaskDyn {
val s = streams.value
val args = ArgsParser.parsed.toList
val isHelp = args.contains(Arg.Help)
val isQuiet = args.contains(Arg.Quiet)
val isBrowse = args.contains(Arg.Browse)
if isHelp then Def.task { s.log.info(usageText); "" }
else
val formatOpt = (args
.collect { case Arg.Format(fmt) => fmt })
.reverse
.headOption
val outFileNameOpt = (args
.collect { case Arg.Out(out) => out })
.reverse
.headOption
val outFileOpt = outFileNameOpt.map(new File(_))
val format = (formatOpt, outFileNameOpt) match
case (None, Some(out)) if out.endsWith(".dot") => Fmt.Graph
case (None, Some(out)) if out.endsWith(".html") => Fmt.Html
case (None, Some(out)) if out.endsWith(".xml") => Fmt.Xml
case (None, Some(out)) if out.endsWith(".json") => Fmt.Json
case (Some(Fmt.Graph), Some(out)) if out.endsWith(".html") => Fmt.HtmlGraph
case (Some(Fmt.Graph), _) if isBrowse => Fmt.HtmlGraph
case (Some(fmt), _) => fmt
case _ => Fmt.Tree
val config = configuration.value.name
val targetDir = target.value / config / format.toString.toLowerCase(Locale.ENGLISH)
format match
case Fmt.Tree | Fmt.List | Fmt.Stats =>
Def.task {
val graph = dependencyTreeModuleGraph0.value
val output = format match
case Fmt.List => rendering.FlatList.render(_.id.idString)(graph)
case Fmt.Stats => rendering.Statistics.renderModuleStatsList(graph)
case _ => rendering.AsciiTree.asciiTree(graph, asciiGraphWidth.value)
handleOutput(output, outFileOpt, isQuiet, s.log)
}
case Fmt.Json =>
Def.task {
val graph = dependencyTreeModuleGraph0.value
val output = TreeView.createJson(graph)
handleOutput(output, outFileOpt, isQuiet, s.log)
}
case Fmt.Xml =>
Def.task {
val graph = dependencyTreeModuleGraph0.value
val output = rendering.GraphML.graphMLAsString(graph)
handleOutput(output, outFileOpt, isQuiet, s.log)
}
case Fmt.Html =>
Def.task {
val graph = dependencyTreeModuleGraph0.value
val renderedTree = TreeView.createJson(graph)
val outputFile = TreeView.createFile(renderedTree, targetDir)
if isBrowse then openBrowser(outputFile.toURI)
outputFile.getAbsolutePath
}
case Fmt.Graph | Fmt.HtmlGraph =>
Def.task {
val graph = dependencyTreeModuleGraph0.value
val output = rendering.DOT.dotGraph(
graph,
dependencyDotHeader.value,
dependencyDotNodeLabel.value,
rendering.DOT.HTMLLabelRendering.AngleBrackets,
dependencyDotNodeColors.value
)
if format == Fmt.Graph then handleOutput(output, outFileOpt, isQuiet, s.log)
else
val outputFile = DagreHTML.createFile(output, targetDir)
if isBrowse then openBrowser(outputFile.toURI)
outputFile.getAbsolutePath
}
}).evaluated,
whatDependsOn := {
val ArtifactPattern(org, name, versionFilter) = artifactPatternParser.parsed
val graph = dependencyTreeModuleGraph0.value
@ -155,112 +253,32 @@ object DependencyTreeSettings {
}
output
},
) ++
renderingAlternatives.flatMap { (key, renderer) => renderingTaskSettings(key, renderer) }
def renderingAlternatives: Seq[(TaskKey[Unit], ModuleGraph => String)] =
Seq(
dependencyList -> rendering.FlatList.render(_.id.idString),
dependencyStats -> rendering.Statistics.renderModuleStatsList,
dependencyLicenseInfo -> rendering.LicenseInfo.render
)
def renderingTaskSettings(key: TaskKey[Unit], renderer: ModuleGraph => String): Seq[Setting[?]] =
renderingTaskSettings(key) :+ {
key / asString := renderer(dependencyTreeModuleGraph0.value)
}
private def handleOutput(
content: String,
outputFileOpt: Option[File],
isQuiet: Boolean,
log: Logger,
): String =
outputFileOpt match
case Some(output) =>
IO.write(output, content, IO.utf8)
if !isQuiet then log.info(s"wrote dependencies to $output")
output.toString
case None =>
if isQuiet then content
else
Console.out.println(content); ""
def renderingTaskSettings(key: TaskKey[Unit]): Seq[Setting[?]] =
Seq(
key := {
val s = streams.value
val str = (key / asString).value
synchronized {
s.log.info(str)
}
},
(key / toFile) := {
val (targetFile, force) = targetFileAndForceParser.parsed
writeToFile(key.key.label, (key / asString).value, targetFile, force, streams.value)
},
)
def dependencyGraphMLTask =
Def.task {
val resultFile = dependencyGraphMLFile.value
val graph = dependencyTreeModuleGraph0.value
rendering.GraphML.saveAsGraphML(graph, resultFile.getAbsolutePath)
streams.value.log.info(s"Wrote dependency graph to '${resultFile}'")
resultFile
}
def browseGraphHTMLTask =
Def.task {
val graph = dependencyTreeModuleGraph0.value
val dotGraph = rendering.DOT.dotGraph(
graph,
dependencyDotHeader.value,
dependencyDotNodeLabel.value,
rendering.DOT.HTMLLabelRendering.AngleBrackets,
dependencyDotNodeColors.value
)
val link = DagreHTML.createLink(dotGraph, dependencyBrowseGraphTarget.value)
streams.value.log.info(s"HTML graph written to $link")
link
}
def browseTreeHTMLTask =
Def.task {
val graph = dependencyTreeModuleGraph0.value
val renderedTree = TreeView.createJson(graph)
val link = TreeView.createLink(renderedTree, dependencyBrowseTreeTarget.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(s"Wrote dependency graph to '${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(s"Opening ${uri} in browser...")
val desktop = java.awt.Desktop.getDesktop
desktop.synchronized {
desktop.browse(uri)
}
uri
def openBrowser(uri: URI): Unit =
val desktop = java.awt.Desktop.getDesktop
desktop.synchronized {
desktop.browse(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 =
@ -320,4 +338,4 @@ object DependencyTreeSettings {
case _ => None
}
}
}
end DependencyTreeSettings

View File

@ -1,28 +0,0 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package plugins
import sbt.PluginTrigger.AllRequirements
import sbt.ProjectExtra.*
import sbt.librarymanagement.Configurations.{ Compile, Test }
object MiniDependencyTreePlugin extends AutoPlugin {
object autoImport extends MiniDependencyTreeKeys
import autoImport.*
override def trigger: PluginTrigger = AllRequirements
override def globalSettings: Seq[Def.Setting[?]] = Seq(
dependencyTreeIncludeScalaLibrary := false
)
override lazy val projectSettings: Seq[Def.Setting[?]] =
DependencyTreeSettings.coreSettings ++
inConfig(Compile)(DependencyTreeSettings.baseBasicReportingSettings) ++
inConfig(Test)(DependencyTreeSettings.baseBasicReportingSettings)
}

View File

@ -0,0 +1,43 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package plugins
import sbt.internal.util.complete.Parser
import DependencyTreeSettings.{ Arg, ArgsParser, Fmt, FmtParser }
object DependencyTreeTest extends verify.BasicTestSuite:
test("Parse args") {
assert(parseArgs(List("help")) == List(Arg.Help))
assert(parseArgs(List("--help")) == List(Arg.Help))
assert(parseArgs(List("--quiet")) == List(Arg.Quiet))
assert(parseArgs(List("tree")) == List(Arg.Format(Fmt.Tree)))
assert(parseArgs(List("--out", "/tmp/deps.txt")) == List(Arg.Out("/tmp/deps.txt")))
assert(parseArgs(List("--browse")) == List(Arg.Browse))
}
test("Parse format") {
assert(parseFormat("tree") == Fmt.Tree)
assert(parseFormat("list") == Fmt.List)
assert(parseFormat("stats") == Fmt.Stats)
assert(parseFormat("json") == Fmt.Json)
assert(parseFormat("html") == Fmt.Html)
assert(parseFormat("graph") == Fmt.Graph)
}
def parseArgs(args: List[String]): Seq[Arg] =
Parser.parse(" " + args.mkString(" "), ArgsParser) match
case Right(args) => args
case Left(err) => sys.error(err)
def parseFormat(fmt: String): Fmt =
Parser.parse(fmt, FmtParser) match
case Right(x) => x
case Left(err) => sys.error(err)
end DependencyTreeTest

View File

@ -5,11 +5,13 @@ name := "asciiGraphWidthSpecs"
lazy val whenIsDefault = (project in file("when-is-default"))
.settings(
name := "whenisdefault",
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.1.0",
check := checkTask.value
)
lazy val whenIs20 = (project in file("when-is-20"))
.settings(
name := "whenis20",
asciiGraphWidth := 20,
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.1.0",
check := checkTask.value
@ -19,6 +21,11 @@ lazy val check = taskKey[Unit]("check")
lazy val checkTask = Def.task {
val context = thisProject.value
val expected = IO.read(file(s"${context.base}/expected.txt"))
val actual = (Compile / dependencyTree / asString).value
require(actual == expected, s"${context.id} is failed.")
val actual = (Compile / dependencyTree).toTask(" --quiet").value
require(actual == expected, s"""${context.id} failed
actual: $actual
expected: $expected
""")
}

View File

@ -1 +1 @@
addDependencyTreePlugin
// addDependencyTreePlugin

View File

@ -5,7 +5,7 @@ updateOptions := updateOptions.value.withCachedResolution(true)
TaskKey[Unit]("check") := {
val report = (Test / updateFull).value
val graph = (Test / dependencyTree / asString).value
val graph = (Test / dependencyTree).toTask(" --quiet").value
def sanitize(str: String): String = str.split('\n').drop(1).mkString("\n")
val expectedGraph =

View File

@ -1,5 +1,6 @@
ThisBuild / scalaVersion := "2.12.20"
name := "foo"
libraryDependencies ++= Seq(
"org.slf4j" % "slf4j-api" % "1.7.2",
"ch.qos.logback" % "logback-classic" % "1.0.7"
@ -8,8 +9,11 @@ csrMavenDependencyOverride := false
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 graph = (Test / dependencyTree).toTask(" --quiet").value
def sanitize(str: String): String = str.linesIterator.toList
.drop(1)
.map(_.trim)
.mkString("\n")
/*
Started to return:
@ -31,8 +35,8 @@ default:sbt_8ae1da13_2.12:0.1.0-SNAPSHOT [S]
| |
| +-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"))
// 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

@ -5,7 +5,7 @@ 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)
val candidates = "tree list stats".split(' ').map(_.trim)
candidates.foreach { c =>
val expected = new File(s"expected/$c.txt")
val actual = new File(s"target/$c.txt")

View File

@ -0,0 +1,5 @@
> dependencyTree --out target/tree.txt
> dependencyTree list --out target/list.txt
> dependencyTree stats --out target/stats.txt
# > dependencyLicenseInfo/toFile target/licenses.txt
> check

View File

@ -5,7 +5,7 @@ libraryDependencies +=
TaskKey[Unit]("check") := {
val report = updateFull.value
val graph = (Test / dependencyTree / asString).value
val graph = (Test / dependencyTree).toTask(" --quiet").value
def sanitize(str: String): String = str.split('\n').drop(1).mkString("\n")
val expectedGraph =

View File

@ -14,7 +14,7 @@ lazy val test_project = project
.dependsOn(justADependencyProject, justATransitiveDependencyProject)
.settings(
TaskKey[Unit]("check") := {
val dotFile = (dependencyDot in Compile).value
val graph = (Compile / dependencyTree).toTask(" dot --quiet").value
val expectedGraph =
"""digraph "dependency-graph" {
| graph[rankdir="LR"; splines=polyline]
@ -23,7 +23,7 @@ lazy val test_project = project
| ]
| "justadependencyproject:justadependencyproject_2.9.2:0.1-SNAPSHOT"[shape=box label=<justadependencyproject<BR/><B>justadependencyproject_2.9.2</B><BR/>0.1-SNAPSHOT> style="" penwidth="5" color="#B6E316"]
| "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT"[shape=box label=<justatransitivedependencyproject<BR/><B>justatransitivedependencyproject_2.9.2</B><BR/>0.1-SNAPSHOT> style="" penwidth="5" color="#0E92BE"]
| "justatransitivedependencyendpointproject:justatransitivedependencyendpointproject_2.9.2:0.1-SNAPSHOT"[shape=box label=<justatransitivedependencyendpointproject<BR/><B>justatransitivedependencyendpointproject_2.9.2</B><BR/>0.1-SNAPSHOT> style="" penwidth="5" color="#9EAD1B"]
| "justatransitivedependencyendpointproject:justatransitivedependencyendpointproject_2.9.2:0.1-SNAPSHOT"[shape=box label=<justatransitivedependencyendpointproject<BR/><B>justatransitivedependencyendpointproject_2.9.2</B><BR/>0.1-SNAPSHOT> style="" penwidth="5" color="#A1168F"]
| "test_project:test_project_2.9.2:0.1-SNAPSHOT"[shape=box label=<test_project<BR/><B>test_project_2.9.2</B><BR/>0.1-SNAPSHOT> style="" penwidth="5" color="#C37661"]
| "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT" -> "justatransitivedependencyendpointproject:justatransitivedependencyendpointproject_2.9.2:0.1-SNAPSHOT"
| "test_project:test_project_2.9.2:0.1-SNAPSHOT" -> "justadependencyproject:justadependencyproject_2.9.2:0.1-SNAPSHOT"
@ -31,8 +31,7 @@ lazy val test_project = project
|}
""".stripMargin
val graph: String = scala.io.Source.fromFile(dotFile.getAbsolutePath).mkString
val errors = compareByLine(graph, expectedGraph)
val errors = compareByLine(graph.trim, expectedGraph.trim)
require(errors.isEmpty, errors.mkString("\n"))
()
}
@ -40,7 +39,7 @@ lazy val test_project = project
def compareByLine(got: String, expected: String): Seq[String] = {
val errors = ListBuffer[String]()
got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach {
got.linesIterator.toList.sorted.zip(expected.linesIterator.toList.sorted).zipWithIndex.foreach {
case ((got_line: String, expected_line: String), i: Int) =>
if (got_line != expected_line) {
errors.append("""not matching lines at line %s
@ -49,5 +48,5 @@ def compareByLine(got: String, expected: String): Seq[String] = {
|""".stripMargin.format(i, expected_line, got_line))
}
}
errors
errors.toList
}

View File

@ -14,7 +14,7 @@ lazy val test_project = project
.dependsOn(justADependencyProject, justATransitiveDependencyProject)
.settings(
TaskKey[Unit]("check") := {
val htmlFile = (dependencyBrowseGraphHTML in Compile).value
val htmlFile = (Compile / dependencyTree).toTask(" html-graph").value
val expectedHtml =
"""<!doctype html>
|
@ -60,11 +60,10 @@ lazy val test_project = project
|<script>
| d3.select("#graph").graphviz().renderDot(decodeURIComponent(data));
|</script>
|
""".stripMargin
val html: String = scala.io.Source.fromFile(htmlFile).mkString
val errors = compareByLine(html, expectedHtml)
val errors = compareByLine(html.trim, expectedHtml.trim)
require(errors.isEmpty, errors.mkString("\n"))
()
}
@ -72,7 +71,7 @@ lazy val test_project = project
def compareByLine(got: String, expected: String): Seq[String] = {
val errors = ListBuffer[String]()
got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach {
got.linesIterator.toList.zip(expected.linesIterator.toList).zipWithIndex.foreach {
case ((got_line: String, expected_line: String), i: Int) =>
if (got_line != expected_line) {
errors.append("""not matching lines at line %s
@ -81,5 +80,5 @@ def compareByLine(got: String, expected: String): Seq[String] = {
|""".stripMargin.format(i, expected_line, got_line))
}
}
errors
errors.toList
}

View File

@ -1 +0,0 @@
addDependencyTreePlugin

View File

@ -1,5 +0,0 @@
> dependencyTree/toFile target/tree.txt
> dependencyList/toFile target/list.txt
> dependencyStats/toFile target/stats.txt
> dependencyLicenseInfo/toFile target/licenses.txt
> check

View File

@ -11,26 +11,26 @@ libraryDependencies ++= Seq(
val check = TaskKey[Unit]("check")
check := {
def sanitize(str: String): String = str.split('\n').map(_.trim).mkString("\n")
def sanitize(str: String): String = str.linesIterator.toList.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]")
require(sanitize(expected) == sanitize(output),
s"Tree should have been [\n${expected}\n] but was [\n${output}\n]")
val withVersion =
(Compile / whatDependsOn)
.toTask(" org.typelevel cats-core_2.13 2.6.0")
.value
val expectedGraphWithVersion = {
val expectedGraphWithVersion =
"""org.typelevel:cats-core_2.13:2.6.0 [S]
|+-org.typelevel:cats-effect-kernel_2.13:3.1.0 [S]
|+-org.typelevel:cats-effect-std_2.13:3.1.0 [S]
|| +-org.typelevel:cats-effect_2.13:3.1.0 [S]
|| +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]
||
|+-org.typelevel:cats-effect_2.13:3.1.0 [S]
|+-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]""".stripMargin
}
| +-org.typelevel:cats-effect-kernel_2.13:3.1.0 [S]
| +-org.typelevel:cats-effect-std_2.13:3.1.0 [S]
| | +-org.typelevel:cats-effect_2.13:3.1.0 [S]
| | +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]
| |
| +-org.typelevel:cats-effect_2.13:3.1.0 [S]
| +-whatdependson:whatdependson_2.13:0.1.0-SNAPSHOT [S]""".stripMargin
checkOutput(withVersion.trim, expectedGraphWithVersion.trim)
checkOutput(withVersion.trim, expectedGraphWithVersion)
val withoutVersion =
(Compile / whatDependsOn)