mirror of https://github.com/sbt/sbt.git
sbt 2.x allows `dependsOn(...)` between subprojects with mismatched Scala versions without any warning or error. This can lead to confusing classpath issues at compile or runtime, especially now that Scala 3.8+ has dropped backward TASTy compatibility with 2.13. Per review feedback, move the Scala version mismatch check from compileTask to projectDependenciesTask, where PR #8681 already handles Scala version mixing logic. This provides earlier detection and keeps the validation co-located with cross-version resolution. Generated-by: Copilot Co-authored-by: dev-miro26 <121471669+dev-miro26@users.noreply.github.com>
This commit is contained in:
parent
d4d5e72961
commit
bf33f51d10
|
|
@ -182,6 +182,7 @@ object Defaults extends BuildCommon {
|
|||
autoScalaLibrary :== true,
|
||||
managedScalaInstance :== true,
|
||||
allowUnsafeScalaLibUpgrade :== false,
|
||||
allowMismatchScala :== false,
|
||||
classpathEntryDefinesClass := Def.uncached { (file: File) =>
|
||||
sys.error("use classpathEntryDefinesClassVF instead")
|
||||
},
|
||||
|
|
@ -4180,10 +4181,12 @@ object Classpaths {
|
|||
|
||||
def projectDependenciesTask: Initialize[Task[Seq[ModuleID]]] =
|
||||
Def.task {
|
||||
val sv = scalaVersion.value
|
||||
val sbv = scalaBinaryVersion.value
|
||||
val ref = thisProjectRef.value
|
||||
val data = settingsData.value
|
||||
val deps = buildDependencies.value
|
||||
val allow = allowMismatchScala.value
|
||||
deps
|
||||
.classpath(ref)
|
||||
.flatMap: dep =>
|
||||
|
|
@ -4192,45 +4195,55 @@ object Classpaths {
|
|||
depSBV <- (dep.project / scalaBinaryVersion).get(data)
|
||||
depCross <- (dep.project / crossVersion).get(data)
|
||||
depAuto <- (dep.project / autoScalaLibrary).get(data)
|
||||
yield depCross match
|
||||
case b: CrossVersion.Binary
|
||||
if depAuto && VirtualAxis.isScala2Scala3Sandwich(sbv, depSBV) =>
|
||||
depProjId
|
||||
.withCrossVersion(CrossVersion.constant(b.prefix + depSBV))
|
||||
.withConfigurations(dep.configuration)
|
||||
.withExplicitArtifacts(Vector.empty)
|
||||
case b: CrossVersion.Binary if sbv != depSBV =>
|
||||
depProjId
|
||||
.withCrossVersion(CrossVersion.constant(b.prefix + depSBV + b.suffix))
|
||||
.withConfigurations(dep.configuration)
|
||||
.withExplicitArtifacts(Vector.empty)
|
||||
case f: CrossVersion.Full if sbv != depSBV =>
|
||||
val cross = (dep.project / scalaVersion)
|
||||
.get(data)
|
||||
.map(sv => CrossVersion.constant(f.prefix + sv + f.suffix))
|
||||
.getOrElse(depProjId.crossVersion)
|
||||
depProjId
|
||||
.withCrossVersion(cross)
|
||||
.withConfigurations(dep.configuration)
|
||||
.withExplicitArtifacts(Vector.empty)
|
||||
// For3Use2_13/For2_13Use3 publish under compat suffix (e.g. bar_2.13 on Scala 3),
|
||||
// not raw depSBV; sandwich case uses constant(depSBV) so would request wrong artifact.
|
||||
case c: sbt.librarymanagement.For3Use2_13 if sbv != depSBV =>
|
||||
val compat =
|
||||
if (depSBV == "3" || depSBV.startsWith("3.0.0")) "2.13"
|
||||
else depSBV
|
||||
depProjId
|
||||
.withCrossVersion(CrossVersion.constant(c.prefix + compat + c.suffix))
|
||||
.withConfigurations(dep.configuration)
|
||||
.withExplicitArtifacts(Vector.empty)
|
||||
case c: sbt.librarymanagement.For2_13Use3 if sbv != depSBV =>
|
||||
val compat = if (depSBV == "2.13") "3" else depSBV
|
||||
depProjId
|
||||
.withCrossVersion(CrossVersion.constant(c.prefix + compat + c.suffix))
|
||||
.withConfigurations(dep.configuration)
|
||||
.withExplicitArtifacts(Vector.empty)
|
||||
case _ =>
|
||||
depProjId.withConfigurations(dep.configuration).withExplicitArtifacts(Vector.empty)
|
||||
yield
|
||||
if !allow && sbv != depSBV then
|
||||
val depCp = (dep.project / crossPaths).get(data).getOrElse(true)
|
||||
if depCp then
|
||||
val depSv = (dep.project / scalaVersion).get(data).getOrElse("")
|
||||
if !ClasspathImpl.isAllowedScalaMismatch(sv, depSv) then
|
||||
sys.error(
|
||||
s"Scala version mismatch: ${ref.project} (Scala $sv) depends on ${dep.project.project} (Scala $depSv). " +
|
||||
s"To allow this, set `ThisProject / allowMismatchScala := true`"
|
||||
)
|
||||
depCross match
|
||||
case b: CrossVersion.Binary
|
||||
if depAuto && VirtualAxis.isScala2Scala3Sandwich(sbv, depSBV) =>
|
||||
depProjId
|
||||
.withCrossVersion(CrossVersion.constant(b.prefix + depSBV))
|
||||
.withConfigurations(dep.configuration)
|
||||
.withExplicitArtifacts(Vector.empty)
|
||||
case b: CrossVersion.Binary if sbv != depSBV =>
|
||||
depProjId
|
||||
.withCrossVersion(CrossVersion.constant(b.prefix + depSBV + b.suffix))
|
||||
.withConfigurations(dep.configuration)
|
||||
.withExplicitArtifacts(Vector.empty)
|
||||
case f: CrossVersion.Full if sbv != depSBV =>
|
||||
val cross = (dep.project / scalaVersion)
|
||||
.get(data)
|
||||
.map(sv => CrossVersion.constant(f.prefix + sv + f.suffix))
|
||||
.getOrElse(depProjId.crossVersion)
|
||||
depProjId
|
||||
.withCrossVersion(cross)
|
||||
.withConfigurations(dep.configuration)
|
||||
.withExplicitArtifacts(Vector.empty)
|
||||
// For3Use2_13/For2_13Use3 publish under compat suffix (e.g. bar_2.13 on Scala 3),
|
||||
// not raw depSBV; sandwich case uses constant(depSBV) so would request wrong artifact.
|
||||
case c: sbt.librarymanagement.For3Use2_13 if sbv != depSBV =>
|
||||
val compat =
|
||||
if (depSBV == "3" || depSBV.startsWith("3.0.0")) "2.13"
|
||||
else depSBV
|
||||
depProjId
|
||||
.withCrossVersion(CrossVersion.constant(c.prefix + compat + c.suffix))
|
||||
.withConfigurations(dep.configuration)
|
||||
.withExplicitArtifacts(Vector.empty)
|
||||
case c: sbt.librarymanagement.For2_13Use3 if sbv != depSBV =>
|
||||
val compat = if (depSBV == "2.13") "3" else depSBV
|
||||
depProjId
|
||||
.withCrossVersion(CrossVersion.constant(c.prefix + compat + c.suffix))
|
||||
.withConfigurations(dep.configuration)
|
||||
.withExplicitArtifacts(Vector.empty)
|
||||
case _ =>
|
||||
depProjId.withConfigurations(dep.configuration).withExplicitArtifacts(Vector.empty)
|
||||
}
|
||||
|
||||
private[sbt] def depMap: Initialize[Task[Map[ModuleRevisionId, ModuleDescriptor]]] =
|
||||
|
|
|
|||
|
|
@ -666,6 +666,7 @@ object Keys {
|
|||
val autoScalaLibrary = settingKey[Boolean]("Adds a dependency on scala-library if true.").withRank(ASetting)
|
||||
val managedScalaInstance = settingKey[Boolean]("Automatically obtains Scala tools as managed dependencies if true.").withRank(BSetting)
|
||||
val allowUnsafeScalaLibUpgrade = settingKey[Boolean]("Allow the Scala library on the compilation classpath to be newer than the scalaVersion (see Scala SIP-51).").withRank(CSetting)
|
||||
val allowMismatchScala = settingKey[Boolean]("When set to true, allow dependsOn to an arbitrary Scala version subproject without error.").withRank(CSetting)
|
||||
val sbtResolver = settingKey[Resolver]("Provides a resolver for obtaining sbt as a dependency.").withRank(BMinusSetting)
|
||||
val sbtResolvers = settingKey[Seq[Resolver]]("The external resolvers for sbt and plugin dependencies.").withRank(BMinusSetting)
|
||||
val sbtDependency = settingKey[ModuleID]("Provides a definition for declaring the current version of sbt.").withRank(BMinusSetting)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import sbt.nio.Keys.*
|
|||
import sbt.nio.file.{ Glob, RecursiveGlob }
|
||||
import sbt.Def.Initialize
|
||||
import sbt.internal.util.{ Attributed, Dag }
|
||||
import sbt.librarymanagement.{ Configuration, TrackLevel }
|
||||
import sbt.librarymanagement.{ Configuration, CrossVersion, TrackLevel }
|
||||
import sbt.librarymanagement.Configurations.names
|
||||
import sbt.SlashSyntax0.*
|
||||
import sbt.std.TaskExtra.*
|
||||
|
|
@ -309,6 +309,14 @@ private[sbt] object ClasspathImpl {
|
|||
): Task[Classpath] =
|
||||
getClasspath(unmanagedJars, dep, conf, data)
|
||||
|
||||
private[sbt] def isAllowedScalaMismatch(sv1: String, sv2: String): Boolean =
|
||||
val pv1 = CrossVersion.partialVersion(sv1)
|
||||
val pv2 = CrossVersion.partialVersion(sv2)
|
||||
(pv1, pv2) match
|
||||
case (Some((2, 13)), Some((3, minor))) => minor <= 7
|
||||
case (Some((3, minor)), Some((2, 13))) => minor <= 7
|
||||
case _ => false
|
||||
|
||||
def interDependencies[A](
|
||||
projectRef: ProjectRef,
|
||||
deps: BuildDependencies,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2023, Scala center
|
||||
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt
|
||||
package internal
|
||||
|
||||
import hedgehog.*
|
||||
import hedgehog.runner.*
|
||||
|
||||
object ClasspathImplTest extends Properties:
|
||||
override def tests: List[Test] = List(
|
||||
example(
|
||||
"isAllowedScalaMismatch: 2.13.x and 3.5.x is allowed",
|
||||
Result.assert(ClasspathImpl.isAllowedScalaMismatch("2.13.16", "3.5.1"))
|
||||
),
|
||||
example(
|
||||
"isAllowedScalaMismatch: 3.5.x and 2.13.x is allowed (reverse)",
|
||||
Result.assert(ClasspathImpl.isAllowedScalaMismatch("3.5.1", "2.13.16"))
|
||||
),
|
||||
example(
|
||||
"isAllowedScalaMismatch: 2.13.x and 3.0.0 is allowed",
|
||||
Result.assert(ClasspathImpl.isAllowedScalaMismatch("2.13.12", "3.0.0"))
|
||||
),
|
||||
example(
|
||||
"isAllowedScalaMismatch: 2.13.x and 3.7.0 is allowed",
|
||||
Result.assert(ClasspathImpl.isAllowedScalaMismatch("2.13.12", "3.7.0"))
|
||||
),
|
||||
example(
|
||||
"isAllowedScalaMismatch: 2.13.x and 3.8.0 is NOT allowed",
|
||||
Result.assert(!ClasspathImpl.isAllowedScalaMismatch("2.13.12", "3.8.0"))
|
||||
),
|
||||
example(
|
||||
"isAllowedScalaMismatch: 2.13.x and 3.8.1 is NOT allowed",
|
||||
Result.assert(!ClasspathImpl.isAllowedScalaMismatch("2.13.16", "3.8.1"))
|
||||
),
|
||||
example(
|
||||
"isAllowedScalaMismatch: 2.12.x and 3.5.x is NOT allowed",
|
||||
Result.assert(!ClasspathImpl.isAllowedScalaMismatch("2.12.21", "3.5.1"))
|
||||
),
|
||||
example(
|
||||
"isAllowedScalaMismatch: same versions is NOT a mismatch",
|
||||
Result.assert(!ClasspathImpl.isAllowedScalaMismatch("3.5.1", "3.5.1"))
|
||||
),
|
||||
example(
|
||||
"isAllowedScalaMismatch: 2.12.x and 2.13.x is NOT allowed",
|
||||
Result.assert(!ClasspathImpl.isAllowedScalaMismatch("2.12.21", "2.13.16"))
|
||||
),
|
||||
)
|
||||
end ClasspathImplTest
|
||||
|
|
@ -16,4 +16,5 @@ lazy val baz = project
|
|||
.settings(
|
||||
scalaVersion := "2.13.12",
|
||||
name := "baz",
|
||||
allowMismatchScala := true,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ lazy val b = project
|
|||
.settings(
|
||||
scalaVersion := "2.13.10",
|
||||
libraryDependencies += "org.scala-lang" % "scala-reflect" % "2.13.10",
|
||||
allowMismatchScala := true,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
lazy val scala3 = project
|
||||
.settings(
|
||||
scalaVersion := "3.5.1",
|
||||
)
|
||||
|
||||
lazy val scala213 = project
|
||||
.settings(
|
||||
scalaVersion := "2.13.16",
|
||||
)
|
||||
.dependsOn(scala3)
|
||||
|
||||
// Separate pair for testing allowMismatchScala override (avoids SIP-51 scala-library conflict)
|
||||
lazy val dep212 = project
|
||||
.settings(
|
||||
scalaVersion := "2.12.21",
|
||||
)
|
||||
|
||||
lazy val app213 = project
|
||||
.settings(
|
||||
scalaVersion := "2.13.16",
|
||||
allowMismatchScala := true,
|
||||
)
|
||||
.dependsOn(dep212)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# 2.13 depending on 3.5 (<=3.7) is allowed as a sandwich exception
|
||||
> scala213/compile
|
||||
|
||||
# Now change scala3 to 3.8.x to trigger the mismatch error
|
||||
> set scala3 / scalaVersion := "3.8.1"
|
||||
-> scala213/compile
|
||||
|
||||
# allowMismatchScala := true bypasses the check (using 2.13/2.12 pair to avoid SIP-51)
|
||||
> app213/compile
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
lazy val a = project.dependsOn(b)
|
||||
.settings(scalaVersion := "2.9.3")
|
||||
.settings(scalaVersion := "2.9.3", allowMismatchScala := true)
|
||||
lazy val b = RootProject(uri("b"))
|
||||
lazy val check = taskKey[Unit]("Checks the configured scalaBinaryVersion")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue