Skip to content

Commit c95f4af

Browse files
committed
changes to dist/bin/scala script:
1. pass @<argsFile> and -color:* options to compiler 2. add -save|-savecompiled option 3. recognize scripts with #!.*scala regardless of extension 4. if -save is specified and <scriptPath>.jar file is newer than <scriptPath> 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 <scriptPath> 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 <scriptFile>.jar"
1 parent 2cf4dc4 commit c95f4af

File tree

8 files changed

+201
-20
lines changed

8 files changed

+201
-20
lines changed

compiler/src/dotty/tools/dotc/core/Contexts.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ object Contexts {
273273
/** Sourcefile corresponding to given abstract file, memoized */
274274
def getSource(file: AbstractFile, codec: => Codec = Codec(settings.encoding.value)) = {
275275
util.Stats.record("Context.getSource")
276-
base.sources.getOrElseUpdate(file, new SourceFile(file, codec))
276+
base.sources.getOrElseUpdate(file, SourceFile(file, codec))
277277
}
278278

279279
/** SourceFile with given path name, memoized */

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

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,35 @@ object ScriptSourceFile {
2222
@sharable private val headerPattern = Pattern.compile("""^(::)?!#.*(\r|\n|\r\n)""", Pattern.MULTILINE)
2323
private val headerStarts = List("#!", "::#!")
2424

25+
/** Return true if has a script header */
26+
def hasScriptHeader(content: Array[Char]): Boolean = {
27+
headerStarts exists (content startsWith _)
28+
}
29+
2530
def apply(file: AbstractFile, content: Array[Char]): SourceFile = {
2631
/** Length of the script header from the given content, if there is one.
27-
* The header begins with "#!" or "::#!" and ends with a line starting
28-
* with "!#" or "::!#".
32+
* The header begins with "#!" or "::#!" and is either a single line,
33+
* or it ends with a line starting with "!#" or "::!#", if present.
2934
*/
3035
val headerLength =
3136
if (headerStarts exists (content startsWith _)) {
3237
val matcher = headerPattern matcher content.mkString
3338
if (matcher.find) matcher.end
34-
else throw new IOException("script file does not close its header with !# or ::!#")
39+
else content.indexOf('\n') // end of first line
3540
}
3641
else 0
37-
new SourceFile(file, content drop headerLength) {
42+
43+
// overwrite hash-bang lines with all spaces
44+
val hashBangLines = content.take(headerLength).mkString.split("\\r?\\n")
45+
if hashBangLines.nonEmpty then
46+
for i <- 0 until headerLength do
47+
content(i) match {
48+
case '\r' | '\n' =>
49+
case _ =>
50+
content(i) = ' '
51+
}
52+
53+
new SourceFile(file, content) {
3854
override val underlying = new SourceFile(this.file, this.content)
3955
}
4056
}
@@ -245,6 +261,24 @@ object SourceFile {
245261
else
246262
sourcePath.toString
247263
}
264+
265+
/** Return true if file is a script:
266+
* if filename extension is not .scala and has a script header.
267+
*/
268+
def isScript(file: AbstractFile, content: Array[Char]): Boolean =
269+
if file.hasExtension(".scala") then
270+
false
271+
else
272+
ScriptSourceFile.hasScriptHeader(content)
273+
274+
def apply(file: AbstractFile, codec: Codec): SourceFile =
275+
// see note above re: Files.exists is remarkably slow
276+
val chars = try new String(file.toByteArray, codec.charSet).toCharArray
277+
catch case _: java.nio.file.NoSuchFileException => Array[Char]()
278+
if isScript(file, chars) then
279+
ScriptSourceFile(file, chars)
280+
else
281+
new SourceFile(file, chars)
248282
}
249283

250284
@sharable object NoSource extends SourceFile(NoAbstractFile, Array[Char]()) {
Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,82 @@
11
package dotty.tools.scripting
22

33
import java.io.File
4+
import java.nio.file.Path
45

56
/** Main entry point to the Scripting execution engine */
67
object Main:
78
/** All arguments before -script <target_script> are compiler arguments.
89
All arguments afterwards are script arguments.*/
9-
def distinguishArgs(args: Array[String]): (Array[String], File, Array[String]) =
10-
val (compilerArgs, rest) = args.splitAt(args.indexOf("-script"))
10+
private def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean) =
11+
val (leftArgs, rest) = args.splitAt(args.indexOf("-script"))
12+
if( rest.size < 2 ) then
13+
sys.error(s"missing: -script <scriptName>")
14+
1115
val file = File(rest(1))
1216
val scriptArgs = rest.drop(2)
13-
(compilerArgs, file, scriptArgs)
17+
var saveJar = false
18+
val compilerArgs = leftArgs.filter {
19+
case "-save" | "-savecompiled" =>
20+
saveJar = true
21+
false
22+
case _ =>
23+
true
24+
}
25+
(compilerArgs, file, scriptArgs, saveJar)
1426
end distinguishArgs
1527

1628
def main(args: Array[String]): Unit =
17-
val (compilerArgs, scriptFile, scriptArgs) = distinguishArgs(args)
18-
try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun()
29+
val (compilerArgs, scriptFile, scriptArgs, saveJar) = distinguishArgs(args)
30+
val driver = ScriptingDriver(compilerArgs, scriptFile, scriptArgs)
31+
try driver.compileAndRun { (outDir:Path, classpath:String, mainClass: String) =>
32+
if saveJar then
33+
// write a standalone jar to the script parent directory
34+
writeJarfile(outDir, scriptFile, scriptArgs, classpath, mainClass)
35+
}
1936
catch
2037
case ScriptingException(msg) =>
2138
println(s"Error: $msg")
2239
sys.exit(1)
40+
41+
case e: java.lang.reflect.InvocationTargetException =>
42+
throw e.getCause
43+
44+
private def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String],
45+
classpath:String, mainClassName: String): Unit =
46+
47+
val javaClasspath = sys.props("java.class.path")
48+
val runtimeClasspath = s"${classpath}$pathsep$javaClasspath"
49+
50+
val jarTargetDir: Path = Option(scriptFile.toPath.getParent) match {
51+
case None => sys.error(s"no parent directory for script file [$scriptFile]")
52+
case Some(parent) => parent
53+
}
54+
55+
def scriptBasename = scriptFile.getName.takeWhile(_!='.')
56+
val jarPath = s"$jarTargetDir/$scriptBasename.jar"
57+
58+
val cpPaths = runtimeClasspath.split(pathsep).map {
59+
// protect relative paths from being converted to absolute
60+
case str if str.startsWith(".") && File(str).isDirectory => s"${str.withSlash}/"
61+
case str if str.startsWith(".") => str.withSlash
62+
case str => File(str).toURI.toURL.toString
63+
}
64+
65+
import java.util.jar.Attributes.Name
66+
val cpString:String = cpPaths.distinct.mkString(" ")
67+
val manifestAttributes:Seq[(Name, String)] = Seq(
68+
(Name.MANIFEST_VERSION, "1.0.0"),
69+
(Name.MAIN_CLASS, mainClassName),
70+
(Name.CLASS_PATH, cpString),
71+
)
72+
import dotty.tools.io.{Jar, Directory}
73+
val jar = new Jar(jarPath)
74+
val writer = jar.jarWriter(manifestAttributes:_*)
75+
writer.writeAllFrom(Directory(outDir))
76+
end writeJarfile
77+
78+
def pathsep = sys.props("path.separator")
79+
80+
extension(pathstr:String) {
81+
def withSlash:String = pathstr.replace('\\', '/')
82+
}

compiler/src/dotty/tools/scripting/ScriptingDriver.scala

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import dotty.tools.dotc.config.Settings.Setting._
1717
import sys.process._
1818

1919
class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver:
20-
def compileAndRun(): Unit =
20+
def compileAndRun(pack:(Path, String, String) => Unit = null): Unit =
2121
val outDir = Files.createTempDirectory("scala3-scripting")
2222
val (toCompile, rootCtx) = setup(compilerArgs :+ scriptFile.getAbsolutePath, initCtx.fresh)
2323
given Context = rootCtx.fresh.setSetting(rootCtx.settings.outputDir,
@@ -26,7 +26,14 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs:
2626
if doCompile(newCompiler, toCompile).hasErrors then
2727
throw ScriptingException("Errors encountered during compilation")
2828

29-
try detectMainMethod(outDir, ctx.settings.classpath.value).invoke(null, scriptArgs)
29+
try
30+
val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, ctx.settings.classpath.value, scriptFile)
31+
Option(pack) match
32+
case Some(func) =>
33+
func(outDir, ctx.settings.classpath.value, mainClass)
34+
case None =>
35+
end match
36+
mainMethod.invoke(null, scriptArgs)
3037
catch
3138
case e: java.lang.reflect.InvocationTargetException =>
3239
throw e.getCause
@@ -41,12 +48,13 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs:
4148
target.delete()
4249
end deleteFile
4350

