Merge pull request #8912 from bitloi/fix/6694-scala3-eviction-downgrade

[2.x] feat: Eviction error for downgrade of Scala 3.x
This commit is contained in:
Anatolii Kmetiuk 2026-03-17 14:19:10 +09:00 committed by GitHub
commit a1f6fd2f65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 96 additions and 20 deletions

View File

@ -826,6 +826,7 @@ lazy val serverTestProj = (project in file("server-test"))
.dependsOn(sbtProj % "compile->test", scriptedSbtProj % "compile->test")
.settings(
testedBaseSettings,
allowUnsafeScalaLibUpgrade := true, // demote Scala 3 eviction to warn if deps bring newer scala3-library_3
Utils.noPublish,
// make server tests serial
Test / watchTriggers += baseDirectory.value.toGlob / "src" / "server-test" / **,

View File

@ -158,6 +158,34 @@ object Compiler:
val fullReport = Keys.update.value
val s = Keys.streams.value
def reportScalaLibEviction(
proj: String,
sv: String,
libVer: String,
libName: String,
err: Boolean,
alignmentLine: String,
evictedLine: String
): Unit =
val fix =
if err then
"""Upgrade the `scalaVersion` to fix the build. If upgrading the Scala compiler version is
|not possible (for example due to a regression in the compiler or a missing dependency),
|this error can be demoted by setting `allowUnsafeScalaLibUpgrade := true`.""".stripMargin
else s"""Note that the dependency classpath and the runtime classpath of your project
|contain the newer $libName $libVer, even if the scalaVersion is $sv.
|Compilation (macro expansion) or using the Scala REPL in sbt may fail with a LinkageError.""".stripMargin
val msg =
s"""Expected `$proj scalaVersion` to be $libVer or later, but found $sv.
|$alignmentLine
|
|$fix
|
|$evictedLine
|""".stripMargin
if err then sys.error(msg)
else s.log.warn(msg)
// For Scala 3, update scala-library.jar in `scala-tool` and `scala-doc-tool` in case a newer version
// is present in the `compile` configuration. This is needed once forwards binary compatibility is dropped
// to avoid NoSuchMethod exceptions when expanding macros.
@ -194,26 +222,39 @@ object Compiler:
val proj =
Def.displayBuildRelative(Keys.thisProjectRef.value.build, Keys.thisProjectRef.value)
if VersionNumber(sv).matchesSemVer(SemanticSelector(s"<$libVer")) then
val err = !Keys.allowUnsafeScalaLibUpgrade.value
val fix =
if err then
"""Upgrade the `scalaVersion` to fix the build. If upgrading the Scala compiler version is
|not possible (for example due to a regression in the compiler or a missing dependency),
|this error can be demoted by setting `allowUnsafeScalaLibUpgrade := true`.""".stripMargin
else s"""Note that the dependency classpath and the runtime classpath of your project
|contain the newer $libName $libVer, even if the scalaVersion is $sv.
|Compilation (macro expansion) or using the Scala REPL in sbt may fail with a LinkageError.""".stripMargin
val msg =
s"""Expected `$proj scalaVersion` to be $libVer or later, but found $sv.
|To support backwards-only binary compatibility (SIP-51), the Scala 2.13 compiler
|should not be older than $libName on the dependency classpath.
|
|$fix
|
|See `$proj evicted` to know why $libName $libVer is getting pulled in.
|""".stripMargin
if err then sys.error(msg)
else s.log.warn(msg)
reportScalaLibEviction(
proj,
sv,
libVer,
libName,
!Keys.allowUnsafeScalaLibUpgrade.value,
s"""To support backwards-only binary compatibility (SIP-51), the Scala 2.13 compiler
|should not be older than $libName on the dependency classpath.""".stripMargin,
s"See `$proj evicted` to know why $libName $libVer is getting pulled in."
)
else ()
else if ScalaArtifacts.isScala3(sv) then
val scala3LibOpt = for
compileReport <- fullReport.configuration(Configurations.Compile)
lib <- compileReport.modules.find(
_.module.name.startsWith(ScalaArtifacts.Scala3LibraryPrefix)
)
yield lib
for lib <- scala3LibOpt do
val libVer = lib.module.revision
val libName = lib.module.name
val proj =
Def.displayBuildRelative(Keys.thisProjectRef.value.build, Keys.thisProjectRef.value)
if VersionNumber(sv).matchesSemVer(SemanticSelector(s"<$libVer")) then
reportScalaLibEviction(
proj,
sv,
libVer,
libName,
!Keys.allowUnsafeScalaLibUpgrade.value,
s"To maintain the compile-time alignment, the Scala compiler cannot be older than $libName on the dependency classpath.",
s"See `$proj evicted` to know why $libName was upgraded from $sv to $libVer."
)
else ()
else ()
def file(id: String): Option[File] =

View File

@ -0,0 +1,12 @@
// Regression test for #6694: Scala 3 eviction error when scalaVersion < scala3-library_3 on classpath.
// low has scalaVersion 3.3.2 and depends on high (3.3.4), so resolved scala3-library_3 is 3.3.4.
// Without allowUnsafeScalaLibUpgrade, compile must fail with the eviction error.
lazy val high = project.settings(
scalaVersion := "3.3.4",
)
lazy val low = project.dependsOn(high).settings(
scalaVersion := "3.3.2",
// do NOT set allowUnsafeScalaLibUpgrade — we expect the build to fail
)

View File

@ -0,0 +1 @@
object H { def id[A](a: A): A = a }

View File

@ -0,0 +1 @@
object L { def main(args: Array[String]): Unit = println(H.id(1)) }

View File

@ -0,0 +1,2 @@
# Expect compile to fail: scalaVersion 3.3.2 < scala3-library_3 3.3.4 from high
-> low/compile

View File

@ -0,0 +1,11 @@
// Same as stdlib-unfreeze-scala3-eviction but with allowUnsafeScalaLibUpgrade := true on low.
// low has scalaVersion 3.3.2 and depends on high (3.3.4); we demote the eviction to a warning so compile succeeds.
lazy val high = project.settings(
scalaVersion := "3.3.4",
)
lazy val low = project.dependsOn(high).settings(
allowUnsafeScalaLibUpgrade := true,
scalaVersion := "3.3.2",
)

View File

@ -0,0 +1 @@
object H { def id[A](a: A): A = a }

View File

@ -0,0 +1 @@
object L { def main(args: Array[String]): Unit = println(H.id(1)) }

View File

@ -0,0 +1,3 @@
# With allowUnsafeScalaLibUpgrade := true, compile and run succeed (eviction is a warning only)
> low/compile
> low/run

View File

@ -16,6 +16,7 @@ lazy val a3 = project.settings(
)
lazy val b3 = project.dependsOn(a3).settings(
allowUnsafeScalaLibUpgrade := true, // b3 has 3.3.2, a3 brings 3.3.4; demote to warn so b3/run still passes
scalaVersion := "3.3.2", // 2.13.12 library
TaskKey[Unit]("checkScala") := {
val i = scalaInstance.value

View File

@ -1,4 +1,5 @@
ThisBuild / scalaVersion := "3-latest.candidate"
ThisBuild / allowUnsafeScalaLibUpgrade := true // dynamic scalaVersion (3-latest.candidate) vs resolved scala3-library_3; demote to warn
lazy val checkDynVersion = taskKey[Unit]("Check that scalaDynVersion resolves correctly")