Skip to content

Commit 8b17db9

Browse files
committed
wip
1 parent 7877f1f commit 8b17db9

File tree

4 files changed

+185
-0
lines changed

4 files changed

+185
-0
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ val `scala3-compiler-nonbootstrapped` = Build.`scala3-compiler-nonbootstrapped`
99
val `scala3-compiler-bootstrapped-new` = Build.`scala3-compiler-bootstrapped-new`
1010

1111
val `scala3-repl` = Build.`scala3-repl`
12+
val `scala3-repl-embedded` = Build.`scala3-repl-embedded`
1213

1314
// The Standard Library
1415
val `scala-library-nonbootstrapped` = Build.`scala-library-nonbootstrapped`

project/Build.scala

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import dotty.tools.sbtplugin.ScalaLibraryPlugin
2626
import dotty.tools.sbtplugin.ScalaLibraryPlugin.autoImport._
2727
import dotty.tools.sbtplugin.DottyJSPlugin
2828
import dotty.tools.sbtplugin.DottyJSPlugin.autoImport._
29+
import sbtassembly.AssemblyPlugin.autoImport._
30+
import sbtassembly.{MergeStrategy, PathList}
31+
import com.eed3si9n.jarjarabrams.ShadeRule
2932

3033
import sbt.plugins.SbtPlugin
3134
import sbt.ScriptedPlugin.autoImport._
@@ -1175,6 +1178,46 @@ object Build {
11751178
},
11761179
)
11771180

1181+
lazy val `scala3-repl-embedded` = project.in(file("repl-embedded"))
1182+
.dependsOn(`scala3-repl`)
1183+
.settings(
1184+
name := "scala3-repl-embedded",
1185+
moduleName := "scala3-repl-embedded",
1186+
version := dottyVersion,
1187+
versionScheme := Some("semver-spec"),
1188+
scalaVersion := referenceVersion,
1189+
crossPaths := true,
1190+
autoScalaLibrary := false,
1191+
// Source directories
1192+
Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src"),
1193+
// Assembly configuration for shading
1194+
assembly / assemblyJarName := s"scala3-repl-embedded-${version.value}.jar",
1195+
assembly / mainClass := Some("scala.tools.repl.EmbeddedReplMain"),
1196+
// Shading rules: relocate specific packages to dotty.tools.repl.shaded, except scala.*, java.*, javax.*
1197+
assembly / assemblyShadeRules := Seq(
1198+
ShadeRule.rename("dotty.**" -> "dotty.tools.repl.shaded.dotty.@1").inAll,
1199+
ShadeRule.rename("org.**" -> "dotty.tools.repl.shaded.org.@1").inAll,
1200+
ShadeRule.rename("com.**" -> "dotty.tools.repl.shaded.com.@1").inAll,
1201+
ShadeRule.rename("io.**" -> "dotty.tools.repl.shaded.io.@1").inAll,
1202+
ShadeRule.rename("coursier.**" -> "dotty.tools.repl.shaded.coursier.@1").inAll,
1203+
ShadeRule.rename("dependency.**" -> "dotty.tools.repl.shaded.dependency.@1").inAll,
1204+
ShadeRule.rename("pprint.**" -> "dotty.tools.repl.shaded.pprint.@1").inAll,
1205+
ShadeRule.rename("fansi.**" -> "dotty.tools.repl.shaded.fansi.@1").inAll,
1206+
ShadeRule.rename("sourcecode.**" -> "dotty.tools.repl.shaded.sourcecode.@1").inAll,
1207+
),
1208+
// Merge strategy for assembly
1209+
assembly / assemblyMergeStrategy := {
1210+
case PathList("META-INF", xs @ _*) => xs match {
1211+
case "MANIFEST.MF" :: Nil => MergeStrategy.discard
1212+
case _ => MergeStrategy.discard
1213+
}
1214+
case x if x.endsWith(".proto") => MergeStrategy.first
1215+
case x => MergeStrategy.first
1216+
},
1217+
// Don't run tests for assembly
1218+
assembly / test := {},
1219+
)
1220+
11781221
// ==============================================================================================
11791222
// =================================== SCALA STANDARD LIBRARY ===================================
11801223
// ==============================================================================================

project/plugins.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ addSbtPlugin("com.gradle" % "sbt-develocity" % "1.3.1")
2525
addSbtPlugin("com.gradle" % "sbt-develocity-common-custom-user-data" % "1.1")
2626

2727
addSbtPlugin("com.github.sbt" % "sbt-jdi-tools" % "1.2.0")
28+
29+
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.5")
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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

Comments
 (0)