Skip to content

Commit c93f13f

Browse files
committed
Fallback on CompilerClassLoader
1 parent 70ef66a commit c93f13f

File tree

2 files changed

+159
-2
lines changed

2 files changed

+159
-2
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package xsbt;
2+
3+
import java.lang.reflect.Field;
4+
5+
import java.net.URL;
6+
import java.net.URLClassLoader;
7+
8+
import java.util.WeakHashMap;
9+
10+
/**
11+
* A classloader to run the compiler
12+
* <p>
13+
* A CompilerClassLoader is constructed from a list of `urls` that need to be on
14+
* the classpath to run the compiler and the classloader used by sbt.
15+
* <p>
16+
* To understand why a custom classloader is needed for the compiler, let us
17+
* describe some alternatives that wouldn't work.
18+
* <ul>
19+
* <li>`new URLClassLoader(urls)`:
20+
* The compiler contains sbt phases that callback to sbt using the `xsbti.*`
21+
* interfaces. If `urls` does not contain the sbt interfaces we'll get a
22+
* `ClassNotFoundException` in the compiler when we try to use them, if
23+
* `urls` does contain the interfaces we'll get a `ClassCastException` or a
24+
* `LinkageError` because if the same class is loaded by two different
25+
* classloaders, they are considered distinct by the JVM.
26+
* <li>`new URLClassLoader(urls, sbtLoader)`:
27+
* Because of the JVM delegation model, this means that we will only load
28+
* a class from `urls` if it's not present in the parent `sbtLoader`, but
29+
* sbt uses its own version of the scala compiler and scala library which
30+
* is not the one we need to run the compiler.
31+
* </ul>
32+
* <p>
33+
* Our solution is to implement a subclass of URLClassLoader with no parent, instead
34+
* we override `loadClass` to load the `xsbti.*` interfaces from `sbtLoader`.
35+
*/
36+
public class CompilerClassLoader extends URLClassLoader {
37+
private final ClassLoader sbtLoader;
38+
39+
public CompilerClassLoader(URL[] urls, ClassLoader sbtLoader) {
40+
super(urls, null);
41+
this.sbtLoader = sbtLoader;
42+
}
43+
44+
@Override
45+
public Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
46+
if (className.startsWith("xsbti.")) {
47+
// We can't use the loadClass overload with two arguments because it's
48+
// protected, but we can do the same by hand (the classloader instance
49+
// from which we call resolveClass does not matter).
50+
Class<?> c = sbtLoader.loadClass(className);
51+
if (resolve)
52+
resolveClass(c);
53+
return c;
54+
} else {
55+
return super.loadClass(className, resolve);
56+
}
57+
}
58+
59+
/**
60+
* Cache the result of `fixBridgeLoader`.
61+
* <p>
62+
* Reusing ClassLoaders is important for warm performance since otherwise the
63+
* JIT code cache for the compiler will be discarded between every call to
64+
* the sbt `compile` task.
65+
*/
66+
private static WeakHashMap<ClassLoader, ClassLoader> fixedLoaderCache = new WeakHashMap<>();
67+
68+
/**
69+
* Fix the compiler bridge ClassLoader
70+
* <p>
71+
* Soundtrack: https://www.youtube.com/watch?v=imamcajBEJs
72+
* <p>
73+
* The classloader that we get from sbt looks like:
74+
* <p>
75+
* URLClassLoader(bridgeURLs,
76+
* DualLoader(scalaLoader, notXsbtiFilter, sbtLoader, xsbtiFilter))
77+
* <p>
78+
* DualLoader will load the `xsbti.*` interfaces using `sbtLoader` and
79+
* everything else with `scalaLoader`. Once we have loaded the dotty Main
80+
* class using `scalaLoader`, subsequent classes in the dotty compiler will
81+
* also be loaded by `scalaLoader` and _not_ by the DualLoader. But the sbt
82+
* compiler phases are part of dotty and still need access to the `xsbti.*`
83+
* interfaces in `sbtLoader`, therefore DualLoader does not work for us
84+
* (this issue is not present with scalac because the sbt phases are
85+
* currently defined in the compiler bridge itself, not in scalac).
86+
* <p>
87+
* CompilerClassLoader is a replacement for DualLoader. Until we can fix
88+
* this in sbt proper, we need to use reflection to construct our own
89+
* fixed classloader:
90+
* <p>
91+
* URLClassLoader(bridgeURLs,
92+
* CompilerClassLoader(scalaLoader.getURLs, sbtLoader))
93+
*
94+
* @param bridgeLoader The classloader that sbt uses to load the compiler bridge
95+
* @return A fixed classloader that works with dotty
96+
*/
97+
synchronized public static ClassLoader fixBridgeLoader(ClassLoader bridgeLoader) {
98+
return fixedLoaderCache.computeIfAbsent(bridgeLoader, k -> computeFixedLoader(k));
99+
}
100+
101+
private static ClassLoader computeFixedLoader(ClassLoader bridgeLoader) {
102+
URLClassLoader urlBridgeLoader = (URLClassLoader) bridgeLoader;
103+
ClassLoader dualLoader = urlBridgeLoader.getParent();
104+
Class<?> dualLoaderClass = dualLoader.getClass();
105+
106+
try {
107+
// DualLoader.parentA and DualLoader.parentB are private
108+
Field parentAField = dualLoaderClass.getDeclaredField("parentA");
109+
parentAField.setAccessible(true);
110+
Field parentBField = dualLoaderClass.getDeclaredField("parentB");
111+
parentBField.setAccessible(true);
112+
URLClassLoader scalaLoader = (URLClassLoader) parentAField.get(dualLoader);
113+
ClassLoader sbtLoader = (ClassLoader) parentBField.get(dualLoader);
114+
115+
URL[] bridgeURLs = urlBridgeLoader.getURLs();
116+
return new URLClassLoader(bridgeURLs,
117+
new CompilerClassLoader(scalaLoader.getURLs(), sbtLoader));
118+
} catch (NoSuchFieldException | IllegalAccessException e) {
119+
throw new RuntimeException(e);
120+
}
121+
}
122+
}

