Skip to content

Commit 722cf12

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 <scriptPath>.jar file is newer than <scriptPath> execute it (no compile) 5. set -Dscript.name for both script execution paths (script or .jar) changes to dotty.tools.scripting package: 1. moved mainMethod execution from ScriptingDriver to Main 2. detectMainMethod also detects main class name 3. on -save option: a. generate 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 4. additional compiler args splitting and filtering 5. throw sys.error if .class file files generated (e.g., if script source is blank) added new tests to verify the following: 1. hash bang section is 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. without -save option, no jar file is generated 6. -save option causes jar file with expected name to be generated 7. generated jar file is directly executable via "java -jar <scriptFile>.jar"
2 parents 9dd1c6f + e07104f commit 722cf12

File tree

5 files changed

+110
-56
lines changed

5 files changed

+110
-56
lines changed

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

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,59 +11,52 @@ import java.lang.reflect.{ Modifier, Method }
1111
object Main:
1212
/** All arguments before -script <target_script> are compiler arguments.
1313
All arguments afterwards are script arguments.*/
14-
def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean) =
14+
private def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean) =
1515
// NOTE: if -script is required but not present, quit with error.
1616
val (leftArgs, rest) = args.splitAt(args.indexOf("-script"))
1717
if( rest.size < 2 ) then
1818
sys.error(s"missing: -script <scriptName>")
1919

2020
val file = File(rest(1))
2121
val scriptArgs = rest.drop(2)
22-
var saveCompiled = false
22+
var saveJar = false
2323
val compilerArgs = leftArgs.filter {
2424
case "-save" | "-savecompiled" =>
25-
saveCompiled = true
25+
saveJar = true
2626
false
2727
case _ =>
2828
true
2929
}
30-
(compilerArgs, file, scriptArgs, saveCompiled)
30+
(compilerArgs, file, scriptArgs, saveJar)
3131
end distinguishArgs
3232

