/* __ *\ ** ________ ___ / / ___ Scala Ant Tasks ** ** / __/ __// _ | / / / _ | (c) 2005-2009, LAMP/EPFL ** ** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** ** /____/\___/_/ |_/____/_/ | | ** ** |/ ** \* */ // $Id: ScalaTool.scala 18623 2009-09-01 12:38:38Z rytz $ package scala.tools.ant import java.io.{File, InputStream, FileWriter} import org.apache.tools.ant.BuildException import org.apache.tools.ant.taskdefs.MatchingTask import org.apache.tools.ant.types.{Path, Reference} /** <p> * An Ant task that generates a shell or batch script to execute a * Scala program. * This task can take the following parameters as attributes: * </p><ul> * <li>file (mandatory),</li> * <li>class (mandatory),</li> * <li>platforms,</li> * <li>classpath,</li> * <li>properties,</li> * <li>javaflags,</li> * <li>toolflags.</li></ul> * * @author Gilles Dubochet * @version 1.1 */ class ScalaTool extends MatchingTask { private def emptyPath = new Path(getProject) /*============================================================================*\ ** Ant user-properties ** \*============================================================================*/ abstract class PermissibleValue { val values: List[String] def isPermissible(value: String): Boolean = (value == "") || values.exists(_.startsWith(value)) } /** Defines valid values for the platforms property. */ object Platforms extends PermissibleValue { val values = List("unix", "windows") } /** The path to the exec script file. ".bat" will be appended for the * Windows BAT file, if generated. */ private var file: Option[File] = None /** The main class to run. */ private var mainClass: Option[String] = None /** Supported platforms for the script. Either "unix" or "windows". * Defaults to both. */ private var platforms: List[String] = List("unix", "windows") /** An (optional) path to all JARs that this script depend on. Paths must be * relative to the scala home directory. If not set, all JAR archives and * folders in "lib/" are automatically added. */ private var classpath: List[String] = Nil /** Comma-separated Java system properties to pass to the JRE. Properties * are formated as name=value. Properties scala.home, scala.tool.name and * scala.tool.version are always set. */ private var properties: List[(String, String)] = Nil /** Additional flags passed to the JRE ("java [javaFlags] class"). */ private var javaFlags: String = "" /** Additional flags passed to the tool ("java class [toolFlags]"). Can only * be set when a main class is defined. */ private var toolFlags: String = "" /*============================================================================*\ ** Properties setters ** \*============================================================================*/ /** Sets the file attribute. */ def setFile(input: File) = file = Some(input) /** Sets the main class attribute. */ def setClass(input: String) = mainClass = Some(input) /** Sets the platforms attribute. */ def setPlatforms(input: String) = { platforms = input.split(",").toList.flatMap { s: String => val st = s.trim if (Platforms.isPermissible(st)) (if (input != "") List(st) else Nil) else { error("Platform " + st + " does not exist.") Nil } } } /** * Sets the classpath with which to run the tool. * Note that this mechanism of setting the classpath is generally preferred * for general purpose scripts, as this does not assume all elements are * relative to the ant basedir. Additionally, the platform specific demarcation * of any script variables (e.g. ${SCALA_HOME} or %SCALA_HOME%) can be specified * in a platform independant way (e.g. @SCALA_HOME@) and automatically translated * for you. */ def setClassPath(input: String): Unit = { classpath = classpath ::: input.split(",").toList } /** * Adds an Ant Path reference to the tool's classpath. * Note that all entries in the path must exist either relative to the project * basedir or with an absolute path to a file in the filesystem. As a result, * this is not a mechanism for setting the classpath for more general use scripts, * such as those distributed within sbaz distribution packages. */ def setClassPathRef(input: Reference): Unit = { val tmpPath = emptyPath tmpPath.setRefid(input) classpath = classpath ::: tmpPath.list.toList } /** Sets JVM properties that will be set whilst running the tool. */ def setProperties(input: String) = { properties = input.split(",").toList.flatMap { s: String => val st = s.trim val stArray = st.split("=", 2) if (stArray.length == 2) { if (input != "") List(Pair(stArray(0), stArray(1))) else Nil } else error("Property " + st + " is not formatted properly.") } } /** Sets flags to be passed to the Java interpreter. */ def setJavaflags(input: String) = javaFlags = input.trim /** Sets flags to be passed to the tool. */ def setToolflags(input: String) = toolFlags = input.trim /*============================================================================*\ ** Properties getters ** \*============================================================================*/ /** Gets the value of the classpath attribute in a Scala-friendly form. * @returns The class path as a list of files. */ private def getUnixclasspath: String = transposeVariableMarkup(classpath.mkString("", ":", "").replace('\\', '/'), "${", "}") /** Gets the value of the classpath attribute in a Scala-friendly form. * @returns The class path as a list of files. */ private def getWinclasspath: String = transposeVariableMarkup(classpath.mkString("", ";", "").replace('/', '\\'), "%", "%") private def getProperties: String = properties.map({ case Pair(name,value) => "-D" + name + "=\"" + value + "\"" }).mkString("", " ", "") /*============================================================================*\ ** Compilation and support methods ** \*============================================================================*/ /** Generates a build error. Error location will be the current task in the * ant file. * @param message A message describing the error. * @throws BuildException A build error exception thrown in every case. */ private def error(message: String): Nothing = throw new BuildException(message, getLocation()) // XXX encoding and generalize private def getResourceAsCharStream(clazz: Class[_], resource: String): Stream[Char] = { val stream = clazz.getClassLoader() getResourceAsStream resource if (stream == null) Stream.empty else Stream continually stream.read() takeWhile (_ != -1) map (_.asInstanceOf[Char]) } // Converts a variable like @SCALA_HOME@ to ${SCALA_HOME} when pre = "${" and post = "}" private def transposeVariableMarkup(text: String, pre: String, post: String) : String = { val chars = scala.io.Source.fromString(text) val builder = new StringBuilder() while (chars.hasNext) { val char = chars.next if (char == '@') { var char = chars.next val token = new StringBuilder() while (chars.hasNext && char != '@') { token.append(char) char = chars.next } if (token.toString == "") builder.append('@') else builder.append(pre + token.toString + post) } else builder.append(char) } builder.toString } private def readAndPatchResource(resource: String, tokens: Map[String, String]): String = { val chars = getResourceAsCharStream(this.getClass, resource).iterator val builder = new StringBuilder() while (chars.hasNext) { val char = chars.next if (char == '@') { var char = chars.next val token = new StringBuilder() while (chars.hasNext && char != '@') { token.append(char) char = chars.next } if (tokens.contains(token.toString)) builder.append(tokens(token.toString)) else if (token.toString == "") builder.append('@') else builder.append("@" + token.toString + "@") } else builder.append(char) } builder.toString } private def writeFile(file: File, content: String) = if (file.exists() && !file.canWrite()) error("File " + file + " is not writable") else { val writer = new FileWriter(file, false) writer.write(content) writer.close() } /*============================================================================*\ ** The big execute method ** \*============================================================================*/ /** Performs the tool creation. */ override def execute() = { // Tests if all mandatory attributes are set and valid. if (file.isEmpty) error("Attribute 'file' is not set.") if (mainClass.isEmpty) error("Main class must be set.") val resourceRoot = "scala/tools/ant/templates/" val patches = Map ( ("class", mainClass.get), ("properties", getProperties), ("javaflags", javaFlags), ("toolflags", toolFlags) ) if (platforms.contains("unix")) { val unixPatches = patches + (("classpath", getUnixclasspath)) val unixTemplateResource = resourceRoot + "tool-unix.tmpl" val unixTemplate = readAndPatchResource(unixTemplateResource, unixPatches) writeFile(file.get, unixTemplate) } if (platforms.contains("windows")) { val winPatches = patches + (("classpath", getWinclasspath)) val winTemplateResource = resourceRoot + "tool-windows.tmpl" val winTemplate = readAndPatchResource(winTemplateResource, winPatches) writeFile(new File(file.get.getAbsolutePath() + ".bat"), winTemplate) } } }