/* __ ______________ *\ ** / |/ / ____/ ____/ ** ** / | | /___ / /___ ** ** /_/|__/_____/_____/ Copyright 2005-2007 LAMP/EPFL ** ** ** \* */ // $Id$ package scala.tools.ant import java.io.File import org.apache.tools.ant.{BuildException, Project} import org.apache.tools.ant.taskdefs.MatchingTask import org.apache.tools.ant.types.{Path, Reference} import org.apache.tools.ant.util.{FileUtils, GlobPatternMapper, SourceFileScanner} import scala.tools.nsc.{Global, Settings} import scala.tools.nsc.reporters.{Reporter, ConsoleReporter} /**

* An Ant task to compile with the new Scala compiler (NSC). *

*

* This task can take the following parameters as attributes: *

* *

* It also takes the following parameters as nested elements: *

* * * @author Gilles Dubochet, Stephane Micheloud */ class Scalac extends MatchingTask { /** The unique Ant file utilities instance to use in this task. */ private val fileUtils = FileUtils.newFileUtils() /*============================================================================*\ ** 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 logging property. */ object LoggingLevel extends PermissibleValue { val values = List("none", "verbose", "debug") } /** Defines valid values for properties that refer to compiler phases. */ object CompilerPhase extends PermissibleValue { val values = List("namer", "typer", "pickler", "uncurry", "tailcalls", "transmatch", "explicitouter", "erasure", "lambdalift", "flatten", "constructors", "mixin", "icode", "jvm", "terminal") } /** Defines valid values for the target property. */ object Target extends PermissibleValue { val values = List("jvm-1.5", "jvm-1.4", "msil", "cldc") } /** Defines valid values for the deprecation and * unchecked properties. */ object Flag extends PermissibleValue { val values = List("yes", "no", "on", "off") } /** The directories that contain source files to compile. */ private var origin: Option[Path] = None /** The directory to put the compiled files in. */ private var destination: Option[File] = None /** The class path to use for this compilation. */ private var classpath: Option[Path] = None /** The source path to use for this compilation. */ private var sourcepath: Option[Path] = None /** The boot class path to use for this compilation. */ private var bootclasspath: Option[Path] = None /** The external extensions path to use for this compilation. */ private var extdirs: Option[Path] = None /** The character encoding of the files to compile. */ private var encoding: Option[String] = None // the targetted backend private var backend: Option[String] = None /** Whether to force compilation of all files or not. */ private var force: Boolean = false /** How much logging output to print. Either none (default), * verbose or debug. */ private var logging: Option[String] = None /** Which compilation phases should be logged during compilation. */ private var logPhase: List[String] = Nil /** Whether to use implicit predefined values or not. */ private var usepredefs: Boolean = true /** Instruct the compiler to generate debugging information */ private var debugInfo: String = "line" /** Instruct the compiler to use additional parameters */ private var addParams: String = "" /** Instruct the compiler to generate deprecation information. */ private var deprecation: Boolean = false /** Instruct the compiler to generate unchecked information. */ private var unchecked: Boolean = false // Name of the output assembly (only relevant with -target:msil) private var assemname: String = "" // List of assemblies referenced by the program (only relevant with -target:msil) private var assemrefs: String = "." /** Whether the compiler is being debuged. Prints more information in case * in case of failure. */ private var scalacDebugging: Boolean = false /*============================================================================*\ ** Properties setters ** \*============================================================================*/ /** Sets the srcdir attribute. Used by Ant. * * @param input The value of origin. */ def setSrcdir(input: Path) = if (origin.isEmpty) origin = Some(input) else origin.get.append(input) /** Sets the origin as a nested src Ant parameter. * * @return An origin path to be configured. */ def createSrc(): Path = { if (origin.isEmpty) origin = Some(new Path(getProject())) origin.get.createPath() } /** Sets the origin as an external reference Ant parameter. * * @param input A reference to an origin path. */ def setSrcref(input: Reference) = createSrc().setRefid(input) /** Sets the destdir attribute. Used by Ant. * * @param input The value of destination. */ def setDestdir(input: File) = destination = Some(input) /** Sets the classpath attribute. Used by Ant. * * @param input The value of classpath. */ def setClasspath(input: Path) = if (classpath.isEmpty) classpath = Some(input) else classpath.get.append(input) /** Sets the classpath as a nested classpath Ant parameter. * * @return A class path to be configured. */ def createClasspath(): Path = { if (classpath.isEmpty) classpath = Some(new Path(getProject())) classpath.get.createPath() } /** Sets the classpath as an external reference Ant parameter. * * @param input A reference to a class path. */ def setClasspathref(input: Reference) = createClasspath().setRefid(input) /** Sets the sourcepath attribute. Used by Ant. * * @param input The value of sourcepath. */ def setSourcepath(input: Path) = if (sourcepath.isEmpty) sourcepath = Some(input) else sourcepath.get.append(input) /** Sets the sourcepath as a nested sourcepath Ant parameter. * * @return A source path to be configured. */ def createSourcepath(): Path = { if (sourcepath.isEmpty) sourcepath = Some(new Path(getProject())) sourcepath.get.createPath() } /** Sets the sourcepath as an external reference Ant parameter. * * @param input A reference to a source path. */ def setSourcepathref(input: Reference) = createSourcepath().setRefid(input) /** Sets the boot classpath attribute. Used by Ant. * * @param input The value of bootclasspath. */ def setBootclasspath(input: Path) = if (bootclasspath.isEmpty) bootclasspath = Some(input) else bootclasspath.get.append(input) /** Sets the bootclasspath as a nested sourcepath Ant * parameter. * * @return A source path to be configured. */ def createBootclasspath(): Path = { if (bootclasspath.isEmpty) bootclasspath = Some(new Path(getProject())) bootclasspath.get.createPath() } /** Sets the bootclasspath as an external reference Ant * parameter. * * @param input A reference to a source path. */ def setBootclasspathref(input: Reference) = createBootclasspath().setRefid(input) /** Sets the external extensions path attribute. Used by Ant. * * @param input The value of extdirs. */ def setExtdirs(input: Path) = if (extdirs.isEmpty) extdirs = Some(input) else extdirs.get.append(input) /** Sets the extdirs as a nested sourcepath Ant parameter. * * @return An extensions path to be configured. */ def createExtdirs(): Path = { if (extdirs.isEmpty) extdirs = Some(new Path(getProject())) extdirs.get.createPath() } /** Sets the extdirs as an external reference Ant parameter. * * @param input A reference to an extensions path. */ def setExtdirsref(input: Reference) = createExtdirs().setRefid(input) /** Sets the encoding attribute. Used by Ant. * * @param input The value of encoding. */ def setEncoding(input: String): Unit = encoding = Some(input) /** Sets the target attribute. Used by Ant. * * @param input The value for target. */ def setTarget(input: String): Unit = if (Target.isPermissible(input)) backend = Some(input) else error("Unknown target '" + input + "'") /** Sets the force attribute. Used by Ant. * * @param input The value for force. */ def setForce(input: Boolean): Unit = force = input /** Sets the logging level attribute. Used by Ant. * * @param input The value for logging. */ def setLogging(input: String) = if (LoggingLevel.isPermissible(input)) logging = Some(input) else error("Logging level '" + input + "' does not exist.") /** Sets the logphase attribute. Used by Ant. * * @param input The value for logPhase. */ def setLogPhase(input: String) = { logPhase = List.fromArray(input.split(",")).flatMap { s: String => val st = s.trim() if (CompilerPhase.isPermissible(st)) (if (input != "") List(st) else Nil) else { error("Phase " + st + " in log does not exist.") Nil } } } /** Sets the usepredefs attribute. Used by Ant. * * @param input The value for usepredefs. */ def setUsepredefs(input: Boolean): Unit = usepredefs = input /** Set the debug info attribute. * * @param input The value for debug. */ def setDebuginfo(input: String): Unit = debugInfo = input /** Set the addparams info attribute. * * @param input The value for addparams. */ def setAddparams(input: String): Unit = addParams = input /** Set the deprecation info attribute. * * @param input One of the flags yes/no or on/off. */ def setDeprecation(input: String): Unit = if (Flag.isPermissible(input)) deprecation = "yes".equals(input) || "on".equals(input) else error("Unknown deprecation flag '" + input + "'") /** Set the unchecked info attribute. * * @param input One of the flags yes/no or on/off. */ def setUnchecked(input: String): Unit = if (Flag.isPermissible(input)) unchecked = "yes".equals(input) || "on".equals(input) else error("Unknown unchecked flag '" + input + "'") /** Set the scalacdebugging info attribute. * * @param input The specified flag */ def setScalacdebugging(input: Boolean): Unit = scalacDebugging = input def setAssemname(input: String): Unit = assemname = input def setAssemrefs(input: String): Unit = assemrefs = input /*============================================================================*\ ** Properties getters ** \*============================================================================*/ /** Gets the value of the classpath attribute in a * Scala-friendly form. * * @return The class path as a list of files. */ private def getClasspath: List[File] = if (classpath.isEmpty) error("Member 'classpath' is empty.") else List.fromArray(classpath.get.list()).map(nameToFile) /** Gets the value of the origin attribute in a * Scala-friendly form. * * @return The origin path as a list of files. */ private def getOrigin: List[File] = if (origin.isEmpty) error("Member 'origin' is empty.") else List.fromArray(origin.get.list()).map(nameToFile) /** Gets the value of the destination attribute in a * Scala-friendly form. * * @return The destination as a file. */ private def getDestination: File = if (destination.isEmpty) error("Member 'destination' is empty.") else existing(getProject().resolveFile(destination.get.toString())) /** Gets the value of the sourcepath attribute in a * Scala-friendly form. * * @return The source path as a list of files. */ private def getSourcepath: List[File] = if (sourcepath.isEmpty) error("Member 'sourcepath' is empty.") else List.fromArray(sourcepath.get.list()).map(nameToFile) /** Gets the value of the bootclasspath attribute in a * Scala-friendly form. * * @return The boot class path as a list of files. */ private def getBootclasspath: List[File] = if (bootclasspath.isEmpty) error("Member 'bootclasspath' is empty.") else List.fromArray(bootclasspath.get.list()).map(nameToFile) /** Gets the value of the extdirs attribute in a * Scala-friendly form. * * @return The extensions path as a list of files. */ private def getExtdirs: List[File] = if (extdirs.isEmpty) error("Member 'extdirs' is empty.") else List.fromArray(extdirs.get.list()).map(nameToFile) /*============================================================================*\ ** Compilation and support methods ** \*============================================================================*/ /** This is forwarding method to circumvent bug #281 in Scala 2. Remove when * bug has been corrected. */ override protected def getDirectoryScanner(baseDir: File) = super.getDirectoryScanner(baseDir) /** Transforms a string name into a file relative to the provided base * directory. * * @param base A file pointing to the location relative to which the name * will be resolved. * @param name A relative or absolute path to the file as a string. * @return A file created from the name and the base file. */ private def nameToFile(base: File)(name: String): File = existing(fileUtils.resolveFile(base, name)) /** Transforms a string name into a file relative to the build root * directory. * * @param name A relative or absolute path to the file as a string. * @return A file created from the name. */ private def nameToFile(name: String): File = existing(getProject().resolveFile(name)) /** Tests if a file exists and prints a warning in case it doesn't. Always * returns the file, even if it doesn't exist. * * @param file A file to test for existance. * @return The same file. */ private def existing(file: File): File = { if (!file.exists()) log("Element '" + file.toString() + "' does not exist.", Project.MSG_WARN) file } /** Transforms a path into a Scalac-readable string. * * @param path A path to convert. * @return A string-representation of the path like a.jar:b.jar. */ private def asString(path: List[File]): String = path.map(asString).mkString("", File.pathSeparator, "") /** Transforms a file into a Scalac-readable string. * * @param path A file to convert. * @return A string-representation of the file like /x/k/a.scala. */ private def asString(file: File): String = file.getAbsolutePath() /** 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()) /*============================================================================*\ ** The big execute method ** \*============================================================================*/ /** Initializes settings and source files */ protected def initialize: Pair[Settings, List[File]] = { // Tests if all mandatory attributes are set and valid. if (origin.isEmpty) error("Attribute 'srcdir' is not set.") if (getOrigin.isEmpty) error("Attribute 'srcdir' is not set.") if (!destination.isEmpty && !destination.get.isDirectory()) error("Attribute 'destdir' does not refer to an existing directory.") if (destination.isEmpty) destination = Some(getOrigin.head) val mapper = new GlobPatternMapper() mapper.setTo("*.class") mapper.setFrom("*.scala") // Scans source directories to build up a compile lists. // If force is false, only files were the .class file in destination is // older than the .scala file will be used. val sourceFiles: List[File] = for { val originDir <- getOrigin val originFile <- { var includedFiles = getDirectoryScanner(originDir).getIncludedFiles() if (!force) { includedFiles = new SourceFileScanner(this). restrict(includedFiles, originDir, destination.get, mapper) } val list = List.fromArray(includedFiles) if (scalacDebugging && list.length > 0) log( list.mkString( "Compiling source file" + (if (list.length > 1) "s: " else ": "), ", ", " " ) + "to " + getDestination.toString() ) else if (list.length > 0) log( "Compiling " + list.length + " source file" + (if (list.length > 1) "s" else "") + (" to " + getDestination.toString()) ) else log("No files selected for compilation", Project.MSG_VERBOSE) list } } yield { log(originFile.toString(), Project.MSG_DEBUG) nameToFile(originDir)(originFile) } // Builds-up the compilation settings for Scalac with the existing Ant // parameters. val settings = new Settings(error) settings.outdir.value = asString(destination.get) if (!classpath.isEmpty) settings.classpath.value = asString(getClasspath) if (!sourcepath.isEmpty) settings.sourcepath.value = asString(getSourcepath) else if (origin.get.size() > 0) settings.sourcepath.value = origin.get.list()(0) if (!bootclasspath.isEmpty) settings.bootclasspath.value = asString(getBootclasspath) if (!extdirs.isEmpty) settings.extdirs.value = asString(getExtdirs) if (!encoding.isEmpty) settings.encoding.value = encoding.get if (!backend.isEmpty) settings.target.value = backend.get if (!logging.isEmpty && logging.get == "verbose") settings.verbose.value = true else if (!logging.isEmpty && logging.get == "debug") { settings.verbose.value = true settings.debug.value = true } if (!logPhase.isEmpty) settings.log.value = logPhase settings.nopredefs.value = !usepredefs settings.debuginfo.value = debugInfo settings.deprecation.value = deprecation settings.unchecked.value = unchecked settings.assemname.value = assemname settings.assemrefs.value = assemrefs log("Scalac params = '" + addParams + "'", Project.MSG_DEBUG) var args = if (addParams.trim() == "") Nil else List.fromArray(addParams.trim().split(" ")).map(.trim()) while (!args.isEmpty) { val argsBuf = args if (args.head startsWith "-") { for (val setting <- settings.allSettings) args = setting.tryToSet(args); } else error("Parameter '" + args.head + "' does not start with '-'.") if (argsBuf eq args) error("Parameter '" + args.head + "' is not recognised by Scalac.") } Pair(settings, sourceFiles) } /** Performs the compilation. */ override def execute() = { val Pair(settings, sourceFiles) = initialize val reporter = new ConsoleReporter(settings) // Compiles the actual code val compiler = new Global(settings, reporter) try { (new compiler.Run).compile(sourceFiles.map (.toString())) } catch { case exception: Throwable if (exception.getMessage ne null) => exception.printStackTrace() error("Compile failed because of an internal compiler error (" + exception.getMessage + "); see the error output for details.") case exception => exception.printStackTrace() error("Compile failed because of an internal compiler error " + "(no error message provided); see the error output for details.") } reporter.printSummary() if (reporter.errors > 0) error( "Compile failed with " + reporter.errors + " error" + (if (reporter.errors > 1) "s" else "") + "; see the compiler error output for details.") else if (reporter.warnings > 0) log( "Compile suceeded with " + reporter.warnings + " warning" + (if (reporter.warnings > 1) "s" else "") + "; see the compiler output for details.") } }