/* sbt -- Simple Build Tool * Copyright 2009, 2010 Mark Harrah */ package sbt import java.io.File import CacheIO.{fromFile, toFile} import sbinary.Format import scala.reflect.Manifest import scala.collection.mutable import IO.{delete, read, write} object Tracked { /** Creates a tracker that provides the last time it was evaluated. * If 'useStartTime' is true, the recorded time is the start of the evaluated function. * If 'useStartTime' is false, the recorded time is when the evaluated function completes. * In both cases, the timestamp is not updated if the function throws an exception.*/ def tstamp(cacheFile: File, useStartTime: Boolean = true): Timestamp = new Timestamp(cacheFile, useStartTime) /** Creates a tracker that only evaluates a function when the input has changed.*/ //def changed[O](cacheFile: File)(implicit format: Format[O], equiv: Equiv[O]): Changed[O] = // new Changed[O](cacheFile) /** Creates a tracker that provides the difference between a set of input files for successive invocations.*/ def diffInputs(cache: File, style: FilesInfo.Style): Difference = Difference.inputs(cache, style) /** Creates a tracker that provides the difference between a set of output files for successive invocations.*/ def diffOutputs(cache: File, style: FilesInfo.Style): Difference = Difference.outputs(cache, style) def lastOutput[I,O](cacheFile: File)(f: (I,Option[O]) => O)(implicit o: Format[O], mf: Manifest[Format[O]]): I => O = in => { val previous: Option[O] = fromFile[O](cacheFile) val next = f(in, previous) toFile(next)(cacheFile) next } def inputChanged[I,O](cacheFile: File)(f: (Boolean, I) => O)(implicit ic: InputCache[I]): I => O = in => { val help = new CacheHelp(ic) val conv = help.convert(in) val changed = help.changed(cacheFile, conv) val result = f(changed, in) if(changed) help.save(cacheFile, conv) result } def outputChanged[I,O](cacheFile: File)(f: (Boolean, I) => O)(implicit ic: InputCache[I]): (() => I) => O = in => { val initial = in() val help = new CacheHelp(ic) val changed = help.changed(cacheFile, help.convert(initial)) val result = f(changed, initial) if(changed) help.save(cacheFile, help.convert(in())) result } final class CacheHelp[I](val ic: InputCache[I]) { def convert(i: I): ic.Internal = ic.convert(i) def save(cacheFile: File, value: ic.Internal): Unit = Using.fileOutputStream()(cacheFile)(out => ic.write(out, value) ) def changed(cacheFile: File, converted: ic.Internal): Boolean = try { val prev = Using.fileInputStream(cacheFile)(x => ic.read(x)) !ic.equiv.equiv(converted, prev) } catch { case e: Exception => true } } } trait Tracked { /** Cleans outputs and clears the cache.*/ def clean: Unit } class Timestamp(val cacheFile: File, useStartTime: Boolean) extends Tracked { def clean = delete(cacheFile) /** Reads the previous timestamp, evaluates the provided function, * and then updates the timestamp if the function completes normally.*/ def apply[T](f: Long => T): T = { val start = now() val result = f(readTimestamp) write(cacheFile, (if(useStartTime) start else now()).toString) result } private def now() = System.currentTimeMillis def readTimestamp: Long = try { read(cacheFile).toLong } catch { case _: NumberFormatException | _: java.io.FileNotFoundException => 0 } } class Changed[O](val cacheFile: File)(implicit equiv: Equiv[O], format: Format[O]) extends Tracked { def clean = delete(cacheFile) def apply[O2](ifChanged: O => O2, ifUnchanged: O => O2): O => O2 = value => { if(uptodate(value)) ifUnchanged(value) else { update(value) ifChanged(value) } } def update(value: O): Unit = Using.fileOutputStream(false)(cacheFile)(stream => format.writes(stream, value)) def uptodate(value: O): Boolean = try { Using.fileInputStream(cacheFile) { stream => equiv.equiv(value, format.reads(stream)) } } catch { case _: Exception => false } } object Difference { def constructor(defineClean: Boolean, filesAreOutputs: Boolean): (File, FilesInfo.Style) => Difference = (cache, style) => new Difference(cache, style, defineClean, filesAreOutputs) /** Provides a constructor for a Difference that removes the files from the previous run on a call to 'clean' and saves the * hash/last modified time of the files as they are after running the function. This means that this information must be evaluated twice: * before and after running the function.*/ val outputs = constructor(true, true) /** Provides a constructor for a Difference that does nothing on a call to 'clean' and saves the * hash/last modified time of the files as they were prior to running the function.*/ val inputs = constructor(false, false) } class Difference(val cache: File, val style: FilesInfo.Style, val defineClean: Boolean, val filesAreOutputs: Boolean) extends Tracked { def clean = { if(defineClean) delete(raw(cachedFilesInfo)) else () clearCache() } private def clearCache() = delete(cache) private def cachedFilesInfo = fromFile(style.formats, style.empty)(cache)(style.manifest).files private def raw(fs: Set[style.F]): Set[File] = fs.map(_.file) def apply[T](files: Set[File])(f: ChangeReport[File] => T): T = { val lastFilesInfo = cachedFilesInfo apply(files, lastFilesInfo)(f)(_ => files) } def apply[T](f: ChangeReport[File] => T)(implicit toFiles: T => Set[File]): T = { val lastFilesInfo = cachedFilesInfo apply(raw(lastFilesInfo), lastFilesInfo)(f)(toFiles) } private def abs(files: Set[File]) = files.map(_.getAbsoluteFile) private[this] def apply[T](files: Set[File], lastFilesInfo: Set[style.F])(f: ChangeReport[File] => T)(extractFiles: T => Set[File]): T = { val lastFiles = raw(lastFilesInfo) val currentFiles = abs(files) val currentFilesInfo = style(currentFiles) val report = new ChangeReport[File] { lazy val checked = currentFiles lazy val removed = lastFiles -- checked // all files that were included previously but not this time. This is independent of whether the files exist. lazy val added = checked -- lastFiles // all files included now but not previously. This is independent of whether the files exist. lazy val modified = raw(lastFilesInfo -- currentFilesInfo.files) ++ added lazy val unmodified = checked -- modified } val result = f(report) val info = if(filesAreOutputs) style(abs(extractFiles(result))) else currentFilesInfo toFile(style.formats)(info)(cache)(style.manifest) result } } object FileFunction { type UpdateFunction = (ChangeReport[File], ChangeReport[File]) => Set[File] def cached(cacheBaseDirectory: File, inStyle: FilesInfo.Style = FilesInfo.lastModified, outStyle: FilesInfo.Style = FilesInfo.exists)(action: Set[File] => Set[File]): Set[File] => Set[File] = cached(cacheBaseDirectory)(inStyle, outStyle)( (in, out) => action(in.checked) ) def cached(cacheBaseDirectory: File)(inStyle: FilesInfo.Style, outStyle: FilesInfo.Style)(action: UpdateFunction): Set[File] => Set[File] = { import Path._ lazy val inCache = Difference.inputs(cacheBaseDirectory / "in-cache", inStyle) lazy val outCache = Difference.outputs(cacheBaseDirectory / "out-cache", outStyle) inputs => { inCache(inputs) { inReport => outCache { outReport => if(inReport.modified.isEmpty && outReport.modified.isEmpty) outReport.checked else action(inReport, outReport) } } } } }