sbt/tasks/standard/Compile.scala

171 lines
8.9 KiB
Scala

/* sbt -- Simple Build Tool
* Copyright 2009, 2010 Mark Harrah
*/
package xsbt
import java.io.File
import xsbt.api.{APIFormat, SameAPI}
import xsbti.api.Source
trait CompileImpl[R]
{
def apply(sourceChanges: ChangeReport[File], classpathChanges: ChangeReport[File], outputDirectory: File, options: Seq[String]): Task[R]
def tracked: Seq[Tracked]
}
final class Compile[R](val cacheDirectory: File, val sources: Task[Set[File]], val classpath: Task[Set[File]],
val outputDirectory: Task[File], val options: Task[Seq[String]], compileImpl: CompileImpl[R]) extends TrackedTaskDefinition[R]
{
val trackedClasspath = Difference.inputs(classpath, FilesInfo.lastModified, cacheFile("classpath"))
val trackedSource = Difference.inputs(sources, FilesInfo.hash, cacheFile("sources"))
val trackedOptions =
{
import Cache._
import Task._
new Changed((outputDirectory, options) map ( "-d" :: _.getAbsolutePath :: _.toList), cacheFile("options"))
}
val task =
trackedClasspath { rawClasspathChanges => // detect changes to the classpath (last modified only)
trackedSource { rawSourceChanges => // detect changes to sources (hash only)
val newOpts = (opts: Seq[String]) => (opts, rawSourceChanges.markAllModified, rawClasspathChanges.markAllModified) // if options changed, mark everything changed
val sameOpts = (opts: Seq[String]) => (opts, rawSourceChanges, rawClasspathChanges)
trackedOptions(newOpts, sameOpts) bind { // detect changes to options
case (options, sourceChanges, classpathChanges) =>
outputDirectory bind { outDir =>
FileUtilities.createDirectory(outDir)
compileImpl(sourceChanges, classpathChanges, outDir, options)
}
}
}
} dependsOn(sources, classpath, options, outputDirectory)// raise these dependencies to the top for parallelism
lazy val tracked = Seq(trackedClasspath, trackedSource, trackedOptions) ++ compileImpl.tracked
}
object AggressiveCompile
{
def apply(sources: Task[Set[File]], classpath: Task[Set[File]], outputDirectory: Task[File], options: Task[Seq[String]],
cacheDirectory: File, compilerTask: Task[AnalyzingCompiler], log: CompileLogger): Compile[Set[File]] =
{
val implCache = new File(cacheDirectory, "deps")
val baseCache = new File(cacheDirectory, "inputs")
val impl = new AggressiveCompile(implCache, compilerTask, log)
new Compile(baseCache, sources, classpath, outputDirectory, options, impl)
}
}
class AggressiveCompile(val cacheDirectory: File, val compilerTask: Task[AnalyzingCompiler], val log: CompileLogger) extends CompileImpl[Set[File]]
{
def apply(sourceChanges: ChangeReport[File], classpathChanges: ChangeReport[File], outputDirectory: File, options: Seq[String]): Task[Set[File]] =
compilerTask bind { compiler =>
tracking { tracker =>
timestamp { tstamp =>
Task {
log.info("Removed sources: \n\t" + sourceChanges.removed.mkString("\n\t"))
log.info("Added sources: \n\t" + sourceChanges.added.mkString("\n\t"))
log.info("Modified sources: \n\t" + (sourceChanges.modified -- sourceChanges.added -- sourceChanges.removed).mkString("\n\t"))
val classpath = classpathChanges.checked
val readTracker = tracker.read
// directories that are no longer on the classpath, not necessarily removed from the filesystem
val removedDirectories = classpathChanges.removed.filter(_.isDirectory)
log.info("Directories no longer on classpath:\n\t" + removedDirectories.mkString("\n\t"))
def uptodate(time: Long, files: Iterable[File]) = files.forall(_.lastModified < time)
def isOutofdate(file: File, related: => Iterable[File]) = !file.exists || !uptodate(file.lastModified, related)
def invalidatesUses(file: File) = !file.exists || file.lastModified > tstamp
def isProductOutofdate(product: File) = isOutofdate(product, readTracker.sources(product))
def inRemovedDirectory(file: File) = removedDirectories.exists(dir => FileUtilities.relativize(dir, file).isDefined)
def isUsedOutofdate(file: File) = classpathChanges.modified(file) || inRemovedDirectory(file) || invalidatesUses(file)
// these are products that no longer exist or are older than the sources that produced them
val outofdateProducts = readTracker.allProducts.filter(isProductOutofdate)
log.info("Out of date products:\n\t" + outofdateProducts.mkString("\n\t"))
// used classes and jars that a) no longer exist b) are no longer on the classpath or c) are newer than the sources that use them
val outofdateUses = readTracker.allUsed.filter(isUsedOutofdate)
log.info("Out of date binaries:\n\t" + outofdateUses.mkString("\n\t"))
val modifiedSources = sourceChanges.modified
val invalidatedByClasspath = outofdateUses.flatMap(readTracker.usedBy)
log.info("Invalidated by classpath changes:\n\t" + invalidatedByClasspath.mkString("\n\t"))
val invalidatedByRemovedSrc = sourceChanges.removed.flatMap(readTracker.dependsOn)
log.info("Invalidated by removed sources:\n\t" + invalidatedByRemovedSrc.mkString("\n\t"))
val productsOutofdate = outofdateProducts.flatMap(readTracker.sources)
log.info("Invalidated by out of date products:\n\t" + productsOutofdate.mkString("\n\t"))
val rawInvalidatedSources = modifiedSources ++ invalidatedByClasspath ++ invalidatedByRemovedSrc ++ productsOutofdate
val invalidatedSources = scc(readTracker, rawInvalidatedSources)
val sources = invalidatedSources.filter(_.exists)
val previousAPIMap = Map() ++ sources.map { src => (src, APIFormat.read(readTracker.tag(src))) }
val invalidatedProducts = outofdateProducts ++ products(readTracker, invalidatedSources)
val transitiveIfNeeded = InvalidateTransitive(tracker, sources)
tracker.removeAll(invalidatedProducts ++ classpathChanges.modified ++ (invalidatedSources -- sources))
tracker.pending(sources)
FileUtilities.delete(invalidatedProducts)
log.info("All initially invalidated sources:\n\t" + sources.mkString("\n\t"))
if(!sources.isEmpty)
{
val newAPIMap = doCompile(sources, classpath, outputDirectory, options, tracker, compiler, log)
val apiChanged = sources filter { src => !sameAPI(previousAPIMap, newAPIMap, src) }
log.info("Sources with API changes:\n\t" + apiChanged.mkString("\n\t"))
val finalAPIMap =
// if either nothing changed or everything was recompiled, stop here
if(apiChanged.isEmpty || sources.size == sourceChanges.checked.size) newAPIMap
else
{
//val changedNames = TopLevel.nameChanges(newAPIMap.values, previousAPIMap.values)
InvalidateTransitive.clean(tracker, FileUtilities.delete, transitiveIfNeeded)
val sources = transitiveIfNeeded.invalid ** sourceChanges.checked
log.info("All sources invalidated by API changes:\n\t" + sources.mkString("\n\t"))
doCompile(sources, classpath, outputDirectory, options, tracker, compiler, log)
}
finalAPIMap.foreach { case (src, api) => tracker.tag(src, APIFormat.write(api)) }
}
Set() ++ tracker.read.allProducts
}
}}
}
def products(tracker: ReadTracking[File], srcs: Set[File]): Set[File] = srcs.flatMap(tracker.products)
def doCompile(sources: Set[File], classpath: Set[File], outputDirectory: File, options: Seq[String], tracker: UpdateTracking[File], compiler: AnalyzingCompiler, log: CompileLogger): scala.collection.Map[File, Source] =
{
val callback = new APIAnalysisCallback(tracker)
log.debug("Compiling using compiler " + compiler)
compiler(sources, classpath, outputDirectory, options, callback, 100, log)
callback.apiMap
}
import sbinary.DefaultProtocol.FileFormat
val tracking = new DependencyTracked(cacheDirectory, true, (files: File) => FileUtilities.delete(files))
val timestamp = new Timestamp(new File(cacheDirectory,"timestamp"))
def tracked = Seq(tracking, timestamp)
def sameAPI[T](a: scala.collection.Map[T, Source], b: scala.collection.Map[T, Source], t: T): Boolean = sameAPI(a.get(t), b.get(t))
def sameAPI(a: Option[Source], b: Option[Source]): Boolean =
if(a.isEmpty) b.isEmpty else (b.isDefined && SameAPI(a.get, b.get))
// TODO: implement
def scc(readTracker: ReadTracking[File], sources: Set[File]) = sources
}
private final class APIAnalysisCallback(tracking: UpdateTracking[File]) extends xsbti.AnalysisCallback
{
val apiMap = new scala.collection.mutable.HashMap[File, Source]
def sourceDependency(dependsOn: File, source: File) { tracking.dependency(source, dependsOn) }
def jarDependency(jar: File, source: File) { tracking.use(source, jar) }
def classDependency(clazz: File, source: File) { tracking.dependency(source, clazz) }
def generatedClass(source: File, clazz: File) { tracking.product(source, clazz) }
def api(source: File, api: xsbti.api.Source) { apiMap(source) = api }
def superclassNames = Array()
def superclassNotFound(superclassName: String) {}
def beginSource(source: File) {}
def endSource(source: File) {}
def foundApplication(source: File, className: String) {}
def foundSubclass(source: File, subclassName: String, superclassName: String, isModule: Boolean) {}
}