diff --git a/cache/src/test/scala/CacheTest.scala b/cache/src/test/scala/CacheTest.scala
index d4d767c01..f0cf919aa 100644
--- a/cache/src/test/scala/CacheTest.scala
+++ b/cache/src/test/scala/CacheTest.scala
@@ -16,6 +16,7 @@ object CacheTest// extends Properties("Cache test")
val cTask = (createTask :: cached :: TNil) map { case (file :: len :: HNil) => println("File: " + file + " length: " + len); len :: file :: HNil }
val cachedC = Cache(cTask, new File("/tmp/c-cache"))
- TaskRunner(cachedC).left.foreach(_.foreach(f => f.exception.printStackTrace))
+ try { TaskRunner(cachedC) }
+ catch { case TasksFailed(failures) => failures.foreach(_.exception.printStackTrace) }
}
}
\ No newline at end of file
diff --git a/compile/Compiler.scala b/compile/Compiler.scala
new file mode 100644
index 000000000..98ae84455
--- /dev/null
+++ b/compile/Compiler.scala
@@ -0,0 +1,137 @@
+package xsbt
+
+import xsbti.{AnalysisCallback, Logger}
+import java.io.File
+import java.net.URLClassLoader
+
+/** A component manager provides access to the pieces of xsbt that are distributed as components.
+* There are two types of components. The first type is compiled subproject jars with their dependencies.
+* The second type is a subproject distributed as a source jar so that it can be compiled against a specific
+* version of Scala.
+*
+* The component manager provides services to install and retrieve components to the local repository.
+* This is used for source jars so that the compilation need not be repeated for other projects on the same
+* machine.
+*/
+trait ComponentManager extends NotNull
+{
+ def directory(id: String): File =
+ {
+ error("TODO: implement")
+ }
+ def jars(id: String): Iterable[File] =
+ {
+ val dir = directory(id)
+ if(dir.isDirectory)
+ FileUtilities.jars(dir)
+ else
+ Nil
+ }
+}
+
+
+/** Interface to the Scala compiler. This class uses the Scala library and compiler obtained through the 'scalaLoader' class
+* loader. This class requires a ComponentManager in order to obtain the interface code to scalac and the analysis plugin. Because
+* these call Scala code for a different Scala version, they must be compiled for the version of Scala being used.
+* It is essential that the provided 'scalaVersion' be a 1:1 mapping to the actual version of Scala being used for compilation
+* (-SNAPSHOT is bad). Otherwise, binary compatibility issues will ensue!*/
+class Compiler(scalaLoader: ClassLoader, val scalaVersion: String, private[xsbt] val manager: ComponentManager)
+{
+ // this is the instance used to compile the analysis
+ lazy val componentCompiler = new ComponentCompiler(this)
+ /** A basic interface to the compiler. It is called in the same virtual machine, but no dependency analysis is done. This
+ * is used, for example, to compile the interface/plugin code.*/
+ object raw
+ {
+ def apply(arguments: Seq[String])
+ {
+ // reflection is required for binary compatibility
+ // The following inputs ensure there is a compile error if the class names change, but they should not actually be used
+ import scala.tools.nsc.{CompilerCommand, FatalError, Global, Settings, reporters, util}
+ val mainClass = Class.forName("scala.tools.nsc.Main", true, scalaLoader)
+ val main = mainClass.asInstanceOf[{def process(args: Array[String]): Unit }]
+ main.process(arguments.toArray)
+ }
+ }
+ /** Interface to the compiler that uses the dependency analysis plugin.*/
+ object analysis
+ {
+ /** The compiled plugin jar. This will be passed to scalac as a compiler plugin.*/
+ private lazy val analyzerJar =
+ componentCompiler("analyzerPlugin").toList match
+ {
+ case x :: Nil => x
+ case Nil => error("Analyzer plugin component not found")
+ case xs => error("Analyzer plugin component must be a single jar (was: " + xs.mkString(", ") + ")")
+ }
+ /** The compiled interface jar. This is used to configure and call the compiler. It redirects logging and sets up
+ * the dependency analysis plugin.*/
+ private lazy val interfaceJars = componentCompiler("compilerInterface")
+ def apply(arguments: Seq[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger)
+ {
+ val argsWithPlugin = ("-Xplugin:" + analyzerJar.getAbsolutePath) :: arguments.toList
+ val interfaceLoader = new URLClassLoader(interfaceJars.toSeq.map(_.toURI.toURL).toArray, scalaLoader)
+ val interface = Class.forName("xsbt.CompilerInterface", true, interfaceLoader).newInstance
+ val runnable = interface.asInstanceOf[{ def run(args: Array[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger): Unit }]
+ runnable.run(argsWithPlugin.toArray, callback, maximumErrors, log) // safe to pass across the ClassLoader boundary because the types are defined in Java
+ }
+ def forceInitialization() {interfaceJars; analyzerJar}
+ }
+}
+class ComponentCompiler(compiler: Compiler)
+{
+ import compiler.{manager, scalaVersion}
+
+ val xsbtiID = "xsbti"
+ lazy val xsbtiJars =
+ {
+ val js = manager.jars(xsbtiID)
+ if(js.isEmpty)
+ error("Could not find required xsbti component")
+ else
+ js
+ }
+
+ import FileUtilities.{copy, createDirectory, zip, jars, unzip, withTemporaryDirectory}
+ def apply(id: String): Iterable[File] =
+ {
+ val binID = id + "-bin_" + scalaVersion
+ val binaryDirectory = manager.directory(binID)
+ if(binaryDirectory.isDirectory)
+ jars(binaryDirectory)
+ else
+ {
+ createDirectory(binaryDirectory)
+ val srcID = id + "-src"
+ val srcDirectory = manager.directory(srcID)
+ if(srcDirectory.isDirectory)
+ {
+ val targetJar = new File(binaryDirectory, id + ".jar")
+ compileSources(srcDirectory, compiler, targetJar, id)
+ Seq(targetJar)
+ }
+ else
+ notFound(id)
+ }
+ }
+ private def notFound(id: String) = error("Couldn't find xsbt source component " + id + " for Scala " + scalaVersion)
+ private def compileSources(srcDirectory: File, compiler: Compiler, targetJar: File, id: String)
+ {
+ val sources = jars(srcDirectory)
+ if(sources.isEmpty)
+ notFound(id)
+ else
+ {
+ withTemporaryDirectory { dir =>
+ val extractedSources = (Set[File]() /: sources) { (extracted, sourceJar)=> extracted ++ unzip(sourceJar, dir) }
+ val (sourceFiles, resources) = extractedSources.partition(_.getName.endsWith(".scala"))
+ withTemporaryDirectory { outputDirectory =>
+ val arguments = Seq("-d", outputDirectory.getAbsolutePath, "-cp", xsbtiJars.mkString(File.pathSeparator)) ++ sourceFiles.toSeq.map(_.getAbsolutePath)
+ compiler.raw(arguments)
+ copy(resources, outputDirectory, PathMapper.relativeTo(dir))
+ zip(Seq(outputDirectory), targetJar, true, PathMapper.relativeTo(outputDirectory))
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compile/interface/CompileLogger.scala b/compile/interface/CompileLogger.scala
new file mode 100644
index 000000000..caaead3fe
--- /dev/null
+++ b/compile/interface/CompileLogger.scala
@@ -0,0 +1,109 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2008, 2009 Mark Harrah
+ */
+package xsbt
+
+import xsbti.{F0,Logger}
+
+// The following code is based on scala.tools.nsc.reporters.{AbstractReporter, ConsoleReporter}
+// Copyright 2002-2009 LAMP/EPFL
+// Original author: Martin Odersky
+private final class LoggerReporter(maximumErrors: Int, log: Logger) extends scala.tools.nsc.reporters.Reporter
+{
+ import scala.tools.nsc.util.{FakePos,NoPosition,Position}
+ private val positions = new scala.collection.mutable.HashMap[Position, Severity]
+
+ def error(msg: String) { error(FakePos("scalac"), msg) }
+
+ def printSummary()
+ {
+ if(WARNING.count > 0)
+ log.warn(Message(countElementsAsString(WARNING.count, "warning") + " found"))
+ if(ERROR.count > 0)
+ log.error(Message(countElementsAsString(ERROR.count, "error") + " found"))
+ }
+
+ def display(pos: Position, msg: String, severity: Severity)
+ {
+ severity.count += 1
+ if(severity != ERROR || maximumErrors < 0 || severity.count <= maximumErrors)
+ print(severityLogger(severity), pos, msg)
+ }
+ private def severityLogger(severity: Severity) =
+ (m: F0[String]) =>
+ {
+ (severity match
+ {
+ case ERROR => log.error(m)
+ case WARNING => log.warn(m)
+ case INFO => log.info(m)
+ })
+ }
+
+ private def print(logger: F0[String] => Unit, posIn: Position, msg: String)
+ {
+ def log(s: => String) = logger(Message(s))
+ // the implicits keep source compatibility with the changes in 2.8 : Position.{source,line,column} are no longer Options
+ implicit def anyToOption[T <: AnyRef](t: T): Option[T] = Some(t)
+ implicit def intToOption(t: Int): Option[Int] = Some(t)
+ val pos =
+ posIn match
+ {
+ case null | NoPosition => NoPosition
+ case x: FakePos => x
+ case x =>
+ posIn.inUltimateSource(posIn.source.get)
+ }
+ pos match
+ {
+ case NoPosition => log(msg)
+ case FakePos(fmsg) => log(fmsg+" "+msg)
+ case _ =>
+ val sourcePrefix = pos.source.map(_.file.path).getOrElse("")
+ val lineNumberString = pos.line.map(line => ":" + line + ":").getOrElse(":") + " "
+ log(sourcePrefix + lineNumberString + msg)
+ if (!pos.line.isEmpty)
+ {
+ val lineContent = pos.lineContent.stripLineEnd
+ log(lineContent) // source line with error/warning
+ for(offset <- pos.offset; src <- pos.source)
+ {
+ val pointer = offset - src.lineToOffset(src.offsetToLine(offset))
+ val pointerSpace = lineContent.take(pointer).map { case '\t' => '\t'; case x => ' ' }
+ log(pointerSpace.mkString + "^") // pointer to the column position of the error/warning
+ }
+ }
+ }
+ }
+ override def reset =
+ {
+ super.reset
+ positions.clear
+ }
+
+ protected def info0(pos: Position, msg: String, severity: Severity, force: Boolean)
+ {
+ severity match
+ {
+ case WARNING | ERROR =>
+ {
+ if(!testAndLog(pos, severity))
+ display(pos, msg, severity)
+ }
+ case _ => display(pos, msg, severity)
+ }
+ }
+
+ private def testAndLog(pos: Position, severity: Severity): Boolean =
+ {
+ if(pos == null || pos.offset.isEmpty)
+ false
+ else if(positions.get(pos).map(_ >= severity).getOrElse(false))
+ true
+ else
+ {
+ positions(pos) = severity
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/compile/interface/CompilerInterface.scala b/compile/interface/CompilerInterface.scala
new file mode 100644
index 000000000..2644f30c0
--- /dev/null
+++ b/compile/interface/CompilerInterface.scala
@@ -0,0 +1,30 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2008, 2009 Mark Harrah
+ */
+package xsbt
+
+import xsbti.{AnalysisCallback,AnalysisCallbackContainer,Logger}
+
+class CompilerInterface
+{
+ def run(args: Array[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger)
+ {
+ import scala.tools.nsc.{CompilerCommand, FatalError, Global, Settings, reporters, util}
+ import util.FakePos
+ val reporter = new LoggerReporter(maximumErrors, log)
+ val settings = new Settings(reporter.error)
+ val command = new CompilerCommand(args.toList, settings, error, false)
+
+ object compiler extends Global(command.settings, reporter) with AnalysisCallbackContainer
+ {
+ def analysisCallback = callback
+ }
+ if(!reporter.hasErrors)
+ {
+ val run = new compiler.Run
+ run compile command.files
+ reporter.printSummary()
+ }
+ !reporter.hasErrors
+ }
+}
\ No newline at end of file
diff --git a/compile/interface/Message.scala b/compile/interface/Message.scala
new file mode 100644
index 000000000..f83cb2094
--- /dev/null
+++ b/compile/interface/Message.scala
@@ -0,0 +1,11 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2008, 2009 Mark Harrah
+ */
+package xsbt
+
+import xsbti.F0
+
+object Message
+{
+ def apply(s: => String) = new F0[String] { def apply() = s }
+}
\ No newline at end of file
diff --git a/compile/plugin/Analyzer.scala b/compile/plugin/Analyzer.scala
new file mode 100644
index 000000000..b9ffb12bc
--- /dev/null
+++ b/compile/plugin/Analyzer.scala
@@ -0,0 +1,196 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2008, 2009 Mark Harrah
+ */
+package xsbt
+
+import scala.tools.nsc.{io, plugins, symtab, Global, Phase}
+import io.{AbstractFile, PlainFile, ZipArchive}
+import plugins.{Plugin, PluginComponent}
+import symtab.Flags
+import scala.collection.mutable.{HashMap, HashSet, Map, Set}
+
+import java.io.File
+import xsbti.{AnalysisCallback, AnalysisCallbackContainer}
+
+class Analyzer(val global: Global) extends Plugin
+{
+ val callback = global.asInstanceOf[AnalysisCallbackContainer].analysisCallback
+
+ import global._
+
+ val name = "sbt-analyzer"
+ val description = "A plugin to find all concrete instances of a given class and extract dependency information."
+ val components = List[PluginComponent](Component)
+
+ /* ================================================== */
+ // These two templates abuse scope for source compatibility between Scala 2.7.x and 2.8.x so that a single
+ // sbt codebase compiles with both series of versions.
+ // In 2.8.x, PluginComponent.runsAfter has type List[String] and the method runsBefore is defined on
+ // PluginComponent with default value Nil.
+ // In 2.7.x, runsBefore does not exist on PluginComponent and PluginComponent.runsAfter has type String.
+ //
+ // Therefore, in 2.8.x, object runsBefore is shadowed by PluginComponent.runsBefore (which is Nil) and so
+ // afterPhase :: runsBefore
+ // is equivalent to List[String](afterPhase)
+ // In 2.7.x, object runsBefore is not shadowed and so runsAfter has type String.
+ private object runsBefore { def :: (s: String) = s }
+ private abstract class CompatiblePluginComponent(afterPhase: String) extends PluginComponent
+ {
+ override val runsAfter = afterPhase :: runsBefore
+ }
+ /* ================================================== */
+
+ private object Component extends CompatiblePluginComponent("jvm")
+ {
+ val global = Analyzer.this.global
+ val phaseName = Analyzer.this.name
+ def newPhase(prev: Phase) = new AnalyzerPhase(prev)
+ }
+
+ private class AnalyzerPhase(prev: Phase) extends Phase(prev)
+ {
+ def name = Analyzer.this.name
+ def run
+ {
+ val outputDirectory = new File(global.settings.outdir.value)
+ val superclassNames = callback.superclassNames.map(newTermName)
+ val superclassesAll =
+ for(name <- superclassNames) yield
+ {
+ try { Some(global.definitions.getClass(name)) }
+ catch { case fe: scala.tools.nsc.FatalError => callback.superclassNotFound(name.toString); None }
+ }
+ val superclasses = superclassesAll.filter(_.isDefined).map(_.get)
+
+ for(unit <- currentRun.units)
+ {
+ // build dependencies structure
+ val sourceFile = unit.source.file.file
+ callback.beginSource(sourceFile)
+ for(on <- unit.depends)
+ {
+ val onSource = on.sourceFile
+ if(onSource == null)
+ {
+ classFile(on) match
+ {
+ case Some(f) =>
+ {
+ f match
+ {
+ case ze: ZipArchive#Entry => callback.jarDependency(new File(ze.getArchive.getName), sourceFile)
+ case pf: PlainFile => callback.classDependency(pf.file, sourceFile)
+ case _ => ()
+ }
+ }
+ case None => ()
+ }
+ }
+ else
+ callback.sourceDependency(onSource.file, sourceFile)
+ }
+
+ // find subclasses and modules with main methods
+ for(clazz @ ClassDef(mods, n, _, _) <- unit.body)
+ {
+ val sym = clazz.symbol
+ if(sym != NoSymbol && mods.isPublic && !mods.isAbstract && !mods.isTrait &&
+ !sym.isImplClass && sym.isStatic && !sym.isNestedClass)
+ {
+ val isModule = sym.isModuleClass
+ for(superclass <- superclasses.filter(sym.isSubClass))
+ callback.foundSubclass(sourceFile, sym.fullNameString, superclass.fullNameString, isModule)
+ if(isModule && hasMainMethod(sym))
+ callback.foundApplication(sourceFile, sym.fullNameString)
+ }
+ }
+
+ // build list of generated classes
+ for(iclass <- unit.icode)
+ {
+ val sym = iclass.symbol
+ def addGenerated(separatorRequired: Boolean)
+ {
+ val classFile = fileForClass(outputDirectory, sym, separatorRequired)
+ if(classFile.exists)
+ callback.generatedClass(sourceFile, classFile)
+ }
+ if(sym.isModuleClass && !sym.isImplClass)
+ {
+ if(isTopLevelModule(sym) && sym.linkedClassOfModule == NoSymbol)
+ addGenerated(false)
+ addGenerated(true)
+ }
+ else
+ addGenerated(false)
+ }
+ callback.endSource(sourceFile)
+ }
+ }
+ }
+
+ private def classFile(sym: Symbol): Option[AbstractFile] =
+ {
+ import scala.tools.nsc.symtab.Flags
+ val name = sym.fullNameString(java.io.File.separatorChar) + (if (sym.hasFlag(Flags.MODULE)) "$" else "")
+ val entry = classPath.root.find(name, false)
+ if (entry ne null)
+ Some(entry.classFile)
+ else if(isTopLevelModule(sym))
+ {
+ val linked = sym.linkedClassOfModule
+ if(linked == NoSymbol)
+ None
+ else
+ classFile(linked)
+ }
+ else
+ None
+ }
+
+ private def isTopLevelModule(sym: Symbol): Boolean =
+ atPhase (currentRun.picklerPhase.next) {
+ sym.isModuleClass && !sym.isImplClass && !sym.isNestedClass
+ }
+ private def fileForClass(outputDirectory: File, s: Symbol, separatorRequired: Boolean): File =
+ fileForClass(outputDirectory, s, separatorRequired, ".class")
+ private def fileForClass(outputDirectory: File, s: Symbol, separatorRequired: Boolean, postfix: String): File =
+ {
+ if(s.owner.isPackageClass && s.isPackageClass)
+ new File(packageFile(outputDirectory, s), postfix)
+ else
+ fileForClass(outputDirectory, s.owner.enclClass, true, s.simpleName + (if(separatorRequired) "$" else "") + postfix)
+ }
+ private def packageFile(outputDirectory: File, s: Symbol): File =
+ {
+ if(s.isEmptyPackageClass || s.isRoot)
+ outputDirectory
+ else
+ new File(packageFile(outputDirectory, s.owner.enclClass), s.simpleName.toString)
+ }
+
+ private def hasMainMethod(sym: Symbol): Boolean =
+ {
+ val main = sym.info.nonPrivateMember(newTermName("main"))//nme.main)
+ main.tpe match
+ {
+ case OverloadedType(pre, alternatives) => alternatives.exists(alt => isVisible(alt) && isMainType(pre.memberType(alt)))
+ case tpe => isVisible(main) && isMainType(main.owner.thisType.memberType(main))
+ }
+ }
+ private def isVisible(sym: Symbol) = sym != NoSymbol && sym.isPublic && !sym.isDeferred
+ private def isMainType(tpe: Type) =
+ {
+ tpe match
+ {
+ // singleArgument is of type Symbol in 2.8.0 and type Type in 2.7.x
+ case MethodType(List(singleArgument), result) => isUnitType(result) && isStringArray(singleArgument)
+ case _ => false
+ }
+ }
+ private lazy val StringArrayType = appliedType(definitions.ArrayClass.typeConstructor, definitions.StringClass.tpe :: Nil)
+ // isStringArray is overloaded to handle the incompatibility between 2.7.x and 2.8.0
+ private def isStringArray(tpe: Type): Boolean = tpe.typeSymbol == StringArrayType.typeSymbol
+ private def isStringArray(sym: Symbol): Boolean = isStringArray(sym.tpe)
+ private def isUnitType(tpe: Type) = tpe.typeSymbol == definitions.UnitClass
+}
\ No newline at end of file
diff --git a/interface/src/main/java/xsbti/AnalysisCallback.java b/interface/src/main/java/xsbti/AnalysisCallback.java
new file mode 100644
index 000000000..2ceedd4a6
--- /dev/null
+++ b/interface/src/main/java/xsbti/AnalysisCallback.java
@@ -0,0 +1,35 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2008, 2009 Mark Harrah
+ */
+package xsbti;
+
+import java.io.File;
+
+public interface AnalysisCallback
+{
+ /** The names of classes that the analyzer should find subclasses of.*/
+ public String[] superclassNames();
+ /** Called when the the given superclass could not be found on the classpath by the compiler.*/
+ public void superclassNotFound(String superclassName);
+ /** Called before the source at the given location is processed. */
+ public void beginSource(File source);
+ /** Called when the a subclass of one of the classes given in superclassNames is
+ * discovered.*/
+ public void foundSubclass(File source, String subclassName, String superclassName, boolean isModule);
+ /** Called to indicate that the source file source depends on the source file
+ * dependsOn.*/
+ public void sourceDependency(File dependsOn, File source);
+ /** Called to indicate that the source file source depends on the jar
+ * jar.*/
+ public void jarDependency(File jar, File source);
+ /** Called to indicate that the source file source depends on the class file
+ * clazz.*/
+ public void classDependency(File clazz, File source);
+ /** Called to indicate that the source file source produces a class file at
+ * module.*/
+ public void generatedClass(File source, File module);
+ /** Called after the source at the given location has been processed. */
+ public void endSource(File sourcePath);
+ /** Called when a module with a public 'main' method with the right signature is found.*/
+ public void foundApplication(File source, String className);
+}
\ No newline at end of file
diff --git a/interface/src/main/java/xsbti/AnalysisCallbackContainer.java b/interface/src/main/java/xsbti/AnalysisCallbackContainer.java
new file mode 100644
index 000000000..3d0641ed7
--- /dev/null
+++ b/interface/src/main/java/xsbti/AnalysisCallbackContainer.java
@@ -0,0 +1,12 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2009 Mark Harrah
+ */
+package xsbti;
+
+/** Provides access to an AnalysisCallback. This is used by the plugin to
+* get the callback to use. The scalac Global instance it is passed must
+* implement this interface. */
+public interface AnalysisCallbackContainer
+{
+ public AnalysisCallback analysisCallback();
+}
\ No newline at end of file
diff --git a/interface/src/main/java/xsbti/F0.java b/interface/src/main/java/xsbti/F0.java
new file mode 100644
index 000000000..90e713b6b
--- /dev/null
+++ b/interface/src/main/java/xsbti/F0.java
@@ -0,0 +1,9 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2009 Mark Harrah
+ */
+package xsbti;
+
+public interface F0
+{
+ public T apply();
+}
diff --git a/interface/src/main/java/xsbti/Logger.java b/interface/src/main/java/xsbti/Logger.java
new file mode 100644
index 000000000..3b676650d
--- /dev/null
+++ b/interface/src/main/java/xsbti/Logger.java
@@ -0,0 +1,13 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2009 Mark Harrah
+ */
+package xsbti;
+
+public interface Logger
+{
+ public void error(F0 msg);
+ public void warn(F0 msg);
+ public void info(F0 msg);
+ public void debug(F0 msg);
+ public void trace(F0 exception);
+}
diff --git a/notes b/notes
index 730c87dcf..667d46c6d 100644
--- a/notes
+++ b/notes
@@ -30,3 +30,15 @@ Dependency Management
- drop explicit managers
- resolvers are completely defined in project definition (use Resolver.withDefaultResolvers)
- configurations completely defined within project (use ModuleConfiguration.configurations)
+
+
+TODO:
+compiler analysis callback does not check classes against output directory. This must now be done in callback itself:
+
+ Path.relativize(outputPath, pf.file) match
+ {
+ case None => // dependency is a class file outside of the output directory
+ callback.classDependency(pf.file, sourcePath)
+ case Some(relativeToOutput) => // dependency is a product of a source not included in this compilation
+ callback.productDependency(relativeToOutput, sourcePath)
+ }
\ No newline at end of file
diff --git a/project/build/XSbt.scala b/project/build/XSbt.scala
index 662a250f4..647b27ecf 100644
--- a/project/build/XSbt.scala
+++ b/project/build/XSbt.scala
@@ -3,23 +3,33 @@ import sbt._
class XSbt(info: ProjectInfo) extends ParentProject(info)
{
def utilPath = path("util")
+ def compilePath = path("compile")
+
+ val commonDeps = project("common", "Dependencies", new CommonDependencies(_))
+ val interfaceSub = project("interface", "Interface", new InterfaceProject(_))
val controlSub = project(utilPath / "control", "Control", new Base(_))
val collectionSub = project(utilPath / "collection", "Collections", new Base(_))
- val ioSub = project(utilPath / "io", "IO", new Base(_),controlSub)
+ val ioSub = project(utilPath / "io", "IO", new Base(_), controlSub, commonDeps)
val classpathSub = project(utilPath / "classpath", "Classpath", new Base(_))
+ val analysisPluginSub = project(compilePath / "plugin", "Analyzer Compiler Plugin", new Base(_), interfaceSub)
+ val compilerInterfaceSub = project(compilePath / "interface", "Compiler Interface", new Base(_), interfaceSub)
+
val ivySub = project("ivy", "Ivy", new IvyProject(_))
val logSub = project(utilPath / "log", "Logging", new Base(_))
- val taskSub = project("tasks", "Tasks", new TaskProject(_), controlSub, collectionSub)
+ val taskSub = project("tasks", "Tasks", new TaskProject(_), controlSub, collectionSub, commonDeps)
val cacheSub = project("cache", "Cache", new CacheProject(_), taskSub, ioSub)
+ val compilerSub = project(compilePath, "Compile", new Base(_), interfaceSub, ivySub, analysisPluginSub, ioSub, compilerInterfaceSub)
- override def parallelExecution = true
- class TaskProject(info: ProjectInfo) extends Base(info)
+ class CommonDependencies(info: ProjectInfo) extends ParentProject(info)
{
val sc = "org.scala-tools.testing" % "scalacheck" % "1.5" % "test->default"
}
+
+ override def parallelExecution = true
+ class TaskProject(info: ProjectInfo) extends Base(info)
class CacheProject(info: ProjectInfo) extends Base(info)
{
//override def compileOptions = super.compileOptions ++ List(Unchecked,ExplainTypes, CompileOption("-Xlog-implicits"))
@@ -32,4 +42,9 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
{
val ivy = "org.apache.ivy" % "ivy" % "2.0.0"
}
+ class InterfaceProject(info: ProjectInfo) extends DefaultProject(info)
+ {
+ override def sourceExtensions: NameFilter = "*.java"
+ override def compileOrder = CompileOrder.JavaThenScala
+ }
}
\ No newline at end of file
diff --git a/util/io/FileUtilities.scala b/util/io/FileUtilities.scala
new file mode 100644
index 000000000..fad8d374a
--- /dev/null
+++ b/util/io/FileUtilities.scala
@@ -0,0 +1,297 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2008, 2009 Mark Harrah
+ */
+package xsbt
+
+import OpenResource._
+import ErrorHandling.translate
+
+import java.io.{File, FileInputStream, InputStream, OutputStream}
+import java.util.jar.{Attributes, JarEntry, JarFile, JarInputStream, JarOutputStream, Manifest}
+import java.util.zip.{GZIPOutputStream, ZipEntry, ZipFile, ZipInputStream, ZipOutputStream}
+import scala.collection.mutable.HashSet
+
+object FileUtilities
+{
+ /** The maximum number of times a unique temporary filename is attempted to be created.*/
+ private val MaximumTries = 10
+ /** The producer of randomness for unique name generation.*/
+ private lazy val random = new java.util.Random
+ val temporaryDirectory = new File(System.getProperty("java.io.tmpdir"))
+ /** The size of the byte or char buffer used in various methods.*/
+ private val BufferSize = 8192
+ private val Newline = System.getProperty("line.separator")
+
+ def createDirectory(dir: File): Unit =
+ translate("Could not create directory " + dir + ": ")
+ {
+ if(dir.exists)
+ {
+ if(!dir.isDirectory)
+ error("file exists and is not a directory.")
+ }
+ else if(!dir.mkdirs())
+ error("")
+ }
+ def unzip(from: File, toDirectory: File): Set[File] = unzip(from, toDirectory, AllPassFilter)
+ def unzip(from: File, toDirectory: File, filter: NameFilter): Set[File] = fileInputStream(from)(in => unzip(in, toDirectory, filter))
+ def unzip(from: InputStream, toDirectory: File, filter: NameFilter): Set[File] =
+ {
+ createDirectory(toDirectory)
+ zipInputStream(from) { zipInput => extract(zipInput, toDirectory, filter) }
+ }
+ private def extract(from: ZipInputStream, toDirectory: File, filter: NameFilter) =
+ {
+ val set = new HashSet[File]
+ def next()
+ {
+ val entry = from.getNextEntry
+ if(entry == null)
+ ()
+ else
+ {
+ val name = entry.getName
+ if(filter.accept(name))
+ {
+ val target = new File(toDirectory, name)
+ //log.debug("Extracting zip entry '" + name + "' to '" + target + "'")
+ if(entry.isDirectory)
+ createDirectory(target)
+ else
+ {
+ set += target
+ translate("Error extracting zip entry '" + name + "' to '" + target + "': ") {
+ fileOutputStream(false)(target) { out => transfer(from, out) }
+ }
+ }
+ //target.setLastModified(entry.getTime)
+ }
+ else
+ {
+ //log.debug("Ignoring zip entry '" + name + "'")
+ }
+ from.closeEntry()
+ next()
+ }
+ }
+ next()
+ Set() ++ set
+ }
+
+ /** Copies all bytes from the given input stream to the given output stream.
+ * Neither stream is closed.*/
+ def transfer(in: InputStream, out: OutputStream): Unit = transferImpl(in, out, false)
+ /** Copies all bytes from the given input stream to the given output stream. The
+ * input stream is closed after the method completes.*/
+ def transferAndClose(in: InputStream, out: OutputStream): Unit = transferImpl(in, out, true)
+ private def transferImpl(in: InputStream, out: OutputStream, close: Boolean)
+ {
+ try
+ {
+ val buffer = new Array[Byte](BufferSize)
+ def read()
+ {
+ val byteCount = in.read(buffer)
+ if(byteCount >= 0)
+ {
+ out.write(buffer, 0, byteCount)
+ read()
+ }
+ }
+ read()
+ }
+ finally { if(close) in.close }
+ }
+
+ /** Creates a temporary directory and provides its location to the given function. The directory
+ * is deleted after the function returns.*/
+ def withTemporaryDirectory[T](action: File => T): T =
+ {
+ val dir = createTemporaryDirectory
+ try { action(dir) }
+ finally { delete(dir) }
+ }
+ def createTemporaryDirectory: File =
+ {
+ def create(tries: Int): File =
+ {
+ if(tries > MaximumTries)
+ error("Could not create temporary directory.")
+ else
+ {
+ val randomName = "sbt_" + java.lang.Integer.toHexString(random.nextInt)
+ val f = new File(temporaryDirectory, randomName)
+
+ try { createDirectory(f); f }
+ catch { case e: Exception => create(tries + 1) }
+ }
+ }
+ create(0)
+ }
+
+ private[xsbt] def jars(dir: File): Iterable[File] = wrapNull(dir.listFiles(GlobFilter("*.jar")))
+
+ def delete(files: Iterable[File]): Unit = files.foreach(delete)
+ def delete(file: File)
+ {
+ translate("Error deleting file " + file + ": ")
+ {
+ if(file.isDirectory)
+ {
+ delete(wrapNull(file.listFiles))
+ file.delete
+ }
+ else if(file.exists)
+ file.delete
+ }
+ }
+ private def wrapNull(a: Array[File]): Array[File] =
+ if(a == null)
+ new Array[File](0)
+ else
+ a
+
+
+ /** Creates a jar file.
+ * @param sources The files to include in the jar file.
+ * @param outputJar The file to write the jar to.
+ * @param manifest The manifest for the jar.
+ * @param recursive If true, any directories in sources are recursively processed.
+ * @param mapper The mapper that determines the name of a File in the jar. */
+ def jar(sources: Iterable[File], outputJar: File, manifest: Manifest, recursive: Boolean, mapper: PathMapper): Unit =
+ archive(sources, outputJar, Some(manifest), recursive, mapper)
+ /** Creates a zip file.
+ * @param sources The files to include in the jar file.
+ * @param outputZip The file to write the zip to.
+ * @param recursive If true, any directories in sources are recursively processed. Otherwise,
+ * they are not
+ * @param mapper The mapper that determines the name of a File in the jar. */
+ def zip(sources: Iterable[File], outputZip: File, recursive: Boolean, mapper: PathMapper): Unit =
+ archive(sources, outputZip, None, recursive, mapper)
+
+ private def archive(sources: Iterable[File], outputFile: File, manifest: Option[Manifest], recursive: Boolean, mapper: PathMapper)
+ {
+ if(outputFile.isDirectory)
+ error("Specified output file " + outputFile + " is a directory.")
+ else
+ {
+ val outputDir = outputFile.getParentFile
+ createDirectory(outputDir)
+ withZipOutput(outputFile, manifest)
+ { output =>
+ val createEntry: (String => ZipEntry) = if(manifest.isDefined) new JarEntry(_) else new ZipEntry(_)
+ writeZip(sources, output, recursive, mapper)(createEntry)
+ }
+ }
+ }
+ private def writeZip(sources: Iterable[File], output: ZipOutputStream, recursive: Boolean, mapper: PathMapper)(createEntry: String => ZipEntry)
+ {
+ def add(sourceFile: File)
+ {
+ if(sourceFile.isDirectory)
+ {
+ if(recursive)
+ wrapNull(sourceFile.listFiles).foreach(add)
+ }
+ else if(sourceFile.exists)
+ {
+ val name = mapper(sourceFile)
+ val nextEntry = createEntry(name)
+ nextEntry.setTime(sourceFile.lastModified)
+ output.putNextEntry(nextEntry)
+ transferAndClose(new FileInputStream(sourceFile), output)
+ }
+ else
+ error("Source " + sourceFile + " does not exist.")
+ }
+ sources.foreach(add)
+ output.closeEntry()
+ }
+
+ private def withZipOutput(file: File, manifest: Option[Manifest])(f: ZipOutputStream => Unit)
+ {
+ fileOutputStream(false)(file) { fileOut =>
+ val (zipOut, ext) =
+ manifest match
+ {
+ case Some(mf) =>
+ {
+ import Attributes.Name.MANIFEST_VERSION
+ val main = mf.getMainAttributes
+ if(!main.containsKey(MANIFEST_VERSION))
+ main.put(MANIFEST_VERSION, "1.0")
+ (new JarOutputStream(fileOut, mf), "jar")
+ }
+ case None => (new ZipOutputStream(fileOut), "zip")
+ }
+ try { f(zipOut) }
+ catch { case e: Exception => "Error writing " + ext + ": " + e.toString }
+ finally { zipOut.close }
+ }
+ }
+ def relativize(base: File, file: File): Option[String] =
+ {
+ val pathString = file.getAbsolutePath
+ baseFileString(base) flatMap
+ {
+ baseString =>
+ {
+ if(pathString.startsWith(baseString))
+ Some(pathString.substring(baseString.length))
+ else
+ None
+ }
+ }
+ }
+ private def baseFileString(baseFile: File): Option[String] =
+ {
+ if(baseFile.isDirectory)
+ {
+ val cp = baseFile.getAbsolutePath
+ assert(cp.length > 0)
+ if(cp.charAt(cp.length - 1) == File.separatorChar)
+ Some(cp)
+ else
+ Some(cp + File.separatorChar)
+ }
+ else
+ None
+ }
+ def copy(sources: Iterable[File], destinationDirectory: File, mapper: PathMapper) =
+ {
+ val targetSet = new scala.collection.mutable.HashSet[File]
+ copyImpl(sources, destinationDirectory) { from =>
+ val to = new File(destinationDirectory, mapper(from))
+ targetSet += to
+ if(!to.exists || from.lastModified > to.lastModified)
+ {
+ if(from.isDirectory)
+ createDirectory(to)
+ else
+ {
+ //log.debug("Copying " + source + " to " + toPath)
+ copyFile(from, to)
+ }
+ }
+ }
+ targetSet.readOnly
+ }
+ private def copyImpl(sources: Iterable[File], target: File)(doCopy: File => Unit)
+ {
+ if(!target.isDirectory)
+ createDirectory(target)
+ sources.toList.foreach(doCopy)
+ }
+ def copyFile(sourceFile: File, targetFile: File)
+ {
+ require(sourceFile.exists, "Source file '" + sourceFile.getAbsolutePath + "' does not exist.")
+ require(!sourceFile.isDirectory, "Source file '" + sourceFile.getAbsolutePath + "' is a directory.")
+ fileInputChannel(sourceFile) { in =>
+ fileOutputChannel(targetFile) { out =>
+ val copied = out.transferFrom(in, 0, in.size)
+ if(copied != in.size)
+ error("Could not copy '" + sourceFile + "' to '" + targetFile + "' (" + copied + "/" + in.size + " bytes copied)")
+ }
+ }
+ }
+}
diff --git a/util/io/NameFilter.scala b/util/io/NameFilter.scala
new file mode 100644
index 000000000..274565aa4
--- /dev/null
+++ b/util/io/NameFilter.scala
@@ -0,0 +1,72 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2008, 2009 Mark Harrah
+ */
+package xsbt
+
+import java.io.File
+import java.util.regex.Pattern
+
+trait FileFilter extends java.io.FileFilter with NotNull
+{
+ def || (filter: FileFilter): FileFilter = new SimpleFileFilter( file => accept(file) || filter.accept(file) )
+ def && (filter: FileFilter): FileFilter = new SimpleFileFilter( file => accept(file) && filter.accept(file) )
+ def -- (filter: FileFilter): FileFilter = new SimpleFileFilter( file => accept(file) && !filter.accept(file) )
+ def unary_- : FileFilter = new SimpleFileFilter( file => !accept(file) )
+}
+trait NameFilter extends FileFilter with NotNull
+{
+ def accept(name: String): Boolean
+ final def accept(file: File): Boolean = accept(file.getName)
+ def | (filter: NameFilter): NameFilter = new SimpleFilter( name => accept(name) || filter.accept(name) )
+ def & (filter: NameFilter): NameFilter = new SimpleFilter( name => accept(name) && filter.accept(name) )
+ def - (filter: NameFilter): NameFilter = new SimpleFilter( name => accept(name) && !filter.accept(name) )
+ override def unary_- : NameFilter = new SimpleFilter( name => !accept(name) )
+}
+object HiddenFileFilter extends FileFilter {
+ def accept(file: File) = file.isHidden && file.getName != "."
+}
+object ExistsFileFilter extends FileFilter {
+ def accept(file: File) = file.exists
+}
+object DirectoryFilter extends FileFilter {
+ def accept(file: File) = file.isDirectory
+}
+class SimpleFileFilter(val acceptFunction: File => Boolean) extends FileFilter
+{
+ def accept(file: File) = acceptFunction(file)
+}
+class ExactFilter(val matchName: String) extends NameFilter
+{
+ def accept(name: String) = matchName == name
+}
+class SimpleFilter(val acceptFunction: String => Boolean) extends NameFilter
+{
+ def accept(name: String) = acceptFunction(name)
+}
+class PatternFilter(val pattern: Pattern) extends NameFilter
+{
+ def accept(name: String) = pattern.matcher(name).matches
+}
+object AllPassFilter extends NameFilter
+{
+ def accept(name: String) = true
+}
+object NothingFilter extends NameFilter
+{
+ def accept(name: String) = false
+}
+
+object GlobFilter
+{
+ def apply(expression: String): NameFilter =
+ {
+ require(!expression.exists(java.lang.Character.isISOControl), "Control characters not allowed in filter expression.")
+ if(expression == "*")
+ AllPassFilter
+ else if(expression.indexOf('*') < 0) // includes case where expression is empty
+ new ExactFilter(expression)
+ else
+ new PatternFilter(Pattern.compile(expression.split("\\*", -1).map(quote).mkString(".*")))
+ }
+ private def quote(s: String) = if(s.isEmpty) "" else Pattern.quote(s.replaceAll("\n", """\n"""))
+}
\ No newline at end of file
diff --git a/util/io/OpenResource.scala b/util/io/OpenResource.scala
index ea0be13ce..de3207b85 100644
--- a/util/io/OpenResource.scala
+++ b/util/io/OpenResource.scala
@@ -16,21 +16,6 @@ import java.util.zip.{GZIPOutputStream, ZipEntry, ZipFile, ZipInputStream, ZipOu
import ErrorHandling.translate
import OpenResource._
-object FileUtilities
-{
- def createDirectory(dir: File): Unit =
- translate("Could not create directory " + dir + ": ")
- {
- if(dir.exists)
- {
- if(!dir.isDirectory)
- error("file exists and is not a directory.")
- }
- else if(!dir.mkdirs())
- error("")
- }
-}
-
abstract class OpenResource[Source, T] extends NotNull
{
protected def open(src: Source): T
diff --git a/util/io/PathMapper.scala b/util/io/PathMapper.scala
new file mode 100644
index 000000000..4112701ff
--- /dev/null
+++ b/util/io/PathMapper.scala
@@ -0,0 +1,21 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2008, 2009 Mark Harrah
+ */
+package xsbt
+
+import java.io.File
+
+trait PathMapper extends NotNull
+{
+ def apply(file: File): String
+}
+
+object PathMapper
+{
+ val basic = new FMapper(_.getPath)
+ def relativeTo(base: File) = new FMapper(file => FileUtilities.relativize(base, file).getOrElse(file.getPath))
+}
+class FMapper(f: File => String) extends PathMapper
+{
+ def apply(file: File) = f(file)
+}
\ No newline at end of file
diff --git a/util/io/src/test/scala/NameFilterSpecification.scala b/util/io/src/test/scala/NameFilterSpecification.scala
new file mode 100644
index 000000000..515230f2c
--- /dev/null
+++ b/util/io/src/test/scala/NameFilterSpecification.scala
@@ -0,0 +1,39 @@
+/* sbt -- Simple Build Tool
+ * Copyright 2008 Mark Harrah */
+
+package xsbt
+
+import org.scalacheck._
+import Prop._
+
+object NameFilterSpecification extends Properties("NameFilter")
+{
+ specify("All pass accepts everything", (s: String) => AllPassFilter.accept(s))
+ specify("Exact filter matches provided string",
+ (s1: String, s2: String) => (new ExactFilter(s1)).accept(s2) == (s1 == s2) )
+ specify("Exact filter matches valid string", (s: String) => (new ExactFilter(s)).accept(s) )
+
+ specify("Glob filter matches provided string if no *s",
+ (s1: String, s2: String) =>
+ {
+ val stripped = stripAsterisksAndControl(s1)
+ (GlobFilter(stripped).accept(s2) == (stripped == s2))
+ })
+ specify("Glob filter matches valid string if no *s",
+ (s: String) =>
+ {
+ val stripped = stripAsterisksAndControl(s)
+ GlobFilter(stripped).accept(stripped)
+ })
+
+ specify("Glob filter matches valid",
+ (list: List[String]) =>
+ {
+ val stripped = list.map(stripAsterisksAndControl)
+ GlobFilter(stripped.mkString("*")).accept(stripped.mkString)
+ })
+
+ /** Raw control characters are stripped because they are not allowed in expressions.
+ * Asterisks are stripped because they are added under the control of the tests.*/
+ private def stripAsterisksAndControl(s: String) = s.filter(c => !java.lang.Character.isISOControl(c) && c != '*').toString
+}
\ No newline at end of file