Skip to content

Commit 9f03cf5

Browse files
committed
add MainGenericCompiler
1 parent e5abec0 commit 9f03cf5

10 files changed

+324
-73
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package dotty.tools
2+
3+
import scala.language.unsafeNulls
4+
5+
import scala.annotation.tailrec
6+
import scala.io.Source
7+
import scala.util.Try
8+
import java.io.File
9+
import java.lang.Thread
10+
import scala.annotation.internal.sharable
11+
import dotty.tools.dotc.util.ClasspathFromClassloader
12+
import dotty.tools.runner.ObjectRunner
13+
import dotty.tools.dotc.config.Properties.envOrNone
14+
import dotty.tools.io.Jar
15+
import dotty.tools.runner.ScalaClassLoader
16+
import java.nio.file.Paths
17+
import dotty.tools.dotc.config.CommandLineParser
18+
import dotty.tools.scripting.StringDriver
19+
20+
enum CompileMode:
21+
case Guess
22+
case Compile
23+
case Decompile
24+
case PrintTasty
25+
case Script
26+
case Repl
27+
case Run
28+
29+
case class CompileSettings(
30+
verbose: Boolean = false,
31+
classPath: List[String] = List.empty,
32+
compileMode: CompileMode = CompileMode.Guess,
33+
exitCode: Int = 0,
34+
javaArgs: List[String] = List.empty,
35+
javaProps: List[(String, String)] = List.empty,
36+
scalaArgs: List[String] = List.empty,
37+
residualArgs: List[String] = List.empty,
38+
scriptArgs: List[String] = List.empty,
39+
targetScript: String = "",
40+
compiler: Boolean = false,
41+
quiet: Boolean = false,
42+
colors: Boolean = false,
43+
) {
44+
def withCompileMode(em: CompileMode): CompileSettings = this.compileMode match
45+
case CompileMode.Guess =>
46+
this.copy(compileMode = em)
47+
case _ =>
48+
println(s"compile_mode==[$compileMode], attempted overwrite by [$em]")
49+
this.copy(exitCode = 1)
50+
end withCompileMode
51+
52+
def withScalaArgs(args: String*): CompileSettings =
53+
this.copy(scalaArgs = scalaArgs.appendedAll(args.toList.filter(_.nonEmpty)))
54+
55+
def withJavaArgs(args: String*): CompileSettings =
56+
this.copy(javaArgs = javaArgs.appendedAll(args.toList.filter(_.nonEmpty)))
57+
58+
def withJavaProps(args: (String, String)*): CompileSettings =
59+
this.copy(javaProps = javaProps.appendedAll(args.toList))
60+
61+
def withResidualArgs(args: String*): CompileSettings =
62+
this.copy(residualArgs = residualArgs.appendedAll(args.toList.filter(_.nonEmpty)))
63+
64+
def withScriptArgs(args: String*): CompileSettings =
65+
this.copy(scriptArgs = scriptArgs.appendedAll(args.toList.filter(_.nonEmpty)))
66+
67+
def withTargetScript(file: String): CompileSettings =
68+
Try(Source.fromFile(file)).toOption match
69+
case Some(_) => this.copy(targetScript = file)
70+
case None =>
71+
println(s"not found $file")
72+
this.copy(exitCode = 2)
73+
end withTargetScript
74+
75+
def withCompiler: CompileSettings =
76+
this.copy(compiler = true)
77+
78+
def withQuiet: CompileSettings =
79+
this.copy(quiet = true)
80+
81+
def withColors: CompileSettings =
82+
this.copy(colors = true)
83+
84+
def withNoColors: CompileSettings =
85+
this.copy(colors = false)
86+
}
87+
88+
object MainGenericCompiler {
89+
90+
val classpathSeparator = File.pathSeparator
91+
92+
@sharable val javaOption = raw"""-J(.*)""".r
93+
@sharable val javaPropOption = raw"""-D(.+?)=(.?)""".r
94+
@tailrec
95+
def process(args: List[String], settings: CompileSettings): CompileSettings = args match
96+
case Nil =>
97+
settings
98+
case "--" :: tail =>
99+
process(Nil, settings.withResidualArgs(tail.toList*))
100+
case ("-v" | "-verbose" | "--verbose") :: tail =>
101+
process(tail, settings.withScalaArgs("-verbose"))
102+
case ("-q" | "-quiet") :: tail =>
103+
process(tail, settings.withQuiet)
104+
case "-Oshort" :: tail =>
105+
process(tail, settings.withJavaArgs("-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1"))
106+
case "-repl" :: tail =>
107+
process(tail, settings.withCompileMode(CompileMode.Repl))
108+
case "-script" :: targetScript :: tail =>
109+
process(Nil, settings
110+
.withCompileMode(CompileMode.Script)
111+
.withJavaProps("script.path" -> targetScript)
112+
.withTargetScript(targetScript)
113+
.withScriptArgs(tail.toList*))
114+
case "-compile" :: tail =>
115+
process(tail, settings.withCompileMode(CompileMode.Compile))
116+
case "-decompile" :: tail =>
117+
process(tail, settings.withCompileMode(CompileMode.Decompile))
118+
case "-print-tasty" :: tail =>
119+
process(tail, settings.withCompileMode(CompileMode.PrintTasty))
120+
case "-run" :: tail =>
121+
process(tail, settings.withCompileMode(CompileMode.Run))
122+
case "-colors" :: tail =>
123+
process(tail, settings.withColors)
124+
case "-no-colors" :: tail =>
125+
process(tail, settings.withNoColors)
126+
case "-with-compiler" :: tail =>
127+
process(tail, settings.withCompiler)
128+
case ("-cp" | "-classpath" | "--class-path") :: cp :: tail =>
129+
val cpEntries = cp.split(classpathSeparator).toList
130+
val singleEntryClasspath: Boolean = cpEntries.sizeIs == 1
131+
val globdir: String = if singleEntryClasspath then cp.replaceAll("[\\\\/][^\\\\/]*$", "") else "" // slash/backslash agnostic
132+
def validGlobbedJar(s: String): Boolean = s.startsWith(globdir) && ((s.toLowerCase.endsWith(".jar") || s.toLowerCase.endsWith(".zip")))
133+
val (tailargs, newEntries) = if singleEntryClasspath && validGlobbedJar(cpEntries.head) then
134+
// reassemble globbed wildcard classpath
135+
// globdir is wildcard directory for globbed jar files, reconstruct the intended classpath
136+
val cpJars = tail.takeWhile( f => validGlobbedJar(f) )
137+
val remainingArgs = tail.drop(cpJars.size)
138+
(remainingArgs, cpEntries ++ cpJars)
139+
else
140+
(tail, cpEntries)
141+
142+
process(tailargs, settings.copy(classPath = settings.classPath ++ newEntries.filter(_.nonEmpty)))
143+
case (o @ javaOption(stripped)) :: tail =>
144+
process(tail, settings.withJavaArgs(stripped))
145+
case (javaPropOption(opt, value)) :: tail =>
146+
process(tail, settings.withJavaProps(opt -> value))
147+
case arg :: tail =>
148+
process(tail, settings.withResidualArgs(arg))
149+
end process
150+
151+
def main(args: Array[String]): Unit =
152+
val settings = process(args.toList, CompileSettings())
153+
if settings.exitCode != 0 then System.exit(settings.exitCode)
154+
155+
def classpathSetting =
156+
if settings.classPath.isEmpty then List()
157+
else List("-classpath", settings.classPath.mkString(classpathSeparator))
158+
159+
def reconstructedArgs() =
160+
classpathSetting ++ settings.scalaArgs ++ settings.residualArgs
161+
162+
def addJavaProps(): Unit =
163+
settings.javaProps.foreach { (k, v) => sys.props(k) = v }
164+
165+
def run(settings: CompileSettings): Unit = settings.compileMode match
166+
case CompileMode.Compile =>
167+
addJavaProps()
168+
val properArgs = reconstructedArgs()
169+
dotty.tools.dotc.Main.main(properArgs.toArray)
170+
case CompileMode.Decompile =>
171+
addJavaProps()
172+
val properArgs = reconstructedArgs()
173+
dotty.tools.dotc.decompiler.Main.main(properArgs.toArray)
174+
case CompileMode.PrintTasty =>
175+
addJavaProps()
176+
val properArgs = reconstructedArgs()
177+
dotty.tools.dotc.core.tasty.TastyPrinter.main(properArgs.toArray)
178+
case CompileMode.Script => // Naive copy from scalac bash script
179+
addJavaProps()
180+
val properArgs =
181+
reconstructedArgs()
182+
++ (if settings.compiler then List("-with-compiler") else Nil)
183+
++ List("-script", settings.targetScript)
184+
++ settings.scriptArgs
185+
scripting.Main.main(properArgs.toArray)
186+
case CompileMode.Repl | CompileMode.Run =>
187+
addJavaProps()
188+
val properArgs = reconstructedArgs()
189+
repl.Main.main(properArgs.toArray)
190+
case CompileMode.Guess =>
191+
run(settings.withCompileMode(CompileMode.Compile))
192+
end run
193+
194+
run(settings)
195+
end main
196+
}

