|
| 1 | +package scala.tools.repl |
| 2 | + |
| 3 | +import java.net.{URL, URLClassLoader} |
| 4 | +import java.io.InputStream |
| 5 | + |
| 6 | +/** |
| 7 | + * A classloader that remaps shaded classes back to their original package names. |
| 8 | + * |
| 9 | + * This classloader intercepts class loading requests and remaps them from |
| 10 | + * dotty.tools.repl.shaded.* back to their original package names, allowing the |
| 11 | + * shaded classes to be loaded as if they were in their original packages. |
| 12 | + * |
| 13 | + * The scala.* packages are not shaded, so they pass through normally. |
| 14 | + */ |
| 15 | +class UnshadingClassLoader(parent: ClassLoader) extends ClassLoader(parent) { |
| 16 | + |
| 17 | + private val SHADED_PREFIX = "dotty.tools.repl.shaded." |
| 18 | + |
| 19 | + // Packages that were shaded |
| 20 | + private val SHADED_PACKAGES = Seq("dotty.", "org.", "com.", "io.", "coursier.", "dependency.", "pprint.", "fansi.", "sourcecode.") |
| 21 | + |
| 22 | + override def loadClass(name: String, resolve: Boolean): Class[?] = { |
| 23 | + // Check if this is a class from a package we shaded (and not already in the shaded package) |
| 24 | + val shouldUnshade = SHADED_PACKAGES.exists(pkg => name.startsWith(pkg)) && |
| 25 | + !name.startsWith(SHADED_PREFIX) |
| 26 | + |
| 27 | + if (shouldUnshade) { |
| 28 | + // Try to find the shaded version |
| 29 | + val shadedName = SHADED_PREFIX + name |
| 30 | + |
| 31 | + // First check if we've already loaded this class |
| 32 | + val loaded = findLoadedClass(name) |
| 33 | + if (loaded != null) return loaded |
| 34 | + |
| 35 | + try { |
| 36 | + // Load the shaded class bytes from parent |
| 37 | + val resourceName = shadedName.replace('.', '/') + ".class" |
| 38 | + val is = getParent.getResourceAsStream(resourceName) |
| 39 | + |
| 40 | + if (is != null) { |
| 41 | + try { |
| 42 | + // Read the class bytes |
| 43 | + val bytes = readAllBytes(is) |
| 44 | + |
| 45 | + // Define the class with the unshaded name |
| 46 | + val clazz = defineClass(name, bytes, 0, bytes.length) |
| 47 | + if (resolve) resolveClass(clazz) |
| 48 | + return clazz |
| 49 | + } finally { |
| 50 | + is.close() |
| 51 | + } |
| 52 | + } |
| 53 | + } catch { |
| 54 | + case _: Exception => // Fall through to parent |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + // For everything else (scala.* and already shaded classes), delegate to parent |
| 59 | + super.loadClass(name, resolve) |
| 60 | + } |
| 61 | + |
| 62 | + private def readAllBytes(is: InputStream): Array[Byte] = { |
| 63 | + val buffer = new Array[Byte](8192) |
| 64 | + var bytesRead = 0 |
| 65 | + val baos = new java.io.ByteArrayOutputStream() |
| 66 | + |
| 67 | + while ({bytesRead = is.read(buffer); bytesRead != -1}) { |
| 68 | + baos.write(buffer, 0, bytesRead) |
| 69 | + } |
| 70 | + |
| 71 | + baos.toByteArray() |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +/** |
| 76 | + * Main entry point for the embedded shaded REPL. |
| 77 | + * |
| 78 | + * This creates an isolated classloader that loads the shaded REPL classes |
| 79 | + * as if they were unshaded, instantiates a ReplDriver, and runs it. |
| 80 | + */ |
| 81 | +object EmbeddedReplMain { |
| 82 | + def main(args: Array[String]): Unit = { |
| 83 | + // Get the location of the current jar to use as classpath |
| 84 | + val codeSource = getClass.getProtectionDomain.getCodeSource |
| 85 | + val jarPath = if (codeSource != null) { |
| 86 | + val location = codeSource.getLocation |
| 87 | + if (location.getProtocol == "file") { |
| 88 | + new java.io.File(location.toURI).getAbsolutePath |
| 89 | + } else { |
| 90 | + location.toString |
| 91 | + } |
| 92 | + } else { |
| 93 | + // Fallback: try to extract from classpath |
| 94 | + System.getProperty("java.class.path") |
| 95 | + } |
| 96 | + |
| 97 | + // Add -classpath argument pointing to the shaded jar itself |
| 98 | + // This allows the ReplDriver's compiler to find scala.* classes |
| 99 | + val argsWithClasspath = if (args.exists(arg => arg == "-classpath" || arg == "-cp")) { |
| 100 | + args // Already has classpath |
| 101 | + } else { |
| 102 | + Array("-classpath", jarPath) ++ args |
| 103 | + } |
| 104 | + |
| 105 | + // Create the unshading classloader with the current classloader as parent |
| 106 | + // This ensures it has access to all dependencies in the shaded jar |
| 107 | + val unshadingClassLoader = new UnshadingClassLoader( |
| 108 | + getClass.getClassLoader // Use current classloader to access all dependencies |
| 109 | + ) |
| 110 | + |
| 111 | + // Load the ReplDriver class through the unshading classloader |
| 112 | + val replDriverClass = unshadingClassLoader.loadClass("dotty.tools.repl.ReplDriver") |
| 113 | + |
| 114 | + // Get the constructor: ReplDriver(Array[String], PrintStream, Option[ClassLoader], String) |
| 115 | + val constructor = replDriverClass.getConstructors()(0) |
| 116 | + |
| 117 | + // Create an Option[ClassLoader] containing the external classloader |
| 118 | + val scalaOptionClass = unshadingClassLoader.loadClass("scala.Option") |
| 119 | + val scalaOptionModule = unshadingClassLoader.loadClass("scala.Option$") |
| 120 | + val someMethod = scalaOptionModule.getField("MODULE$").get(null) |
| 121 | + .asInstanceOf[Object].getClass.getMethod("apply", classOf[Object]) |
| 122 | + val classLoaderOption = someMethod.invoke( |
| 123 | + scalaOptionModule.getField("MODULE$").get(null), |
| 124 | + getClass.getClassLoader // Pass the external classloader |
| 125 | + ) |
| 126 | + |
| 127 | + // Create the ReplDriver instance with classpath argument |
| 128 | + val replDriver = constructor.newInstance( |
| 129 | + argsWithClasspath, // settings: Array[String] (now includes -classpath) |
| 130 | + System.out, // out: PrintStream |
| 131 | + classLoaderOption, // classLoader: Option[ClassLoader] |
| 132 | + "" // extraPredef: String |
| 133 | + ) |
| 134 | + |
| 135 | + // Call tryRunning on the ReplDriver |
| 136 | + val tryRunningMethod = replDriverClass.getMethod("tryRunning") |
| 137 | + tryRunningMethod.invoke(replDriver) |
| 138 | + } |
| 139 | +} |
0 commit comments