diff --git a/core/src/main/scala/sbt/internal/librarymanagement/VersionSchemes.scala b/core/src/main/scala/sbt/internal/librarymanagement/VersionSchemes.scala index b8a5b1293..e214cc377 100644 --- a/core/src/main/scala/sbt/internal/librarymanagement/VersionSchemes.scala +++ b/core/src/main/scala/sbt/internal/librarymanagement/VersionSchemes.scala @@ -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 + } } diff --git a/core/src/main/scala/sbt/librarymanagement/EvictionError.scala b/core/src/main/scala/sbt/librarymanagement/EvictionError.scala index ee9be4629..aaf926dec 100644 --- a/core/src/main/scala/sbt/librarymanagement/EvictionError.scala +++ b/core/src/main/scala/sbt/librarymanagement/EvictionError.scala @@ -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" diff --git a/core/src/main/scala/sbt/librarymanagement/EvictionWarning.scala b/core/src/main/scala/sbt/librarymanagement/EvictionWarning.scala index 926e93529..d646303e4 100644 --- a/core/src/main/scala/sbt/librarymanagement/EvictionWarning.scala +++ b/core/src/main/scala/sbt/librarymanagement/EvictionWarning.scala @@ -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. */ diff --git a/ivy/src/test/scala/sbt/internal/librarymanagement/EvictionErrorSpec.scala b/ivy/src/test/scala/sbt/internal/librarymanagement/EvictionErrorSpec.scala index 76edf91f9..a6b97c99e 100644 --- a/ivy/src/test/scala/sbt/internal/librarymanagement/EvictionErrorSpec.scala +++ b/ivy/src/test/scala/sbt/internal/librarymanagement/EvictionErrorSpec.scala @@ -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"))