Implement assumedVersionScheme

Ref https://github.com/sbt/sbt/issues/6302
Ref https://github.com/sbt/sbt/issues/6301

Apparently some users are interested in keeping the eviction warning
feature, so here's an option to bring guessing back.
This commit is contained in:
Eugene Yokota 2021-02-21 04:49:36 -05:00
parent 70e7718353
commit dc0b682d7e
4 changed files with 87 additions and 27 deletions

View File

@ -10,7 +10,7 @@ package internal
package librarymanagement
import sbt.internal.librarymanagement.mavenint.SbtPomExtraProperties
import sbt.librarymanagement.ModuleID
import sbt.librarymanagement.{ EvictionWarningOptions, ModuleID, ScalaModuleInfo }
// See APIMappings.scala
private[sbt] object VersionSchemes {
@ -42,4 +42,15 @@ private[sbt] object VersionSchemes {
def extractFromExtraAttributes(extraAttributes: Map[String, String]): Option[String] =
extraAttributes.get(SbtPomExtraProperties.VERSION_SCHEME_KEY)
def evalFunc(
scheme: String
): Function1[(ModuleID, Option[ModuleID], Option[ScalaModuleInfo]), Boolean] =
scheme match {
case EarlySemVer => EvictionWarningOptions.guessEarlySemVer
case SemVerSpec => EvictionWarningOptions.guessSemVer
case PackVer => EvictionWarningOptions.evalPvp
case Strict => EvictionWarningOptions.guessStrict
case Always => EvictionWarningOptions.guessTrue
}
}

View File

@ -3,23 +3,47 @@ package librarymanagement
import scala.collection.mutable
import sbt.internal.librarymanagement.VersionSchemes
import sbt.util.ShowLines
import sbt.util.{ Level, ShowLines }
import EvictionWarningOptions.isNameScalaSuffixed
object EvictionError {
def apply(
report: UpdateReport,
module: ModuleDescriptor,
schemes: Seq[ModuleID],
): EvictionError = {
apply(report, module, schemes, "always", "always", Level.Debug)
}
def apply(
report: UpdateReport,
module: ModuleDescriptor,
schemes: Seq[ModuleID],
assumedVersionScheme: String,
assumedVersionSchemeJava: String,
assumedEvictionErrorLevel: Level.Value,
): EvictionError = {
val options = EvictionWarningOptions.full
val evictions = EvictionWarning.buildEvictions(options, report)
processEvictions(module, options, evictions, schemes)
processEvictions(
module,
options,
evictions,
schemes,
assumedVersionScheme,
assumedVersionSchemeJava,
assumedEvictionErrorLevel,
)
}
private[sbt] def processEvictions(
module: ModuleDescriptor,
options: EvictionWarningOptions,
reports: Seq[OrganizationArtifactReport],
schemes: Seq[ModuleID],
assumedVersionScheme: String,
assumedVersionSchemeJava: String,
assumedEvictionErrorLevel: Level.Value,
): EvictionError = {
val directDependencies = module.directDependencies
val pairs = reports map { detail =>
@ -35,6 +59,7 @@ object EvictionError {
)
}
val incompatibleEvictions: mutable.ListBuffer[(EvictionPair, String)] = mutable.ListBuffer()
val assumedIncompatEvictions: mutable.ListBuffer[(EvictionPair, String)] = mutable.ListBuffer()
val sbvOpt = module.scalaModuleInfo.map(_.scalaBinaryVersion)
val userDefinedSchemes: Map[(String, String), String] = Map(schemes flatMap { s =>
val organization = s.organization
@ -57,7 +82,8 @@ object EvictionError {
List((s.organization, s.name) -> versionScheme)
}
}: _*)
def calculateCompatible(p: EvictionPair): (Boolean, String) = {
def calculateCompatible(p: EvictionPair): (Boolean, String, Boolean, String) = {
val winnerOpt = p.winner map { _.module }
val extraAttributes = ((p.winner match {
case Some(r) => r.extraAttributes.toMap
@ -73,36 +99,34 @@ object EvictionError {
.orElse(VersionSchemes.extractFromExtraAttributes(extraAttributes))
.orElse(userDefinedSchemes.get(("*", "*")))
val f = (winnerOpt, schemeOpt) match {
case (Some(_), Some(VersionSchemes.Always)) =>
EvictionWarningOptions.guessTrue
case (Some(_), Some(VersionSchemes.Strict)) =>
EvictionWarningOptions.guessStrict
case (Some(_), Some(VersionSchemes.EarlySemVer)) =>
EvictionWarningOptions.guessEarlySemVer
case (Some(_), Some(VersionSchemes.SemVerSpec)) =>
EvictionWarningOptions.guessSemVer
case (Some(_), Some(VersionSchemes.PackVer)) =>
EvictionWarningOptions.evalPvp
case _ => EvictionWarningOptions.guessTrue
case (Some(_), Some(scheme)) => VersionSchemes.evalFunc(scheme)
case _ => EvictionWarningOptions.guessTrue
}
val scheme =
if (isNameScalaSuffixed(p.name)) assumedVersionScheme
else assumedVersionSchemeJava
val guess = VersionSchemes.evalFunc(scheme)
(p.evicteds forall { r =>
f((r.module, winnerOpt, module.scalaModuleInfo))
}, schemeOpt.getOrElse("?"))
}, schemeOpt.getOrElse("?"), p.evicteds forall { r =>
guess((r.module, winnerOpt, module.scalaModuleInfo))
}, scheme)
}
pairs foreach {
// don't report on a transitive eviction that does not have a winner
// https://github.com/sbt/sbt/issues/4946
case p if p.winner.isDefined =>
// don't report on a transitive eviction that does not have a winner
// https://github.com/sbt/sbt/issues/4946
if (p.winner.isDefined) {
val r = calculateCompatible(p)
if (!r._1) {
incompatibleEvictions += (p -> r._2)
}
val r = calculateCompatible(p)
if (!r._1) {
incompatibleEvictions += (p -> r._2)
} else if (!r._3) {
assumedIncompatEvictions += (p -> r._4)
}
case _ => ()
}
new EvictionError(
incompatibleEvictions.toList,
assumedIncompatEvictions.toList,
)
}
@ -113,17 +137,22 @@ object EvictionError {
final class EvictionError private[sbt] (
val incompatibleEvictions: Seq[(EvictionPair, String)],
val assumedIncompatibleEvictions: Seq[(EvictionPair, String)],
) {
def run(): Unit =
if (incompatibleEvictions.nonEmpty) {
sys.error(toLines.mkString("\n"))
}
def toLines: List[String] = {
def toLines: List[String] = toLines(incompatibleEvictions, false)
def toAssumedLines: List[String] = toLines(assumedIncompatibleEvictions, true)
def toLines(evictions: Seq[(EvictionPair, String)], assumed: Boolean): List[String] = {
val out: mutable.ListBuffer[String] = mutable.ListBuffer()
out += "found version conflict(s) in library dependencies; some are suspected to be binary incompatible:"
out += ""
incompatibleEvictions.foreach({
evictions.foreach({
case (a, scheme) =>
val revs = a.evicteds map { _.module.revision }
val revsStr = if (revs.size <= 1) revs.mkString else "{" + revs.mkString(", ") + "}"
@ -138,8 +167,9 @@ final class EvictionError private[sbt] (
}
}
}
val que = if (assumed) "?" else ""
val winnerRev = a.winner match {
case Some(r) => s":${r.module.revision} ($scheme) is selected over ${revsStr}"
case Some(r) => s":${r.module.revision} ($scheme$que) is selected over ${revsStr}"
case _ => " is evicted for all versions"
}
val title = s"\t* ${a.organization}:${a.name}$winnerRev"

View File

@ -98,7 +98,7 @@ object EvictionWarningOptions {
lazy val defaultGuess: Function1[(ModuleID, Option[ModuleID], Option[ScalaModuleInfo]), Boolean] =
guessSbtOne orElse guessSecondSegment orElse guessSemVer orElse guessFalse
private def isNameScalaSuffixed(name: String): Boolean =
private[sbt] def isNameScalaSuffixed(name: String): Boolean =
name.contains("_2.") || name.contains("_3") || name.contains("_4")
/** A partial function that checks if given m2 is suffixed, and use pvp to evaluate. */

View File

@ -3,6 +3,7 @@ package sbt.internal.librarymanagement
import sbt.librarymanagement._
import sbt.internal.librarymanagement.cross.CrossVersionUtil
import sbt.librarymanagement.syntax._
import sbt.util.Level
object EvictionErrorSpec extends BaseIvySpecification {
// This is a specification to check the eviction errors
@ -50,6 +51,24 @@ object EvictionErrorSpec extends BaseIvySpecification {
)
}
test("it should be able to emulate eviction warnings") {
val deps = Vector(`scala2.10.4`, `bananaSesame0.4`, `akkaRemote2.3.4`)
val m = module(defaultModuleId, deps, Some("2.10.4"))
val report = ivyUpdate(m)
assert(
EvictionError(report, m, Nil, "pvp", "early-semver", Level.Warn).toAssumedLines ==
List(
"found version conflict(s) in library dependencies; some are suspected to be binary incompatible:",
"",
"\t* com.typesafe.akka:akka-actor_2.10:2.3.4 (pvp?) is selected over 2.1.4",
"\t +- com.typesafe.akka:akka-remote_2.10:2.3.4 (depends on 2.3.4)",
"\t +- org.w3:banana-rdf_2.10:0.4 (depends on 2.1.4)",
"\t +- org.w3:banana-sesame_2.10:0.4 (depends on 2.1.4)",
""
)
)
}
test("it should detect Semantic Versioning violations") {
val deps = Vector(`scala2.13.3`, `http4s0.21.11`, `cats-effect3.0.0-M4`)
val m = module(defaultModuleId, deps, Some("2.13.3"))