mirror of https://github.com/sbt/sbt.git
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:
parent
56b047035a
commit
5644b936fe
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2009 Mark Harrah
|
||||
*/
|
||||
package xsbti;
|
||||
|
||||
public interface F0<T>
|
||||
{
|
||||
public T apply();
|
||||
}
|
||||
|
|
@ -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
12
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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"""))
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue