* Many logging related changes and fixes. Added FilterLogger and cleaned up interaction between Logger,

scripted testing, and the builder projects.  This included removing the recordingDepth hack from Logger.  
Logger buffering is now enabled/disabled per thread.
 * Fix compileOptions being fixed after the first compile
 * Minor fixes to output directory checking
 * Added defaultLoggingLevel method for setting the initial level of a project's Logger
 * Cleaned up internal approach to adding extra default configurations like 'plugin'
 * Added syncPathsTask for synchronizing paths to a target directory
 * Allow multiple instances of Jetty (new jettyRunTasks can be defined with different ports)
 * jettyRunTask accepts configuration in a single configuration wrapper object instead of many parameters
 * Fix web application class loading (#35) by using jettyClasspath=testClasspath---jettyRunClasspath for 
loading Jetty.  A better way would be to have a 'jetty' configuration and have 
jettyClasspath=managedClasspath('jetty'), but this maintains compatibility.
 * Copy resources to target/resources and target/test-resources using copyResources and copyTestResources 
tasks.  Properly include all resources in web applications and classpaths (#36).  mainResources and 
testResources are now the definitive methods for getting resources.



git-svn-id: https://simple-build-tool.googlecode.com/svn/trunk@936 d89573ee-9141-11dd-94d4-bdf5e562f29c
This commit is contained in:
dmharrah 2009-08-02 01:26:18 +00:00
parent 22f2d5b993
commit cd8c8ea007
16 changed files with 308 additions and 299 deletions

View File

@ -59,9 +59,9 @@ trait AnalysisCallback extends NotNull
/** Called when a module with a public 'main' method with the right signature is found.*/
def foundApplication(sourcePath: Path, className: String): Unit
}
abstract class BasicAnalysisCallback[A <: BasicCompileAnalysis](val basePath: Path, val superclassNames: Iterable[String],
protected val analysis: A) extends AnalysisCallback
abstract class BasicAnalysisCallback[A <: BasicCompileAnalysis](val basePath: Path, protected val analysis: A) extends AnalysisCallback
{
def superclassNames: Iterable[String]
def superclassNotFound(superclassName: String) {}
def beginSource(sourcePath: Path): Unit =
@ -85,8 +85,8 @@ abstract class BasicAnalysisCallback[A <: BasicCompileAnalysis](val basePath: Pa
def endSource(sourcePath: Path): Unit =
analysis.removeSelfDependency(sourcePath)
}
abstract class BasicCompileAnalysisCallback(basePath: Path, superclassNames: Iterable[String], analysis: CompileAnalysis)
extends BasicAnalysisCallback(basePath, superclassNames, analysis)
abstract class BasicCompileAnalysisCallback(basePath: Path, analysis: CompileAnalysis)
extends BasicAnalysisCallback(basePath, analysis)
{
def foundApplication(sourcePath: Path, className: String): Unit =
analysis.addApplication(sourcePath, className)

View File

@ -6,22 +6,8 @@ package sbt
trait AutoCompilerPlugins extends BasicScalaProject
{
import Configurations.CompilerPlugin
abstract override def ivyConfigurations =
{
val superConfigurations = super.ivyConfigurations.toList
val newConfigurations =
if(superConfigurations.isEmpty)
{
if(useDefaultConfigurations)
CompilerPlugin :: Configurations.defaultMavenConfigurations
else
Configurations.Default :: CompilerPlugin :: Nil
}
else
CompilerPlugin :: superConfigurations
log.debug("Auto configurations: " + newConfigurations.toList.mkString(", "))
Configurations.removeDuplicates(newConfigurations)
}
abstract override def extraDefaultConfigurations =
CompilerPlugin :: super.extraDefaultConfigurations
abstract override def compileOptions = compilerPlugins ++ super.compileOptions
/** A PathFinder that provides the classpath to search for compiler plugins. */

View File

@ -232,7 +232,7 @@ trait BasicManagedProject extends ManagedProject with ReflectiveManagedProject w
/** The dependency manager that represents inline declarations. The default manager packages the information
* from 'ivyXML', 'projectID', 'repositories', and 'libraryDependencies' and does not typically need to be
* be overridden. */
def manager = new SimpleManager(ivyXML, true, projectID, repositories, ivyConfigurations, defaultConfiguration, artifacts, libraryDependencies.toList: _*)
def manager = new SimpleManager(ivyXML, true, projectID, repositories.toSeq, ivyConfigurations, defaultConfiguration, artifacts, libraryDependencies.toList: _*)
/** The pattern for Ivy to use when retrieving dependencies into the local project. Classpath management
* depends on the first directory being [conf] and the extension being [ext].*/
@ -245,24 +245,18 @@ trait BasicManagedProject extends ManagedProject with ReflectiveManagedProject w
override def ivyConfigurations: Iterable[Configuration] =
{
val reflective = super.ivyConfigurations
val extra = extraDefaultConfigurations
if(useDefaultConfigurations)
{
if(reflective.isEmpty && !useIntegrationTestConfiguration)
if(reflective.isEmpty && extra.isEmpty)
Nil
else
{
val base = Configurations.defaultMavenConfigurations ++ reflective
val allConfigurations =
if(useIntegrationTestConfiguration)
base ++ List(Configurations.IntegrationTest)
else
base
Configurations.removeDuplicates(allConfigurations)
}
Configurations.removeDuplicates(Configurations.defaultMavenConfigurations ++ reflective ++ extra)
}
else
reflective
reflective ++ extraDefaultConfigurations
}
def extraDefaultConfigurations: List[Configuration] = Nil
def useIntegrationTestConfiguration = false
def defaultConfiguration: Option[Configuration] = Some(Configurations.DefaultConfiguration(useDefaultConfigurations))
def useMavenConfigurations = true // TODO: deprecate after going through a minor version series to verify that this works ok

View File

@ -7,6 +7,7 @@ import BasicProjectPaths._
sealed abstract class InternalProject extends Project
{
override def defaultLoggingLevel = Level.Warn
override final def historyPath = None
override def tasks: Map[String, Task] = Map.empty
override final protected def disableCrossPaths = false
@ -76,8 +77,9 @@ private sealed abstract class BasicBuilderProject extends InternalProject with S
}
}
protected def analysisCallback: AnalysisCallback =
new BasicAnalysisCallback(info.projectPath, List(Project.ProjectClassName), analysis)
new BasicAnalysisCallback(info.projectPath, analysis)
{
def superclassNames = List(Project.ProjectClassName)
def foundApplication(sourcePath: Path, className: String) {}
def foundSubclass(sourcePath: Path, subclassName: String, superclassName: String, isModule: Boolean)
{
@ -107,12 +109,12 @@ private sealed abstract class BasicBuilderProject extends InternalProject with S
override final def methods = Map.empty
}
/** The project definition used to build project definitions. */
private final class BuilderProject(val info: ProjectInfo, val pluginPath: Path, additional: Iterable[Path], override protected val logImpl: Logger) extends BasicBuilderProject
private final class BuilderProject(val info: ProjectInfo, val pluginPath: Path, additional: Iterable[Path], rawLogger: Logger) extends BasicBuilderProject
{
private lazy val pluginProject =
{
if(pluginPath.exists)
Some(new PluginBuilderProject(ProjectInfo(pluginPath.asFile, Nil, None)))
Some(new PluginBuilderProject(ProjectInfo(pluginPath.asFile, Nil, None)(rawLogger)))
else
None
}
@ -125,7 +127,6 @@ private final class BuilderProject(val info: ProjectInfo, val pluginPath: Path,
final class PluginBuilderProject(val info: ProjectInfo) extends BasicBuilderProject
{
override protected def logImpl = BuilderProject.this.log
val pluginUptodate = propertyOptional[Boolean](false)
def tpe = "plugin definition"
def managedSourcePath = path(BasicDependencyPaths.DefaultManagedSourceDirectoryName)
@ -159,8 +160,8 @@ private final class BuilderProject(val info: ProjectInfo, val pluginPath: Path,
{
Control.thread(projectDefinition) {
case Some(definition) =>
logInfo("\nUpdating plugins")
val pluginInfo = ProjectInfo(info.projectPath.asFile, Nil, None)
logInfo("\nUpdating plugins...")
val pluginInfo = ProjectInfo(info.projectPath.asFile, Nil, None)(rawLogger)
val pluginBuilder = Project.constructProject(pluginInfo, Project.getProjectClass[PluginDefinition](definition, projectClasspath, getClass.getClassLoader))
pluginBuilder.projectName() = "Plugin builder"
pluginBuilder.projectVersion() = OpaqueVersion("1.0")
@ -199,6 +200,7 @@ private final class BuilderProject(val info: ProjectInfo, val pluginPath: Path,
}
class PluginDefinition(val info: ProjectInfo) extends InternalProject with BasicManagedProject
{
override def defaultLoggingLevel = Level.Info
override final def outputPattern = "[artifact](-[revision]).[ext]"
override final val tasks = Map("update" -> update)
override def projectClasspath(config: Configuration) = Path.emptyPathFinder

View File

@ -213,12 +213,11 @@ class CompileConditional(override val config: CompileConfiguration) extends Abst
protected def constructAnalysis(analysisPath: Path, projectPath: Path, log: Logger) =
new CompileAnalysis(analysisPath, projectPath, log)
protected def analysisCallback = new CompileAnalysisCallback
protected class CompileAnalysisCallback extends BasicCompileAnalysisCallback(projectPath, testDefinitionClassNames, analysis)
protected class CompileAnalysisCallback extends BasicCompileAnalysisCallback(projectPath, analysis)
{
def foundSubclass(sourcePath: Path, subclassName: String, superclassName: String, isModule: Boolean)
{
def superclassNames = testDefinitionClassNames
def foundSubclass(sourcePath: Path, subclassName: String, superclassName: String, isModule: Boolean): Unit =
analysis.addTest(sourcePath, TestDefinition(isModule, subclassName, superclassName))
}
}
}
abstract class AbstractCompileConditional(val config: AbstractCompileConfiguration) extends Conditional[Path, Path, File]

View File

@ -147,13 +147,13 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec
}
}
/** The unmanaged base classpath. By default, the unmanaged classpaths for test and run include this classpath. */
protected def mainUnmanagedClasspath = mainCompilePath +++ mainResourceClasspath +++ unmanagedClasspath
protected def mainUnmanagedClasspath = mainCompilePath +++ mainResourcesOutputPath +++ unmanagedClasspath
/** The unmanaged classpath for the run configuration. By default, it includes the base classpath returned by
* `mainUnmanagedClasspath`.*/
protected def runUnmanagedClasspath = mainUnmanagedClasspath +++ mainDependencies.scalaCompiler
/** The unmanaged classpath for the test configuration. By default, it includes the run classpath, which includes the base
* classpath returned by `mainUnmanagedClasspath`.*/
protected def testUnmanagedClasspath = testCompilePath +++ testResourceClasspath +++ testDependencies.scalaCompiler +++ runUnmanagedClasspath
protected def testUnmanagedClasspath = testCompilePath +++ testResourcesOutputPath +++ testDependencies.scalaCompiler +++ runUnmanagedClasspath
/** @deprecated Use `mainDependencies.scalaJars`*/
@deprecated protected final def scalaJars: Iterable[File] = mainDependencies.scalaJars.get.map(_.asFile)
@ -181,9 +181,8 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec
def log = BasicScalaProject.this.log
def projectPath = info.projectPath
def baseCompileOptions: Seq[CompileOption]
lazy val localBaseOptions = baseCompileOptions
def options = optionsAsString(localBaseOptions.filter(!_.isInstanceOf[MaxCompileErrors]))
def maxErrors = maximumErrors(localBaseOptions)
def options = optionsAsString(baseCompileOptions.filter(!_.isInstanceOf[MaxCompileErrors]))
def maxErrors = maximumErrors(baseCompileOptions)
def compileOrder = BasicScalaProject.this.compileOrder
}
class MainCompileConfig extends BaseCompileConfig
@ -233,9 +232,9 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec
protected def compileAction = task { doCompile(mainCompileConditional) } describedAs MainCompileDescription
protected def testCompileAction = task { doCompile(testCompileConditional) } dependsOn compile describedAs TestCompileDescription
protected def cleanAction = cleanTask(outputPath, cleanOptions) describedAs CleanDescription
protected def runAction = task { args => runTask(getMainClass(true), runClasspath, args, getRunner) dependsOn(compile) } describedAs RunDescription
protected def runAction = task { args => runTask(getMainClass(true), runClasspath, args, getRunner) dependsOn(compile, copyResources) } describedAs RunDescription
protected def consoleQuickAction = consoleTask(consoleClasspath, getRunner) describedAs ConsoleQuickDescription
protected def consoleAction = consoleTask(consoleClasspath, getRunner).dependsOn(testCompile) describedAs ConsoleDescription
protected def consoleAction = consoleTask(consoleClasspath, getRunner).dependsOn(testCompile, copyResources, copyTestResources) describedAs ConsoleDescription
protected def docAction = scaladocTask(mainLabel, mainSources, mainDocPath, docClasspath, documentOptions).dependsOn(compile) describedAs DocDescription
protected def docTestAction = scaladocTask(testLabel, testSources, testDocPath, docClasspath, documentOptions).dependsOn(testCompile) describedAs TestDocDescription
protected def testAction = defaultTestTask(testOptions)
@ -246,7 +245,7 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec
protected def defaultTestQuickMethod(failedOnly: Boolean) =
testQuickMethod(testCompileConditional.analysis, testOptions)(options => defaultTestTask(quickOptions(failedOnly) ::: options.toList))
protected def defaultTestTask(testOptions: => Seq[TestOption]) =
testTask(testFrameworks, testClasspath, testCompileConditional.analysis, testOptions).dependsOn(testCompile) describedAs TestDescription
testTask(testFrameworks, testClasspath, testCompileConditional.analysis, testOptions).dependsOn(testCompile, copyResources, copyTestResources) describedAs TestDescription
override def packageToPublishActions: Seq[ManagedTask] = `package` :: Nil
@ -263,6 +262,9 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec
protected def incrementVersionAction = task { incrementVersionNumber(); None } describedAs IncrementVersionDescription
protected def releaseAction = (test && packageAll && incrementVersion) describedAs ReleaseDescription
protected def copyResourcesAction = syncPathsTask(mainResources, mainResourcesOutputPath) describedAs CopyResourcesDescription
protected def copyTestResourcesAction = syncPathsTask(testResources, testResourcesOutputPath) describedAs CopyTestResourcesDescription
lazy val compile = compileAction
lazy val testCompile = testCompileAction
lazy val clean = cleanAction
@ -283,6 +285,8 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec
lazy val graph = graphAction
lazy val incrementVersion = incrementVersionAction
lazy val release = releaseAction
lazy val copyResources = copyResourcesAction
lazy val copyTestResources = copyTestResourcesAction
lazy val testQuick = testQuickAction
lazy val testFailed = testFailedAction
@ -306,21 +310,39 @@ abstract class BasicScalaProject extends ScalaProject with BasicDependencyProjec
override def watchPaths = mainSources +++ testSources +++ mainResources +++ testResources
}
abstract class BasicWebScalaProject extends BasicScalaProject with WebScalaProject with WebScalaPaths
{
{ p =>
import BasicWebScalaProject._
override def watchPaths = super.watchPaths +++ webappResources
lazy val prepareWebapp = prepareWebappAction
protected def prepareWebappAction =
prepareWebappTask(webappResources, temporaryWarPath, webappClasspath, mainDependencies.scalaJars) dependsOn(compile)
prepareWebappTask(webappResources, temporaryWarPath, webappClasspath, mainDependencies.scalaJars) dependsOn(compile, copyResources)
private lazy val jettyInstance = new JettyRunner(jettyConfiguration)
def jettyConfiguration =
new DefaultJettyConfiguration
{
def classpath = jettyRunClasspath
def jettyClasspath = p.jettyClasspath
def war = jettyWebappPath
def contextPath = jettyContextPath
def classpathName = "test"
def scanDirectories = p.scanDirectories.map(_.asFile)
def scanInterval = p.scanInterval
def port = jettyPort
def log = p.log
}
/** This is the classpath used to determine what classes, resources, and jars to put in the war file.*/
def webappClasspath = publicClasspath
def jettyRunClasspath = testClasspath
/** This is the classpath containing Jetty.*/
def jettyClasspath = testClasspath --- jettyRunClasspath
/** This is the classpath containing the web application.*/
def jettyRunClasspath = publicClasspath
def jettyWebappPath = temporaryWarPath
lazy val jettyRun = jettyRunAction
lazy val jetty = task { idle() } dependsOn(jettyRun) describedAs(JettyDescription)
protected def jettyRunAction =
jettyRunTask(jettyWebappPath, jettyContextPath, jettyPort, jettyRunClasspath, "test", scanDirectories.map(_.asFile), scanInterval) dependsOn(prepareWebapp) describedAs(JettyRunDescription)
protected def jettyRunAction = jettyRunTask(jettyInstance) dependsOn(prepareWebapp) describedAs(JettyRunDescription)
private def idle() =
{
log.info("Waiting... (press any key to interrupt)")
@ -338,13 +360,13 @@ abstract class BasicWebScalaProject extends BasicScalaProject with WebScalaProje
/** The directories that should be watched to determine if the web application needs to be reloaded..*/
def scanDirectories: Seq[Path] = jettyWebappPath :: Nil
/** The time in seconds between scans that check whether the web application should be reloaded.*/
def scanInterval: Int = 3
def scanInterval: Int = JettyRunner.DefaultScanInterval
/** The port that Jetty runs on. */
def jettyPort: Int = JettyRun.DefaultPort
def jettyPort: Int = JettyRunner.DefaultPort
lazy val jettyRestart = jettyStop && jettyRun
lazy val jettyStop = jettyStopAction
protected def jettyStopAction = jettyStopTask describedAs(JettyStopDescription)
protected def jettyStopAction = jettyStopTask(jettyInstance) describedAs(JettyStopDescription)
/** The clean action for a web project is modified so that it first stops jetty if it is running,
* since the webapp directory will be removed by the clean.*/
@ -403,6 +425,10 @@ object BasicScalaProject
"Increments the micro part of the version (the third number) by one. (This is only valid for versions of the form #.#.#-*)"
val ReleaseDescription =
"Compiles, tests, generates documentation, packages, and increments the version."
val CopyResourcesDescription =
"Copies resources to the target directory where they can be included on classpaths."
val CopyTestResourcesDescription =
"Copies test resources to the target directory where they can be included on the test classpath."
private def warnMultipleMainClasses(log: Logger) =
{

View File

@ -56,6 +56,14 @@ trait BasicIntegrationTesting extends ScalaIntegrationTesting with IntegrationTe
def integrationTestFrameworks = testFrameworks
override def useIntegrationTestConfiguration = false
abstract override def extraDefaultConfigurations =
{
val superConfigurations = super.extraDefaultConfigurations
if(useIntegrationTestConfiguration)
integrationTestConfiguration :: superConfigurations
else
superConfigurations
}
abstract override def fullUnmanagedClasspath(config: Configuration) =
{
val superClasspath = super.fullUnmanagedClasspath(config)

View File

@ -96,6 +96,34 @@ final class MultiLogger(delegates: List[Logger]) extends BasicLogger
private def dispatch(event: LogEvent) { delegates.foreach(_.log(event)) }
}
/** A filter logger is used to delegate messages but not the logging level to another logger. This means
* that messages are logged at the higher of the two levels set by this logger and its delegate.
* */
final class FilterLogger(delegate: Logger) extends BasicLogger
{
def trace(t: => Throwable)
{
if(traceEnabled)
delegate.trace(t)
}
def log(level: Level.Value, message: => String)
{
if(atLevel(level))
delegate.log(level, message)
}
def success(message: => String)
{
if(atLevel(Level.Info))
delegate.success(message)
}
def control(event: ControlEvent.Value, message: => String)
{
if(atLevel(Level.Info))
delegate.control(event, message)
}
def logAll(events: Seq[LogEvent]): Unit = events.foreach(delegate.log)
}
/** A logger that can buffer the logging done on it by currently executing Thread and
* then can flush the buffer to the delegate logger provided in the constructor. Use
* 'startRecording' to start buffering and then 'play' from to flush the buffer for the
@ -110,78 +138,67 @@ final class MultiLogger(delegates: List[Logger]) extends BasicLogger
final class BufferedLogger(delegate: Logger) extends Logger
{
private[this] val buffers = wrap.Wrappers.weakMap[Thread, Buffer[LogEvent]]
/* The recording depth part is to enable a weak nesting of recording calls. When recording is
* nested (recordingDepth >= 2), calls to play/playAll add the buffers for worker Threads to the
* serial buffer (main Thread) and calls to clear/clearAll clear worker Thread buffers only. */
private[this] def recording = recordingDepth > 0
private[this] var recordingDepth = 0
private[this] var recordingAll = false
private[this] val mainThread = Thread.currentThread
private[this] def getBuffer(key: Thread) = buffers.getOrElseUpdate(key, new ListBuffer[LogEvent])
private[this] def buffer = getBuffer(key)
private[this] def getOrCreateBuffer = buffers.getOrElseUpdate(key, createBuffer)
private[this] def buffer = if(recordingAll) Some(getOrCreateBuffer) else buffers.get(key)
private[this] def createBuffer = new ListBuffer[LogEvent]
private[this] def key = Thread.currentThread
private[this] def serialBuffer = getBuffer(mainThread)
private[this] def inWorker = Thread.currentThread ne mainThread
/** Enables buffering. */
def startRecording() { synchronized { recordingDepth += 1 } }
@deprecated def startRecording() = recordAll()
/** Enables buffering for logging coming from the current Thread. */
def record(): Unit = synchronized { buffers(key) = createBuffer }
/** Enables buffering for logging coming from all Threads. */
def recordAll(): Unit = synchronized{ recordingAll = true }
def buffer[T](f: => T): T =
{
record()
try { f }
finally { Control.trap(stop()) }
}
def bufferAll[T](f: => T): T =
{
recordAll()
try { f }
finally { Control.trap(stopAll()) }
}
/** Flushes the buffer to the delegate logger for the current thread. This method calls logAll on the delegate
* so that the messages are written consecutively. The buffer is cleared in the process. */
def play(): Unit =
synchronized
{
if(recordingDepth == 1)
for(buffer <- buffers.get(key))
delegate.logAll(wrap.Wrappers.readOnly(buffer))
else if(recordingDepth > 1 && inWorker)
serialBuffer ++= buffer
}
def playAll(): Unit =
synchronized
{
if(recordingDepth == 1)
{
for(buffer <- buffers.values)
delegate.logAll(wrap.Wrappers.readOnly(buffer))
}
else if(recordingDepth > 1)
{
for((key, buffer) <- buffers.toList if key ne mainThread)
serialBuffer ++= buffer
}
for(buffer <- buffers.values)
delegate.logAll(wrap.Wrappers.readOnly(buffer))
}
/** Clears buffered events for the current thread. It does not disable buffering. */
def clear(): Unit = synchronized { if(recordingDepth == 1 || inWorker) buffers -= key }
/** Clears buffered events for all threads and disables buffering. */
/** Clears buffered events for the current thread and disables buffering. */
def clear(): Unit = synchronized { buffers -= key }
/** Clears buffered events for all threads and disables all buffering. */
def clearAll(): Unit = synchronized { buffers.clear(); recordingAll = false }
/** Plays buffered events for the current thread and disables buffering. */
def stop(): Unit =
synchronized
{
clearAll()
if(recordingDepth > 0)
recordingDepth -= 1
play()
clear()
}
/** Clears buffered events for all threads. */
def clearAll(): Unit =
def stopAll(): Unit =
synchronized
{
if(recordingDepth <= 1)
buffers.clear()
else
{
val serial = serialBuffer
buffers.clear()
buffers(mainThread) = serial
}
playAll()
clearAll()
}
def runAndFlush[T](f: => T): T =
{
try { f }
finally { play(); clear() }
}
def setLevel(newLevel: Level.Value): Unit =
synchronized {
if(recording) buffer += new SetLevel(newLevel)
synchronized
{
buffer.foreach{_ += new SetLevel(newLevel) }
delegate.setLevel(newLevel)
}
def getLevel = synchronized { delegate.getLevel }
@ -189,58 +206,39 @@ final class BufferedLogger(delegate: Logger) extends Logger
def enableTrace(flag: Boolean): Unit =
synchronized
{
if(recording) buffer += new SetTrace(flag)
buffer.foreach{_ += new SetTrace(flag) }
delegate.enableTrace(flag)
}
def trace(t: => Throwable): Unit =
synchronized
{
if(traceEnabled)
{
if(recording) buffer += new Trace(t)
else delegate.trace(t)
}
}
doBufferableIf(traceEnabled, new Trace(t), _.trace(t))
def success(message: => String): Unit =
synchronized
{
if(atLevel(Level.Info))
{
if(recording)
buffer += new Success(message)
else
delegate.success(message)
}
}
doBufferable(Level.Info, new Success(message), _.success(message))
def log(level: Level.Value, message: => String): Unit =
synchronized
{
if(atLevel(level))
{
if(recording)
buffer += new Log(level, message)
else
delegate.log(level, message)
}
}
doBufferable(level, new Log(level, message), _.log(level, message))
def logAll(events: Seq[LogEvent]): Unit =
synchronized
{
if(recording)
buffer ++= events
else
delegate.logAll(events)
buffer match
{
case Some(b) => b ++= events
case None => delegate.logAll(events)
}
}
def control(event: ControlEvent.Value, message: => String): Unit =
doBufferable(Level.Info, new ControlEvent(event, message), _.control(event, message))
private def doBufferable(level: Level.Value, appendIfBuffered: => LogEvent, doUnbuffered: Logger => Unit): Unit =
doBufferableIf(atLevel(level), appendIfBuffered, doUnbuffered)
private def doBufferableIf(condition: => Boolean, appendIfBuffered: => LogEvent, doUnbuffered: Logger => Unit): Unit =
synchronized
{
if(atLevel(Level.Info))
if(condition)
{
if(recording)
buffer += new ControlEvent(event, message)
else
delegate.control(event, message)
buffer match
{
case Some(b) => b += appendIfBuffered
case None => doUnbuffered(delegate)
}
}
}
}

View File

@ -13,7 +13,13 @@ trait Project extends TaskManager with Dag[Project] with BasicEnvironment
{
/** The logger for this project definition. */
final val log: Logger = logImpl
protected def logImpl: Logger = new BufferedLogger(new ConsoleLogger)
protected def logImpl: Logger =
{
val lg = new BufferedLogger(info.logger)
lg.setLevel(defaultLoggingLevel)
lg
}
protected def defaultLoggingLevel = Level.Info
trait ActionOption extends NotNull
@ -166,7 +172,7 @@ trait Project extends TaskManager with Dag[Project] with BasicEnvironment
* The construct function is used to obtain the Project instance. Any project/build/ directory for the project
* is ignored. The project is declared to have the dependencies given by deps.*/
def project[P <: Project](path: Path, name: String, construct: ProjectInfo => P, deps: Project*): P =
initialize(construct(ProjectInfo(path.asFile, deps, Some(this))), Some(new SetupInfo(name, None, None, false)), log)
initialize(construct(ProjectInfo(path.asFile, deps, Some(this))(log)), Some(new SetupInfo(name, None, None, false)), log)
/** Initializes the project directories when a user has requested that sbt create a new project.*/
def initializeDirectories() {}
@ -186,14 +192,14 @@ trait Project extends TaskManager with Dag[Project] with BasicEnvironment
/** The list of directories to which this project writes. This is used to verify that multiple
* projects have not been defined with the same output directories. */
def outputDirectories: Iterable[Path] = outputRootPath :: Nil
def outputDirectories: Iterable[Path] = outputPath :: Nil
def rootProject = Project.rootProject(this)
/** The path to the file that provides persistence for properties.*/
final def envBackingPath = info.builderPath / Project.DefaultEnvBackingName
/** The path to the file that provides persistence for history. */
def historyPath: Option[Path] = Some(outputRootPath / ".history")
def outputPath = crossPath(outputRootPath)
def outputRootPath = outputDirectoryName
def outputRootPath: Path = outputDirectoryName
def outputDirectoryName = DefaultOutputDirectoryName
private def getProject(result: LoadResult, path: Path): Project =
@ -297,7 +303,7 @@ object Project
loadProject(projectDirectory, deps, parent, getClass.getClassLoader, log)
private[sbt] def loadProject(projectDirectory: File, deps: Iterable[Project], parent: Option[Project], additional: ClassLoader, log: Logger): LoadResult =
{
val info = ProjectInfo(projectDirectory, deps, parent)
val info = ProjectInfo(projectDirectory, deps, parent)(log)
ProjectInfo.setup(info, log) match
{
case err: SetupError => new LoadSetupError(err.message)
@ -310,12 +316,9 @@ object Project
{
try
{
val oldLevel = log.getLevel
log.setLevel(Level.Warn)
val result =
for(builderClass <- getProjectDefinition(info, additional, log).right) yield
initialize(constructProject(info, builderClass), setupInfo, log)
log.setLevel(oldLevel)
result.fold(new LoadError(_), new LoadSuccess(_))
}
catch
@ -381,7 +384,7 @@ object Project
{
val pluginProjectPath = info.builderPath / PluginProjectDirectoryName
val additionalPaths = additional match { case u: URLClassLoader => u.getURLs.map(url => Path.fromFile(FileUtilities.toFile(url))); case _ => Nil }
val builderProject = new BuilderProject(ProjectInfo(builderProjectPath.asFile, Nil, None), pluginProjectPath, additionalPaths, buildLog)
val builderProject = new BuilderProject(ProjectInfo(builderProjectPath.asFile, Nil, None)(buildLog), pluginProjectPath, additionalPaths, buildLog)
builderProject.compile.run.toLeft(()).right.flatMap { ignore =>
builderProject.projectDefinition.right.map {
case Some(definition) => getProjectClass[Project](definition, builderProject.projectClasspath, additional)

View File

@ -6,8 +6,9 @@ package sbt
import java.io.File
import FileUtilities._
final case class ProjectInfo(projectDirectory: File, dependencies: Iterable[Project], parent: Option[Project]) extends NotNull
final case class ProjectInfo(projectDirectory: File, dependencies: Iterable[Project], parent: Option[Project])(log: Logger) extends NotNull
{
val logger = new FilterLogger(log)
val projectPath: Path =
{
val toRoot = parent.flatMap(p => Path.relativize(p.info.projectPath, projectDirectory))

View File

@ -26,9 +26,6 @@ trait ScalaPaths extends PackagePaths
/** A PathFinder that selects all test resources. */
def testResources: PathFinder
def mainResourceClasspath: PathFinder
def testResourceClasspath: PathFinder
def mainCompilePath: Path
def testCompilePath: Path
def mainAnalysisPath: Path
@ -36,6 +33,8 @@ trait ScalaPaths extends PackagePaths
def mainDocPath: Path
def testDocPath: Path
def graphPath: Path
def mainResourcesOutputPath: Path
def testResourcesOutputPath: Path
/** A PathFinder that selects all the classes compiled from the main sources.*/
def mainClasses: PathFinder
@ -66,6 +65,7 @@ trait BasicScalaPaths extends Project with ScalaPaths
{
def mainResourcesPath: PathFinder
def testResourcesPath: PathFinder
def managedDependencyPath: Path
def managedDependencyRootPath: Path
def dependencyPath: Path
@ -82,8 +82,6 @@ trait BasicScalaPaths extends Project with ScalaPaths
}
def testSources = sources(testSourceRoots)
def mainResourceClasspath = mainResourcesPath
def testResourceClasspath = testResourcesPath
def mainResources = descendents(mainResourcesPath ##, "*")
def testResources = descendents(testResourcesPath ##, "*")
@ -100,7 +98,7 @@ trait BasicScalaPaths extends Project with ScalaPaths
info.bootPath +++ info.builderProjectOutputPath +++
info.pluginsOutputPath +++ info.pluginsManagedSourcePath +++ info.pluginsManagedDependencyPath
override def outputDirectories = outputRootPath :: managedDependencyRootPath :: Nil
override def outputDirectories = outputPath :: managedDependencyPath :: Nil
}
@deprecated trait BasicProjectPaths extends MavenStyleScalaPaths
@ -123,6 +121,8 @@ trait MavenStyleScalaPaths extends BasicScalaPaths with BasicPackagePaths
def graphDirectoryName = DefaultGraphDirectoryName
def mainAnalysisDirectoryName = DefaultMainAnalysisDirectoryName
def testAnalysisDirectoryName = DefaultTestAnalysisDirectoryName
def mainResourcesOutputDirectoryName = DefautMainResourcesOutputDirectoryName
def testResourcesOutputDirectoryName = DefautTestResourcesOutputDirectoryName
def sourcePath = path(sourceDirectoryName)
@ -132,6 +132,7 @@ trait MavenStyleScalaPaths extends BasicScalaPaths with BasicPackagePaths
def mainResourcesPath = mainSourcePath / resourcesDirectoryName
def mainDocPath = docPath / mainDirectoryName / apiDirectoryName
def mainCompilePath = outputPath / mainCompileDirectoryName
def mainResourcesOutputPath = outputPath / mainResourcesOutputDirectoryName
def mainAnalysisPath = outputPath / mainAnalysisDirectoryName
def testSourcePath = sourcePath / testDirectoryName
@ -140,6 +141,7 @@ trait MavenStyleScalaPaths extends BasicScalaPaths with BasicPackagePaths
def testResourcesPath = testSourcePath / resourcesDirectoryName
def testDocPath = docPath / testDirectoryName / apiDirectoryName
def testCompilePath = outputPath / testCompileDirectoryName
def testResourcesOutputPath = outputPath / testResourcesOutputDirectoryName
def testAnalysisPath = outputPath / testAnalysisDirectoryName
def docPath = outputPath / docDirectoryName
@ -183,6 +185,8 @@ object BasicProjectPaths
val DefaultGraphDirectoryName = "graph"
val DefaultMainAnalysisDirectoryName = "analysis"
val DefaultTestAnalysisDirectoryName = "test-analysis"
val DefautMainResourcesOutputDirectoryName = "resources"
val DefautTestResourcesOutputDirectoryName = "test-resources"
val DefaultMainDirectoryName = "main"
val DefaultScalaDirectoryName = "scala"

View File

@ -79,21 +79,21 @@ class Resources(val baseDirectory: File, additional: ClassLoader)
else
{
val buffered = new BufferedLogger(log)
buffered.setLevel(Level.Debug)
buffered.enableTrace(true)
def error(msg: String) =
{
buffered.playAll()
buffered.stop()
buffered.stopAll()
Left(msg)
}
buffered.startRecording()
buffered.recordAll()
resultToEither(Project.loadProject(dir, Nil, None, additional, buffered)) match
{
case Left(msg) =>
reload match
{
case ReloadErrorExpected =>
buffered.stop()
buffered.clearAll()
previousProject.toRight("Initial project load failed.")
case s: ReloadSuccessExpected => error(s.prefixIfError + msg)
case NoReload /* shouldn't happen */=> error(msg)
@ -103,7 +103,7 @@ class Resources(val baseDirectory: File, additional: ClassLoader)
{
case ReloadErrorExpected => error("Expected project load failure, but it succeeded.")
case _ =>
buffered.stop()
buffered.clearAll()
Right(p)
}
}
@ -111,15 +111,13 @@ class Resources(val baseDirectory: File, additional: ClassLoader)
loadResult match
{
case Right(project) =>
project.log.enableTrace(log.traceEnabled)
project.log.setLevel(log.getLevel)
f(project) match
{
case ContinueResult(newF, newReload) => withProject(log, Some(project), newReload, dir)(newF)
case ValueResult(value) => Right(value)
case err: ErrorResult => Left(err.message)
case err: ErrorResult => error(err.message)
}
case Left(message) => Left(message)
case Left(message) => error(message)
}
}

View File

@ -143,6 +143,8 @@ trait ScalaProject extends SimpleScalaProject with FileTasks with MultiTaskProje
}
}
def syncPathsTask(sources: PathFinder, destinationDirectory: Path): Task =
task { FileUtilities.syncPaths(sources, destinationDirectory, log) }
def syncTask(sourceDirectory: Path, destinationDirectory: Path): Task =
task { FileUtilities.sync(sourceDirectory, destinationDirectory, log) }
def copyTask(sources: PathFinder, destinationDirectory: Path): Task =
@ -323,14 +325,8 @@ trait WebScalaProject extends ScalaProject
}
}}}}).left.toOption
}
def jettyRunTask(warPath: => Path, defaultContextPath: => String, port: Int, classpath: PathFinder, classpathName: String, scanDirectories: Seq[File], scanInterval: Int): Task =
task { JettyRun(classpath.get, classpathName, warPath, defaultContextPath, port, scanDirectories, scanInterval, log) }
def jettyRunTask(warPath: => Path, defaultContextPath: => String, classpath: PathFinder, classpathName: String, scanDirectories: Seq[File], scanInterval: Int): Task =
jettyRunTask(warPath, defaultContextPath, JettyRun.DefaultPort, classpath, classpathName, scanDirectories, scanInterval)
def jettyRunTask(warPath: => Path, defaultContextPath: => String, classpath: PathFinder, classpathName: String,
jettyConfigurationXML: scala.xml.NodeSeq, jettyConfigurationFiles: Seq[File]): Task =
task { JettyRun(classpath.get, classpathName, warPath, defaultContextPath, jettyConfigurationXML, jettyConfigurationFiles, log) }
def jettyStopTask = task { JettyRun.stop(); None }
def jettyRunTask(jettyRun: JettyRunner) = task { jettyRun() }
def jettyStopTask(jettyRun: JettyRunner) = task { jettyRun.stop(); None }
}
object ScalaProject
{

View File

@ -7,10 +7,13 @@ import java.io.File
import java.net.{URL, URLClassLoader}
import scala.xml.NodeSeq
object JettyRun extends ExitHook
object JettyRunner
{
val DefaultPort = 8080
val DefaultScanInterval = 3
}
class JettyRunner(configuration: JettyConfiguration) extends ExitHook
{
ExitHooks.register(this)
def name = "jetty-shutdown"
@ -19,49 +22,38 @@ object JettyRun extends ExitHook
private def started(s: Stoppable) { running = Some(s) }
def stop()
{
synchronized
running.foreach(_.stop())
running = None
}
def apply(): Option[String] =
{
import configuration._
def runJetty() =
{
running.foreach(_.stop())
running = None
val baseLoader = this.getClass.getClassLoader
val classpathURLs = jettyClasspath.get.map(_.asURL).toSeq
val loader: ClassLoader = new java.net.URLClassLoader(classpathURLs.toArray, baseLoader)
val lazyLoader = new LazyFrameworkLoader(implClassName, Array(FileUtilities.sbtJar.toURI.toURL), loader, baseLoader)
val runner = ModuleUtilities.getObject(implClassName, lazyLoader).asInstanceOf[JettyRun]
runner(configuration)
}
if(running.isDefined)
Some("This instance of Jetty is already running.")
else
{
try
{
started(runJetty())
None
}
catch
{
case e: NoClassDefFoundError => runError(e, "Jetty and its dependencies must be on the " + classpathName + " classpath: ", log)
case e => runError(e, "Error running Jetty: ", log)
}
}
}
def apply(classpath: Iterable[Path], classpathName: String, war: Path, defaultContextPath: String, jettyConfigurationXML: NodeSeq,
jettyConfigurationFiles: Seq[File], log: Logger): Option[String] =
run(classpathName, new JettyRunConfiguration(war, defaultContextPath, DefaultPort, jettyConfigurationXML,
jettyConfigurationFiles, Nil, 0, toURLs(classpath)), log)
def apply(classpath: Iterable[Path], classpathName: String, war: Path, defaultContextPath: String, port: Int, scanDirectories: Seq[File],
scanPeriod: Int, log: Logger): Option[String] =
run(classpathName, new JettyRunConfiguration(war, defaultContextPath, port, NodeSeq.Empty, Nil, scanDirectories, scanPeriod, toURLs(classpath)), log)
private def toURLs(paths: Iterable[Path]) = paths.map(_.asURL).toSeq
private def run(classpathName: String, configuration: JettyRunConfiguration, log: Logger): Option[String] =
synchronized
{
import configuration._
def runJetty() =
{
val baseLoader = this.getClass.getClassLoader
val loader: ClassLoader = new java.net.URLClassLoader(classpathURLs.toArray, baseLoader)
val lazyLoader = new LazyFrameworkLoader(implClassName, Array(FileUtilities.sbtJar.toURI.toURL), loader, baseLoader)
val runner = ModuleUtilities.getObject(implClassName, lazyLoader).asInstanceOf[JettyRun]
runner(configuration, log)
}
if(running.isDefined)
Some("Jetty is already running.")
else
{
try
{
started(runJetty())
None
}
catch
{
case e: NoClassDefFoundError => runError(e, "Jetty and its dependencies must be on the " + classpathName + " classpath: ", log)
case e => runError(e, "Error running Jetty: ", log)
}
}
}
private val implClassName = "sbt.LazyJettyRun"
private def runError(e: Throwable, messageBase: String, log: Logger) =
@ -77,11 +69,30 @@ private trait Stoppable
}
private trait JettyRun
{
def apply(configuration: JettyRunConfiguration, log: Logger): Stoppable
def apply(configuration: JettyConfiguration): Stoppable
}
sealed trait JettyConfiguration extends NotNull
{
def war: Path
def scanDirectories: Seq[File]
def scanInterval: Int
/** The classpath to get Jetty from. */
def jettyClasspath: PathFinder
/** The classpath containing the classes, jars, and resources for the web application. */
def classpath: PathFinder
def classpathName: String
def log: Logger
}
trait DefaultJettyConfiguration extends JettyConfiguration
{
def contextPath: String
def port: Int
}
abstract class CustomJettyConfiguration extends JettyConfiguration
{
def jettyConfigurationFiles: Seq[File] = Nil
def jettyConfigurationXML: NodeSeq = NodeSeq.Empty
}
private class JettyRunConfiguration(val war: Path, val defaultContextPath: String, val port: Int,
val jettyConfigurationXML: NodeSeq, val jettyConfigurationFiles: Seq[File],
val scanDirectories: Seq[File], val scanInterval: Int, val classpathURLs: Seq[URL]) extends NotNull
/* This class starts Jetty.
* NOTE: DO NOT actively use this class. You will see NoClassDefFoundErrors if you fail
@ -102,51 +113,51 @@ private object LazyJettyRun extends JettyRun
val DefaultMaxIdleTime = 30000
def apply(configuration: JettyRunConfiguration, log: Logger): Stoppable =
def apply(configuration: JettyConfiguration): Stoppable =
{
import configuration._
val oldLog = Log.getLog
Log.setLog(new JettyLogger(log))
Log.setLog(new JettyLogger(configuration.log))
val server = new Server
val useDefaults = jettyConfigurationXML.isEmpty && jettyConfigurationFiles.isEmpty
val listener =
if(useDefaults)
configuration match
{
configureDefaultConnector(server, port)
def createLoader = new URLClassLoader(classpathURLs.toArray, this.getClass.getClassLoader)
val webapp = new WebAppContext(war.absolutePath, defaultContextPath)
webapp.setClassLoader(createLoader)
server.setHandler(webapp)
Some(new Scanner.BulkListener {
def filesChanged(files: java.util.List[_]) {
reload(server, webapp.setClassLoader(createLoader), log)
}
})
}
else
{
for(x <- jettyConfigurationXML)
(new XmlConfiguration(x.toString)).configure(server)
for(file <- jettyConfigurationFiles)
(new XmlConfiguration(file.toURI.toURL)).configure(server)
None
case c: DefaultJettyConfiguration =>
import c._
configureDefaultConnector(server, port)
def classpathURLs = classpath.get.map(_.asURL).toSeq
def createLoader = new URLClassLoader(classpathURLs.toArray, this.getClass.getClassLoader)
val webapp = new WebAppContext(war.absolutePath, contextPath)
webapp.setClassLoader(createLoader)
server.setHandler(webapp)
Some(new Scanner.BulkListener {
def filesChanged(files: java.util.List[_]) {
reload(server, webapp.setClassLoader(createLoader), log)
}
})
case c: CustomJettyConfiguration =>
for(x <- c.jettyConfigurationXML)
(new XmlConfiguration(x.toString)).configure(server)
for(file <- c.jettyConfigurationFiles)
(new XmlConfiguration(file.toURI.toURL)).configure(server)
None
}
def configureScanner() =
{
val scanDirectories = configuration.scanDirectories
if(listener.isEmpty || scanDirectories.isEmpty)
None
else
{
log.debug("Scanning for changes to: " + scanDirectories.mkString(", "))
configuration.log.debug("Scanning for changes to: " + scanDirectories.mkString(", "))
val scanner = new Scanner
val list = new java.util.ArrayList[File]
scanDirectories.foreach(x => list.add(x))
scanner.setScanDirs(list)
scanner.setRecursive(true)
scanner.setScanInterval(scanInterval)
scanner.setScanInterval(configuration.scanInterval)
scanner.setReportExistingFilesOnStartup(false)
scanner.addListener(listener.get)
scanner.start()
@ -186,18 +197,15 @@ private object LazyJettyRun extends JettyRun
}
private def reload(server: Server, reconfigure: => Unit, log: Logger)
{
JettyRun.synchronized
{
log.info("Reloading web application...")
val handlers = wrapNull(server.getHandlers, server.getHandler)
log.debug("Stopping handlers: " + handlers.mkString(", "))
handlers.foreach(_.stop)
log.debug("Reconfiguring...")
reconfigure
log.debug("Restarting handlers: " + handlers.mkString(", "))
handlers.foreach(_.start)
log.info("Reload complete.")
}
log.info("Reloading web application...")
val handlers = wrapNull(server.getHandlers, server.getHandler)
log.debug("Stopping handlers: " + handlers.mkString(", "))
handlers.foreach(_.stop)
log.debug("Reconfiguring...")
reconfigure
log.debug("Restarting handlers: " + handlers.mkString(", "))
handlers.foreach(_.start)
log.info("Reload complete.")
}
private def wrapNull(a: Array[Handler], b: Handler) =
(a, b) match

View File

@ -105,9 +105,7 @@ private abstract class AbstractProcessBuilder extends ProcessBuilder with SinkPa
private[this] def runBuffered(log: Logger, connectInput: Boolean) =
{
val log2 = new BufferedLogger(log)
log2.startRecording()
try { run(log2, connectInput).exitValue() }
finally { log2.playAll(); log2.clearAll() }
log2.bufferAll { run(log2, connectInput).exitValue() }
}
def !(io: ProcessIO) = run(io).exitValue()

View File

@ -2,6 +2,7 @@
* Copyright 2009 Mark Harrah
*/
package sbt.impl
import sbt._
import scala.collection.{immutable, mutable}
import scala.collection.Map
@ -44,11 +45,7 @@ private final class RunTask(root: Task, rootName: String, maximumTasks: Int) ext
// it ignores the root task so that the root task may be run with buffering disabled so that the output
// occurs without delay.
private def runTasksExceptRoot() =
{
withBuffered(_.startRecording())
try { ParallelRunner.run(expandedRoot, expandedTaskName, runIfNotRoot, maximumTasks, (t: Task) => t.manager.log) }
finally { withBuffered(_.stop()) }
}
ParallelRunner.run(expandedRoot, expandedTaskName, runIfNotRoot, maximumTasks, (t: Task) => t.manager.log)
private def withBuffered(f: BufferedLogger => Unit)
{
for(buffered <- bufferedLoggers)
@ -69,38 +66,29 @@ private final class RunTask(root: Task, rootName: String, maximumTasks: Int) ext
val label = if(multiProject) (action.manager.name + " / " + actionName) else actionName
def banner(event: ControlEvent.Value, firstSeparator: String, secondSeparator: String) =
Control.trap(action.manager.log.control(event, firstSeparator + " " + label + " " + secondSeparator))
if(parallel)
val buffered = parallel && !isRoot(action)
if(buffered)
banner(ControlEvent.Start, "\n ", "...")
def doRun() =
{
try { banner(ControlEvent.Start, "\n ", "...") }
finally { flush(action) }
banner(ControlEvent.Header, "\n==", "==")
try { action.invoke }
catch { case e: Exception => action.manager.log.trace(e); Some(e.toString) }
finally { banner(ControlEvent.Finish, "==", "==") }
}
banner(ControlEvent.Header, "\n==", "==")
try { action.invoke }
catch { case e: Exception => action.manager.log.trace(e); Some(e.toString) }
finally
if(buffered)
bufferLogging(action, doRun())
else
doRun()
}
private def bufferLogging[T](action: Task, f: => T) =
bufferedLogger(action.manager) match
{
banner(ControlEvent.Finish, "==", "==")
if(parallel)
flush(action)
case Some(buffered) => buffered.buffer { f }
case None => f
}
}
private def trapFinally(toTrap: => Unit)(runFinally: => Unit)
{
try { toTrap }
catch { case e: Exception => () }
finally { try { runFinally } catch { case e: Exception => () } }
}
private def flush(action: Task)
{
for(buffered <- bufferedLogger(action.manager))
Control.trap(flush(buffered))
}
private def flush(buffered: BufferedLogger)
{
buffered.play()
buffered.clear()
}
/* Most of the following is for implicitly adding dependencies (see the expand method)*/
private val projectDependencyCache = identityMap[Project, Iterable[Project]]
private def dependencies(project: Project) = projectDependencyCache.getOrElseUpdate(project, project.topologicalSort.dropRight(1))