Skip to content

Commit

Permalink
add MainGenericCompiler
Browse files Browse the repository at this point in the history
  • Loading branch information
bishabosha committed May 18, 2022
1 parent e5abec0 commit 9f03cf5
Show file tree
Hide file tree
Showing 10 changed files with 324 additions and 73 deletions.
196 changes: 196 additions & 0 deletions compiler/src/dotty/tools/MainGenericCompiler.scala
@@ -0,0 +1,196 @@
package dotty.tools

import scala.language.unsafeNulls

import scala.annotation.tailrec
import scala.io.Source
import scala.util.Try
import java.io.File
import java.lang.Thread
import scala.annotation.internal.sharable
import dotty.tools.dotc.util.ClasspathFromClassloader
import dotty.tools.runner.ObjectRunner
import dotty.tools.dotc.config.Properties.envOrNone
import dotty.tools.io.Jar
import dotty.tools.runner.ScalaClassLoader
import java.nio.file.Paths
import dotty.tools.dotc.config.CommandLineParser
import dotty.tools.scripting.StringDriver

enum CompileMode:
case Guess
case Compile
case Decompile
case PrintTasty
case Script
case Repl
case Run

case class CompileSettings(
verbose: Boolean = false,
classPath: List[String] = List.empty,
compileMode: CompileMode = CompileMode.Guess,
exitCode: Int = 0,
javaArgs: List[String] = List.empty,
javaProps: List[(String, String)] = List.empty,
scalaArgs: List[String] = List.empty,
residualArgs: List[String] = List.empty,
scriptArgs: List[String] = List.empty,
targetScript: String = "",
compiler: Boolean = false,
quiet: Boolean = false,
colors: Boolean = false,
) {
def withCompileMode(em: CompileMode): CompileSettings = this.compileMode match
case CompileMode.Guess =>
this.copy(compileMode = em)
case _ =>
println(s"compile_mode==[$compileMode], attempted overwrite by [$em]")
this.copy(exitCode = 1)
end withCompileMode

def withScalaArgs(args: String*): CompileSettings =
this.copy(scalaArgs = scalaArgs.appendedAll(args.toList.filter(_.nonEmpty)))

def withJavaArgs(args: String*): CompileSettings =
this.copy(javaArgs = javaArgs.appendedAll(args.toList.filter(_.nonEmpty)))

def withJavaProps(args: (String, String)*): CompileSettings =
this.copy(javaProps = javaProps.appendedAll(args.toList))

def withResidualArgs(args: String*): CompileSettings =
this.copy(residualArgs = residualArgs.appendedAll(args.toList.filter(_.nonEmpty)))

def withScriptArgs(args: String*): CompileSettings =
this.copy(scriptArgs = scriptArgs.appendedAll(args.toList.filter(_.nonEmpty)))

def withTargetScript(file: String): CompileSettings =
Try(Source.fromFile(file)).toOption match
case Some(_) => this.copy(targetScript = file)
case None =>
println(s"not found $file")
this.copy(exitCode = 2)
end withTargetScript

def withCompiler: CompileSettings =
this.copy(compiler = true)

def withQuiet: CompileSettings =
this.copy(quiet = true)

def withColors: CompileSettings =
this.copy(colors = true)

def withNoColors: CompileSettings =
this.copy(colors = false)
}

