From e07104fb3875427b8114266e50a7398cfe80908a Mon Sep 17 00:00:00 2001 From: Phil Walker Date: Tue, 12 Jan 2021 13:31:02 -0700 Subject: [PATCH 01/10] initial limited checkin of idea --- compiler/src/dotty/tools/scripting/Main.scala | 4 +++- compiler/src/dotty/tools/scripting/ScriptingDriver.scala | 8 +++++++- compiler/test/dotty/tools/scripting/ScriptingTests.scala | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/scripting/Main.scala b/compiler/src/dotty/tools/scripting/Main.scala index f820421860a6..07871baf9564 100644 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -15,7 +15,9 @@ object Main: def main(args: Array[String]): Unit = val (compilerArgs, scriptFile, scriptArgs) = distinguishArgs(args) - try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun() + try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun { (tmpDir:java.nio.file.Path,cp:String) => + // compile to jar, with classpath + } catch case ScriptingException(msg) => println(s"Error: $msg") diff --git a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala index d5fad4ef520a..d00144a0927d 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) => 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,6 +26,12 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: if doCompile(newCompiler, toCompile).hasErrors then throw ScriptingException("Errors encountered during compilation") + Option(pack) match { + case None => + case Some(func) => + func(outDir,ctx.settings.classpath.value) + } + try detectMainMethod(outDir, ctx.settings.classpath.value).invoke(null, scriptArgs) catch case e: java.lang.reflect.InvocationTargetException => diff --git a/compiler/test/dotty/tools/scripting/ScriptingTests.scala b/compiler/test/dotty/tools/scripting/ScriptingTests.scala index 547f525e98e4..f44951647ecd 100644 --- a/compiler/test/dotty/tools/scripting/ScriptingTests.scala +++ b/compiler/test/dotty/tools/scripting/ScriptingTests.scala @@ -36,4 +36,8 @@ class ScriptingTests: "-classpath", TestConfiguration.basicClasspath), scriptFile = scriptFile, scriptArgs = scriptArgs - ).compileAndRun() + ).compileAndRun { (path:java.nio.file.Path,classpath:String) => + path.toFile.listFiles.foreach { (f:File) => printf(" [%s]\n",f.getName) } + printf("%s\n%s\n",path,classpath) + } + From 797c7ee7853fbf6f52fe31f8db85ca1027c9ab4b Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 3 Feb 2021 09:21:45 -0700 Subject: [PATCH 02/10] stuff --- bin/common | 1 + compiler/src/dotty/tools/scripting/Main.scala | 12 ++++++++++-- .../src/dotty/tools/scripting/ScriptingDriver.scala | 8 +++++++- .../test/dotty/tools/scripting/ScriptingTests.scala | 6 +++++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/bin/common b/bin/common index 7d3aa7148265..abe896180b1e 100755 --- a/bin/common +++ b/bin/common @@ -15,6 +15,7 @@ version="$ROOT/dist/target/pack/VERSION" # Create the target if absent or if file changed in ROOT/compiler new_files="$(find "$ROOT/compiler" \( -iname "*.scala" -o -iname "*.java" \) -newer "$version" 2> /dev/null)" +set -x if [ ! -f "$version" ] || [ ! -z "$new_files" ]; then echo "Building Dotty..." (cd $ROOT && sbt "dist/pack") diff --git a/compiler/src/dotty/tools/scripting/Main.scala b/compiler/src/dotty/tools/scripting/Main.scala index f820421860a6..1333017a2c99 100644 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -1,21 +1,29 @@ 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]) = + args.foreach { printf("arg[%s]\n",_) } val (compilerArgs, rest) = args.splitAt(args.indexOf("-script")) - val file = File(rest(1)) + if( rest.isEmpty ){ + sys.error(s"missing: -script ") + } + val file = File(rest.take(1).mkString) val scriptArgs = rest.drop(2) (compilerArgs, file, scriptArgs) end distinguishArgs def main(args: Array[String]): Unit = val (compilerArgs, scriptFile, scriptArgs) = distinguishArgs(args) - try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun() + try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun{ (tmpDir:Path,classpath:String) => + printf("%s\n",tmpDir.toString) + printf("%s\n",classpath) + } catch case ScriptingException(msg) => println(s"Error: $msg") diff --git a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala index d5fad4ef520a..d00144a0927d 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) => 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,6 +26,12 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: if doCompile(newCompiler, toCompile).hasErrors then throw ScriptingException("Errors encountered during compilation") + Option(pack) match { + case None => + case Some(func) => + func(outDir,ctx.settings.classpath.value) + } + try detectMainMethod(outDir, ctx.settings.classpath.value).invoke(null, scriptArgs) catch case e: java.lang.reflect.InvocationTargetException => diff --git a/compiler/test/dotty/tools/scripting/ScriptingTests.scala b/compiler/test/dotty/tools/scripting/ScriptingTests.scala index 547f525e98e4..f44951647ecd 100644 --- a/compiler/test/dotty/tools/scripting/ScriptingTests.scala +++ b/compiler/test/dotty/tools/scripting/ScriptingTests.scala @@ -36,4 +36,8 @@ class ScriptingTests: "-classpath", TestConfiguration.basicClasspath), scriptFile = scriptFile, scriptArgs = scriptArgs - ).compileAndRun() + ).compileAndRun { (path:java.nio.file.Path,classpath:String) => + path.toFile.listFiles.foreach { (f:File) => printf(" [%s]\n",f.getName) } + printf("%s\n%s\n",path,classpath) + } + From 61303b3bd75ebb5c8f0cae319b4285c02c9fc1a4 Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 8 Feb 2021 06:17:37 -0700 Subject: [PATCH 03/10] first feature complete code --- .../src/dotty/tools/dotc/core/Contexts.scala | 2 +- .../dotty/tools/dotc/util/SourceFile.scala | 45 +++++- compiler/src/dotty/tools/scripting/Main.scala | 146 ++++++++++++++++-- .../tools/scripting/ScriptingDriver.scala | 71 +++------ .../tools/scripting/ScriptingTests.scala | 3 +- dist/bin/scala | 22 ++- scala3doc/scala3-docs/blog | 2 +- 7 files changed, 219 insertions(+), 72 deletions(-) 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..10d9779a7b9b 100644 --- a/compiler/src/dotty/tools/dotc/util/SourceFile.scala +++ b/compiler/src/dotty/tools/dotc/util/SourceFile.scala @@ -22,19 +22,36 @@ 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 _)) { + // convert initial hash-bang line to a comment 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) } } @@ -201,6 +218,7 @@ class SourceFile(val file: AbstractFile, computeContent: => Array[Char]) extends override def toString: String = file.toString } object SourceFile { + implicit def eqSource: CanEqual[SourceFile, SourceFile] = CanEqual.derived implicit def fromContext(using Context): SourceFile = ctx.source @@ -245,6 +263,25 @@ 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 1333017a2c99..006babc09bcd 100644 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -1,30 +1,148 @@ package dotty.tools.scripting import java.io.File -import java.nio.file.Path +import java.nio.file.{Files, Paths, Path} +import dotty.tools.dotc.util.SourceFile +import java.net.{ URL, URLClassLoader } +import java.lang.reflect.{ Modifier, Method } + /** 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]) = - args.foreach { printf("arg[%s]\n",_) } - val (compilerArgs, rest) = args.splitAt(args.indexOf("-script")) - if( rest.isEmpty ){ + def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean) = + // NOTE: if -script is required but not present, quit with error. + val (leftArgs, rest) = args.splitAt(args.indexOf("-script")) + if( rest.size < 2 ) then sys.error(s"missing: -script ") - } - val file = File(rest.take(1).mkString) + + val file = File(rest(1)) val scriptArgs = rest.drop(2) - (compilerArgs, file, scriptArgs) + var saveCompiled = false + val compilerArgs = leftArgs.filter { + case "-save" | "-savecompiled" => + saveCompiled = true + false + case _ => + true + } + (compilerArgs, file, scriptArgs, saveCompiled) end distinguishArgs + val pathsep = sys.props("path.separator") + def main(args: Array[String]): Unit = - val (compilerArgs, scriptFile, scriptArgs) = distinguishArgs(args) - try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun{ (tmpDir:Path,classpath:String) => - printf("%s\n",tmpDir.toString) - printf("%s\n",classpath) + val (compilerArgs, scriptFile, scriptArgs, saveCompiled) = distinguishArgs(args) + if verbose then showArgs(args, compilerArgs, scriptFile, scriptArgs) + try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun { (outDir:Path, classpath:String) => + val classFiles = outDir.toFile.listFiles.toList match { + case Nil => sys.error(s"no files below [$outDir]") + case list => list + } + + val (mainClassName, mainMethod) = detectMainMethod(outDir, classpath, scriptFile) + + if saveCompiled then + // write a standalone jar to the script parent directory + writeJarfile(outDir, scriptFile, scriptArgs, classpath, mainClassName) + + try + // invoke the compiled script main method + mainMethod.invoke(null, scriptArgs) + catch + case e: java.lang.reflect.InvocationTargetException => + throw e.getCause + } catch - case ScriptingException(msg) => - println(s"Error: $msg") + case e:Exception => + e.printStackTrace + println(s"Error: ${e.getMessage}") sys.exit(1) + + def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String], classpath:String, mainClassName: String): Unit = + import java.net.{URI, URL} + val jarTargetDir: Path = Option(scriptFile.toPath.getParent) match { + case None => sys.error(s"no parent directory for script file [$scriptFile]") + case Some(parent) => parent + } + + val scriptBasename = scriptFile.getName.takeWhile(_!='.') + val jarPath = s"$jarTargetDir/$scriptBasename.jar" + + val cpPaths = classpath.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 + + lazy val verbose = Option(System.getenv("DOTC_VERBOSE")) != None + + def showArgs(args:Array[String], compilerArgs:Array[String], scriptFile:File, scriptArgs:Array[String]): Unit = + args.foreach { printf("args[%s]\n", _) } + compilerArgs.foreach { printf("compilerArgs[%s]\n", _) } + scriptArgs.foreach { printf("scriptArgs[%s]\n", _) } + printf("scriptFile[%s]\n", scriptFile) + + private def detectMainMethod(outDir: Path, classpath: String, scriptFile: File): (String, Method) = + val outDirURL = outDir.toUri.toURL + val classpathUrls = classpath.split(pathsep).map(File(_).toURI.toURL) + val cl = URLClassLoader(classpathUrls :+ outDirURL) + + def collectMainMethods(target: File, path: String): List[(String, Method)] = + val nameWithoutExtension = target.getName.takeWhile(_ != '.') + val targetPath = + if path.nonEmpty then s"${path}.${nameWithoutExtension}" + else nameWithoutExtension + + if verbose then printf("targetPath [%s]\n",targetPath) + + if target.isDirectory then + for + packageMember <- target.listFiles.toList + membersMainMethod <- collectMainMethods(packageMember, targetPath) + yield membersMainMethod + else if target.getName.endsWith(".class") then + val cls = cl.loadClass(targetPath) + try + val method = cls.getMethod("main", classOf[Array[String]]) + if Modifier.isStatic(method.getModifiers) then List((cls.getName, method)) else Nil + catch + case _: java.lang.NoSuchMethodException => Nil + else Nil + end collectMainMethods + + val candidates = for + file <- outDir.toFile.listFiles.toList + method <- collectMainMethods(file, "") + yield method + + candidates match + case Nil => + if verbose then outDir.toFile.listFiles.toList.foreach { f => System.err.printf("%s\n",f.toString) } + 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 + + 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 d00144a0927d..a7a42043e1d1 100644 --- a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala +++ b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala @@ -13,31 +13,44 @@ import dotty.tools.dotc.config.CompilerCommand import dotty.tools.io.{ PlainDirectory, Directory } import dotty.tools.dotc.reporting.Reporter import dotty.tools.dotc.config.Settings.Setting._ +import dotty.tools.dotc.util.ScriptSourceFile +import dotty.tools.io.AbstractFile import sys.process._ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver: - def compileAndRun(pack:(Path,String) => Unit = null): Unit = + def compileAndRun(pack:(Path, 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, new PlainDirectory(Directory(outDir))) - if doCompile(newCompiler, toCompile).hasErrors then - throw ScriptingException("Errors encountered during compilation") + val result = doCompile(newCompiler, toCompile) + if result.hasErrors then + throw ScriptingException(s"Errors encountered during compilation to dir [$outDir]") - Option(pack) match { - case None => - case Some(func) => - func(outDir,ctx.settings.classpath.value) - } + try + if outDir.toFile.listFiles.toList.isEmpty then + sys.error(s"no files generated by compiling script ${scriptFile}") - try detectMainMethod(outDir, ctx.settings.classpath.value).invoke(null, scriptArgs) + Option(pack) match { + case None => + case Some(func) => + val javaClasspath = sys.props("java.class.path") + val pathsep = sys.props("path.separator") + val runtimeClasspath = s"${ctx.settings.classpath.value}$pathsep$javaClasspath" + func(outDir, runtimeClasspath) + } catch case e: java.lang.reflect.InvocationTargetException => throw e.getCause finally deleteFile(outDir.toFile) + + def content(file: Path): Array[Char] = new String(Files.readAllBytes(file)).toCharArray + def scriptSource(file: Path) = ScriptSourceFile(AbstractFile.getFile(file), content(file)) + end compileAndRun private def deleteFile(target: File): Unit = @@ -47,46 +60,6 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: target.delete() end deleteFile - private def detectMainMethod(outDir: Path, classpath: String): Method = - val outDirURL = outDir.toUri.toURL - val classpathUrls = classpath.split(":").map(File(_).toURI.toURL) - val cl = URLClassLoader(classpathUrls :+ outDirURL) - - def collectMainMethods(target: File, path: String): List[Method] = - val nameWithoutExtension = target.getName.takeWhile(_ != '.') - val targetPath = - if path.nonEmpty then s"${path}.${nameWithoutExtension}" - else nameWithoutExtension - - if target.isDirectory then - for - packageMember <- target.listFiles.toList - membersMainMethod <- collectMainMethods(packageMember, targetPath) - yield membersMainMethod - else if target.getName.endsWith(".class") then - val cls = cl.loadClass(targetPath) - try - val method = cls.getMethod("main", classOf[Array[String]]) - if Modifier.isStatic(method.getModifiers) then List(method) else Nil - catch - case _: java.lang.NoSuchMethodException => Nil - else Nil - end collectMainMethods - - val candidates = for - file <- outDir.toFile.listFiles.toList - method <- collectMainMethods(file, "") - yield method - - candidates match - case Nil => - throw ScriptingException("No main methods detected in your script") - 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 ScriptingDriver case class ScriptingException(msg: String) extends RuntimeException(msg) diff --git a/compiler/test/dotty/tools/scripting/ScriptingTests.scala b/compiler/test/dotty/tools/scripting/ScriptingTests.scala index f44951647ecd..ceb91bc16629 100644 --- a/compiler/test/dotty/tools/scripting/ScriptingTests.scala +++ b/compiler/test/dotty/tools/scripting/ScriptingTests.scala @@ -16,6 +16,7 @@ class ScriptingTests: @Test def scriptingTests = val testFiles = scripts("/scripting") + testFiles.foreach { (f:File) => System.err.printf("script[%s]\n",f.toString) } val argss: Map[String, Array[String]] = ( for @@ -38,6 +39,6 @@ class ScriptingTests: scriptArgs = scriptArgs ).compileAndRun { (path:java.nio.file.Path,classpath:String) => path.toFile.listFiles.foreach { (f:File) => printf(" [%s]\n",f.getName) } - printf("%s\n%s\n",path,classpath) + printf("%s\n%s\n",path,classpath.length) } diff --git a/dist/bin/scala b/dist/bin/scala index 3c522a082d3b..69f609b8bb98 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,7 +92,8 @@ while [[ $# -gt 0 ]]; do shift ;; *) if [ $execute_script == false ]; then - if [[ "$1" == *.scala ]]; then + # also a script if has scala hash bang + if [[ "$1" == *.scala || "$1" == *.sc || -f "$1" && `head -n 1 -- "$1" | grep '#!.*scala'` ]]; then execute_script=true target_script="$1" else @@ -101,7 +112,14 @@ 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 + if [ $save_compiled == true -a -f "$target_jar" -a $target_jar -nt $target_script ]; then + java -jar $target_jar + else + [ $save_compiled == true -a -f $target_jar ] && rm -f $target_jar + residual_args+=("-Dscript.name=${target_script##*/}") + 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\"" diff --git a/scala3doc/scala3-docs/blog b/scala3doc/scala3-docs/blog index b4f84f8e60f7..478f4ace2d41 120000 --- a/scala3doc/scala3-docs/blog +++ b/scala3doc/scala3-docs/blog @@ -1 +1 @@ -../../docs/blog/ \ No newline at end of file +../../docs/blog \ No newline at end of file From 8829a9195139640820713d81ec0e1e8f5a4f58cb Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 8 Feb 2021 06:21:09 -0700 Subject: [PATCH 04/10] add test sources --- .../test-resources/scripting/hashBang.scala | 20 +++++++++++++++++ .../scripting/mainClassOnStack.scala | 22 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 compiler/test-resources/scripting/hashBang.scala create mode 100644 compiler/test-resources/scripting/mainClassOnStack.scala diff --git a/compiler/test-resources/scripting/hashBang.scala b/compiler/test-resources/scripting/hashBang.scala new file mode 100644 index 000000000000..690b8809f2bf --- /dev/null +++ b/compiler/test-resources/scripting/hashBang.scala @@ -0,0 +1,20 @@ +#!/usr/bin/env scala +export 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("[:)].*","") + }.distinct.take(1).mkString("") + } diff --git a/compiler/test-resources/scripting/mainClassOnStack.scala b/compiler/test-resources/scripting/mainClassOnStack.scala new file mode 100644 index 000000000000..a2c53a3b47b5 --- /dev/null +++ b/compiler/test-resources/scripting/mainClassOnStack.scala @@ -0,0 +1,22 @@ +#!/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("script.name: %s\n",sys.props("script.name")) + 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("|") + } +} From fa7f3dd4ac8ef53bd11c5652e35ce577f8bf1ac3 Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 8 Feb 2021 06:35:44 -0700 Subject: [PATCH 05/10] remove debris --- bin/common | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/common b/bin/common index abe896180b1e..7d3aa7148265 100755 --- a/bin/common +++ b/bin/common @@ -15,7 +15,6 @@ version="$ROOT/dist/target/pack/VERSION" # Create the target if absent or if file changed in ROOT/compiler new_files="$(find "$ROOT/compiler" \( -iname "*.scala" -o -iname "*.java" \) -newer "$version" 2> /dev/null)" -set -x if [ ! -f "$version" ] || [ ! -z "$new_files" ]; then echo "Building Dotty..." (cd $ROOT && sbt "dist/pack") From 1e6a8c3e764b658fd1d6159232cb053ee4212e11 Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 8 Feb 2021 12:53:25 -0700 Subject: [PATCH 06/10] passed all but dotty.tools.dotc.classpath.MultiReleaseJarTest.mrJar --- compiler/src/dotty/tools/dotc/util/SourceFile.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/util/SourceFile.scala b/compiler/src/dotty/tools/dotc/util/SourceFile.scala index 10d9779a7b9b..a97bffcc902b 100644 --- a/compiler/src/dotty/tools/dotc/util/SourceFile.scala +++ b/compiler/src/dotty/tools/dotc/util/SourceFile.scala @@ -218,7 +218,6 @@ class SourceFile(val file: AbstractFile, computeContent: => Array[Char]) extends override def toString: String = file.toString } object SourceFile { - implicit def eqSource: CanEqual[SourceFile, SourceFile] = CanEqual.derived implicit def fromContext(using Context): SourceFile = ctx.source From 9dd1c6fe053ec468f84b6b51cb2a53bd8bec536b Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 8 Feb 2021 13:42:36 -0700 Subject: [PATCH 07/10] merge working code plus tests from local work branch --- compiler/src/dotty/tools/scripting/Main.scala | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/scripting/Main.scala b/compiler/src/dotty/tools/scripting/Main.scala index 006babc09bcd..7757386f8647 100644 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -35,7 +35,9 @@ object Main: def main(args: Array[String]): Unit = val (compilerArgs, scriptFile, scriptArgs, saveCompiled) = distinguishArgs(args) if verbose then showArgs(args, compilerArgs, scriptFile, scriptArgs) - try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun { (outDir:Path, classpath:String) => + try + ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun { + (outDir:Path, classpath:String) => val classFiles = outDir.toFile.listFiles.toList match { case Nil => sys.error(s"no files below [$outDir]") case list => list @@ -61,7 +63,8 @@ object Main: println(s"Error: ${e.getMessage}") sys.exit(1) - def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String], classpath:String, mainClassName: String): Unit = + def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String], + classpath:String, mainClassName: String): Unit = import java.net.{URI, URL} val jarTargetDir: Path = Option(scriptFile.toPath.getParent) match { case None => sys.error(s"no parent directory for script file [$scriptFile]") @@ -93,13 +96,15 @@ object Main: lazy val verbose = Option(System.getenv("DOTC_VERBOSE")) != None - def showArgs(args:Array[String], compilerArgs:Array[String], scriptFile:File, scriptArgs:Array[String]): Unit = + def showArgs(args:Array[String], compilerArgs:Array[String], + scriptFile:File, scriptArgs:Array[String]): Unit = args.foreach { printf("args[%s]\n", _) } compilerArgs.foreach { printf("compilerArgs[%s]\n", _) } scriptArgs.foreach { printf("scriptArgs[%s]\n", _) } printf("scriptFile[%s]\n", scriptFile) - private def detectMainMethod(outDir: Path, classpath: String, scriptFile: File): (String, Method) = + private def detectMainMethod(outDir: Path, classpath: String, + scriptFile: File): (String, Method) = val outDirURL = outDir.toUri.toURL val classpathUrls = classpath.split(pathsep).map(File(_).toURI.toURL) val cl = URLClassLoader(classpathUrls :+ outDirURL) From e25c64bb0af0cfce4487c5f88910dc59513df168 Mon Sep 17 00:00:00 2001 From: philwalk Date: Tue, 9 Feb 2021 10:24:08 -0700 Subject: [PATCH 08/10] Update blog --- scala3doc/scala3-docs/blog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scala3doc/scala3-docs/blog b/scala3doc/scala3-docs/blog index 478f4ace2d41..f73ce04b29ba 120000 --- a/scala3doc/scala3-docs/blog +++ b/scala3doc/scala3-docs/blog @@ -1 +1 @@ -../../docs/blog \ No newline at end of file +../../docs/blog/ From aec895869f0daf145c06e94f61fafc71ce39a6d5 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 10 Feb 2021 08:49:25 -0700 Subject: [PATCH 09/10] removed unused imports; renamed detectMainMethod to reflect changed return tuple --- compiler/src/dotty/tools/scripting/Main.scala | 17 +++++++---------- .../dotty/tools/scripting/ScriptingDriver.scala | 8 ++------ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/scripting/Main.scala b/compiler/src/dotty/tools/scripting/Main.scala index 7e6ae8197071..488acafb3e2f 100644 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -1,18 +1,16 @@ package dotty.tools.scripting import java.io.File -import java.nio.file.{Files, Paths, Path} -import dotty.tools.dotc.util.SourceFile -import java.net.{ URL, URLClassLoader } +import java.nio.file.Path +import java.net.URLClassLoader import java.lang.reflect.{ Modifier, Method } - /** 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) = - // NOTE: if -script is required but not present, quit with error. + // NOTE: if -script not present, quit with error. val (leftArgs, rest) = args.splitAt(args.indexOf("-script")) if( rest.size < 2 ) then sys.error(s"missing: -script ") @@ -38,7 +36,7 @@ object Main: case list => list } - val (mainClassName, mainMethod) = detectMainMethod(outDir, classpath, scriptFile) + val (mainClassName, mainMethod) = detectMainClassAndMethod(outDir, classpath, scriptFile) if saveJar then // write a standalone jar to the script parent directory @@ -58,13 +56,12 @@ object Main: private def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String], classpath:String, mainClassName: String): Unit = - import java.net.{URI, URL} val jarTargetDir: Path = Option(scriptFile.toPath.getParent) match { case None => sys.error(s"no parent directory for script file [$scriptFile]") case Some(parent) => parent } - val scriptBasename = scriptFile.getName.takeWhile(_!='.') + def scriptBasename = scriptFile.getName.takeWhile(_!='.') val jarPath = s"$jarTargetDir/$scriptBasename.jar" val cpPaths = classpath.split(pathsep).map { @@ -87,7 +84,7 @@ object Main: writer.writeAllFrom(Directory(outDir)) end writeJarfile - private def detectMainMethod(outDir: Path, classpath: String, + private def detectMainClassAndMethod(outDir: Path, classpath: String, scriptFile: File): (String, Method) = val outDirURL = outDir.toUri.toURL val classpathUrls = classpath.split(pathsep).map(File(_).toURI.toURL) @@ -127,7 +124,7 @@ object Main: 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") diff --git a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala index a7a42043e1d1..71a92706c9bd 100644 --- a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala +++ b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala @@ -2,22 +2,18 @@ package dotty.tools.scripting import java.nio.file.{ Files, Path } import java.io.File -import java.net.{ URL, URLClassLoader } import java.lang.reflect.{ Modifier, Method } import scala.jdk.CollectionConverters._ -import dotty.tools.dotc.{ Driver, Compiler } -import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ContextBase, ctx } -import dotty.tools.dotc.config.CompilerCommand +import dotty.tools.dotc.{ Driver } +import dotty.tools.dotc.core.Contexts, Contexts.{ Context, ctx } import dotty.tools.io.{ PlainDirectory, Directory } import dotty.tools.dotc.reporting.Reporter import dotty.tools.dotc.config.Settings.Setting._ import dotty.tools.dotc.util.ScriptSourceFile import dotty.tools.io.AbstractFile -import sys.process._ - class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver: def compileAndRun(pack:(Path, String) => Unit = null): Unit = val outDir = Files.createTempDirectory("scala3-scripting") From b399c6f5a45c0d4e1f214f8b0eded91016d93905 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 10 Feb 2021 09:03:44 -0700 Subject: [PATCH 10/10] fix blog symlink conflict --- scala3doc/scala3-docs/blog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scala3doc/scala3-docs/blog b/scala3doc/scala3-docs/blog index f73ce04b29ba..0a1e1b992fba 120000 --- a/scala3doc/scala3-docs/blog +++ b/scala3doc/scala3-docs/blog @@ -1 +1 @@ -../../docs/blog/ +../../blog \ No newline at end of file