-
Notifications
You must be signed in to change notification settings - Fork 186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
define & apply binary compatibility strategy #1565
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,8 @@ import sbt.nio.Keys._ | |
import sbt.plugins.JvmPlugin | ||
import com.typesafe.tools.mima.plugin.MimaPlugin.autoImport._ | ||
import sbtbuildinfo.BuildInfoKey | ||
import sbtbuildinfo.BuildInfoPlugin.autoImport.{BuildInfoKey, _} | ||
import sbtbuildinfo.BuildInfoPlugin.autoImport._ | ||
import sbtversionpolicy.SbtVersionPolicyPlugin.autoImport._ | ||
import com.typesafe.sbt.sbtghpages.GhpagesKeys | ||
import sbt.librarymanagement.ivy.IvyDependencyResolution | ||
import sbt.plugins.IvyPlugin | ||
|
@@ -179,6 +180,13 @@ object ScalafixBuild extends AutoPlugin with GhpagesKeys { | |
private val PreviousScalaVersion: Map[String, String] = Map( | ||
) | ||
|
||
override def buildSettings: Seq[Setting[_]] = List( | ||
versionPolicyIgnoredInternalDependencyVersions := | ||
Some("^\\d+\\.\\d+\\.\\d+\\+\\d+".r), | ||
versionScheme := Some("early-semver"), | ||
versionPolicyIntention := Compatibility.None // TODO: harden after 0.10.0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I verified that with
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. opened https://github.com/scalacenter/scalafix/pull/1568/commits as draft for a reminder There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You probably also want to add: versionPolicyIgnoredInternalDependencyVersions := Some("^\\d+\\.\\d+\\.\\d+\\+\\d+".r) See https://github.com/scalacenter/sbt-version-policy#how-to-integrate-with-sbt-dynver |
||
) | ||
|
||
override def projectSettings: Seq[Def.Setting[_]] = List( | ||
scalacOptions ++= compilerOptions.value, | ||
Compile / console / scalacOptions := | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") | ||
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") | ||
addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3") | ||
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.0.1") | ||
addSbtPlugin("ch.epfl.scala" % "sbt-version-policy" % "2.0.1") | ||
addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.1") | ||
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") | ||
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.34") |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,10 @@ import scalafix.Versions | |
import scalafix.cli.ExitStatus | ||
import scalafix.interfaces._ | ||
import scalafix.internal.config.ScalaVersion | ||
import scalafix.internal.util.Compatibility | ||
import scalafix.internal.util.Compatibility.Compatible | ||
import scalafix.internal.util.Compatibility.Incompatible | ||
import scalafix.internal.util.Compatibility.Unknown | ||
import scalafix.internal.v1.Args | ||
import scalafix.internal.v1.MainOps | ||
import scalafix.internal.v1.Rules | ||
|
@@ -76,7 +80,51 @@ final case class ScalafixArgumentsImpl(args: Args = Args.default) | |
customDependenciesCoordinates, | ||
Versions.scalaVersion | ||
) | ||
val extraURLs = customURLs.asScala ++ customDependenciesJARs.asScala | ||
|
||
// External rules are built against `scalafix-core` to expose `scalafix.v1.Rule` implementations. The | ||
// classloader loading `scalafix-cli` already contains `scalafix-core` to be able to discover them (which | ||
// is why it must be the parent of the one loading the tool classpath), so effectively, the version/instance | ||
// in the tool classpath will not be used. This adds a sanity check to warn or prevent the user in case of | ||
// mismatch. | ||
val scalafixCore = coursierapi.Module.parse( | ||
"ch.epfl.scala::scalafix-core", | ||
coursierapi.ScalaVersion.of(Versions.scalaVersion) | ||
) | ||
customDependenciesJARs.getDependencies.asScala | ||
.find(_.getModule == scalafixCore) | ||
.foreach { dependency => | ||
// We only check compatibility against THE scalafix-core returned by the coursier resolution, but | ||
// this is not exhaustive as some old rules might coexist with recent ones, so the older versions | ||
// get evicted. coursier-interface does not provide more granularity while the native coursier | ||
// is stuck on a 2-years old version because it no longer cross-publishes for 2.11, so for now, | ||
// the easiest option would be to run several resolutions in isolation, which seems like a massive | ||
// cost for just issuing a warning. | ||
Compatibility.earlySemver( | ||
dependency.getVersion, | ||
Versions.nightly | ||
) match { | ||
case Incompatible => | ||
throw new ScalafixException( | ||
s"Scalafix version ${Versions.nightly} cannot load the registered external rules, " + | ||
s"please upgrade to ${dependency.getVersion} or later" | ||
) | ||
case Unknown => | ||
args.out.println( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sbt-scalafix sets the printstream very late, so this gets dumped on plain stdout. It would be possible to wire a fallback sbt logger from the beginning. |
||
s"""INFO: loading external rule(s) built against an old version of scalafix (${dependency.getVersion}). | ||
|This might not be a problem, but if you run into unexpected behavior, you should either: | ||
|1. downgrade scalafix to ${dependency.getVersion} | ||
|2. try a more recent version of the rules(s) if available; request the rule maintainer | ||
| to build against scalafix ${Versions.nightly} or later if that does not help | ||
""".stripMargin | ||
) | ||
case Compatible => | ||
} | ||
} | ||
|
||
val extraURLs = customURLs.asScala ++ customDependenciesJARs | ||
.getFiles() | ||
.asScala | ||
.map(_.toURI().toURL()) | ||
val classLoader = new URLClassLoader( | ||
extraURLs.toArray, | ||
getClass.getClassLoader | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package scalafix.internal.util | ||
|
||
sealed trait Compatibility | ||
|
||
object Compatibility { | ||
|
||
case object Compatible extends Compatibility | ||
case object Unknown extends Compatibility | ||
case object Incompatible extends Compatibility | ||
|
||
private val Snapshot = """.*-SNAPSHOT""".r | ||
private val XYZ = """^([0-9]+)\.([0-9]+)\.([0-9]+)""".r | ||
|
||
def earlySemver(builtAgainst: String, runWith: String): Compatibility = { | ||
(builtAgainst, runWith) match { | ||
case (Snapshot(), _) => Unknown | ||
case (_, Snapshot()) => Unknown | ||
case (XYZ(bX, _, _), XYZ(rX, _, _)) if bX.toInt > rX.toInt => | ||
Incompatible | ||
case (XYZ(bX, _, _), XYZ(rX, _, _)) if bX.toInt < rX.toInt => | ||
Unknown | ||
// --- X match given the cases above | ||
case (XYZ(_, bY, _), XYZ(_, rY, _)) if bY.toInt > rY.toInt => | ||
Incompatible | ||
case (XYZ("0", bY, _), XYZ("0", rY, _)) if bY.toInt < rY.toInt => | ||
Unknown | ||
case (XYZ(_, bY, _), XYZ(_, rY, _)) if bY.toInt < rY.toInt => | ||
Compatible | ||
// --- X.Y match given the cases above | ||
case (XYZ("0", _, bZ), XYZ("0", _, rZ)) if bZ.toInt > rZ.toInt => | ||
Incompatible | ||
case _ => Compatible | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package scalafix.tests.cli | ||
|
||
import org.scalatest.funsuite.AnyFunSuite | ||
import scalafix.internal.util.Compatibility | ||
import scalafix.internal.util.Compatibility._ | ||
|
||
class CompatibilitySuite extends AnyFunSuite { | ||
|
||
// to avoid struggles when testing nightlies | ||
test("EarlySemver unknown if run or build is a snapshot") { | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "0.9.34+52-a83785c4-SNAPSHOT", | ||
runWith = "1.2.3" | ||
) == Unknown | ||
) | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "0.9.34", | ||
runWith = "1.2.3+1-bfe5ccd4-SNAPSHOT" | ||
) == Unknown | ||
) | ||
} | ||
|
||
// backward compatibility within X.*.*, 0.Y.*, ... | ||
test( | ||
"EarlySemver compatible if run is equal or greater by minor (or patch in 0.)" | ||
) { | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "1.3.27", | ||
runWith = "1.3.28" | ||
) == Compatible | ||
) | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "1.10.20", | ||
runWith = "1.12.0" | ||
) == Compatible | ||
) | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "0.6.12", | ||
runWith = "0.6.12" | ||
) == Compatible | ||
) | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "0.9.0", | ||
runWith = "0.9.20" | ||
) == Compatible | ||
) | ||
} | ||
|
||
// no forward compatibility: build might reference classes unknown to run | ||
test("EarlySemver incompatible if run is lower by minor (or patch in 0.)") { | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "0.10.8", | ||
runWith = "0.9.16" | ||
) == Incompatible | ||
) | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "0.10.17", | ||
runWith = "0.10.4" | ||
) == Incompatible | ||
) | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "2.0.0", | ||
runWith = "1.1.1" | ||
) == Incompatible | ||
) | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "1.4.7", | ||
runWith = "1.2.8" | ||
) == Incompatible | ||
) | ||
} | ||
|
||
// might be false positive/negative tree matches or link failures | ||
test("EarlySemver unknown if run is greater by major (or minor in 0.)") { | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "1.0.41", | ||
runWith = "2.0.0" | ||
) == Unknown | ||
) | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "0.9.38", | ||
runWith = "0.10.2" | ||
) == Unknown | ||
) | ||
assert( | ||
Compatibility.earlySemver( | ||
builtAgainst = "0.9.38", | ||
runWith = "1.0.0" | ||
) == Unknown | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't block publishing of snapshots 🤞