@@ -8,11 +8,16 @@ import sbt.librarymanagement.{
88 VersionNumber
99}
1010import sbt .internal .inc .ScalaInstance
11+ import sbt .internal .inc .classpath .ClassLoaderCache
1112import xsbti .compile ._
13+ import xsbti .AppConfiguration
1214import java .net .URLClassLoader
1315import java .util .Optional
16+ import java .util .{Enumeration , Collections }
17+ import java .net .URL
1418import scala .util .Properties .isJavaAtLeast
1519
20+
1621object DottyPlugin extends AutoPlugin {
1722 object autoImport {
1823 val isDotty = settingKey[Boolean ](" Is this project compiled with Dotty?" )
@@ -521,15 +526,34 @@ object DottyPlugin extends AutoPlugin {
521526 scalaLibraryJar,
522527 dottyLibraryJar,
523528 compilerJar,
524- allJars
529+ allJars,
530+ appConfiguration.value
525531 )
526532 }
527533
528534 // Adapted from private mkScalaInstance in sbt
529535 def makeScalaInstance (
530- state : State , dottyVersion : String , scalaLibrary : File , dottyLibrary : File , compiler : File , all : Seq [File ]
536+ state : State , dottyVersion : String , scalaLibrary : File , dottyLibrary : File , compiler : File , all : Seq [File ], appConfiguration : AppConfiguration
531537 ): ScalaInstance = {
532- val libraryLoader = state.classLoaderCache(List (dottyLibrary, scalaLibrary))
538+ /**
539+ * The compiler bridge must load the xsbti classes from the sbt
540+ * classloader, and similarly the Scala repl must load the sbt provided
541+ * jline terminal. To do so we add the `appConfiguration` loader in
542+ * the parent hierarchy of the scala 3 instance loader.
543+ *
544+ * The [[TopClassLoader ]] ensures that the xsbti and jline classes
545+ * only are loaded from the sbt loader. That is necessary because
546+ * the sbt class loader contains the Scala 2.12 library and compiler
547+ * bridge.
548+ */
549+ val topLoader = new TopClassLoader (appConfiguration.provider.loader)
550+
551+ val libraryJars = Array (dottyLibrary, scalaLibrary)
552+ val libraryLoader = state.classLoaderCache.cachedCustomClassloader(
553+ libraryJars.toList,
554+ () => new URLClassLoader (libraryJars.map(_.toURI.toURL), topLoader)
555+ )
556+
533557 class DottyLoader
534558 extends URLClassLoader (all.map(_.toURI.toURL).toArray, libraryLoader)
535559 val fullLoader = state.classLoaderCache.cachedCustomClassloader(
@@ -540,10 +564,49 @@ object DottyPlugin extends AutoPlugin {
540564 dottyVersion,
541565 fullLoader,
542566 libraryLoader,
543- Array (dottyLibrary, scalaLibrary) ,
567+ libraryJars ,
544568 compiler,
545569 all.toArray,
546570 None )
571+ }
572+ }
547573
574+ /**
575+ * The parent classloader of the Scala compiler.
576+ *
577+ * A TopClassLoader is constructed from the sbt classloader.
578+ *
579+ * To understand why a custom parent classloader is needed for the compiler,
580+ * let us describe some alternatives that wouldn't work.
581+ *
582+ * - `new URLClassLoader(urls)`:
583+ * The compiler contains sbt phases that callback to sbt using the `xsbti.*`
584+ * interfaces. If `urls` does not contain the sbt interfaces we'll get a
585+ * `ClassNotFoundException` in the compiler when we try to use them, if
586+ * `urls` does contain the interfaces we'll get a `ClassCastException` or a
587+ * `LinkageError` because if the same class is loaded by two different
588+ * classloaders, they are considered distinct by the JVM.
589+ *
590+ * - `new URLClassLoader(urls, sbtLoader)`:
591+ * Because of the JVM delegation model, this means that we will only load
592+ * a class from `urls` if it's not present in the parent `sbtLoader`, but
593+ * sbt uses its own version of the scala compiler and scala library which
594+ * is not the one we need to run the compiler.
595+ *
596+ * Our solution is to implement an URLClassLoader whose parent is
597+ * `new TopClassLoader(sbtLoader)`. We override `loadClass` to load the
598+ * `xsbti.*` interfaces from `sbtLoader`.
599+ *
600+ * The parent loader of the TopClassLoader is set to `null` so that the JDK
601+ * classes and only the JDK classes are loade from it.
602+ */
603+ private class TopClassLoader (sbtLoader : ClassLoader ) extends ClassLoader (null ) {
604+ override protected def loadClass (name : String , resolve : Boolean ): Class [_] = {
605+ if (name.startsWith(" xsbti." ) || name.startsWith(" org.jline." )) {
606+ val c = sbtLoader.loadClass(name)
607+ if (resolve) resolveClass(c)
608+ c
609+ }
610+ else super .loadClass(name, resolve)
548611 }
549612}
0 commit comments