From 98acae19bc2846eec4b9a2febdbc9c26239045aa Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sun, 29 Nov 2015 20:54:19 +0100 Subject: [PATCH 1/2] Allow to force some module versions during resolution What SBT calls "dependency overrides" --- .../main/scala/coursier/core/Resolution.scala | 41 ++++--- .../src/main/scala/coursier/package.scala | 2 + .../scala/coursier/test/ResolutionTests.scala | 106 +++++++++++++++++- 3 files changed, 130 insertions(+), 19 deletions(-) diff --git a/core/shared/src/main/scala/coursier/core/Resolution.scala b/core/shared/src/main/scala/coursier/core/Resolution.scala index 3b19714eb..fa4203bdd 100644 --- a/core/shared/src/main/scala/coursier/core/Resolution.scala +++ b/core/shared/src/main/scala/coursier/core/Resolution.scala @@ -163,25 +163,34 @@ object Resolution { * Returns the conflicted dependencies, and the merged others. */ def merge( - dependencies: TraversableOnce[Dependency] + dependencies: TraversableOnce[Dependency], + forceVersions: Map[Module, String] ): (Seq[Dependency], Seq[Dependency]) = { val mergedByModVer = dependencies .toList .groupBy(dep => dep.module) - .mapValues { deps => - if (deps.lengthCompare(1) == 0) \/-(deps) - else { - val versions = deps - .map(_.version) - .distinct - val versionOpt = mergeVersions(versions) - - versionOpt match { - case Some(version) => - \/-(deps.map(dep => dep.copy(version = version))) + .map { case (module, deps) => + module -> { + forceVersions.get(module) match { case None => - -\/(deps) + if (deps.lengthCompare(1) == 0) \/-(deps) + else { + val versions = deps + .map(_.version) + .distinct + val versionOpt = mergeVersions(versions) + + versionOpt match { + case Some(version) => + \/-(deps.map(dep => dep.copy(version = version))) + case None => + -\/(deps) + } + } + + case Some(forcedVersion) => + \/-(deps.map(dep => dep.copy(version = forcedVersion))) } } } @@ -380,6 +389,7 @@ object Resolution { case class Resolution( rootDependencies: Set[Dependency], dependencies: Set[Dependency], + forceVersions: Map[Module, String], conflicts: Set[Dependency], projectCache: Map[Resolution.ModuleVersion, (Artifact.Source, Project)], errorCache: Map[Resolution.ModuleVersion, Seq[String]], @@ -426,9 +436,10 @@ case class Resolution( * the dependencies. */ def nextDependenciesAndConflicts: (Seq[Dependency], Seq[Dependency]) = + // TODO Provide the modules whose version was forced by dependency overrides too merge( - rootDependencies.map(withDefaultScope) ++ dependencies ++ - transitiveDependencies + rootDependencies.map(withDefaultScope) ++ dependencies ++ transitiveDependencies, + forceVersions ) /** diff --git a/core/shared/src/main/scala/coursier/package.scala b/core/shared/src/main/scala/coursier/package.scala index d060d0b4b..6803f43ed 100644 --- a/core/shared/src/main/scala/coursier/package.scala +++ b/core/shared/src/main/scala/coursier/package.scala @@ -63,6 +63,7 @@ package object coursier { def apply( rootDependencies: Set[Dependency] = Set.empty, dependencies: Set[Dependency] = Set.empty, + forceVersions: Map[Module, String] = Map.empty, conflicts: Set[Dependency] = Set.empty, projectCache: Map[ModuleVersion, (Artifact.Source, Project)] = Map.empty, errorCache: Map[ModuleVersion, Seq[String]] = Map.empty, @@ -72,6 +73,7 @@ package object coursier { core.Resolution( rootDependencies, dependencies, + forceVersions, conflicts, projectCache, errorCache, diff --git a/tests/shared/src/test/scala/coursier/test/ResolutionTests.scala b/tests/shared/src/test/scala/coursier/test/ResolutionTests.scala index 5b17d8e1e..fd9d0ea4d 100644 --- a/tests/shared/src/test/scala/coursier/test/ResolutionTests.scala +++ b/tests/shared/src/test/scala/coursier/test/ResolutionTests.scala @@ -9,12 +9,15 @@ import coursier.test.compatibility._ object ResolutionTests extends TestSuite { - def resolve0(deps: Set[Dependency], filter: Option[Dependency => Boolean] = None) = { - Resolution(deps, filter = filter) + def resolve0( + deps: Set[Dependency], + filter: Option[Dependency => Boolean] = None, + forceVersions: Map[Module, String] = Map.empty + ) = + Resolution(deps, filter = filter, forceVersions = forceVersions) .process .run(Fetch.default(repositories)) .runF - } implicit class ProjectOps(val p: Project) extends AnyVal { def kv: (ModuleVersion, (Artifact.Source, Project)) = p.moduleVersion -> (testRepository.source, p) @@ -134,9 +137,16 @@ object ResolutionTests extends TestSuite { Project(Module("an-org", "a-name"), "1.0"), + Project(Module("an-org", "a-name"), "1.2"), + Project(Module("an-org", "a-lib"), "1.0", Seq(Dependency(Module("an-org", "a-name"), "1.0"))), + Project(Module("an-org", "a-lib"), "1.1"), + + Project(Module("an-org", "a-lib"), "1.2", + Seq(Dependency(Module("an-org", "a-name"), "1.2"))), + Project(Module("an-org", "another-lib"), "1.0", Seq(Dependency(Module("an-org", "a-name"), "1.0"))), @@ -144,7 +154,15 @@ object ResolutionTests extends TestSuite { Project(Module("an-org", "an-app"), "1.0", Seq( Dependency(Module("an-org", "a-lib"), "1.0", exclusions = Set(("an-org", "a-name"))), - Dependency(Module("an-org", "another-lib"), "1.0", optional = true))) + Dependency(Module("an-org", "another-lib"), "1.0", optional = true))), + + Project(Module("an-org", "an-app"), "1.1", + Seq( + Dependency(Module("an-org", "a-lib"), "1.1"))), + + Project(Module("an-org", "an-app"), "1.2", + Seq( + Dependency(Module("an-org", "a-lib"), "1.2"))) ) val projectsMap = projects.map(p => p.moduleVersion -> p).toMap @@ -483,6 +501,86 @@ object ResolutionTests extends TestSuite { } } + 'dependencyOverrides - { + * - { + async { + val deps = Set( + Dependency(Module("an-org", "a-name"), "1.1")) + val depOverrides = Map( + Module("an-org", "a-name") -> "1.0") + + val res = await(resolve0( + deps, + forceVersions = depOverrides, + filter = Some(_.scope == Scope.Compile) + )).copy(filter = None, projectCache = Map.empty, errorCache = Map.empty) + + val expected = Resolution( + rootDependencies = deps, + dependencies = Set( + Dependency(Module("an-org", "a-name"), "1.0") + ).map(_.withCompileScope), + forceVersions = depOverrides + ) + + assert(res == expected) + } + } + + * - { + async { + val deps = Set( + Dependency(Module("an-org", "an-app"), "1.1")) + val depOverrides = Map( + Module("an-org", "a-lib") -> "1.0") + + val res = await(resolve0( + deps, + forceVersions = depOverrides, + filter = Some(_.scope == Scope.Compile) + )).copy(filter = None, projectCache = Map.empty, errorCache = Map.empty) + + val expected = Resolution( + rootDependencies = deps, + dependencies = Set( + Dependency(Module("an-org", "an-app"), "1.1"), + Dependency(Module("an-org", "a-lib"), "1.0"), + Dependency(Module("an-org", "a-name"), "1.0") + ).map(_.withCompileScope), + forceVersions = depOverrides + ) + + assert(res == expected) + } + } + + * - { + async { + val deps = Set( + Dependency(Module("an-org", "an-app"), "1.2")) + val depOverrides = Map( + Module("an-org", "a-lib") -> "1.1") + + val res = await(resolve0( + deps, + forceVersions = depOverrides, + filter = Some(_.scope == Scope.Compile) + )).copy(filter = None, projectCache = Map.empty, errorCache = Map.empty) + + val expected = Resolution( + rootDependencies = deps, + dependencies = Set( + Dependency(Module("an-org", "an-app"), "1.2"), + Dependency(Module("an-org", "a-lib"), "1.1") + ).map(_.withCompileScope), + forceVersions = depOverrides + ) + + assert(res == expected) + } + } + } + 'parts{ 'propertySubstitution{ val res = From a2bcc355b716490c5b9dbdc1c42c17b9036ca404 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Sun, 29 Nov 2015 20:54:23 +0100 Subject: [PATCH 2/2] Allow to force module versions from CLI Fixes https://github.com/alexarchambault/coursier/issues/17 --- .../main/scala/coursier/cli/Coursier.scala | 4 ++ cli/src/main/scala/coursier/cli/Helper.scala | 53 ++++++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/cli/src/main/scala/coursier/cli/Coursier.scala b/cli/src/main/scala/coursier/cli/Coursier.scala index 190e87442..9dd707773 100644 --- a/cli/src/main/scala/coursier/cli/Coursier.scala +++ b/cli/src/main/scala/coursier/cli/Coursier.scala @@ -30,6 +30,10 @@ case class CommonOptions( @HelpMessage("Repositories - for multiple repositories, separate with comma and/or repeat this option (e.g. -r central,ivy2local -r sonatype-snapshots, or equivalently -r central,ivy2local,sonatype-snapshots)") @ExtraName("r") repository: List[String], + @HelpMessage("Force module version") + @ValueDescription("organization:name:forcedVersion") + @ExtraName("V") + forceVersion: List[String], @HelpMessage("Maximum number of parallel downloads (default: 6)") @ExtraName("n") parallel: Int = 6, diff --git a/cli/src/main/scala/coursier/cli/Helper.scala b/cli/src/main/scala/coursier/cli/Helper.scala index 8f303cc25..cb7a53741 100644 --- a/cli/src/main/scala/coursier/cli/Helper.scala +++ b/cli/src/main/scala/coursier/cli/Helper.scala @@ -117,7 +117,7 @@ class Helper( notFoundRepositoryIds.mkString(", ") ) - sys.exit(1) + sys.exit(255) } val files = cache.files().copy(concurrentDownloadCount = parallel) @@ -139,15 +139,30 @@ class Helper( .map(_.split(":", 3).toSeq) .partition(_.length == 3) + val (splitForceVersions, malformedForceVersions) = forceVersion + .map(_.split(":", 3).toSeq) + .partition(_.length == 3) + if (splitDependencies.isEmpty) { Console.err.println(s"Error: no dependencies specified.") // CaseApp.printUsage[Coursier]() sys exit 1 } - if (malformed.nonEmpty) { - errPrintln(s"Malformed dependencies:\n${malformed.map(_.mkString(":")).mkString("\n")}") - sys exit 1 + if (malformed.nonEmpty || malformedForceVersions.nonEmpty) { + if (malformed.nonEmpty) { + errPrintln("Malformed dependency(ies), should be like org:name:version") + for (s <- malformed) + errPrintln(s" ${s.mkString(":")}") + } + + if (malformedForceVersions.nonEmpty) { + errPrintln("Malformed force version(s), should be like org:name:forcedVersion") + for (s <- malformedForceVersions) + errPrintln(s" ${s.mkString(":")}") + } + + sys.exit(1) } val moduleVersions = splitDependencies.map{ @@ -159,8 +174,24 @@ class Helper( Dependency(mod, ver, scope = Scope.Runtime) } + val forceVersions = { + val forceVersions0 = splitForceVersions.map { + case Seq(org, name, version) => (Module(org, name), version) + } + + val grouped = forceVersions0 + .groupBy { case (mod, _) => mod } + .map { case (mod, l) => mod -> l.map { case (_, version) => version } } + + for ((mod, forcedVersions) <- grouped if forcedVersions.distinct.lengthCompare(1) > 0) + errPrintln(s"Warning: version of $mod forced several times, using only the last one (${forcedVersions.last})") + + grouped.map { case (mod, versions) => mod -> versions.last } + } + val startRes = Resolution( deps.toSet, + forceVersions = forceVersions, filter = Some(dep => keepOptional || !dep.optional) ) @@ -182,8 +213,18 @@ class Helper( print.flatMap(_ => fetchQuiet(modVers)) } - if (verbose0 >= 0) - errPrintln(s"Resolving\n" + moduleVersions.map{case (mod, ver) => s" $mod:$ver"}.mkString("\n")) + if (verbose0 >= 0) { + errPrintln("Dependencies:") + for ((mod, ver) <- moduleVersions) + errPrintln(s" $mod:$ver") + + if (forceVersions.nonEmpty) { + errPrintln("Force versions:") + for ((mod, ver) <- forceVersions.toVector.sortBy { case (mod, _) => mod.toString }) + errPrintln(s" $mod:$ver") + } + } + val res = startRes .process