Automatic eviction warning

This extracts info.versionScheme from POM and uses that to guide the
eviction report instead of taking a stab in the dark that all Scala
libraries are using pvp.
This commit is contained in:
Eugene Yokota 2020-08-05 00:08:23 -04:00
parent 50ca3902c3
commit 9d87715100
5 changed files with 178 additions and 19 deletions

View File

@ -13,6 +13,7 @@ public class SbtPomExtraProperties {
public static final String POM_SCALA_VERSION = "scalaVersion";
public static final String POM_SBT_VERSION = "sbtVersion";
public static final String POM_API_KEY = "info.apiURL";
public static final String VERSION_SCHEME_KEY = "info.versionScheme";
public static final String LICENSE_COUNT_KEY = "license.count";

View File

@ -0,0 +1,43 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
package librarymanagement
import sbt.internal.librarymanagement.mavenint.SbtPomExtraProperties
import sbt.librarymanagement.ModuleID
// See APIMappings.scala
private[sbt] object VersionSchemes {
final val EarlySemVer = "early-semver"
final val SemVerSpec = "semver-spec"
final val PackVer = "pvp"
def validateScheme(value: String): Unit =
value match {
case EarlySemVer | SemVerSpec | PackVer => ()
case "semver" =>
sys.error(
s"""'semver' is ambiguous.
|Based on the Semantic Versioning 2.0.0, 0.y.z updates are all initial development and thus
|0.6.0 and 0.6.1 would NOT maintain any compatibility, but in Scala ecosystem it is
|common to start adopting binary compatibility even in 0.y.z releases.
|
|Specify 'early-semver' for the early variant.
|Specify 'semver-spec' for the spec-correct SemVer.""".stripMargin
)
case x => sys.error(s"unknown version scheme: $x")
}
/** info.versionScheme property will be included into POM after sbt 1.4.0.
*/
def extractFromId(mid: ModuleID): Option[String] = extractFromExtraAttributes(mid.extraAttributes)
def extractFromExtraAttributes(extraAttributes: Map[String, String]): Option[String] =
extraAttributes.get(SbtPomExtraProperties.VERSION_SCHEME_KEY)
}

View File

@ -3,6 +3,7 @@ package sbt.librarymanagement
import collection.mutable
import Configurations.Compile
import ScalaArtifacts.{ LibraryID, CompilerID }
import sbt.internal.librarymanagement.VersionSchemes
import sbt.util.Logger
import sbt.util.ShowLines
@ -136,6 +137,17 @@ object EvictionWarningOptions {
}
}
lazy val guessEarlySemVer
: 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.EarlySemVer
.isCompatible(VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2))
case _ => false
}
}
lazy val guessFalse
: PartialFunction[(ModuleID, Option[ModuleID], Option[ScalaModuleInfo]), Boolean] = {
case (_, _, _) => false
@ -290,9 +302,25 @@ object EvictionWarning {
var binaryIncompatibleEvictionExists = false
def guessCompatible(p: EvictionPair): Boolean =
p.evicteds forall { r =>
options.guessCompatible(
(r.module, p.winner map { _.module }, module.scalaModuleInfo)
)
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
})
val schemeOpt = VersionSchemes.extractFromExtraAttributes(extraAttributes)
val f = (winnerOpt, schemeOpt) match {
case (Some(_), Some(VersionSchemes.EarlySemVer)) =>
EvictionWarningOptions.guessEarlySemVer
case (Some(_), Some(VersionSchemes.SemVerSpec)) =>
EvictionWarningOptions.guessSemVer
case (Some(_), Some(VersionSchemes.PackVer)) =>
EvictionWarningOptions.guessSecondSegment
case _ => options.guessCompatible(_)
}
f((r.module, winnerOpt, module.scalaModuleInfo))
}
pairs foreach {
case p if isScalaArtifact(module, p.organization, p.name) =>

View File

@ -167,9 +167,19 @@ object VersionNumber {
* Also API compatibility is expected even when the first segment is zero.
*/
object SecondSegment extends VersionNumberCompatibility {
def name: String = "Second Segment Variant"
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean =
PackVer.isCompatible(v1, v2)
}
/** A variant of SemVar that seems to be common among the Scala libraries.
* The second segment (y in x.y.z) increments breaks the binary compatibility even when x > 0.
* Also API compatibility is expected even when the first segment is zero.
*/
object PackVer extends VersionNumberCompatibility {
import SemVer._
def name: String = "Second Segment Variant"
def name: String = "Package Versioning Policy"
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean =
doIsCompat(dropBuildMetadata(v1), dropBuildMetadata(v2))
@ -182,6 +192,69 @@ object VersionNumber {
}
}
}
/** A variant of SemVar that enforces API compatibility when the first segment is zero.
*/
object EarlySemVer extends VersionNumberCompatibility {
import SemVer._
def name: String = "Early Semantic Versioning"
/* Quotes of parts of the rules in the SemVer Spec relevant to compatibility checking:
*
* Rule 2:
* > A normal version number MUST take the form X.Y.Z
*
* Rule 6:
* > Patch version Z (x.y.Z | x > 0) MUST be incremented if only backwards compatible bug fixes are introduced.
*
* Rule 7:
* > Minor version Y (x.Y.z | x > 0) MUST be incremented if new, backwards compatible functionality is introduced.
*
* Rule 8:
* > Major version X (X.y.z | X > 0) MUST be incremented if any backwards incompatible changes are introduced.
*
* Rule 9:
* > A pre-release version MAY be denoted by appending a hyphen and a series of
* > dot separated identifiers immediately following the patch version.
* > Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-].
* > Identifiers MUST NOT be empty.
* > Numeric identifiers MUST NOT include leading zeroes.
* > Pre-release versions have a lower precedence than the associated normal version.
* > A pre-release version indicates that the version is unstable and might not satisfy the
* > intended compatibility requirements as denoted by its associated normal version.
* > Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92.
*
* Rule 10:
* > Build metadata MAY be denoted by appending a plus sign and a series of
* > dot separated identifiers immediately following the patch or pre-release version.
* > Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-].
* > Identifiers MUST NOT be empty.
* > Build metadata SHOULD be ignored when determining version precedence.
* > Thus two versions that differ only in the build metadata, have the same precedence.
* > Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85.
*
* Rule 10 means that build metadata is never considered for compatibility
* we'll enforce this immediately by dropping them from both versions
* Rule 2 we enforce with custom extractors.
* Rule 6, 7 & 8 means version compatibility is determined by comparing the two X values
* Rule 9..
* Dale thinks means pre-release versions are fully equals checked..
* Eugene thinks means pre-releases before 1.0.0 are not compatible, if not they are..
* Rule 4 is modified in this variant.
*/
def isCompatible(v1: VersionNumber, v2: VersionNumber): Boolean =
doIsCompat(dropBuildMetadata(v1), dropBuildMetadata(v2))
private[this] def doIsCompat(v1: VersionNumber, v2: VersionNumber): Boolean =
(v1, v2) match {
case (NormalVersion(0, _, 0), NormalVersion(0, _, 0)) => v1 == v2
case (NormalVersion(0, y1, _), NormalVersion(0, y2, _)) => y1 == y2
case (NormalVersion(_, 0, 0), NormalVersion(_, 0, 0)) => v1 == v2 // R9 maybe?
case (NormalVersion(x1, _, _), NormalVersion(x2, _, _)) => x1 == x2 // R6, R7 & R8
case _ => false
}
}
}
trait VersionNumberCompatibility {

View File

@ -4,7 +4,7 @@ import org.scalatest.{ FreeSpec, Inside, Matchers }
// This is a specification to check VersionNumber and VersionNumberCompatibility.
class VersionNumberSpec extends FreeSpec with Matchers with Inside {
import VersionNumber.{ SemVer, SecondSegment }
import VersionNumber.{ EarlySemVer, SemVer, PackVer }
version("1") { v =>
assertParsesTo(v, Seq(1), Seq(), Seq())
@ -28,10 +28,15 @@ class VersionNumberSpec extends FreeSpec with Matchers with Inside {
assertIsNotCompatibleWith(v, "2.0.0", SemVer)
assertIsNotCompatibleWith(v, "1.0.0-M1", SemVer)
assertIsCompatibleWith(v, "1.0.1", SecondSegment)
assertIsNotCompatibleWith(v, "1.1.1", SecondSegment)
assertIsNotCompatibleWith(v, "2.0.0", SecondSegment)
assertIsNotCompatibleWith(v, "1.0.0-M1", SecondSegment)
assertIsCompatibleWith(v, "1.0.1", EarlySemVer)
assertIsCompatibleWith(v, "1.1.1", EarlySemVer)
assertIsNotCompatibleWith(v, "2.0.0", EarlySemVer)
assertIsNotCompatibleWith(v, "1.0.0-M1", EarlySemVer)
assertIsCompatibleWith(v, "1.0.1", PackVer)
assertIsNotCompatibleWith(v, "1.1.1", PackVer)
assertIsNotCompatibleWith(v, "2.0.0", PackVer)
assertIsNotCompatibleWith(v, "1.0.0-M1", PackVer)
}
version("1.0.0.0") { v =>
@ -49,9 +54,13 @@ class VersionNumberSpec extends FreeSpec with Matchers with Inside {
assertIsNotCompatibleWith(v, "0.12.1", SemVer)
assertIsNotCompatibleWith(v, "0.12.1-M1", SemVer)
assertIsNotCompatibleWith(v, "0.12.0-RC1", SecondSegment)
assertIsCompatibleWith(v, "0.12.1", SecondSegment)
assertIsCompatibleWith(v, "0.12.1-M1", SecondSegment)
assertIsNotCompatibleWith(v, "0.12.0-RC1", EarlySemVer)
assertIsCompatibleWith(v, "0.12.1", EarlySemVer)
assertIsCompatibleWith(v, "0.12.1-M1", EarlySemVer)
assertIsNotCompatibleWith(v, "0.12.0-RC1", PackVer)
assertIsCompatibleWith(v, "0.12.1", PackVer)
assertIsCompatibleWith(v, "0.12.1-M1", PackVer)
}
version("0.1.0-SNAPSHOT") { v =>
@ -62,9 +71,13 @@ class VersionNumberSpec extends FreeSpec with Matchers with Inside {
assertIsNotCompatibleWith(v, "0.1.0", SemVer)
assertIsCompatibleWith(v, "0.1.0-SNAPSHOT+001", SemVer)
assertIsCompatibleWith(v, "0.1.0-SNAPSHOT", SecondSegment)
assertIsNotCompatibleWith(v, "0.1.0", SecondSegment)
assertIsCompatibleWith(v, "0.1.0-SNAPSHOT+001", SecondSegment)
assertIsCompatibleWith(v, "0.1.0-SNAPSHOT", EarlySemVer)
assertIsNotCompatibleWith(v, "0.1.0", EarlySemVer)
assertIsCompatibleWith(v, "0.1.0-SNAPSHOT+001", EarlySemVer)
assertIsCompatibleWith(v, "0.1.0-SNAPSHOT", PackVer)
assertIsNotCompatibleWith(v, "0.1.0", PackVer)
assertIsCompatibleWith(v, "0.1.0-SNAPSHOT+001", PackVer)
}
version("0.1.0-M1") { v =>
@ -86,7 +99,7 @@ class VersionNumberSpec extends FreeSpec with Matchers with Inside {
assertParsesTo(v, Seq(2, 10, 4), Seq("20140115", "000117", "b3a", "sources"), Seq())
assertCascadesTo(v, Seq("2.10.4-20140115-000117-b3a-sources", "2.10.4", "2.10"))
assertIsCompatibleWith(v, "2.0.0", SemVer)
assertIsNotCompatibleWith(v, "2.0.0", SecondSegment)
assertIsNotCompatibleWith(v, "2.0.0", PackVer)
}
version("20140115000117-b3a-sources") { v =>
@ -187,9 +200,10 @@ class VersionNumberSpec extends FreeSpec with Matchers with Inside {
) = {
val prefix = if (expectOutcome) "should" else "should NOT"
val compatibilityStrategy = vnc match {
case SemVer => "SemVer"
case SecondSegment => "SecondSegment"
case _ => val s = vnc.name; if (s contains " ") s""""$s"""" else s
case SemVer => "SemVer"
case PackVer => "PackVer"
case EarlySemVer => "EarlySemVer"
case _ => val s = vnc.name; if (s contains " ") s""""$s"""" else s
}
s"$prefix be $compatibilityStrategy compatible with $v2" in {
vnc.isCompatible(VersionNumber(v1.value), VersionNumber(v2)) shouldBe expectOutcome