44-
private def detectMainMethod(outDir: Path, classpath: String): Method =
51+
private def detectMainClassAndMethod(outDir: Path, classpath: String,
52+
scriptFile: File): (String, Method) =
4553
val outDirURL = outDir.toUri.toURL
46-
val classpathUrls = classpath.split(":").map(File(_).toURI.toURL)
54+
val classpathUrls = classpath.split(pathsep).map(File(_).toURI.toURL)
4755
val cl = URLClassLoader(classpathUrls :+ outDirURL)
4856

49-
def collectMainMethods(target: File, path: String): List[Method] =
57+
def collectMainMethods(target: File, path: String): List[(String, Method)] =
5058
val nameWithoutExtension = target.getName.takeWhile(_ != '.')
5159
val targetPath =
5260
if path.nonEmpty then s"${path}.${nameWithoutExtension}"
@@ -61,7 +69,7 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs:
6169
val cls = cl.loadClass(targetPath)
6270
try
6371
val method = cls.getMethod("main", classOf[Array[String]])
64-
if Modifier.isStatic(method.getModifiers) then List(method) else Nil
72+
if Modifier.isStatic(method.getModifiers) then List((cls.getName, method)) else Nil
6573
catch
6674
case _: java.lang.NoSuchMethodException => Nil
6775
else Nil
@@ -74,13 +82,16 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs:
7482

7583
candidates match
7684
case Nil =>
77-
throw ScriptingException("No main methods detected in your script")
85+
throw ScriptingException(s"No main methods detected in script ${scriptFile}")
7886
case _ :: _ :: _ =>
7987
throw ScriptingException("A script must contain only one main method. " +
8088
s"Detected the following main methods:\n${candidates.mkString("\n")}")
8189
case m :: Nil => m
8290
end match
83-
end detectMainMethod
91+
end detectMainClassAndMethod
92+
93+
def pathsep = sys.props("path.separator")
94+
8495
end ScriptingDriver
8596

