Skip to content

Commit be1072f

Browse files
committed
+ 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
1 parent 4604261 commit be1072f

File tree

5 files changed

+146
-35
lines changed

5 files changed

+146
-35
lines changed

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

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,41 @@ package dotty.tools.scripting
22

33
import java.io.File
44
import java.nio.file.Path
5+
import dotty.tools.dotc.config.Properties.isWin
56

67
/** Main entry point to the Scripting execution engine */
78
object Main:
89
/** All arguments before -script <target_script> are compiler arguments.
910
All arguments afterwards are script arguments.*/
10-
private def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean) =
11+
private def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean, Boolean) =
1112
val (leftArgs, rest) = args.splitAt(args.indexOf("-script"))
1213
assert(rest.size >= 2,s"internal error: rest == Array(${rest.mkString(",")})")
1314

1415
val file = File(rest(1))
1516
val scriptArgs = rest.drop(2)
1617
var saveJar = false
18+
var invokeFlag = true // by default, script main method is invoked
1719
val compilerArgs = leftArgs.filter {
1820
case "-save" | "-savecompiled" =>
1921
saveJar = true
2022
false
23+
case "-compile-only" =>
24+
invokeFlag = false // no call to script main method
25+
false
2126
case _ =>
2227
true
2328
}
24-
(compilerArgs, file, scriptArgs, saveJar)
29+
(compilerArgs, file, scriptArgs, saveJar, invokeFlag)
2530
end distinguishArgs
2631

2732
def main(args: Array[String]): Unit =
28-
val (compilerArgs, scriptFile, scriptArgs, saveJar) = distinguishArgs(args)
33+
val (compilerArgs, scriptFile, scriptArgs, saveJar, invokeFlag) = distinguishArgs(args)
2934
val driver = ScriptingDriver(compilerArgs, scriptFile, scriptArgs)
3035
try driver.compileAndRun { (outDir:Path, classpath:String, mainClass: String) =>
3136
if saveJar then
3237
// write a standalone jar to the script parent directory
3338
writeJarfile(outDir, scriptFile, scriptArgs, classpath, mainClass)
39+
invokeFlag
3440
}
3541
catch
3642
case ScriptingException(msg) =>
@@ -54,12 +60,7 @@ object Main:
5460
def scriptBasename = scriptFile.getName.takeWhile(_!='.')
5561
val jarPath = s"$jarTargetDir/$scriptBasename.jar"
5662

57-
val cpPaths = runtimeClasspath.split(pathsep).map {
58-
// protect relative paths from being converted to absolute
59-
case str if str.startsWith(".") && File(str).isDirectory => s"${str.withSlash}/"
60-
case str if str.startsWith(".") => str.withSlash
61-
case str => File(str).toURI.toURL.toString
62-
}
63+
val cpPaths = runtimeClasspath.split(pathsep).map(_.absPath)
6364

6465
import java.util.jar.Attributes.Name
6566
val cpString:String = cpPaths.distinct.mkString(" ")
@@ -79,6 +80,29 @@ object Main:
7980

8081
def pathsep = sys.props("path.separator")
8182

