/
ScriptRunner.scala
209 lines (184 loc) · 7.81 KB
/
ScriptRunner.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package scala.tools.nsc
import scala.reflect.io.{ AbstractFile, Directory, File, Path }
import scala.tools.nsc.classpath.ClassPathFactory
import scala.tools.nsc.io.Jar
import scala.tools.nsc.reporters.{ ConsoleReporter, Reporter }
import scala.util.chaining._
import scala.util.control.NonFatal
import java.io.IOException
/** An object that runs Scala code in script files.
*
* For example, here is a complete Scala script on Unix:
* {{{
* #!/bin/sh
* exec scala "\$0" "\$@"
* !#
* Console.println("Hello, world!")
* args.toList foreach Console.println
* }}}
* And here is a batch file example on Windows XP:
* {{{
* ::#!
* @echo off
* call scala %0 %*
* goto :eof
* ::!#
* Console.println("Hello, world!")
* args.toList foreach Console.println
* }}}
*
* @author Lex Spoon
* @todo It would be better if error output went to stderr instead
* of stdout...
*/
trait ScriptRunner {
/** Run a script file by name, with the given arguments.
* @return optionally an error, None for success
*/
def runScript(script: String, scriptArgs: List[String]): Option[Throwable]
/** Run the script text as supplied, with the given arguments.
* @return optionally an error, None for success
*/
def runScriptText(script: String, scriptArgs: List[String]): Option[Throwable]
}
class DefaultScriptRunner(settings: GenericRunnerSettings) extends AbstractScriptRunner(settings) {
protected def doCompile(scriptFile: String) = {
val reporter = new ConsoleReporter(settings)
val compiler = newGlobal(settings, reporter)
if (settings.pastefiles.value.nonEmpty) new compiler.Run().compile(settings.pastefiles.value)
// Setting settings.script.value informs the compiler this is not a self-contained compilation unit.
settings.script.value = mainClass
new compiler.Run().compile(scriptFile :: Nil)
!reporter.hasErrors
}
protected def newGlobal(settings: Settings, reporter: Reporter) = Global(settings, reporter)
}
abstract class AbstractScriptRunner(settings: GenericRunnerSettings) extends ScriptRunner {
/** Do compile the given script file, returning true for success. */
protected def doCompile(scriptFile: String): Boolean
protected final def mainClass: String = settings.script.value
/** Compile a script and then run the specified closure with
* a classpath for the compiled script.
*
* @return true if compilation and the handler succeeds, false otherwise.
*/
private def withCompiledScript(scriptFile: String)(handler: String => Option[Throwable]): Option[Throwable] = {
/* Compiles the script file, with the output set to either
* the user-specified location (jar or dir), or a temp dir.
* Returns the output location on success.
*/
def compile: Option[Path] = {
val outpath =
if (settings.outdir.isSetByUser)
Path(settings.outdir.value)
else
Directory.makeTemp("scalascript").tap { tmp =>
// delete the directory after the user code has finished
Runtime.getRuntime.addShutdownHook(new Thread(() => tmp.deleteRecursively()))
settings.outdir.value = tmp.path
}
if (doCompile(scriptFile)) Some(outpath) else None
}
def hasClassToRun(location: Path): Boolean = {
val cp = ClassPathFactory.newClassPath(AbstractFile.getDirectory(location), settings)
cp.findClass(mainClass).isDefined
}
// under -save, compile to a jar, specified either by -d or based on script name.
// if -d specifies a dir, assemble the jar by hand.
def withLatestJar(): Option[Throwable] = {
val outputToJar = settings.outdir.value.endsWith(".jar")
def stripped = List(".scala", ".sc").find(scriptFile.endsWith).map(scriptFile.stripSuffix).getOrElse(scriptFile)
val jarFile = File(
if (outputToJar) settings.outdir.value
else s"$stripped.jar".tap(j => if (!settings.outdir.isSetByUser) settings.outdir.value = j)
)
def jarOK = jarFile.canRead && jarFile.isFresher(File(scriptFile))
def recompile(): Option[Throwable] = {
jarFile.delete()
compile match {
case Some(compiledPath) =>
if (hasClassToRun(compiledPath)) {
// user -d mydir -save means assemble script.jar, don't delete mydir
if (!Jar.isJarOrZip(compiledPath)) {
try {
Jar.create(jarFile, compiledPath.toDirectory, mainClass)
None
} catch {
case NonFatal(e) => jarFile.delete() ; Some(e)
}
} else None
} else Some(NoScriptError)
case _ => Some(ScriptCompileError)
}
}
val err = if (!jarOK) recompile() else None
err orElse handler(jarFile.toAbsolute.path) filterNot { case NoScriptError => true case _ => false }
}
/* The script runner calls System.exit to communicate a return value, but this must
* not take place until there are no non-daemon threads running. Tickets #1955, #2006.
*/
util.waitingForThreads {
// either update the jar or don't use a cache jar at all, just use the class files, if they exist
if (settings.save.value) withLatestJar()
else {
compile match {
case Some(cp) if hasClassToRun(cp) => handler(cp.path)
case Some(_) => None
case _ => Some(ScriptCompileError)
}
}
}
}
/** Run a script after it has been compiled. Prints any exceptions.
*
* @return true if execution succeeded, false otherwise
*/
private def runCompiled(compiledLocation: String, scriptArgs: List[String]): Option[Throwable] = {
val cp = File(compiledLocation).toURL +: settings.classpathURLs
ObjectRunner.runAndCatch(cp, mainClass, scriptArgs)
}
final def runScript(scriptFile: String, scriptArgs: List[String]): Option[Throwable] = {
val f = File(scriptFile)
def usingCompilationServer = settings.Yscriptrunner.valueSetByUser.map(_ != "default").getOrElse(false)
if (!f.exists) Some(new IOException(s"no such file: $scriptFile"))
else if (!f.canRead) Some(new IOException(s"can't read: $scriptFile"))
else if (f.isDirectory) Some(new IOException(s"can't compile a directory: $scriptFile"))
else if (!f.isFile && usingCompilationServer) Some(new IOException(s"compile server requires a regular file: $scriptFile"))
else withCompiledScript(scriptFile) { runCompiled(_, scriptArgs) }
}
final def runScriptText(command: String, scriptArgs: List[String]): Option[Throwable] = {
val scriptFile = File.makeTemp("scalacmd", ".scala")
// save the command to the file
scriptFile writeAll command
try withCompiledScript(scriptFile.path) { runCompiled(_, scriptArgs) }
catch {
case NonFatal(e) => Some(e)
}
finally scriptFile.delete() // in case there was a compilation error
}
}
object ScriptRunner {
import scala.reflect.internal.util.ScalaClassLoader
def apply(settings: GenericRunnerSettings): ScriptRunner =
settings.Yscriptrunner.value match {
case "default" => new DefaultScriptRunner(settings)
case "resident" => new fsc.ResidentScriptRunner(settings)
case "shutdown" => new fsc.DaemonKiller(settings)
case custom =>
val loader = new ClassLoader(getClass.getClassLoader) with ScalaClassLoader
loader.create[ScriptRunner](custom, settings.errorFn)(settings)
}
}
object ScriptCompileError extends scala.util.control.ControlThrowable
object NoScriptError extends scala.util.control.ControlThrowable