diff --git a/main-command/src/main/scala/sbt/FileTreeViewConfig.scala b/main-command/src/main/scala/sbt/FileTreeViewConfig.scala index 9fe1771d4..4b1179bfe 100644 --- a/main-command/src/main/scala/sbt/FileTreeViewConfig.scala +++ b/main-command/src/main/scala/sbt/FileTreeViewConfig.scala @@ -8,10 +8,11 @@ package sbt import sbt.Watched.WatchSource import sbt.internal.io.{ WatchServiceBackedObservable, WatchState } -import sbt.io.{ FileEventMonitor, FileTreeDataView, FileTreeView } +import sbt.io._ +import FileTreeDataView.{ Observable, Observer } import sbt.util.Logger -import scala.concurrent.duration.FiniteDuration +import scala.concurrent.duration._ /** * Configuration for viewing and monitoring the file system. @@ -25,6 +26,11 @@ final class FileTreeViewConfig private ( ) => FileEventMonitor[StampedFile] ) object FileTreeViewConfig { + private implicit class RepositoryOps(val repository: FileTreeRepository[StampedFile]) { + def register(sources: Seq[WatchSource]): Unit = sources foreach { s => + repository.register(s.base.toPath, if (s.recursive) Integer.MAX_VALUE else 0) + } + } /** * Create a new FileTreeViewConfig. This factory takes a generic parameter, T, that is bounded @@ -50,14 +56,19 @@ object FileTreeViewConfig { ) /** - * Provides a default [[FileTreeViewConfig]]. This view does not cache entries. - * @param pollingInterval the maximum duration that the sbt.internal.io.EventMonitor will poll - * the underlying sbt.io.WatchService when monitoring for file events + * Provides a [[FileTreeViewConfig]] with semantics as close as possible to sbt 1.2.0. This means + * that there is no file tree caching and the sbt.io.FileEventMonitor will use an + * sbt.io.WatchService for monitoring the file system. + * @param delay the maximum delay for which the background thread will poll the + * sbt.io.WatchService for file system events * @param antiEntropy the duration of the period after a path triggers a build for which it is * quarantined from triggering another build * @return a [[FileTreeViewConfig]] instance. */ - def default(pollingInterval: FiniteDuration, antiEntropy: FiniteDuration): FileTreeViewConfig = + def sbt1_2_compat( + delay: FiniteDuration, + antiEntropy: FiniteDuration + ): FileTreeViewConfig = FileTreeViewConfig( () => FileTreeView.DEFAULT.asDataView(StampedFile.converter), (_: FileTreeDataView[StampedFile], sources, logger) => { @@ -65,7 +76,7 @@ object FileTreeViewConfig { FileEventMonitor.antiEntropy( new WatchServiceBackedObservable( WatchState.empty(Watched.createWatchService(), sources), - pollingInterval, + delay, StampedFile.converter, closeService = true, ioLogger @@ -75,4 +86,27 @@ object FileTreeViewConfig { ) } ) + + /** + * Provides a default [[FileTreeViewConfig]]. This view caches entries and solely relies on + * file system events from the operating system to update its internal representation of the + * file tree. + * @param antiEntropy the duration of the period after a path triggers a build for which it is + * quarantined from triggering another build + * @return a [[FileTreeViewConfig]] instance. + */ + def default(antiEntropy: FiniteDuration): FileTreeViewConfig = + FileTreeViewConfig( + () => FileTreeRepository.default(StampedFile.converter), + (repository: FileTreeRepository[StampedFile], sources: Seq[WatchSource], logger: Logger) => { + repository.register(sources) + val copied = new Observable[StampedFile] { + override def addObserver(observer: Observer[StampedFile]): Int = + repository.addObserver(observer) + override def removeObserver(handle: Int): Unit = repository.removeObserver(handle) + override def close(): Unit = {} // Don't close the underlying observable + } + FileEventMonitor.antiEntropy(copied, antiEntropy, msg => logger.debug(msg.toString)) + } + ) } diff --git a/main-command/src/test/scala/sbt/WatchedSpec.scala b/main-command/src/test/scala/sbt/WatchedSpec.scala index 3babca877..9d66b8f84 100644 --- a/main-command/src/test/scala/sbt/WatchedSpec.scala +++ b/main-command/src/test/scala/sbt/WatchedSpec.scala @@ -23,7 +23,7 @@ import scala.concurrent.duration._ class WatchedSpec extends FlatSpec with Matchers { object Defaults { - private val fileTreeViewConfig = FileTreeViewConfig.default(50.millis, 50.millis) + private val fileTreeViewConfig = FileTreeViewConfig.default(50.millis) def config( sources: Seq[WatchSource], fileEventMonitor: Option[FileEventMonitor[StampedFile]] = None, diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 393328a40..1aa5bb35f 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -271,7 +271,7 @@ object Defaults extends BuildCommon { None }, watchStartMessage := Watched.defaultStartWatch, - fileTreeViewConfig := FileTreeViewConfig.default(pollInterval.value, watchAntiEntropy.value), + fileTreeViewConfig := FileTreeViewConfig.default(watchAntiEntropy.value), fileTreeView := state.value .get(BasicKeys.globalFileTreeView) .getOrElse(FileTreeView.DEFAULT.asDataView(StampedFile.converter)), @@ -363,11 +363,13 @@ object Defaults extends BuildCommon { crossPaths.value ) }, - unmanagedSources := collectFiles( - unmanagedSourceDirectories, - includeFilter in unmanagedSources, - excludeFilter in unmanagedSources - ).value, + unmanagedSources := FileManagement + .collectFiles( + unmanagedSourceDirectories, + includeFilter in unmanagedSources, + excludeFilter in unmanagedSources + ) + .value, watchSources in ConfigGlobal := (watchSources in ConfigGlobal).value ++ { val baseDir = baseDirectory.value val bases = unmanagedSourceDirectories.value @@ -412,11 +414,13 @@ object Defaults extends BuildCommon { resourceDirectories := Classpaths .concatSettings(unmanagedResourceDirectories, managedResourceDirectories) .value, - unmanagedResources := collectFiles( - unmanagedResourceDirectories, - includeFilter in unmanagedResources, - excludeFilter in unmanagedResources - ).value, + unmanagedResources := FileManagement + .collectFiles( + unmanagedResourceDirectories, + includeFilter in unmanagedResources, + excludeFilter in unmanagedResources + ) + .value, watchSources in ConfigGlobal := (watchSources in ConfigGlobal).value ++ { val bases = unmanagedResourceDirectories.value val include = (includeFilter in unmanagedResources).value @@ -430,19 +434,11 @@ object Defaults extends BuildCommon { managedResources := generate(resourceGenerators).value, resources := Classpaths.concat(managedResources, unmanagedResources).value ) + def addBaseSources = FileManagement.appendBaseSources lazy val outputConfigPaths = Seq( classDirectory := crossTarget.value / (prefix(configuration.value.name) + "classes"), target in doc := crossTarget.value / (prefix(configuration.value.name) + "api") ) - def addBaseSources = Seq( - unmanagedSources := { - val srcs = unmanagedSources.value - val f = (includeFilter in unmanagedSources).value - val excl = (excludeFilter in unmanagedSources).value - val baseDir = baseDirectory.value - if (sourcesInBase.value) (srcs +++ baseDir * (f -- excl)).get else srcs - } - ) // This is included into JvmPlugin.projectSettings def compileBase = inTask(console)(compilersSetting :: Nil) ++ compileBaseGlobal ++ Seq( @@ -659,7 +655,7 @@ object Defaults extends BuildCommon { }, watchStartMessage := Watched.projectOnWatchMessage(thisProjectRef.value.project), watch := watchSetting.value, - fileTreeViewConfig := FileTreeViewConfig.default(pollInterval.value, watchAntiEntropy.value), + fileTreeViewConfig := FileTreeViewConfig.default(watchAntiEntropy.value) ) def generate(generators: SettingKey[Seq[Task[Seq[File]]]]): Initialize[Task[Seq[File]]] = @@ -1192,10 +1188,7 @@ object Defaults extends BuildCommon { dirs: ScopedTaskable[Seq[File]], filter: ScopedTaskable[FileFilter], excludes: ScopedTaskable[FileFilter] - ): Initialize[Task[Seq[File]]] = - Def.task { - dirs.toTask.value.descendantsExcept(filter.toTask.value, excludes.toTask.value).get - } + ): Initialize[Task[Seq[File]]] = FileManagement.collectFiles(dirs, filter, excludes) def artifactPathSetting(art: SettingKey[Artifact]): Initialize[File] = Def.setting { val f = artifactName.value @@ -1767,7 +1760,7 @@ object Defaults extends BuildCommon { lazy val compileSettings: Seq[Setting[_]] = configSettings ++ - (mainBgRunMainTask +: mainBgRunTask +: addBaseSources) ++ + (mainBgRunMainTask +: mainBgRunTask +: FileManagement.appendBaseSources) ++ Classpaths.addUnmanagedLibrary lazy val testSettings: Seq[Setting[_]] = configSettings ++ testTasks diff --git a/main/src/main/scala/sbt/internal/FileManagement.scala b/main/src/main/scala/sbt/internal/FileManagement.scala new file mode 100644 index 000000000..bbdbb346c --- /dev/null +++ b/main/src/main/scala/sbt/internal/FileManagement.scala @@ -0,0 +1,85 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal + +import java.io.IOException +import java.nio.file.Path + +import sbt.Keys._ +import sbt.io.FileTreeDataView.Entry +import sbt.io.syntax.File +import sbt.io.{ FileFilter, FileTreeRepository, FileTreeDataView } +import sbt.{ Def, ScopedTaskable, StampedFile, Task } + +private[sbt] object FileManagement { + private[sbt] implicit class FileTreeDataViewOps[+T](val fileTreeDataView: FileTreeDataView[T]) { + def register(path: Path, maxDepth: Int): Either[IOException, Boolean] = { + fileTreeDataView match { + case r: FileTreeRepository[T] => r.register(path, maxDepth) + case _ => Right(false) + } + } + } + + private[sbt] def collectFiles( + dirs: ScopedTaskable[Seq[File]], + filter: ScopedTaskable[FileFilter], + excludes: ScopedTaskable[FileFilter] + ): Def.Initialize[Task[Seq[File]]] = + Def.task { + val sourceDirs = dirs.toTask.value + val view = fileTreeView.value + val include = filter.toTask.value + val ex = excludes.toTask.value + val sourceFilter: Entry[StampedFile] => Boolean = (entry: Entry[StampedFile]) => { + entry.value match { + case Right(sf) => include.accept(sf) && !ex.accept(sf) + case _ => false + } + } + sourceDirs.flatMap { dir => + view.register(dir.toPath, maxDepth = Integer.MAX_VALUE) + view + .listEntries(dir.toPath, maxDepth = Integer.MAX_VALUE, sourceFilter) + .map(e => e.value.getOrElse(e.typedPath.getPath.toFile)) + } + } + + private[sbt] def appendBaseSources: Seq[Def.Setting[Task[Seq[File]]]] = Seq( + unmanagedSources := { + val sources = unmanagedSources.value + val f = (includeFilter in unmanagedSources).value + val excl = (excludeFilter in unmanagedSources).value + val baseDir = baseDirectory.value + val view = fileTreeView.value + if (sourcesInBase.value) { + view.register(baseDir.toPath, maxDepth = 0) + sources ++ + view + .listEntries( + baseDir.toPath, + maxDepth = 0, + e => { + val tp = e.typedPath + /* + * The TypedPath has the isDirectory and isFile properties embedded. By overriding + * these methods in java.io.File, FileFilters may be applied without needing to + * stat the file (which is expensive) for isDirectory and isFile checks. + */ + val file = new java.io.File(tp.getPath.toString) { + override def isDirectory: Boolean = tp.isDirectory + override def isFile: Boolean = tp.isFile + } + f.accept(file) && !excl.accept(file) + } + ) + .flatMap(_.value.toOption) + } else sources + } + ) +}