Allow to print dependency trees

This commit is contained in:
Alexandre Archambault 2016-05-06 13:53:49 +02:00
parent 4b0589dc90
commit 3834a9519c
No known key found for this signature in database
GPG Key ID: 14640A6839C263A9
8 changed files with 461 additions and 87 deletions

View File

@ -354,10 +354,16 @@ class Helper(
lazy val projCache = res.projectCache.mapValues { case (_, p) => p }
if (printResultStdout || verbosityLevel >= 1) {
if ((printResultStdout && verbosityLevel >= 1) || verbosityLevel >= 2)
if (printResultStdout || verbosityLevel >= 1 || tree || reverseTree) {
if ((printResultStdout && verbosityLevel >= 1) || verbosityLevel >= 2 || tree || reverseTree)
errPrintln(s" Result:")
val depsStr = Print.dependenciesUnknownConfigs(trDeps, projCache)
val depsStr =
if (reverseTree || tree)
Print.dependencyTree(dependencies, res, printExclusions = verbosityLevel >= 1, reverse = reverseTree)
else
Print.dependenciesUnknownConfigs(trDeps, projCache)
if (printResultStdout)
println(depsStr)
else

View File

@ -61,6 +61,12 @@ case class CommonOptions(
@Short("B")
@Value("Number of warm-up resolutions - if negative, doesn't print per iteration benchmark (less overhead)")
benchmark: Int,
@Help("Print dependencies as a tree")
@Short("t")
tree: Boolean,
@Help("Print dependencies as an inversed tree (dependees as children)")
@Short("T")
reverseTree: Boolean,
@Recurse
cacheOptions: CacheOptions
) {

View File

@ -529,6 +529,16 @@ final case class Resolution(
} else
Nil
def dependenciesOf(dep: Dependency, withReconciledVersions: Boolean = true): Seq[Dependency] =
if (withReconciledVersions)
finalDependencies0(dep).map { trDep =>
trDep.copy(
version = reconciledVersions.getOrElse(trDep.module, trDep.version)
)
}
else
finalDependencies0(dep)
/**
* Transitive dependencies of the current dependencies, according to
* what there currently is in cache.
@ -558,6 +568,9 @@ final case class Resolution(
forceVersions
)
def reconciledVersions: Map[Module, String] =
nextDependenciesAndConflicts._3
/**
* The modules we miss some info about.
*/
@ -974,10 +987,9 @@ final case class Resolution(
* @param dependencies: the dependencies to keep from this `Resolution`
*/
def subset(dependencies: Set[Dependency]): Resolution = {
val (_, _, finalVersions) = nextDependenciesAndConflicts
def updateVersion(dep: Dependency): Dependency =
dep.copy(version = finalVersions.getOrElse(dep.module, dep.version))
dep.copy(version = reconciledVersions.getOrElse(dep.module, dep.version))
@tailrec def helper(current: Set[Dependency]): Set[Dependency] = {
val newDeps = current ++ current

View File

@ -1,6 +1,6 @@
package coursier.util
import coursier.core.{Module, Project, Orders, Dependency}
import coursier.core.{ Attributes, Dependency, Module, Orders, Project, Resolution }
object Print {
@ -41,4 +41,154 @@ object Print {
deps1.map(dependency).mkString("\n")
}
private def compatibleVersions(first: String, second: String): Boolean = {
// too loose for now
// e.g. RCs and milestones should not be considered compatible with subsequent non-RC or
// milestone versions - possibly not with each other either
first.split('.').take(2).toSeq == second.split('.').take(2).toSeq
}
def dependencyTree(
roots: Seq[Dependency],
resolution: Resolution,
printExclusions: Boolean,
reverse: Boolean
): String = {
case class Elem(dep: Dependency, excluded: Boolean) {
lazy val reconciledVersion = resolution.reconciledVersions
.getOrElse(dep.module, dep.version)
lazy val repr =
if (excluded)
resolution.reconciledVersions.get(dep.module) match {
case None =>
s"${Console.YELLOW}(excluded)${Console.RESET} ${dep.module}:${dep.version}"
case Some(version) =>
val versionMsg =
if (version == dep.version)
"this version"
else
s"version $version"
s"${dep.module}:${dep.version} " +
s"${Console.RED}(excluded, $versionMsg present anyway)${Console.RESET}"
}
else {
val versionStr =
if (reconciledVersion == dep.version)
dep.version
else {
val assumeCompatibleVersions = compatibleVersions(dep.version, reconciledVersion)
(if (assumeCompatibleVersions) Console.YELLOW else Console.RED) +
s"${dep.version} -> $reconciledVersion" +
Console.RESET
}
s"${dep.module}:$versionStr"
}
lazy val children: Seq[Elem] =
if (excluded)
Nil
else {
val dep0 = dep.copy(version = reconciledVersion)
val dependencies = resolution.dependenciesOf(
dep0,
withReconciledVersions = false
).sortBy { trDep =>
(trDep.module.organization, trDep.module.name, trDep.version)
}
def excluded = resolution
.dependenciesOf(
dep0.copy(exclusions = Set.empty),
withReconciledVersions = false
)
.sortBy { trDep =>
(trDep.module.organization, trDep.module.name, trDep.version)
}
.map(_.moduleVersion)
.filterNot(dependencies.map(_.moduleVersion).toSet).map {
case (mod, ver) =>
Elem(
Dependency(mod, ver, "", Set.empty, Attributes("", ""), false, false),
excluded = true
)
}
dependencies.map(Elem(_, excluded = false)) ++
(if (printExclusions) excluded else Nil)
}
}
if (reverse) {
case class Parent(
module: Module,
version: String,
dependsOn: Module,
wantVersion: String,
gotVersion: String,
excluding: Boolean
) {
lazy val repr: String =
if (excluding)
s"${Console.YELLOW}(excluded by)${Console.RESET} $module:$version"
else if (wantVersion == gotVersion)
s"$module:$version"
else {
val assumeCompatibleVersions = compatibleVersions(wantVersion, gotVersion)
s"$module:$version " +
(if (assumeCompatibleVersions) Console.YELLOW else Console.RED) +
s"(wants $dependsOn:$wantVersion, got $gotVersion)" +
Console.RESET
}
}
val parents: Map[Module, Seq[Parent]] = {
val links = for {
dep <- resolution.dependencies.toVector
elem <- Elem(dep, excluded = false).children
}
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
}
def children(par: Parent) =
if (par.excluding)
Nil
else
parents.getOrElse(par.module, Nil)
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)
}
}

View File

@ -0,0 +1,30 @@
package coursier.util
import scala.collection.mutable.ArrayBuffer
object Tree {
def apply[T](roots: IndexedSeq[T])(children: T => Seq[T], print: T => String): String = {
def helper(elems: Seq[T], prefix: String, acc: String => Unit): Unit =
for ((elem, idx) <- elems.zipWithIndex) {
val isLast = idx == elems.length - 1
val tee = if (isLast) "└─ " else "├─ "
acc(prefix + tee + print(elem))
val children0 = children(elem)
if (children0.nonEmpty) {
val extraPrefix = if (isLast) " " else "| "
helper(children0, prefix + extraPrefix, acc)
}
}
val b = new ArrayBuffer[String]
helper(roots, "", b += _)
b.mkString("\n")
}
}

View File

@ -24,10 +24,26 @@ object CoursierPlugin extends AutoPlugin {
val coursierProjects = Keys.coursierProjects
val coursierPublications = Keys.coursierPublications
val coursierSbtClassifiersModule = Keys.coursierSbtClassifiersModule
val coursierConfigurations = Keys.coursierConfigurations
val coursierResolution = Keys.coursierResolution
val coursierSbtClassifiersResolution = Keys.coursierSbtClassifiersResolution
val coursierDependencyTree = Keys.coursierDependencyTree
val coursierDependencyInverseTree = Keys.coursierDependencyInverseTree
}
import autoImport._
lazy val treeSettings = Seq(
coursierDependencyTree <<= Tasks.coursierDependencyTreeTask(
inverse = false
),
coursierDependencyInverseTree <<= Tasks.coursierDependencyTreeTask(
inverse = true
)
)
override lazy val projectSettings = Seq(
coursierParallelDownloads := 6,
@ -53,7 +69,14 @@ object CoursierPlugin extends AutoPlugin {
coursierProject <<= Tasks.coursierProjectTask,
coursierProjects <<= Tasks.coursierProjectsTask,
coursierPublications <<= Tasks.coursierPublicationsTask,
coursierSbtClassifiersModule <<= classifiersModule in updateSbtClassifiers
)
coursierSbtClassifiersModule <<= classifiersModule in updateSbtClassifiers,
coursierConfigurations <<= Tasks.coursierConfigurationsTask,
coursierResolution <<= Tasks.resolutionTask(),
coursierSbtClassifiersResolution <<= Tasks.resolutionTask(
sbtClassifiers = true
)
) ++
inConfig(Compile)(treeSettings) ++
inConfig(Test)(treeSettings)
}

View File

@ -27,4 +27,18 @@ object Keys {
val coursierPublications = TaskKey[Seq[(String, Publication)]]("coursier-publications", "")
val coursierSbtClassifiersModule = TaskKey[GetClassifiersModule]("coursier-sbt-classifiers-module", "")
val coursierConfigurations = TaskKey[Map[String, Set[String]]]("coursier-configurations", "")
val coursierResolution = TaskKey[Resolution]("coursier-resolution", "")
val coursierSbtClassifiersResolution = TaskKey[Resolution]("coursier-sbt-classifiers-resolution", "")
val coursierDependencyTree = TaskKey[Unit](
"coursier-dependency-tree",
"Prints dependencies and transitive dependencies as a tree"
)
val coursierDependencyInverseTree = TaskKey[Unit](
"coursier-dependency-inverse-tree",
"Prints dependencies and transitive dependencies as an inverted tree (dependees as children)"
)
}

View File

@ -166,16 +166,49 @@ object Tasks {
sbtArtifactsPublication ++ extraSbtArtifactsPublication
}
// FIXME More things should possibly be put here too (resolvers, etc.)
private case class CacheKey(
def coursierConfigurationsTask = Def.task {
val configs0 = ivyConfigurations.value.map { config =>
config.name -> config.extendsConfigs.map(_.name)
}.toMap
def allExtends(c: String) = {
// possibly bad complexity
def helper(current: Set[String]): Set[String] = {
val newSet = current ++ current.flatMap(configs0.getOrElse(_, Nil))
if ((newSet -- current).nonEmpty)
helper(newSet)
else
newSet
}
helper(Set(c))
}
configs0.map {
case (config, _) =>
config -> allExtends(config)
}
}
private case class ResolutionCacheKey(
project: Project,
repositories: Seq[Repository],
resolution: Resolution,
sbtClassifiers: Boolean
)
private case class ReportCacheKey(
project: Project,
resolution: Resolution,
withClassifiers: Boolean,
sbtClassifiers: Boolean
)
private val resolutionsCache = new mutable.HashMap[CacheKey, UpdateReport]
private val resolutionsCache = new mutable.HashMap[ResolutionCacheKey, Resolution]
// these may actually not need to be cached any more, now that the resolutions
// are cached
private val reportsCache = new mutable.HashMap[ReportCacheKey, UpdateReport]
private def forcedScalaModules(scalaVersion: String): Map[Module, String] =
Map(
@ -185,17 +218,12 @@ object Tasks {
Module("org.scala-lang", "scalap") -> scalaVersion
)
def updateTask(
withClassifiers: Boolean,
sbtClassifiers: Boolean = false,
ignoreArtifactErrors: Boolean = false
) = Def.task {
private def projectDescription(project: Project) =
s"${project.module.organization}:${project.module.name}:${project.version}"
def grouped[K, V](map: Seq[(K, V)]): Map[K, Seq[V]] =
map.groupBy { case (k, _) => k }.map {
case (k, l) =>
k -> l.map { case (_, v) => v }
}
def resolutionTask(
sbtClassifiers: Boolean = false
) = Def.task {
// let's update only one module at once, for a better output
// Downloads are already parallel, no need to parallelize further anyway
@ -230,25 +258,10 @@ object Tasks {
(proj.copy(publications = publications), fallbackDeps)
}
val ivySbt0 = ivySbt.value
val ivyCacheManager = ivySbt0.withIvy(streams.value.log)(ivy =>
ivy.getResolutionCacheManager
)
val ivyModule = ModuleRevisionId.newInstance(
currentProject.module.organization,
currentProject.module.name,
currentProject.version,
currentProject.module.attributes.asJava
)
val cacheIvyFile = ivyCacheManager.getResolvedIvyFileInCache(ivyModule)
val cacheIvyPropertiesFile = ivyCacheManager.getResolvedIvyPropertiesInCache(ivyModule)
val projects = coursierProjects.value
val parallelDownloads = coursierParallelDownloads.value
val checksums = coursierChecksums.value
val artifactsChecksums = coursierArtifactsChecksums.value
val maxIterations = coursierMaxIterations.value
val cachePolicies = coursierCachePolicies.value
val cache = coursierCache.value
@ -298,22 +311,6 @@ object Tasks {
forceVersions = userForceVersions ++ forcedScalaModules(sv) ++ projects.map(_.moduleVersion)
)
// required for publish to be fine, later on
def writeIvyFiles() = {
val printer = new scala.xml.PrettyPrinter(80, 2)
val b = new StringBuilder
b ++= """<?xml version="1.0" encoding="UTF-8"?>"""
b += '\n'
b ++= printer.format(MakeIvyXml(currentProject))
cacheIvyFile.getParentFile.mkdirs()
Files.write(cacheIvyFile.toPath, b.result().getBytes("UTF-8"))
// Just writing an empty file here... Are these only used?
cacheIvyPropertiesFile.getParentFile.mkdirs()
Files.write(cacheIvyPropertiesFile.toPath, "".getBytes("UTF-8"))
}
if (verbosityLevel >= 2) {
log.info("InterProjectRepository")
for (p <- projects)
@ -354,7 +351,7 @@ object Tasks {
}
}
def report = {
def resolution = {
val pool = Executors.newFixedThreadPool(parallelDownloads, Strategy.DefaultDaemonThreadFactory)
def createLogger() = new TermDisplay(new OutputStreamWriter(System.err))
@ -374,11 +371,6 @@ object Tasks {
s"${dep.module}:${dep.version}:$config->${dep.configuration}"
}.sorted.distinct
def depsRepr0(deps: Seq[Dependency]) =
deps.map { dep =>
s"${dep.module}:${dep.version}:${dep.configuration}"
}.sorted.distinct
if (verbosityLevel >= 2) {
val repoReprs = repositories.map {
case r: IvyRepository =>
@ -399,7 +391,10 @@ object Tasks {
}
if (verbosityLevel >= 0)
log.info(s"Updating ${currentProject.module.organization}:${currentProject.module.name}:${currentProject.version}")
log.info(
s"Updating ${projectDescription(currentProject)}" +
(if (sbtClassifiers) " (sbt classifiers)" else "")
)
if (verbosityLevel >= 2)
for (depRepr <- depsRepr(currentProject.dependencies))
log.info(s" $depRepr")
@ -443,34 +438,115 @@ object Tasks {
throw new Exception(s"Encountered ${res.errors.length} error(s) in dependency resolution")
}
val depsByConfig = grouped(currentProject.dependencies)
if (verbosityLevel >= 0)
log.info(s"Resolved ${projectDescription(currentProject)} dependencies")
val configs = {
val configs0 = ivyConfigurations.value.map { config =>
config.name -> config.extendsConfigs.map(_.name)
}.toMap
res
}
def allExtends(c: String) = {
// possibly bad complexity
def helper(current: Set[String]): Set[String] = {
val newSet = current ++ current.flatMap(configs0.getOrElse(_, Nil))
if ((newSet -- current).nonEmpty)
helper(newSet)
else
newSet
}
resolutionsCache.getOrElseUpdate(
ResolutionCacheKey(
currentProject,
repositories,
startRes.copy(filter = None),
sbtClassifiers
),
resolution
)
}
}
helper(Set(c))
}
def updateTask(
withClassifiers: Boolean,
sbtClassifiers: Boolean = false,
ignoreArtifactErrors: Boolean = false
) = Def.task {
configs0.map {
case (config, _) =>
config -> allExtends(config)
}
def grouped[K, V](map: Seq[(K, V)]): Map[K, Seq[V]] =
map.groupBy { case (k, _) => k }.map {
case (k, l) =>
k -> l.map { case (_, v) => v }
}
// let's update only one module at once, for a better output
// Downloads are already parallel, no need to parallelize further anyway
synchronized {
lazy val cm = coursierSbtClassifiersModule.value
val currentProject =
if (sbtClassifiers) {
val sv = scalaVersion.value
val sbv = scalaBinaryVersion.value
FromSbt.project(
cm.id,
cm.modules,
cm.configurations.map(cfg => cfg.name -> cfg.extendsConfigs.map(_.name)).toMap,
sv,
sbv
)
} else {
val proj = coursierProject.value
val publications = coursierPublications.value
proj.copy(publications = publications)
}
if (verbosityLevel >= 0)
log.info("Resolution done")
val ivySbt0 = ivySbt.value
val ivyCacheManager = ivySbt0.withIvy(streams.value.log)(ivy =>
ivy.getResolutionCacheManager
)
val ivyModule = ModuleRevisionId.newInstance(
currentProject.module.organization,
currentProject.module.name,
currentProject.version,
currentProject.module.attributes.asJava
)
val cacheIvyFile = ivyCacheManager.getResolvedIvyFileInCache(ivyModule)
val cacheIvyPropertiesFile = ivyCacheManager.getResolvedIvyPropertiesInCache(ivyModule)
val parallelDownloads = coursierParallelDownloads.value
val artifactsChecksums = coursierArtifactsChecksums.value
val cachePolicies = coursierCachePolicies.value
val cache = coursierCache.value
val log = streams.value.log
val verbosityLevel = coursierVerbosity.value
// required for publish to be fine, later on
def writeIvyFiles() = {
val printer = new scala.xml.PrettyPrinter(80, 2)
val b = new StringBuilder
b ++= """<?xml version="1.0" encoding="UTF-8"?>"""
b += '\n'
b ++= printer.format(MakeIvyXml(currentProject))
cacheIvyFile.getParentFile.mkdirs()
Files.write(cacheIvyFile.toPath, b.result().getBytes("UTF-8"))
// Just writing an empty file here... Are these only used?
cacheIvyPropertiesFile.getParentFile.mkdirs()
Files.write(cacheIvyPropertiesFile.toPath, "".getBytes("UTF-8"))
}
val res = {
if (withClassifiers && sbtClassifiers)
coursierSbtClassifiersResolution
else
coursierResolution
}.value
def report = {
val pool = Executors.newFixedThreadPool(parallelDownloads, Strategy.DefaultDaemonThreadFactory)
def createLogger() = new TermDisplay(new OutputStreamWriter(System.err))
val depsByConfig = grouped(currentProject.dependencies)
val configs = coursierConfigurations.value
if (verbosityLevel >= 2) {
val finalDeps = Config.dependenciesWithConfig(
res,
@ -520,7 +596,10 @@ object Tasks {
}
if (verbosityLevel >= 0)
log.info("Fetching artifacts")
log.info(
s"Fetching artifacts of ${projectDescription(currentProject)}" +
(if (sbtClassifiers) " (sbt classifiers)" else "")
)
artifactsLogger.init()
@ -534,7 +613,10 @@ object Tasks {
artifactsLogger.stop()
if (verbosityLevel >= 0)
log.info("Fetching artifacts: done")
log.info(
s"Fetched artifacts of ${projectDescription(currentProject)}" +
(if (sbtClassifiers) " (sbt classifiers)" else "")
)
val artifactFiles = artifactFilesOrErrors.collect {
case (artifact, \/-(file)) =>
@ -602,11 +684,10 @@ object Tasks {
)
}
resolutionsCache.getOrElseUpdate(
CacheKey(
reportsCache.getOrElseUpdate(
ReportCacheKey(
currentProject,
repositories,
startRes.copy(filter = None),
res,
withClassifiers,
sbtClassifiers
),
@ -615,4 +696,56 @@ object Tasks {
}
}
def coursierDependencyTreeTask(
inverse: Boolean,
sbtClassifiers: Boolean = false,
ignoreArtifactErrors: Boolean = false
) = Def.task {
val currentProject =
if (sbtClassifiers) {
val cm = coursierSbtClassifiersModule.value
val sv = scalaVersion.value
val sbv = scalaBinaryVersion.value
FromSbt.project(
cm.id,
cm.modules,
cm.configurations.map(cfg => cfg.name -> cfg.extendsConfigs.map(_.name)).toMap,
sv,
sbv
)
} else {
val proj = coursierProject.value
val publications = coursierPublications.value
proj.copy(publications = publications)
}
val res = {
if (sbtClassifiers)
coursierSbtClassifiersResolution
else
coursierResolution
}.value
val config = classpathConfiguration.value.name
val configs = coursierConfigurations.value
val includedConfigs = configs.getOrElse(config, Set.empty) + config
val dependencies0 = currentProject.dependencies.collect {
case (cfg, dep) if includedConfigs(cfg) => dep
}.sortBy { dep =>
(dep.module.organization, dep.module.name, dep.version)
}
val subRes = res.subset(dependencies0.toSet)
// use sbt logging?
println(
projectDescription(currentProject) + "\n" +
Print.dependencyTree(dependencies0, subRes, printExclusions = true, inverse)
)
}
}