sbt-bridge/src/xsbt/CompilerInterface.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,50 @@
88
import xsbti.Reporter;
99
import xsbti.compile.*;
1010

11+
import dotty.tools.dotc.Main;
12+
13+
import java.lang.reflect.InvocationTargetException;
1114
import java.io.File;
1215

1316
/**
1417
* The new compiler interface is [[dotty.tools.xsbt.CompilerBridge]] that extends the new `xsbti.CompilerInterface2`.
1518
* This interface is kept for compatibility with Mill and the sbt 1.3.x series.
1619
*/
1720
public final class CompilerInterface {
18-
public CachedCompiler newCompiler(String[] options, Output output, Logger initialLog, Reporter initialDelegate) {
19-
return new CachedCompilerImpl(options, output);
21+
public CachedCompiler newCompiler(String[] options, Output output, Logger initialLog, Reporter initialDelegate) throws java.lang.Exception {
22+
if(isClassLoaderValid()) {
23+
return new CachedCompilerImpl(options, output);
24+
} else {
25+
initialLog.warn(() ->
26+
"The compiler class loader is badly configured.\n" +
27+
"Consider using a more recent version of your build tool:\n" +
28+
" - sbt >= 1.4.0 and sbt-dotty >= 1.5.0\n" +
29+
" - Mill >= 0.9.3-21-002361\n" +
30+
" - Bloop >= 1.4.6-23-20a501bc"
31+
);
32+
// To workaround the wrong class loader, we construct our own and run
33+
// the following code with it:
34+
// new CachedCompilerImpl(options, output)
35+
try {
36+
ClassLoader bridgeLoader = this.getClass().getClassLoader();
37+
ClassLoader fixedLoader = CompilerClassLoader.fixBridgeLoader(bridgeLoader);
38+
Class<?> cciClass = fixedLoader.loadClass("xsbt.CachedCompilerImpl");
39+
return (CachedCompiler) cciClass.getConstructors()[0].newInstance(options, output);
40+
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
41+
throw new RuntimeException(e);
42+
}
43+
}
44+
}
45+
46+
private boolean isClassLoaderValid() {
47+
// Check that the `xsbti.*` classes are loaded from the same class loader in the compiler and the bridge
48+
ClassLoader compilerClassLoader = Main.class.getClassLoader();
49+
Class<Logger> clazz = Logger.class;
50+
try {
51+
return compilerClassLoader.loadClass("xsbti.Logger") == clazz;
52+
} catch (ClassNotFoundException e) {
53+
throw new RuntimeException(e);
54+
}
2055
}
2156

2257
public void run(File[] sources, DependencyChanges changes, AnalysisCallback callback, Logger log,

0 commit comments

Comments
 (0)