Setting up compiler support and several related additions to util/io

* Added the top-level interface project for communicating across scala versions within a jvm.
 * Added plugin project containing analysis compiler plugin
 * Added component compiler to build xsbt components against required version of Scala on the fly
 * Added interface to compiler that runs in the same version of Scala
 * Added frontend that compiles against a given version of Scala with or without analysis.
This commit is contained in:
Mark Harrah 2009-08-17 10:51:43 -04:00
parent 56b047035a
commit 5644b936fe
17 changed files with 1014 additions and 20 deletions

View File

@ -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) }
}
}

137
compile/Compiler.scala Normal file
View File

@ -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))
}
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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 }
}

View File

@ -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
}

View File

@ -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 <code>superclassNames</code> is
* discovered.*/
public void foundSubclass(File source, String subclassName, String superclassName, boolean isModule);
/** Called to indicate that the source file <code>source</code> depends on the source file
* <code>dependsOn</code>.*/
public void sourceDependency(File dependsOn, File source);
/** Called to indicate that the source file <code>source</code> depends on the jar
* <code>jar</code>.*/
public void jarDependency(File jar, File source);
/** Called to indicate that the source file <code>source</code> depends on the class file
* <code>clazz</code>.*/
public void classDependency(File clazz, File source);
/** Called to indicate that the source file <code>source</code> produces a class file at
* <code>module</code>.*/
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);
}

View File

@ -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();
}

View File

@ -0,0 +1,9 @@
/* sbt -- Simple Build Tool
* Copyright 2009 Mark Harrah
*/
package xsbti;
public interface F0<T>
{
public T apply();
}

View File

@ -0,0 +1,13 @@
/* sbt -- Simple Build Tool
* Copyright 2009 Mark Harrah
*/
package xsbti;
public interface Logger
{
public void error(F0<String> msg);
public void warn(F0<String> msg);
public void info(F0<String> msg);
public void debug(F0<String> msg);
public void trace(F0<Throwable> exception);
}

12
notes
View File

@ -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)
}

View File

@ -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
}
}

297
util/io/FileUtilities.scala Normal file
View File

@ -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("<unknown 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 <code>sources</code> 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 <code>sources</code> 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)")
}
}
}
}

72
util/io/NameFilter.scala Normal file
View File

@ -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"""))
}

View File

@ -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("<unknown error>")
}
}
abstract class OpenResource[Source, T] extends NotNull
{
protected def open(src: Source): T

21
util/io/PathMapper.scala Normal file
View File

@ -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)
}

View File

@ -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
}