object MainGenericCompiler {

val classpathSeparator = File.pathSeparator

@sharable val javaOption = raw"""-J(.*)""".r
@sharable val javaPropOption = raw"""-D(.+?)=(.?)""".r
@tailrec
def process(args: List[String], settings: CompileSettings): CompileSettings = args match
case Nil =>
settings
case "--" :: tail =>
process(Nil, settings.withResidualArgs(tail.toList*))
case ("-v" | "-verbose" | "--verbose") :: tail =>
process(tail, settings.withScalaArgs("-verbose"))
case ("-q" | "-quiet") :: tail =>
process(tail, settings.withQuiet)
case "-Oshort" :: tail =>
process(tail, settings.withJavaArgs("-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1"))
case "-repl" :: tail =>
process(tail, settings.withCompileMode(CompileMode.Repl))
case "-script" :: targetScript :: tail =>
process(Nil, settings
.withCompileMode(CompileMode.Script)
.withJavaProps("script.path" -> targetScript)
.withTargetScript(targetScript)
.withScriptArgs(tail.toList*))
case "-compile" :: tail =>
process(tail, settings.withCompileMode(CompileMode.Compile))
case "-decompile" :: tail =>
process(tail, settings.withCompileMode(CompileMode.Decompile))
case "-print-tasty" :: tail =>
process(tail, settings.withCompileMode(CompileMode.PrintTasty))
case "-run" :: tail =>
process(tail, settings.withCompileMode(CompileMode.Run))
case "-colors" :: tail =>
process(tail, settings.withColors)
case "-no-colors" :: tail =>
process(tail, settings.withNoColors)
case "-with-compiler" :: tail =>
process(tail, settings.withCompiler)
case ("-cp" | "-classpath" | "--class-path") :: cp :: tail =>
val cpEntries = cp.split(classpathSeparator).toList
val singleEntryClasspath: Boolean = cpEntries.sizeIs == 1
val globdir: String = if singleEntryClasspath then cp.replaceAll("[\\\\/][^\\\\/]*$", "") else "" // slash/backslash agnostic
def validGlobbedJar(s: String): Boolean = s.startsWith(globdir) && ((s.toLowerCase.endsWith(".jar") || s.toLowerCase.endsWith(".zip")))
val (tailargs, newEntries) = if singleEntryClasspath && validGlobbedJar(cpEntries.head) then
// reassemble globbed wildcard classpath
// globdir is wildcard directory for globbed jar files, reconstruct the intended classpath
val cpJars = tail.takeWhile( f => validGlobbedJar(f) )
val remainingArgs = tail.drop(cpJars.size)
(remainingArgs, cpEntries ++ cpJars)
else
(tail, cpEntries)

process(tailargs, settings.copy(classPath = settings.classPath ++ newEntries.filter(_.nonEmpty)))
case (o @ javaOption(stripped)) :: tail =>
process(tail, settings.withJavaArgs(stripped))
case (javaPropOption(opt, value)) :: tail =>
process(tail, settings.withJavaProps(opt -> value))
case arg :: tail =>
process(tail, settings.withResidualArgs(arg))
end process

def main(args: Array[String]): Unit =
val settings = process(args.toList, CompileSettings())
if settings.exitCode != 0 then System.exit(settings.exitCode)

def classpathSetting =
if settings.classPath.isEmpty then List()
else List("-classpath", settings.classPath.mkString(classpathSeparator))

def reconstructedArgs() =
classpathSetting ++ settings.scalaArgs ++ settings.residualArgs

def addJavaProps(): Unit =
settings.javaProps.foreach { (k, v) => sys.props(k) = v }

def run(settings: CompileSettings): Unit = settings.compileMode match
case CompileMode.Compile =>
addJavaProps()
val properArgs = reconstructedArgs()
dotty.tools.dotc.Main.main(properArgs.toArray)
case CompileMode.Decompile =>
addJavaProps()
val properArgs = reconstructedArgs()
dotty.tools.dotc.decompiler.Main.main(properArgs.toArray)
case CompileMode.PrintTasty =>
addJavaProps()
val properArgs = reconstructedArgs()
dotty.tools.dotc.core.tasty.TastyPrinter.main(properArgs.toArray)
case CompileMode.Script => // Naive copy from scalac bash script
addJavaProps()
val properArgs =
reconstructedArgs()
++ (if settings.compiler then List("-with-compiler") else Nil)
++ List("-script", settings.targetScript)
++ settings.scriptArgs
scripting.Main.main(properArgs.toArray)
case CompileMode.Repl | CompileMode.Run =>
addJavaProps()
val properArgs = reconstructedArgs()
repl.Main.main(properArgs.toArray)
case CompileMode.Guess =>
run(settings.withCompileMode(CompileMode.Compile))
end run

run(settings)
end main
}
22 changes: 14 additions & 8 deletions compiler/src/dotty/tools/MainGenericRunner.scala
Expand Up @@ -12,8 +12,9 @@ import dotty.tools.dotc.util.ClasspathFromClassloader
import dotty.tools.runner.ObjectRunner
import dotty.tools.dotc.config.Properties.envOrNone
import dotty.tools.io.Jar
import dotty.tools.io.ClassPath
import dotty.tools.runner.ScalaClassLoader
import java.nio.file.Paths
import java.nio.file.{Path, Paths}
import dotty.tools.dotc.config.CommandLineParser
import dotty.tools.scripting.StringDriver

