Skip to content

Commit

Permalink
Implement Reconciliation.Relaxed
Browse files Browse the repository at this point in the history
Fixes coursier#1284

This implements an alternative reconciliation rule that first tries the basic reconciliation, and if it fails falls back to using the max values of the specific versions and lower bounds of the intervals.
  • Loading branch information
eed3si9n committed Aug 1, 2019
1 parent 54da18c commit d90e63a
Show file tree
Hide file tree
Showing 18 changed files with 169 additions and 13 deletions.
Expand Up @@ -5,7 +5,7 @@ import cats.data.{Validated, ValidatedNel}
import cats.implicits._
import coursier.core._
import coursier.params.ResolutionParams
import coursier.parse.{DependencyParser, ModuleParser, RuleParser}
import coursier.parse.{DependencyParser, ModuleParser, ReconciliationParser, RuleParser}

final case class ResolutionOptions(

Expand Down Expand Up @@ -47,6 +47,10 @@ final case class ResolutionOptions(
@Short("rule")
rules: List[String] = Nil,

@Help("Choose reconciliation strategy")
@Value("organization:name:(basic|relaxed)")
reconciliation: List[String] = Nil,

strict: Option[Boolean] = None,

strictInclude: List[String] = Nil,
Expand Down Expand Up @@ -126,8 +130,14 @@ final case class ResolutionOptions(
}
.map(_.flatten)

(maxIterationsV, forceVersionV, extraPropertiesV, forcedPropertiesV, rulesV).mapN {
(maxIterations, forceVersion, extraProperties, forcedProperties, rules) =>
val reconciliationV =
ReconciliationParser.reconciliation(reconciliation, scalaVersionOrDefault) match {
case Left(e) => Validated.invalidNel(e.mkString("\n"))
case Right(elems) => Validated.validNel(elems)
}

(maxIterationsV, forceVersionV, extraPropertiesV, forcedPropertiesV, rulesV, reconciliationV).mapN {
(maxIterations, forceVersion, extraProperties, forcedProperties, rules, reconciliation) =>
ResolutionParams()
.withKeepOptionalDependencies(keepOptional)
.withMaxIterations(maxIterations)
Expand All @@ -139,6 +149,7 @@ final case class ResolutionOptions(
.withForceScalaVersion(forceScalaVersion)
.withTypelevel(typelevel)
.withRules(rules)
.withReconciliation(reconciliation)
.withDefaultConfiguration(Configuration(defaultConfiguration))
}
}
Expand Down
Expand Up @@ -98,5 +98,21 @@ object Reconciliation {
VersionConstraint.merge(constraints: _*)
}

/**
* Implements the relaxed reconciliation.
*/
abstract class RelaxedReconciliation extends AbstractReconciliation {
override protected def mergeVersionConstraints(constraints: Seq[VersionConstraint]): Option[VersionConstraint] =
VersionConstraint.relaxedMerge(constraints: _*)
}

/**
* Implements the basic reconciliation rule based on `VersionConstraint.merge`.
*/
case object Basic extends BasicReconciliation

/**
* Implements the relaxed reconciliation.
*/
case object Relaxed extends RelaxedReconciliation
}
25 changes: 21 additions & 4 deletions modules/core/shared/src/main/scala/coursier/core/Resolution.scala
Expand Up @@ -217,10 +217,14 @@ object Resolution {
*/
def merge(
dependencies: TraversableOnce[Dependency],
forceVersions: Map[Module, String]
forceVersions: Map[Module, String],
reconciliation: Option[Module => Reconciliation]
): (Seq[Dependency], Seq[Dependency], Map[Module, String]) = {
def reconcilerByMod(mod: Module): Reconciliation = Reconciliation.Basic

def reconcilerByMod(mod: Module): Reconciliation =
reconciliation match {
case Some(f) => f(mod)
case _ => Reconciliation.Basic
}
val mergedByModVer = dependencies
.toVector
.groupBy(dep => dep.module)
Expand Down Expand Up @@ -604,6 +608,7 @@ object Resolution {
errorCache: Map[Resolution.ModuleVersion, Seq[String]],
finalDependenciesCache: Map[Dependency, Seq[Dependency]],
filter: Option[Dependency => Boolean],
reconciliation: Option[Module => Reconciliation],
osInfo: Activation.Os,
jdkVersion: Option[Version],
userActivations: Option[Map[String, Boolean]],
Expand All @@ -621,6 +626,7 @@ object Resolution {
errorCache,
finalDependenciesCache,
filter,
reconciliation,
osInfo,
jdkVersion,
userActivations,
Expand Down Expand Up @@ -655,6 +661,7 @@ object Resolution {
errorCache,
finalDependenciesCache,
filter,
None,
osInfo,
jdkVersion,
userActivations,
Expand All @@ -674,6 +681,7 @@ object Resolution {
Map.empty,
Map.empty,
None,
None,
Activation.Os.empty,
None,
None,
Expand Down Expand Up @@ -720,6 +728,7 @@ final class Resolution private (
val errorCache: Map[Resolution.ModuleVersion, Seq[String]],
val finalDependenciesCache: Map[Dependency, Seq[Dependency]],
val filter: Option[Dependency => Boolean],
val reconciliation: Option[Module => Reconciliation],
val osInfo: Activation.Os,
val jdkVersion: Option[Version],
val userActivations: Option[Map[String, Boolean]],
Expand All @@ -743,6 +752,7 @@ final class Resolution private (
errorCache == other.errorCache &&
finalDependenciesCache == other.finalDependenciesCache &&
filter == other.filter &&
reconciliation == other.reconciliation &&
osInfo == other.osInfo &&
jdkVersion == other.jdkVersion &&
userActivations == other.userActivations &&
Expand All @@ -763,6 +773,7 @@ final class Resolution private (
code = 37 * code + errorCache.##
code = 37 * code + finalDependenciesCache.##
code = 37 * code + filter.##
code = 37 * code + reconciliation.##
code = 37 * code + osInfo.##
code = 37 * code + jdkVersion.##
code = 37 * code + userActivations.##
Expand All @@ -783,6 +794,7 @@ final class Resolution private (
b ++= errorCache.toString; b ++= ", "
b ++= finalDependenciesCache.toString; b ++= ", "
b ++= filter.toString; b ++= ", "
b ++= reconciliation.toString; b ++= ", "
b ++= osInfo.toString; b ++= ", "
b ++= jdkVersion.toString; b ++= ", "
b ++= userActivations.toString; b ++= ", "
Expand All @@ -803,6 +815,7 @@ final class Resolution private (
errorCache: Map[Resolution.ModuleVersion, Seq[String]] = errorCache,
finalDependenciesCache: Map[Dependency, Seq[Dependency]] = finalDependenciesCache,
filter: Option[Dependency => Boolean] = filter,
reconciliation: Option[Module => Reconciliation] = reconciliation,
osInfo: Activation.Os = osInfo,
jdkVersion: Option[Version] = jdkVersion,
userActivations: Option[Map[String, Boolean]] = userActivations,
Expand All @@ -820,6 +833,7 @@ final class Resolution private (
errorCache,
finalDependenciesCache,
filter,
reconciliation,
osInfo,
jdkVersion,
userActivations,
Expand All @@ -839,6 +853,7 @@ final class Resolution private (
errorCache: Map[Resolution.ModuleVersion, Seq[String]] = errorCache,
finalDependenciesCache: Map[Dependency, Seq[Dependency]] = finalDependenciesCache,
filter: Option[Dependency => Boolean] = filter,
reconciliation: Option[Module => Reconciliation] = reconciliation,
osInfo: Activation.Os = osInfo,
jdkVersion: Option[Version] = jdkVersion,
userActivations: Option[Map[String, Boolean]] = userActivations,
Expand All @@ -855,6 +870,7 @@ final class Resolution private (
errorCache,
finalDependenciesCache,
filter,
reconciliation,
osInfo,
jdkVersion,
userActivations,
Expand Down Expand Up @@ -1025,7 +1041,8 @@ final class Resolution private (
// TODO Provide the modules whose version was forced by dependency overrides too
merge(
rootDependencies.map(withDefaultConfig(_, defaultConfiguration)) ++ dependencySet.minimizedSet ++ transitiveDependencies,
forceVersions
forceVersions,
reconciliation
)

private def updatedRootDependencies =
Expand Down
Expand Up @@ -61,4 +61,21 @@ object VersionConstraint {

constraintOpt.filter(_.isValid)
}

def relaxedMerge(constraints: VersionConstraint*): Option[VersionConstraint] = {
merge(constraints: _*) orElse {
val nonZeroIntervals = constraints.flatMap(c =>
if (c.interval == VersionInterval.zero) Nil
else List(c.interval)
)
val lowerBounds = nonZeroIntervals.flatMap(c =>
if (c.fromIncluded) c.from.toList
else Nil
)
val preferreds = constraints.flatMap(_.preferred).toList
val vs = (lowerBounds ++ preferreds).distinct
if (vs.isEmpty) None
else Some(VersionConstraint.preferred(vs.max))
}
}
}
15 changes: 13 additions & 2 deletions modules/coursier/shared/src/main/scala/coursier/Resolve.scala
@@ -1,7 +1,7 @@
package coursier

import coursier.cache.{Cache, CacheLogger}
import coursier.core.{Activation, DependencySet, Exclusions}
import coursier.core.{Activation, DependencySet, Exclusions, Reconciliation}
import coursier.error.ResolutionError
import coursier.error.conflict.UnsatisfiedRule
import coursier.internal.Typelevel
Expand Down Expand Up @@ -306,6 +306,16 @@ object Resolve extends PlatformResolve {
l.reduceOption((f, g) => dep => f(g(dep)))
}

val reconciliation: Option[Module => Reconciliation] =
if (params.reconciliation.isEmpty) None
else
Some((m: Module) => {
params.reconciliation.find(_._1.matches(m)) match {
case Some((_, r)) => r
case _ => Reconciliation.Basic
}
})

coursier.core.Resolution(
rootDependencies = dependencies,
dependencySet = DependencySet.empty,
Expand All @@ -314,7 +324,8 @@ object Resolve extends PlatformResolve {
projectCache = Map.empty,
errorCache = Map.empty,
finalDependenciesCache = Map.empty,
filter = Some(dep => params.keepOptionalDependencies || !dep.optional),
filter = Some((dep: Dependency) => params.keepOptionalDependencies || !dep.optional),
reconciliation = reconciliation,
osInfo = params.osInfoOpt.getOrElse {
if (params.useSystemOsInfo)
// call from Sync[F].delay?
Expand Down
Expand Up @@ -5,19 +5,21 @@
// DO EDIT MANUALLY from now on
package coursier.params

import coursier.core.{Activation, Configuration, Module, ModuleName, Organization, Version}
import coursier.core.{Activation, Configuration, Module, ModuleName, Organization, Reconciliation, Version}
import coursier.params.rule.{Rule, RuleResolution}
import coursier.util.ModuleMatchers

final class ResolutionParams private (
val keepOptionalDependencies: Boolean,
val maxIterations: Int,
val forceVersion: Map[coursier.core.Module, String],
val forceVersion: Map[Module, String],
val forcedProperties: Map[String, String],
val profiles: Set[String],
val scalaVersion: Option[String],
val forceScalaVersion: Option[Boolean],
val typelevel: Boolean,
val rules: Seq[(Rule, RuleResolution)],
val reconciliation: Seq[(ModuleMatchers, Reconciliation)],
val properties: Seq[(String, String)],
val exclusions: Set[(Organization, ModuleName)],
val osInfoOpt: Option[Activation.Os],
Expand Down Expand Up @@ -49,6 +51,7 @@ final class ResolutionParams private (
forceScalaVersion,
typelevel,
rules,
Nil,
properties,
exclusions,
None,
Expand Down Expand Up @@ -122,6 +125,7 @@ final class ResolutionParams private (
forceScalaVersion == x.forceScalaVersion &&
typelevel == x.typelevel &&
rules == x.rules &&
reconciliation == x.reconciliation &&
properties == x.properties &&
exclusions == x.exclusions &&
osInfoOpt == x.osInfoOpt &&
Expand All @@ -142,6 +146,7 @@ final class ResolutionParams private (
code = 37 * (code + forceScalaVersion.##)
code = 37 * (code + typelevel.##)
code = 37 * (code + rules.##)
code = 37 * (code + reconciliation.##)
code = 37 * (code + properties.##)
code = 37 * (code + exclusions.##)
code = 37 * (code + osInfoOpt.##)
Expand All @@ -163,6 +168,7 @@ final class ResolutionParams private (
forceScalaVersion,
typelevel,
rules,
reconciliation,
properties,
exclusions,
osInfoOpt,
Expand All @@ -185,6 +191,7 @@ final class ResolutionParams private (
forceScalaVersion: Option[Boolean] = forceScalaVersion,
typelevel: Boolean = typelevel,
rules: Seq[(Rule, RuleResolution)] = rules,
reconciliation: Seq[(ModuleMatchers, Reconciliation)] = reconciliation,
properties: Seq[(String, String)] = properties,
exclusions: Set[(Organization, ModuleName)] = exclusions,
osInfoOpt: Option[Activation.Os] = osInfoOpt,
Expand All @@ -203,6 +210,7 @@ final class ResolutionParams private (
forceScalaVersion,
typelevel,
rules,
reconciliation,
properties,
exclusions,
osInfoOpt,
Expand Down Expand Up @@ -236,6 +244,8 @@ final class ResolutionParams private (
copy(typelevel = typelevel)
def withRules(rules: Seq[(Rule, RuleResolution)]): ResolutionParams =
copy(rules = rules)
def withReconciliation(reconciliation: Seq[(ModuleMatchers, Reconciliation)]): ResolutionParams =
copy(reconciliation = reconciliation)
def withExclusions(exclusions: Set[(Organization, ModuleName)]): ResolutionParams =
copy(exclusions = exclusions)
def withOsInfo(osInfo: Activation.Os): ResolutionParams =
Expand Down Expand Up @@ -423,6 +433,7 @@ object ResolutionParams {
forceScalaVersion: Option[Boolean],
typelevel: Boolean,
rules: Seq[(Rule, RuleResolution)],
reconciliation: Seq[(ModuleMatchers, Reconciliation)],
properties: Seq[(String, String)],
exclusions: Set[(Organization, ModuleName)],
osInfoOpt: Option[Activation.Os],
Expand All @@ -440,6 +451,7 @@ object ResolutionParams {
forceScalaVersion,
typelevel,
rules,
reconciliation,
properties,
exclusions,
osInfoOpt,
Expand All @@ -459,6 +471,7 @@ object ResolutionParams {
forceScalaVersion: Boolean,
typelevel: Boolean,
rules: Seq[(Rule, RuleResolution)],
reconciliation: Seq[(ModuleMatchers, Reconciliation)],
properties: Seq[(String, String)],
exclusions: Set[(Organization, ModuleName)],
osInfoOpt: Option[Activation.Os],
Expand All @@ -476,6 +489,7 @@ object ResolutionParams {
Option(forceScalaVersion),
typelevel,
rules,
reconciliation,
properties,
exclusions,
osInfoOpt,
Expand Down
@@ -0,0 +1,33 @@
package coursier.parse

import coursier.core.{Module, ModuleName, Organization, Reconciliation}
import coursier.util.{ModuleMatcher, ModuleMatchers, ValidationNel}
import coursier.util.Traverse._

object ReconciliationParser {
def reconciliation(input: Seq[String], scalaVersionOrDefault: String): Either[List[String], Seq[(ModuleMatchers, Reconciliation)]] = {
DependencyParser.moduleVersions(
input, scalaVersionOrDefault
).either match {
case Left(e) => Left(e)
case Right(elems) =>
(elems.validationNelTraverse { case (m: Module, v: String) =>
val e = reconciliation(m, v)
ValidationNel.fromEither(e)
}).either
}
}

def reconciliation(module: Module, v: String): Either[String, (ModuleMatchers, Reconciliation)] = {
val m =
if (module.organization == Organization("*") && module.name == ModuleName("*")) ModuleMatchers.all
else ModuleMatchers(exclude = Set(ModuleMatcher.all), include = Set(ModuleMatcher(module)))
(v match {
case "basic" => Right(Reconciliation.Basic)
case "relaxed" => Right(Reconciliation.Relaxed)
case _ => Left(s"Unknown reconciliation '$v'")
}) map { r =>
m -> r
}
}
}

0 comments on commit d90e63a

Please sign in to comment.