Add FileCacheEntry

Previously, we were leaking the internal details of incremental
compilation to users by defining FileTree(DataView|Repository)[Stamp].
To avoid this, I introduce the new class FileCacheEntry that is quite
similar to Stamp except defined using scala Options rather than java
Optionals. The implementation class just delegates to an actual Stamp
and I provided a private[sbt] ops class that adds a
method `stamp` to FileCacheEntry. This will usually just extract the
stamp from the implementation class. This allows us to use
FileCacheEntry almost interchangeably with Stamp while still avoiding
exposing users to Stamp.
This commit is contained in:
Ethan Atkins 2018-12-04 11:04:25 -08:00
parent ba0494df14
commit e8af828c73
11 changed files with 126 additions and 59 deletions

View File

@ -9,13 +9,11 @@ package sbt
import java.io.File
import sbt.internal.util.AttributeKey
import sbt.internal.inc.classpath.ClassLoaderCache
import sbt.internal.server.ServerHandler
import sbt.io.FileTreeDataView
import sbt.internal.util.AttributeKey
import sbt.librarymanagement.ModuleID
import sbt.util.Level
import xsbti.compile.analysis.Stamp
object BasicKeys {
val historyPath = AttributeKey[Option[File]](

View File

@ -7,11 +7,11 @@
package sbt
import sbt.Watched.WatchSource
import sbt.internal.FileCacheEntry
import sbt.internal.io.{ HybridPollingFileTreeRepository, WatchServiceBackedObservable, WatchState }
import sbt.io.FileTreeDataView.{ Observable, Observer }
import sbt.io._
import FileTreeDataView.{ Observable, Observer }
import sbt.util.Logger
import xsbti.compile.analysis.Stamp
import scala.concurrent.duration._
@ -19,15 +19,15 @@ import scala.concurrent.duration._
* Configuration for viewing and monitoring the file system.
*/
final class FileTreeViewConfig private (
val newDataView: () => FileTreeDataView[Stamp],
val newDataView: () => FileTreeDataView[FileCacheEntry],
val newMonitor: (
FileTreeDataView[Stamp],
FileTreeDataView[FileCacheEntry],
Seq[WatchSource],
Logger
) => FileEventMonitor[Stamp]
) => FileEventMonitor[FileCacheEntry]
)
object FileTreeViewConfig {
private implicit class RepositoryOps(val repository: FileTreeRepository[Stamp]) {
private implicit class RepositoryOps(val repository: FileTreeRepository[FileCacheEntry]) {
def register(sources: Seq[WatchSource]): Unit = sources foreach { s =>
repository.register(s.base.toPath, if (s.recursive) Integer.MAX_VALUE else 0)
}
@ -35,10 +35,10 @@ object FileTreeViewConfig {
/**
* Create a new FileTreeViewConfig. This factory takes a generic parameter, T, that is bounded
* by {{{sbt.io.FileTreeDataView[Stamp]}}}. The reason for this is to ensure that a
* by {{{sbt.io.FileTreeDataView[FileCacheEntry]}}}. The reason for this is to ensure that a
* sbt.io.FileTreeDataView that is instantiated by [[FileTreeViewConfig.newDataView]] can be
* passed into [[FileTreeViewConfig.newMonitor]] without constraining the type of view to be
* {{{sbt.io.FileTreeDataView[Stamp]}}}.
* {{{sbt.io.FileTreeDataView[FileCacheEntry]}}}.
* @param newDataView create a new sbt.io.FileTreeDataView. This value may be cached in a global
* attribute
* @param newMonitor create a new sbt.io.FileEventMonitor using the sbt.io.FileTreeDataView
@ -46,13 +46,13 @@ object FileTreeViewConfig {
* @tparam T the subtype of sbt.io.FileTreeDataView that is returned by [[FileTreeViewConfig.newDataView]]
* @return a [[FileTreeViewConfig]] instance.
*/
def apply[T <: FileTreeDataView[Stamp]](
def apply[T <: FileTreeDataView[FileCacheEntry]](
newDataView: () => T,
newMonitor: (T, Seq[WatchSource], Logger) => FileEventMonitor[Stamp]
newMonitor: (T, Seq[WatchSource], Logger) => FileEventMonitor[FileCacheEntry]
): FileTreeViewConfig =
new FileTreeViewConfig(
newDataView,
(view: FileTreeDataView[Stamp], sources: Seq[WatchSource], logger: Logger) =>
(view: FileTreeDataView[FileCacheEntry], sources: Seq[WatchSource], logger: Logger) =>
newMonitor(view.asInstanceOf[T], sources, logger)
)
@ -71,14 +71,14 @@ object FileTreeViewConfig {
antiEntropy: FiniteDuration
): FileTreeViewConfig =
FileTreeViewConfig(
() => FileTreeView.DEFAULT.asDataView(Stamped.converter),
(_: FileTreeDataView[Stamp], sources, logger) => {
() => FileTreeView.DEFAULT.asDataView(FileCacheEntry.default),
(_: FileTreeDataView[FileCacheEntry], sources, logger) => {
val ioLogger: sbt.io.WatchLogger = msg => logger.debug(msg.toString)
FileEventMonitor.antiEntropy(
new WatchServiceBackedObservable(
WatchState.empty(Watched.createWatchService(), sources),
delay,
Stamped.converter,
FileCacheEntry.default,
closeService = true,
ioLogger
),
@ -98,11 +98,15 @@ object FileTreeViewConfig {
*/
def default(antiEntropy: FiniteDuration): FileTreeViewConfig =
FileTreeViewConfig(
() => FileTreeRepository.default(Stamped.converter),
(repository: FileTreeRepository[Stamp], sources: Seq[WatchSource], logger: Logger) => {
() => FileTreeRepository.default(FileCacheEntry.default),
(
repository: FileTreeRepository[FileCacheEntry],
sources: Seq[WatchSource],
logger: Logger
) => {
repository.register(sources)
val copied = new Observable[Stamp] {
override def addObserver(observer: Observer[Stamp]): Int =
val copied = new Observable[FileCacheEntry] {
override def addObserver(observer: Observer[FileCacheEntry]): Int =
repository.addObserver(observer)
override def removeObserver(handle: Int): Unit = repository.removeObserver(handle)
override def close(): Unit = {} // Don't close the underlying observable
@ -155,9 +159,9 @@ object FileTreeViewConfig {
pollingInterval: FiniteDuration,
pollingSources: Seq[WatchSource],
): FileTreeViewConfig = FileTreeViewConfig(
() => FileTreeRepository.hybrid(Stamped.converter, pollingSources: _*),
() => FileTreeRepository.hybrid(FileCacheEntry.default, pollingSources: _*),
(
repository: HybridPollingFileTreeRepository[Stamp],
repository: HybridPollingFileTreeRepository[FileCacheEntry],
sources: Seq[WatchSource],
logger: Logger
) => {

View File

@ -10,6 +10,7 @@ package sbt
import java.io.{ File => JFile }
import java.nio.file.Path
import sbt.internal.FileCacheEntry
import sbt.internal.inc.Stamper
import sbt.io.TypedPath
import xsbti.compile.analysis.Stamp
@ -20,17 +21,17 @@ import xsbti.compile.analysis.Stamp
* performance anywhere where we need to check if files have changed before doing potentially
* expensive work.
*/
trait Stamped {
def stamp: Stamp
private[sbt] trait Stamped {
private[sbt] def stamp: Stamp
}
/**
* Provides converter functions from TypedPath to [[Stamped]].
*/
object Stamped {
private[sbt] object Stamped {
type File = JFile with Stamped with TypedPath
def file(typedPath: TypedPath, stamp: Stamp): JFile with Stamped with TypedPath =
new StampedFileImpl(typedPath, stamp)
def file(typedPath: TypedPath, entry: FileCacheEntry): JFile with Stamped with TypedPath =
new StampedFileImpl(typedPath, entry.stamp)
/**
* Converts a TypedPath instance to a [[Stamped]] by calculating the file hash.
@ -47,10 +48,13 @@ object Stamped {
* using the last modified time and all other files using the file hash.
*/
val converter: TypedPath => Stamp = (tp: TypedPath) =>
tp.toPath.toString match {
case s if s.endsWith(".jar") => binaryConverter(tp)
case s if s.endsWith(".class") => binaryConverter(tp)
case _ => sourceConverter(tp)
if (tp.isDirectory) binaryConverter(tp)
else {
tp.toPath.toString match {
case s if s.endsWith(".jar") => binaryConverter(tp)
case s if s.endsWith(".class") => binaryConverter(tp)
case _ => sourceConverter(tp)
}
}
/**

View File

@ -18,15 +18,14 @@ import sbt.BasicCommandStrings.{
}
import sbt.BasicCommands.otherCommandParser
import sbt.internal.LabeledFunctions._
import sbt.internal.LegacyWatched
import sbt.internal.io.{ EventMonitor, Source, WatchState }
import sbt.internal.util.Types.const
import sbt.internal.util.complete.{ DefaultParsers, Parser }
import sbt.internal.util.{ AttributeKey, JLine }
import sbt.internal.{ FileCacheEntry, LegacyWatched }
import sbt.io.FileEventMonitor.{ Creation, Deletion, Event, Update }
import sbt.io._
import sbt.util.{ Level, Logger }
import xsbti.compile.analysis.Stamp
import scala.annotation.tailrec
import scala.concurrent.duration._
@ -147,7 +146,7 @@ object Watched {
private[sbt] def onEvent(
sources: Seq[WatchSource],
projectSources: Seq[WatchSource]
): Event[Stamp] => Watched.Action =
): Event[FileCacheEntry] => Watched.Action =
event =>
if (sources.exists(_.accept(event.entry.typedPath.toPath))) Watched.Trigger
else if (projectSources.exists(_.accept(event.entry.typedPath.toPath))) event match {
@ -459,7 +458,7 @@ trait WatchConfig {
*
* @return an sbt.io.FileEventMonitor instance.
*/
def fileEventMonitor: FileEventMonitor[Stamp]
def fileEventMonitor: FileEventMonitor[FileCacheEntry]
/**
* A function that is periodically invoked to determine whether the watch should stop or
@ -482,7 +481,7 @@ trait WatchConfig {
* @param event the detected sbt.io.FileEventMonitor.Event.
* @return the next [[Watched.Action Action]] to run.
*/
def onWatchEvent(event: Event[Stamp]): Watched.Action
def onWatchEvent(event: Event[FileCacheEntry]): Watched.Action
/**
* Transforms the state after the watch terminates.
@ -538,10 +537,10 @@ object WatchConfig {
*/
def default(
logger: Logger,
fileEventMonitor: FileEventMonitor[Stamp],
fileEventMonitor: FileEventMonitor[FileCacheEntry],
handleInput: InputStream => Watched.Action,
preWatch: (Int, Boolean) => Watched.Action,
onWatchEvent: Event[Stamp] => Watched.Action,
onWatchEvent: Event[FileCacheEntry] => Watched.Action,
onWatchTerminated: (Watched.Action, String, State) => State,
triggeredMessage: (TypedPath, Int) => Option[String],
watchingMessage: Int => Option[String]
@ -556,11 +555,11 @@ object WatchConfig {
val wm = watchingMessage
new WatchConfig {
override def logger: Logger = l
override def fileEventMonitor: FileEventMonitor[Stamp] = fem
override def fileEventMonitor: FileEventMonitor[FileCacheEntry] = fem
override def handleInput(inputStream: InputStream): Watched.Action = hi(inputStream)
override def preWatch(count: Int, lastResult: Boolean): Watched.Action =
pw(count, lastResult)
override def onWatchEvent(event: Event[Stamp]): Watched.Action = owe(event)
override def onWatchEvent(event: Event[FileCacheEntry]): Watched.Action = owe(event)
override def onWatchTerminated(action: Watched.Action, command: String, state: State): State =
owt(action, command, state)
override def triggeredMessage(typedPath: TypedPath, count: Int): Option[String] =

View File

@ -0,0 +1,62 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
import java.lang
import java.util.Optional
import sbt.internal.inc.{ EmptyStamp, LastModified, Stamp }
import sbt.io.TypedPath
import xsbti.compile.analysis.{ Stamp => XStamp }
/**
* Represents a cache entry for a FileTreeRepository. It can be extended to add user defined
* data to the FileTreeRepository cache.
*/
trait FileCacheEntry {
def hash: Option[String]
def lastModified: Option[Long]
}
object FileCacheEntry {
def default(typedPath: TypedPath): FileCacheEntry =
DelegateFileCacheEntry(Stamped.converter(typedPath))
private[sbt] implicit class FileCacheEntryOps(val e: FileCacheEntry) extends AnyVal {
private[sbt] def stamp: XStamp = e match {
case DelegateFileCacheEntry(s) => s
case _ =>
e.hash
.map(Stamp.fromString)
.orElse(e.lastModified.map(new LastModified(_)))
.getOrElse(EmptyStamp)
}
}
private case class DelegateFileCacheEntry(private val stamp: XStamp)
extends FileCacheEntry
with XStamp {
override def getValueId: Int = stamp.getValueId
override def writeStamp(): String = stamp.writeStamp()
override def getHash: Optional[String] = stamp.getHash
override def getLastModified: Optional[lang.Long] = stamp.getLastModified
override def hash: Option[String] = getHash match {
case h if h.isPresent => Some(h.get)
case _ => None
}
override def lastModified: Option[Long] = getLastModified match {
case l if l.isPresent => Some(l.get)
case _ => None
}
override def equals(o: Any): Boolean = o match {
case that: DelegateFileCacheEntry => this.stamp == that.stamp
case that: XStamp => this.stamp == that
case _ => false
}
override def hashCode: Int = stamp.hashCode
override def toString: String = s"FileCacheEntry(hash = $hash, lastModified = $lastModified)"
}
}

View File

@ -14,10 +14,10 @@ import java.util.concurrent.atomic.AtomicBoolean
import org.scalatest.{ FlatSpec, Matchers }
import sbt.Watched._
import sbt.WatchedSpec._
import sbt.internal.FileCacheEntry
import sbt.io.FileEventMonitor.Event
import sbt.io.{ FileEventMonitor, IO, TypedPath }
import sbt.util.Logger
import xsbti.compile.analysis.Stamp
import scala.collection.mutable
import scala.concurrent.duration._
@ -27,11 +27,11 @@ class WatchedSpec extends FlatSpec with Matchers {
private val fileTreeViewConfig = FileTreeViewConfig.default(50.millis)
def config(
sources: Seq[WatchSource],
fileEventMonitor: Option[FileEventMonitor[Stamp]] = None,
fileEventMonitor: Option[FileEventMonitor[FileCacheEntry]] = None,
logger: Logger = NullLogger,
handleInput: InputStream => Action = _ => Ignore,
preWatch: (Int, Boolean) => Action = (_, _) => CancelWatch,
onWatchEvent: Event[Stamp] => Action = _ => Ignore,
onWatchEvent: Event[FileCacheEntry] => Action = _ => Ignore,
triggeredMessage: (TypedPath, Int) => Option[String] = (_, _) => None,
watchingMessage: Int => Option[String] = _ => None
): WatchConfig = {

View File

@ -274,7 +274,7 @@ object Defaults extends BuildCommon {
fileTreeViewConfig := FileManagement.defaultFileTreeView.value,
fileTreeView := state.value
.get(Keys.globalFileTreeView)
.getOrElse(FileTreeView.DEFAULT.asDataView(Stamped.converter)),
.getOrElse(FileTreeView.DEFAULT.asDataView(FileCacheEntry.default)),
externalHooks := {
val view = fileTreeView.value
compileOptions =>

View File

@ -31,7 +31,6 @@ import sbt.librarymanagement.ivy.{ Credentials, IvyConfiguration, IvyPaths, Upda
import sbt.testing.Framework
import sbt.util.{ Level, Logger }
import xsbti.compile._
import xsbti.compile.analysis.Stamp
import scala.concurrent.duration.{ Duration, FiniteDuration }
import scala.xml.{ NodeSeq, Node => XNode }
@ -94,14 +93,14 @@ object Keys {
@deprecated("This is no longer used for continuous execution", "1.3.0")
val watch = SettingKey(BasicKeys.watch)
val suppressSbtShellNotification = settingKey[Boolean]("""True to suppress the "Executing in batch mode.." message.""").withRank(CSetting)
val fileTreeView = taskKey[FileTreeDataView[Stamp]]("A view of the file system")
val fileTreeView = taskKey[FileTreeDataView[FileCacheEntry]]("A view of the file system")
val pollInterval = settingKey[FiniteDuration]("Interval between checks for modified sources by the continuous execution command.").withRank(BMinusSetting)
val pollingDirectories = settingKey[Seq[Watched.WatchSource]]("Directories that cannot be cached and must always be rescanned. Typically these will be NFS mounted or something similar.").withRank(DSetting)
val watchAntiEntropy = settingKey[FiniteDuration]("Duration for which the watch EventMonitor will ignore events for a file after that file has triggered a build.").withRank(BMinusSetting)
val watchConfig = taskKey[WatchConfig]("The configuration for continuous execution.").withRank(BMinusSetting)
val watchLogger = taskKey[Logger]("A logger that reports watch events.").withRank(DSetting)
val watchHandleInput = settingKey[InputStream => Watched.Action]("Function that is periodically invoked to determine if the continous build should be stopped or if a build should be triggered. It will usually read from stdin to respond to user commands.").withRank(BMinusSetting)
val watchOnEvent = taskKey[Event[Stamp] => Watched.Action]("Determines how to handle a file event").withRank(BMinusSetting)
val watchOnEvent = taskKey[Event[FileCacheEntry] => Watched.Action]("Determines how to handle a file event").withRank(BMinusSetting)
val watchOnTermination = taskKey[(Watched.Action, String, State) => State]("Transforms the input state after the continuous build completes.").withRank(BMinusSetting)
val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting)
val watchProjectSources = taskKey[Seq[Watched.WatchSource]]("Defines the sources for the sbt meta project to watch to trigger a reload.").withRank(CSetting)
@ -457,7 +456,7 @@ object Keys {
val (executionRoots, dummyRoots) = Def.dummy[Seq[ScopedKey[_]]]("executionRoots", "The list of root tasks for this task execution. Roots are the top-level tasks that were directly requested to be run.")
val state = Def.stateKey
val streamsManager = Def.streamsManagerKey
private[sbt] val globalFileTreeView = AttributeKey[FileTreeDataView[Stamp]](
private[sbt] val globalFileTreeView = AttributeKey[FileTreeDataView[FileCacheEntry]](
"globalFileTreeView",
"Provides a view into the file system that may or may not cache the tree in memory",
1000

View File

@ -25,7 +25,6 @@ import sbt.io.syntax._
import sbt.io.{ FileTreeDataView, IO }
import sbt.util.{ Level, Logger, Show }
import xsbti.compile.CompilerCache
import xsbti.compile.analysis.Stamp
import scala.annotation.tailrec
import scala.concurrent.ExecutionContext
@ -863,7 +862,7 @@ object BuiltinCommands {
()
}
val (_, config: FileTreeViewConfig) = extracted.runTask(Keys.fileTreeViewConfig, s)
val view: FileTreeDataView[Stamp] = config.newDataView()
val view: FileTreeDataView[FileCacheEntry] = config.newDataView()
val newState = s.addExitHook(cleanup())
cleanup()
newState

View File

@ -20,7 +20,10 @@ import scala.collection.mutable
private[sbt] object ExternalHooks {
private val javaHome = Option(System.getProperty("java.home")).map(Paths.get(_))
def apply(options: CompileOptions, view: FileTreeDataView[Stamp]): DefaultExternalHooks = {
def apply(
options: CompileOptions,
view: FileTreeDataView[FileCacheEntry]
): DefaultExternalHooks = {
import scala.collection.JavaConverters._
val sources = options.sources()
val cachedSources = new java.util.HashMap[File, Stamp]
@ -30,7 +33,7 @@ private[sbt] object ExternalHooks {
case f: File => cachedSources.put(f, converter(f))
}
view match {
case r: FileTreeRepository[Stamp] =>
case r: FileTreeRepository[FileCacheEntry] =>
r.register(options.classesDirectory.toPath, Integer.MAX_VALUE)
options.classpath.foreach { f =>
r.register(f.toPath, Integer.MAX_VALUE)
@ -41,7 +44,7 @@ private[sbt] object ExternalHooks {
options.classpath.foreach { f =>
view.listEntries(f.toPath, Integer.MAX_VALUE, _ => true) foreach { e =>
e.value match {
case Right(value) => allBinaries.put(e.typedPath.toPath.toFile, value)
case Right(value) => allBinaries.put(e.typedPath.toPath.toFile, value.stamp)
case _ =>
}
}
@ -49,7 +52,7 @@ private[sbt] object ExternalHooks {
// rather than a directory.
view.listEntries(f.toPath, -1, _ => true) foreach { e =>
e.value match {
case Right(value) => allBinaries.put(e.typedPath.toPath.toFile, value)
case Right(value) => allBinaries.put(e.typedPath.toPath.toFile, value.stamp)
case _ =>
}
}

View File

@ -10,13 +10,12 @@ package sbt.internal
import java.io.IOException
import java.nio.file.Path
import sbt.BasicCommandStrings.ContinuousExecutePrefix
import sbt.Keys._
import sbt._
import sbt.io.FileTreeDataView.Entry
import sbt.io.syntax.File
import sbt.io.{ FileFilter, FileTreeDataView, FileTreeRepository }
import sbt._
import BasicCommandStrings.ContinuousExecutePrefix
import xsbti.compile.analysis.Stamp
private[sbt] object FileManagement {
private[sbt] def defaultFileTreeView: Def.Initialize[Task[FileTreeViewConfig]] = Def.task {
@ -52,7 +51,7 @@ private[sbt] object FileManagement {
val view = fileTreeView.value
val include = filter.toTask.value
val ex = excludes.toTask.value
val sourceFilter: Entry[Stamp] => Boolean = (entry: Entry[Stamp]) => {
val sourceFilter: Entry[FileCacheEntry] => Boolean = (entry: Entry[FileCacheEntry]) => {
val typedPath = entry.typedPath
val file = new java.io.File(typedPath.toPath.toString) {
override def isDirectory: Boolean = typedPath.isDirectory