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