Skip to content
This repository was archived by the owner on Apr 8, 2021. It is now read-only.

Commit 8aa58cd

Browse files
committed
For common operations introduce asString, printToConsole, and toFile subtasks, fixes #164
1 parent 01ac4dd commit 8aa58cd

File tree

12 files changed

+199
-84
lines changed

12 files changed

+199
-84
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,30 @@ the notes of version [0.8.2](https://github.com/jrudolph/sbt-dependency-graph/tr
3737
* `ivyReport`: Lets ivy generate the resolution report for you project. Use
3838
`show ivyReport` for the filename of the generated report
3939

40+
The following tasks also support the `toFile` subtask to save the contents to a file:
41+
42+
* `dependencyTree`
43+
* `dependencyList`
44+
* `dependencyStats`
45+
* `dependencyLicenseInfo`
46+
47+
The `toFile` subtask has the following syntax:
48+
49+
```
50+
<config>:<task>::toFile <filename> [-f|--force]
51+
```
52+
53+
Use `-f` to force overwriting an existing file.
54+
55+
E.g. `test:dependencyStats::toFile target/depstats.txt` will write the output of the `dependencyStats` in the `test`
56+
configuration to the file `target/depstats.txt` but would not overwrite an existing file.
57+
4058
All tasks can be scoped to a configuration to get the report for a specific configuration. `test:dependencyGraph`,
4159
for example, prints the dependencies in the `test` configuration. If you don't specify any configuration, `compile` is
4260
assumed as usual.
4361

44-
Note: If you want to run tasks with parameters from outside the sbt shell, make sure to put the whole task invocation in quotes, e.g. `sbt "whatDependsOn <org> <module> <version>"`.
62+
Note: If you want to run tasks with parameters from outside the sbt shell, make sure to put the whole task invocation in
63+
quotes, e.g. `sbt "whatDependsOn <org> <module> <version>"`.
4564

4665
## Configuration settings
4766

src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphKeys.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ package net.virtualvoid.sbt.graph
1919
import sbt._
2020

2121
trait DependencyGraphKeys {
22+
val asString = TaskKey[String]("asString", "Provides the string value for the task it is scoped for")
23+
val printToConsole = TaskKey[Unit]("printToConsole", "Prints the tasks value to the console")
24+
val toFile = InputKey[File]("toFile", "Writes the task value to the given file")
25+
2226
val dependencyGraphMLFile = SettingKey[File](
2327
"dependency-graph-ml-file",
2428
"The location the graphml file should be generated at")

src/main/scala/net/virtualvoid/sbt/graph/DependencyGraphSettings.scala

Lines changed: 99 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import internal.librarymanagement._
2727
import librarymanagement._
2828
import sbt.dependencygraph.SbtAccess
2929
import sbt.dependencygraph.DependencyGraphSbtCompat.Implicits._
30+
import sbt.complete.Parsers
3031

3132
object DependencyGraphSettings {
3233
import DependencyGraphKeys._
@@ -47,70 +48,95 @@ object DependencyGraphSettings {
4748
def reportSettings =
4849
Seq(Compile, Test, IntegrationTest, Runtime, Provided, Optional).flatMap(ivyReportForConfig)
4950

50-
def ivyReportForConfig(config: Configuration) = inConfig(config)(Seq(
51-
ivyReport := { Def.task { ivyReportFunction.value.apply(config.toString) } dependsOn (ignoreMissingUpdate) }.value,
52-
crossProjectId := sbt.CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(projectID.value),
53-
moduleGraphSbt :=
54-
ignoreMissingUpdate.value.configuration(configuration.value).map(report SbtUpdateReport.fromConfigurationReport(report, crossProjectId.value)).getOrElse(ModuleGraph.empty),
55-
moduleGraphIvyReport := IvyReport.fromReportFile(absoluteReportPath(ivyReport.value)),
56-
moduleGraph := {
57-
sbtVersion.value match {
58-
case Version(0, 13, x, _) if x >= 6 moduleGraphSbt.value
59-
case Version(1, _, _, _) moduleGraphSbt.value
60-
}
61-
},
62-
moduleGraph := {
63-
// FIXME: remove busywork
64-
val scalaVersion = Keys.scalaVersion.value
65-
val moduleGraph = DependencyGraphKeys.moduleGraph.value
66-
67-
if (filterScalaLibrary.value) GraphTransformations.ignoreScalaLibrary(scalaVersion, moduleGraph)
68-
else moduleGraph
69-
},
70-
moduleGraphStore := (moduleGraph storeAs moduleGraphStore triggeredBy moduleGraph).value,
71-
asciiTree := rendering.AsciiTree.asciiTree(moduleGraph.value),
72-
dependencyTree := print(asciiTree).value,
73-
dependencyGraphMLFile := { target.value / "dependencies-%s.graphml".format(config.toString) },
74-
dependencyGraphML := dependencyGraphMLTask.value,
75-
dependencyDotFile := { target.value / "dependencies-%s.dot".format(config.toString) },
76-
dependencyDotString := rendering.DOT.dotGraph(moduleGraph.value, dependencyDotHeader.value, dependencyDotNodeLabel.value, rendering.DOT.AngleBrackets),
77-
dependencyDot := writeToFile(dependencyDotString, dependencyDotFile).value,
78-
dependencyBrowseGraphTarget := { target.value / "browse-dependency-graph" },
79-
dependencyBrowseGraphHTML := browseGraphHTMLTask.value,
80-
dependencyBrowseGraph := openBrowser(dependencyBrowseGraphHTML).value,
81-
dependencyBrowseTreeTarget := { target.value / "browse-dependency-tree" },
82-
dependencyBrowseTreeHTML := browseTreeHTMLTask.value,
83-
dependencyBrowseTree := openBrowser(dependencyBrowseTreeHTML).value,
84-
dependencyList := printFromGraph(rendering.FlatList.render(_, _.id.idString)).value,
85-
dependencyStats := printFromGraph(rendering.Statistics.renderModuleStatsList).value,
86-
dependencyDotHeader :=
87-
"""|digraph "dependency-graph" {
51+
val renderingAlternatives: Seq[(TaskKey[Unit], ModuleGraph String)] =
52+
Seq(
53+
dependencyTree -> rendering.AsciiTree.asciiTree _,
54+
dependencyList -> rendering.FlatList.render(_.id.idString),
55+
dependencyStats -> rendering.Statistics.renderModuleStatsList _,
56+
licenseInfo -> rendering.LicenseInfo.render _)
57+
58+
def ivyReportForConfig(config: Configuration) = inConfig(config)(
59+
Seq(
60+
ivyReport := { Def.task { ivyReportFunction.value.apply(config.toString) } dependsOn (ignoreMissingUpdate) }.value,
61+
crossProjectId := sbt.CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(projectID.value),
62+
moduleGraphSbt :=
63+
ignoreMissingUpdate.value.configuration(configuration.value).map(report SbtUpdateReport.fromConfigurationReport(report, crossProjectId.value)).getOrElse(ModuleGraph.empty),
64+
moduleGraphIvyReport := IvyReport.fromReportFile(absoluteReportPath(ivyReport.value)),
65+
moduleGraph := {
66+
sbtVersion.value match {
67+
case Version(0, 13, x, _) if x >= 6 moduleGraphSbt.value
68+
case Version(1, _, _, _) moduleGraphSbt.value
69+
}
70+
},
71+
moduleGraph := {
72+
// FIXME: remove busywork
73+
val scalaVersion = Keys.scalaVersion.value
74+
val moduleGraph = DependencyGraphKeys.moduleGraph.value
75+
76+
if (filterScalaLibrary.value) GraphTransformations.ignoreScalaLibrary(scalaVersion, moduleGraph)
77+
else moduleGraph
78+
},
79+
moduleGraphStore := (moduleGraph storeAs moduleGraphStore triggeredBy moduleGraph).value,
80+
81+
// browse
82+
dependencyBrowseGraphTarget := { target.value / "browse-dependency-graph" },
83+
dependencyBrowseGraphHTML := browseGraphHTMLTask.value,
84+
dependencyBrowseGraph := openBrowser(dependencyBrowseGraphHTML).value,
85+
86+
dependencyBrowseTreeTarget := { target.value / "browse-dependency-tree" },
87+
dependencyBrowseTreeHTML := browseTreeHTMLTask.value,
88+
dependencyBrowseTree := openBrowser(dependencyBrowseTreeHTML).value,
89+
90+
// dot support
91+
dependencyDotFile := { target.value / "dependencies-%s.dot".format(config.toString) },
92+
dependencyDotString := rendering.DOT.dotGraph(moduleGraph.value, dependencyDotHeader.value, dependencyDotNodeLabel.value, rendering.DOT.AngleBrackets),
93+
dependencyDot := writeToFile(dependencyDotString, dependencyDotFile).value,
94+
dependencyDotHeader :=
95+
"""|digraph "dependency-graph" {
8896
| graph[rankdir="LR"]
8997
| edge [
9098
| arrowtail="none"
9199
| ]""".stripMargin,
92-
dependencyDotNodeLabel := { (organisation: String, name: String, version: String)
93-
"""%s<BR/><B>%s</B><BR/>%s""".format(organisation, name, version)
94-
},
95-
whatDependsOn := {
96-
val ArtifactPattern(org, name, versionFilter) = artifactPatternParser.parsed
97-
val graph = moduleGraph.value
98-
val modules =
99-
versionFilter match {
100-
case Some(version) ModuleId(org, name, version) :: Nil
101-
case None graph.nodes.filter(m m.id.organisation == org && m.id.name == name).map(_.id)
102-
}
103-
val output =
104-
modules
105-
.map { module
106-
rendering.AsciiTree.asciiTree(GraphTransformations.reverseGraphStartingAt(graph, module))
100+
dependencyDotNodeLabel := { (organisation: String, name: String, version: String)
101+
"""%s<BR/><B>%s</B><BR/>%s""".format(organisation, name, version)
102+
},
103+
104+
// GraphML support
105+
dependencyGraphMLFile := { target.value / "dependencies-%s.graphml".format(config.toString) },
106+
dependencyGraphML := dependencyGraphMLTask.value,
107+
108+
whatDependsOn := {
109+
val ArtifactPattern(org, name, versionFilter) = artifactPatternParser.parsed
110+
val graph = moduleGraph.value
111+
val modules =
112+
versionFilter match {
113+
case Some(version) ModuleId(org, name, version) :: Nil
114+
case None graph.nodes.filter(m m.id.organisation == org && m.id.name == name).map(_.id)
107115
}
108-
.mkString("\n")
116+
val output =
117+
modules
118+
.map { module
119+
rendering.AsciiTree.asciiTree(GraphTransformations.reverseGraphStartingAt(graph, module))
120+
}
121+
.mkString("\n")
109122

110-
streams.value.log.info(output)
111-
output
112-
},
113-
licenseInfo := showLicenseInfo(moduleGraph.value, streams.value)) ++ AsciiGraph.asciiGraphSetttings)
123+
streams.value.log.info(output)
124+
output
125+
},
126+
// deprecated settings
127+
asciiTree := (asString in dependencyTree).value) ++
128+
renderingAlternatives.flatMap((renderingTaskSettings _).tupled) ++
129+
AsciiGraph.asciiGraphSetttings)
130+
131+
def renderingTaskSettings(key: TaskKey[Unit], renderer: ModuleGraph String): Seq[Setting[_]] =
132+
Seq(
133+
asString in key := renderer(moduleGraph.value),
134+
printToConsole in key := streams.value.log.info((asString in key).value),
135+
toFile in key := {
136+
val (targetFile, force) = targetFileAndForceParser.parsed
137+
writeToFile(key.key.label, (asString in key).value, targetFile, force, streams.value)
138+
},
139+
key := (printToConsole in key).value)
114140

115141
def ivyReportFunctionTask = Def.task {
116142
val ivyConfig = Keys.ivyConfiguration.value.asInstanceOf[InlineIvyConfiguration]
@@ -157,13 +183,17 @@ object DependencyGraphSettings {
157183
outFile
158184
}
159185

160-
def absoluteReportPath = (file: File) file.getAbsolutePath
186+
def writeToFile(what: String, data: String, targetFile: File, force: Boolean, streams: TaskStreams): File =
187+
if (targetFile.exists && !force)
188+
throw new RuntimeException(s"Target file for $what already exists at ${targetFile.getAbsolutePath}. Use '-f' to override")
189+
else {
190+
IOUtil.writeToFile(data, targetFile)
161191

162-
def print(key: TaskKey[String]) =
163-
Def.task { streams.value.log.info(key.value) }
192+
streams.log.info(s"Wrote $what to '$targetFile'")
193+
targetFile
194+
}
164195

165-
def printFromGraph(f: ModuleGraph String) =
166-
Def.task { streams.value.log.info(f(moduleGraph.value)) }
196+
def absoluteReportPath = (file: File) file.getAbsolutePath
167197

168198
def openBrowser(uriKey: TaskKey[URI]) =
169199
Def.task {
@@ -173,33 +203,16 @@ object DependencyGraphSettings {
173203
uri
174204
}
175205

176-
def showLicenseInfo(graph: ModuleGraph, streams: TaskStreams): Unit = {
177-
val output =
178-
graph.nodes.filter(_.isUsed).groupBy(_.license).toSeq.sortBy(_._1).map {
179-
case (license, modules)
180-
license.getOrElse("No license specified") + "\n" +
181-
modules.map(_.id.idString formatted "\t %s").mkString("\n")
182-
}.mkString("\n\n")
183-
streams.log.info(output)
184-
}
185-
186-
import Project._
187-
val shouldForceParser: State Parser[Boolean] = { (state: State)
188-
import sbt.complete.DefaultParsers._
189-
190-
(Space ~> token("--force")).?.map(_.isDefined)
191-
}
192-
193206
case class ArtifactPattern(
194207
organisation: String,
195208
name: String,
196209
version: Option[String])
197210

211+
import sbt.complete.DefaultParsers._
198212
val artifactPatternParser: Def.Initialize[State Parser[ArtifactPattern]] =
199213
resolvedScoped { ctx (state: State)
200214
val graph = loadFromContext(moduleGraphStore, ctx, state) getOrElse ModuleGraph(Nil, Nil)
201215

202-
import sbt.complete.DefaultParsers._
203216
graph.nodes
204217
.map(_.id)
205218
.groupBy(m (m.organisation, m.name))
@@ -222,6 +235,10 @@ object DependencyGraphSettings {
222235
}
223236
}
224237
}
238+
val shouldForceParser: Parser[Boolean] = (Space ~> (Parser.literal("-f") | "--force")).?.map(_.isDefined)
239+
240+
val targetFileAndForceParser: Parser[(File, Boolean)] =
241+
Parsers.fileParser(new File(".")) ~ shouldForceParser
225242

226243
// This is to support 0.13.8's InlineConfigurationWithExcludes while not forcing 0.13.8
227244
type HasModule = {
@@ -230,7 +247,7 @@ object DependencyGraphSettings {
230247
def crossName(ivyModule: IvySbt#Module) =
231248
ivyModule.moduleSettings match {
232249
case ic: InlineConfiguration ic.module.name
233-
case hm: HasModule if hm.getClass.getName == "sbt.InlineConfigurationWithExcludes" hm.module.name
250+
case hm: HasModule @unchecked if hm.getClass.getName == "sbt.InlineConfigurationWithExcludes" hm.module.name
234251
case _
235252
throw new IllegalStateException("sbt-dependency-graph plugin currently only supports InlineConfiguration of ivy settings (the default in sbt)")
236253
}

src/main/scala/net/virtualvoid/sbt/graph/rendering/FlatList.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package net.virtualvoid.sbt.graph
1818
package rendering
1919

2020
object FlatList {
21-
def render(graph: ModuleGraph, display: Module String): String =
21+
def render(display: Module String)(graph: ModuleGraph): String =
2222
graph.modules.values.toSeq
2323
.distinct
2424
.filterNot(_.isEvicted)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package net.virtualvoid.sbt.graph.rendering
2+
3+
import net.virtualvoid.sbt.graph.ModuleGraph
4+
5+
object LicenseInfo {
6+
def render(graph: ModuleGraph): String =
7+
graph.nodes.filter(_.isUsed).groupBy(_.license).toSeq.sortBy(_._1).map {
8+
case (license, modules)
9+
license.getOrElse("No license specified") + "\n" +
10+
modules.map(_.id.idString formatted "\t %s").mkString("\n")
11+
}.mkString("\n\n")
12+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
scalaVersion := "2.12.6"
2+
3+
organization := "org.example"
4+
5+
name := "blubber"
6+
7+
version := "0.1"
8+
9+
libraryDependencies ++= Seq(
10+
"com.codahale" % "jerkson_2.9.1" % "0.5.0"
11+
)
12+
13+
TaskKey[Unit]("check") := {
14+
val candidates = "tree list stats licenses".split(' ').map(_.trim)
15+
candidates.foreach { c =>
16+
val expected = new File(s"expected/$c.txt")
17+
val actual = new File(s"target/$c.txt")
18+
19+
import sys.process._
20+
val exit = s"diff -U3 ${expected.getPath} ${actual.getPath}".!
21+
require(exit == 0, s"Diff was non-zero for ${actual.getName}")
22+
}
23+
24+
//require(sanitize(graph) == sanitize(expectedGraph), "Graph for report %s was '\n%s' but should have been '\n%s'" format (report, sanitize(graph), sanitize(expectedGraph)))
25+
()
26+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
No license specified
2+
org.example:blubber_2.12:0.1
3+
4+
The Apache Software License, Version 2.0
5+
org.codehaus.jackson:jackson-mapper-asl:1.9.11
6+
org.codehaus.jackson:jackson-core-asl:1.9.11
7+
8+
The MIT License
9+
com.codahale:jerkson_2.9.1:0.5.0
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
com.codahale:jerkson_2.9.1:0.5.0
2+
org.codehaus.jackson:jackson-core-asl:1.9.11
3+
org.codehaus.jackson:jackson-mapper-asl:1.9.11
4+
org.example:blubber_2.12:0.1
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
TotSize JarSize #TDe #Dep Module
2+
1.754 MB ------- MB 3 1 org.example:blubber_2.12:0.1
3+
1.754 MB 0.741 MB 2 2 com.codahale:jerkson_2.9.1:0.5.0
4+
1.013 MB 0.780 MB 1 1 org.codehaus.jackson:jackson-mapper-asl:1.9.11
5+
0.232 MB 0.232 MB 0 0 org.codehaus.jackson:jackson-core-asl:1.9.11
6+
7+
Columns are
8+
- Jar-Size including dependencies
9+
- Jar-Size
10+
- Number of transitive dependencies
11+
- Number of direct dependencies
12+
- ModuleID
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
org.example:blubber_2.12:0.1 [S]
2+
+-com.codahale:jerkson_2.9.1:0.5.0 [S]
3+
+-org.codehaus.jackson:jackson-core-asl:1.9.11
4+
+-org.codehaus.jackson:jackson-mapper-asl:1.9.11
5+
+-org.codehaus.jackson:jackson-core-asl:1.9.11
6+

0 commit comments

Comments
 (0)