33-
val pathsep = sys.props("path.separator")
34-
3533
def main(args: Array[String]): Unit =
36-
val (compilerArgs, scriptFile, scriptArgs, saveCompiled) = distinguishArgs(args)
37-
if verbose then showArgs(args, compilerArgs, scriptFile, scriptArgs)
38-
try
39-
ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun {
40-
(outDir:Path, classpath:String) =>
34+
val (compilerArgs, scriptFile, scriptArgs, saveJar) = distinguishArgs(args)
35+
try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun { (outDir:Path, classpath:String) =>
4136
val classFiles = outDir.toFile.listFiles.toList match {
4237
case Nil => sys.error(s"no files below [$outDir]")
4338
case list => list
4439
}
4540

4641
val (mainClassName, mainMethod) = detectMainMethod(outDir, classpath, scriptFile)
4742

48-
if saveCompiled then
43+
if saveJar then
4944
// write a standalone jar to the script parent directory
5045
writeJarfile(outDir, scriptFile, scriptArgs, classpath, mainClassName)
5146

52-
try
53-
// invoke the compiled script main method
54-
mainMethod.invoke(null, scriptArgs)
55-
catch
56-
case e: java.lang.reflect.InvocationTargetException =>
57-
throw e.getCause
58-
47+
// invoke the compiled script main method
48+
mainMethod.invoke(null, scriptArgs)
5949
}
6050
catch
6151
case e:Exception =>
6252
e.printStackTrace
6353
println(s"Error: ${e.getMessage}")
6454
sys.exit(1)
6555

66-
def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String],
56+
case e: java.lang.reflect.InvocationTargetException =>
57+
throw e.getCause
58+
59+
private def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String],
6760
classpath:String, mainClassName: String): Unit =
6861
import java.net.{URI, URL}
6962
val jarTargetDir: Path = Option(scriptFile.toPath.getParent) match {
@@ -94,15 +87,6 @@ object Main:
9487
writer.writeAllFrom(Directory(outDir))
9588
end writeJarfile
9689

97-
lazy val verbose = Option(System.getenv("DOTC_VERBOSE")) != None
98-
99-
def showArgs(args:Array[String], compilerArgs:Array[String],
100-
scriptFile:File, scriptArgs:Array[String]): Unit =
101-
args.foreach { printf("args[%s]\n", _) }
102-
compilerArgs.foreach { printf("compilerArgs[%s]\n", _) }
103-
scriptArgs.foreach { printf("scriptArgs[%s]\n", _) }
104-
printf("scriptFile[%s]\n", scriptFile)
105-
10690
private def detectMainMethod(outDir: Path, classpath: String,
10791
scriptFile: File): (String, Method) =
10892
val outDirURL = outDir.toUri.toURL
@@ -115,8 +99,6 @@ object Main:
11599
if path.nonEmpty then s"${path}.${nameWithoutExtension}"
116100
else nameWithoutExtension
117101

118-
if verbose then printf("targetPath [%s]\n",targetPath)
119-
120102
if target.isDirectory then
121103
for
122104
packageMember <- target.listFiles.toList
@@ -139,7 +121,6 @@ object Main:
139121

140122
candidates match
141123
case Nil =>
142-
if verbose then outDir.toFile.listFiles.toList.foreach { f => System.err.printf("%s\n",f.toString) }
143124
throw ScriptingException(s"No main methods detected in script ${scriptFile}")
144125
case _ :: _ :: _ =>
145126
throw ScriptingException("A script must contain only one main method. " +
@@ -148,6 +129,8 @@ object Main:
148129
end match
149130
end detectMainMethod
150131

132+
def pathsep = sys.props("path.separator")
133+
151134
extension(pathstr:String) {
152135
def withSlash:String = pathstr.replace('\\', '/')
153136
}

compiler/test-resources/scripting/hashBang.scala

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env scala
2-
export STUFF=nada
2+
# comment
3+
STUFF=nada
34
!#
45

56
def main(args: Array[String]): Unit =
@@ -10,9 +11,7 @@ def main(args: Array[String]): Unit =
1011
val result = new java.io.StringWriter()
1112
new RuntimeException("stack").printStackTrace(new java.io.PrintWriter(result))
1213
val stack = result.toString.split("[\r\n]+").toList
13-
for( s <- stack ){
14-
System.err.printf("[%s]\n",s)
15-
}
14+
//for( s <- stack ){ System.err.printf("[%s]\n",s) }
1615
stack.filter { str => str.contains(".main(") }.map {
1716
_.replaceAll(".*[(]","").
1817
replaceAll("[:)].*","")
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env scala
2+
3+
def main(args: Array[String]): Unit =
4+
val name = sys.props("script.name")
5+
printf("script.name: %s\n",name)
6+
assert(name == "scriptName.scala")

compiler/test/dotty/tools/scripting/ScriptingTests.scala

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,98 @@ import org.junit.Test
99
import vulpix.TestConfiguration
1010

1111

12-
/** Runs all tests contained in `compiler/test-resources/repl/` */
12+
/** Runs all tests contained in `compiler/test-resources/scripting/` */
1313
class ScriptingTests:
1414
extension (str: String) def dropExtension =
1515
str.reverse.dropWhile(_ != '.').drop(1).reverse
1616

17-
@Test def scriptingTests =
18-
val testFiles = scripts("/scripting")
19-
testFiles.foreach { (f:File) => System.err.printf("script[%s]\n",f.toString) }
17+
def testFiles = scripts("/scripting")
2018

21-
val argss: Map[String, Array[String]] = (
22-
for
23-
argFile <- testFiles
24-
if argFile.getName.endsWith(".args")
25-
name = argFile.getName.dropExtension
26-
scriptArgs = readLines(argFile).toArray
27-
yield name -> scriptArgs).toMap
19+
def script2jar(scriptFile: File) =
20+
val jarName = s"${scriptFile.getName.dropExtension}.jar"
21+
File(scriptFile.getParent,jarName)
22+
23+
val argss: Map[String, Array[String]] = (
24+
for
25+
argFile <- testFiles
26+
if argFile.getName.endsWith(".args")
27+
name = argFile.getName.dropExtension
28+
scriptArgs = readLines(argFile).toArray
29+
yield name -> scriptArgs).toMap
30+
31+
@Test def scriptingDriverTests =
2832

2933
for
3034
scriptFile <- testFiles
3135
if scriptFile.getName.endsWith(".scala")
3236
name = scriptFile.getName.dropExtension
3337
scriptArgs = argss.getOrElse(name, Array.empty[String])
3438
do
39+
val unexpectedJar = script2jar(scriptFile)
40+
unexpectedJar.delete
41+
3542
ScriptingDriver(
3643
compilerArgs = Array(
37-
"-classpath", TestConfiguration.basicClasspath),
44+
"-classpath", TestConfiguration.basicClasspath
45+
),
3846
scriptFile = scriptFile,
3947
scriptArgs = scriptArgs
4048
).compileAndRun { (path:java.nio.file.Path,classpath:String) =>
4149
path.toFile.listFiles.foreach { (f:File) => printf(" [%s]\n",f.getName) }
42-
printf("%s\n%s\n",path,classpath.length)
50+
printf("path: %s\nclasspath.length: %s\n",path,classpath.length)
51+
}
52+
printf("not expecting a jar file: %s\n",unexpectedJar.getName)
53+
assert(! unexpectedJar.exists )
54+
55+
@Test def scriptingMainTests =
56+
57+
for
58+
scriptFile <- testFiles
59+
if scriptFile.getName.endsWith(".scala")
60+
name = scriptFile.getName.dropExtension
61+
scriptArgs = argss.getOrElse(name, Array.empty[String])
62+
do
63+
val unexpectedJar = script2jar(scriptFile)
64+
unexpectedJar.delete
65+
66+
sys.props("script.name") = scriptFile.getName
67+
val mainArgs: Array[String] = Array(
68+
"-classpath", TestConfiguration.basicClasspath.toString,
69+
"-script", scriptFile.toString,
70+
) ++ scriptArgs
71+
72+
Main.main(mainArgs)
73+
74+
printf("not expecting a jar file: %s\n",unexpectedJar.getName)
75+
assert(! unexpectedJar.exists )
76+
77+
@Test def scriptingJarTest =
78+
79+
for
80+
scriptFile <- testFiles
81+
if scriptFile.getName.endsWith(".scala")
82+
name = scriptFile.getName.dropExtension
83+
scriptArgs = argss.getOrElse(name, Array.empty[String])
84+
do
85+
val expectedJar = script2jar(scriptFile)
86+
expectedJar.delete
87+
88+
sys.props("script.name") = scriptFile.getName
89+
val mainArgs: Array[String] = Array(
90+
"-classpath", TestConfiguration.basicClasspath.toString,
91+
"-save",
92+
"-script", scriptFile.toString,
93+
) ++ scriptArgs
94+
95+
Main.main(mainArgs)
96+
97+
printf("expected jar file: %s\n",expectedJar.getName)
98+
assert(expectedJar.exists)
99+
100+
extension(f: File){
101+
def slashPath = f.getAbsolutePath.replace('\\','/')
43102
}
44-
103+
import scala.sys.process._
104+
val cmd = Array("java",s"-Dscript.name=${scriptFile.getName}","-jar",expectedJar.slashPath)
105+
++ scriptArgs
106+
Process(cmd).lazyLines_!.foreach { println }

dist/bin/scala

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,10 @@ while [[ $# -gt 0 ]]; do
9292
shift ;;
9393
*)
9494
if [ $execute_script == false ]; then
95-
# also a script if has scala hash bang
95+
# is a script if extension .scala or .sc or if has scala hash bang
9696
if [[ "$1" == *.scala || "$1" == *.sc || -f "$1" && `head -n 1 -- "$1" | grep '#!.*scala'` ]]; then
9797
execute_script=true
98+
[ -n "$SCALA_OPTS" ] && java_options+=($SCALA_OPTS)
9899
target_script="$1"
99100
else
100101
residual_args+=("$1")
@@ -112,12 +113,15 @@ if [ $execute_script == true ]; then
112113
if [ "$CLASS_PATH" ]; then
113114
cp_arg="-classpath \"$CLASS_PATH\""
114115
fi
115-
target_jar=${target_script%.*}.jar
116-
if [ $save_compiled == true -a -f "$target_jar" -a $target_jar -nt $target_script ]; then
117-
java -jar $target_jar
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[@]}"
118122
else
119-
[ $save_compiled == true -a -f $target_jar ] && rm -f $target_jar
120-
residual_args+=("-Dscript.name=${target_script##*/}")
123+
[[ $save_compiled == true && -f $target_jar ]] && rm -f $target_jar
124+
residual_args+=($setScriptName)
121125
eval "\"$PROG_HOME/bin/scalac\" $cp_arg ${java_options[@]} ${residual_args[@]} -script $target_script ${script_args[@]}"
122126
fi
123127
elif [ $execute_repl == true ] || ([ $execute_run == false ] && [ $options_indicator == 0 ]); then

0 commit comments

Comments
 (0)