compiler/src/dotty/tools/MainGenericRunner.scala

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import dotty.tools.dotc.util.ClasspathFromClassloader
1212
import dotty.tools.runner.ObjectRunner
1313
import dotty.tools.dotc.config.Properties.envOrNone
1414
import dotty.tools.io.Jar
15+
import dotty.tools.io.ClassPath
1516
import dotty.tools.runner.ScalaClassLoader
16-
import java.nio.file.Paths
17+
import java.nio.file.{Path, Paths}
1718
import dotty.tools.dotc.config.CommandLineParser
1819
import dotty.tools.scripting.StringDriver
1920

@@ -170,7 +171,7 @@ object MainGenericRunner {
170171
val newSettings = if arg.startsWith("-") then settings else settings.withPossibleEntryPaths(arg).withModeShouldBePossibleRun
171172
process(tail, newSettings.withResidualArgs(arg))
172173
end process
173-
174+
174175
def main(args: Array[String]): Unit =
175176
val scalaOpts = envOrNone("SCALA_OPTS").toArray.flatMap(_.split(" ")).filter(_.nonEmpty)
176177
val allArgs = scalaOpts ++ args
@@ -207,14 +208,18 @@ object MainGenericRunner {
207208
settings.withExecuteMode(ExecuteMode.Repl)
208209
run(newSettings)
209210
case ExecuteMode.Run =>
210-
val scalaClasspath = ClasspathFromClassloader(Thread.currentThread().getContextClassLoader).split(classpathSeparator)
211-
val newClasspath = (settings.classPath.flatMap(_.split(classpathSeparator).filter(_.nonEmpty)) ++ removeCompiler(scalaClasspath) :+ ".").map(File(_).toURI.toURL)
212-
val res = ObjectRunner.runAndCatch(newClasspath, settings.targetToRun, settings.residualArgs).flatMap {
211+
val bootclasspath = ClasspathFromClassloader(Thread.currentThread().getContextClassLoader).split(classpathSeparator)
212+
val adjustCompiler = removeCompiler(bootclasspath)
213+
val userClasspath = settings.classPath.flatMap(_.split(classpathSeparator).filter(_.nonEmpty))
214+
val fullClasspath = (adjustCompiler ++ userClasspath :+ ".").mkString(classpathSeparator)
215+
val classpathEntries: Seq[Path] = ClassPath.expandPath(fullClasspath, expandStar=true).map { Paths.get(_) }
216+
val newClasspath = classpathEntries.map(_.toFile.toURI.toURL)
217+
val res = ObjectRunner.runAndCatch(newClasspath, settings.compiler, settings.targetToRun, settings.residualArgs).flatMap {
213218
case ex: ClassNotFoundException if ex.getMessage == settings.targetToRun =>
214219
val file = settings.targetToRun
215220
Jar(file).mainClass match
216221
case Some(mc) =>
217-
ObjectRunner.runAndCatch(newClasspath :+ File(file).toURI.toURL, mc, settings.residualArgs)
222+
ObjectRunner.runAndCatch(newClasspath :+ File(file).toURI.toURL, settings.compiler, mc, settings.residualArgs)
218223
case None =>
219224
Some(IllegalArgumentException(s"No main class defined in manifest in jar: $file"))
220225
case ex => Some(ex)
@@ -232,7 +237,7 @@ object MainGenericRunner {
232237
val scalaClasspath = ClasspathFromClassloader(Thread.currentThread().getContextClassLoader).split(classpathSeparator)
233238
val newClasspath = (settings.classPath.flatMap(_.split(classpathSeparator).filter(_.nonEmpty)) ++ removeCompiler(scalaClasspath) :+ ".").map(File(_).toURI.toURL)
234239
val res = if mainClass.nonEmpty then
235-
ObjectRunner.runAndCatch(newClasspath :+ File(targetJar).toURI.toURL, mainClass, settings.scriptArgs)
240+
ObjectRunner.runAndCatch(newClasspath :+ File(targetJar).toURI.toURL, settings.compiler, mainClass, settings.scriptArgs)
236241
else
237242
Some(IllegalArgumentException(s"No main class defined in manifest in jar: $precompiledJar"))
238243
errorFn("", res)
@@ -242,6 +247,7 @@ object MainGenericRunner {
242247
List("-classpath", settings.classPath.mkString(classpathSeparator)).filter(Function.const(settings.classPath.nonEmpty))
243248
++ settings.residualArgs
244249
++ (if settings.save then List("-save") else Nil)
250+
++ (if settings.compiler then List("-with-compiler") else Nil)
245251
++ settings.scalaArgs
246252
++ List("-script", settings.targetScript)
247253
++ settings.scriptArgs
@@ -253,7 +259,7 @@ object MainGenericRunner {
253259
}
254260
val cpArgs = if cp.isEmpty then Nil else List("-classpath", cp)
255261
val properArgs = cpArgs ++ settings.residualArgs ++ settings.scalaArgs
256-
val driver = StringDriver(properArgs.toArray, settings.targetExpression)
262+
val driver = StringDriver(properArgs.toArray, settings.compiler, settings.targetExpression)
257263
driver.compileAndRun(settings.classPath)
258264

259265
case ExecuteMode.Guess =>

compiler/src/dotty/tools/dotc/util/ClasspathFromClassloader.scala

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import java.nio.file.Paths
77

88
import dotty.tools.repl.AbstractFileClassLoader
99

10+
import java.nio.file.FileSystemNotFoundException
11+
1012
object ClasspathFromClassloader {
1113

1214
/** Attempt to recreate a classpath from a classloader.
@@ -27,8 +29,11 @@ object ClasspathFromClassloader {
2729
// the classpath coming from the child is added at the _end_ of the
2830
// classpath.
2931
classpathBuff ++=
30-
cl.getURLs.iterator.map(url => Paths.get(url.toURI).toAbsolutePath.toString)
31-
case _ =>
32+
cl.getURLs.iterator.flatMap(url =>
33+
try Paths.get(url.toURI).toAbsolutePath.toString :: Nil
34+
catch case _: FileSystemNotFoundException => Nil
35+
)
36+
case _ =>
3237
if cl.getClass.getName == classOf[AbstractFileClassLoader].getName then
3338
// HACK: We can't just collect the classpath from arbitrary parent
3439
// classloaders since the current classloader might intentionally

compiler/src/dotty/tools/runner/ObjectRunner.scala

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,24 @@ import java.util.concurrent.ExecutionException
1111
* This is a copy implementation from scala/scala scala.tools.nsc.CommonRunner trait
1212
*/
1313
trait CommonRunner {
14-
/** Run a given object, specified by name, using a
14+
15+
/** Run a given object, specified by name, using a
1516
* specified classpath and argument list.
1617
*
1718
* @throws java.lang.ClassNotFoundException
1819
* @throws java.lang.NoSuchMethodException
1920
* @throws java.lang.reflect.InvocationTargetException
2021
*/
21-
def run(urls: Seq[URL], objectName: String, arguments: Seq[String]): Unit = {
22+
def run(urls: Seq[URL], withCompiler: Boolean, objectName: String, arguments: Seq[String]): Unit = {
2223
import RichClassLoader._
23-
ScalaClassLoader.fromURLsParallelCapable(urls).run(objectName, arguments)
24+
ScalaClassLoader.filteringCompiler(urls, withCompiler).run(objectName, arguments)
2425
}
2526

2627
/** Catches any non-fatal exception thrown by run (in the case of InvocationTargetException,
2728
* unwrapping it) and returns it in an Option.
2829
*/
29-
def runAndCatch(urls: Seq[URL], objectName: String, arguments: Seq[String]): Option[Throwable] =
30-
try { run(urls, objectName, arguments) ; None }
30+
def runAndCatch(urls: Seq[URL], withCompiler: Boolean, objectName: String, arguments: Seq[String]): Option[Throwable] =
31+
try { run(urls, withCompiler, objectName, arguments) ; None }
3132
catch { case NonFatal(e) => Some(rootCause(e)) }
3233

3334
private def rootCause(x: Throwable): Throwable = x match {

compiler/src/dotty/tools/runner/ScalaClassLoader.scala

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import java.lang.reflect.{ InvocationTargetException, UndeclaredThrowableExcepti
1212
import scala.annotation.internal.sharable
1313
import scala.annotation.tailrec
1414
import scala.util.control.Exception.catching
15+
import java.lang.reflect.Method
1516

1617
final class RichClassLoader(private val self: ClassLoader) extends AnyVal {
1718
/** Execute an action with this classloader as context classloader. */
@@ -33,7 +34,11 @@ final class RichClassLoader(private val self: ClassLoader) extends AnyVal {
3334
val method = clsToRun.getMethod("main", classOf[Array[String]])
3435
if !Modifier.isStatic(method.getModifiers) then
3536
throw new NoSuchMethodException(s"$objectName.main is not static")
36-
try asContext(method.invoke(null, Array(arguments.toArray: AnyRef): _*))
37+
run(method, arguments.toArray)
38+
}
39+
40+
def run(main: Method, arguments: Array[String]): Unit = {
41+
try asContext(main.invoke(null, Array(arguments: AnyRef): _*))
3742
catch unwrapHandler({ case ex => throw ex })
3843
}
3944

@@ -59,10 +64,30 @@ object RichClassLoader {
5964
}
6065

6166
object ScalaClassLoader {
67+
68+
private val compilerClassPrefixes = List(
69+
"dotty.tools",
70+
"scala.quoted.staging",
71+
"scala.tasty",
72+
)
73+
6274
def setContext(cl: ClassLoader) = Thread.currentThread.setContextClassLoader(cl)
6375

6476
def fromURLsParallelCapable(urls: Seq[URL], parent: ClassLoader | Null = null): URLClassLoader =
65-
new URLClassLoader(urls.toArray, if parent == null then bootClassLoader else parent)
77+
filteringCompiler(urls, withCompiler = true, parent)
78+
79+
def filteringCompiler(urls: Seq[URL], withCompiler: Boolean, parent: ClassLoader | Null = null): URLClassLoader =
80+
new URLClassLoader(urls.toArray, if parent == null then bootClassLoader else parent) {
81+
ClassLoader.registerAsParallelCapable()
82+
83+
override def loadClass(name: String, resolve: Boolean): Class[?] = {
84+
if !withCompiler && compilerClassPrefixes.exists(name.startsWith) then
85+
throw new ClassNotFoundException(s"Class $name can not be loaded without the `-with-compiler` flag.")
86+
else
87+
super.loadClass(name, resolve)
88+
}
89+
90+
}
6691

6792
@sharable private[this] val bootClassLoader: ClassLoader =
6893
if scala.util.Properties.isJavaAtLeast("9") then

0 commit comments

Comments
 (0)