Print: use roots when rendering reverse graph (#808)

* Print: use roots when rendering reverse graph

* Print: Refactored to be able to return Tree data structure

* SBT: Refactored code and added coursierWhatDependsOn Task (?)

* SBT: Added tests to the new coursierWhatDependsOn task

* Tasks: Another try to fix the build

* Tree: Restored Apply[A] for binary compatibility

* Print.scala: Fix the build

* sbt-coursier: Refined what depends on check to check correctness

* PrintTests: Added test for reverseTree

* CR Fixes...

1. changed gotVersion to reconciledVersion
2. Moved AppliedTree to tests project
3. Changed message (wanted version, got version) to X -> Y
This commit is contained in:
Shani Elharrar 2018-03-19 00:45:11 +02:00 committed by Alexandre Archambault
parent 15c9be7950
commit db2a871391
11 changed files with 278 additions and 102 deletions

View File

@ -4,6 +4,35 @@ import coursier.core.{ Attributes, Dependency, Module, Orders, Project, Resoluti
object Print {
object Colors {
private val `with`: Colors = Colors(Console.RED, Console.YELLOW, Console.RESET)
private val `without`: Colors = Colors("", "", "")
def get(colors: Boolean): Colors = if (colors) `with` else `without`
}
case class Colors private(red: String, yellow: String, reset: String)
trait Renderable {
def repr(colors: Colors): String
}
trait Elem extends Renderable {
def dep: Dependency
def excluded: Boolean
def reconciledVersion: String
def children: Seq[Elem]
}
trait Parent extends Renderable {
def module: Module
def version: String
def dependsOn: Module
def wantVersion: String
def reconciledVersion: String
def excluding: Boolean
}
def dependency(dep: Dependency): String =
dependency(dep, printExclusions = false)
@ -80,23 +109,27 @@ object Print {
reverse: Boolean,
colors: Boolean
): String = {
val colorsCase = Colors.get(colors)
val (red, yellow, reset) =
if (colors)
(Console.RED, Console.YELLOW, Console.RESET)
else
("", "", "")
if (reverse) {
reverseTree(resolution.dependencies.toSeq, resolution, printExclusions).render(_.repr(colorsCase))
} else {
normalTree(roots, resolution, printExclusions).render(_.repr(colorsCase))
}
final case class Elem(dep: Dependency, excluded: Boolean) {
}
lazy val reconciledVersion = resolution.reconciledVersions
private def getElemFactory(resolution: Resolution, withExclusions: Boolean): Dependency => Elem = {
final case class ElemImpl(dep: Dependency, excluded: Boolean) extends Elem {
val reconciledVersion: String = resolution.reconciledVersions
.getOrElse(dep.module, dep.version)
lazy val repr =
def repr(colors: Colors): String =
if (excluded)
resolution.reconciledVersions.get(dep.module) match {
case None =>
s"$yellow(excluded)$reset ${dep.module}:${dep.version}"
s"${colors.yellow}(excluded)${colors.reset} ${dep.module}:${dep.version}"
case Some(version) =>
val versionMsg =
if (version == dep.version)
@ -104,8 +137,8 @@ object Print {
else
s"version $version"
s"${dep.module}:${dep.version} " +
s"$red(excluded, $versionMsg present anyway)$reset"
s"${dep.module}:${dep.version} " +
s"${colors.red}(excluded, $versionMsg present anyway)${colors.reset}"
}
else {
val versionStr =
@ -114,16 +147,16 @@ object Print {
else {
val assumeCompatibleVersions = compatibleVersions(dep.version, reconciledVersion)
(if (assumeCompatibleVersions) yellow else red) +
(if (assumeCompatibleVersions) colors.yellow else colors.red) +
s"${dep.version} -> $reconciledVersion" +
(if (assumeCompatibleVersions || colors) "" else " (possible incompatibility)") +
reset
(if (assumeCompatibleVersions) "" else " (possible incompatibility)") +
colors.reset
}
s"${dep.module}:$versionStr"
}
lazy val children: Seq[Elem] =
val children: Seq[Elem] =
if (excluded)
Nil
else {
@ -146,81 +179,85 @@ object Print {
}
.map(_.moduleVersion)
.filterNot(dependencies.map(_.moduleVersion).toSet).map {
case (mod, ver) =>
Elem(
Dependency(mod, ver, "", Set.empty, Attributes("", ""), false, false),
excluded = true
)
}
case (mod, ver) =>
ElemImpl(
Dependency(mod, ver, "", Set.empty, Attributes("", ""), false, false),
excluded = true
)
}
dependencies.map(Elem(_, excluded = false)) ++
(if (printExclusions) excluded else Nil)
dependencies.map(ElemImpl(_, excluded = false)) ++
(if (withExclusions) excluded else Nil)
}
}
if (reverse) {
a => ElemImpl(a, excluded = false)
}
final case class Parent(
module: Module,
version: String,
dependsOn: Module,
wantVersion: String,
gotVersion: String,
excluding: Boolean
) {
lazy val repr: String =
if (excluding)
s"$yellow(excluded by)$reset $module:$version"
else if (wantVersion == gotVersion)
s"$module:$version"
else {
val assumeCompatibleVersions = compatibleVersions(wantVersion, gotVersion)
def normalTree(roots: Seq[Dependency], resolution: Resolution, withExclusions: Boolean): Tree[Elem] = {
val elemFactory = getElemFactory(resolution, withExclusions)
Tree[Elem](roots.toVector.map(elemFactory), (elem: Elem) => elem.children)
}
s"$module:$version " +
(if (assumeCompatibleVersions) yellow else red) +
s"(wants $dependsOn:$wantVersion, got $gotVersion)" +
reset
}
}
def reverseTree(roots: Seq[Dependency], resolution: Resolution, withExclusions: Boolean): Tree[Parent] = {
val elemFactory = getElemFactory(resolution, withExclusions)
val parents: Map[Module, Seq[Parent]] = {
val links = for {
dep <- resolution.dependencies.toVector
elem <- Elem(dep, excluded = false).children
final case class ParentImpl(
module: Module,
version: String,
dependsOn: Module,
wantVersion: String,
reconciledVersion: String,
excluding: Boolean
) extends Parent {
def repr(colors: Colors): String =
if (excluding)
s"${colors.yellow}(excluded by)${colors.reset} $module:$version"
else if (wantVersion == reconciledVersion)
s"$module:$version"
else {
val assumeCompatibleVersions = compatibleVersions(wantVersion, reconciledVersion)
s"$module:$version " +
(if (assumeCompatibleVersions) colors.yellow else colors.red) +
s"$dependsOn:$wantVersion -> $reconciledVersion" +
colors.reset
}
yield elem.dep.module -> Parent(
dep.module,
dep.version,
elem.dep.module,
elem.dep.version,
elem.reconciledVersion,
elem.excluded
)
}
links
.groupBy(_._1)
.mapValues(_.map(_._2).distinct.sortBy(par => (par.module.organization, par.module.name)))
.iterator
.toMap
val parents: Map[Module, Seq[Parent]] = {
val links = for {
dep <- resolution.dependencies.toVector
elem <- elemFactory(dep).children
}
yield elem.dep.module -> ParentImpl(
dep.module,
dep.version,
elem.dep.module,
elem.dep.version,
elem.reconciledVersion,
elem.excluded
)
def children(par: Parent) =
if (par.excluding)
Nil
else
parents.getOrElse(par.module, Nil)
links
.groupBy(_._1)
.mapValues(_.map(_._2).distinct.sortBy(par => (par.module.organization, par.module.name)))
.iterator
.toMap
}
Tree(
resolution
.dependencies
.toVector
.sortBy(dep => (dep.module.organization, dep.module.name, dep.version))
.map(dep =>
Parent(dep.module, dep.version, dep.module, dep.version, dep.version, excluding = false)
)
)(children, _.repr)
} else
Tree(roots.toVector.map(Elem(_, excluded = false)))(_.children, _.repr)
def children(par: Parent) =
if (par.excluding)
Nil
else
parents.getOrElse(par.module, Nil)
Tree[Parent](roots
.toVector
.sortBy(dep => (dep.module.organization, dep.module.name, dep.version))
.map(dep => {
ParentImpl(dep.module, dep.version, dep.module, dep.version, dep.version, excluding = false)
}), (par: Parent) => children(par))
}
}

View File

@ -4,7 +4,15 @@ import scala.collection.mutable.ArrayBuffer
object Tree {
def apply[T](roots: IndexedSeq[T])(children: T => Seq[T], show: T => String): String = {
def apply[A](roots: IndexedSeq[A])(children: A => Seq[A], show: A => String): String = {
Tree(roots, children).render(show)
}
}
case class Tree[A](roots: IndexedSeq[A], children: A => Seq[A]) {
def render(show: A => String): String = {
/**
* Recursively go down the resolution for the elems to construct the tree for print out.
@ -14,8 +22,8 @@ object Tree {
* @param prefix prefix for the print out
* @param acc accumulation method on a string
*/
def recursivePrint(elems: Seq[T], ancestors: Set[T], prefix: String, acc: String => Unit): Unit = {
val unseenElems: Seq[T] = elems.filterNot(ancestors.contains)
def recursivePrint(elems: Seq[A], ancestors: Set[A], prefix: String, acc: String => Unit): Unit = {
val unseenElems: Seq[A] = elems.filterNot(ancestors.contains)
val unseenElemsLen = unseenElems.length
for ((elem, idx) <- unseenElems.iterator.zipWithIndex) {
val isLast = idx == unseenElemsLen - 1

View File

@ -45,6 +45,7 @@ object CoursierPlugin extends AutoPlugin {
val coursierDependencyTree = Keys.coursierDependencyTree
val coursierDependencyInverseTree = Keys.coursierDependencyInverseTree
val coursierWhatDependsOn = Keys.coursierWhatDependsOn
val coursierArtifacts = Keys.coursierArtifacts
val coursierSignedArtifacts = Keys.coursierSignedArtifacts
@ -65,7 +66,12 @@ object CoursierPlugin extends AutoPlugin {
).value,
coursierDependencyInverseTree := Tasks.coursierDependencyTreeTask(
inverse = true
).value
).value,
coursierWhatDependsOn := Def.inputTaskDyn {
import sbt.complete.DefaultParsers._
val input = token(SpaceClass ~ NotQuoted, "<arg>").parsed._2
Tasks.coursierWhatDependsOnTask(input)
}.evaluated
)
def makeIvyXmlBefore[T](

View File

@ -5,7 +5,7 @@ import java.net.URL
import coursier.core.Publication
import sbt.librarymanagement.GetClassifiersModule
import sbt.{Resolver, SettingKey, TaskKey}
import sbt.{InputKey, Resolver, SettingKey, TaskKey}
import scala.concurrent.duration.Duration
@ -62,6 +62,10 @@ object Keys {
"Prints dependencies and transitive dependencies as an inverted tree (dependees as children)"
)
val coursierWhatDependsOn = InputKey[String](
"coursier-what-depends-on",
"Prints dependencies and transitive dependencies as an inverted tree for a specific module (dependees as children)"
)
val coursierArtifacts = TaskKey[Map[Artifact, Either[FileError, File]]]("coursier-artifacts")
val coursierSignedArtifacts = TaskKey[Map[Artifact, Either[FileError, File]]]("coursier-signed-artifacts")
val coursierClassifiersArtifacts = TaskKey[Map[Artifact, Either[FileError, File]]]("coursier-classifiers-artifacts")

View File

@ -10,7 +10,8 @@ import coursier.interop.scalaz._
import coursier.ivy.{IvyRepository, PropertiesPattern}
import coursier.Keys._
import coursier.Structure._
import coursier.util.Print
import coursier.util.Print.Colors
import coursier.util.{Parse, Print}
import sbt.librarymanagement._
import sbt.{Classpaths, Def, Resolver, UpdateReport}
import sbt.Keys._
@ -1349,13 +1350,12 @@ object Tasks {
}
}
def coursierDependencyTreeTask(
inverse: Boolean,
case class ResolutionResult(configs: Set[String], resolution: Resolution, dependencies: Seq[Dependency])
private def coursierResolutionTask(
sbtClassifiers: Boolean = false,
ignoreArtifactErrors: Boolean = false
) = Def.taskDyn {
val projectName = thisProjectRef.value.project
): Def.Initialize[sbt.Task[Seq[ResolutionResult]]] = Def.taskDyn {
val currentProjectTask =
if (sbtClassifiers)
@ -1393,9 +1393,9 @@ object Tasks {
val resolutions = resolutionsTask.value
for {
(subGraphConfigs, res) <- resolutions
(subGraphConfigs, res) <- resolutions.toSeq
if subGraphConfigs.exists(includedConfigs)
} {
} yield {
val dependencies0 = currentProject.dependencies.collect {
case (cfg, dep) if includedConfigs(cfg) && subGraphConfigs(cfg) => dep
@ -1405,20 +1405,60 @@ object Tasks {
val subRes = res.subset(dependencies0.toSet)
// use sbt logging?
println(
s"$projectName (configurations ${subGraphConfigs.toVector.sorted.mkString(", ")})" + "\n" +
Print.dependencyTree(
dependencies0,
subRes,
printExclusions = true,
inverse,
colors = !sys.props.get("sbt.log.noformat").toSeq.contains("true")
)
)
ResolutionResult(subGraphConfigs, subRes, dependencies0)
}
}
}
}
def coursierDependencyTreeTask(
inverse: Boolean,
sbtClassifiers: Boolean = false,
ignoreArtifactErrors: Boolean = false
) = Def.task {
val projectName = thisProjectRef.value.project
val resolutions = coursierResolutionTask(sbtClassifiers, ignoreArtifactErrors).value
for (ResolutionResult(subGraphConfigs, resolution, dependencies) <- resolutions) {
// use sbt logging?
println(
s"$projectName (configurations ${subGraphConfigs.toVector.sorted.mkString(", ")})" + "\n" +
Print.dependencyTree(
dependencies,
resolution,
printExclusions = true,
inverse,
colors = !sys.props.get("sbt.log.noformat").toSeq.contains("true")
)
)
}
}
def coursierWhatDependsOnTask(
moduleName: String,
sbtClassifiers: Boolean = false,
ignoreArtifactErrors: Boolean = false
) = Def.task {
val module = Parse.module(moduleName, scalaVersion.value)
.right
.getOrElse(throw new RuntimeException(s"Could not parse module `$moduleName`"))
val projectName = thisProjectRef.value.project
val resolutions = coursierResolutionTask(sbtClassifiers, ignoreArtifactErrors).value
val result = new mutable.StringBuilder()
for (ResolutionResult(subGraphConfigs, resolution, _) <- resolutions) {
val roots: Seq[Dependency] = resolution.transitiveDependencies.filter(f => f.module == module)
val strToPrint = s"$projectName (configurations ${subGraphConfigs.toVector.sorted.mkString(", ")})" + "\n" +
Print.reverseTree(roots, resolution, withExclusions = true)
.render(_.repr(Colors.get(!sys.props.get("sbt.log.noformat").toSeq.contains("true"))));
println(strToPrint)
result.append(strToPrint)
result.append("\n")
}
result.toString
}
}

View File

@ -0,0 +1,13 @@
scalaVersion := "2.11.8"
libraryDependencies += "org.apache.zookeeper" % "zookeeper" % "3.5.0-alpha"
lazy val whatDependsOnCheck = TaskKey[Unit]("whatDependsOnCheck")
import CoursierPlugin.autoImport._
whatDependsOnCheck := {
val result = (coursierWhatDependsOn in Compile).toTask(" log4j:log4j").value
val file = new File("whatDependsOnResult.log")
assert(IO.read(file).toString == result)
}

View File

@ -0,0 +1,11 @@
{
val pluginVersion = sys.props.getOrElse(
"plugin.version",
throw new RuntimeException(
"""|The system property 'plugin.version' is not defined.
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin
)
)
addSbtPlugin("io.get-coursier" % "sbt-coursier" % pluginVersion)
}

View File

@ -0,0 +1,8 @@
import java.io.File
import java.nio.file.Files
import org.apache.zookeeper.ZooKeeper
object Main extends App {
Files.write(new File("output").toPath, classOf[ZooKeeper].getSimpleName.getBytes("UTF-8"))
}

View File

@ -0,0 +1,2 @@
> coursierDependencyTree
> whatDependsOnCheck

View File

@ -0,0 +1,9 @@
dependency-graph (configurations compile, compile-internal, optional, provided, runtime, runtime-internal, test, test-internal)
├─ log4j:log4j:1.2.16
│ ├─ org.apache.zookeeper:zookeeper:3.5.0-alpha log4j:log4j:1.2.16 -> 1.2.17
│ └─ org.slf4j:slf4j-log4j12:1.7.5
│ └─ org.apache.zookeeper:zookeeper:3.5.0-alpha
└─ log4j:log4j:1.2.17
├─ org.apache.zookeeper:zookeeper:3.5.0-alpha log4j:log4j:1.2.16 -> 1.2.17
└─ org.slf4j:slf4j-log4j12:1.7.5
└─ org.apache.zookeeper:zookeeper:3.5.0-alpha

View File

@ -1,11 +1,25 @@
package coursier.util
import coursier.core.Attributes
import coursier.test.CentralTests
import coursier.{Dependency, Module}
import utest._
import scala.concurrent.ExecutionContext.Implicits.global
object PrintTests extends TestSuite {
object AppliedTree {
def apply[A](tree: Tree[A]): Seq[AppliedTree[A]] = {
tree.roots.map(root => {
AppliedTree[A](root, apply(Tree(tree.children(root).toIndexedSeq, tree.children)))
})
}
}
case class AppliedTree[A](root: A, children: Seq[AppliedTree[A]])
val tests = Tests {
'ignoreAttributes - {
val dep = Dependency(
@ -23,6 +37,30 @@ object PrintTests extends TestSuite {
assert(res == expectedRes)
}
'reverseTree - {
val junit = Module("junit", "junit")
val junitVersion = "4.10"
CentralTests.resolve(Set(Dependency(junit, junitVersion))).map(result => {
val hamcrest = Module("org.hamcrest", "hamcrest-core")
val hamcrestVersion = "1.1"
val reverseTree = Print.reverseTree(Seq(Dependency(hamcrest, hamcrestVersion)),
result, withExclusions = true)
val applied = AppliedTree.apply(reverseTree)
assert(applied.length == 1)
val expectedHead = applied.head
assert(expectedHead.root.module == hamcrest)
assert(expectedHead.root.version == hamcrestVersion)
assert(expectedHead.children.length == 1)
val expectedChild = expectedHead.children.head
assert(expectedChild.root.module == junit)
assert(expectedChild.root.version == junitVersion)
})
}
}
}