Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement scalac script in Scala #15212

Merged
merged 3 commits into from May 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
188 changes: 188 additions & 0 deletions compiler/src/dotty/tools/MainGenericCompiler.scala
@@ -0,0 +1,188 @@
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 "-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*))
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 (tailargs, newEntries) = MainGenericRunner.processClasspath(cp, tail)
process(tailargs, settings.copy(classPath = settings.classPath ++ newEntries.filter(_.nonEmpty)))
case "-Oshort" :: tail =>
// Nothing is to be done here. Request that the user adds the relevant flags manually.
// i.e this has no effect when MainGenericRunner is invoked programatically.
val addTC="-XX:+TieredCompilation"
val tStopAtLvl="-XX:TieredStopAtLevel=1"
println(s"ignoring deprecated -Oshort flag, please add `-J$addTC` and `-J$tStopAtLvl` flags manually")
process(tail, settings)
case 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()
++ 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
}
31 changes: 16 additions & 15 deletions compiler/src/dotty/tools/MainGenericRunner.scala
Expand Up @@ -100,6 +100,20 @@ object MainGenericRunner {

val classpathSeparator = File.pathSeparator

def processClasspath(cp: String, tail: List[String]): (List[String], List[String]) =
val cpEntries = cp.split(classpathSeparator).toList
val singleEntryClasspath: Boolean = cpEntries.take(2).size == 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")))
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)

@sharable val javaOption = raw"""-J(.*)""".r
@sharable val scalaOption = raw"""@.*""".r
@sharable val colorOption = raw"""-color:.*""".r
Expand All @@ -110,21 +124,8 @@ object MainGenericRunner {
case "-run" :: fqName :: tail =>
process(tail, settings.withExecuteMode(ExecuteMode.Run).withTargetToRun(fqName))
case ("-cp" | "-classpath" | "--class-path") :: cp :: tail =>
val cpEntries = cp.split(classpathSeparator).toList
val singleEntryClasspath: Boolean = cpEntries.take(2).size == 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)

val (tailargs, newEntries) = processClasspath(cp, tail)
process(tailargs, settings.copy(classPath = settings.classPath ++ newEntries.filter(_.nonEmpty)))

case ("-version" | "--version") :: _ =>
settings.copy(
executeMode = ExecuteMode.Repl,
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
4 changes: 2 additions & 2 deletions dist/bin/scala
Expand Up @@ -33,9 +33,9 @@ while [[ $# -gt 0 ]]; do
-D*)
# pass to scala as well: otherwise we lose it sometimes when we
# need it, e.g. communicating with a server compiler.
# respect user-supplied -Dscala.usejavacp
addJava "$1"
addScala "$1"
# respect user-supplied -Dscala.usejavacp
shift
;;
-J*)
Expand All @@ -47,7 +47,7 @@ while [[ $# -gt 0 ]]; do
;;
-classpath*)
if [ "$1" != "${1##* }" ]; then
# hashbang-combined args "-classpath 'lib/*'"
# -classpath and its value have been supplied in a single string e.g. "-classpath 'lib/*'"
A=$1 ; shift # consume $1 before adding its substrings back
set -- $A "$@" # split $1 on whitespace and put it back
else
Expand Down
84 changes: 47 additions & 37 deletions dist/bin/scalac
Expand Up @@ -30,43 +30,54 @@ source "$PROG_HOME/bin/common"

[ -z "$PROG_NAME" ] && PROG_NAME=$CompilerMain

withCompiler=true

while [[ $# -gt 0 ]]; do
case "$1" in
--) shift; for arg; do addResidual "$arg"; done; set -- ;;
bishabosha marked this conversation as resolved.
Show resolved Hide resolved
-v|-verbose) verbose=true && addScala "-verbose" && shift ;;
-q|-quiet) quiet=true && shift ;;

case "$1" in
--)
# pass all remaining arguments to scala, e.g. to avoid interpreting them here as -D or -J
while [[ $# -gt 0 ]]; do addScala "$1" && shift ; done
;;
-script)
# pass all remaining arguments to scala, e.g. to avoid interpreting them here as -D or -J
while [[ $# -gt 0 ]]; do addScala "$1" && shift ; done
;;
# Optimize for short-running applications, see https://github.com/lampepfl/dotty/issues/222
-Oshort) addJava "-XX:+TieredCompilation -XX:TieredStopAtLevel=1" && shift ;;
-repl) PROG_NAME="$ReplMain" && shift ;;
-script) PROG_NAME="$ScriptingMain" && target_script="$2" && shift && shift
while [[ $# -gt 0 ]]; do addScript "$1" && shift ; done ;;
-compile) PROG_NAME="$CompilerMain" && shift ;;
-decompile) PROG_NAME="$DecompilerMain" && shift ;;
-print-tasty) PROG_NAME="$TastyPrinterMain" && shift ;;
-run) PROG_NAME="$ReplMain" && shift ;;
-colors) colors=true && shift ;;
-no-colors) unset colors && shift ;;
-with-compiler) jvm_cp_args="$PSEP$DOTTY_COMP$PSEP$TASTY_CORE" && shift ;;

