Skip to content

Instantly share code, notes, and snippets.

@jrudolph
Last active October 1, 2019 19:40
Show Gist options
  • Save jrudolph/9e0e0e8a0df35c23713dfc314670db87 to your computer and use it in GitHub Desktop.
Save jrudolph/9e0e0e8a0df35c23713dfc314670db87 to your computer and use it in GitHub Desktop.
Better sbt `inspect tree`

Better sbt inspection commands

Usage

Put the scala file into project. Add the new setting to build.sbt.

inspect2 tree <key>

Usual inspect tree will miss edges when they go through delegation. inspect2 tree fixes this to get a more accurate picture.

inspect2 how <goal> <dependency>

When writing a plugin you often try to find out how a key transitively relates to its dependency. This new command shows all dependency paths from a key to a transitive dependency. E.g.

> inspect2 how compile unmanagedSourceDirectories
[info] backend/compile:compile -> backend/compile:manipulateBytecode -> backend/compile:compileIncremental -> backend/compile:compile::compileInputs -> backend/compile:sources -> backend/compile:unmanagedSources -> backend/compile:unmanagedSourceDirectories
[info] backend/compile:compile -> backend/compile:manipulateBytecode -> backend/compile:compileIncremental -> backend/compile:compile::compileInputs -> backend/compile:scalariformFormat -> backend/compile:sourceDirectories -> backend/compile:unmanagedSourceDirectories
commands += sbt.myinspect.SettingGraph.myInspect
/* sbt -- Simple Build Tool
* Copyright 2011 Mark Harrah, Eugene Yokota
*/
package sbt
package myinspect
import java.io.File
import Def.{ScopedKey, compiled, flattenLocals}
import Predef.{any2stringadd => _, _}
object SettingGraph {
def myInspect =
Command("inspect2", ("",""), "")(Inspect.parser) {
case (s, (option, sk)) =>
s.log.info(Inspect.output(s, option, sk))
s
}
def apply(structure: BuildStructure, basedir: File, scoped: ScopedKey[_], generation: Int)(implicit display: Show[ScopedKey[_]]): SettingGraph =
{
val comp = compiled(structure.settings, actual = true /* <--- The only thing I actually changed */)(structure.delegates, structure.scopeLocal, display)
val cMap = flattenLocals(comp)
def loop(scoped: ScopedKey[_], generation: Int): SettingGraph =
{
val key = scoped.key
val scope = scoped.scope
val definedIn = structure.data.definingScope(scope, key) map { sc => display(ScopedKey(sc, key)) }
val depends = cMap.get(scoped) match { case Some(c) => c.dependencies.toSet; case None => Set.empty }
SettingGraph(display(scoped), definedIn,
Project.scopedKeyData(structure, scope, key),
key.description, basedir,
depends map { (x: ScopedKey[_]) => loop(x, generation + 1) })
}
loop(scoped, generation)
}
trait Graph[N] {
def successors(node: N): Seq[N]
}
def findPaths[N](from: N, to: N)(implicit graph: Graph[N]): Seq[List[N]] = {
def find(from: N, visited: Set[N]): (Set[N], Seq[List[N]]) = {
if (visited.contains(from)) (visited, Seq.empty) // been here...
else if (from == to) (visited, Seq(to :: Nil))
else {
graph.successors(from)
.foldLeft((visited + from, Seq.empty[List[N]])) { (state, succ) =>
val (visited, found) = state
val (newVisited, newlyFound) = find(succ, visited)
(newVisited, found ++ newlyFound.map(from :: _))
}
}
}
find(from, Set.empty)._2
}
def findChains(structure: BuildStructure, basedir: File, from: ScopedKey[_], to: ScopedKey[_])(implicit display: Show[ScopedKey[_]]): String = {
val comp = compiled(structure.settings, actual = true /* <--- The only thing I actually changed */)(structure.delegates, structure.scopeLocal, display)
val cMap = flattenLocals(comp)
implicit val keyGraph = new Graph[ScopedKey[_]] {
def successors(node: ScopedKey[_]): Seq[ScopedKey[_]] =
cMap.get(node) match { case Some(c) => c.dependencies.toSeq; case None => Seq.empty }
}
findPaths[ScopedKey[_]](from, to)(keyGraph).map { path =>
path.map(display(_)).mkString(" -> ")
}.mkString("\n")
}
}
case class SettingGraph(name: String,
definedIn: Option[String],
data: Option[ScopedKeyData[_]],
description: Option[String],
basedir: File,
depends: Set[SettingGraph]) {
def dataString: String =
data map { d =>
d.settingValue map {
case f: File => IO.relativize(basedir, f) getOrElse { f.toString }
case x => x.toString
} getOrElse { d.typeName }
} getOrElse { "" }
def dependsAscii(defaultWidth: Int) = Graph.toAscii(this,
(x: SettingGraph) => x.depends.toSeq.sortBy(_.name),
(x: SettingGraph) => "%s = %s" format (x.definedIn getOrElse { "" }, x.dataString),
defaultWidth
)
}
object Graph {
// [info] foo
// [info] +-bar
// [info] | +-baz
// [info] |
// [info] +-quux
def toAscii[A](top: A, children: A => Seq[A], display: A => String, defaultWidth: Int): String = {
val maxColumn = math.max(JLine.usingTerminal(_.getWidth), defaultWidth) - 8
val twoSpaces = " " + " " // prevent accidentally being converted into a tab
def limitLine(s: String): String =
if (s.length > maxColumn) s.slice(0, maxColumn - 2) + ".."
else s
def insertBar(s: String, at: Int): String =
if (at < s.length)
s.slice(0, at) +
(s(at).toString match {
case " " => "|"
case x => x
}) +
s.slice(at + 1, s.length)
else s
def toAsciiLines(node: A, level: Int): 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 (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
}
toAsciiLines(top, 0).mkString("\n")
}
}
import complete.{ DefaultParsers, Parser }
import DefaultParsers._
import Def.ScopedKey
//import Types.idFun
import java.io.File
object Inspect {
sealed trait Mode
final case class Details(actual: Boolean) extends Mode
private[this] final class Opt(override val toString: String) extends Mode
val DependencyTree: Mode = new Opt("tree")
val Uses: Mode = new Opt("inspect")
val Definitions: Mode = new Opt("definitions")
val How: Mode = new Opt("how")
case class HowComplete(from: ScopedKey[_]) extends Mode
def parser: State => Parser[(Inspect.Mode, ScopedKey[_])] = (s: State) => spacedModeParser(s) flatMap {
case opt @ (Uses | Definitions) => allKeyParser(s).map(key => (opt, Def.ScopedKey(Global, key)))
case opt @ (DependencyTree | Details(_)) => spacedKeyParser(s).map(key => (opt, key))
case opt @ How => (spacedKeyParser(s) ~ spacedKeyParser(s)).map {
case (from, to) => (HowComplete(from), to)
}
}
val spacedModeParser: (State => Parser[Mode]) = (s: State) => {
val actual = "actual" ^^^ Details(true)
val tree = "tree" ^^^ DependencyTree
val uses = "uses" ^^^ Uses
val definitions = "definitions" ^^^ Definitions
val how = "how" ^^^ How
token(Space ~> (tree | actual | uses | definitions | how)) ?? Details(false)
}
def allKeyParser(s: State): Parser[AttributeKey[_]] =
{
val keyMap = Project.structure(s).index.keyMap
token(Space ~> (ID !!! "Expected key" examples keyMap.keySet)) flatMap { key => Act.getKey(keyMap, key, idFun) }
}
val spacedKeyParser: State => Parser[ScopedKey[_]] = (s: State) => Act.requireSession(s, token(Space) ~> Act.scopedKeyParser(s))
def output(s: State, option: Mode, sk: Def.ScopedKey[_]): String =
{
val extracted = Project.extract(s)
import extracted._
option match {
case Details(actual) =>
Project.details(structure, actual, sk.scope, sk.key)
case DependencyTree =>
val basedir = new File(Project.session(s).current.build)
SettingGraph(structure, basedir, sk, 0).dependsAscii(get(sbt.Keys.asciiGraphWidth))
case Uses =>
Project.showUses(Project.usedBy(structure, true, sk.key))
case Definitions =>
Project.showDefinitions(sk.key, Project.definitions(structure, true, sk.key))
case HowComplete(from) =>
val basedir = new File(Project.session(s).current.build)
SettingGraph.findChains(structure, basedir, from, sk)
}
}
}
// A messy version that also works with sbt 1.2.1 (but without the "how" subcommand)
/* sbt -- Simple Build Tool
* Copyright 2011 Mark Harrah, Eugene Yokota
*/
package sbt
package internal
package myinspect
//import sbt.internal.util.{ AttributeKey, complete, Types }
import complete.{ DefaultParsers, Parser }
import DefaultParsers._
//import Def.ScopedKey
//import Types.idFun
//import java.io.File
import Scope.Global
import sbt.internal.util.{ JLine }
//import sbt.util.Show
//import java.io.File
import Def.{ compiled, flattenLocals }
import Predef.{ any2stringadd => _, _ }
//import sbt.io.IO
case class SettingGraph(
name: String,
definedIn: Option[String],
data: Option[ScopedKeyData[_]],
description: Option[String],
basedir: File,
depends: Set[SettingGraph]
) {
def dataString: String =
data map { d =>
d.settingValue map {
case f: File => IO.relativize(basedir, f) getOrElse { f.toString }
case x => x.toString
} getOrElse { d.typeName }
} getOrElse { "" }
def dependsAscii(defaultWidth: Int): String = Graph.toAscii(
this,
(x: SettingGraph) => x.depends.toSeq.sortBy(_.name),
(x: SettingGraph) => "%s = %s" format (x.definedIn getOrElse { "" }, x.dataString),
defaultWidth
)
}
object SettingGraph {
def apply(structure: BuildStructure, basedir: File, scoped: ScopedKey[_], generation: Int)(
implicit display: Show[ScopedKey[_]]
): SettingGraph = {
println("Got here!")
val cMap = flattenLocals(
compiled(structure.settings, true)(structure.delegates, structure.scopeLocal, display)
)
def loop(scoped: ScopedKey[_], generation: Int): SettingGraph = {
val key = scoped.key
val scope = scoped.scope
val definedIn = structure.data.definingScope(scope, key) map { sc =>
display.show(Def.ScopedKey(sc, key))
}
val depends = cMap.get(scoped) match {
case Some(c) => c.dependencies.toSet; case None => Set.empty
}
// val related = cMap.keys.filter(k => k.key == key && k.scope != scope)
// val reverse = reverseDependencies(cMap, scoped)
SettingGraph(
display.show(scoped),
definedIn,
Project.scopedKeyData(structure, scope, key),
key.description,
basedir,
depends map { (x: ScopedKey[_]) =>
loop(x, generation + 1)
}
)
}
loop(scoped, generation)
}
}
object Inspect2 {
def myInspect =
Command("inspect2", ("",""), "")(Inspect2.parser) {
case (s, f) =>
s.log.info(f())
s
}
sealed trait Mode
final case class Details(actual: Boolean) extends Mode
private[sbt] case object DependencyTreeMode extends Mode { override def toString = "tree" }
private[sbt] case object UsesMode extends Mode { override def toString = "inspect" }
private[sbt] case object DefinitionsMode extends Mode { override def toString = "definitions" }
val DependencyTree: Mode = DependencyTreeMode
val Uses: Mode = UsesMode
val Definitions: Mode = DefinitionsMode
def parser: State => Parser[() => String] =
(s: State) =>
spacedModeParser(s) flatMap { mode =>
commandHandler(s, mode) | keyHandler(s)(mode)
}
val spacedModeParser: State => Parser[Mode] = (_: State) => {
val default = "-" ^^^ Details(false)
val actual = "actual" ^^^ Details(true)
val tree = "tree" ^^^ DependencyTree
val uses = "uses" ^^^ Uses
val definitions = "definitions" ^^^ Definitions
token(Space ~> (default | tree | actual | uses | definitions)) ?? Details(false)
}
def allKeyParser(s: State): Parser[AttributeKey[_]] = {
val keyMap = Project.structure(s).index.keyMap
token(Space ~> (ID !!! "Expected key" examples keyMap.keySet)) flatMap { key =>
Act.getKey(keyMap, key, idFun)
}
}
val spacedKeyParser: State => Parser[ScopedKey[_]] = (s: State) =>
Act.requireSession(s, token(Space) ~> Act.scopedKeyParser(s))
def keyHandler(s: State): Mode => Parser[() => String] = {
case opt @ (UsesMode | DefinitionsMode) =>
allKeyParser(s).map(key => () => keyOutput(s, opt, Def.ScopedKey(Global, key)))
case opt @ (DependencyTreeMode | Details(_)) =>
spacedKeyParser(s).map(key => () => keyOutput(s, opt, key))
}
def commandHandler(s: State, mode: Mode): Parser[() => String] = {
Space ~> commandParser(s).flatMap {
case (name, cmd) =>
cmd.tags.get(BasicCommands.CommandAliasKey) match {
case Some((_, aliasFor)) =>
def header = s"Alias for: $aliasFor"
Parser
.parse(" " ++ aliasFor, keyHandler(s)(mode))
.fold(
// If we can't find a task key for the alias target
// we don't display anymore information
_ => success(() => header),
success
)
case None =>
success(() => s"Command: $name")
}
}
}
def commandParser: State => Parser[(String, Command)] = { s =>
oneOf(s.definedCommands.map(cmd => cmd -> cmd.nameOption) collect {
case (cmd, Some(name)) => DefaultParsers.literal(name).map(_ -> cmd)
})
}
def keyOutput(s: State, option: Mode, sk: Def.ScopedKey[_]): String = {
val extracted = Project.extract(s)
import extracted._
option match {
case Details(actual) =>
Project.details(structure, actual, sk.scope, sk.key)
case DependencyTreeMode =>
val basedir = new File(Project.session(s).current.build)
SettingGraph(structure, basedir, sk,0).dependsAscii(get(sbt.Keys.asciiGraphWidth))
case UsesMode =>
Project.showUses(Project.usedBy(structure, true, sk.key))
case DefinitionsMode =>
Project.showDefinitions(sk.key, Project.definitions(structure, true, sk.key))
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment