diff --git a/cli/src/main/scala/coursier/cli/Coursier.scala b/cli/src/main/scala/coursier/cli/Coursier.scala index a8bc40a0e..23f7cb2de 100644 --- a/cli/src/main/scala/coursier/cli/Coursier.scala +++ b/cli/src/main/scala/coursier/cli/Coursier.scala @@ -13,7 +13,7 @@ import scalaz.{-\/, \/-} case class Coursier(scope: List[String], keepOptional: Boolean, fetch: Boolean, - @ExtraName("N") maxIterations: Int) extends App { + @ExtraName("N") maxIterations: Int = 100) extends App { val scopes0 = if (scope.isEmpty) List(Scope.Compile, Scope.Runtime) @@ -85,12 +85,12 @@ case class Coursier(scope: List[String], Dependency(mod, ver, scope = Scope.Runtime) } - val res = resolve( + val startRes = Resolution( deps.toSet, - fetchFrom(repositories), - maxIterations = Some(maxIterations).filter(_ > 0), filter = Some(dep => (keepOptional || !dep.optional) && scopes(dep.scope)) - ).run + ) + + val res = startRes.last(fetchFrom(repositories), maxIterations).run if (!res.isDone) { Console.err.println(s"Maximum number of iteration reached!") @@ -107,7 +107,7 @@ case class Coursier(scope: List[String], s"${dep.module.organization}:${dep.module.name}:${dep.artifact.`type`}:${Some(dep.artifact.classifier).filter(_.nonEmpty).map(_+":").mkString}$version$extra" } - val trDeps = res.dependencies.toList.sortBy(repr) + val trDeps = res.minDependencies.toList.sortBy(repr) println("\n" + trDeps.map(repr).distinct.mkString("\n")) @@ -129,7 +129,7 @@ case class Coursier(scope: List[String], val cachePolicy: CachePolicy = CachePolicy.Default - val m = res.dependencies.groupBy(dep => res.projectsCache.get(dep.moduleVersion).map(_._1)) + val m = res.minDependencies.groupBy(dep => res.projectsCache.get(dep.moduleVersion).map(_._1)) val (notFound, remaining0) = m.partition(_._1.isEmpty) if (notFound.nonEmpty) { val notFound0 = notFound.values.flatten.toList.map(repr).sorted diff --git a/core/src/main/scala/coursier/core/Resolution.scala b/core/src/main/scala/coursier/core/Resolution.scala index 33c574d6d..f03f5ad67 100644 --- a/core/src/main/scala/coursier/core/Resolution.scala +++ b/core/src/main/scala/coursier/core/Resolution.scala @@ -312,52 +312,19 @@ object Resolution { def defaultFilter(dep: Dependency): Boolean = !dep.optional && dep.scope == Scope.Compile - /** - * Get all the transitive dependencies of `dependencies`, solving any dependency version mismatch. - * - * Iteratively fetches the missing info of the current dependencies / add newly discovered dependencies - * to the current ones. The maximum number of such iterations can be bounded with `maxIterations`. - * - * ... - * - */ - def resolve(dependencies: Set[Dependency], - fetch: ModuleVersion => EitherT[Task, List[String], (Repository, Project)], - maxIterations: Option[Int], - filter: Option[Dependency => Boolean], - profileActivation: Option[(String, Activation, Map[String, String]) => Boolean]): Task[Resolution] = { - - val dependencies0 = dependencies.map(withDefaultScope) - - val startResolution = Resolution( - dependencies0, Set.empty, Set.empty, - Map.empty, Map.empty, - filter, - profileActivation - ) - - def helper(resolution: Resolution, remainingIter: Option[Int]): Task[(Resolution, Option[Int])] = { - if (resolution.isDone || remainingIter.exists(_ <= 0)) - Task.now((resolution, remainingIter)) - else - resolution.next(fetch).flatMap(helper(_, remainingIter.map(_ - 1))) - } - - helper(startResolution, maxIterations).map(_._1) - } } - /** - * State of a dependency resolution. - * - * Done if method `isDone` returns `true`. - * - * @param dependencies: current set of dependencies - * @param conflicts: conflicting dependencies - * @param projectsCache: cache of known projects - * @param errors: keeps track of the modules whose project definition could not be found - */ +/** + * State of a dependency resolution. + * + * Done if method `isDone` returns `true`. + * + * @param dependencies: current set of dependencies + * @param conflicts: conflicting dependencies + * @param projectsCache: cache of known projects + * @param errors: keeps track of the modules whose project definition could not be found + */ case class Resolution(rootDependencies: Set[Dependency], dependencies: Set[Dependency], conflicts: Set[Dependency], @@ -382,10 +349,9 @@ case class Resolution(rootDependencies: Set[Dependency], * No attempt is made to solve version conflicts here. */ def transitiveDependencies: Seq[Dependency] = - for { - dep <- (dependencies -- conflicts).toList - trDep <- finalDependencies0(dep) - } yield trDep + (dependencies -- conflicts) + .toList + .flatMap(finalDependencies0) /** * The "next" dependency set, made of the current dependencies and their transitive dependencies, @@ -397,7 +363,7 @@ case class Resolution(rootDependencies: Set[Dependency], * Returns a tuple made of the conflicting dependencies, and all the dependencies. */ def nextDependenciesAndConflicts: (Seq[Dependency], Seq[Dependency]) = { - merge(rootDependencies ++ dependencies ++ transitiveDependencies) + merge(rootDependencies.map(withDefaultScope) ++ dependencies ++ transitiveDependencies) } /** @@ -456,7 +422,7 @@ case class Resolution(rootDependencies: Set[Dependency], * The versions of all the dependencies returned are erased (emptied). */ def remainingDependencies: Set[Dependency] = { - val rootDependencies0 = rootDependencies.map(eraseVersion) + val rootDependencies0 = rootDependencies.map(withDefaultScope).map(eraseVersion) @tailrec def helper(reverseDeps: Map[Dependency, Vector[Dependency]]): Map[Dependency, Vector[Dependency]] = { @@ -632,4 +598,14 @@ case class Resolution(rootDependencies: Set[Dependency], } } + def last(fetchModule: ModuleVersion => EitherT[Task, List[String], (Repository, Project)], maxIterations: Int = -1): Task[Resolution] = { + if (maxIterations == 0 || isDone) Task.now(this) + else { + next(fetchModule) + .flatMap(_.last(fetchModule, if (maxIterations > 0) maxIterations - 1 else maxIterations)) + } + } + + def minDependencies: Set[Dependency] = + Orders.minDependencies(dependencies) } diff --git a/core/src/main/scala/coursier/package.scala b/core/src/main/scala/coursier/package.scala index 5a9a0d079..476b730ee 100644 --- a/core/src/main/scala/coursier/package.scala +++ b/core/src/main/scala/coursier/package.scala @@ -88,6 +88,14 @@ package object coursier { maxIterations: Option[Int] = Some(200), filter: Option[Dependency => Boolean] = None, profileActivation: Option[(String, Profile.Activation, Map[String, String]) => Boolean] = None): Task[Resolution] = { - core.Resolution.resolve(dependencies, fetch, maxIterations, filter, profileActivation) + + val startResolution = Resolution( + dependencies, Set.empty, Set.empty, + Map.empty, Map.empty, + filter, + profileActivation + ) + + startResolution.last(fetch, maxIterations.getOrElse(-1)) } } diff --git a/core/src/test/scala/coursier/test/CentralTests.scala b/core/src/test/scala/coursier/test/CentralTests.scala index 9e7683f82..693769100 100644 --- a/core/src/test/scala/coursier/test/CentralTests.scala +++ b/core/src/test/scala/coursier/test/CentralTests.scala @@ -38,7 +38,7 @@ object CentralTests extends TestSuite { .copy(projectsCache = Map.empty, errors = Map.empty) // No validating these here val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set( dep.withCompileScope, Dependency(Module("ch.qos.logback", "logback-core"), "1.1.3").withCompileScope, @@ -54,7 +54,7 @@ object CentralTests extends TestSuite { .copy(projectsCache = Map.empty, errors = Map.empty) // No validating these here val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set( dep.withCompileScope, Dependency(Module("org.ow2.asm", "asm-tree"), "5.0.2").withCompileScope, @@ -70,7 +70,7 @@ object CentralTests extends TestSuite { val res = res0.copy(projectsCache = Map.empty, errors = Map.empty) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set( dep.withCompileScope)) diff --git a/core/src/test/scala/coursier/test/ResolutionTests.scala b/core/src/test/scala/coursier/test/ResolutionTests.scala index 9e9b577b3..d10db84e5 100644 --- a/core/src/test/scala/coursier/test/ResolutionTests.scala +++ b/core/src/test/scala/coursier/test/ResolutionTests.scala @@ -166,7 +166,7 @@ object ResolutionTests extends TestSuite { ).runF) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope), errors = Map(dep.moduleVersion -> Seq("Not found")) ) @@ -183,7 +183,7 @@ object ResolutionTests extends TestSuite { ).runF) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope), projectsCache = Map(dep.moduleVersion -> (testRepository, projectsMap(dep.moduleVersion))) ) @@ -201,7 +201,7 @@ object ResolutionTests extends TestSuite { ).runF) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope, trDep.withCompileScope), projectsCache = Map( projectsMap(dep.moduleVersion).kv, @@ -225,7 +225,7 @@ object ResolutionTests extends TestSuite { ).runF) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope), projectsCache = Map( projectsMap(dep.moduleVersion).kv @@ -250,7 +250,7 @@ object ResolutionTests extends TestSuite { ).runF) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope), projectsCache = Map( projectsMap(dep.moduleVersion).kv @@ -275,7 +275,7 @@ object ResolutionTests extends TestSuite { ).runF) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope), projectsCache = Map( projectsMap(dep.moduleVersion).kv @@ -295,7 +295,7 @@ object ResolutionTests extends TestSuite { ).runF).copy(filter = None) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope), projectsCache = Map( projectsMap(dep.moduleVersion).kv @@ -319,7 +319,7 @@ object ResolutionTests extends TestSuite { ).runF).copy(filter = None, projectsCache = Map.empty) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope) ) @@ -339,7 +339,7 @@ object ResolutionTests extends TestSuite { ).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope) ) @@ -358,7 +358,7 @@ object ResolutionTests extends TestSuite { ).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope) ) @@ -375,7 +375,7 @@ object ResolutionTests extends TestSuite { ).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope) ) @@ -394,7 +394,7 @@ object ResolutionTests extends TestSuite { ).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope) ) @@ -415,7 +415,7 @@ object ResolutionTests extends TestSuite { ).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope) ) @@ -438,7 +438,7 @@ object ResolutionTests extends TestSuite { ).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope) ) @@ -460,7 +460,7 @@ object ResolutionTests extends TestSuite { ).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty) val expected = Resolution( - rootDependencies = Set(dep.withCompileScope), + rootDependencies = Set(dep), dependencies = Set(dep.withCompileScope) ++ trDeps.map(_.withCompileScope) ) @@ -484,7 +484,7 @@ object ResolutionTests extends TestSuite { ).runF).copy(filter = None, projectsCache = Map.empty, errors = Map.empty) val expected = Resolution( - rootDependencies = deps.map(_.withCompileScope), + rootDependencies = deps, dependencies = (deps ++ trDeps).map(_.withCompileScope) ) diff --git a/core/src/test/scala/coursier/test/package.scala b/core/src/test/scala/coursier/test/package.scala index 863eb2519..af3c811e8 100644 --- a/core/src/test/scala/coursier/test/package.scala +++ b/core/src/test/scala/coursier/test/package.scala @@ -1,9 +1,26 @@ package coursier +import scalaz.EitherT +import scalaz.concurrent.Task + package object test { implicit class DependencyOps(val underlying: Dependency) extends AnyVal { def withCompileScope: Dependency = underlying.copy(scope = Scope.Compile) } + def resolve(dependencies: Set[Dependency], + fetch: ModuleVersion => EitherT[Task, List[String], (Repository, Project)], + maxIterations: Option[Int] = Some(200), + filter: Option[Dependency => Boolean] = None, + profileActivation: Option[(String, Profile.Activation, Map[String, String]) => Boolean] = None): Task[Resolution] = { + + val startResolution = Resolution( + dependencies, + filter = filter, + profileActivation = profileActivation + ) + + startResolution.last(fetch, maxIterations.getOrElse(-1)) + } } diff --git a/web/src/main/scala/coursier/web/Backend.scala b/web/src/main/scala/coursier/web/Backend.scala index c37f491e0..317ec5efe 100644 --- a/web/src/main/scala/coursier/web/Backend.scala +++ b/web/src/main/scala/coursier/web/Backend.scala @@ -1,7 +1,7 @@ package coursier package web -import coursier.core.{Resolver, Logger, Remote} +import coursier.core.{Logger, Remote} import japgolly.scalajs.react.vdom.{TagMod, Attr} import japgolly.scalajs.react.vdom.Attrs.dangerouslySetInnerHtml import japgolly.scalajs.react.{ReactEventI, ReactComponentB, BackendScope} @@ -71,13 +71,15 @@ class Backend($: BackendScope[Unit, State]) { def updateTree(resolution: Resolution, target: String, reverse: Boolean) = { def depsOf(dep: Dependency) = - resolution.projectsCache.get(dep.moduleVersion).toSeq.flatMap(t => Resolver.finalDependencies(dep, t._2).filter(resolution.filter getOrElse Resolver.defaultFilter)) + resolution.projectsCache.get(dep.moduleVersion).toSeq.flatMap(t => core.Resolution.finalDependencies(dep, t._2).filter(resolution.filter getOrElse core.Resolution.defaultFilter)) + + val minDependencies = resolution.minDependencies lazy val reverseDeps = { var m = Map.empty[Module, Seq[Dependency]] for { - dep <- resolution.dependencies + dep <- minDependencies trDep <- depsOf(dep) } { m += trDep.module -> (m.getOrElse(trDep.module, Nil) :+ dep) @@ -95,8 +97,8 @@ class Backend($: BackendScope[Unit, State]) { else Seq("nodes" -> js.Array(deps.map(tree): _*)) }: _*) - println(resolution.dependencies.toList.map(tree).map(js.JSON.stringify(_))) - g.$(target).treeview(js.Dictionary("data" -> js.Array(resolution.dependencies.toList.map(tree): _*))) + println(minDependencies.toList.map(tree).map(js.JSON.stringify(_))) + g.$(target).treeview(js.Dictionary("data" -> js.Array(minDependencies.toList.map(tree): _*))) } def resolve(action: => Unit = ()) = { @@ -119,11 +121,14 @@ class Backend($: BackendScope[Unit, State]) { } val s = $.state - def task = coursier.resolve( - s.modules.toSet, - fetchFrom(s.repositories.map(_.copy(logger = Some(logger)))), - filter = Some(dep => (s.options.followOptional || !dep.optional) && (s.options.keepTest || dep.scope != Scope.Test)) - ) + def task = { + val res = coursier.Resolution( + s.modules.toSet, + filter = Some(dep => (s.options.followOptional || !dep.optional) && (s.options.keepTest || dep.scope != Scope.Test)) + ) + + res.last(fetchFrom(s.repositories.map(_.copy(logger = Some(logger)))), 100) + } // For reasons that are unclear to me, not delaying this when using the runNow execution context // somehow discards the $.modState above. (Not a major problem as queue is used by default.) @@ -258,7 +263,7 @@ object App { ) } - val sortedDeps = res.dependencies.toList + val sortedDeps = res.minDependencies.toList .sortBy(dep => coursier.core.Module.unapply(dep.module).get) <.table(^.`class` := "table",