# break out -D and -J options and add them to java_args so
# they reach the JVM in time to do some good. The -D options
# will be available as system properties.
-D*) addJava "$1" && shift ;;
-J*) addJava "${1:2}" && shift ;;
*) addResidual "$1" && shift ;;
-Oshort)
addScala "-Oshort" && \
addJava "-XX:+TieredCompilation" && addJava "-XX:TieredStopAtLevel=1" && shift ;;
-D*)
# pass to scala as well: otherwise we lose it sometimes when we
# need it, e.g. communicating with a server compiler.
# respect user-supplied -Dscala.usejavacp
addJava "$1"
addScala "$1"
shift
;;
-J*)
# as with -D, pass to scala even though it will almost
# never be used.
addJava "${1:2}"
addScala "$1"
shift
;;
-classpath*)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same confusion here. This did not use to be special in the old script. Why is it special now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is copied from the Scala script, I think there was the same Cygwin bug there

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's related to cygwin; the code and comments appear to be inherited from the scala2 version of the script.

if [ "$1" != "${1##* }" ]; then
# -classpath and its value have been supplied in a single string e.g. "-classpath 'lib/*'"
A=$1 ; shift # consume $1 before adding its substrings back
set -- $A "$@" # split $1 on whitespace and put it back
else
addScala "$1"
shift
fi
;;
*)
addScala "$1"
shift
;;
esac
done

compilerJavaClasspathArgs

if [ "$PROG_NAME" == "$ScriptingMain" ]; then
setScriptName="-Dscript.path=$target_script"
scripting_string="-script $target_script ${script_args[@]}"
fi

[ -n "$script_trace" ] && set -x
[ -z "${ConEmuPID-}" -o -n "${cygwin-}" ] && export MSYSTEM= PWD= # workaround for #12405

Expand All @@ -75,11 +86,10 @@ eval "\"$JAVACMD\"" \
${JAVA_OPTS:-$default_java_opts} \
"${java_args[@]}" \
"-classpath \"$jvm_cp_args\"" \
-Dscala.usejavacp=true \
"$setScriptName" \
"$PROG_NAME" \
"${scala_args[@]}" \
"${residual_args[@]}" \
"${scripting_string-}"
scala_exit_status=$?
"-Dscala.usejavacp=true" \
"-Dscala.home=$PROG_HOME" \
Comment on lines +89 to +90
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these things that the cs runner would be able to pass to MainGenericRunner?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CS can configure properties, but probably does not have a magic variable for the launch directory (-Dscala.home was copied from the Scala script for use in programs), I don't think it should be supported in CS because the directory structure is not equivalent

Copy link
Contributor

@philwalk philwalk May 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some scripts will fail if -Dscala.home=$PROG_HOME isn't defined.
And there is no simple alternative to figuring out the path to the current runtime scala.
Different scripts can specify different versions of scala via the hashbang line, so environment variables can't be relied on. See #13758
It might not matter for non-script code, although it can be useful there as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had a look and we can check if the property coursier.mainJar is set, which will be the path of the coursier launcher script - we can then use that to set scala.home to the parent directory. - problem here is that using the native script then scala is at s"${sys.props("scala.home")}/bin/scala", but for coursier it is s"${sys.props("scala.home")}/scala" (we could go one directory above to preserve /bin but I think this won't work on a custom Coursier install dir. [edit: I checked and for windows the default it also in some /bin directory])

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea from @sjrd it could likely be much more useful to provide properties that have the classpath of the scala compiler and scala standard libraries, that can be provided as arguments to the java command, this is more uniform than a fragile directory structure that depends on the package manager.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this suggestion, it would then be possible to find the first scala3-library*.jar in the classpath, and two directories up would be scala.home.

"dotty.tools.MainGenericCompiler" \
"${scala_args[@]}"

scala_exit_status=$?
onExit
14 changes: 14 additions & 0 deletions project/scripts/bootstrappedOnlyCmdTests
Expand Up @@ -47,12 +47,26 @@ echo "testing sbt scalac -decompile from file"
./bin/scalac -decompile -color:never "$OUT/$TASTY" > "$tmp"
grep -qe "def main(args: scala.Array\[scala.Predef.String\]): scala.Unit =" "$tmp"

# check that `sbt scalac -print-tasty` runs
echo "testing sbt scalac -print-tasty from file"
./bin/scalac -print-tasty -color:never "$OUT/$TASTY" > "$tmp"
grep -qe "118: STRINGconst 32 \[hello world\]" "$tmp"

echo "testing loading tasty from .tasty file in jar"
clear_out "$OUT"
./bin/scalac -d "$OUT/out.jar" "$SOURCE"
./bin/scalac -decompile -color:never "$OUT/out.jar" > "$tmp"
grep -qe "def main(args: scala.Array\[scala.Predef.String\]): scala.Unit =" "$tmp"

echo "testing printing tasty from .tasty file in jar"
./bin/scalac -print-tasty -color:never "$OUT/out.jar" > "$tmp"
grep -qe "118: STRINGconst 32 \[hello world\]" "$tmp"

echo "testing -script from scalac"
clear_out "$OUT"
./bin/scalac -script "$SOURCE" > "$tmp"
test "$EXPECTED_OUTPUT" = "$(cat "$tmp")"

echo "testing sbt scalac with suspension"
clear_out "$OUT"
"$SBT" "scala3-compiler-bootstrapped/scalac -d $OUT tests/pos-macros/macros-in-same-project-1/Bar.scala tests/pos-macros/macros-in-same-project-1/Foo.scala" > "$tmp"
Expand Down