diff --git a/cli/src/main/scala-2.11/coursier/cli/Helper.scala b/cli/src/main/scala-2.11/coursier/cli/Helper.scala index eb31ec8fb..c81650ae7 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Helper.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Helper.scala @@ -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 diff --git a/cli/src/main/scala-2.11/coursier/cli/Options.scala b/cli/src/main/scala-2.11/coursier/cli/Options.scala index 9cb1b08c3..a0d635795 100644 --- a/cli/src/main/scala-2.11/coursier/cli/Options.scala +++ b/cli/src/main/scala-2.11/coursier/cli/Options.scala @@ -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 ) { diff --git a/core/shared/src/main/scala/coursier/core/Resolution.scala b/core/shared/src/main/scala/coursier/core/Resolution.scala index eaca558d5..1762db013 100644 --- a/core/shared/src/main/scala/coursier/core/Resolution.scala +++ b/core/shared/src/main/scala/coursier/core/Resolution.scala @@ -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 diff --git a/core/shared/src/main/scala/coursier/util/Print.scala b/core/shared/src/main/scala/coursier/util/Print.scala index bdd380b06..9dbb5b52f 100644 --- a/core/shared/src/main/scala/coursier/util/Print.scala +++ b/core/shared/src/main/scala/coursier/util/Print.scala @@ -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) + } + } diff --git a/core/shared/src/main/scala/coursier/util/Tree.scala b/core/shared/src/main/scala/coursier/util/Tree.scala new file mode 100644 index 000000000..a2a985641 --- /dev/null +++ b/core/shared/src/main/scala/coursier/util/Tree.scala @@ -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") + } + +} diff --git a/plugin/src/main/scala-2.10/coursier/CoursierPlugin.scala b/plugin/src/main/scala-2.10/coursier/CoursierPlugin.scala index 436f20171..16db767ba 100644 --- a/plugin/src/main/scala-2.10/coursier/CoursierPlugin.scala +++ b/plugin/src/main/scala-2.10/coursier/CoursierPlugin.scala @@ -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) } diff --git a/plugin/src/main/scala-2.10/coursier/Keys.scala b/plugin/src/main/scala-2.10/coursier/Keys.scala index 796031c87..e22dcfdf6 100644 --- a/plugin/src/main/scala-2.10/coursier/Keys.scala +++ b/plugin/src/main/scala-2.10/coursier/Keys.scala @@ -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)" + ) } diff --git a/plugin/src/main/scala-2.10/coursier/Tasks.scala b/plugin/src/main/scala-2.10/coursier/Tasks.scala index 4b6809ed2..021e46568 100644 --- a/plugin/src/main/scala-2.10/coursier/Tasks.scala +++ b/plugin/src/main/scala-2.10/coursier/Tasks.scala @@ -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 ++= """""" - 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 ++= """""" + 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) + ) + } + }