mirror of https://github.com/sbt/sbt.git
Implement EvictionError
Ref https://eed3si9n.com/enforcing-semver-with-sbt-strict-update This adds EvictionError, a replacement for EvictionWarning. The problem with the current eviction warning is that it has too many false positives. Using the versionScheme information that could be supplied by the library authors and/or the build users, this eliminates the guessing work. At which point, we can fail the resolution.
This commit is contained in:
parent
aac1edb426
commit
9297139f6a
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue