/* NSC -- new Scala compiler * Copyright 2005-2010 LAMP/EPFL * @author Martin Odersky */ package scala.tools.nsc import java.io.{ InputStream, OutputStream, BufferedReader, FileInputStream, FileOutputStream, FileReader, InputStreamReader, PrintWriter, FileWriter, IOException } import java.io.{ File => JFile } import io.{ Directory, File, Path, PlainFile } import java.net.URL import java.util.jar.{ JarEntry, JarOutputStream } import util.{ waitingForThreads } import scala.tools.util.PathResolver import scala.tools.nsc.reporters.{Reporter,ConsoleReporter} import util.Exceptional.unwrap /** 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!") * argv.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!") * argv.toList foreach Console.println ** * @author Lex Spoon * @version 1.0, 15/05/2006 * @todo It would be better if error output went to stderr instead * of stdout... */ object ScriptRunner { /* While I'm chasing down the fsc and script bugs. */ def DBG(msg: Any) { System.err.println(msg.toString) System.err.flush() } /** Default name to use for the wrapped script */ val defaultScriptMain = "Main" /** Pick a main object name from the specified settings */ def scriptMain(settings: Settings) = settings.script.value match { case "" => defaultScriptMain case x => x } def isScript(settings: Settings) = settings.script.value != "" /** Choose a jar filename to hold the compiled version of a script. */ private def jarFileFor(scriptFile: String): File = { val name = if (scriptFile endsWith ".jar") scriptFile else scriptFile + ".jar" File(name) } def copyStreams(in: InputStream, out: OutputStream) = { val buf = new Array[Byte](10240) def loop: Unit = in.read(buf, 0, buf.length) match { case -1 => in.close() case n => out.write(buf, 0, n) ; loop } loop } /** Try to create a jar file out of all the contents * of the directory
sourcePath
.
*/
private def tryMakeJar(jarFile: File, sourcePath: Directory) = {
def addFromDir(jar: JarOutputStream, dir: Directory, prefix: String) {
def addFileToJar(entry: File) = {
jar putNextEntry new JarEntry(prefix + entry.name)
copyStreams(entry.inputStream, jar)
jar.closeEntry
}
dir.list foreach { entry =>
if (entry.isFile) addFileToJar(entry.toFile)
else addFromDir(jar, entry.toDirectory, prefix + entry.name + "/")
}
}
try {
val jar = new JarOutputStream(jarFile.outputStream())
addFromDir(jar, sourcePath, "")
jar.close
}
catch {
case _: Exception => jarFile.delete()
}
}
/** Read the entire contents of a file as a String. */
private def contentsOfFile(filename: String) = File(filename).slurp()
/** Split a fully qualified object name into a
* package and an unqualified object name */
private def splitObjectName(fullname: String): (Option[String], String) =
(fullname lastIndexOf '.') match {
case -1 => (None, fullname)
case idx => (Some(fullname take idx), fullname drop (idx + 1))
}
/** Compile a script using the fsc compilation daemon.
*
* @param settings ...
* @param scriptFileIn ...
* @return ...
*/
private def compileWithDaemon(
settings: GenericRunnerSettings,
scriptFileIn: String): Boolean =
{
val scriptFile = Path(scriptFileIn).toAbsolute.path
val compSettingNames = new Settings(sys.error).visibleSettings.toList map (_.name)
val compSettings = settings.visibleSettings.toList filter (compSettingNames contains _.name)
val coreCompArgs = compSettings flatMap (_.unparse)
val compArgs = coreCompArgs ::: List("-Xscript", scriptMain(settings), scriptFile)
var compok = true
val socket = CompileSocket getOrCreateSocket "" getOrElse (return false)
socket.applyReaderAndWriter { (in, out) =>
out println (CompileSocket getPassword socket.getPort)
out println (compArgs mkString "\0")
try {
for (fromServer <- (Iterator continually in.readLine()) takeWhile (_ != null)) {
Console.err println fromServer
if (CompileSocket.errorPattern matcher fromServer matches)
compok = false
}
}
finally socket.close()
}
compok
}
protected def newGlobal(settings: Settings, reporter: Reporter) =
new Global(settings, reporter)
/** 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(
settings: GenericRunnerSettings,
scriptFile: String)
(handler: String => Boolean): Boolean =
{
/** Compiles the script file, and returns the directory with the compiled
* class files, if the compilation succeeded.
*/
def compile: Option[Directory] = {
val compiledPath = Directory makeTemp "scalascript"
// delete the directory after the user code has finished
sys.addShutdownHook(compiledPath.deleteRecursively())
settings.outdir.value = compiledPath.path
if (settings.nocompdaemon.value) {
/** Setting settings.script.value informs the compiler this is not a
* self contained compilation unit.
*/
settings.script.value = scriptMain(settings)
val reporter = new ConsoleReporter(settings)
val compiler = newGlobal(settings, reporter)
val cr = new compiler.Run
cr compile List(scriptFile)
if (reporter.hasErrors) None else Some(compiledPath)
}
else if (compileWithDaemon(settings, scriptFile)) Some(compiledPath)
else None
}
/** The script runner calls sys.exit to communicate a return value, but this must
* not take place until there are no non-daemon threads running. Tickets #1955, #2006.
*/
waitingForThreads {
if (settings.savecompiled.value) {
val jarFile = jarFileFor(scriptFile)
def jarOK = jarFile.canRead && (jarFile isFresher File(scriptFile))
def recompile() = {
jarFile.delete()
compile match {
case Some(compiledPath) =>
tryMakeJar(jarFile, compiledPath)
if (jarOK) {
compiledPath.deleteRecursively()
handler(jarFile.toAbsolute.path)
}
// jar failed; run directly from the class files
else handler(compiledPath.path)
case _ => false
}
}
if (jarOK) handler(jarFile.toAbsolute.path) // pre-compiled jar is current
else recompile() // jar old - recompile the script.
}
// don't use a cache jar at all--just use the class files
else compile exists (cp => handler(cp.path))
}
}
/** Run a script after it has been compiled
*
* @return true if execution succeeded, false otherwise
*/
private def runCompiled(
settings: GenericRunnerSettings,
compiledLocation: String,
scriptArgs: List[String]): Boolean =
{
val pr = new PathResolver(settings)
val classpath = File(compiledLocation).toURL +: pr.asURLs
ObjectRunner.runAndCatch(classpath, scriptMain(settings), scriptArgs) match {
case Left(ex) => ex.printStackTrace() ; false
case _ => true
}
}
/** Run a script file with the specified arguments and compilation
* settings.
*
* @return true if compilation and execution succeeded, false otherwise.
*/
def runScript(
settings: GenericRunnerSettings,
scriptFile: String,
scriptArgs: List[String]): Boolean =
{
if (File(scriptFile).isFile)
withCompiledScript(settings, scriptFile) { runCompiled(settings, _, scriptArgs) }
else
throw new IOException("no such file: " + scriptFile)
}
/** Calls runScript and catches the enumerated exceptions, routing
* them to Left(ex) if thrown.
*/
def runScriptAndCatch(
settings: GenericRunnerSettings,
scriptFile: String,
scriptArgs: List[String]): Either[Throwable, Boolean] =
{
try Right(runScript(settings, scriptFile, scriptArgs))
catch { case e => Left(unwrap(e)) }
}
/** Run a command
*
* @return true if compilation and execution succeeded, false otherwise.
*/
def runCommand(
settings: GenericRunnerSettings,
command: String,
scriptArgs: List[String]): Boolean =
{
val scriptFile = File.makeTemp("scalacmd", ".scala")
// save the command to the file
scriptFile writeAll command
try withCompiledScript(settings, scriptFile.path) { runCompiled(settings, _, scriptArgs) }
finally scriptFile.delete() // in case there was a compilation error
}
}