From f4eff2da772dd6085d138575848adeab037bbd0b Mon Sep 17 00:00:00 2001 From: bitloi Date: Mon, 16 Mar 2026 00:15:06 +0100 Subject: [PATCH 1/4] [2.x] fix: Eviction error for downgrade of Scala 3.x (#6694) **Problem** When scalaVersion is Scala 3.x and a dependency brings a newer scala3-library_3, sbt did not report an eviction error or warning. The compiler could be older than the standard library on the classpath, breaking compile-time alignment (e.g. scala/scala3#25406). **Solution** - In Compiler.scala, add an else if ScalaArtifacts.isScala3(sv) branch in scalaInstanceConfigFromUpdate that finds scala3-library_* on the Compile report and, if scalaVersion < that revision, fails (or warns when allowUnsafeScalaLibUpgrade := true) with the same pattern as the existing Scala 2.13 check (PR 7480). Message uses compile-time alignment wording and "See evicted to know why ... was upgraded from". - Set allowUnsafeScalaLibUpgrade := true on b3 in stdlib-unfreeze so existing b3/run and b3/checkScala still pass. - Add scripted tests: stdlib-unfreeze-scala3-eviction (expect compile to fail) and stdlib-unfreeze-scala3-warn (expect success with warning). Closes #6694 --- .../main/scala/sbt/internal/Compiler.scala | 33 +++++++++++++++++++ .../stdlib-unfreeze-scala3-eviction/build.sbt | 12 +++++++ .../high/H.scala | 1 + .../low/L.scala | 1 + .../stdlib-unfreeze-scala3-eviction/test | 2 ++ .../stdlib-unfreeze-scala3-warn/build.sbt | 11 +++++++ .../stdlib-unfreeze-scala3-warn/high/H.scala | 1 + .../stdlib-unfreeze-scala3-warn/low/L.scala | 1 + .../stdlib-unfreeze-scala3-warn/test | 3 ++ .../stdlib-unfreeze/build.sbt | 1 + 10 files changed, 66 insertions(+) create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/build.sbt create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/high/H.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/low/L.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/test create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/build.sbt create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/high/H.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/low/L.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/test diff --git a/main/src/main/scala/sbt/internal/Compiler.scala b/main/src/main/scala/sbt/internal/Compiler.scala index 93d1fc5c8..aefee7471 100644 --- a/main/src/main/scala/sbt/internal/Compiler.scala +++ b/main/src/main/scala/sbt/internal/Compiler.scala @@ -215,6 +215,39 @@ object Compiler: if err then sys.error(msg) else s.log.warn(msg) 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 + 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 maintain the compile-time alignment, the Scala compiler cannot be older than $libName on the dependency classpath. + | + |$fix + | + |See `$proj evicted` to know why $libName was upgraded from $sv to $libVer. + |""".stripMargin + if err then sys.error(msg) + else s.log.warn(msg) + else () else () def file(id: String): Option[File] = for diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/build.sbt b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/build.sbt new file mode 100644 index 000000000..32f1dd59e --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/build.sbt @@ -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 +) diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/high/H.scala b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/high/H.scala new file mode 100644 index 000000000..ddf3d0078 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/high/H.scala @@ -0,0 +1 @@ +object H { def id[A](a: A): A = a } diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/low/L.scala b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/low/L.scala new file mode 100644 index 000000000..271d731a9 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/low/L.scala @@ -0,0 +1 @@ +object L { def main(args: Array[String]): Unit = println(H.id(1)) } diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/test b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/test new file mode 100644 index 000000000..6700c4044 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-eviction/test @@ -0,0 +1,2 @@ +# Expect compile to fail: scalaVersion 3.3.2 < scala3-library_3 3.3.4 from high +-> low/compile diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/build.sbt b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/build.sbt new file mode 100644 index 000000000..29cb13495 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/build.sbt @@ -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", +) diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/high/H.scala b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/high/H.scala new file mode 100644 index 000000000..ddf3d0078 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/high/H.scala @@ -0,0 +1 @@ +object H { def id[A](a: A): A = a } diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/low/L.scala b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/low/L.scala new file mode 100644 index 000000000..271d731a9 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/low/L.scala @@ -0,0 +1 @@ +object L { def main(args: Array[String]): Unit = println(H.id(1)) } diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/test b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/test new file mode 100644 index 000000000..29571a432 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-scala3-warn/test @@ -0,0 +1,3 @@ +# With allowUnsafeScalaLibUpgrade := true, compile and run succeed (eviction is a warning only) +> low/compile +> low/run diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/build.sbt b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/build.sbt index ab04ff427..bf9ab5c55 100644 --- a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/build.sbt +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/build.sbt @@ -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 From 7ad38eb22113c11195b4ccc4ec72f332bbe205f4 Mon Sep 17 00:00:00 2001 From: bitloi Date: Mon, 16 Mar 2026 00:15:06 +0100 Subject: [PATCH 2/4] [2.x] fix: Eviction error for downgrade of Scala 3.x (#6694) **Problem** When scalaVersion is Scala 3.x and a dependency brings a newer scala3-library_3, sbt did not report an eviction error or warning. The compiler could be older than the standard library on the classpath, breaking compile-time alignment (e.g. scala/scala3#25406). **Solution** - In Compiler.scala, add an else if ScalaArtifacts.isScala3(sv) branch in scalaInstanceConfigFromUpdate that finds scala3-library_* on the Compile report and, if scalaVersion < that revision, fails (or warns when allowUnsafeScalaLibUpgrade := true) with the same pattern as the existing Scala 2.13 check (PR 7480). Message uses compile-time alignment wording and "See evicted to know why ... was upgraded from". - Set allowUnsafeScalaLibUpgrade := true on b3 in stdlib-unfreeze so existing b3/run and b3/checkScala still pass. - Add scripted tests: stdlib-unfreeze-scala3-eviction (expect compile to fail) and stdlib-unfreeze-scala3-warn (expect success with warning). Closes #6694 --- sbt-app/src/sbt-test/project/scala-dyn-version/build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/sbt-app/src/sbt-test/project/scala-dyn-version/build.sbt b/sbt-app/src/sbt-test/project/scala-dyn-version/build.sbt index cade63196..c734d84ae 100644 --- a/sbt-app/src/sbt-test/project/scala-dyn-version/build.sbt +++ b/sbt-app/src/sbt-test/project/scala-dyn-version/build.sbt @@ -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") From 7fc522da65b0760530004a654aea6f1eb32f202d Mon Sep 17 00:00:00 2001 From: bitloi Date: Mon, 16 Mar 2026 17:07:31 +0100 Subject: [PATCH 3/4] Refactor: extract reportScalaLibEviction for Scala 2.13 and 3 eviction messages --- .../main/scala/sbt/internal/Compiler.scala | 86 ++++++++++--------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/main/src/main/scala/sbt/internal/Compiler.scala b/main/src/main/scala/sbt/internal/Compiler.scala index aefee7471..bb77d7505 100644 --- a/main/src/main/scala/sbt/internal/Compiler.scala +++ b/main/src/main/scala/sbt/internal/Compiler.scala @@ -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,16 @@ 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 @@ -228,25 +246,15 @@ 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 maintain the compile-time alignment, the Scala compiler cannot be older than $libName on the dependency classpath. - | - |$fix - | - |See `$proj evicted` to know why $libName was upgraded from $sv to $libVer. - |""".stripMargin - if err then sys.error(msg) - else s.log.warn(msg) + 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] = From 4696b91a17d3b48bfd6453adde59d88149439903 Mon Sep 17 00:00:00 2001 From: bitloi Date: Mon, 16 Mar 2026 17:39:06 +0100 Subject: [PATCH 4/4] CI: allowUnsafeScalaLibUpgrade for serverTestProj (Scala 3 eviction) --- build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sbt b/build.sbt index 6eb17df62..4156937df 100644 --- a/build.sbt +++ b/build.sbt @@ -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" / **,