Expand Down Expand Up @@ -170,7 +171,7 @@ object MainGenericRunner {
val newSettings = if arg.startsWith("-") then settings else settings.withPossibleEntryPaths(arg).withModeShouldBePossibleRun
process(tail, newSettings.withResidualArgs(arg))
end process

def main(args: Array[String]): Unit =
val scalaOpts = envOrNone("SCALA_OPTS").toArray.flatMap(_.split(" ")).filter(_.nonEmpty)
val allArgs = scalaOpts ++ args
Expand Down Expand Up @@ -207,14 +208,18 @@ object MainGenericRunner {
settings.withExecuteMode(ExecuteMode.Repl)
run(newSettings)
case ExecuteMode.Run =>
val scalaClasspath = ClasspathFromClassloader(Thread.currentThread().getContextClassLoader).split(classpathSeparator)
val newClasspath = (settings.classPath.flatMap(_.split(classpathSeparator).filter(_.nonEmpty)) ++ removeCompiler(scalaClasspath) :+ ".").map(File(_).toURI.toURL)
val res = ObjectRunner.runAndCatch(newClasspath, settings.targetToRun, settings.residualArgs).flatMap {
val bootclasspath = ClasspathFromClassloader(Thread.currentThread().getContextClassLoader).split(classpathSeparator)
val adjustCompiler = removeCompiler(bootclasspath)
val userClasspath = settings.classPath.flatMap(_.split(classpathSeparator).filter(_.nonEmpty))
val fullClasspath = (adjustCompiler ++ userClasspath :+ ".").mkString(classpathSeparator)
val classpathEntries: Seq[Path] = ClassPath.expandPath(fullClasspath, expandStar=true).map { Paths.get(_) }
val newClasspath = classpathEntries.map(_.toFile.toURI.toURL)
val res = ObjectRunner.runAndCatch(newClasspath, settings.compiler, settings.targetToRun, settings.residualArgs).flatMap {
case ex: ClassNotFoundException if ex.getMessage == settings.targetToRun =>
val file = settings.targetToRun
Jar(file).mainClass match
case Some(mc) =>
ObjectRunner.runAndCatch(newClasspath :+ File(file).toURI.toURL, mc, settings.residualArgs)
ObjectRunner.runAndCatch(newClasspath :+ File(file).toURI.toURL, settings.compiler, mc, settings.residualArgs)
case None =>
Some(IllegalArgumentException(s"No main class defined in manifest in jar: $file"))
case ex => Some(ex)
Expand All @@ -232,7 +237,7 @@ object MainGenericRunner {
val scalaClasspath = ClasspathFromClassloader(Thread.currentThread().getContextClassLoader).split(classpathSeparator)
val newClasspath = (settings.classPath.flatMap(_.split(classpathSeparator).filter(_.nonEmpty)) ++ removeCompiler(scalaClasspath) :+ ".").map(File(_).toURI.toURL)
val res = if mainClass.nonEmpty then
ObjectRunner.runAndCatch(newClasspath :+ File(targetJar).toURI.toURL, mainClass, settings.scriptArgs)
ObjectRunner.runAndCatch(newClasspath :+ File(targetJar).toURI.toURL, settings.compiler, mainClass, settings.scriptArgs)
else
Some(IllegalArgumentException(s"No main class defined in manifest in jar: $precompiledJar"))
errorFn("", res)
Expand All @@ -242,6 +247,7 @@ object MainGenericRunner {
List("-classpath", settings.classPath.mkString(classpathSeparator)).filter(Function.const(settings.classPath.nonEmpty))
++ settings.residualArgs
++ (if settings.save then List("-save") else Nil)
++ (if settings.compiler then List("-with-compiler") else Nil)
++ settings.scalaArgs
++ List("-script", settings.targetScript)
++ settings.scriptArgs
Expand All @@ -253,7 +259,7 @@ object MainGenericRunner {
}
val cpArgs = if cp.isEmpty then Nil else List("-classpath", cp)
val properArgs = cpArgs ++ settings.residualArgs ++ settings.scalaArgs
val driver = StringDriver(properArgs.toArray, settings.targetExpression)
val driver = StringDriver(properArgs.toArray, settings.compiler, settings.targetExpression)
driver.compileAndRun(settings.classPath)

case ExecuteMode.Guess =>
Expand Down
Expand Up @@ -7,6 +7,8 @@ import java.nio.file.Paths

import dotty.tools.repl.AbstractFileClassLoader

import java.nio.file.FileSystemNotFoundException

object ClasspathFromClassloader {

/** Attempt to recreate a classpath from a classloader.
Expand All @@ -27,8 +29,11 @@ object ClasspathFromClassloader {
// the classpath coming from the child is added at the _end_ of the
// classpath.
classpathBuff ++=
cl.getURLs.iterator.map(url => Paths.get(url.toURI).toAbsolutePath.toString)
case _ =>
cl.getURLs.iterator.flatMap(url =>
try Paths.get(url.toURI).toAbsolutePath.toString :: Nil
catch case _: FileSystemNotFoundException => Nil
)
case _ =>
if cl.getClass.getName == classOf[AbstractFileClassLoader].getName then
// HACK: We can't just collect the classpath from arbitrary parent
// classloaders since the current classloader might intentionally
Expand Down
11 changes: 6 additions & 5 deletions compiler/src/dotty/tools/runner/ObjectRunner.scala
Expand Up @@ -11,23 +11,24 @@ import java.util.concurrent.ExecutionException
* This is a copy implementation from scala/scala scala.tools.nsc.CommonRunner trait
*/
trait CommonRunner {
/** Run a given object, specified by name, using a

/** Run a given object, specified by name, using a
* specified classpath and argument list.
*
* @throws java.lang.ClassNotFoundException
* @throws java.lang.NoSuchMethodException
* @throws java.lang.reflect.InvocationTargetException
*/
def run(urls: Seq[URL], objectName: String, arguments: Seq[String]): Unit = {
def run(urls: Seq[URL], withCompiler: Boolean, objectName: String, arguments: Seq[String]): Unit = {
import RichClassLoader._
ScalaClassLoader.fromURLsParallelCapable(urls).run(objectName, arguments)
ScalaClassLoader.filteringCompiler(urls, withCompiler).run(objectName, arguments)
}

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

private def rootCause(x: Throwable): Throwable = x match {
Expand Down
29 changes: 27 additions & 2 deletions compiler/src/dotty/tools/runner/ScalaClassLoader.scala
Expand Up @@ -12,6 +12,7 @@ import java.lang.reflect.{ InvocationTargetException, UndeclaredThrowableExcepti
import scala.annotation.internal.sharable
import scala.annotation.tailrec
import scala.util.control.Exception.catching
import java.lang.reflect.Method

final class RichClassLoader(private val self: ClassLoader) extends AnyVal {
/** Execute an action with this classloader as context classloader. */
Expand All @@ -33,7 +34,11 @@ final class RichClassLoader(private val self: ClassLoader) extends AnyVal {
val method = clsToRun.getMethod("main", classOf[Array[String]])
if !Modifier.isStatic(method.getModifiers) then
throw new NoSuchMethodException(s"$objectName.main is not static")
try asContext(method.invoke(null, Array(arguments.toArray: AnyRef): _*))
run(method, arguments.toArray)
}

def run(main: Method, arguments: Array[String]): Unit = {
try asContext(main.invoke(null, Array(arguments: AnyRef): _*))
catch unwrapHandler({ case ex => throw ex })
}

Expand All @@ -59,10 +64,30 @@ object RichClassLoader {
}

object ScalaClassLoader {

private val compilerClassPrefixes = List(
"dotty.tools",
"scala.quoted.staging",
"scala.tasty",
)

def setContext(cl: ClassLoader) = Thread.currentThread.setContextClassLoader(cl)

def fromURLsParallelCapable(urls: Seq[URL], parent: ClassLoader | Null = null): URLClassLoader =
new URLClassLoader(urls.toArray, if parent == null then bootClassLoader else parent)
filteringCompiler(urls, withCompiler = true, parent)

def filteringCompiler(urls: Seq[URL], withCompiler: Boolean, parent: ClassLoader | Null = null): URLClassLoader =
new URLClassLoader(urls.toArray, if parent == null then bootClassLoader else parent) {
ClassLoader.registerAsParallelCapable()

override def loadClass(name: String, resolve: Boolean): Class[?] = {
if !withCompiler && compilerClassPrefixes.exists(name.startsWith) then
throw new ClassNotFoundException(s"Class $name can not be loaded without the `-with-compiler` flag.")
else
super.loadClass(name, resolve)
}

}

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

0 comments on commit 9f03cf5

Please sign in to comment.