From db2a871391a4d48a2a91992efaed41e498b889dd Mon Sep 17 00:00:00 2001 From: Shani Elharrar Date: Mon, 19 Mar 2018 00:45:11 +0200 Subject: [PATCH] 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 --- .../src/main/scala/coursier/util/Print.scala | 193 +++++++++++------- .../src/main/scala/coursier/util/Tree.scala | 14 +- .../main/scala/coursier/CoursierPlugin.scala | 8 +- .../src/main/scala/coursier/Keys.scala | 6 +- .../src/main/scala/coursier/Tasks.scala | 78 +++++-- .../sbt-coursier/dependency-graph/build.sbt | 13 ++ .../dependency-graph/project/plugins.sbt | 11 + .../src/main/scala/Main.scala | 8 + .../sbt-coursier/dependency-graph/test | 2 + .../dependency-graph/whatDependsOnResult.log | 9 + .../test/scala/coursier/util/PrintTests.scala | 38 ++++ 11 files changed, 278 insertions(+), 102 deletions(-) create mode 100644 sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/build.sbt create mode 100644 sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/project/plugins.sbt create mode 100644 sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/src/main/scala/Main.scala create mode 100644 sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/test create mode 100644 sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/whatDependsOnResult.log diff --git a/core/shared/src/main/scala/coursier/util/Print.scala b/core/shared/src/main/scala/coursier/util/Print.scala index b2b468840..7d10ce024 100644 --- a/core/shared/src/main/scala/coursier/util/Print.scala +++ b/core/shared/src/main/scala/coursier/util/Print.scala @@ -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)) } } diff --git a/core/shared/src/main/scala/coursier/util/Tree.scala b/core/shared/src/main/scala/coursier/util/Tree.scala index 39c9ab692..f65b2994e 100644 --- a/core/shared/src/main/scala/coursier/util/Tree.scala +++ b/core/shared/src/main/scala/coursier/util/Tree.scala @@ -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 diff --git a/sbt-coursier/src/main/scala/coursier/CoursierPlugin.scala b/sbt-coursier/src/main/scala/coursier/CoursierPlugin.scala index 9699603f3..c2d520f22 100644 --- a/sbt-coursier/src/main/scala/coursier/CoursierPlugin.scala +++ b/sbt-coursier/src/main/scala/coursier/CoursierPlugin.scala @@ -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, "").parsed._2 + Tasks.coursierWhatDependsOnTask(input) + }.evaluated ) def makeIvyXmlBefore[T]( diff --git a/sbt-coursier/src/main/scala/coursier/Keys.scala b/sbt-coursier/src/main/scala/coursier/Keys.scala index 015a10391..20b66924b 100644 --- a/sbt-coursier/src/main/scala/coursier/Keys.scala +++ b/sbt-coursier/src/main/scala/coursier/Keys.scala @@ -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") diff --git a/sbt-coursier/src/main/scala/coursier/Tasks.scala b/sbt-coursier/src/main/scala/coursier/Tasks.scala index 3670b21a5..6a8e24bfc 100644 --- a/sbt-coursier/src/main/scala/coursier/Tasks.scala +++ b/sbt-coursier/src/main/scala/coursier/Tasks.scala @@ -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 + } + } diff --git a/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/build.sbt b/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/build.sbt new file mode 100644 index 000000000..c628273cb --- /dev/null +++ b/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/build.sbt @@ -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) +} diff --git a/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/project/plugins.sbt b/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/project/plugins.sbt new file mode 100644 index 000000000..152225a9e --- /dev/null +++ b/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/project/plugins.sbt @@ -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) +} diff --git a/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/src/main/scala/Main.scala b/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/src/main/scala/Main.scala new file mode 100644 index 000000000..032874759 --- /dev/null +++ b/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/src/main/scala/Main.scala @@ -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")) +} diff --git a/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/test b/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/test new file mode 100644 index 000000000..d2b57bc5c --- /dev/null +++ b/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/test @@ -0,0 +1,2 @@ +> coursierDependencyTree +> whatDependsOnCheck diff --git a/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/whatDependsOnResult.log b/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/whatDependsOnResult.log new file mode 100644 index 000000000..365ee39a0 --- /dev/null +++ b/sbt-coursier/src/sbt-test/sbt-coursier/dependency-graph/whatDependsOnResult.log @@ -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 diff --git a/tests/shared/src/test/scala/coursier/util/PrintTests.scala b/tests/shared/src/test/scala/coursier/util/PrintTests.scala index 3b0b37645..17abdee24 100644 --- a/tests/shared/src/test/scala/coursier/util/PrintTests.scala +++ b/tests/shared/src/test/scala/coursier/util/PrintTests.scala @@ -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) + }) + } } }