82-
extension(pathstr:String) {
83-
def withSlash:String = pathstr.replace('\\', '/')
83+
84+
extension(file: File){
85+
def norm: String = file.toString.norm
86+
}
87+
88+
extension(path: String) {
89+
// Normalize path separator, convert relative path to absolute
90+
def norm: String =
91+
path.replace('\\', '/') match {
92+
case s if s.secondChar == ":" => s.drop(2)
93+
case s if s.startsWith("./") => s.drop(2)
94+
case s => s
95+
}
96+
97+
// convert to absolute path relative to cwd.
98+
def absPath: String = norm match
99+
case str if str.isAbsolute => norm
100+
case _ => s"/${sys.props("user.dir").norm}/$norm"
101+
102+
def absFile: File = File(path.absPath)
103+
104+
// Treat norm paths with a leading '/' as absolute.
105+
// Windows java.io.File#isAbsolute treats them as relative.
106+
def isAbsolute = path.norm.startsWith("/") || (isWin && path.secondChar == ":")
107+
def secondChar: String = path.take(2).drop(1).mkString("")
84108
}

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

Lines changed: 9 additions & 7 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(pack:(Path, String, String) => Unit = null): Unit =
20+
def compileAndRun(pack:(Path, String, String) => Boolean = 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,
@@ -28,12 +28,14 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs:
2828

2929
try
3030
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)
31+
val invokeMain: Boolean =
32+
Option(pack) match
33+
case Some(func) =>
34+
func(outDir, ctx.settings.classpath.value, mainClass)
35+
case None =>
36+
true
37+
end match
38+
if invokeMain then mainMethod.invoke(null, scriptArgs)
3739
catch
3840
case e: java.lang.reflect.InvocationTargetException =>
3941
throw e.getCause
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env scala
2+
3+
import java.io.File
4+
5+
// create an empty file
6+
def main(args: Array[String]): Unit =
7+
val file = File("touchedFile.out")
8+
file.createNewFile();

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

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ class ScriptingTests:
1414
extension (str: String) def dropExtension =
1515
str.reverse.dropWhile(_ != '.').drop(1).reverse
1616

17+
extension(f: File) def absPath =
18+
f.getAbsolutePath.replace('\\','/')
19+
1720
def testFiles = scripts("/scripting")
1821

1922
def script2jar(scriptFile: File) =
@@ -23,7 +26,6 @@ class ScriptingTests:
2326
def showScriptUnderTest(scriptFile: File): Unit =
2427
printf("===> test script name [%s]\n",scriptFile.getName)
2528

26-
2729
val argss: Map[String, Array[String]] = (
2830
for
2931
argFile <- testFiles
@@ -40,8 +42,17 @@ class ScriptingTests:
4042
scriptArgs = argss.getOrElse(name, Array.empty[String])
4143
yield scriptFile -> scriptArgs).toList.sortBy { (file,args) => file.getName }
4244

43-
@Test def scriptingDriverTests =
45+
def callExecutableJar(script: File,jar: File, scriptArgs: Array[String] = Array.empty[String]) = {
46+
import scala.sys.process._
47+
val cmd = Array("java",s"-Dscript.path=${script.getName}","-jar",jar.absPath)
48+
++ scriptArgs
49+
Process(cmd).lazyLines_!.foreach { println }
50+
}
4451

52+
/*
53+
* Call .scala scripts without -save option, verify no jar created
54+
*/
55+
@Test def scriptingDriverTests =
4556
for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".scala") do
4657
showScriptUnderTest(scriptFile)
4758
val unexpectedJar = script2jar(scriptFile)
@@ -56,9 +67,13 @@ class ScriptingTests:
5667
scriptArgs = scriptArgs
5768
).compileAndRun { (path:java.nio.file.Path,classpath:String, mainClass:String) =>
5869
printf("mainClass from ScriptingDriver: %s\n",mainClass)
70+
true // call compiled script main method
5971
}
6072
assert(! unexpectedJar.exists, s"not expecting jar file: ${unexpectedJar.absPath}")
6173

74+
/*
75+
* Call .sc scripts without -save option, verify no jar created
76+
*/
6277
@Test def scriptingMainTests =
6378
for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".sc") do
6479
showScriptUnderTest(scriptFile)
@@ -74,6 +89,9 @@ class ScriptingTests:
7489
Main.main(mainArgs)
7590
assert(! unexpectedJar.exists, s"not expecting jar file: ${unexpectedJar.absPath}")
7691

92+
/*
93+
* Call .sc scripts with -save option, verify jar is created.
94+
*/
7795
@Test def scriptingJarTest =
7896
for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".sc") do
7997
showScriptUnderTest(scriptFile)
@@ -92,11 +110,74 @@ class ScriptingTests:
92110
printf("===> test script jar name [%s]\n",expectedJar.getName)
93111
assert(expectedJar.exists)
94112

95-
import scala.sys.process._
96-
val cmd = Array("java",s"-Dscript.path=${scriptFile.getName}","-jar",expectedJar.absPath)
97-
++ scriptArgs
98-
Process(cmd).lazyLines_!.foreach { println }
99-
100-
extension(f: File){
101-
def absPath = f.getAbsolutePath.replace('\\','/')
102-
}
113+
callExecutableJar(scriptFile, expectedJar, scriptArgs)
114+
115+
/*
116+
* Verify that when ScriptingDriver callback returns true, main is called.
117+
* Verify that when ScriptingDriver callback returns false, main is not called.
118+
*/
119+
@Test def scriptCompileOnlyTests =
120+
val scriptFile = touchFileScript
121+
showScriptUnderTest(scriptFile)
122+
123+
// verify main method not called when false is returned
124+
printf("testing script compile, with no call to script main method.\n")
125+
touchedFile.delete
126+
assert(!touchedFile.exists, s"unable to delete ${touchedFile}")
127+
ScriptingDriver(
128+
compilerArgs = Array("-classpath", TestConfiguration.basicClasspath),
129+
scriptFile = scriptFile,
130+
scriptArgs = Array.empty[String]
131+
).compileAndRun { (path:java.nio.file.Path,classpath:String, mainClass:String) =>
132+
printf("success: no call to main method in mainClass: %s\n",mainClass)
133+
false // no call to compiled script main method
134+
}
135+
touchedFile.delete
136+
assert( !touchedFile.exists, s"unable to delete ${touchedFile}" )
137+
138+
// verify main method is called when true is returned
139+
printf("testing script compile, with call to script main method.\n")
140+
ScriptingDriver(
141+
compilerArgs = Array("-classpath", TestConfiguration.basicClasspath),
142+
scriptFile = scriptFile,
143+
scriptArgs = Array.empty[String]
144+
).compileAndRun { (path:java.nio.file.Path,classpath:String, mainClass:String) =>
145+
printf("call main method in mainClass: %s\n",mainClass)
146+
true // call compiled script main method, create touchedFile
147+
}
148+
149+
if touchedFile.exists then
150+
printf("success: script created file %s\n",touchedFile)
151+
if touchedFile.exists then printf("success: created file %s\n",touchedFile)
152+
assert( touchedFile.exists, s"expected to find file ${touchedFile}" )
153+
154+
/*
155+
* Compile touchFile.sc to create executable jar, verify jar execution succeeds.
156+
*/
157+
@Test def scriptingNoCompileJar =
158+
val scriptFile = touchFileScript
159+
showScriptUnderTest(scriptFile)
160+
val expectedJar = script2jar(scriptFile)
161+
sys.props("script.path") = scriptFile.absPath
162+
val mainArgs: Array[String] = Array(
163+
"-classpath", TestConfiguration.basicClasspath.toString,
164+
"-save",
165+
"-script", scriptFile.toString,
166+
"-compile-only"
167+
)
168+
169+
expectedJar.delete
170+
Main.main(mainArgs) // create executable jar
171+
printf("===> test script jar name [%s]\n",expectedJar.getName)
172+
assert(expectedJar.exists,s"unable to create executable jar [$expectedJar]")
173+
174+
touchedFile.delete
175+
assert(!touchedFile.exists,s"unable to delete ${touchedFile}")
176+
printf("calling executable jar %s\n",expectedJar)
177+
callExecutableJar(scriptFile, expectedJar)
178+
if touchedFile.exists then
179+
printf("success: executable jar created file %s\n",touchedFile)
180+
assert( touchedFile.exists, s"expected to find file ${touchedFile}" )
181+
182+
def touchFileScript = testFiles.find(_.getName == "touchFile.sc").get
183+
def touchedFile = File("touchedFile.out")

dist/bin/scala

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ truncated_params="${*#-}"
5353
# options_indicator != 0 if at least one parameter is not an option
5454
options_indicator=$(( ${#all_params} - ${#truncated_params} - $# ))
5555

56+
[ -n "$SCALA_OPTS" ] && set -- "$@" $SCALA_OPTS
57+
5658
while [[ $# -gt 0 ]]; do
5759
case "$1" in
5860
-repl)
@@ -73,7 +75,7 @@ while [[ $# -gt 0 ]]; do
7375
with_compiler=true
7476
shift
7577
;;
76-
@*|-color:*)
78+
@*|-color:*|-compile-only)
7779
addDotcOptions "${1}"
7880
shift
7981
;;
@@ -113,12 +115,6 @@ if [ $execute_script == true ]; then
113115
if [ "$CLASS_PATH" ]; then
114116
cp_arg="-classpath \"$CLASS_PATH\""
115117
fi
116-
if [ -n "$SCALA_OPTS" ]; then
117-
java_options+=($SCALA_OPTS)
118-
if [ "${SCALA_OPTS##*-save}" != "${SCALA_OPTS}" ]; then
119-
save_compiled=true
120-
fi
121-
fi
122118
setScriptName="-Dscript.path=$target_script"
123119
target_jar="${target_script%.*}.jar"
124120
if [[ $save_compiled == true && "$target_jar" -nt "$target_script" ]]; then

0 commit comments

Comments
 (0)