8697
case class ScriptingException(msg: String) extends RuntimeException(msg)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env scala
2+
# comment
3+
STUFF=nada
4+
!#
5+
6+
def main(args: Array[String]): Unit =
7+
System.err.printf("mainClassFromStack: %s\n",mainFromStack)
8+
assert(mainFromStack.contains("hashBang"),s"fromStack[$mainFromStack]")
9+
10+
lazy val mainFromStack:String = {
11+
val result = new java.io.StringWriter()
12+
new RuntimeException("stack").printStackTrace(new java.io.PrintWriter(result))
13+
val stack = result.toString.split("[\r\n]+").toList
14+
//for( s <- stack ){ System.err.printf("[%s]\n",s) }
15+
stack.filter { str => str.contains(".main(") }.map {
16+
_.replaceAll(".*[(]","").
17+
replaceAll("\\.main\\(.*","").
18+
replaceAll(".scala.*","")
19+
}.distinct.take(1).mkString("")
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env scala
2+
export STUFF=nada
3+
#lots of other stuff that isn't valid scala
4+
!#
5+
object Zoo {
6+
def main(args: Array[String]): Unit =
7+
printf("mainClassFromStack: %s\n",mainFromStack)
8+
assert(mainFromStack == "Zoo",s"fromStack[$mainFromStack]")
9+
10+
lazy val mainFromStack:String = {
11+
val result = new java.io.StringWriter()
12+
new RuntimeException("stack").printStackTrace(new java.io.PrintWriter(result))
13+
val stack = result.toString.split("[\r\n]+").toList
14+
// for( s <- stack ){ System.err.printf("[%s]\n",s) }
15+
val shortStack = stack.filter { str => str.contains(".main(") && ! str.contains("$") }.map {
16+
_.replaceAll("[.].*","").replaceAll("\\s+at\\s+","")
17+
}
18+
// for( s <- shortStack ){ System.err.printf("[%s]\n",s) }
19+
shortStack.take(1).mkString("|")
20+
}
21+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env scala
2+
3+
def main(args: Array[String]): Unit =
4+
val name = Option(sys.props("script.name")) match {
5+
case None => printf("no script.name property is defined\n")
6+
case Some(name) =>
7+
if( name == null ){
8+
printf("unexpected null script.name property")
9+
} else {
10+
printf("script.name: %s\n",name)
11+
assert(name == "scriptName.scala")
12+
}
13+
}

dist/bin/scala

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ execute_script=false
4545
with_compiler=false
4646
class_path_count=0
4747
CLASS_PATH=""
48+
save_compiled=false
4849

4950
# Little hack to check if all arguments are options
5051
all_params="$*"
@@ -72,6 +73,15 @@ while [[ $# -gt 0 ]]; do
7273
with_compiler=true
7374
shift
7475
;;
76+
@*|-color:*)
77+
addDotcOptions "${1}"
78+
shift
79+
;;
80+
-save|-savecompiled)
81+
save_compiled=1
82+
addDotcOptions "${1}"
83+
shift
84+
;;
7585
-d)
7686
DEBUG="$DEBUG_STR"
7787
shift
@@ -82,8 +92,10 @@ while [[ $# -gt 0 ]]; do
8292
shift ;;
8393
*)
8494
if [ $execute_script == false ]; then
85-
if [[ "$1" == *.scala ]]; then
95+
# is a script if extension .scala or .sc or if has scala hash bang
96+
if [[ "$1" == *.scala || "$1" == *.sc || -f "$1" && `head -n 1 -- "$1" | grep '#!.*scala'` ]]; then
8697
execute_script=true
98+
[ -n "$SCALA_OPTS" ] && java_options+=($SCALA_OPTS)
8799
target_script="$1"
88100
else
89101
residual_args+=("$1")
@@ -101,7 +113,17 @@ if [ $execute_script == true ]; then
101113
if [ "$CLASS_PATH" ]; then
102114
cp_arg="-classpath \"$CLASS_PATH\""
103115
fi
104-
eval "\"$PROG_HOME/bin/scalac\" $cp_arg ${java_options[@]} ${residual_args[@]} -script $target_script ${script_args[@]}"
116+
target_jar="${target_script%.*}.jar"
117+
jar_found=false
118+
setScriptName="-Dscript.name=${target_script##*/}"
119+
[[ $save_compiled == true && -f "$target_jar" ]] && jar_found=true
120+
if [[ $jar_found == true && "$target_jar" -nt "$target_script" ]]; then
121+
java $setScriptName -jar "$target_jar" "${script_args[@]}"
122+
else
123+
[[ $save_compiled == true && -f $target_jar ]] && rm -f $target_jar
124+
residual_args+=($setScriptName)
125+
eval "\"$PROG_HOME/bin/scalac\" $cp_arg ${java_options[@]} ${residual_args[@]} -script $target_script ${script_args[@]}"
126+
fi
105127
elif [ $execute_repl == true ] || ([ $execute_run == false ] && [ $options_indicator == 0 ]); then
106128
if [ "$CLASS_PATH" ]; then
107129
cp_arg="-classpath \"$CLASS_PATH\""

0 commit comments

Comments
 (0)