@@ -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?" )
@@ -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