Skip to content

Commit 77e053d

Browse files
committed
[sbt-dotty] use sbt loader as parent of scala instance loader
1 parent bf87cd1 commit 77e053d

File tree

2 files changed

+73
-5
lines changed

2 files changed

+73
-5
lines changed

project/Build.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,8 @@ object Build {
337337
scalaLibrary,
338338
dottyLibrary,
339339
dottyCompiler,
340-
allJars
340+
allJars,
341+
appConfiguration.value
341342
)
342343
},
343344
// sbt-dotty defines `scalaInstance in doc` so we need to override it manually

sbt-dotty/src/dotty/tools/sbtplugin/DottyPlugin.scala

+71-4
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ import sbt.librarymanagement.{
88
VersionNumber
99
}
1010
import sbt.internal.inc.ScalaInstance
11+
import sbt.internal.inc.classpath.ClassLoaderCache
1112
import xsbti.compile._
13+
import xsbti.AppConfiguration
1214
import java.net.URLClassLoader
1315
import java.util.Optional
16+
import java.util.{Enumeration, Collections}
17+
import java.net.URL
1418
import scala.util.Properties.isJavaAtLeast
1519

20+
1621
object DottyPlugin extends AutoPlugin {
1722
object autoImport {
1823
val isDotty = settingKey[Boolean]("Is this project compiled with Dotty?")
@@ -524,15 +529,34 @@ object DottyPlugin extends AutoPlugin {
524529
scalaLibraryJar,
525530
dottyLibraryJar,
526531
compilerJar,
527-
allJars
532+
allJars,
533+
appConfiguration.value
528534
)
529535
}
530536

531537
// Adapted from private mkScalaInstance in sbt
532538
def makeScalaInstance(
533-
state: State, dottyVersion: String, scalaLibrary: File, dottyLibrary: File, compiler: File, all: Seq[File]
539+
state: State, dottyVersion: String, scalaLibrary: File, dottyLibrary: File, compiler: File, all: Seq[File], appConfiguration: AppConfiguration
534540
): ScalaInstance = {
535-
val libraryLoader = state.classLoaderCache(List(dottyLibrary, scalaLibrary))
541+
/**
542+
* The compiler bridge must load the xsbti classes from the sbt
543+
* classloader, and similarly the Scala repl must load the sbt provided
544+
* jline terminal. To do so we add the `appConfiguration` loader in
545+
* the parent hierarchy of the scala 3 instance loader.
546+
*
547+
* The [[TopClassLoader]] ensures that the xsbti and jline classes
548+
* only are loaded from the sbt loader. That is necessary because
549+
* the sbt class loader contains the Scala 2.12 library and compiler
550+
* bridge.
551+
*/
552+
val topLoader = new TopClassLoader(appConfiguration.provider.loader)
553+
554+
val libraryJars = Array(dottyLibrary, scalaLibrary)
555+
val libraryLoader = state.classLoaderCache.cachedCustomClassloader(
556+
libraryJars.toList,
557+
() => new URLClassLoader(libraryJars.map(_.toURI.toURL), topLoader)
558+
)
559+
536560
class DottyLoader
537561
extends URLClassLoader(all.map(_.toURI.toURL).toArray, libraryLoader)
538562
val fullLoader = state.classLoaderCache.cachedCustomClassloader(
@@ -543,10 +567,53 @@ object DottyPlugin extends AutoPlugin {
543567
dottyVersion,
544568
fullLoader,
545569
libraryLoader,
546-
Array(dottyLibrary, scalaLibrary),
570+
libraryJars,
547571
compiler,
548572
all.toArray,
549573
None)
574+
}
575+
}
550576

577+
/**
578+
* The parent classloader of the Scala compiler.
579+
*
580+
* A TopClassLoader is constructed from the sbt classloader.
581+
*
582+
* To understand why a custom parent classloader is needed for the compiler,
583+
* let us describe some alternatives that wouldn't work.
584+
*
585+
* - `new URLClassLoader(urls)`:
586+
* The compiler contains sbt phases that callback to sbt using the `xsbti.*`
587+
* interfaces. If `urls` does not contain the sbt interfaces we'll get a
588+
* `ClassNotFoundException` in the compiler when we try to use them, if
589+
* `urls` does contain the interfaces we'll get a `ClassCastException` or a
590+
* `LinkageError` because if the same class is loaded by two different
591+
* classloaders, they are considered distinct by the JVM.
592+
*
593+
* - `new URLClassLoader(urls, sbtLoader)`:
594+
* Because of the JVM delegation model, this means that we will only load
595+
* a class from `urls` if it's not present in the parent `sbtLoader`, but
596+
* sbt uses its own version of the scala compiler and scala library which
597+
* is not the one we need to run the compiler.
598+
*
599+
* Our solution is to implement an URLClassLoader whose parent is
600+
* `new TopClassLoader(sbtLoader)`. We override `loadClass` to load the
601+
* `xsbti.*` interfaces from `sbtLoader`.
602+
*
603+
* The parent loader of the TopClassLoader is set to `null` so that the JDK
604+
* classes and only the JDK classes are loade from it.
605+
*/
606+
private class TopClassLoader(sbtLoader: ClassLoader) extends ClassLoader(null) {
607+
// We can't use the loadClass overload with two arguments because it's
608+
// protected, but we can do the same by hand (the classloader instance
609+
// from which we call resolveClass does not matter).
610+
// The one argument overload of loadClass delegates to this one.
611+
override protected def loadClass(name: String, resolve: Boolean): Class[_] = {
612+
if (name.startsWith("xsbti.") || name.startsWith("org.jline.")) {
613+
val c = sbtLoader.loadClass(name)
614+
if (resolve) resolveClass(c)
615+
c
616+
}
617+
else super.loadClass(name, resolve)
551618
}
552619
}

0 commit comments

Comments
 (0)