[1.x] Add Scala 3.8 REPL support

**Problem**
Scala 3.8 REPL won't work since they've split the repl artifact into another JAR.

**Solution**
This works around it by creating a yet-another sandbox configuration ScalaReplTool
(similar to ScalaTool and ScalaDocTool) and a separate scalaInstance for
console task, so when Zinc is invoked we'll be able to conjure the right array of JARs.
This commit is contained in:
Eugene Yokota 2025-11-01 20:50:01 -04:00
parent ed7da85ef0
commit 840b851445
4 changed files with 206 additions and 177 deletions

View File

@ -11,7 +11,7 @@ import scala.util.Try
// ThisBuild settings take lower precedence,
// but can be shared across the multi projects.
ThisBuild / version := {
val v = "1.11.8-SNAPSHOT"
val v = "1.12.0-SNAPSHOT"
nightlyVersion.getOrElse(v)
}
ThisBuild / version2_13 := "2.0.0-SNAPSHOT"

View File

@ -671,89 +671,94 @@ object Defaults extends BuildCommon {
)
// This is included into JvmPlugin.projectSettings
def compileBase = inTask(console)(compilersSetting :: Nil) ++ compileBaseGlobal ++ Seq(
useScalaReplJLine :== false,
scalaInstanceTopLoader := {
val topLoader = if (!useScalaReplJLine.value) {
// the JLineLoader contains the SbtInterfaceClassLoader
classOf[org.jline.terminal.Terminal].getClassLoader
} else classOf[Compilers].getClassLoader // the SbtInterfaceClassLoader
def compileBase =
inTask(console)(
Seq(
scalaInstance := Compiler.scalaInstanceTask(Some(Configurations.ScalaReplTool)).value,
) ++ compilersSetting
) ++ compileBaseGlobal ++ Seq(
useScalaReplJLine :== false,
scalaInstanceTopLoader := {
val topLoader = if (!useScalaReplJLine.value) {
// the JLineLoader contains the SbtInterfaceClassLoader
classOf[org.jline.terminal.Terminal].getClassLoader
} else classOf[Compilers].getClassLoader // the SbtInterfaceClassLoader
// Scala 2.10 shades jline in the console so we need to make sure that it loads a compatible
// jansi version. Because of the shading, console does not work with the thin client for 2.10.x.
if (scalaVersion.value.startsWith("2.10.")) new ClassLoader(topLoader) {
override protected def loadClass(name: String, resolve: Boolean): Class[_] = {
if (name.startsWith("org.fusesource")) throw new ClassNotFoundException(name)
super.loadClass(name, resolve)
// Scala 2.10 shades jline in the console so we need to make sure that it loads a compatible
// jansi version. Because of the shading, console does not work with the thin client for 2.10.x.
if (scalaVersion.value.startsWith("2.10.")) new ClassLoader(topLoader) {
override protected def loadClass(name: String, resolve: Boolean): Class[_] = {
if (name.startsWith("org.fusesource")) throw new ClassNotFoundException(name)
super.loadClass(name, resolve)
}
}
}
else topLoader
},
scalaInstance := Compiler.scalaInstanceTask.value,
crossVersion := (if (crossPaths.value) CrossVersion.binary else CrossVersion.disabled),
pluginCrossBuild / sbtBinaryVersion := binarySbtVersion(
(pluginCrossBuild / sbtVersion).value
),
// Use (sbtVersion in pluginCrossBuild) to pick the sbt module to depend from the plugin.
// Because `sbtVersion in pluginCrossBuild` can be scoped to project level,
// this setting needs to be set here too.
pluginCrossBuild / sbtDependency := {
val app = appConfiguration.value
val id = app.provider.id
val sv = (pluginCrossBuild / sbtVersion).value
val scalaV = (pluginCrossBuild / scalaVersion).value
val binVersion = (pluginCrossBuild / scalaBinaryVersion).value
val cross = id.crossVersionedValue match {
case CrossValue.Disabled => Disabled()
case CrossValue.Full => CrossVersion.full
case CrossValue.Binary => CrossVersion.binary
}
val base = ModuleID(id.groupID, id.name, sv).withCrossVersion(cross)
CrossVersion(scalaV, binVersion)(base).withCrossVersion(Disabled())
},
crossSbtVersions := Vector((pluginCrossBuild / sbtVersion).value),
crossTarget := makeCrossTarget(
target.value,
scalaVersion.value,
scalaBinaryVersion.value,
(pluginCrossBuild / sbtBinaryVersion).value,
sbtPlugin.value,
crossPaths.value
),
cleanIvy := IvyActions.cleanCachedResolutionCache(ivyModule.value, streams.value.log),
clean := {
val _ = cleanIvy.value
try {
val store = AnalysisUtil.staticCachedStore(
analysisFile = (Compile / compileAnalysisFile).value.toPath,
useTextAnalysis = !(Compile / enableBinaryCompileAnalysis).value,
useConsistent = (Compile / enableConsistentCompileAnalysis).value,
)
store.clearCache()
} catch {
case NonFatal(_) => ()
}
clean.value
},
scalaCompilerBridgeBinaryJar := Def.settingDyn {
val sv = scalaVersion.value
if (ScalaArtifacts.isScala3(sv) || VersionNumber(sv)
.matchesSemVer(SemanticSelector(s"=2.13 >=${ZincLmUtil.scala2SbtBridgeStart}")))
fetchBridgeBinaryJarTask(sv)
else Def.task[Option[File]](None)
}.value,
scalaCompilerBridgeSource := ZincLmUtil.getDefaultBridgeSourceModule(scalaVersion.value),
auxiliaryClassFiles ++= {
if (ScalaArtifacts.isScala3(scalaVersion.value)) List(TastyFiles.instance)
else Nil
},
consoleProject / scalaCompilerBridgeBinaryJar := None,
consoleProject / scalaCompilerBridgeSource := ZincLmUtil.getDefaultBridgeSourceModule(
appConfiguration.value.provider.scalaProvider.version
),
classpathOptions := ClasspathOptionsUtil.noboot(scalaVersion.value),
console / classpathOptions := ClasspathOptionsUtil.replNoboot(scalaVersion.value),
)
else topLoader
},
scalaInstance := Compiler.scalaInstanceTask(None).value,
crossVersion := (if (crossPaths.value) CrossVersion.binary else CrossVersion.disabled),
pluginCrossBuild / sbtBinaryVersion := binarySbtVersion(
(pluginCrossBuild / sbtVersion).value
),
// Use (sbtVersion in pluginCrossBuild) to pick the sbt module to depend from the plugin.
// Because `sbtVersion in pluginCrossBuild` can be scoped to project level,
// this setting needs to be set here too.
pluginCrossBuild / sbtDependency := {
val app = appConfiguration.value
val id = app.provider.id
val sv = (pluginCrossBuild / sbtVersion).value
val scalaV = (pluginCrossBuild / scalaVersion).value
val binVersion = (pluginCrossBuild / scalaBinaryVersion).value
val cross = id.crossVersionedValue match {
case CrossValue.Disabled => Disabled()
case CrossValue.Full => CrossVersion.full
case CrossValue.Binary => CrossVersion.binary
}
val base = ModuleID(id.groupID, id.name, sv).withCrossVersion(cross)
CrossVersion(scalaV, binVersion)(base).withCrossVersion(Disabled())
},
crossSbtVersions := Vector((pluginCrossBuild / sbtVersion).value),
crossTarget := makeCrossTarget(
target.value,
scalaVersion.value,
scalaBinaryVersion.value,
(pluginCrossBuild / sbtBinaryVersion).value,
sbtPlugin.value,
crossPaths.value
),
cleanIvy := IvyActions.cleanCachedResolutionCache(ivyModule.value, streams.value.log),
clean := {
val _ = cleanIvy.value
try {
val store = AnalysisUtil.staticCachedStore(
analysisFile = (Compile / compileAnalysisFile).value.toPath,
useTextAnalysis = !(Compile / enableBinaryCompileAnalysis).value,
useConsistent = (Compile / enableConsistentCompileAnalysis).value,
)
store.clearCache()
} catch {
case NonFatal(_) => ()
}
clean.value
},
scalaCompilerBridgeBinaryJar := Def.settingDyn {
val sv = scalaVersion.value
if (ScalaArtifacts.isScala3(sv) || VersionNumber(sv)
.matchesSemVer(SemanticSelector(s"=2.13 >=${ZincLmUtil.scala2SbtBridgeStart}")))
fetchBridgeBinaryJarTask(sv)
else Def.task[Option[File]](None)
}.value,
scalaCompilerBridgeSource := ZincLmUtil.getDefaultBridgeSourceModule(scalaVersion.value),
auxiliaryClassFiles ++= {
if (ScalaArtifacts.isScala3(scalaVersion.value)) List(TastyFiles.instance)
else Nil
},
consoleProject / scalaCompilerBridgeBinaryJar := None,
consoleProject / scalaCompilerBridgeSource := ZincLmUtil.getDefaultBridgeSourceModule(
appConfiguration.value.provider.scalaProvider.version
),
classpathOptions := ClasspathOptionsUtil.noboot(scalaVersion.value),
console / classpathOptions := ClasspathOptionsUtil.replNoboot(scalaVersion.value),
)
// must be a val: duplication detected by object identity
private[this] lazy val compileBaseGlobal: Seq[Setting[_]] = globalDefaults(
Seq(
@ -1140,7 +1145,7 @@ object Defaults extends BuildCommon {
}
@deprecated("Use Compiler.scalaInstanceTask", "1.12.0")
def scalaInstanceTask: Initialize[Task[ScalaInstance]] = Compiler.scalaInstanceTask
def scalaInstanceTask: Initialize[Task[ScalaInstance]] = Compiler.scalaInstanceTask(None)
// Returns the ScalaInstance only if it was not constructed via `update`
// This is necessary to prevent cycles between `update` and `scalaInstance`
@ -1149,8 +1154,9 @@ object Defaults extends BuildCommon {
if (scalaHome.value.isDefined) Def.task(Some(scalaInstance.value)) else Def.task(None)
}
@deprecated("Use Compiler.scalaInstanceFromUpdate", "1.12.0")
def scalaInstanceFromUpdate: Initialize[Task[ScalaInstance]] =
Compiler.scalaInstanceFromUpdate
Compiler.scalaInstanceFromUpdate(None)
def makeScalaInstance(
version: String,
@ -2062,6 +2068,7 @@ object Defaults extends BuildCommon {
def docTaskSettings(key: TaskKey[File] = doc): Seq[Setting[_]] =
inTask(key)(
Seq(
scalaInstance := Compiler.scalaInstanceTask(Some(Configurations.ScalaDocTool)).value,
apiMappings ++= {
val dependencyCp = dependencyClasspath.value
val log = streams.value.log
@ -2139,7 +2146,7 @@ object Defaults extends BuildCommon {
}
out
}
)
) ++ compilersSetting
)
def mainBgRunTask = mainBgRunTaskForConfig(Select(Runtime))
@ -3189,7 +3196,7 @@ object Classpaths {
ivyConfigurations ++= Configurations.auxiliary,
ivyConfigurations ++= {
if (managedScalaInstance.value && scalaHome.value.isEmpty)
Configurations.ScalaTool :: Configurations.ScalaDocTool :: Nil
Configurations.ScalaTool :: Configurations.ScalaDocTool :: Configurations.ScalaReplTool :: Nil
else Nil
},
// Coursier needs these
@ -3385,17 +3392,18 @@ object Classpaths {
val pluginAdjust =
if (isPlugin) sbtdeps +: base
else base
val sbtOrg = scalaOrganization.value
val scalaOrg = scalaOrganization.value
val version = scalaVersion.value
val extResolvers = externalResolvers.value
val isScala3M123 = ScalaArtifacts.isScala3M123(version)
val allToolDeps =
if (scalaHome.value.isDefined || scalaModuleInfo.value.isEmpty || !managedScalaInstance.value)
Nil
else if (!isScala3M123 || extResolvers.contains(Resolver.JCenterRepository)) {
ScalaArtifacts.toolDependencies(sbtOrg, version) ++
ScalaArtifacts.docToolDependencies(sbtOrg, version)
} else ScalaArtifacts.toolDependencies(sbtOrg, version)
else if (isScala3M123)
ScalaArtifacts.toolDependencies(scalaOrg, version)
else
ScalaArtifacts.toolDependencies(scalaOrg, version) ++
ScalaArtifacts.docToolDependencies(scalaOrg, version) ++
ScalaArtifacts.replToolDependencies(scalaOrg, version)
allToolDeps ++ pluginAdjust
},
// in case of meta build, exclude all sbt modules from the dependency graph, so we can use the sbt resolved by the launcher

View File

@ -1,6 +1,7 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
@ -12,16 +13,23 @@ import java.io.File
import sbt.internal.inc.ScalaInstance
import sbt.librarymanagement.{
Artifact,
Configuration,
Configurations,
ConfigurationReport,
ScalaArtifacts,
SemanticSelector,
UpdateReport,
VersionNumber
}
import xsbti.ScalaProvider
private[sbt] object Compiler {
def scalaInstanceTask: Def.Initialize[Task[ScalaInstance]] =
/**
* Returns a ScalaInstance.
* extraToolConf is used for Scala 3 since it started splitting up scaladoc and repl.
*/
def scalaInstanceTask(extraToolConf: Option[Configuration]): Def.Initialize[Task[ScalaInstance]] =
Def.taskDyn {
val sh = Keys.scalaHome.value
val app = Keys.appConfiguration.value
@ -35,7 +43,7 @@ private[sbt] object Compiler {
val scalaProvider = app.provider.scalaProvider
if (!managed) emptyScalaInstance
else if (sv == scalaProvider.version) optimizedScalaInstance(sv, scalaProvider)
else scalaInstanceFromUpdate
else scalaInstanceFromUpdate(extraToolConf)
}
}
@ -95,60 +103,49 @@ private[sbt] object Compiler {
)
}
def scalaInstanceFromUpdate: Def.Initialize[Task[ScalaInstance]] = Def.task {
val sv = Keys.scalaVersion.value
val fullReport = Keys.update.value
val s = Keys.streams.value
/**
* Returns a ScalaInstance.
* extraToolConf is used for Scala 3 since it started splitting up scaladoc and repl.
*/
def scalaInstanceFromUpdate(
extraToolConf: Option[Configuration]
): Def.Initialize[Task[ScalaInstance]] =
Def.task {
val sv = Keys.scalaVersion.value
val fullReport = Keys.update.value
val s = Keys.streams.value
// 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.
def updateLibraryToCompileConfiguration(report: ConfigurationReport) =
if (!ScalaArtifacts.isScala3(sv)) report
else
(for {
compileConf <- fullReport.configuration(Configurations.Compile)
compileLibMod <- compileConf.modules.find(_.module.name == ScalaArtifacts.LibraryID)
reportLibMod <- report.modules.find(_.module.name == ScalaArtifacts.LibraryID)
if VersionNumber(reportLibMod.module.revision)
.matchesSemVer(SemanticSelector(s"<${compileLibMod.module.revision}"))
} yield {
val newMods = report.modules
.filterNot(_.module.name == ScalaArtifacts.LibraryID) :+ compileLibMod
report.withModules(newMods)
}).getOrElse(report)
val toolReport = updateLibraryToCompileConfiguration(sv, fullReport)(
fullReport
.configuration(Configurations.ScalaTool)
.getOrElse(sys.error(noToolConfiguration(Keys.managedScalaInstance.value)))
)
val toolReport = updateLibraryToCompileConfiguration(
fullReport
.configuration(Configurations.ScalaTool)
.getOrElse(sys.error(noToolConfiguration(Keys.managedScalaInstance.value)))
)
if (Classpaths.isScala213(sv)) {
val scalaDeps = for {
compileReport <- fullReport.configuration(Configurations.Compile).iterator
libName <- ScalaArtifacts.Artifacts.iterator
lib <- compileReport.modules.find(_.module.name == libName)
} yield lib
for (lib <- scalaDeps.take(1)) {
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"))) {
val err = !Keys.allowUnsafeScalaLibUpgrade.value
val fix =
if (err)
"""Upgrade the `scalaVersion` to fix the build. If upgrading the Scala compiler version is
if (Classpaths.isScala213(sv)) {
val scalaDeps = for {
compileReport <- fullReport.configuration(Configurations.Compile).iterator
libName <- ScalaArtifacts.Artifacts.iterator
lib <- compileReport.modules.find(_.module.name == libName)
} yield lib
for (lib <- scalaDeps.take(1)) {
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"))) {
val err = !Keys.allowUnsafeScalaLibUpgrade.value
val fix =
if (err)
"""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
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.
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.
|
@ -156,60 +153,84 @@ private[sbt] object Compiler {
|
|See `$proj evicted` to know why $libName $libVer is getting pulled in.
|""".stripMargin
if (err) sys.error(msg)
else s.log.warn(msg)
if (err) sys.error(msg)
else s.log.warn(msg)
}
}
}
}
def file(id: String): File = {
val files = for {
m <- toolReport.modules if m.module.name.startsWith(id)
(art, file) <- m.artifacts if art.`type` == Artifact.DefaultType
} yield file
files.headOption getOrElse sys.error(s"Missing $id jar file")
def file(id: String): File = {
val files = for {
m <- toolReport.modules if m.module.name.startsWith(id)
(art, file) <- m.artifacts if art.`type` == Artifact.DefaultType
} yield file
files.headOption getOrElse sys.error(s"Missing $id jar file")
}
val allCompilerJars = toolReport.modules.flatMap(_.artifacts.map(_._2))
val extraToolJars =
extraToolConf match {
case Some(extra) =>
fullReport
.configuration(extra)
.map(updateLibraryToCompileConfiguration(sv, fullReport))
.toSeq
.flatMap(_.modules)
.flatMap(_.artifacts.map(_._2))
case None => Nil
}
val libraryJars = ScalaArtifacts.libraryIds(sv).map(file)
makeScalaInstance(
sv,
libraryJars,
allCompilerJars,
extraToolJars,
Keys.state.value,
Keys.scalaInstanceTopLoader.value,
)
}
val allCompilerJars = toolReport.modules.flatMap(_.artifacts.map(_._2))
val allDocJars =
fullReport
.configuration(Configurations.ScalaDocTool)
.map(updateLibraryToCompileConfiguration)
.toSeq
.flatMap(_.modules)
.flatMap(_.artifacts.map(_._2))
val libraryJars = ScalaArtifacts.libraryIds(sv).map(file)
makeScalaInstance(
sv,
libraryJars,
allCompilerJars,
allDocJars,
Keys.state.value,
Keys.scalaInstanceTopLoader.value,
)
}
// 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.
private def updateLibraryToCompileConfiguration(sv: String, fullReport: UpdateReport)(
report: ConfigurationReport
) =
if (!ScalaArtifacts.isScala3(sv)) report
else
(for {
compileConf <- fullReport.configuration(Configurations.Compile)
compileLibMod <- compileConf.modules.find(_.module.name == ScalaArtifacts.LibraryID)
reportLibMod <- report.modules.find(_.module.name == ScalaArtifacts.LibraryID)
if VersionNumber(reportLibMod.module.revision)
.matchesSemVer(SemanticSelector(s"<${compileLibMod.module.revision}"))
} yield {
val newMods = report.modules
.filterNot(_.module.name == ScalaArtifacts.LibraryID) :+ compileLibMod
report.withModules(newMods)
}).getOrElse(report)
def makeScalaInstance(
version: String,
libraryJars: Array[File],
allCompilerJars: Seq[File],
allDocJars: Seq[File],
extraToolJars: Seq[File],
state: State,
topLoader: ClassLoader,
): ScalaInstance = {
val classLoaderCache = state.extendedClassLoaderCache
val compilerJars = allCompilerJars.filterNot(libraryJars.contains).distinct.toArray
val docJars = allDocJars
val toolJars = extraToolJars
.filterNot(jar => libraryJars.contains(jar) || compilerJars.contains(jar))
.distinct
.toArray
val allJars = libraryJars ++ compilerJars ++ docJars
val allJars = libraryJars ++ compilerJars ++ toolJars
val libraryLoader = classLoaderCache(libraryJars.toList, topLoader)
val compilerLoader = classLoaderCache(compilerJars.toList, libraryLoader)
val fullLoader =
if (docJars.isEmpty) compilerLoader
else classLoaderCache(docJars.distinct.toList, compilerLoader)
if (toolJars.isEmpty) compilerLoader
else classLoaderCache(toolJars.distinct.toList, compilerLoader)
new ScalaInstance(
version = version,
loader = fullLoader,

View File

@ -14,8 +14,8 @@ object Dependencies {
// sbt modules
private val ioVersion = nightlyVersion.getOrElse("1.10.5")
private val lmVersion =
sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.11.6")
val zincVersion = nightlyVersion.getOrElse("1.11.0")
sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.12.0-M1")
val zincVersion = nightlyVersion.getOrElse("1.12.0-M1")
private val sbtIO = "org.scala-sbt" %% "io" % ioVersion