Merge pull request #356 from eed3si9n/wip/eviction_error

EvictionError
This commit is contained in:
eugene yokota 2020-12-20 20:44:48 -05:00 committed by GitHub
commit a1f577a591
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 292 additions and 2 deletions

View File

@ -17,10 +17,12 @@ private[sbt] object VersionSchemes {
final val EarlySemVer = "early-semver"
final val SemVerSpec = "semver-spec"
final val PackVer = "pvp"
final val Strict = "strict"
final val Always = "always"
def validateScheme(value: String): Unit =
value match {
case EarlySemVer | SemVerSpec | PackVer => ()
case EarlySemVer | SemVerSpec | PackVer | Strict | Always => ()
case "semver" =>
sys.error(
s"""'semver' is ambiguous.

View File

@ -0,0 +1,151 @@
package sbt
package librarymanagement
import scala.collection.mutable
import sbt.internal.librarymanagement.VersionSchemes
import sbt.util.ShowLines
object EvictionError {
def apply(
report: UpdateReport,
module: ModuleDescriptor,
schemes: Seq[ModuleID],
): EvictionError = {
val options = EvictionWarningOptions.full
val evictions = EvictionWarning.buildEvictions(options, report)
processEvictions(module, options, evictions, schemes)
}
private[sbt] def processEvictions(
module: ModuleDescriptor,
options: EvictionWarningOptions,
reports: Seq[OrganizationArtifactReport],
schemes: Seq[ModuleID],
): EvictionError = {
val directDependencies = module.directDependencies
val pairs = reports map { detail =>
val evicteds = detail.modules filter { _.evicted }
val winner = (detail.modules filterNot { _.evicted }).headOption
new EvictionPair(
detail.organization,
detail.name,
winner,
evicteds,
true,
options.showCallers
)
}
val incompatibleEvictions: 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
VersionSchemes.validateScheme(s.revision)
val versionScheme = s.revision
(s.crossVersion, sbvOpt) match {
case (_: Binary, Some("2.13")) =>
List(
(s.organization, s"${s.name}_2.13") -> versionScheme,
(s.organization, s"${s.name}_3") -> versionScheme
)
case (_: Binary, Some(sbv)) if sbv.startsWith("3.0") || sbv == "3" =>
List(
(s.organization, s"${s.name}_$sbv") -> versionScheme,
(s.organization, s"${s.name}_2.13") -> versionScheme
)
case (_: Binary, Some(sbv)) =>
List((s.organization, s"${s.name}_$sbv") -> versionScheme)
case _ =>
List((s.organization, s.name) -> versionScheme)
}
}: _*)
def calculateCompatible(p: EvictionPair): (Boolean, String) = {
val winnerOpt = p.winner map { _.module }
val extraAttributes = (p.winner match {
case Some(r) => r.extraAttributes
case _ => Map.empty
}) ++ (winnerOpt match {
case Some(w) => w.extraAttributes
case _ => Map.empty
})
// prioritize user-defined version scheme to allow overriding the real scheme
val schemeOpt = userDefinedSchemes
.get((p.organization, p.name))
.orElse(userDefinedSchemes.get((p.organization, "*")))
.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
}
(p.evicteds forall { r =>
f((r.module, winnerOpt, module.scalaModuleInfo))
}, schemeOpt.getOrElse("?"))
}
pairs foreach {
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)
}
}
case _ => ()
}
new EvictionError(
incompatibleEvictions.toList,
)
}
implicit val evictionErrorLines: ShowLines[EvictionError] = ShowLines { a: EvictionError =>
a.toLines
}
}
final class EvictionError private[sbt] (
val incompatibleEvictions: Seq[(EvictionPair, String)],
) {
def run(): Unit =
if (incompatibleEvictions.nonEmpty) {
sys.error(toLines.mkString("\n"))
}
def toLines: 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({
case (a, scheme) =>
val revs = a.evicteds map { _.module.revision }
val revsStr = if (revs.size <= 1) revs.mkString else "{" + revs.mkString(", ") + "}"
val seen: mutable.Set[ModuleID] = mutable.Set()
val callers: List[String] = (a.evicteds.toList ::: a.winner.toList) flatMap { r =>
val rev = r.module.revision
r.callers.toList flatMap { caller =>
if (seen(caller.caller)) Nil
else {
seen += caller.caller
List(f"\t +- ${caller}%-50s (depends on $rev)")
}
}
}
val winnerRev = a.winner match {
case Some(r) => s":${r.module.revision} ($scheme) is selected over ${revsStr}"
case _ => " is evicted for all versions"
}
val title = s"\t* ${a.organization}:${a.name}$winnerRev"
val lines = title :: (if (a.showCallers) callers.reverse else Nil) ::: List("")
out ++= lines
})
out.toList
}
}

View File

@ -161,10 +161,26 @@ object EvictionWarningOptions {
}
}
lazy val guessStrict
: PartialFunction[(ModuleID, Option[ModuleID], Option[ScalaModuleInfo]), Boolean] = {
case (m1, Some(m2), _) =>
(m1.revision, m2.revision) match {
case (VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2)) =>
VersionNumber.Strict
.isCompatible(VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2))
case _ => false
}
}
lazy val guessFalse
: PartialFunction[(ModuleID, Option[ModuleID], Option[ScalaModuleInfo]), Boolean] = {
case (_, _, _) => false
}
lazy val guessTrue
: PartialFunction[(ModuleID, Option[ModuleID], Option[ScalaModuleInfo]), Boolean] = {
case (_, _, _) => true
}
}
final class EvictionPair private[sbt] (
@ -325,6 +341,10 @@ object EvictionWarning {
})
val schemeOpt = VersionSchemes.extractFromExtraAttributes(extraAttributes)
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)) =>

View File

@ -67,12 +67,13 @@ object CustomPomParser {
/** The key prefix that indicates that this is used only to store extra information and is not intended for dependency resolution.*/
val InfoKeyPrefix = SbtPomExtraProperties.POM_INFO_KEY_PREFIX
val ApiURLKey = SbtPomExtraProperties.POM_API_KEY
val VersionSchemeKey = SbtPomExtraProperties.VERSION_SCHEME_KEY
val SbtVersionKey = PomExtraDependencyAttributes.SbtVersionKey
val ScalaVersionKey = PomExtraDependencyAttributes.ScalaVersionKey
val ExtraAttributesKey = PomExtraDependencyAttributes.ExtraAttributesKey
private[this] val unqualifiedKeys =
Set(SbtVersionKey, ScalaVersionKey, ExtraAttributesKey, ApiURLKey)
Set(SbtVersionKey, ScalaVersionKey, ExtraAttributesKey, ApiURLKey, VersionSchemeKey)
// packagings that should be jars, but that Ivy doesn't handle as jars
// TODO - move this elsewhere.

View File

@ -0,0 +1,116 @@
package sbt.internal.librarymanagement
import sbt.librarymanagement._
import sbt.internal.librarymanagement.cross.CrossVersionUtil
import sbt.librarymanagement.syntax._
object EvictionErrorSpec extends BaseIvySpecification {
// This is a specification to check the eviction errors
import sbt.util.ShowLines._
test("Eviction error should detect binary incompatible Scala libraries") {
val deps = Vector(`scala2.10.4`, `akkaActor2.1.4`, `akkaActor2.3.0`)
val m = module(defaultModuleId, deps, Some("2.10.4"))
val report = ivyUpdate(m)
assert(EvictionError(report, m, oldAkkaPvp).incompatibleEvictions.size == 1)
}
test("it should print out message about the eviction") {
val deps = Vector(`scala2.10.4`, `akkaActor2.1.4`, `akkaActor2.3.0`)
val m = module(defaultModuleId, deps, Some("2.10.4"))
val report = ivyUpdate(m)
assert(
EvictionError(report, m, oldAkkaPvp).lines ==
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.0 (pvp) is selected over 2.1.4",
"\t +- com.example:foo:0.1.0 (depends on 2.1.4)",
""
)
)
}
test("it should print out message including the transitive dependencies") {
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, oldAkkaPvp).lines ==
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"))
val report = ivyUpdate(m)
assert(
EvictionError(report, m, Nil).lines ==
List(
"found version conflict(s) in library dependencies; some are suspected to be binary incompatible:",
"",
"\t* org.typelevel:cats-effect_2.13:3.0.0-M4 (early-semver) is selected over {2.0.0, 2.2.0}",
"\t +- com.example:foo:0.1.0 (depends on 3.0.0-M4)",
"\t +- co.fs2:fs2-core_2.13:2.4.5 (depends on 2.2.0)",
"\t +- org.http4s:http4s-core_2.13:0.21.11 (depends on 2.2.0)",
"\t +- io.chrisdavenport:vault_2.13:2.0.0 (depends on 2.0.0)",
"\t +- io.chrisdavenport:unique_2.13:2.0.0 (depends on 2.0.0)",
""
)
)
}
test("it should selectively allow opt-out from the error") {
val deps = Vector(`scala2.13.3`, `http4s0.21.11`, `cats-effect3.0.0-M4`)
val m = module(defaultModuleId, deps, Some("2.13.3"))
val report = ivyUpdate(m)
val overrideRules = List("org.typelevel" %% "cats-effect" % "always")
assert(EvictionError(report, m, overrideRules).incompatibleEvictions.isEmpty)
}
// older Akka was on pvp
def oldAkkaPvp = List("com.typesafe.akka" % "*" % "pvp")
lazy val `akkaActor2.1.4` =
ModuleID("com.typesafe.akka", "akka-actor", "2.1.4").withConfigurations(Some("compile")) cross CrossVersion.binary
lazy val `akkaActor2.3.0` =
ModuleID("com.typesafe.akka", "akka-actor", "2.3.0").withConfigurations(Some("compile")) cross CrossVersion.binary
lazy val `scala2.10.4` =
ModuleID("org.scala-lang", "scala-library", "2.10.4").withConfigurations(Some("compile"))
lazy val `scala2.13.3` =
ModuleID("org.scala-lang", "scala-library", "2.13.3").withConfigurations(Some("compile"))
lazy val `bananaSesame0.4` =
ModuleID("org.w3", "banana-sesame", "0.4").withConfigurations(Some("compile")) cross CrossVersion.binary // uses akka-actor 2.1.4
lazy val `akkaRemote2.3.4` =
ModuleID("com.typesafe.akka", "akka-remote", "2.3.4").withConfigurations(Some("compile")) cross CrossVersion.binary // uses akka-actor 2.3.4
lazy val `http4s0.21.11` =
("org.http4s" %% "http4s-blaze-server" % "0.21.11").withConfigurations(Some("compile"))
// https://repo1.maven.org/maven2/org/typelevel/cats-effect_2.13/3.0.0-M4/cats-effect_2.13-3.0.0-M4.pom
// is published with early-semver
lazy val `cats-effect3.0.0-M4` =
("org.typelevel" %% "cats-effect" % "3.0.0-M4").withConfigurations(Some("compile"))
lazy val `cats-parse0.1.0` =
("org.typelevel" %% "cats-parse" % "0.1.0").withConfigurations(Some("compile"))
lazy val `cats-parse0.2.0` =
("org.typelevel" %% "cats-parse" % "0.2.0").withConfigurations(Some("compile"))
def dummyScalaModuleInfo(v: String): ScalaModuleInfo =
ScalaModuleInfo(
scalaFullVersion = v,
scalaBinaryVersion = CrossVersionUtil.binaryScalaVersion(v),
configurations = Vector.empty,
checkExplicit = true,
filterImplicit = false,
overrideScalaVersion = true
)
}