mirror of https://github.com/sbt/sbt.git
[2.x] fix: Make consoleProject bindings non-null in Scala 3
**Problem** After #9073, `consoleProject` starts a Scala 3 REPL successfully, but `currentState`, `extracted`, and `cpHelpers` show as `null` in the REPL. Reviewer eed3si9n reported this on the PR. Root cause: the Scala 3 REPL's `dotty.tools.repl.AbstractFileClassLoader` ends up defining a fresh copy of `sbt.internal.ConsoleProjectBindings` (and the other sbt classes referenced by the bindings) from the sbt module jars on `unit.classpath`, instead of delegating to sbt's own class loader via parent-first delegation. The two `Class` objects are distinct, so setting static fields on sbt's copy has no effect on the one the REPL compiles against. A strongly typed cast would additionally hit `LinkageError: loader constraint violation` on `sbt.State`. **Solution** Strip every jar shipping sbt core code from the runtime class loader used by the `consoleProject` REPL. The jars are detected by content — any jar that contains `sbt/State.class`, `sbt/Extracted.class`, `sbt/internal/ConsoleProjectBindings$.class`, or `sbt/internal/Load$.class` is considered an sbt module jar and filtered out. The full classpath is still passed to `Console` below, so the REPL's compile-time classpath (from the `-classpath` flag) is unchanged and the DSL types typecheck as before. Only the runtime URLClassLoader is affected, forcing `sbt.*` class references inside the REPL to resolve via parent delegation and reach sbt's own singleton copies. **Test** Update `project/scala3-console-project` to write a marker file from the REPL's `initialCommands` via `Files.writeString(path, currentState.toString.length.toString + ...)`. If any binding is null, the `toString` throws an NPE, the REPL swallows it, the marker is never written, and `$ exists` fails. The test now verifies the REPL actually sees non-null bindings, not just that `consoleProject` starts successfully.
This commit is contained in:
parent
1285562c76
commit
385c5c7578
|
|
@ -98,7 +98,19 @@ object ConsoleProject:
|
|||
val bindingImports = BuildUtil.importAll(bindings.map(_._1))
|
||||
val allLines = baseImports ++ bindingDefs ++ bindingImports
|
||||
val initCommands = allLines.mkString("", ";\n", ";\n\n") + extra
|
||||
val loader = ClasspathUtil.makeLoader(unit.classpath, si, tempDir)
|
||||
// Remove sbt's own module jars from the runtime loader's URL list so
|
||||
// that `sbt.*` classes (including `sbt.State`, `sbt.Extracted` and
|
||||
// `sbt.internal.ConsoleProjectBindings`) can only be resolved via
|
||||
// parent delegation, reaching sbt's own class loader. Without this,
|
||||
// the Scala 3 REPL's `AbstractFileClassLoader` would define a fresh
|
||||
// copy of each sbt class from `unit.classpath`, and any attempt to
|
||||
// use the bindings would trigger
|
||||
// `LinkageError: loader constraint violation` — two different JVM
|
||||
// `Class` objects for the same `sbt.State`. The full classpath is
|
||||
// still passed to `Console` below so the REPL's compile-time
|
||||
// classpath is unchanged. See sbt/sbt#7722.
|
||||
val runtimeClasspath = unit.classpath.filterNot(isSbtModuleJar)
|
||||
val loader = ClasspathUtil.makeLoader(runtimeClasspath, si, tempDir)
|
||||
val terminal = Terminal.get
|
||||
// TODO - Hook up dsl classpath correctly...
|
||||
try
|
||||
|
|
@ -113,6 +125,38 @@ object ConsoleProject:
|
|||
finally ConsoleProjectBindings.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Classes that identify an sbt module jar — if any of these entries is
|
||||
* present, the jar ships sbt core code that must be excluded from the
|
||||
* consoleProject runtime classloader. See `isSbtModuleJar`.
|
||||
*/
|
||||
private val SbtModuleMarkerClasses: Seq[String] = Seq(
|
||||
"sbt/State.class",
|
||||
"sbt/Extracted.class",
|
||||
"sbt/internal/ConsoleProjectBindings$.class",
|
||||
"sbt/internal/Load$.class",
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns true when a `Path` refers to a jar that ships sbt core code
|
||||
* (anything containing `sbt.State`, `sbt.Extracted`, etc.). These jars
|
||||
* must be excluded from the `consoleProject` REPL runtime classloader
|
||||
* so that `sbt.*` references resolve via parent delegation and reach
|
||||
* sbt's own singleton copies — rather than being defined fresh by the
|
||||
* Scala 3 REPL's `AbstractFileClassLoader`, which would break the
|
||||
* static-field bindings and trigger a `LinkageError: loader
|
||||
* constraint violation` when the bindings are used. See sbt/sbt#7722.
|
||||
*/
|
||||
private def isSbtModuleJar(p: java.nio.file.Path): Boolean =
|
||||
val name = p.getFileName.toString
|
||||
if !name.endsWith(".jar") || !java.nio.file.Files.isRegularFile(p) then false
|
||||
else
|
||||
try
|
||||
val zf = new java.util.zip.ZipFile(p.toFile)
|
||||
try SbtModuleMarkerClasses.exists(zf.getEntry(_) ne null)
|
||||
finally zf.close()
|
||||
catch case _: java.io.IOException => false
|
||||
|
||||
/** Conveniences for consoleProject that shouldn't normally be used for builds. */
|
||||
final class Imports private[sbt] (extracted: Extracted, state: State) {
|
||||
import extracted.*
|
||||
|
|
|
|||
|
|
@ -15,12 +15,14 @@ import scala.compiletime.uninitialized
|
|||
* Static holder for consoleProject bindings.
|
||||
*
|
||||
* Scala 3 compiler bridge does not implement REPL binding injection
|
||||
* (https://github.com/scala/scala3/issues/5069), so we store the runtime
|
||||
* objects here and generate `val` definitions in `initialCommands` that
|
||||
* read from this holder.
|
||||
* (https://github.com/scala/scala3/issues/5069), so sbt generates `val`
|
||||
* definitions in `initialCommands` that read from this holder.
|
||||
*
|
||||
* Must NOT be `private[sbt]` because the Scala 3 REPL compiles
|
||||
* `initialCommands` outside the `sbt` package.
|
||||
* The holder has to be resolved to the same JVM `Class` object from both
|
||||
* sbt itself and the Scala 3 REPL's `AbstractFileClassLoader`. sbt's
|
||||
* own sbt module jars are removed from the REPL runtime classloader in
|
||||
* `ConsoleProject.apply`, so that all `sbt.*` references in the REPL go
|
||||
* through the parent chain and reach the sbt singleton.
|
||||
*/
|
||||
object ConsoleProjectBindings:
|
||||
@volatile private var _state: State = uninitialized
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
ThisBuild / scalaVersion := "3.7.4"
|
||||
|
||||
lazy val markerFile = settingKey[java.io.File]("marker file written by consoleProject REPL when bindings resolve")
|
||||
|
||||
lazy val root = project.in(file(".")).settings(
|
||||
// Verify that consoleProject bindings are accessible in the Scala 3 REPL.
|
||||
// These assertions run as initialCommands; if any binding is null, the REPL fails.
|
||||
consoleProject / initialCommands :=
|
||||
"""assert(currentState != null, "currentState binding missing")
|
||||
|assert(extracted != null, "extracted binding missing")
|
||||
|assert(cpHelpers != null, "cpHelpers binding missing")
|
||||
|""".stripMargin,
|
||||
markerFile := target.value / "console-bindings-ok",
|
||||
Global / initialCommands := {
|
||||
val path = markerFile.value.getAbsolutePath.replace("\\", "\\\\")
|
||||
s"""_root_.java.nio.file.Files.writeString(_root_.java.nio.file.Paths.get("$path"), currentState.toString.length.toString + "/" + extracted.toString.length.toString + "/" + cpHelpers.toString.length.toString)
|
||||
|""".stripMargin
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
> consoleProject
|
||||
$ exists target/out/**/console-bindings-ok
|
||||
|
|
|
|||
Loading…
Reference in New Issue