From c95f4af476df82598f4d9d95c26853f8c6ae7f4e Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 10 Feb 2021 13:25:03 -0700 Subject: [PATCH 1/8] changes to dist/bin/scala script: 1. pass @ and -color:* options to compiler 2. add -save|-savecompiled option 3. recognize scripts with #!.*scala regardless of extension 4. if -save is specified and .jar file is newer than execute it (no compile) 5. set -Dscript.name for script execution paths (script and .jar) changes to dotty.tools.scripting package: 1. additional compiler args splitting and filtering 2. renamed detectMainMethod to detectMainClassAndMethod, returns both main class name and reflect.Method object 3. on -save option: a. if compile is successful, create same-name jar file in parent directory b. "java.class.path" appended to context classpath with deduplication c. write "Main-Class" and "Class-Path" to jar manifest added new tests to verify the following: 1. one line and multi-line hash bang sections are ignored by compiler 2. main class name in stack dump is as expected when main class is declared in script 3. main class name in stack dump is as expected when main class is not declared in script 4. script.name property matches scriptFile.getName 5. verify that with -save option jar file with expected name is generated 6. verify that without -save option, no jar file is generated 7. generated jar file is executable via "java -jar .jar" --- .../src/dotty/tools/dotc/core/Contexts.scala | 2 +- .../dotty/tools/dotc/util/SourceFile.scala | 42 +++++++++-- compiler/src/dotty/tools/scripting/Main.scala | 70 +++++++++++++++++-- .../tools/scripting/ScriptingDriver.scala | 27 ++++--- .../test-resources/scripting/hashBang.scala | 20 ++++++ .../scripting/mainClassOnStack.scala | 21 ++++++ .../test-resources/scripting/scriptName.scala | 13 ++++ dist/bin/scala | 26 ++++++- 8 files changed, 201 insertions(+), 20 deletions(-) create mode 100755 compiler/test-resources/scripting/hashBang.scala create mode 100755 compiler/test-resources/scripting/mainClassOnStack.scala create mode 100755 compiler/test-resources/scripting/scriptName.scala diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index d504809a9a2e..1489a2e74d95 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -273,7 +273,7 @@ object Contexts { /** Sourcefile corresponding to given abstract file, memoized */ def getSource(file: AbstractFile, codec: => Codec = Codec(settings.encoding.value)) = { util.Stats.record("Context.getSource") - base.sources.getOrElseUpdate(file, new SourceFile(file, codec)) + base.sources.getOrElseUpdate(file, SourceFile(file, codec)) } /** SourceFile with given path name, memoized */ diff --git a/compiler/src/dotty/tools/dotc/util/SourceFile.scala b/compiler/src/dotty/tools/dotc/util/SourceFile.scala index fcd8b2d55d06..a7f660c2b084 100644 --- a/compiler/src/dotty/tools/dotc/util/SourceFile.scala +++ b/compiler/src/dotty/tools/dotc/util/SourceFile.scala @@ -22,19 +22,35 @@ object ScriptSourceFile { @sharable private val headerPattern = Pattern.compile("""^(::)?!#.*(\r|\n|\r\n)""", Pattern.MULTILINE) private val headerStarts = List("#!", "::#!") + /** Return true if has a script header */ + def hasScriptHeader(content: Array[Char]): Boolean = { + headerStarts exists (content startsWith _) + } + def apply(file: AbstractFile, content: Array[Char]): SourceFile = { /** Length of the script header from the given content, if there is one. - * The header begins with "#!" or "::#!" and ends with a line starting - * with "!#" or "::!#". + * The header begins with "#!" or "::#!" and is either a single line, + * or it ends with a line starting with "!#" or "::!#", if present. */ val headerLength = if (headerStarts exists (content startsWith _)) { val matcher = headerPattern matcher content.mkString if (matcher.find) matcher.end - else throw new IOException("script file does not close its header with !# or ::!#") + else content.indexOf('\n') // end of first line } else 0 - new SourceFile(file, content drop headerLength) { + + // overwrite hash-bang lines with all spaces + val hashBangLines = content.take(headerLength).mkString.split("\\r?\\n") + if hashBangLines.nonEmpty then + for i <- 0 until headerLength do + content(i) match { + case '\r' | '\n' => + case _ => + content(i) = ' ' + } + + new SourceFile(file, content) { override val underlying = new SourceFile(this.file, this.content) } } @@ -245,6 +261,24 @@ object SourceFile { else sourcePath.toString } + + /** Return true if file is a script: + * if filename extension is not .scala and has a script header. + */ + def isScript(file: AbstractFile, content: Array[Char]): Boolean = + if file.hasExtension(".scala") then + false + else + ScriptSourceFile.hasScriptHeader(content) + + def apply(file: AbstractFile, codec: Codec): SourceFile = + // see note above re: Files.exists is remarkably slow + val chars = try new String(file.toByteArray, codec.charSet).toCharArray + catch case _: java.nio.file.NoSuchFileException => Array[Char]() + if isScript(file, chars) then + ScriptSourceFile(file, chars) + else + new SourceFile(file, chars) } @sharable object NoSource extends SourceFile(NoAbstractFile, Array[Char]()) { diff --git a/compiler/src/dotty/tools/scripting/Main.scala b/compiler/src/dotty/tools/scripting/Main.scala index f820421860a6..005b54f0fe18 100644 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -1,22 +1,82 @@ package dotty.tools.scripting import java.io.File +import java.nio.file.Path /** Main entry point to the Scripting execution engine */ object Main: /** All arguments before -script are compiler arguments. All arguments afterwards are script arguments.*/ - def distinguishArgs(args: Array[String]): (Array[String], File, Array[String]) = - val (compilerArgs, rest) = args.splitAt(args.indexOf("-script")) + private def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean) = + val (leftArgs, rest) = args.splitAt(args.indexOf("-script")) + if( rest.size < 2 ) then + sys.error(s"missing: -script ") + val file = File(rest(1)) val scriptArgs = rest.drop(2) - (compilerArgs, file, scriptArgs) + var saveJar = false + val compilerArgs = leftArgs.filter { + case "-save" | "-savecompiled" => + saveJar = true + false + case _ => + true + } + (compilerArgs, file, scriptArgs, saveJar) end distinguishArgs def main(args: Array[String]): Unit = - val (compilerArgs, scriptFile, scriptArgs) = distinguishArgs(args) - try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun() + val (compilerArgs, scriptFile, scriptArgs, saveJar) = distinguishArgs(args) + val driver = ScriptingDriver(compilerArgs, scriptFile, scriptArgs) + try driver.compileAndRun { (outDir:Path, classpath:String, mainClass: String) => + if saveJar then + // write a standalone jar to the script parent directory + writeJarfile(outDir, scriptFile, scriptArgs, classpath, mainClass) + } catch case ScriptingException(msg) => println(s"Error: $msg") sys.exit(1) + + case e: java.lang.reflect.InvocationTargetException => + throw e.getCause + + private def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String], + classpath:String, mainClassName: String): Unit = + + val javaClasspath = sys.props("java.class.path") + val runtimeClasspath = s"${classpath}$pathsep$javaClasspath" + + val jarTargetDir: Path = Option(scriptFile.toPath.getParent) match { + case None => sys.error(s"no parent directory for script file [$scriptFile]") + case Some(parent) => parent + } + + def scriptBasename = scriptFile.getName.takeWhile(_!='.') + val jarPath = s"$jarTargetDir/$scriptBasename.jar" + + val cpPaths = runtimeClasspath.split(pathsep).map { + // protect relative paths from being converted to absolute + case str if str.startsWith(".") && File(str).isDirectory => s"${str.withSlash}/" + case str if str.startsWith(".") => str.withSlash + case str => File(str).toURI.toURL.toString + } + + import java.util.jar.Attributes.Name + val cpString:String = cpPaths.distinct.mkString(" ") + val manifestAttributes:Seq[(Name, String)] = Seq( + (Name.MANIFEST_VERSION, "1.0.0"), + (Name.MAIN_CLASS, mainClassName), + (Name.CLASS_PATH, cpString), + ) + import dotty.tools.io.{Jar, Directory} + val jar = new Jar(jarPath) + val writer = jar.jarWriter(manifestAttributes:_*) + writer.writeAllFrom(Directory(outDir)) + end writeJarfile + + def pathsep = sys.props("path.separator") + + extension(pathstr:String) { + def withSlash:String = pathstr.replace('\\', '/') + } diff --git a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala index d5fad4ef520a..f3f88c0a0422 100644 --- a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala +++ b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala @@ -17,7 +17,7 @@ import dotty.tools.dotc.config.Settings.Setting._ import sys.process._ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver: - def compileAndRun(): Unit = + def compileAndRun(pack:(Path, String, String) => Unit = null): Unit = val outDir = Files.createTempDirectory("scala3-scripting") val (toCompile, rootCtx) = setup(compilerArgs :+ scriptFile.getAbsolutePath, initCtx.fresh) given Context = rootCtx.fresh.setSetting(rootCtx.settings.outputDir, @@ -26,7 +26,14 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: if doCompile(newCompiler, toCompile).hasErrors then throw ScriptingException("Errors encountered during compilation") - try detectMainMethod(outDir, ctx.settings.classpath.value).invoke(null, scriptArgs) + try + val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, ctx.settings.classpath.value, scriptFile) + Option(pack) match + case Some(func) => + func(outDir, ctx.settings.classpath.value, mainClass) + case None => + end match + mainMethod.invoke(null, scriptArgs) catch case e: java.lang.reflect.InvocationTargetException => throw e.getCause @@ -41,12 +48,13 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: target.delete() end deleteFile - private def detectMainMethod(outDir: Path, classpath: String): Method = + private def detectMainClassAndMethod(outDir: Path, classpath: String, + scriptFile: File): (String, Method) = val outDirURL = outDir.toUri.toURL - val classpathUrls = classpath.split(":").map(File(_).toURI.toURL) + val classpathUrls = classpath.split(pathsep).map(File(_).toURI.toURL) val cl = URLClassLoader(classpathUrls :+ outDirURL) - def collectMainMethods(target: File, path: String): List[Method] = + def collectMainMethods(target: File, path: String): List[(String, Method)] = val nameWithoutExtension = target.getName.takeWhile(_ != '.') val targetPath = if path.nonEmpty then s"${path}.${nameWithoutExtension}" @@ -61,7 +69,7 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: val cls = cl.loadClass(targetPath) try val method = cls.getMethod("main", classOf[Array[String]]) - if Modifier.isStatic(method.getModifiers) then List(method) else Nil + if Modifier.isStatic(method.getModifiers) then List((cls.getName, method)) else Nil catch case _: java.lang.NoSuchMethodException => Nil else Nil @@ -74,13 +82,16 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: candidates match case Nil => - throw ScriptingException("No main methods detected in your script") + throw ScriptingException(s"No main methods detected in script ${scriptFile}") case _ :: _ :: _ => throw ScriptingException("A script must contain only one main method. " + s"Detected the following main methods:\n${candidates.mkString("\n")}") case m :: Nil => m end match - end detectMainMethod + end detectMainClassAndMethod + + def pathsep = sys.props("path.separator") + end ScriptingDriver case class ScriptingException(msg: String) extends RuntimeException(msg) diff --git a/compiler/test-resources/scripting/hashBang.scala b/compiler/test-resources/scripting/hashBang.scala new file mode 100755 index 000000000000..1aab26269f86 --- /dev/null +++ b/compiler/test-resources/scripting/hashBang.scala @@ -0,0 +1,20 @@ +#!/usr/bin/env scala +# comment +STUFF=nada +!# + +def main(args: Array[String]): Unit = + System.err.printf("mainClassFromStack: %s\n",mainFromStack) + assert(mainFromStack.contains("hashBang"),s"fromStack[$mainFromStack]") + + lazy val mainFromStack:String = { + val result = new java.io.StringWriter() + new RuntimeException("stack").printStackTrace(new java.io.PrintWriter(result)) + val stack = result.toString.split("[\r\n]+").toList + //for( s <- stack ){ System.err.printf("[%s]\n",s) } + stack.filter { str => str.contains(".main(") }.map { + _.replaceAll(".*[(]",""). + replaceAll("\\.main\\(.*",""). + replaceAll(".scala.*","") + }.distinct.take(1).mkString("") + } diff --git a/compiler/test-resources/scripting/mainClassOnStack.scala b/compiler/test-resources/scripting/mainClassOnStack.scala new file mode 100755 index 000000000000..ac8eff4316cf --- /dev/null +++ b/compiler/test-resources/scripting/mainClassOnStack.scala @@ -0,0 +1,21 @@ +#!/usr/bin/env scala +export STUFF=nada +#lots of other stuff that isn't valid scala +!# +object Zoo { + def main(args: Array[String]): Unit = + printf("mainClassFromStack: %s\n",mainFromStack) + assert(mainFromStack == "Zoo",s"fromStack[$mainFromStack]") + + lazy val mainFromStack:String = { + val result = new java.io.StringWriter() + new RuntimeException("stack").printStackTrace(new java.io.PrintWriter(result)) + val stack = result.toString.split("[\r\n]+").toList + // for( s <- stack ){ System.err.printf("[%s]\n",s) } + val shortStack = stack.filter { str => str.contains(".main(") && ! str.contains("$") }.map { + _.replaceAll("[.].*","").replaceAll("\\s+at\\s+","") + } + // for( s <- shortStack ){ System.err.printf("[%s]\n",s) } + shortStack.take(1).mkString("|") + } +} diff --git a/compiler/test-resources/scripting/scriptName.scala b/compiler/test-resources/scripting/scriptName.scala new file mode 100755 index 000000000000..c78e921811ef --- /dev/null +++ b/compiler/test-resources/scripting/scriptName.scala @@ -0,0 +1,13 @@ +#!/usr/bin/env scala + + def main(args: Array[String]): Unit = + val name = Option(sys.props("script.name")) match { + case None => printf("no script.name property is defined\n") + case Some(name) => + if( name == null ){ + printf("unexpected null script.name property") + } else { + printf("script.name: %s\n",name) + assert(name == "scriptName.scala") + } + } diff --git a/dist/bin/scala b/dist/bin/scala index 3c522a082d3b..010b57a5bf1e 100755 --- a/dist/bin/scala +++ b/dist/bin/scala @@ -45,6 +45,7 @@ execute_script=false with_compiler=false class_path_count=0 CLASS_PATH="" +save_compiled=false # Little hack to check if all arguments are options all_params="$*" @@ -72,6 +73,15 @@ while [[ $# -gt 0 ]]; do with_compiler=true shift ;; + @*|-color:*) + addDotcOptions "${1}" + shift + ;; + -save|-savecompiled) + save_compiled=1 + addDotcOptions "${1}" + shift + ;; -d) DEBUG="$DEBUG_STR" shift @@ -82,8 +92,10 @@ while [[ $# -gt 0 ]]; do shift ;; *) if [ $execute_script == false ]; then - if [[ "$1" == *.scala ]]; then + # is a script if extension .scala or .sc or if has scala hash bang + if [[ "$1" == *.scala || "$1" == *.sc || -f "$1" && `head -n 1 -- "$1" | grep '#!.*scala'` ]]; then execute_script=true + [ -n "$SCALA_OPTS" ] && java_options+=($SCALA_OPTS) target_script="$1" else residual_args+=("$1") @@ -101,7 +113,17 @@ if [ $execute_script == true ]; then if [ "$CLASS_PATH" ]; then cp_arg="-classpath \"$CLASS_PATH\"" fi - eval "\"$PROG_HOME/bin/scalac\" $cp_arg ${java_options[@]} ${residual_args[@]} -script $target_script ${script_args[@]}" + target_jar="${target_script%.*}.jar" + jar_found=false + setScriptName="-Dscript.name=${target_script##*/}" + [[ $save_compiled == true && -f "$target_jar" ]] && jar_found=true + if [[ $jar_found == true && "$target_jar" -nt "$target_script" ]]; then + java $setScriptName -jar "$target_jar" "${script_args[@]}" + else + [[ $save_compiled == true && -f $target_jar ]] && rm -f $target_jar + residual_args+=($setScriptName) + eval "\"$PROG_HOME/bin/scalac\" $cp_arg ${java_options[@]} ${residual_args[@]} -script $target_script ${script_args[@]}" + fi elif [ $execute_repl == true ] || ([ $execute_run == false ] && [ $options_indicator == 0 ]); then if [ "$CLASS_PATH" ]; then cp_arg="-classpath \"$CLASS_PATH\"" From 6c0a15e45e27d04f9f5cf887e1ab521cc2dd7fab Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 10 Feb 2021 15:29:28 -0700 Subject: [PATCH 2/8] changes to dist/bin/scala script: 1. pass @ and -color:* options to compiler 2. add -save|-savecompiled option 3. recognize scripts with #!.*scala regardless of extension 4. if -save is specified and .jar file is newer than execute it (no compile) 5. set -Dscript.path for script execution paths (script and .jar) changes to dotty.tools.scripting package: 1. additional compiler args splitting and filtering 2. renamed detectMainMethod to detectMainClassAndMethod, returns both main class name and reflect.Method object 3. on -save option: a. if compile is successful, create same-name jar file in parent directory b. "java.class.path" appended to context classpath with deduplication c. write "Main-Class" and "Class-Path" to jar manifest added new tests to verify the following: 1. one line and multi-line hash bang sections are ignored by compiler 2. main class name in stack dump is as expected when main class is declared in script 3. main class name in stack dump is as expected when main class is not declared in script 4. script.path property matches scriptFile.absPath 5. verify that with -save option jar file with expected name is generated 6. verify that without -save option, no jar file is generated 7. generated jar file is executable via "java -jar .jar" --- compiler/src/dotty/tools/scripting/Main.scala | 2 +- .../scripting/{hashBang.scala => hashBang.sc} | 15 ++- .../scripting/mainClassOnStack.sc | 29 ++++++ .../scripting/mainClassOnStack.scala | 21 ----- .../test-resources/scripting/scriptName.scala | 13 --- .../scripting/scriptParent.scala | 14 +++ .../test-resources/scripting/scriptPath.sc | 10 ++ .../tools/scripting/ScriptingTests.scala | 91 ++++++++++++++++--- dist/bin/scala | 2 +- 9 files changed, 142 insertions(+), 55 deletions(-) rename compiler/test-resources/scripting/{hashBang.scala => hashBang.sc} (53%) mode change 100755 => 100644 create mode 100644 compiler/test-resources/scripting/mainClassOnStack.sc delete mode 100755 compiler/test-resources/scripting/mainClassOnStack.scala delete mode 100755 compiler/test-resources/scripting/scriptName.scala create mode 100644 compiler/test-resources/scripting/scriptParent.scala create mode 100644 compiler/test-resources/scripting/scriptPath.sc diff --git a/compiler/src/dotty/tools/scripting/Main.scala b/compiler/src/dotty/tools/scripting/Main.scala index 005b54f0fe18..83d215ea2d21 100644 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -65,7 +65,7 @@ object Main: import java.util.jar.Attributes.Name val cpString:String = cpPaths.distinct.mkString(" ") val manifestAttributes:Seq[(Name, String)] = Seq( - (Name.MANIFEST_VERSION, "1.0.0"), + (Name.MANIFEST_VERSION, "1.0"), (Name.MAIN_CLASS, mainClassName), (Name.CLASS_PATH, cpString), ) diff --git a/compiler/test-resources/scripting/hashBang.scala b/compiler/test-resources/scripting/hashBang.sc old mode 100755 new mode 100644 similarity index 53% rename from compiler/test-resources/scripting/hashBang.scala rename to compiler/test-resources/scripting/hashBang.sc index 1aab26269f86..d767bd1a1592 --- a/compiler/test-resources/scripting/hashBang.scala +++ b/compiler/test-resources/scripting/hashBang.sc @@ -2,8 +2,9 @@ # comment STUFF=nada !# - +// everything above this point should be ignored by the compiler def main(args: Array[String]): Unit = + args.zipWithIndex.foreach { case (arg,i) => printf("arg %d: [%s]\n",i,arg) } System.err.printf("mainClassFromStack: %s\n",mainFromStack) assert(mainFromStack.contains("hashBang"),s"fromStack[$mainFromStack]") @@ -11,10 +12,14 @@ def main(args: Array[String]): Unit = val result = new java.io.StringWriter() new RuntimeException("stack").printStackTrace(new java.io.PrintWriter(result)) val stack = result.toString.split("[\r\n]+").toList - //for( s <- stack ){ System.err.printf("[%s]\n",s) } + if verbose then for( s <- stack ){ System.err.printf("[%s]\n",s) } stack.filter { str => str.contains(".main(") }.map { - _.replaceAll(".*[(]",""). - replaceAll("\\.main\\(.*",""). - replaceAll(".scala.*","") + // derive main class name from stack when main object is NOT declared in source + _.replaceAll("[.].*",""). + replaceAll("\\s+at\\s+","") }.distinct.take(1).mkString("") } + + lazy val verbose = Option(System.getenv("DOTC_VERBOSE")) match + case None => false + case _ => true diff --git a/compiler/test-resources/scripting/mainClassOnStack.sc b/compiler/test-resources/scripting/mainClassOnStack.sc new file mode 100644 index 000000000000..a0c974555e76 --- /dev/null +++ b/compiler/test-resources/scripting/mainClassOnStack.sc @@ -0,0 +1,29 @@ +#!/usr/bin/env scala +export STUFF=nada +#lots of other stuff that isn't valid scala +!# +// everything above this point should be ignored by the compiler +object Zoo { + def main(args: Array[String]): Unit = + args.zipWithIndex.foreach { case (arg,i) => printf("arg %d: [%s]\n",i,arg) } + printf("mainClassFromStack: %s\n",mainClassFromStack) + assert(mainClassFromStack == "Zoo",s"fromStack[$mainClassFromStack]") + + lazy val mainClassFromStack:String = { + val result = new java.io.StringWriter() + new RuntimeException("stack").printStackTrace(new java.io.PrintWriter(result)) + val stack = result.toString.split("[\r\n]+").toList + if verbose then for( s <- stack ){ System.err.printf("[%s]\n",s) } + val shortStack = stack.filter { str => str.contains(".main(") && ! str.contains("$") }.map { + // derive main class name from stack when main object is declared in source + _.replaceAll("[.].*",""). + replaceAll("\\s+at\\s+","") + } + // for( s <- shortStack ){ System.err.printf("[%s]\n",s) } + shortStack.take(1).mkString("|") + } + + lazy val verbose = Option(System.getenv("DOTC_VERBOSE")) match + case None => false + case _ => true +} diff --git a/compiler/test-resources/scripting/mainClassOnStack.scala b/compiler/test-resources/scripting/mainClassOnStack.scala deleted file mode 100755 index ac8eff4316cf..000000000000 --- a/compiler/test-resources/scripting/mainClassOnStack.scala +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env scala -export STUFF=nada -#lots of other stuff that isn't valid scala -!# -object Zoo { - def main(args: Array[String]): Unit = - printf("mainClassFromStack: %s\n",mainFromStack) - assert(mainFromStack == "Zoo",s"fromStack[$mainFromStack]") - - lazy val mainFromStack:String = { - val result = new java.io.StringWriter() - new RuntimeException("stack").printStackTrace(new java.io.PrintWriter(result)) - val stack = result.toString.split("[\r\n]+").toList - // for( s <- stack ){ System.err.printf("[%s]\n",s) } - val shortStack = stack.filter { str => str.contains(".main(") && ! str.contains("$") }.map { - _.replaceAll("[.].*","").replaceAll("\\s+at\\s+","") - } - // for( s <- shortStack ){ System.err.printf("[%s]\n",s) } - shortStack.take(1).mkString("|") - } -} diff --git a/compiler/test-resources/scripting/scriptName.scala b/compiler/test-resources/scripting/scriptName.scala deleted file mode 100755 index c78e921811ef..000000000000 --- a/compiler/test-resources/scripting/scriptName.scala +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env scala - - def main(args: Array[String]): Unit = - val name = Option(sys.props("script.name")) match { - case None => printf("no script.name property is defined\n") - case Some(name) => - if( name == null ){ - printf("unexpected null script.name property") - } else { - printf("script.name: %s\n",name) - assert(name == "scriptName.scala") - } - } diff --git a/compiler/test-resources/scripting/scriptParent.scala b/compiler/test-resources/scripting/scriptParent.scala new file mode 100644 index 000000000000..709f8a19a09f --- /dev/null +++ b/compiler/test-resources/scripting/scriptParent.scala @@ -0,0 +1,14 @@ +import java.nio.file.Paths + +object ScriptParent { + def main(args: Array[String]): Unit = { + args.zipWithIndex.foreach { case (arg,i) => printf("arg %d: [%s]\n",i,arg) } + val scriptName = Option(sys.props("script.path")) match { + case None => + printf("no script.path property\n") + case Some(script) => + val p = Paths.get(script).toAbsolutePath.toFile.getParent + printf("parentDir: [%s]\n",p) + } + } +} diff --git a/compiler/test-resources/scripting/scriptPath.sc b/compiler/test-resources/scripting/scriptPath.sc new file mode 100644 index 000000000000..49ed65a76515 --- /dev/null +++ b/compiler/test-resources/scripting/scriptPath.sc @@ -0,0 +1,10 @@ +#!/usr/bin/env scala + + def main(args: Array[String]): Unit = + args.zipWithIndex.foreach { case (arg,i) => printf("arg %d: [%s]\n",i,arg) } + val path = Option(sys.props("script.path")) match { + case None => printf("no script.path property is defined\n") + case Some(path) => + printf("script.path: %s\n",path) + assert(path.endsWith("scriptPath.sc"),s"actual path [$path]") + } diff --git a/compiler/test/dotty/tools/scripting/ScriptingTests.scala b/compiler/test/dotty/tools/scripting/ScriptingTests.scala index 547f525e98e4..af04524193b1 100644 --- a/compiler/test/dotty/tools/scripting/ScriptingTests.scala +++ b/compiler/test/dotty/tools/scripting/ScriptingTests.scala @@ -9,31 +9,94 @@ import org.junit.Test import vulpix.TestConfiguration -/** Runs all tests contained in `compiler/test-resources/repl/` */ +/** Runs all tests contained in `compiler/test-resources/scripting/` */ class ScriptingTests: extension (str: String) def dropExtension = str.reverse.dropWhile(_ != '.').drop(1).reverse - @Test def scriptingTests = - val testFiles = scripts("/scripting") + def testFiles = scripts("/scripting") - val argss: Map[String, Array[String]] = ( - for - argFile <- testFiles - if argFile.getName.endsWith(".args") - name = argFile.getName.dropExtension - scriptArgs = readLines(argFile).toArray - yield name -> scriptArgs).toMap + def script2jar(scriptFile: File) = + val jarName = s"${scriptFile.getName.dropExtension}.jar" + File(scriptFile.getParent,jarName) + def showScriptUnderTest(scriptFile: File): Unit = + printf("===> test script name [%s]\n",scriptFile.getName) + + + val argss: Map[String, Array[String]] = ( + for + argFile <- testFiles + if argFile.getName.endsWith(".args") + name = argFile.getName.dropExtension + scriptArgs = readLines(argFile).toArray + yield name -> scriptArgs).toMap + + def scalaFilesWithArgs(extension: String) = ( for scriptFile <- testFiles - if scriptFile.getName.endsWith(".scala") + if scriptFile.getName.endsWith(extension) name = scriptFile.getName.dropExtension scriptArgs = argss.getOrElse(name, Array.empty[String]) - do + yield scriptFile -> scriptArgs).toList.sortBy { (file,args) => file.getName } + + @Test def scriptingDriverTests = + + for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".scala") do + showScriptUnderTest(scriptFile) + val unexpectedJar = script2jar(scriptFile) + unexpectedJar.delete + + sys.props("script.path") = scriptFile.absPath ScriptingDriver( compilerArgs = Array( - "-classpath", TestConfiguration.basicClasspath), + "-classpath", TestConfiguration.basicClasspath + ), scriptFile = scriptFile, scriptArgs = scriptArgs - ).compileAndRun() + ).compileAndRun { (path:java.nio.file.Path,classpath:String, mainClass:String) => + printf("mainClass from ScriptingDriver: %s\n",mainClass) + } + assert(! unexpectedJar.exists, s"not expecting jar file: ${unexpectedJar.absPath}") + + @Test def scriptingMainTests = + for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".sc") do + showScriptUnderTest(scriptFile) + val unexpectedJar = script2jar(scriptFile) + unexpectedJar.delete + + sys.props("script.path") = scriptFile.absPath + val mainArgs: Array[String] = Array( + "-classpath", TestConfiguration.basicClasspath.toString, + "-script", scriptFile.toString, + ) ++ scriptArgs + + Main.main(mainArgs) + assert(! unexpectedJar.exists, s"not expecting jar file: ${unexpectedJar.absPath}") + + @Test def scriptingJarTest = + for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".sc") do + showScriptUnderTest(scriptFile) + val expectedJar = script2jar(scriptFile) + expectedJar.delete + + sys.props("script.path") = scriptFile.absPath + val mainArgs: Array[String] = Array( + "-classpath", TestConfiguration.basicClasspath.toString, + "-save", + "-script", scriptFile.toString, + ) ++ scriptArgs + + Main.main(mainArgs) + + printf("===> test script jar name [%s]\n",expectedJar.getName) + assert(expectedJar.exists) + + import scala.sys.process._ + val cmd = Array("java",s"-Dscript.path=${scriptFile.getName}","-jar",expectedJar.absPath) + ++ scriptArgs + Process(cmd).lazyLines_!.foreach { println } + + extension(f: File){ + def absPath = f.getAbsolutePath.replace('\\','/') + } diff --git a/dist/bin/scala b/dist/bin/scala index 010b57a5bf1e..14e3a5ffaed5 100755 --- a/dist/bin/scala +++ b/dist/bin/scala @@ -115,7 +115,7 @@ if [ $execute_script == true ]; then fi target_jar="${target_script%.*}.jar" jar_found=false - setScriptName="-Dscript.name=${target_script##*/}" + setScriptName="-Dscript.path=$target_script" [[ $save_compiled == true && -f "$target_jar" ]] && jar_found=true if [[ $jar_found == true && "$target_jar" -nt "$target_script" ]]; then java $setScriptName -jar "$target_jar" "${script_args[@]}" From 693ffc559bfaa823dce73a68969e67d9a33f09e4 Mon Sep 17 00:00:00 2001 From: Phil Date: Thu, 11 Feb 2021 19:08:23 -0700 Subject: [PATCH 3/8] more robust script detection in dist/bin/scala --- dist/bin/scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dist/bin/scala b/dist/bin/scala index 14e3a5ffaed5..fa129f390cf5 100755 --- a/dist/bin/scala +++ b/dist/bin/scala @@ -78,7 +78,7 @@ while [[ $# -gt 0 ]]; do shift ;; -save|-savecompiled) - save_compiled=1 + save_compiled=true addDotcOptions "${1}" shift ;; @@ -93,10 +93,11 @@ while [[ $# -gt 0 ]]; do *) if [ $execute_script == false ]; then # is a script if extension .scala or .sc or if has scala hash bang - if [[ "$1" == *.scala || "$1" == *.sc || -f "$1" && `head -n 1 -- "$1" | grep '#!.*scala'` ]]; then + if [[ -e "$1" && ("$1" == *.scala || "$1" == *.sc || -f "$1" && `head -n 1 -- "$1" | grep '#!.*scala'`) ]]; then execute_script=true [ -n "$SCALA_OPTS" ] && java_options+=($SCALA_OPTS) target_script="$1" + if [ ! -f $target_script else residual_args+=("$1") fi @@ -113,9 +114,9 @@ if [ $execute_script == true ]; then if [ "$CLASS_PATH" ]; then cp_arg="-classpath \"$CLASS_PATH\"" fi + setScriptName="-Dscript.path=$target_script" target_jar="${target_script%.*}.jar" jar_found=false - setScriptName="-Dscript.path=$target_script" [[ $save_compiled == true && -f "$target_jar" ]] && jar_found=true if [[ $jar_found == true && "$target_jar" -nt "$target_script" ]]; then java $setScriptName -jar "$target_jar" "${script_args[@]}" From 20a7533be71e917600e96cb3d2c798715c64aff6 Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 12 Feb 2021 05:51:40 -0700 Subject: [PATCH 4/8] fix problem in dist/bin/script, simplify target_jar test --- dist/bin/scala | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/dist/bin/scala b/dist/bin/scala index fa129f390cf5..19d64eb4d3c3 100755 --- a/dist/bin/scala +++ b/dist/bin/scala @@ -95,9 +95,7 @@ while [[ $# -gt 0 ]]; do # is a script if extension .scala or .sc or if has scala hash bang if [[ -e "$1" && ("$1" == *.scala || "$1" == *.sc || -f "$1" && `head -n 1 -- "$1" | grep '#!.*scala'`) ]]; then execute_script=true - [ -n "$SCALA_OPTS" ] && java_options+=($SCALA_OPTS) target_script="$1" - if [ ! -f $target_script else residual_args+=("$1") fi @@ -114,14 +112,18 @@ if [ $execute_script == true ]; then if [ "$CLASS_PATH" ]; then cp_arg="-classpath \"$CLASS_PATH\"" fi + if [ -n "$SCALA_OPTS" ]; then + java_options+=($SCALA_OPTS) + if [ "${SCALA_OPTS##-save}" != "${SCALA_OPTS}" ]; then + save_compiled=true + fi + fi setScriptName="-Dscript.path=$target_script" target_jar="${target_script%.*}.jar" - jar_found=false - [[ $save_compiled == true && -f "$target_jar" ]] && jar_found=true - if [[ $jar_found == true && "$target_jar" -nt "$target_script" ]]; then + if [[ $save_compiled == true && "$target_jar" -nt "$target_script" ]]; then java $setScriptName -jar "$target_jar" "${script_args[@]}" else - [[ $save_compiled == true && -f $target_jar ]] && rm -f $target_jar + [[ $save_compiled == true ]] && rm -f $target_jar residual_args+=($setScriptName) eval "\"$PROG_HOME/bin/scalac\" $cp_arg ${java_options[@]} ${residual_args[@]} -script $target_script ${script_args[@]}" fi From 355583872991d5c8b961dcc7ec7dba5eafd36f4c Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 12 Feb 2021 06:25:29 -0700 Subject: [PATCH 5/8] resolved requested changes --- .../dotty/tools/dotc/util/SourceFile.scala | 26 +++++++++---------- compiler/src/dotty/tools/scripting/Main.scala | 5 +++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/SourceFile.scala b/compiler/src/dotty/tools/dotc/util/SourceFile.scala index a7f660c2b084..9f555638a40e 100644 --- a/compiler/src/dotty/tools/dotc/util/SourceFile.scala +++ b/compiler/src/dotty/tools/dotc/util/SourceFile.scala @@ -23,9 +23,8 @@ object ScriptSourceFile { private val headerStarts = List("#!", "::#!") /** Return true if has a script header */ - def hasScriptHeader(content: Array[Char]): Boolean = { - headerStarts exists (content startsWith _) - } + def hasScriptHeader(content: Array[Char]): Boolean = + headerStarts.exists(content.startsWith(_)) def apply(file: AbstractFile, content: Array[Char]): SourceFile = { /** Length of the script header from the given content, if there is one. @@ -40,14 +39,14 @@ object ScriptSourceFile { } else 0 - // overwrite hash-bang lines with all spaces + // overwrite hash-bang lines with all spaces to preserve line numbers val hashBangLines = content.take(headerLength).mkString.split("\\r?\\n") if hashBangLines.nonEmpty then for i <- 0 until headerLength do content(i) match { - case '\r' | '\n' => - case _ => - content(i) = ' ' + case '\r' | '\n' => + case _ => + content(i) = ' ' } new SourceFile(file, content) { @@ -266,15 +265,16 @@ object SourceFile { * if filename extension is not .scala and has a script header. */ def isScript(file: AbstractFile, content: Array[Char]): Boolean = - if file.hasExtension(".scala") then - false - else - ScriptSourceFile.hasScriptHeader(content) + ScriptSourceFile.hasScriptHeader(content) def apply(file: AbstractFile, codec: Codec): SourceFile = // see note above re: Files.exists is remarkably slow - val chars = try new String(file.toByteArray, codec.charSet).toCharArray - catch case _: java.nio.file.NoSuchFileException => Array[Char]() + val chars = + try + new String(file.toByteArray, codec.charSet).toCharArray + catch + case _: java.nio.file.NoSuchFileException => Array[Char]() + if isScript(file, chars) then ScriptSourceFile(file, chars) else diff --git a/compiler/src/dotty/tools/scripting/Main.scala b/compiler/src/dotty/tools/scripting/Main.scala index 83d215ea2d21..0808d87c850e 100644 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -72,7 +72,10 @@ object Main: import dotty.tools.io.{Jar, Directory} val jar = new Jar(jarPath) val writer = jar.jarWriter(manifestAttributes:_*) - writer.writeAllFrom(Directory(outDir)) + try + writer.writeAllFrom(Directory(outDir)) + finally + writer.close() end writeJarfile def pathsep = sys.props("path.separator") From 3bce0a08415fa95d850994ca74f2a43f0327bc39 Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 12 Feb 2021 09:36:12 -0700 Subject: [PATCH 6/8] corrected save_compiled test --- dist/bin/scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dist/bin/scala b/dist/bin/scala index 19d64eb4d3c3..2220d646b981 100755 --- a/dist/bin/scala +++ b/dist/bin/scala @@ -109,12 +109,13 @@ while [[ $# -gt 0 ]]; do done if [ $execute_script == true ]; then + [ -n "$script_trace" ] && set -x if [ "$CLASS_PATH" ]; then cp_arg="-classpath \"$CLASS_PATH\"" fi if [ -n "$SCALA_OPTS" ]; then java_options+=($SCALA_OPTS) - if [ "${SCALA_OPTS##-save}" != "${SCALA_OPTS}" ]; then + if [ "${SCALA_OPTS##*-save}" != "${SCALA_OPTS}" ]; then save_compiled=true fi fi From 4604261630343e45c514561819354b2f1d544658 Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 12 Feb 2021 11:14:22 -0700 Subject: [PATCH 7/8] replaced sys.error with assertion w/internal error message --- compiler/src/dotty/tools/scripting/Main.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/scripting/Main.scala b/compiler/src/dotty/tools/scripting/Main.scala index 0808d87c850e..b9ece33e0a0b 100644 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -9,8 +9,7 @@ object Main: All arguments afterwards are script arguments.*/ private def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean) = val (leftArgs, rest) = args.splitAt(args.indexOf("-script")) - if( rest.size < 2 ) then - sys.error(s"missing: -script ") + assert(rest.size >= 2,s"internal error: rest == Array(${rest.mkString(",")})") val file = File(rest(1)) val scriptArgs = rest.drop(2) From be1072fc5e186d9393d103983c2bc8c269fc806e Mon Sep 17 00:00:00 2001 From: Phil Date: Sun, 14 Feb 2021 16:50:36 -0700 Subject: [PATCH 8/8] + added more robust detection of relative classpath entries + convert relative to absolute entries in manifest for equivalent classpath. manifest classpath entries would be relative to jar file location, not cwd. + added "-compile-only" option for compile without calling script main + added tests to verify "-compile-only" option + cleanup tests, update comments. + extensive manual experimentation on Windows and Ubuntu + jar file startup latency is 1/3 to 1/4 of compile and invoke latency. + manual tests of graalvm native-image compile of generated script jars --- compiler/src/dotty/tools/scripting/Main.scala | 46 ++++++-- .../tools/scripting/ScriptingDriver.scala | 16 +-- .../test-resources/scripting/touchFile.sc | 8 ++ .../tools/scripting/ScriptingTests.scala | 101 ++++++++++++++++-- dist/bin/scala | 10 +- 5 files changed, 146 insertions(+), 35 deletions(-) create mode 100755 compiler/test-resources/scripting/touchFile.sc diff --git a/compiler/src/dotty/tools/scripting/Main.scala b/compiler/src/dotty/tools/scripting/Main.scala index b9ece33e0a0b..403af7b46435 100644 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -2,35 +2,41 @@ package dotty.tools.scripting import java.io.File import java.nio.file.Path +import dotty.tools.dotc.config.Properties.isWin /** Main entry point to the Scripting execution engine */ object Main: /** All arguments before -script are compiler arguments. All arguments afterwards are script arguments.*/ - private def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean) = + private def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean, Boolean) = val (leftArgs, rest) = args.splitAt(args.indexOf("-script")) assert(rest.size >= 2,s"internal error: rest == Array(${rest.mkString(",")})") val file = File(rest(1)) val scriptArgs = rest.drop(2) var saveJar = false + var invokeFlag = true // by default, script main method is invoked val compilerArgs = leftArgs.filter { case "-save" | "-savecompiled" => saveJar = true false + case "-compile-only" => + invokeFlag = false // no call to script main method + false case _ => true } - (compilerArgs, file, scriptArgs, saveJar) + (compilerArgs, file, scriptArgs, saveJar, invokeFlag) end distinguishArgs def main(args: Array[String]): Unit = - val (compilerArgs, scriptFile, scriptArgs, saveJar) = distinguishArgs(args) + val (compilerArgs, scriptFile, scriptArgs, saveJar, invokeFlag) = distinguishArgs(args) val driver = ScriptingDriver(compilerArgs, scriptFile, scriptArgs) try driver.compileAndRun { (outDir:Path, classpath:String, mainClass: String) => if saveJar then // write a standalone jar to the script parent directory writeJarfile(outDir, scriptFile, scriptArgs, classpath, mainClass) + invokeFlag } catch case ScriptingException(msg) => @@ -54,12 +60,7 @@ object Main: def scriptBasename = scriptFile.getName.takeWhile(_!='.') val jarPath = s"$jarTargetDir/$scriptBasename.jar" - val cpPaths = runtimeClasspath.split(pathsep).map { - // protect relative paths from being converted to absolute - case str if str.startsWith(".") && File(str).isDirectory => s"${str.withSlash}/" - case str if str.startsWith(".") => str.withSlash - case str => File(str).toURI.toURL.toString - } + val cpPaths = runtimeClasspath.split(pathsep).map(_.absPath) import java.util.jar.Attributes.Name val cpString:String = cpPaths.distinct.mkString(" ") @@ -79,6 +80,29 @@ object Main: def pathsep = sys.props("path.separator") - extension(pathstr:String) { - def withSlash:String = pathstr.replace('\\', '/') + + extension(file: File){ + def norm: String = file.toString.norm + } + + extension(path: String) { + // Normalize path separator, convert relative path to absolute + def norm: String = + path.replace('\\', '/') match { + case s if s.secondChar == ":" => s.drop(2) + case s if s.startsWith("./") => s.drop(2) + case s => s + } + + // convert to absolute path relative to cwd. + def absPath: String = norm match + case str if str.isAbsolute => norm + case _ => s"/${sys.props("user.dir").norm}/$norm" + + def absFile: File = File(path.absPath) + + // Treat norm paths with a leading '/' as absolute. + // Windows java.io.File#isAbsolute treats them as relative. + def isAbsolute = path.norm.startsWith("/") || (isWin && path.secondChar == ":") + def secondChar: String = path.take(2).drop(1).mkString("") } diff --git a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala index f3f88c0a0422..a23840b9f8e0 100644 --- a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala +++ b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala @@ -17,7 +17,7 @@ import dotty.tools.dotc.config.Settings.Setting._ import sys.process._ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver: - def compileAndRun(pack:(Path, String, String) => Unit = null): Unit = + def compileAndRun(pack:(Path, String, String) => Boolean = null): Unit = val outDir = Files.createTempDirectory("scala3-scripting") val (toCompile, rootCtx) = setup(compilerArgs :+ scriptFile.getAbsolutePath, initCtx.fresh) given Context = rootCtx.fresh.setSetting(rootCtx.settings.outputDir, @@ -28,12 +28,14 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: try val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, ctx.settings.classpath.value, scriptFile) - Option(pack) match - case Some(func) => - func(outDir, ctx.settings.classpath.value, mainClass) - case None => - end match - mainMethod.invoke(null, scriptArgs) + val invokeMain: Boolean = + Option(pack) match + case Some(func) => + func(outDir, ctx.settings.classpath.value, mainClass) + case None => + true + end match + if invokeMain then mainMethod.invoke(null, scriptArgs) catch case e: java.lang.reflect.InvocationTargetException => throw e.getCause diff --git a/compiler/test-resources/scripting/touchFile.sc b/compiler/test-resources/scripting/touchFile.sc new file mode 100755 index 000000000000..974f8a64d192 --- /dev/null +++ b/compiler/test-resources/scripting/touchFile.sc @@ -0,0 +1,8 @@ +#!/usr/bin/env scala + +import java.io.File + +// create an empty file +def main(args: Array[String]): Unit = + val file = File("touchedFile.out") + file.createNewFile(); diff --git a/compiler/test/dotty/tools/scripting/ScriptingTests.scala b/compiler/test/dotty/tools/scripting/ScriptingTests.scala index af04524193b1..e7399c68f09a 100644 --- a/compiler/test/dotty/tools/scripting/ScriptingTests.scala +++ b/compiler/test/dotty/tools/scripting/ScriptingTests.scala @@ -14,6 +14,9 @@ class ScriptingTests: extension (str: String) def dropExtension = str.reverse.dropWhile(_ != '.').drop(1).reverse + extension(f: File) def absPath = + f.getAbsolutePath.replace('\\','/') + def testFiles = scripts("/scripting") def script2jar(scriptFile: File) = @@ -23,7 +26,6 @@ class ScriptingTests: def showScriptUnderTest(scriptFile: File): Unit = printf("===> test script name [%s]\n",scriptFile.getName) - val argss: Map[String, Array[String]] = ( for argFile <- testFiles @@ -40,8 +42,17 @@ class ScriptingTests: scriptArgs = argss.getOrElse(name, Array.empty[String]) yield scriptFile -> scriptArgs).toList.sortBy { (file,args) => file.getName } - @Test def scriptingDriverTests = + def callExecutableJar(script: File,jar: File, scriptArgs: Array[String] = Array.empty[String]) = { + import scala.sys.process._ + val cmd = Array("java",s"-Dscript.path=${script.getName}","-jar",jar.absPath) + ++ scriptArgs + Process(cmd).lazyLines_!.foreach { println } + } + /* + * Call .scala scripts without -save option, verify no jar created + */ + @Test def scriptingDriverTests = for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".scala") do showScriptUnderTest(scriptFile) val unexpectedJar = script2jar(scriptFile) @@ -56,9 +67,13 @@ class ScriptingTests: scriptArgs = scriptArgs ).compileAndRun { (path:java.nio.file.Path,classpath:String, mainClass:String) => printf("mainClass from ScriptingDriver: %s\n",mainClass) + true // call compiled script main method } assert(! unexpectedJar.exists, s"not expecting jar file: ${unexpectedJar.absPath}") + /* + * Call .sc scripts without -save option, verify no jar created + */ @Test def scriptingMainTests = for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".sc") do showScriptUnderTest(scriptFile) @@ -74,6 +89,9 @@ class ScriptingTests: Main.main(mainArgs) assert(! unexpectedJar.exists, s"not expecting jar file: ${unexpectedJar.absPath}") + /* + * Call .sc scripts with -save option, verify jar is created. + */ @Test def scriptingJarTest = for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".sc") do showScriptUnderTest(scriptFile) @@ -92,11 +110,74 @@ class ScriptingTests: printf("===> test script jar name [%s]\n",expectedJar.getName) assert(expectedJar.exists) - import scala.sys.process._ - val cmd = Array("java",s"-Dscript.path=${scriptFile.getName}","-jar",expectedJar.absPath) - ++ scriptArgs - Process(cmd).lazyLines_!.foreach { println } - - extension(f: File){ - def absPath = f.getAbsolutePath.replace('\\','/') - } + callExecutableJar(scriptFile, expectedJar, scriptArgs) + + /* + * Verify that when ScriptingDriver callback returns true, main is called. + * Verify that when ScriptingDriver callback returns false, main is not called. + */ + @Test def scriptCompileOnlyTests = + val scriptFile = touchFileScript + showScriptUnderTest(scriptFile) + + // verify main method not called when false is returned + printf("testing script compile, with no call to script main method.\n") + touchedFile.delete + assert(!touchedFile.exists, s"unable to delete ${touchedFile}") + ScriptingDriver( + compilerArgs = Array("-classpath", TestConfiguration.basicClasspath), + scriptFile = scriptFile, + scriptArgs = Array.empty[String] + ).compileAndRun { (path:java.nio.file.Path,classpath:String, mainClass:String) => + printf("success: no call to main method in mainClass: %s\n",mainClass) + false // no call to compiled script main method + } + touchedFile.delete + assert( !touchedFile.exists, s"unable to delete ${touchedFile}" ) + + // verify main method is called when true is returned + printf("testing script compile, with call to script main method.\n") + ScriptingDriver( + compilerArgs = Array("-classpath", TestConfiguration.basicClasspath), + scriptFile = scriptFile, + scriptArgs = Array.empty[String] + ).compileAndRun { (path:java.nio.file.Path,classpath:String, mainClass:String) => + printf("call main method in mainClass: %s\n",mainClass) + true // call compiled script main method, create touchedFile + } + + if touchedFile.exists then + printf("success: script created file %s\n",touchedFile) + if touchedFile.exists then printf("success: created file %s\n",touchedFile) + assert( touchedFile.exists, s"expected to find file ${touchedFile}" ) + + /* + * Compile touchFile.sc to create executable jar, verify jar execution succeeds. + */ + @Test def scriptingNoCompileJar = + val scriptFile = touchFileScript + showScriptUnderTest(scriptFile) + val expectedJar = script2jar(scriptFile) + sys.props("script.path") = scriptFile.absPath + val mainArgs: Array[String] = Array( + "-classpath", TestConfiguration.basicClasspath.toString, + "-save", + "-script", scriptFile.toString, + "-compile-only" + ) + + expectedJar.delete + Main.main(mainArgs) // create executable jar + printf("===> test script jar name [%s]\n",expectedJar.getName) + assert(expectedJar.exists,s"unable to create executable jar [$expectedJar]") + + touchedFile.delete + assert(!touchedFile.exists,s"unable to delete ${touchedFile}") + printf("calling executable jar %s\n",expectedJar) + callExecutableJar(scriptFile, expectedJar) + if touchedFile.exists then + printf("success: executable jar created file %s\n",touchedFile) + assert( touchedFile.exists, s"expected to find file ${touchedFile}" ) + + def touchFileScript = testFiles.find(_.getName == "touchFile.sc").get + def touchedFile = File("touchedFile.out") diff --git a/dist/bin/scala b/dist/bin/scala index 2220d646b981..ceb9fbf0e2df 100755 --- a/dist/bin/scala +++ b/dist/bin/scala @@ -53,6 +53,8 @@ truncated_params="${*#-}" # options_indicator != 0 if at least one parameter is not an option options_indicator=$(( ${#all_params} - ${#truncated_params} - $# )) +[ -n "$SCALA_OPTS" ] && set -- "$@" $SCALA_OPTS + while [[ $# -gt 0 ]]; do case "$1" in -repl) @@ -73,7 +75,7 @@ while [[ $# -gt 0 ]]; do with_compiler=true shift ;; - @*|-color:*) + @*|-color:*|-compile-only) addDotcOptions "${1}" shift ;; @@ -113,12 +115,6 @@ if [ $execute_script == true ]; then if [ "$CLASS_PATH" ]; then cp_arg="-classpath \"$CLASS_PATH\"" fi - if [ -n "$SCALA_OPTS" ]; then - java_options+=($SCALA_OPTS) - if [ "${SCALA_OPTS##*-save}" != "${SCALA_OPTS}" ]; then - save_compiled=true - fi - fi setScriptName="-Dscript.path=$target_script" target_jar="${target_script%.*}.jar" if [[ $save_compiled == true && "$target_jar" -nt "$target_script" ]]; then