From ffa69ea5d690b5d8d548e0eefd98e83587a30de2 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Fri, 22 Mar 2019 07:48:50 -0700 Subject: [PATCH 01/23] Remove relative-source-error test The io library now delegates to swoval for io which generally handles relative sources correctly (by converting them to absolute paths internally). This test started failing after the io bump because of this. It seemed to me that relative sources not working was not a feature so I just removed the test. --- .../relative-source-error/changes/absolute.sbt | 1 - .../relative-source-error/changes/relative.sbt | 1 - .../source-dependencies/relative-source-error/src/A.scala | 1 - .../source-dependencies/relative-source-error/test | 7 ------- 4 files changed, 10 deletions(-) delete mode 100644 sbt/src/sbt-test/source-dependencies/relative-source-error/changes/absolute.sbt delete mode 100644 sbt/src/sbt-test/source-dependencies/relative-source-error/changes/relative.sbt delete mode 100644 sbt/src/sbt-test/source-dependencies/relative-source-error/src/A.scala delete mode 100644 sbt/src/sbt-test/source-dependencies/relative-source-error/test diff --git a/sbt/src/sbt-test/source-dependencies/relative-source-error/changes/absolute.sbt b/sbt/src/sbt-test/source-dependencies/relative-source-error/changes/absolute.sbt deleted file mode 100644 index a5f2d007a..000000000 --- a/sbt/src/sbt-test/source-dependencies/relative-source-error/changes/absolute.sbt +++ /dev/null @@ -1 +0,0 @@ -scalaSource in Compile := baseDirectory.value / "src" \ No newline at end of file diff --git a/sbt/src/sbt-test/source-dependencies/relative-source-error/changes/relative.sbt b/sbt/src/sbt-test/source-dependencies/relative-source-error/changes/relative.sbt deleted file mode 100644 index 8e898fe02..000000000 --- a/sbt/src/sbt-test/source-dependencies/relative-source-error/changes/relative.sbt +++ /dev/null @@ -1 +0,0 @@ -scalaSource in Compile := file("src") \ No newline at end of file diff --git a/sbt/src/sbt-test/source-dependencies/relative-source-error/src/A.scala b/sbt/src/sbt-test/source-dependencies/relative-source-error/src/A.scala deleted file mode 100644 index 528ffce71..000000000 --- a/sbt/src/sbt-test/source-dependencies/relative-source-error/src/A.scala +++ /dev/null @@ -1 +0,0 @@ -object A \ No newline at end of file diff --git a/sbt/src/sbt-test/source-dependencies/relative-source-error/test b/sbt/src/sbt-test/source-dependencies/relative-source-error/test deleted file mode 100644 index fb8e9dce8..000000000 --- a/sbt/src/sbt-test/source-dependencies/relative-source-error/test +++ /dev/null @@ -1,7 +0,0 @@ -$ copy-file changes/relative.sbt build.sbt -> reload --> compile - -$ copy-file changes/absolute.sbt build.sbt -> reload -> compile From f7f7addff7c3470cbf092bcfbe3675e08339a4e5 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Sat, 15 Dec 2018 17:35:05 -0800 Subject: [PATCH 02/23] Bump io This new version of io breaks source and binary compatibility everywhere that uses the register(path: Path, depth: Int) method that is defined on a few interfaces because I changed the signature to register(glob: Glob). I had to convert to using a glob everywhere that register was called. I also noticed a number of places where we were calling .asFile on a file. This is redundant because asFile is an extension method on File that just returns the underlying file. Finally, I share the IOSyntax trait from io in AllSyntax. There was more or less a TODO suggesting this change. The one hairy part is the existence of the Alternative class. This class has unfortunately somehow made it into the sbt package object. While I doubt many plugins are using this, it doesn't seem worth breaking binary compatibility to get rid of it. The issue is that while Alternative is defined private[sbt], the alternative method in IOSyntax is public, so I can't get rid of Alternative without breaking binary compatibility. I'm not deprecating Alternative for now because the sbtProj still has xfatal warnings on. I think in many, if not most, cases, the Alternative class makes the code more confusing as is often the case with custom operators. The confusion is mitigated if the abstraction is used only in the file in which it's defined. --- .../main/scala/sbt/FileTreeViewConfig.scala | 32 +++++---- .../src/test/scala/sbt/WatchedSpec.scala | 31 +++++--- main/src/main/scala/sbt/Defaults.scala | 11 +-- main/src/main/scala/sbt/Opts.scala | 2 +- .../main/scala/sbt/internal/BuildLoader.scala | 7 +- .../scala/sbt/internal/BuildStructure.scala | 2 +- .../scala/sbt/internal/CommandStrings.scala | 2 +- .../scala/sbt/internal/ExternalHooks.scala | 39 +++++----- .../scala/sbt/internal/FileManagement.scala | 71 +++++++++---------- project/Dependencies.scala | 11 +-- sbt/src/main/scala/sbt/AllSyntax.scala | 11 ++- 11 files changed, 122 insertions(+), 97 deletions(-) diff --git a/main-command/src/main/scala/sbt/FileTreeViewConfig.scala b/main-command/src/main/scala/sbt/FileTreeViewConfig.scala index 4ec9359c4..4a48d6c04 100644 --- a/main-command/src/main/scala/sbt/FileTreeViewConfig.scala +++ b/main-command/src/main/scala/sbt/FileTreeViewConfig.scala @@ -27,10 +27,8 @@ final class FileTreeViewConfig private ( ) => FileEventMonitor[FileCacheEntry] ) object FileTreeViewConfig { - 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) - } + private implicit class SourceOps(val s: WatchSource) extends AnyVal { + def toGlob: Glob = Glob(s.base, AllPassFilter, if (s.recursive) Integer.MAX_VALUE else 0) } /** @@ -76,14 +74,16 @@ object FileTreeViewConfig { val ioLogger: sbt.io.WatchLogger = msg => logger.debug(msg.toString) FileEventMonitor.antiEntropy( new WatchServiceBackedObservable( - WatchState.empty(Watched.createWatchService(), sources), + WatchState.empty(sources.map(_.toGlob), Watched.createWatchService()), delay, FileCacheEntry.default, closeService = true, ioLogger ), antiEntropy, - ioLogger + ioLogger, + 50.milliseconds, + 10.seconds ) } ) @@ -104,14 +104,20 @@ object FileTreeViewConfig { sources: Seq[WatchSource], logger: Logger ) => { - repository.register(sources) + sources.view.map(_.toGlob).foreach(repository.register) 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 } - FileEventMonitor.antiEntropy(copied, antiEntropy, msg => logger.debug(msg.toString)) + FileEventMonitor.antiEntropy( + copied, + antiEntropy, + msg => logger.debug(msg.toString), + 50.milliseconds, + 10.seconds + ) } ) @@ -159,18 +165,20 @@ object FileTreeViewConfig { pollingInterval: FiniteDuration, pollingSources: Seq[WatchSource], ): FileTreeViewConfig = FileTreeViewConfig( - () => FileTreeRepository.hybrid(FileCacheEntry.default, pollingSources: _*), + () => FileTreeRepository.hybrid(FileCacheEntry.default, pollingSources.map(_.toGlob): _*), ( repository: HybridPollingFileTreeRepository[FileCacheEntry], sources: Seq[WatchSource], logger: Logger ) => { - repository.register(sources) + sources.view.map(_.toGlob).foreach(repository.register) FileEventMonitor .antiEntropy( - repository.toPollingObservable(pollingInterval, sources, NullWatchLogger), + repository.toPollingRepository(pollingInterval, NullWatchLogger), antiEntropy, - msg => logger.debug(msg.toString) + msg => logger.debug(msg.toString), + 50.milliseconds, + 10.seconds ) } ) diff --git a/main-command/src/test/scala/sbt/WatchedSpec.scala b/main-command/src/test/scala/sbt/WatchedSpec.scala index 7fac652e0..06dd64585 100644 --- a/main-command/src/test/scala/sbt/WatchedSpec.scala +++ b/main-command/src/test/scala/sbt/WatchedSpec.scala @@ -16,7 +16,8 @@ import sbt.Watched._ import sbt.WatchedSpec._ import sbt.internal.FileCacheEntry import sbt.io.FileEventMonitor.Event -import sbt.io.{ FileEventMonitor, IO, TypedPath } +import sbt.io._ +import sbt.io.syntax._ import sbt.util.Logger import scala.collection.mutable @@ -26,7 +27,7 @@ class WatchedSpec extends FlatSpec with Matchers { object Defaults { private val fileTreeViewConfig = FileTreeViewConfig.default(50.millis) def config( - sources: Seq[WatchSource], + globs: Seq[Glob], fileEventMonitor: Option[FileEventMonitor[FileCacheEntry]] = None, logger: Logger = NullLogger, handleInput: InputStream => Action = _ => Ignore, @@ -35,9 +36,17 @@ class WatchedSpec extends FlatSpec with Matchers { triggeredMessage: (TypedPath, Int) => Option[String] = (_, _) => None, watchingMessage: Int => Option[String] = _ => None ): WatchConfig = { - val monitor = fileEventMonitor.getOrElse( - fileTreeViewConfig.newMonitor(fileTreeViewConfig.newDataView(), sources, logger) - ) + val monitor = fileEventMonitor.getOrElse { + val fileTreeRepository = FileTreeRepository.default(FileCacheEntry.default) + globs.foreach(fileTreeRepository.register) + FileEventMonitor.antiEntropy( + fileTreeRepository, + 50.millis, + m => logger.debug(m.toString), + 50.milliseconds, + 100.milliseconds + ) + } WatchConfig.default( logger = logger, monitor, @@ -55,13 +64,13 @@ class WatchedSpec extends FlatSpec with Matchers { override def read(): Int = -1 } "Watched.watch" should "stop" in IO.withTemporaryDirectory { dir => - val config = Defaults.config(sources = Seq(WatchSource(dir.toRealPath))) + val config = Defaults.config(globs = Seq(dir.toRealPath.toGlob)) Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch } it should "trigger" in IO.withTemporaryDirectory { dir => val triggered = new AtomicBoolean(false) val config = Defaults.config( - sources = Seq(WatchSource(dir.toRealPath)), + globs = Seq(dir.toRealPath ** AllPassFilter), preWatch = (count, _) => if (count == 2) CancelWatch else Ignore, onWatchEvent = _ => { triggered.set(true); Trigger }, watchingMessage = _ => { @@ -77,7 +86,7 @@ class WatchedSpec extends FlatSpec with Matchers { val foo = realDir.toPath.resolve("foo") val bar = realDir.toPath.resolve("bar") val config = Defaults.config( - sources = Seq(WatchSource(realDir)), + globs = Seq(realDir ** AllPassFilter), preWatch = (count, _) => if (count == 2) CancelWatch else Ignore, onWatchEvent = e => if (e.entry.typedPath.toPath == foo) Trigger else Ignore, triggeredMessage = (tp, _) => { queue += tp; None }, @@ -92,7 +101,7 @@ class WatchedSpec extends FlatSpec with Matchers { val foo = realDir.toPath.resolve("foo") val bar = realDir.toPath.resolve("bar") val config = Defaults.config( - sources = Seq(WatchSource(realDir)), + globs = Seq(realDir ** AllPassFilter), preWatch = (count, _) => if (count == 3) CancelWatch else Ignore, onWatchEvent = _ => Trigger, triggeredMessage = (tp, _) => { queue += tp; None }, @@ -113,7 +122,7 @@ class WatchedSpec extends FlatSpec with Matchers { it should "halt on error" in IO.withTemporaryDirectory { dir => val halted = new AtomicBoolean(false) val config = Defaults.config( - sources = Seq(WatchSource(dir.toRealPath)), + globs = Seq(dir.toRealPath ** AllPassFilter), preWatch = (_, lastStatus) => if (lastStatus) Ignore else { halted.set(true); HandleError } ) Watched.watch(NullInputStream, () => Right(false), config) shouldBe HandleError @@ -121,7 +130,7 @@ class WatchedSpec extends FlatSpec with Matchers { } it should "reload" in IO.withTemporaryDirectory { dir => val config = Defaults.config( - sources = Seq(WatchSource(dir.toRealPath)), + globs = Seq(dir.toRealPath ** AllPassFilter), preWatch = (_, _) => Ignore, onWatchEvent = _ => Reload, watchingMessage = _ => { new File(dir, "file").createNewFile(); None } diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 47731631d..849de8b85 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -628,12 +628,15 @@ object Defaults extends BuildCommon { watchOnTermination := Watched.onTermination, watchConfig := { val sources = watchTransitiveSources.value ++ watchProjectTransitiveSources.value + val globs = sources.map( + s => Glob(s.base, s.includeFilter -- s.excludeFilter, if (s.recursive) Int.MaxValue else 0) + ) val wm = watchingMessage.?.value - .map(w => (count: Int) => Some(w(WatchState.empty(sources).withCount(count)))) + .map(w => (count: Int) => Some(w(WatchState.empty(globs).withCount(count)))) .getOrElse(watchStartMessage.value) val tm = triggeredMessage.?.value .map( - tm => (_: TypedPath, count: Int) => Some(tm(WatchState.empty(sources).withCount(count))) + tm => (_: TypedPath, count: Int) => Some(tm(WatchState.empty(globs).withCount(count))) ) .getOrElse(watchTriggeredMessage.value) val logger = watchLogger.value @@ -1203,14 +1206,14 @@ object Defaults extends BuildCommon { def artifactPathSetting(art: SettingKey[Artifact]): Initialize[File] = Def.setting { val f = artifactName.value - (crossTarget.value / f( + crossTarget.value / f( ScalaVersion( (scalaVersion in artifactName).value, (scalaBinaryVersion in artifactName).value ), projectID.value, art.value - )).asFile + ) } def artifactSetting: Initialize[Artifact] = diff --git a/main/src/main/scala/sbt/Opts.scala b/main/src/main/scala/sbt/Opts.scala index c099b238b..720fd0f93 100644 --- a/main/src/main/scala/sbt/Opts.scala +++ b/main/src/main/scala/sbt/Opts.scala @@ -47,7 +47,7 @@ object Opts { "sonatype-staging", "https://oss.sonatype.org/service/local/staging/deploy/maven2" ) - val mavenLocalFile = Resolver.file("Local Repository", userHome / ".m2" / "repository" asFile)( + val mavenLocalFile = Resolver.file("Local Repository", userHome / ".m2" / "repository")( Resolver.defaultPatterns ) val sbtSnapshots = Resolver.bintrayRepo("sbt", "maven-snapshots") diff --git a/main/src/main/scala/sbt/internal/BuildLoader.scala b/main/src/main/scala/sbt/internal/BuildLoader.scala index 4c0b5bc8c..32ecca182 100644 --- a/main/src/main/scala/sbt/internal/BuildLoader.scala +++ b/main/src/main/scala/sbt/internal/BuildLoader.scala @@ -11,11 +11,16 @@ package internal import java.io.File import java.net.URI import BuildLoader._ -import sbt.internal.io.Alternatives._ import sbt.internal.util.Types.{ const, idFun } import sbt.util.Logger import sbt.librarymanagement.ModuleID +private[internal] object Alternatives { + private[internal] implicit class Alternative[A, B](val f: A => Option[B]) { + def |(g: A => Option[B]): A => Option[B] = (a: A) => f(a) orElse g(a) + } +} +import Alternatives.Alternative final class MultiHandler[S, T]( builtIn: S => Option[T], root: Option[S => Option[T]], diff --git a/main/src/main/scala/sbt/internal/BuildStructure.scala b/main/src/main/scala/sbt/internal/BuildStructure.scala index fa05bcd46..a1b556fe5 100644 --- a/main/src/main/scala/sbt/internal/BuildStructure.scala +++ b/main/src/main/scala/sbt/internal/BuildStructure.scala @@ -329,5 +329,5 @@ object BuildStreams { def refTarget(ref: ResolvedReference, fallbackBase: File, data: Settings[Scope]): File = refTarget(GlobalScope.copy(project = Select(ref)), fallbackBase, data) def refTarget(scope: Scope, fallbackBase: File, data: Settings[Scope]): File = - (Keys.target in scope get data getOrElse outputDirectory(fallbackBase).asFile) / StreamsDirectory + (Keys.target in scope get data getOrElse outputDirectory(fallbackBase)) / StreamsDirectory } diff --git a/main/src/main/scala/sbt/internal/CommandStrings.scala b/main/src/main/scala/sbt/internal/CommandStrings.scala index b597b0238..692abb787 100644 --- a/main/src/main/scala/sbt/internal/CommandStrings.scala +++ b/main/src/main/scala/sbt/internal/CommandStrings.scala @@ -339,7 +339,7 @@ defaults def sbtRCs(s: State): Seq[File] = (Path.userHome / sbtrc) :: - (s.baseDir / sbtrc asFile) :: + (s.baseDir / sbtrc) :: Nil val CrossCommand = "+" diff --git a/main/src/main/scala/sbt/internal/ExternalHooks.scala b/main/src/main/scala/sbt/internal/ExternalHooks.scala index 0ef876341..758c1b32d 100644 --- a/main/src/main/scala/sbt/internal/ExternalHooks.scala +++ b/main/src/main/scala/sbt/internal/ExternalHooks.scala @@ -11,8 +11,8 @@ import java.util.Optional import sbt.Stamped import sbt.internal.inc.ExternalLookup -import sbt.io.syntax.File -import sbt.io.{ FileTreeRepository, FileTreeDataView, TypedPath } +import sbt.io.syntax._ +import sbt.io.{ AllPassFilter, FileTreeDataView, FileTreeRepository, TypedPath } import xsbti.compile._ import xsbti.compile.analysis.Stamp @@ -34,28 +34,31 @@ private[sbt] object ExternalHooks { } view match { case r: FileTreeRepository[FileCacheEntry] => - r.register(options.classesDirectory.toPath, Integer.MAX_VALUE) - options.classpath.foreach { f => - r.register(f.toPath, Integer.MAX_VALUE) + r.register(options.classesDirectory ** AllPassFilter) + options.classpath.foreach { + case f if f.getName.endsWith(".jar") => r.register(f.toGlob) + case f => r.register(f ** AllPassFilter) } case _ => } val allBinaries = new java.util.HashMap[File, Stamp] - 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.stamp) - case _ => + options.classpath.foreach { + case f if f.getName.endsWith(".jar") => + // This gives us the entry for the path itself, which is necessary if the path is a jar file + // rather than a directory. + view.listEntries(f.toGlob) foreach { e => + e.value match { + case Right(value) => allBinaries.put(e.typedPath.toPath.toFile, value.stamp) + case _ => + } } - } - // This gives us the entry for the path itself, which is necessary if the path is a jar file - // 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.stamp) - case _ => + case f => + view.listEntries(f ** "*.jar") foreach { e => + e.value match { + case Right(value) => allBinaries.put(e.typedPath.toPath.toFile, value.stamp) + case _ => + } } - } } val lookup = new ExternalLookup { diff --git a/main/src/main/scala/sbt/internal/FileManagement.scala b/main/src/main/scala/sbt/internal/FileManagement.scala index fdf9c1988..8497a4bfe 100644 --- a/main/src/main/scala/sbt/internal/FileManagement.scala +++ b/main/src/main/scala/sbt/internal/FileManagement.scala @@ -8,14 +8,13 @@ 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.io._ +import sbt.io.syntax._ private[sbt] object FileManagement { private[sbt] def defaultFileTreeView: Def.Initialize[Task[FileTreeViewConfig]] = Def.task { @@ -33,14 +32,30 @@ private[sbt] object FileManagement { } else FileTreeViewConfig.sbt1_2_compat(pollInterval.value, watchAntiEntropy.value) } private[sbt] implicit class FileTreeDataViewOps[+T](val fileTreeDataView: FileTreeDataView[T]) { - def register(path: Path, maxDepth: Int): Either[IOException, Boolean] = { + def register(glob: Glob): Either[IOException, Boolean] = { fileTreeDataView match { - case r: FileTreeRepository[T] => r.register(path, maxDepth) + case r: FileTreeRepository[T] => r.register(glob) case _ => Right(false) } } } + private def entryFilter( + include: FileFilter, + exclude: FileFilter + ): Entry[FileCacheEntry] => Boolean = { 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.toPath.toString) { + override def isDirectory: Boolean = tp.isDirectory + override def isFile: Boolean = tp.isFile + } + include.accept(file) && !exclude.accept(file) + } private[sbt] def collectFiles( dirs: ScopedTaskable[Seq[File]], filter: ScopedTaskable[FileFilter], @@ -51,51 +66,35 @@ private[sbt] object FileManagement { val view = fileTreeView.value val include = filter.toTask.value val ex = excludes.toTask.value - 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 - override def isFile: Boolean = typedPath.isFile - } - include.accept(file) && !ex.accept(file) - } + val sourceFilter: Entry[FileCacheEntry] => Boolean = entryFilter(include, ex) sourceDirs.flatMap { dir => - view.register(dir.toPath, maxDepth = Integer.MAX_VALUE) + view.register(dir ** AllPassFilter) view - .listEntries(dir.toPath, maxDepth = Integer.MAX_VALUE, sourceFilter) - .flatMap(e => e.value.toOption.map(Stamped.file(e.typedPath, _))) + .listEntries(dir.toPath ** AllPassFilter) + .flatMap { + case e if sourceFilter(e) => e.value.toOption.map(Stamped.file(e.typedPath, _)) + case _ => None + } } } private[sbt] def appendBaseSources: Seq[Def.Setting[Task[Seq[File]]]] = Seq( unmanagedSources := { val sources = unmanagedSources.value - val f = (includeFilter in unmanagedSources).value + val include = (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) + view.register(baseDir.toPath * AllPassFilter) + val filter: Entry[FileCacheEntry] => Boolean = entryFilter(include, excl) 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.toPath.toString) { - override def isDirectory: Boolean = tp.isDirectory - override def isFile: Boolean = tp.isFile - } - f.accept(file) && !excl.accept(file) - } - ) - .flatMap(e => e.value.toOption.map(Stamped.file(e.typedPath, _))) + .listEntries(baseDir * AllPassFilter) + .flatMap { + case e if filter(e) => e.value.toOption.map(Stamped.file(e.typedPath, _)) + case _ => None + } } else sources } ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 1fd0b948d..fff2e65c0 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -9,12 +9,12 @@ object Dependencies { val baseScalaVersion = scala212 // sbt modules - private val ioVersion = "1.3.0-M5" + private val ioVersion = "1.3.0-M7" private val utilVersion = "1.3.0-M5" private val lmVersion = sys.props.get("sbt.build.lm.version") match { case Some(version) => version - case _ => "1.3.0-M1" + case _ => "1.3.0-M1" } private val zincVersion = "1.3.0-M2" @@ -34,13 +34,13 @@ object Dependencies { val lmOrganization = sys.props.get("sbt.build.lm.organization") match { case Some(impl) => impl - case _ => "org.scala-sbt" + case _ => "org.scala-sbt" } val lmModuleName = sys.props.get("sbt.build.lm.moduleName") match { case Some(impl) => impl - case _ => "librarymanagement-ivy" + case _ => "librarymanagement-ivy" } lmOrganization %% lmModuleName % lmVersion @@ -98,7 +98,8 @@ object Dependencies { def addSbtLmCore(p: Project): Project = addSbtModule(p, sbtLmPath, "lmCore", libraryManagementCore) - def addSbtLmImpl(p: Project): Project = addSbtModule(p, sbtLmPath, "lmImpl", libraryManagementImpl) + def addSbtLmImpl(p: Project): Project = + addSbtModule(p, sbtLmPath, "lmImpl", libraryManagementImpl) def addSbtCompilerInterface(p: Project): Project = addSbtModule(p, sbtZincPath, "compilerInterface212", compilerInterface) diff --git a/sbt/src/main/scala/sbt/AllSyntax.scala b/sbt/src/main/scala/sbt/AllSyntax.scala index d59026a4e..9d225c3b4 100644 --- a/sbt/src/main/scala/sbt/AllSyntax.scala +++ b/sbt/src/main/scala/sbt/AllSyntax.scala @@ -7,15 +7,12 @@ package sbt -// Todo share this this io.syntax private[sbt] trait IOSyntax0 extends IOSyntax1 { - implicit def alternative[A, B](f: A => Option[B]): Alternative[A, B] = - g => a => f(a) orElse g(a) + implicit def alternative[A, B](f: A => Option[B]): Alternative[A, B] = new Alternative[A, B] { + override def |(g: A => Option[B]): A => Option[B] = (a: A) => f(a) orElse g(a) + } } +private[sbt] trait IOSyntax1 extends sbt.io.IOSyntax private[sbt] trait Alternative[A, B] { def |(g: A => Option[B]): A => Option[B] } - -private[sbt] trait IOSyntax1 { - implicit def singleFileFinder(file: File): sbt.io.PathFinder = sbt.io.PathFinder(file) -} From 03cb79bbfc534a2d7befe1374a695e3e84fd9613 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Sun, 13 Jan 2019 16:04:58 -0800 Subject: [PATCH 03/23] Stop using ---/pair in SbtLauncherPlugin I'd like to remove '---' and 'pair' in sbt 2 so I'm inlining the logic where I find it. The '---' method is trivially implemented with a filter on the sequence of files and filtering the output will not require io, unlike '---'. For pair, I get confused every time I see it in the code and it is rarely saving more than a line. While I understand that it may have been convenient when the code using pair was originally written, I don't think it is worth the maintenance cost. My specific issue is that to me pair means tuple2, full stop. The definition of pair is: def pair[T](mapper: File => Option[T], errorIfNone: Boolean = true): Seq[(File, T)] First of all, it's not at all obvious when seen inline in the code that it has the side effect of evaluating PathFinder.get. Moreover, it doesn't return a general pair, it's a very specific pair with a File in the first position. I just don't see how using pair improves upon, say: val func: File => Option[(File, String)] = ??? globs.all.flatMap(func) or val func: File => Option[(File, String)] = ??? globs.all.map(f => func(f) match { case Some(r) => r case None => throw new IllegalStateException("Couldn't evaluate func for $f") }) // or just define `func = File => (File, String)` and throw on an error --- project/SbtLauncherPlugin.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/project/SbtLauncherPlugin.scala b/project/SbtLauncherPlugin.scala index c3b5f229b..7386b79cc 100644 --- a/project/SbtLauncherPlugin.scala +++ b/project/SbtLauncherPlugin.scala @@ -1,6 +1,5 @@ -import sbt.io.Path._ +import sbt.Keys._ import sbt._ -import Keys._ import sbt.io.CopyOptions object SbtLauncherPlugin extends AutoPlugin { @@ -43,7 +42,12 @@ object SbtLauncherPlugin extends AutoPlugin { IO.unzip(jar, dir) IO.copy(overrides.map({ case (n, f) => (f, dir / n) }), CopyOptions().withOverwrite(true)) // TODO - is the ok for creating a jar? - IO.zip((dir.allPaths --- dir) pair relativeTo(dir), target) + val rebase: File => Seq[(File, String)] = { + val path = dir.toPath + f => + if (f != dir) f -> path.relativize(f.toPath).toString :: Nil else Nil + } + IO.zip(dir.allPaths.get().flatMap(rebase), target) } target } From 792fb91737985a56d1b028dfc5eae1db57ff2db7 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Fri, 1 Feb 2019 17:15:37 -0800 Subject: [PATCH 04/23] Avoid deprecated Alternative class --- main/src/main/scala/sbt/Defaults.scala | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 849de8b85..85f78bb34 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -1183,10 +1183,14 @@ object Defaults extends BuildCommon { // drop base directories, since there are no valid mappings for these def sourceMappings: Initialize[Task[Seq[(File, String)]]] = Def.task { - val srcs = unmanagedSources.value val sdirs = unmanagedSourceDirectories.value val base = baseDirectory.value - (srcs --- sdirs --- base) pair (relativeTo(sdirs) | relativeTo(base) | flat) + val relative = (f: File) => relativeTo(sdirs)(f).orElse(relativeTo(base)(f)).orElse(flat(f)) + val exclude = Set(sdirs, base) + unmanagedSources.value.flatMap { + case s if !exclude(s) => relative(s).map(s -> _) + case _ => None + } } def resourceMappings = relativeMappings(unmanagedResources, unmanagedResourceDirectories) def relativeMappings( @@ -1194,9 +1198,12 @@ object Defaults extends BuildCommon { dirs: ScopedTaskable[Seq[File]] ): Initialize[Task[Seq[(File, String)]]] = Def.task { - val rs = files.toTask.value - val rdirs = dirs.toTask.value - (rs --- rdirs) pair (relativeTo(rdirs) | flat) + val rdirs = dirs.toTask.value.toSet + val relative = (f: File) => relativeTo(rdirs)(f).orElse(flat(f)) + files.toTask.value.flatMap { + case r if !rdirs(r) => relative(r).map(r -> _) + case _ => None + } } def collectFiles( dirs: ScopedTaskable[Seq[File]], @@ -1730,10 +1737,15 @@ object Defaults extends BuildCommon { def copyResourcesTask = Def.task { val t = classDirectory.value - val dirs = resourceDirectories.value + val dirs = resourceDirectories.value.toSet val s = streams.value val cacheStore = s.cacheStoreFactory make "copy-resources" - val mappings = (resources.value --- dirs) pair (rebase(dirs, t) | flat(t)) + val flt: File => Option[File] = flat(t) + val transform: File => Option[File] = (f: File) => rebase(dirs, t)(f).orElse(flt(f)) + val mappings: Seq[(File, File)] = resources.value.flatMap { + case r if !dirs(r) => transform(r).map(r -> _) + case _ => None + } s.log.debug("Copy resource mappings: " + mappings.mkString("\n\t", "\n\t", "")) Sync.sync(cacheStore)(mappings) mappings From d0310cc8664407bef252d37d2b6d8c8566899459 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Fri, 1 Feb 2019 11:32:49 -0800 Subject: [PATCH 05/23] Rework FileTreeRepository configuration The FileTreeViewConfig abstraction that I added was somewhat unwieldy and confusing. The original intention was to provide users with a lot of flexibility in configuring the global file tree repository used by sbt. I don't think that flexibility is necessary and it was both conceptually complicated and made the implementation complex. In this commit, I add a new boolean flag enableGlobalCachingFileTreeRepository that toggles which kind of FileTreeRepository to use globally. There are actually three kinds of repositories that could be returned: 1) FileTreeRepository.default -- this caches the entire file system tree it hooks into the cache's event callbacks to create a file event monitor. It will be used if enableGlobalCachingFileTreeRepository is true and Global / pollingGlobs := Nil 2) FileTreeRepository.hybrid -- similar to FileTreeRepository.default except that it will not cache any files that are included in Global / pollingGlobs. It will be used if enableGlobalCachingFileTreeRepository is true and Global / pollingGlobs is non empty 3) FileTreeRepository.legacy -- does not cache any of the file system tree, but does maintain a persistent file monitoring process that is implemented with a WatchServiceBackedObservable. Because it doesn't poll, in general, it's ok to leave the monitoring on in the background. One reason to use this is that if there are any issues with the cache being unable to accurately mirror the underlying file system tree, this repository will always poll the file system whenever sbt requests the entries for a given glob. Moreover, the file system tree implementation is very similar to the implementation that was used in 1.2.x so this gives users a way to almost fully opt back in to the old behavior. --- .../main/scala/sbt/FileTreeViewConfig.scala | 185 ------------------ main-command/src/main/scala/sbt/Watched.scala | 10 +- main/src/main/scala/sbt/Defaults.scala | 14 +- main/src/main/scala/sbt/Keys.scala | 11 +- main/src/main/scala/sbt/Main.scala | 20 +- .../scala/sbt/internal/FileManagement.scala | 107 +++++++--- 6 files changed, 109 insertions(+), 238 deletions(-) delete mode 100644 main-command/src/main/scala/sbt/FileTreeViewConfig.scala diff --git a/main-command/src/main/scala/sbt/FileTreeViewConfig.scala b/main-command/src/main/scala/sbt/FileTreeViewConfig.scala deleted file mode 100644 index 4a48d6c04..000000000 --- a/main-command/src/main/scala/sbt/FileTreeViewConfig.scala +++ /dev/null @@ -1,185 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -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 sbt.util.Logger - -import scala.concurrent.duration._ - -/** - * Configuration for viewing and monitoring the file system. - */ -final class FileTreeViewConfig private ( - val newDataView: () => FileTreeDataView[FileCacheEntry], - val newMonitor: ( - FileTreeDataView[FileCacheEntry], - Seq[WatchSource], - Logger - ) => FileEventMonitor[FileCacheEntry] -) -object FileTreeViewConfig { - private implicit class SourceOps(val s: WatchSource) extends AnyVal { - def toGlob: Glob = Glob(s.base, AllPassFilter, if (s.recursive) Integer.MAX_VALUE else 0) - } - - /** - * Create a new FileTreeViewConfig. This factory takes a generic parameter, T, that is bounded - * 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[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 - * created by newDataView - * @tparam T the subtype of sbt.io.FileTreeDataView that is returned by [[FileTreeViewConfig.newDataView]] - * @return a [[FileTreeViewConfig]] instance. - */ - def apply[T <: FileTreeDataView[FileCacheEntry]]( - newDataView: () => T, - newMonitor: (T, Seq[WatchSource], Logger) => FileEventMonitor[FileCacheEntry] - ): FileTreeViewConfig = - new FileTreeViewConfig( - newDataView, - (view: FileTreeDataView[FileCacheEntry], sources: Seq[WatchSource], logger: Logger) => - newMonitor(view.asInstanceOf[T], sources, logger) - ) - - /** - * 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 sbt1_2_compat( - delay: FiniteDuration, - antiEntropy: FiniteDuration - ): FileTreeViewConfig = - FileTreeViewConfig( - () => 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(sources.map(_.toGlob), Watched.createWatchService()), - delay, - FileCacheEntry.default, - closeService = true, - ioLogger - ), - antiEntropy, - ioLogger, - 50.milliseconds, - 10.seconds - ) - } - ) - - /** - * 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(FileCacheEntry.default), - ( - repository: FileTreeRepository[FileCacheEntry], - sources: Seq[WatchSource], - logger: Logger - ) => { - sources.view.map(_.toGlob).foreach(repository.register) - 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 - } - FileEventMonitor.antiEntropy( - copied, - antiEntropy, - msg => logger.debug(msg.toString), - 50.milliseconds, - 10.seconds - ) - } - ) - - /** - * Provides a default [[FileTreeViewConfig]]. When the pollingSources argument is empty, it - * returns the same config as [[sbt.FileTreeViewConfig.default(antiEntropy:scala\.concurrent\.duration\.FiniteDuration)*]]. - * Otherwise, it returns the same config as [[polling]]. - * @param antiEntropy the duration of the period after a path triggers a build for which it is - * quarantined from triggering another build - * @param pollingInterval the frequency with which the sbt.io.FileEventMonitor polls the file - * system for the paths included in pollingSources - * @param pollingSources the sources that will not be cached in the sbt.io.FileTreeRepository and that - * will be periodically polled for changes during continuous builds. - * @return - */ - def default( - antiEntropy: FiniteDuration, - pollingInterval: FiniteDuration, - pollingSources: Seq[WatchSource] - ): FileTreeViewConfig = { - if (pollingSources.isEmpty) default(antiEntropy) - else polling(antiEntropy, pollingInterval, pollingSources) - } - - /** - * Provides a polling [[FileTreeViewConfig]]. Unlike the view returned by newDataView in - * [[sbt.FileTreeViewConfig.default(antiEntropy:scala\.concurrent\.duration\.FiniteDuration)*]], - * the view returned by newDataView will not cache any portion of the file system tree that is is - * covered by the pollingSources parameter. The monitor that is generated by newMonitor, will - * poll these directories for changes rather than relying on file system events from the - * operating system. Any paths that are registered with the view that are not included in the - * pollingSources will be cached and monitored using file system events from the operating system - * in the same way that they are in the default view. - * - * @param antiEntropy the duration of the period after a path triggers a build for which it is - * quarantined from triggering another build - * @param pollingInterval the frequency with which the FileEventMonitor polls the file system - * for the paths included in pollingSources - * @param pollingSources the sources that will not be cached in the sbt.io.FileTreeRepository and that - * will be periodically polled for changes during continuous builds. - * @return a [[FileTreeViewConfig]] instance. - */ - def polling( - antiEntropy: FiniteDuration, - pollingInterval: FiniteDuration, - pollingSources: Seq[WatchSource], - ): FileTreeViewConfig = FileTreeViewConfig( - () => FileTreeRepository.hybrid(FileCacheEntry.default, pollingSources.map(_.toGlob): _*), - ( - repository: HybridPollingFileTreeRepository[FileCacheEntry], - sources: Seq[WatchSource], - logger: Logger - ) => { - sources.view.map(_.toGlob).foreach(repository.register) - FileEventMonitor - .antiEntropy( - repository.toPollingRepository(pollingInterval, NullWatchLogger), - antiEntropy, - msg => logger.debug(msg.toString), - 50.milliseconds, - 10.seconds - ) - } - ) -} diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index b53f1c3d0..6cf5b71c9 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -64,9 +64,8 @@ object Watched { /** * This trait is used to communicate what the watch should do next at various points in time. It - * is heavily linked to a number of callbacks in [[WatchConfig]]. For example, when the - * sbt.io.FileEventMonitor created by [[FileTreeViewConfig.newMonitor]] detects a changed source - * file, then we expect [[WatchConfig.onWatchEvent]] to return [[Trigger]]. + * is heavily linked to a number of callbacks in [[WatchConfig]]. For example, when the event + * monitor detects a changed source we expect [[WatchConfig.onWatchEvent]] to return [[Trigger]]. */ sealed trait Action @@ -427,11 +426,11 @@ object Watched { val Configuration = AttributeKey[Watched]("watched-configuration", "Configures continuous execution.") - def createWatchService(): WatchService = { + def createWatchService(pollDelay: FiniteDuration): WatchService = { def closeWatch = new MacOSXWatchService() sys.props.get("sbt.watch.mode") match { case Some("polling") => - new PollingWatchService(PollDelay) + new PollingWatchService(pollDelay) case Some("nio") => FileSystems.getDefault.newWatchService() case Some("closewatch") => closeWatch @@ -440,6 +439,7 @@ object Watched { FileSystems.getDefault.newWatchService() } } + def createWatchService(): WatchService = createWatchService(PollDelay) } /** diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 85f78bb34..961ca0e97 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -249,7 +249,7 @@ object Defaults extends BuildCommon { extraLoggers :== { _ => Nil }, - pollingDirectories :== Nil, + pollingGlobs :== Nil, watchSources :== Nil, watchProjectSources :== Nil, skip :== false, @@ -280,12 +280,8 @@ object Defaults extends BuildCommon { None }, watchStartMessage := Watched.defaultStartWatch, - fileTreeViewConfig := FileManagement.defaultFileTreeView.value, - fileTreeView := state.value - .get(Keys.globalFileTreeView) - .getOrElse(FileTreeView.DEFAULT.asDataView(FileCacheEntry.default)), externalHooks := { - val view = fileTreeView.value + val view = FileManagement.dataView.value compileOptions => Some(ExternalHooks(compileOptions, view)) }, @@ -640,9 +636,12 @@ object Defaults extends BuildCommon { ) .getOrElse(watchTriggeredMessage.value) val logger = watchLogger.value + val repo = FileManagement.repo.value + globs.foreach(repo.register) + val monitor = FileManagement.monitor(repo, watchAntiEntropy.value, logger) WatchConfig.default( logger, - fileTreeViewConfig.value.newMonitor(fileTreeView.value, sources, logger), + monitor, watchHandleInput.value, watchPreWatch.value, watchOnEvent.value, @@ -653,7 +652,6 @@ object Defaults extends BuildCommon { }, watchStartMessage := Watched.projectOnWatchMessage(thisProjectRef.value.project), watch := watchSetting.value, - fileTreeViewConfig := FileManagement.defaultFileTreeView.value ) def generate(generators: SettingKey[Seq[Task[Seq[File]]]]): Initialize[Task[Seq[File]]] = diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index d96a72f22..480bcfd9a 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -23,7 +23,7 @@ import sbt.internal.librarymanagement.{ CompatibilityWarningOptions, IvySbt } import sbt.internal.server.ServerHandler import sbt.internal.util.{ AttributeKey, SourcePosition } import sbt.io.FileEventMonitor.Event -import sbt.io.{ FileFilter, FileTreeDataView, TypedPath, WatchService } +import sbt.io._ import sbt.librarymanagement.Configurations.CompilerPlugin import sbt.librarymanagement.LibraryManagementCodec._ import sbt.librarymanagement._ @@ -93,9 +93,9 @@ 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[FileCacheEntry]]("A view of the file system") + val enableGlobalCachingFileTreeRepository = settingKey[Boolean]("Toggles whether or not to create a global cache of the file system that can be used by tasks to quickly list a path").withRank(DSetting) 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 pollingGlobs = settingKey[Seq[Glob]]("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) @@ -114,7 +114,6 @@ object Keys { val watchingMessage = settingKey[WatchState => String]("The message to show when triggered execution waits for sources to change.").withRank(DSetting) @deprecated("Use watchTriggeredMessage instead", "1.3.0") val triggeredMessage = settingKey[WatchState => String]("The message to show before triggered execution executes an action after sources change.").withRank(DSetting) - val fileTreeViewConfig = taskKey[FileTreeViewConfig]("Configures how sbt will traverse and monitor the file system.").withRank(BMinusSetting) // Path Keys val baseDirectory = settingKey[File]("The base directory. Depending on the scope, this is the base directory for the build, project, configuration, or task.").withRank(AMinusSetting) @@ -468,8 +467,8 @@ object Keys { @deprecated("No longer used", "1.3.0") private[sbt] val executeProgress = settingKey[State => TaskProgress]("Experimental task execution listener.").withRank(DTask) - private[sbt] val globalFileTreeView = AttributeKey[FileTreeDataView[FileCacheEntry]]( - "globalFileTreeView", + private[sbt] val globalFileTreeRepository = AttributeKey[FileTreeRepository[FileCacheEntry]]( + "global-file-tree-repository", "Provides a view into the file system that may or may not cache the tree in memory", 1000 ) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 275ed4d3d..edb3e180f 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -9,6 +9,7 @@ package sbt import java.io.{ File, IOException } import java.net.URI +import java.util.concurrent.atomic.AtomicBoolean import java.util.{ Locale, Properties } import sbt.BasicCommandStrings.{ Shell, TemplateCommand } @@ -21,8 +22,8 @@ import sbt.internal.inc.ScalaInstance import sbt.internal.util.Types.{ const, idFun } import sbt.internal.util._ import sbt.internal.util.complete.Parser +import sbt.io.IO import sbt.io.syntax._ -import sbt.io.{ FileTreeDataView, IO } import sbt.util.{ Level, Logger, Show } import xsbti.compile.CompilerCache @@ -852,27 +853,26 @@ object BuiltinCommands { } s.put(Keys.stateCompilerCache, cache) } - private[sbt] def registerGlobalCaches(s: State): State = { - val extracted = Project.extract(s) + private[sbt] def registerGlobalCaches(s: State): State = try { + val extracted = Project.extract(s) + val cleanedUp = new AtomicBoolean(false) def cleanup(): Unit = { - s.get(Keys.globalFileTreeView).foreach(_.close()) - s.attributes.remove(Keys.globalFileTreeView) + s.get(Keys.globalFileTreeRepository).foreach(_.close()) + s.attributes.remove(Keys.globalFileTreeRepository) s.get(Keys.taskRepository).foreach(_.close()) s.attributes.remove(Keys.taskRepository) () } - val (_, config: FileTreeViewConfig) = extracted.runTask(Keys.fileTreeViewConfig, s) - val view: FileTreeDataView[FileCacheEntry] = config.newDataView() - val newState = s.addExitHook(cleanup()) cleanup() + val fileTreeRepository = FileManagement.defaultFileTreeRepository(s, extracted) + val newState = s.addExitHook(if (cleanedUp.compareAndSet(false, true)) cleanup()) newState - .put(Keys.globalFileTreeView, view) .put(Keys.taskRepository, new TaskRepository.Repr) + .put(Keys.globalFileTreeRepository, fileTreeRepository) } catch { case NonFatal(_) => s } - } def clearCaches: Command = { val help = Help.more(ClearCaches, ClearCachesDetailed) diff --git a/main/src/main/scala/sbt/internal/FileManagement.scala b/main/src/main/scala/sbt/internal/FileManagement.scala index 8497a4bfe..9a4530d90 100644 --- a/main/src/main/scala/sbt/internal/FileManagement.scala +++ b/main/src/main/scala/sbt/internal/FileManagement.scala @@ -5,38 +5,83 @@ * Licensed under Apache License 2.0 (see LICENSE) */ -package sbt.internal - -import java.io.IOException +package sbt +package internal import sbt.BasicCommandStrings.ContinuousExecutePrefix import sbt.Keys._ -import sbt._ -import sbt.io.FileTreeDataView.Entry +import sbt.internal.io.HybridPollingFileTreeRepository +import sbt.io.FileTreeDataView.{ Entry, Observable, Observer, Observers } import sbt.io._ import sbt.io.syntax._ +import sbt.util.Logger + +import scala.concurrent.duration._ private[sbt] object FileManagement { - private[sbt] def defaultFileTreeView: Def.Initialize[Task[FileTreeViewConfig]] = Def.task { - val remaining = state.value.remainingCommands.map(_.commandLine.trim) + private[sbt] def defaultFileTreeRepository( + state: State, + extracted: Extracted + ): FileTreeRepository[FileCacheEntry] = { + val pollingGlobs = extracted.getOpt(Keys.pollingGlobs).getOrElse(Nil) + val remaining = state.remainingCommands.map(_.commandLine) // If the session is interactive or if the commands include a continuous build, then use // the default configuration. Otherwise, use the sbt1_2_compat config, which does not cache // anything, which makes it less likely to cause issues with CI. - val interactive = remaining.contains("shell") || remaining.lastOption.contains("iflast shell") + val interactive = + remaining.contains("shell") || remaining.lastOption.contains("iflast shell") val scripted = remaining.contains("setUpScripted") - val continuous = remaining.lastOption.exists(_.startsWith(ContinuousExecutePrefix)) - if (!scripted && (interactive || continuous)) { - FileTreeViewConfig - .default(watchAntiEntropy.value, pollInterval.value, pollingDirectories.value) - } else FileTreeViewConfig.sbt1_2_compat(pollInterval.value, watchAntiEntropy.value) + val enableCache = extracted + .getOpt(Keys.enableGlobalCachingFileTreeRepository) + .getOrElse(!scripted && (interactive || continuous)) + if (enableCache) { + if (pollingGlobs.isEmpty) FileTreeRepository.default(FileCacheEntry.default) + else FileTreeRepository.hybrid(FileCacheEntry.default, pollingGlobs: _*) + } else { + FileTreeRepository.legacy( + FileCacheEntry.default, + (_: Any) => {}, + Watched.createWatchService(extracted.getOpt(Keys.pollInterval).getOrElse(500.milliseconds)) + ) + } } - private[sbt] implicit class FileTreeDataViewOps[+T](val fileTreeDataView: FileTreeDataView[T]) { - def register(glob: Glob): Either[IOException, Boolean] = { - fileTreeDataView match { - case r: FileTreeRepository[T] => r.register(glob) - case _ => Right(false) + + private[sbt] def monitor( + repository: FileTreeRepository[FileCacheEntry], + antiEntropy: FiniteDuration, + logger: Logger + ): FileEventMonitor[FileCacheEntry] = { + // Forwards callbacks to the repository. The close method removes all of these + // callbacks. + val copied: Observable[FileCacheEntry] = new Observable[FileCacheEntry] { + private[this] val observers = new Observers[FileCacheEntry] + val (underlying, needClose) = repository match { + case h: HybridPollingFileTreeRepository[FileCacheEntry] => + (h.toPollingRepository(antiEntropy, (msg: Any) => logger.debug(msg.toString)), true) + case r => (r, false) } + private[this] val handle = underlying.addObserver(observers) + override def addObserver(observer: Observer[FileCacheEntry]): Int = + observers.addObserver(observer) + override def removeObserver(handle: Int): Unit = observers.removeObserver(handle) + override def close(): Unit = { + underlying.removeObserver(handle) + if (needClose) underlying.close() + } + } + new FileEventMonitor[FileCacheEntry] { + val monitor = + FileEventMonitor.antiEntropy( + copied, + antiEntropy, + new WatchLogger { override def debug(msg: => Any): Unit = logger.debug(msg.toString) }, + 50.millis, + 10.minutes + ) + override def poll(duration: Duration): Seq[FileEventMonitor.Event[FileCacheEntry]] = + monitor.poll(duration) + override def close(): Unit = monitor.close() } } @@ -56,6 +101,23 @@ private[sbt] object FileManagement { } include.accept(file) && !exclude.accept(file) } + private[sbt] def repo: Def.Initialize[Task[FileTreeRepository[FileCacheEntry]]] = Def.task { + lazy val msg = s"Tried to get FileTreeRepository for uninitialized state." + state.value.get(Keys.globalFileTreeRepository).getOrElse(throw new IllegalStateException(msg)) + } + private[sbt] def dataView: Def.Initialize[Task[FileTreeDataView[FileCacheEntry]]] = Def.task { + state.value + .get(Keys.globalFileTreeRepository) + .map(toDataView) + .getOrElse(FileTreeView.DEFAULT.asDataView(FileCacheEntry.default)) + } + private def toDataView(r: FileTreeRepository[FileCacheEntry]): FileTreeDataView[FileCacheEntry] = + new FileTreeDataView[FileCacheEntry] { + private def reg(glob: Glob): FileTreeDataView[FileCacheEntry] = { r.register(glob); r } + override def listEntries(glob: Glob): Seq[Entry[FileCacheEntry]] = reg(glob).listEntries(glob) + override def list(glob: Glob): Seq[TypedPath] = reg(glob).list(glob) + override def close(): Unit = {} + } private[sbt] def collectFiles( dirs: ScopedTaskable[Seq[File]], filter: ScopedTaskable[FileFilter], @@ -63,12 +125,11 @@ private[sbt] object FileManagement { ): Def.Initialize[Task[Seq[File]]] = Def.task { val sourceDirs = dirs.toTask.value - val view = fileTreeView.value + val view: FileTreeDataView[FileCacheEntry] = dataView.value val include = filter.toTask.value val ex = excludes.toTask.value val sourceFilter: Entry[FileCacheEntry] => Boolean = entryFilter(include, ex) sourceDirs.flatMap { dir => - view.register(dir ** AllPassFilter) view .listEntries(dir.toPath ** AllPassFilter) .flatMap { @@ -84,13 +145,11 @@ private[sbt] object FileManagement { val include = (includeFilter in unmanagedSources).value val excl = (excludeFilter in unmanagedSources).value val baseDir = baseDirectory.value - val view = fileTreeView.value + val r: FileTreeDataView[FileCacheEntry] = dataView.value if (sourcesInBase.value) { - view.register(baseDir.toPath * AllPassFilter) val filter: Entry[FileCacheEntry] => Boolean = entryFilter(include, excl) sources ++ - view - .listEntries(baseDir * AllPassFilter) + r.listEntries(baseDir * AllPassFilter) .flatMap { case e if filter(e) => e.value.toOption.map(Stamped.file(e.typedPath, _)) case _ => None From 571b179574e9a918814d0055f3ff824ebb4ec6d0 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Mon, 3 Dec 2018 18:28:00 -0800 Subject: [PATCH 06/23] Add dsl for collecting globs Right now, the sbt.internal.io.Source is something of a second class citizen within sbt. Since sbt 0.13, there have been extension classes defined that can convert a file to a PathFinder but no analog has been introduced for sbt.internal.io.Source. Given that sbt.internal.io.Source was not really intended to be part of the public api (just look at its package), I think it makes sense to just replace it with Glob. In this commit, I add extension methods to Glob and Seq[Glob] that make it possible to easily retrieve all of the files for a particular Glob within a task. The upshot is that where previously, we'd have had to write something like: watchSources += Source(baseDirectory.value / "src" / "main" / "proto", "*.proto", NothingFilter) now we can write watchGlobs += baseDirectory.value / "src" / "main" / "proto" * "*.proto" Moreover, within a task, we can now do something like: foo := { val allWatchGlobs: Seq[File] = watchGlobs.value.all println(allWatchSources.mkString("all watch source files:\n", "\n", "")) } Before we would have had to manually retrieve the files. The implementation of the dsl uses the new GlobExtractor class which proxies file look ups through a FileTree.Repository. This makes it so that, by default, all file i/o using Sources will use the default FileTree.Repository. The default is a macro that returns `sbt.Keys.fileTreeRepository.value: @sbtUnchecked`. By doing it this way, the default repository can only be used within a task definition (since it delegates to `fileTreeRepository.value`). It does not, however, prevent the user from explicitly providing a FileTree.Repository instance which the user is free to instantiate however they wish. Bonus: optimize imports in Def.scala and Defaults.scala --- .../util/appmacro/MacroDefaults.scala | 25 +++++ .../src/test/scala/sbt/WatchedSpec.scala | 1 - main-settings/src/main/scala/sbt/Def.scala | 5 +- main/src/main/scala/sbt/Defaults.scala | 53 +++++----- main/src/main/scala/sbt/Keys.scala | 1 + .../scala/sbt/internal/ExternalHooks.scala | 22 ++--- .../scala/sbt/internal/FileManagement.scala | 73 +------------- .../main/scala/sbt/internal/FileTree.scala | 53 ++++++++++ .../main/scala/sbt/internal/GlobLister.scala | 97 +++++++++++++++++++ sbt/src/main/scala/package.scala | 3 +- 10 files changed, 218 insertions(+), 115 deletions(-) create mode 100644 core-macros/src/main/scala/sbt/internal/util/appmacro/MacroDefaults.scala create mode 100644 main/src/main/scala/sbt/internal/FileTree.scala create mode 100644 main/src/main/scala/sbt/internal/GlobLister.scala diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/MacroDefaults.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/MacroDefaults.scala new file mode 100644 index 000000000..4de9b65e5 --- /dev/null +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/MacroDefaults.scala @@ -0,0 +1,25 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util.appmacro + +import scala.reflect.macros.blackbox + +object MacroDefaults { + + /** + * Macro to generated default file tree repository. It must be defined as an untyped tree because + * sbt.Keys is not available in this project. This is meant for internal use only, but must be + * public because its a macro. + * @param c the macro context + * @return the tree expressing the default file tree repository. + */ + def fileTreeRepository(c: blackbox.Context): c.Tree = { + import c.universe._ + q"sbt.Keys.fileTreeRepository.value: @sbtUnchecked" + } +} diff --git a/main-command/src/test/scala/sbt/WatchedSpec.scala b/main-command/src/test/scala/sbt/WatchedSpec.scala index 06dd64585..e0f51afa2 100644 --- a/main-command/src/test/scala/sbt/WatchedSpec.scala +++ b/main-command/src/test/scala/sbt/WatchedSpec.scala @@ -25,7 +25,6 @@ import scala.concurrent.duration._ class WatchedSpec extends FlatSpec with Matchers { object Defaults { - private val fileTreeViewConfig = FileTreeViewConfig.default(50.millis) def config( globs: Seq[Glob], fileEventMonitor: Option[FileEventMonitor[FileCacheEntry]] = None, diff --git a/main-settings/src/main/scala/sbt/Def.scala b/main-settings/src/main/scala/sbt/Def.scala index 59111588e..6a0d996e0 100644 --- a/main-settings/src/main/scala/sbt/Def.scala +++ b/main-settings/src/main/scala/sbt/Def.scala @@ -8,12 +8,13 @@ package sbt import sbt.internal.util.Types.const -import sbt.internal.util.{ Attributed, AttributeKey, Init, ConsoleAppender } +import sbt.internal.util.{ AttributeKey, Attributed, ConsoleAppender, Init } import sbt.util.Show import sbt.internal.util.complete.Parser import java.io.File import java.net.URI -import Scope.{ ThisScope, GlobalScope } + +import Scope.{ GlobalScope, ThisScope } import KeyRanks.{ DTask, Invisible } /** A concrete settings system that uses `sbt.Scope` for the scope type. */ diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 961ca0e97..c8a1993c9 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -47,8 +47,8 @@ import sbt.internal.util.Types._ import sbt.internal.util._ import sbt.internal.util.complete._ import sbt.io.Path._ -import sbt.io.syntax._ import sbt.io._ +import sbt.io.syntax._ import sbt.librarymanagement.Artifact.{ DocClassifier, SourceClassifier } import sbt.librarymanagement.Configurations.{ Compile, @@ -68,8 +68,8 @@ import sbt.testing.{ AnnotatedFingerprint, Framework, Runner, SubclassFingerprin import sbt.util.CacheImplicits._ import sbt.util.InterfaceUtil.{ toJavaFunction => f1 } import sbt.util._ -import sjsonnew.shaded.scalajson.ast.unsafe.JValue import sjsonnew._ +import sjsonnew.shaded.scalajson.ast.unsafe.JValue import xsbti.CrossValue import xsbti.compile.{ AnalysisContents, IncOptions, IncToolOptionsUtil } @@ -80,6 +80,7 @@ import scala.xml.NodeSeq // incremental compiler import sbt.SlashSyntax0._ +import sbt.internal.GlobLister._ import sbt.internal.inc.{ Analysis, AnalyzingCompiler, @@ -280,10 +281,14 @@ object Defaults extends BuildCommon { None }, watchStartMessage := Watched.defaultStartWatch, + fileTreeRepository := state.value + .get(Keys.globalFileTreeRepository) + .map(FileTree.repository) + .getOrElse(FileTree.Repository.polling), externalHooks := { - val view = FileManagement.dataView.value + val repository = fileTreeRepository.value compileOptions => - Some(ExternalHooks(compileOptions, view)) + Some(ExternalHooks(compileOptions, repository)) }, watchAntiEntropy :== new FiniteDuration(500, TimeUnit.MILLISECONDS), watchLogger := streams.value.log, @@ -373,13 +378,12 @@ object Defaults extends BuildCommon { crossPaths.value ) }, - unmanagedSources := FileManagement - .collectFiles( - unmanagedSourceDirectories, - includeFilter in unmanagedSources, - excludeFilter in unmanagedSources - ) - .value, + unmanagedSources := { + val filter = + (includeFilter in unmanagedSources).value -- (excludeFilter in unmanagedSources).value + val baseSources = if (sourcesInBase.value) baseDirectory.value * filter :: Nil else Nil + (unmanagedSourceDirectories.value.map(_ ** filter) ++ baseSources).all + }, watchSources in ConfigGlobal := (watchSources in ConfigGlobal).value ++ { val baseDir = baseDirectory.value val bases = unmanagedSourceDirectories.value @@ -413,13 +417,11 @@ object Defaults extends BuildCommon { resourceDirectories := Classpaths .concatSettings(unmanagedResourceDirectories, managedResourceDirectories) .value, - unmanagedResources := FileManagement - .collectFiles( - unmanagedResourceDirectories, - includeFilter in unmanagedResources, - excludeFilter in unmanagedResources - ) - .value, + unmanagedResources := { + val filter = + (includeFilter in unmanagedResources).value -- (excludeFilter in unmanagedResources).value + unmanagedResourceDirectories.value.map(_ ** filter).all + }, watchSources in ConfigGlobal := (watchSources in ConfigGlobal).value ++ { val bases = unmanagedResourceDirectories.value val include = (includeFilter in unmanagedResources).value @@ -433,7 +435,8 @@ object Defaults extends BuildCommon { managedResources := generate(resourceGenerators).value, resources := Classpaths.concat(managedResources, unmanagedResources).value ) - def addBaseSources = FileManagement.appendBaseSources + // This exists for binary compatibility and probably never should have been public. + def addBaseSources: Seq[Def.Setting[Task[Seq[File]]]] = Nil lazy val outputConfigPaths = Seq( classDirectory := crossTarget.value / (prefix(configuration.value.name) + "classes"), semanticdbTargetRoot := crossTarget.value / (prefix(configuration.value.name) + "meta"), @@ -1205,9 +1208,12 @@ object Defaults extends BuildCommon { } def collectFiles( dirs: ScopedTaskable[Seq[File]], - filter: ScopedTaskable[FileFilter], - excludes: ScopedTaskable[FileFilter] - ): Initialize[Task[Seq[File]]] = FileManagement.collectFiles(dirs, filter, excludes) + include: ScopedTaskable[FileFilter], + exclude: ScopedTaskable[FileFilter] + ): Initialize[Task[Seq[File]]] = Def.task { + val filter = include.toTask.value -- exclude.toTask.value + dirs.toTask.value.map(_ ** filter).all + } def artifactPathSetting(art: SettingKey[Artifact]): Initialize[File] = Def.setting { val f = artifactName.value @@ -1807,8 +1813,7 @@ object Defaults extends BuildCommon { ) :+ (classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.RuntimeDependencies) lazy val compileSettings: Seq[Setting[_]] = - configSettings ++ - (mainBgRunMainTask +: mainBgRunTask +: FileManagement.appendBaseSources) ++ + configSettings ++ (mainBgRunMainTask +: mainBgRunTask) ++ Classpaths.addUnmanagedLibrary ++ runtimeLayeringSettings private val testLayeringSettings: Seq[Setting[_]] = TaskRepository.proxy( diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 480bcfd9a..d7c20b91e 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -94,6 +94,7 @@ object Keys { val watch = SettingKey(BasicKeys.watch) val suppressSbtShellNotification = settingKey[Boolean]("""True to suppress the "Executing in batch mode.." message.""").withRank(CSetting) val enableGlobalCachingFileTreeRepository = settingKey[Boolean]("Toggles whether or not to create a global cache of the file system that can be used by tasks to quickly list a path").withRank(DSetting) + val fileTreeRepository = taskKey[FileTree.Repository]("A repository of the file system.") val pollInterval = settingKey[FiniteDuration]("Interval between checks for modified sources by the continuous execution command.").withRank(BMinusSetting) val pollingGlobs = settingKey[Seq[Glob]]("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) diff --git a/main/src/main/scala/sbt/internal/ExternalHooks.scala b/main/src/main/scala/sbt/internal/ExternalHooks.scala index 758c1b32d..fc86c5303 100644 --- a/main/src/main/scala/sbt/internal/ExternalHooks.scala +++ b/main/src/main/scala/sbt/internal/ExternalHooks.scala @@ -11,8 +11,9 @@ import java.util.Optional import sbt.Stamped import sbt.internal.inc.ExternalLookup +import sbt.io.FileTreeDataView.Entry import sbt.io.syntax._ -import sbt.io.{ AllPassFilter, FileTreeDataView, FileTreeRepository, TypedPath } +import sbt.io.{ AllPassFilter, Glob, TypedPath } import xsbti.compile._ import xsbti.compile.analysis.Stamp @@ -20,10 +21,8 @@ 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[FileCacheEntry] - ): DefaultExternalHooks = { + def apply(options: CompileOptions, repo: FileTree.Repository): DefaultExternalHooks = { + def listEntries(glob: Glob): Seq[Entry[FileCacheEntry]] = repo.get(glob) import scala.collection.JavaConverters._ val sources = options.sources() val cachedSources = new java.util.HashMap[File, Stamp] @@ -32,28 +31,19 @@ private[sbt] object ExternalHooks { case sf: Stamped => cachedSources.put(sf, sf.stamp) case f: File => cachedSources.put(f, converter(f)) } - view match { - case r: FileTreeRepository[FileCacheEntry] => - r.register(options.classesDirectory ** AllPassFilter) - options.classpath.foreach { - case f if f.getName.endsWith(".jar") => r.register(f.toGlob) - case f => r.register(f ** AllPassFilter) - } - case _ => - } val allBinaries = new java.util.HashMap[File, Stamp] options.classpath.foreach { case f if f.getName.endsWith(".jar") => // This gives us the entry for the path itself, which is necessary if the path is a jar file // rather than a directory. - view.listEntries(f.toGlob) foreach { e => + listEntries(f.toGlob) foreach { e => e.value match { case Right(value) => allBinaries.put(e.typedPath.toPath.toFile, value.stamp) case _ => } } case f => - view.listEntries(f ** "*.jar") foreach { e => + listEntries(f ** AllPassFilter) foreach { e => e.value match { case Right(value) => allBinaries.put(e.typedPath.toPath.toFile, value.stamp) case _ => diff --git a/main/src/main/scala/sbt/internal/FileManagement.scala b/main/src/main/scala/sbt/internal/FileManagement.scala index 9a4530d90..a2c327386 100644 --- a/main/src/main/scala/sbt/internal/FileManagement.scala +++ b/main/src/main/scala/sbt/internal/FileManagement.scala @@ -11,9 +11,8 @@ package internal import sbt.BasicCommandStrings.ContinuousExecutePrefix import sbt.Keys._ import sbt.internal.io.HybridPollingFileTreeRepository -import sbt.io.FileTreeDataView.{ Entry, Observable, Observer, Observers } -import sbt.io._ -import sbt.io.syntax._ +import sbt.io.FileTreeDataView.{ Observable, Observer, Observers } +import sbt.io.{ FileTreeRepository, _ } import sbt.util.Logger import scala.concurrent.duration._ @@ -85,76 +84,8 @@ private[sbt] object FileManagement { } } - private def entryFilter( - include: FileFilter, - exclude: FileFilter - ): Entry[FileCacheEntry] => Boolean = { 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.toPath.toString) { - override def isDirectory: Boolean = tp.isDirectory - override def isFile: Boolean = tp.isFile - } - include.accept(file) && !exclude.accept(file) - } private[sbt] def repo: Def.Initialize[Task[FileTreeRepository[FileCacheEntry]]] = Def.task { lazy val msg = s"Tried to get FileTreeRepository for uninitialized state." state.value.get(Keys.globalFileTreeRepository).getOrElse(throw new IllegalStateException(msg)) } - private[sbt] def dataView: Def.Initialize[Task[FileTreeDataView[FileCacheEntry]]] = Def.task { - state.value - .get(Keys.globalFileTreeRepository) - .map(toDataView) - .getOrElse(FileTreeView.DEFAULT.asDataView(FileCacheEntry.default)) - } - private def toDataView(r: FileTreeRepository[FileCacheEntry]): FileTreeDataView[FileCacheEntry] = - new FileTreeDataView[FileCacheEntry] { - private def reg(glob: Glob): FileTreeDataView[FileCacheEntry] = { r.register(glob); r } - override def listEntries(glob: Glob): Seq[Entry[FileCacheEntry]] = reg(glob).listEntries(glob) - override def list(glob: Glob): Seq[TypedPath] = reg(glob).list(glob) - override def close(): Unit = {} - } - 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: FileTreeDataView[FileCacheEntry] = dataView.value - val include = filter.toTask.value - val ex = excludes.toTask.value - val sourceFilter: Entry[FileCacheEntry] => Boolean = entryFilter(include, ex) - sourceDirs.flatMap { dir => - view - .listEntries(dir.toPath ** AllPassFilter) - .flatMap { - case e if sourceFilter(e) => e.value.toOption.map(Stamped.file(e.typedPath, _)) - case _ => None - } - } - } - - private[sbt] def appendBaseSources: Seq[Def.Setting[Task[Seq[File]]]] = Seq( - unmanagedSources := { - val sources = unmanagedSources.value - val include = (includeFilter in unmanagedSources).value - val excl = (excludeFilter in unmanagedSources).value - val baseDir = baseDirectory.value - val r: FileTreeDataView[FileCacheEntry] = dataView.value - if (sourcesInBase.value) { - val filter: Entry[FileCacheEntry] => Boolean = entryFilter(include, excl) - sources ++ - r.listEntries(baseDir * AllPassFilter) - .flatMap { - case e if filter(e) => e.value.toOption.map(Stamped.file(e.typedPath, _)) - case _ => None - } - } else sources - } - ) } diff --git a/main/src/main/scala/sbt/internal/FileTree.scala b/main/src/main/scala/sbt/internal/FileTree.scala new file mode 100644 index 000000000..ba2e63f4d --- /dev/null +++ b/main/src/main/scala/sbt/internal/FileTree.scala @@ -0,0 +1,53 @@ +/* + * 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.nio.file.{ WatchService => _ } + +import sbt.internal.util.appmacro.MacroDefaults +import sbt.io.FileTreeDataView.Entry +import sbt.io._ + +import scala.language.experimental.macros + +object FileTree { + trait Repository extends sbt.internal.Repository[Seq, Glob, Entry[FileCacheEntry]] + object Repository { + + /** + * Provide a default [[Repository]] that works within a task definition, e.g. Def.task. It's + * implemented as a macro so that it can call `.value` on a TaskKey. Using a macro also allows + * us to use classes that aren't actually available in this project, e.g. sbt.Keys. + * @return a [[Repository]] instance + */ + implicit def default: FileTree.Repository = macro MacroDefaults.fileTreeRepository + private[sbt] object polling extends Repository { + val view = FileTreeView.DEFAULT.asDataView(FileCacheEntry.default) + override def get(key: Glob): Seq[Entry[FileCacheEntry]] = view.listEntries(key) + override def close(): Unit = {} + } + } + private class ViewRepository(underlying: FileTreeDataView[FileCacheEntry]) extends Repository { + override def get(key: Glob): Seq[Entry[FileCacheEntry]] = underlying.listEntries(key) + override def close(): Unit = {} + } + private class CachingRepository(underlying: FileTreeRepository[FileCacheEntry]) + extends Repository { + override def get(key: Glob): Seq[Entry[FileCacheEntry]] = { + underlying.register(key) + underlying.listEntries(key) + } + override def close(): Unit = underlying.close() + } + private[sbt] def repository(underlying: FileTreeDataView[FileCacheEntry]): Repository = + underlying match { + case r: FileTreeRepository[FileCacheEntry] => new CachingRepository(r) + case v => new ViewRepository(v) + } +} diff --git a/main/src/main/scala/sbt/internal/GlobLister.scala b/main/src/main/scala/sbt/internal/GlobLister.scala new file mode 100644 index 000000000..cb9016d3c --- /dev/null +++ b/main/src/main/scala/sbt/internal/GlobLister.scala @@ -0,0 +1,97 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal + +import sbt.io.{ Glob, TypedPath } + +/** + * Retrieve files from a repository. This should usually be an extension class for + * sbt.io.internal.Glob (or a Traversable collection of source instances) that allows us to + * actually retrieve the files corresponding to those sources. + */ +sealed trait GlobLister extends Any { + + /** + * Get the sources described this [[GlobLister]]. + * + * @param repository the [[FileTree.Repository]] to delegate file i/o. + * @return the files described by this [[GlobLister]]. + */ + def all(implicit repository: FileTree.Repository): Seq[Stamped.File] + + /** + * Get the unique sources described this [[GlobLister]]. + * + * @param repository the [[FileTree.Repository]] to delegate file i/o. + * @return the files described by this [[GlobLister]] with any duplicates removed. + */ + def unique(implicit repository: FileTree.Repository): Seq[Stamped.File] +} + +/** + * Provides implicit definitions to provide a [[GlobLister]] given a Glob or + * Traversable[Glob]. + */ +object GlobLister extends GlobListers + +/** + * Provides implicit definitions to provide a [[GlobLister]] given a Glob or + * Traversable[Glob]. + */ +private[sbt] trait GlobListers { + import GlobListers._ + + /** + * Generate a [[GlobLister]] given a particular [[Glob]]s. + * + * @param source the input Glob + */ + implicit def fromGlob(source: Glob): GlobLister = new impl(source :: Nil) + + /** + * Generate a [[GlobLister]] given a collection of Globs. If the input collection type + * preserves uniqueness, e.g. `Set[Glob]`, then the output of [[GlobLister.all]] will be + * the unique source list. Otherwise duplicates are possible in all and it is necessary to call + * [[GlobLister.unique]] to de-duplicate the files. + * + * @param sources the collection of sources + * @tparam T the source collection type + */ + implicit def fromTraversableGlob[T <: Traversable[Glob]](sources: T): GlobLister = + new impl(sources) +} +private[internal] object GlobListers { + + /** + * Implements [[GlobLister]] given a collection of Globs. If the input collection type + * preserves uniqueness, e.g. `Set[Glob]`, then the output will be the unique source list. + * Otherwise duplicates are possible. + * + * @param globs the input globs + * @tparam T the collection type + */ + private class impl[T <: Traversable[Glob]](val globs: T) extends AnyVal with GlobLister { + private def get[T0 <: Traversable[Glob]]( + traversable: T0, + repository: FileTree.Repository + ): Seq[Stamped.File] = + traversable.flatMap { glob => + val sourceFilter: TypedPath => Boolean = glob.toTypedPathFilter + repository.get(glob).flatMap { + case e if sourceFilter(e.typedPath) => e.value.toOption.map(Stamped.file(e.typedPath, _)) + case _ => None + } + }.toIndexedSeq: Seq[Stamped.File] + + override def all(implicit repository: FileTree.Repository): Seq[Stamped.File] = + get(globs, repository) + override def unique(implicit repository: FileTree.Repository): Seq[Stamped.File] = + get(globs.toSet[Glob], repository) + } +} diff --git a/sbt/src/main/scala/package.scala b/sbt/src/main/scala/package.scala index df8aeab81..d85e366d2 100644 --- a/sbt/src/main/scala/package.scala +++ b/sbt/src/main/scala/package.scala @@ -21,7 +21,8 @@ package object sbt with sbt.BuildSyntax with sbt.OptionSyntax with sbt.SlashSyntax - with sbt.Import { + with sbt.Import + with sbt.internal.GlobListers { // IO def uri(s: String): URI = new URI(s) def file(s: String): File = new File(s) From c77a26e832f90aae81dad09cc8bb2085377b3b7a Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Sun, 13 Jan 2019 09:29:04 -0800 Subject: [PATCH 07/23] Update clean task to use globs The clean task is unreasonably slow because it does a lot of redundant io. In this commit, I update clean to be implemented using globs. This allows us to (optionally) route io through the file system cache. There is a significant performance improvement to this change. Currently, running clean on the sbt project takes O(6 seconds) on my machine. After this change, it takes O(1 second). To implement this, I added a new setting cleanKeepGlobs to replace cleanKeepFiles. I don't think that cleanKeepFiles returning Seq[File] is a big deal for performance because, by default, it just contains the history file so there isn't much benefit to accessing a single file through the cache. The reason I added the setting was more for consistency and to help push people towards globs in their own task implementations. Part of the performance improvement comes from inverting the problem. Before we would walk the file system tree from the base and recursively delete leafs and nodes in a depth first traversal. Now we collect all of the files that we are interested in deleting in advance. We then sort the results lexically by path name and then perform the deletions in that order. Because children will always comes first in this scheme, this will generally allow us to delete a directory. There is an edge case that if files are created in a subdirectory after we've created the list to delete, but before the subdirectory is deleted, then that subdirectory will not be deleted. In general, this will tend to impact target/streams because writes occur to target/streams during traversal. I don't think this really matters for most users. If the target directory is being concurrently modified with clean, then the user is doing something wrong. To ensure legacy compatibility, I re-implement cleanKeepFiles to return no files. Any plugin that was appending files to the cleanKeepFiles task with `+=` or `++=` will continue working as before because I explicitly add those files to the list to delete. I updated the actions/clean-keep scripted test to use both cleanKeepFiles and cleanKeepGlobs to ensure both tasks are correctly used. Bonus: add debug logging of all deleted files --- main/src/main/scala/sbt/Defaults.scala | 46 +++++++++++-------- main/src/main/scala/sbt/Keys.scala | 3 ++ sbt/src/sbt-test/actions/clean-keep/build.sbt | 4 +- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index c8a1993c9..ff8a28c88 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -9,6 +9,7 @@ package sbt import java.io.{ File, PrintWriter } import java.net.{ URI, URL } +import java.nio.file.{ DirectoryNotEmptyException, Files } import java.util.Optional import java.util.concurrent.{ Callable, TimeUnit } @@ -613,8 +614,9 @@ object Defaults extends BuildCommon { lazy val projectTasks: Seq[Setting[_]] = Seq( cleanFiles := cleanFilesTask.value, - cleanKeepFiles := historyPath.value.toVector, - clean := (Def.task { IO.delete(cleanFiles.value) } tag (Tags.Clean)).value, + cleanKeepFiles := Vector.empty, + cleanKeepGlobs := historyPath.value.map(_.toGlob).toSeq, + clean := (cleanTask tag Tags.Clean).value, consoleProject := consoleProjectTask.value, watchTransitiveSources := watchTransitiveSourcesTask.value, watchProjectTransitiveSources := watchTransitiveSourcesTaskImpl(watchProjectSources).value, @@ -1302,24 +1304,30 @@ object Defaults extends BuildCommon { } /** Implements `cleanFiles` task. */ - def cleanFilesTask: Initialize[Task[Vector[File]]] = - Def.task { - val filesAndDirs = Vector(managedDirectory.value, target.value) - val preserve = cleanKeepFiles.value - val (dirs, fs) = filesAndDirs.filter(_.exists).partition(_.isDirectory) - val preserveSet = preserve.filter(_.exists).toSet - // performance reasons, only the direct items under `filesAndDirs` are allowed to be preserved. - val dirItems = dirs flatMap { _.glob("*").get } - (preserveSet diff dirItems.toSet) match { - case xs if xs.isEmpty => () - case xs => - sys.error( - s"cleanKeepFiles contains directory/file that are not directly under cleanFiles: $xs" - ) - } - val toClean = (dirItems filterNot { preserveSet(_) }) ++ fs - toClean + def cleanFilesTask: Initialize[Task[Vector[File]]] = Def.task { Vector.empty[File] } + private[this] def cleanTask: Initialize[Task[Unit]] = Def.task { + val defaults = Seq(managedDirectory.value ** AllPassFilter, target.value ** AllPassFilter) + val excludes = cleanKeepFiles.value.map { + // This mimics the legacy behavior of cleanFilesTask + case f if f.isDirectory => f * AllPassFilter + case f => f.toGlob + } ++ cleanKeepGlobs.value + val excludeFilter: File => Boolean = excludes.toFileFilter.accept + val globDeletions = defaults.unique.filterNot(excludeFilter) + val toDelete = cleanFiles.value.filterNot(excludeFilter) match { + case f @ Seq(_, _*) => (globDeletions ++ f).distinct + case _ => globDeletions } + val logger = streams.value.log + toDelete.sorted.reverseIterator.foreach { f => + logger.debug(s"clean -- deleting file $f") + try Files.deleteIfExists(f.toPath) + catch { + case _: DirectoryNotEmptyException => + logger.debug(s"clean -- unable to delete non-empty directory $f") + } + } + } def bgRunMainTask( products: Initialize[Task[Classpath]], diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index d7c20b91e..05dced647 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -150,8 +150,11 @@ object Keys { // Output paths val classDirectory = settingKey[File]("Directory for compiled classes and copied resources.").withRank(AMinusSetting) + @deprecated("Clean is now implemented using globs.", "1.3.0") val cleanFiles = taskKey[Seq[File]]("The files to recursively delete during a clean.").withRank(BSetting) + @deprecated("Clean is now implemented using globs. Prefer the cleanKeepGlobs task", "1.3.0") val cleanKeepFiles = settingKey[Seq[File]]("Files or directories to keep during a clean. Must be direct children of target.").withRank(CSetting) + val cleanKeepGlobs = settingKey[Seq[Glob]]("Globs to keep during a clean. Must be direct children of target.").withRank(CSetting) val crossPaths = settingKey[Boolean]("If true, enables cross paths, which distinguish input and output directories for cross-building.").withRank(ASetting) val taskTemporaryDirectory = settingKey[File]("Directory used for temporary files for tasks that is deleted after each task execution.").withRank(DSetting) diff --git a/sbt/src/sbt-test/actions/clean-keep/build.sbt b/sbt/src/sbt-test/actions/clean-keep/build.sbt index 64de3093b..38f36e178 100644 --- a/sbt/src/sbt-test/actions/clean-keep/build.sbt +++ b/sbt/src/sbt-test/actions/clean-keep/build.sbt @@ -1,6 +1,6 @@ cleanKeepFiles ++= Seq( target.value / "keep", - target.value / "keepfile", - target.value / "keepdir" + target.value / "keepfile" ) +cleanKeepGlobs += target.value / "keepdir" ** AllPassFilter From 172c8e9a0baa90749f5ceb609a7e465c2c721d13 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Fri, 1 Feb 2019 18:33:07 -0800 Subject: [PATCH 08/23] Support custom clean tasks This rewroks the cleanTask so that it only removes a subset of the files in the target directory. To do this, I add a new task, outputs, that returns the glob representation of the possible output files for the task. It must be a task because some outputs will depend on streams. For each project, the default outputs are all of the files in baseDirectory / target. Long term, we could enhance the clean task to be automatically generated in any scope (as an input task). We could then add the option for the task scoped clean to delete all of the transitive outputs of the class. That is beyond the scope of this commit, however. I copied the scripted tests from #3678 and added an additional test to make sure that the manage source directory was explicitly cleaned. --- main/src/main/scala/sbt/Defaults.scala | 79 ++++++++++++------- main/src/main/scala/sbt/Keys.scala | 1 + .../sbt-test/actions/clean-managed/build.sbt | 7 ++ sbt/src/sbt-test/actions/clean-managed/test | 6 ++ .../sbt-test/actions/compile-clean/build.sbt | 2 + .../compile-clean/src/main/scala/A.scala | 3 + .../compile-clean/src/main/scala/X.scala | 3 + .../compile-clean/src/test/scala/B.scala | 3 + sbt/src/sbt-test/actions/compile-clean/test | 22 ++++++ 9 files changed, 99 insertions(+), 27 deletions(-) create mode 100644 sbt/src/sbt-test/actions/clean-managed/build.sbt create mode 100644 sbt/src/sbt-test/actions/clean-managed/test create mode 100644 sbt/src/sbt-test/actions/compile-clean/build.sbt create mode 100644 sbt/src/sbt-test/actions/compile-clean/src/main/scala/A.scala create mode 100644 sbt/src/sbt-test/actions/compile-clean/src/main/scala/X.scala create mode 100644 sbt/src/sbt-test/actions/compile-clean/src/test/scala/B.scala create mode 100644 sbt/src/sbt-test/actions/compile-clean/test diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index ff8a28c88..a9ce86ebc 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -405,6 +405,7 @@ object Defaults extends BuildCommon { managedSourceDirectories := Seq(sourceManaged.value), managedSources := generate(sourceGenerators).value, sourceGenerators :== Nil, + sourceGenerators / outputs := Seq(managedDirectory.value ** AllPassFilter), sourceDirectories := Classpaths .concatSettings(unmanagedSourceDirectories, managedSourceDirectories) .value, @@ -568,9 +569,14 @@ object Defaults extends BuildCommon { globalDefaults(enableBinaryCompileAnalysis := true) lazy val configTasks: Seq[Setting[_]] = docTaskSettings(doc) ++ inTask(compile)( - compileInputsSettings + compileInputsSettings :+ (clean := cleanTaskIn(ThisScope).value) ) ++ configGlobal ++ defaultCompileSettings ++ compileAnalysisSettings ++ Seq( + outputs := Seq( + compileAnalysisFileTask.value.toGlob, + classDirectory.value ** "*.class" + ) ++ (sourceGenerators / outputs).value, compile := compileTask.value, + clean := cleanTaskIn(ThisScope).value, manipulateBytecode := compileIncremental.value, compileIncremental := (compileIncrementalTask tag (Tags.Compile, Tags.CPU)).value, printWarnings := printWarningsTask.value, @@ -581,7 +587,7 @@ object Defaults extends BuildCommon { val extra = if (crossPaths.value) s"_$binVersion" else "" - s"inc_compile${extra}.zip" + s"inc_compile$extra.zip" }, compileIncSetup := compileIncSetupTask.value, console := consoleTask.value, @@ -616,7 +622,7 @@ object Defaults extends BuildCommon { cleanFiles := cleanFilesTask.value, cleanKeepFiles := Vector.empty, cleanKeepGlobs := historyPath.value.map(_.toGlob).toSeq, - clean := (cleanTask tag Tags.Clean).value, + clean := cleanTaskIn(ThisScope).value, consoleProject := consoleProjectTask.value, watchTransitiveSources := watchTransitiveSourcesTask.value, watchProjectTransitiveSources := watchTransitiveSourcesTaskImpl(watchProjectSources).value, @@ -657,6 +663,7 @@ object Defaults extends BuildCommon { }, watchStartMessage := Watched.projectOnWatchMessage(thisProjectRef.value.project), watch := watchSetting.value, + outputs += target.value ** AllPassFilter, ) def generate(generators: SettingKey[Seq[Task[Seq[File]]]]): Initialize[Task[Seq[File]]] = @@ -1304,30 +1311,45 @@ object Defaults extends BuildCommon { } /** Implements `cleanFiles` task. */ - def cleanFilesTask: Initialize[Task[Vector[File]]] = Def.task { Vector.empty[File] } - private[this] def cleanTask: Initialize[Task[Unit]] = Def.task { - val defaults = Seq(managedDirectory.value ** AllPassFilter, target.value ** AllPassFilter) - val excludes = cleanKeepFiles.value.map { - // This mimics the legacy behavior of cleanFilesTask - case f if f.isDirectory => f * AllPassFilter - case f => f.toGlob - } ++ cleanKeepGlobs.value - val excludeFilter: File => Boolean = excludes.toFileFilter.accept - val globDeletions = defaults.unique.filterNot(excludeFilter) - val toDelete = cleanFiles.value.filterNot(excludeFilter) match { - case f @ Seq(_, _*) => (globDeletions ++ f).distinct - case _ => globDeletions - } - val logger = streams.value.log - toDelete.sorted.reverseIterator.foreach { f => - logger.debug(s"clean -- deleting file $f") - try Files.deleteIfExists(f.toPath) - catch { - case _: DirectoryNotEmptyException => - logger.debug(s"clean -- unable to delete non-empty directory $f") + private[sbt] def cleanFilesTask: Initialize[Task[Vector[File]]] = Def.task { Vector.empty[File] } + + /** + * Provides an implementation for the clean task. It delegates to [[cleanTaskIn]] using the + * resolvedScoped key to set the scope. + * @return the clean task definition. + */ + def cleanTask: Initialize[Task[Unit]] = + Def.taskDyn(cleanTaskIn(resolvedScoped.value.scope)) tag Tags.Clean + + /** + * Implements the clean task in a given scope. It uses the outputs task value in the provided + * scope to determine which files to delete. + * @param scope the scope in which the clean task is implemented + * @return the clean task definition. + */ + def cleanTaskIn(scope: Scope): Initialize[Task[Unit]] = + Def.task { + val excludes = cleanKeepFiles.value.map { + // This mimics the legacy behavior of cleanFilesTask + case f if f.isDirectory => f * AllPassFilter + case f => f.toGlob + } ++ cleanKeepGlobs.value + val excludeFilter: File => Boolean = excludes.toFileFilter.accept + val globDeletions = (outputs in scope).value.unique.filterNot(excludeFilter) + val toDelete = cleanFiles.value.filterNot(excludeFilter) match { + case f @ Seq(_, _*) => (globDeletions ++ f).distinct + case _ => globDeletions } - } - } + val logger = streams.value.log + toDelete.sorted.reverseIterator.foreach { f => + logger.debug(s"clean -- deleting file $f") + try Files.deleteIfExists(f.toPath) + catch { + case _: DirectoryNotEmptyException => + logger.debug(s"clean -- unable to delete non-empty directory $f") + } + } + } tag Tags.Clean def bgRunMainTask( products: Initialize[Task[Classpath]], @@ -1636,6 +1658,8 @@ object Defaults extends BuildCommon { incCompiler.compile(i, s.log) } finally x.close() // workaround for #937 } + private def compileAnalysisFileTask: Def.Initialize[Task[File]] = + Def.task(streams.value.cacheDirectory / compileAnalysisFilename.value) def compileIncSetupTask = Def.task { val lookup = new PerClasspathEntryLookup { private val cachedAnalysisMap = analysisMap(dependencyClasspath.value) @@ -1650,7 +1674,7 @@ object Defaults extends BuildCommon { lookup, (skip in compile).value, // TODO - this is kind of a bad way to grab the cache directory for streams... - streams.value.cacheDirectory / compileAnalysisFilename.value, + compileAnalysisFileTask.value, compilerCache.value, incOptions.value, (compilerReporter in compile).value, @@ -2049,6 +2073,7 @@ object Classpaths { transitiveClassifiers :== Seq(SourceClassifier, DocClassifier), sourceArtifactTypes :== Artifact.DefaultSourceTypes.toVector, docArtifactTypes :== Artifact.DefaultDocTypes.toVector, + outputs :== Nil, sbtDependency := { val app = appConfiguration.value val id = app.provider.id diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 05dced647..9f144259f 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -157,6 +157,7 @@ object Keys { val cleanKeepGlobs = settingKey[Seq[Glob]]("Globs to keep during a clean. Must be direct children of target.").withRank(CSetting) val crossPaths = settingKey[Boolean]("If true, enables cross paths, which distinguish input and output directories for cross-building.").withRank(ASetting) val taskTemporaryDirectory = settingKey[File]("Directory used for temporary files for tasks that is deleted after each task execution.").withRank(DSetting) + val outputs = taskKey[Seq[Glob]]("Describes the output files of a task") // Generators val sourceGenerators = settingKey[Seq[Task[Seq[File]]]]("List of tasks that generate sources.").withRank(CSetting) diff --git a/sbt/src/sbt-test/actions/clean-managed/build.sbt b/sbt/src/sbt-test/actions/clean-managed/build.sbt new file mode 100644 index 000000000..ffa46f97d --- /dev/null +++ b/sbt/src/sbt-test/actions/clean-managed/build.sbt @@ -0,0 +1,7 @@ +Compile / sourceGenerators += Def.task { + val files = Seq(sourceManaged.value / "foo.txt", sourceManaged.value / "bar.txt") + files.foreach(IO.touch(_)) + files +} + +cleanKeepGlobs += (sourceManaged.value / "bar.txt").toGlob diff --git a/sbt/src/sbt-test/actions/clean-managed/test b/sbt/src/sbt-test/actions/clean-managed/test new file mode 100644 index 000000000..f6fd6ce8f --- /dev/null +++ b/sbt/src/sbt-test/actions/clean-managed/test @@ -0,0 +1,6 @@ +> compile +$ exists target/scala-2.12/src_managed/foo.txt target/scala-2.12/src_managed/bar.txt + +> clean +$ absent target/scala-2.12/src_managed/foo.txt +$ exists target/scala-2.12/src_managed/bar.txt diff --git a/sbt/src/sbt-test/actions/compile-clean/build.sbt b/sbt/src/sbt-test/actions/compile-clean/build.sbt new file mode 100644 index 000000000..2e29f6de6 --- /dev/null +++ b/sbt/src/sbt-test/actions/compile-clean/build.sbt @@ -0,0 +1,2 @@ +cleanKeepGlobs in Compile += + ((classDirectory in Compile in compile).value / "X.class").toGlob diff --git a/sbt/src/sbt-test/actions/compile-clean/src/main/scala/A.scala b/sbt/src/sbt-test/actions/compile-clean/src/main/scala/A.scala new file mode 100644 index 000000000..6da20a96e --- /dev/null +++ b/sbt/src/sbt-test/actions/compile-clean/src/main/scala/A.scala @@ -0,0 +1,3 @@ +class A { + val x: Int = 1 +} diff --git a/sbt/src/sbt-test/actions/compile-clean/src/main/scala/X.scala b/sbt/src/sbt-test/actions/compile-clean/src/main/scala/X.scala new file mode 100644 index 000000000..bd84382cd --- /dev/null +++ b/sbt/src/sbt-test/actions/compile-clean/src/main/scala/X.scala @@ -0,0 +1,3 @@ +class X { + val y: Int = 0 +} diff --git a/sbt/src/sbt-test/actions/compile-clean/src/test/scala/B.scala b/sbt/src/sbt-test/actions/compile-clean/src/test/scala/B.scala new file mode 100644 index 000000000..4e79fe7a2 --- /dev/null +++ b/sbt/src/sbt-test/actions/compile-clean/src/test/scala/B.scala @@ -0,0 +1,3 @@ +class B { + val x: Int = 2 +} diff --git a/sbt/src/sbt-test/actions/compile-clean/test b/sbt/src/sbt-test/actions/compile-clean/test new file mode 100644 index 000000000..2e805ffd0 --- /dev/null +++ b/sbt/src/sbt-test/actions/compile-clean/test @@ -0,0 +1,22 @@ +$ touch target/cant-touch-this + +> Test/compile +$ exists target/scala-2.12/classes/A.class +$ exists target/scala-2.12/test-classes/B.class + +> Test/clean +$ exists target/cant-touch-this +# it should clean only compile classes +$ exists target/scala-2.12/classes/A.class +$ exists target/scala-2.12/classes/X.class +$ absent target/scala-2.12/test-classes/B.class + +# compiling everything again, but now cleaning only compile classes +> Test/compile +> Compile/clean +$ exists target/cant-touch-this +# it should clean only compile classes +$ absent target/scala-2.12/classes/A.class +$ exists target/scala-2.12/test-classes/B.class +# and X has to be kept, because of the cleanKeepFiles override +$ exists target/scala-2.12/classes/X.class From de3c09f99d9fa6bcb3df25fabaf8a6f3a7ae3543 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Sat, 2 Feb 2019 12:02:38 -0800 Subject: [PATCH 09/23] Move clean task implementation to Clean.scala The Defaults.scala file has a lot going on. I am trying to generally follow the pattern of implementing the default task implementation in a different file and just adding the appropriate declarations in Defaults.scala. --- main/src/main/scala/sbt/Defaults.scala | 45 +------------- main/src/main/scala/sbt/internal/Clean.scala | 62 ++++++++++++++++++++ 2 files changed, 65 insertions(+), 42 deletions(-) create mode 100644 main/src/main/scala/sbt/internal/Clean.scala diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index a9ce86ebc..cac5e7338 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -9,7 +9,6 @@ package sbt import java.io.{ File, PrintWriter } import java.net.{ URI, URL } -import java.nio.file.{ DirectoryNotEmptyException, Files } import java.util.Optional import java.util.concurrent.{ Callable, TimeUnit } @@ -569,14 +568,14 @@ object Defaults extends BuildCommon { globalDefaults(enableBinaryCompileAnalysis := true) lazy val configTasks: Seq[Setting[_]] = docTaskSettings(doc) ++ inTask(compile)( - compileInputsSettings :+ (clean := cleanTaskIn(ThisScope).value) + compileInputsSettings :+ (clean := Clean.taskIn(ThisScope).value) ) ++ configGlobal ++ defaultCompileSettings ++ compileAnalysisSettings ++ Seq( outputs := Seq( compileAnalysisFileTask.value.toGlob, classDirectory.value ** "*.class" ) ++ (sourceGenerators / outputs).value, compile := compileTask.value, - clean := cleanTaskIn(ThisScope).value, + clean := Clean.taskIn(ThisScope).value, manipulateBytecode := compileIncremental.value, compileIncremental := (compileIncrementalTask tag (Tags.Compile, Tags.CPU)).value, printWarnings := printWarningsTask.value, @@ -622,7 +621,7 @@ object Defaults extends BuildCommon { cleanFiles := cleanFilesTask.value, cleanKeepFiles := Vector.empty, cleanKeepGlobs := historyPath.value.map(_.toGlob).toSeq, - clean := cleanTaskIn(ThisScope).value, + clean := Clean.taskIn(ThisScope).value, consoleProject := consoleProjectTask.value, watchTransitiveSources := watchTransitiveSourcesTask.value, watchProjectTransitiveSources := watchTransitiveSourcesTaskImpl(watchProjectSources).value, @@ -1313,44 +1312,6 @@ object Defaults extends BuildCommon { /** Implements `cleanFiles` task. */ private[sbt] def cleanFilesTask: Initialize[Task[Vector[File]]] = Def.task { Vector.empty[File] } - /** - * Provides an implementation for the clean task. It delegates to [[cleanTaskIn]] using the - * resolvedScoped key to set the scope. - * @return the clean task definition. - */ - def cleanTask: Initialize[Task[Unit]] = - Def.taskDyn(cleanTaskIn(resolvedScoped.value.scope)) tag Tags.Clean - - /** - * Implements the clean task in a given scope. It uses the outputs task value in the provided - * scope to determine which files to delete. - * @param scope the scope in which the clean task is implemented - * @return the clean task definition. - */ - def cleanTaskIn(scope: Scope): Initialize[Task[Unit]] = - Def.task { - val excludes = cleanKeepFiles.value.map { - // This mimics the legacy behavior of cleanFilesTask - case f if f.isDirectory => f * AllPassFilter - case f => f.toGlob - } ++ cleanKeepGlobs.value - val excludeFilter: File => Boolean = excludes.toFileFilter.accept - val globDeletions = (outputs in scope).value.unique.filterNot(excludeFilter) - val toDelete = cleanFiles.value.filterNot(excludeFilter) match { - case f @ Seq(_, _*) => (globDeletions ++ f).distinct - case _ => globDeletions - } - val logger = streams.value.log - toDelete.sorted.reverseIterator.foreach { f => - logger.debug(s"clean -- deleting file $f") - try Files.deleteIfExists(f.toPath) - catch { - case _: DirectoryNotEmptyException => - logger.debug(s"clean -- unable to delete non-empty directory $f") - } - } - } tag Tags.Clean - def bgRunMainTask( products: Initialize[Task[Classpath]], classpath: Initialize[Task[Classpath]], diff --git a/main/src/main/scala/sbt/internal/Clean.scala b/main/src/main/scala/sbt/internal/Clean.scala new file mode 100644 index 000000000..f66323e74 --- /dev/null +++ b/main/src/main/scala/sbt/internal/Clean.scala @@ -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.io.IOException +import java.nio.file.{ DirectoryNotEmptyException, Files } + +import sbt.Def._ +import sbt.Keys._ +import sbt.Project.richInitializeTask +import sbt.internal.GlobLister._ +import sbt.io.AllPassFilter +import sbt.io.syntax._ + +object Clean { + + /** + * Provides an implementation for the clean task. It delegates to [[taskIn]] using the + * resolvedScoped key to set the scope. + * @return the clean task definition. + */ + def task: Def.Initialize[Task[Unit]] = + Def.taskDyn(taskIn(Keys.resolvedScoped.value.scope)) tag Tags.Clean + + /** + * Implements the clean task in a given scope. It uses the outputs task value in the provided + * scope to determine which files to delete. + * @param scope the scope in which the clean task is implemented + * @return the clean task definition. + */ + def taskIn(scope: Scope): Def.Initialize[Task[Unit]] = + Def.task { + val excludes = cleanKeepFiles.value.map { + // This mimics the legacy behavior of cleanFilesTask + case f if f.isDirectory => f * AllPassFilter + case f => f.toGlob + } ++ cleanKeepGlobs.value + val excludeFilter: File => Boolean = excludes.toFileFilter.accept + val globDeletions = (outputs in scope).value.unique.filterNot(excludeFilter) + val toDelete = cleanFiles.value.filterNot(excludeFilter) match { + case f @ Seq(_, _*) => (globDeletions ++ f).distinct + case _ => globDeletions + } + val logger = streams.value.log + toDelete.sorted.reverseIterator.foreach { f => + logger.debug(s"clean -- deleting file $f") + try Files.deleteIfExists(f.toPath) + catch { + case _: DirectoryNotEmptyException => + logger.debug(s"clean -- unable to delete non-empty directory $f") + case e: IOException => + logger.debug(s"Caught unexpected exception $e deleting $f") + } + } + } tag Tags.Clean +} From 16afe41cc17e2842074c76f34c39cf4a8b429da8 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Mon, 4 Feb 2019 20:28:45 -0800 Subject: [PATCH 10/23] Don't try to stamp files that don't exist This was causing slowdowns in windows. --- main-command/src/main/scala/sbt/Stamped.scala | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/main-command/src/main/scala/sbt/Stamped.scala b/main-command/src/main/scala/sbt/Stamped.scala index cf8f63e55..8e3f5fa26 100644 --- a/main-command/src/main/scala/sbt/Stamped.scala +++ b/main-command/src/main/scala/sbt/Stamped.scala @@ -11,7 +11,7 @@ import java.io.{ File => JFile } import java.nio.file.Path import sbt.internal.FileCacheEntry -import sbt.internal.inc.Stamper +import sbt.internal.inc.{ EmptyStamp, Stamper } import sbt.io.TypedPath import xsbti.compile.analysis.Stamp @@ -47,13 +47,14 @@ private[sbt] object Stamped { * A combined convert that converts TypedPath instances representing *.jar and *.class files * using the last modified time and all other files using the file hash. */ - val converter: TypedPath => Stamp = (tp: TypedPath) => - 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) + val converter: TypedPath => Stamp = (_: TypedPath) match { + case typedPath if !typedPath.exists => EmptyStamp + case typedPath if typedPath.isDirectory => binaryConverter(typedPath) + case typedPath => + typedPath.toPath.toString match { + case s if s.endsWith(".jar") => binaryConverter(typedPath) + case s if s.endsWith(".class") => binaryConverter(typedPath) + case _ => sourceConverter(typedPath) } } From b0c5e00c7c852bda1799c28b6a4495c4f794e9bb Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 6 Feb 2019 14:17:01 -0800 Subject: [PATCH 11/23] Reimplement clean I ran into a couple of issues with the clean implementation. I changed the logging to print to stdout instead of streams if enabled. I also added a helper, Clean.deleteContents that recursively deletes all of the contents of a directory except for those that match the exclude filter parameter. Using a normal logger was a bad idea because we are actually deleting the target/streams directory when running clean. The previous implementation worked by getting the full list of files to delete, reverse sorting it and then deleting every element in the list. While this can work well it many circumstances, if the directory is still being written to during the recursive deletion, then we could miss files that were added after we fetched all of the files. The new version lazily lists the subdirectories as needed. --- main/src/main/scala/sbt/Defaults.scala | 4 +- main/src/main/scala/sbt/internal/Clean.scala | 66 +++++++++++++++----- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index cac5e7338..f2646f50a 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -256,9 +256,9 @@ object Defaults extends BuildCommon { skip :== false, taskTemporaryDirectory := { val dir = IO.createTemporaryDirectory; dir.deleteOnExit(); dir }, onComplete := { - val dir = taskTemporaryDirectory.value; + val tempDirectory = taskTemporaryDirectory.value () => - { IO.delete(dir); IO.createDirectory(dir) } + Clean.deleteContents(tempDirectory, _ => false) }, useSuperShell :== sbt.internal.TaskProgress.isEnabled, progressReports := { (s: State) => diff --git a/main/src/main/scala/sbt/internal/Clean.scala b/main/src/main/scala/sbt/internal/Clean.scala index f66323e74..83629d505 100644 --- a/main/src/main/scala/sbt/internal/Clean.scala +++ b/main/src/main/scala/sbt/internal/Clean.scala @@ -14,12 +14,31 @@ import java.nio.file.{ DirectoryNotEmptyException, Files } import sbt.Def._ import sbt.Keys._ import sbt.Project.richInitializeTask -import sbt.internal.GlobLister._ -import sbt.io.AllPassFilter import sbt.io.syntax._ +import sbt.io.{ AllPassFilter, FileTreeView, TypedPath } +import sbt.util.Level object Clean { + def deleteContents(file: File, exclude: TypedPath => Boolean): Unit = + deleteContents(file, exclude, FileTreeView.DEFAULT, tryDelete((_: String) => {})) + def deleteContents( + file: File, + exclude: TypedPath => Boolean, + view: FileTreeView, + delete: File => Unit + ): Unit = { + def deleteRecursive(file: File): Unit = { + view.list(file * AllPassFilter).filterNot(exclude).foreach { + case dir if dir.isDirectory => + deleteRecursive(dir.toPath.toFile) + delete(dir.toPath.toFile) + case f => delete(f.toPath.toFile) + } + } + deleteRecursive(file) + } + /** * Provides an implementation for the clean task. It delegates to [[taskIn]] using the * resolvedScoped key to set the scope. @@ -41,22 +60,37 @@ object Clean { case f if f.isDirectory => f * AllPassFilter case f => f.toGlob } ++ cleanKeepGlobs.value - val excludeFilter: File => Boolean = excludes.toFileFilter.accept - val globDeletions = (outputs in scope).value.unique.filterNot(excludeFilter) - val toDelete = cleanFiles.value.filterNot(excludeFilter) match { - case f @ Seq(_, _*) => (globDeletions ++ f).distinct - case _ => globDeletions + val excludeFilter: TypedPath => Boolean = excludes.toTypedPathFilter + val debug = (logLevel in scope).?.value.orElse(state.value.get(logLevel.key)) match { + case Some(Level.Debug) => + (string: String) => + println(s"[debug] $string") + case _ => + (_: String) => + {} } - val logger = streams.value.log - toDelete.sorted.reverseIterator.foreach { f => - logger.debug(s"clean -- deleting file $f") - try Files.deleteIfExists(f.toPath) - catch { - case _: DirectoryNotEmptyException => - logger.debug(s"clean -- unable to delete non-empty directory $f") - case e: IOException => - logger.debug(s"Caught unexpected exception $e deleting $f") + val delete = tryDelete(debug) + cleanFiles.value.sorted.reverseIterator.foreach(delete) + (outputs in scope).value.foreach { g => + val filter: TypedPath => Boolean = { + val globFilter = g.toTypedPathFilter + tp => + !globFilter(tp) || excludeFilter(tp) } + deleteContents(g.base.toFile, filter, FileTreeView.DEFAULT, delete) + delete(g.base.toFile) } } tag Tags.Clean + private def tryDelete(debug: String => Unit): File => Unit = file => { + try { + debug(s"clean -- deleting file $file") + Files.deleteIfExists(file.toPath) + () + } catch { + case _: DirectoryNotEmptyException => + debug(s"clean -- unable to delete non-empty directory $file") + case e: IOException => + debug(s"Caught unexpected exception $e deleting $file") + } + } } From e3625ca8b0781288f379eff6d6e6534998d64200 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 6 Feb 2019 14:23:54 -0800 Subject: [PATCH 12/23] Fix repository setup on windows Windows io really doesn't handle concurrent readers and writers all that well. Using the LegacyFileTreeRepository was problematic in windows scripted tests because even though the repository implementation did not use the cache in its list methods, it did persistently monitor the directories that were registered. The monitor has to do a lot of io on a background thread to maintain the cache. This caused io contention that would cause IO.createDirectory to fail with an obscure AccessDeniedException. The way to avoid this is to prevent the background io from occurring at all. I don't necessarily think this will impact most users running sbt interactively with a cache, but it did cause scripted tests to fail. For that reason I made the default in non-interactive/shell use cases on windows to be a PollingFileRepository which never monitors the file system except when we are in a watch. The LegacyFileTreeRepository works fine on mac and linux which have a more forgiving file system. To make this work, I had to add FileManagement.toMonitoringRepository. There are now two kinds of repositories that cannot monitor on their own: HybridPollingFileTreeRepository and PollingFileRepository. The FileManagement.toMonitoringRepository makes a new repository that turns on monitoring for those two repository types and disables the close method on all other repositories so that closing the FileEventMonitor does not actually close the global file repository. --- main/src/main/scala/sbt/Defaults.scala | 2 +- .../scala/sbt/internal/FileManagement.scala | 118 ++++++++++++++++-- 2 files changed, 107 insertions(+), 13 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index f2646f50a..46c43c360 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -646,7 +646,7 @@ object Defaults extends BuildCommon { ) .getOrElse(watchTriggeredMessage.value) val logger = watchLogger.value - val repo = FileManagement.repo.value + val repo = FileManagement.toMonitoringRepository(FileManagement.repo.value) globs.foreach(repo.register) val monitor = FileManagement.monitor(repo, watchAntiEntropy.value, logger) WatchConfig.default( diff --git a/main/src/main/scala/sbt/internal/FileManagement.scala b/main/src/main/scala/sbt/internal/FileManagement.scala index a2c327386..348d933df 100644 --- a/main/src/main/scala/sbt/internal/FileManagement.scala +++ b/main/src/main/scala/sbt/internal/FileManagement.scala @@ -8,13 +8,19 @@ package sbt package internal +import java.io.IOException +import java.util.concurrent.ConcurrentHashMap + import sbt.BasicCommandStrings.ContinuousExecutePrefix import sbt.Keys._ import sbt.internal.io.HybridPollingFileTreeRepository -import sbt.io.FileTreeDataView.{ Observable, Observer, Observers } +import sbt.internal.util.Util +import sbt.io.FileTreeDataView.{ Entry, Observable, Observer, Observers } import sbt.io.{ FileTreeRepository, _ } -import sbt.util.Logger +import sbt.util.{ Level, Logger } +import scala.collection.JavaConverters._ +import scala.collection.mutable import scala.concurrent.duration._ private[sbt] object FileManagement { @@ -34,15 +40,26 @@ private[sbt] object FileManagement { val enableCache = extracted .getOpt(Keys.enableGlobalCachingFileTreeRepository) .getOrElse(!scripted && (interactive || continuous)) + val pollInterval = extracted.getOpt(Keys.pollInterval).getOrElse(500.milliseconds) + val watchLogger: WatchLogger = extracted.getOpt(Keys.logLevel) match { + case Level.Debug => + new WatchLogger { override def debug(msg: => Any): Unit = println(s"[watch-debug] $msg") } + case _ => new WatchLogger { override def debug(msg: => Any): Unit = {} } + } if (enableCache) { if (pollingGlobs.isEmpty) FileTreeRepository.default(FileCacheEntry.default) - else FileTreeRepository.hybrid(FileCacheEntry.default, pollingGlobs: _*) + else + new HybridMonitoringRepository[FileCacheEntry]( + FileTreeRepository.hybrid(FileCacheEntry.default, pollingGlobs: _*), + pollInterval, + watchLogger + ) } else { - FileTreeRepository.legacy( - FileCacheEntry.default, - (_: Any) => {}, - Watched.createWatchService(extracted.getOpt(Keys.pollInterval).getOrElse(500.milliseconds)) - ) + if (Util.isWindows) new PollingFileRepository(FileCacheEntry.default) + else { + val service = Watched.createWatchService(pollInterval) + FileTreeRepository.legacy(FileCacheEntry.default _, (_: Any) => {}, service) + } } } @@ -55,10 +72,10 @@ private[sbt] object FileManagement { // callbacks. val copied: Observable[FileCacheEntry] = new Observable[FileCacheEntry] { private[this] val observers = new Observers[FileCacheEntry] - val (underlying, needClose) = repository match { + val underlying = repository match { case h: HybridPollingFileTreeRepository[FileCacheEntry] => - (h.toPollingRepository(antiEntropy, (msg: Any) => logger.debug(msg.toString)), true) - case r => (r, false) + h.toPollingRepository(antiEntropy, (msg: Any) => logger.debug(msg.toString)) + case r => r } private[this] val handle = underlying.addObserver(observers) override def addObserver(observer: Observer[FileCacheEntry]): Int = @@ -66,7 +83,7 @@ private[sbt] object FileManagement { override def removeObserver(handle: Int): Unit = observers.removeObserver(handle) override def close(): Unit = { underlying.removeObserver(handle) - if (needClose) underlying.close() + underlying.close() } } new FileEventMonitor[FileCacheEntry] { @@ -88,4 +105,81 @@ private[sbt] object FileManagement { lazy val msg = s"Tried to get FileTreeRepository for uninitialized state." state.value.get(Keys.globalFileTreeRepository).getOrElse(throw new IllegalStateException(msg)) } + + private[sbt] class HybridMonitoringRepository[T]( + underlying: HybridPollingFileTreeRepository[T], + delay: FiniteDuration, + logger: WatchLogger + ) extends FileTreeRepository[T] { + private val registered: mutable.Set[Glob] = ConcurrentHashMap.newKeySet[Glob].asScala + override def listEntries(glob: Glob): Seq[Entry[T]] = underlying.listEntries(glob) + override def list(glob: Glob): Seq[TypedPath] = underlying.list(glob) + override def addObserver(observer: Observer[T]): Int = underlying.addObserver(observer) + override def removeObserver(handle: Int): Unit = underlying.removeObserver(handle) + override def close(): Unit = underlying.close() + override def register(glob: Glob): Either[IOException, Boolean] = { + registered.add(glob) + underlying.register(glob) + } + override def unregister(glob: Glob): Unit = underlying.unregister(glob) + private[sbt] def toMonitoringRepository: FileTreeRepository[T] = { + val polling = underlying.toPollingRepository(delay, logger) + registered.foreach(polling.register) + polling + } + } + private[sbt] def toMonitoringRepository[T]( + repository: FileTreeRepository[T] + ): FileTreeRepository[T] = repository match { + case p: PollingFileRepository[T] => p.toMonitoringRepository + case h: HybridMonitoringRepository[T] => h.toMonitoringRepository + case r: FileTreeRepository[T] => new CopiedFileRepository(r) + } + private class CopiedFileRepository[T](underlying: FileTreeRepository[T]) + extends FileTreeRepository[T] { + def addObserver(observer: Observer[T]) = underlying.addObserver(observer) + def close(): Unit = {} // Don't close the underlying observable + def list(glob: Glob): Seq[TypedPath] = underlying.list(glob) + def listEntries(glob: Glob): Seq[Entry[T]] = underlying.listEntries(glob) + def removeObserver(handle: Int): Unit = underlying.removeObserver(handle) + def register(glob: Glob): Either[IOException, Boolean] = underlying.register(glob) + def unregister(glob: Glob): Unit = underlying.unregister(glob) + } + private[sbt] class PollingFileRepository[T](converter: TypedPath => T) + extends FileTreeRepository[T] { self => + private val registered: mutable.Set[Glob] = ConcurrentHashMap.newKeySet[Glob].asScala + private[this] val view = FileTreeView.DEFAULT + private[this] val dataView = view.asDataView(converter) + private[this] val handles: mutable.Map[FileTreeRepository[T], Int] = + new ConcurrentHashMap[FileTreeRepository[T], Int].asScala + private val observers: Observers[T] = new Observers + override def addObserver(observer: Observer[T]): Int = observers.addObserver(observer) + override def close(): Unit = { + handles.foreach { case (repo, handle) => repo.removeObserver(handle) } + observers.close() + } + override def list(glob: Glob): Seq[TypedPath] = view.list(glob) + override def listEntries(glob: Glob): Seq[Entry[T]] = dataView.listEntries(glob) + override def removeObserver(handle: Int): Unit = observers.removeObserver(handle) + override def register(glob: Glob): Either[IOException, Boolean] = Right(registered.add(glob)) + override def unregister(glob: Glob): Unit = registered -= glob + + private[sbt] def toMonitoringRepository: FileTreeRepository[T] = { + val legacy = FileTreeRepository.legacy(converter) + registered.foreach(legacy.register) + handles += legacy -> legacy.addObserver(observers) + new FileTreeRepository[T] { + override def listEntries(glob: Glob): Seq[Entry[T]] = legacy.listEntries(glob) + override def list(glob: Glob): Seq[TypedPath] = legacy.list(glob) + def addObserver(observer: Observer[T]): Int = legacy.addObserver(observer) + override def removeObserver(handle: Int): Unit = legacy.removeObserver(handle) + override def close(): Unit = legacy.close() + override def register(glob: Glob): Either[IOException, Boolean] = { + self.register(glob) + legacy.register(glob) + } + override def unregister(glob: Glob): Unit = legacy.unregister(glob) + } + } + } } From 47b85b98c04daa926ac0035b3e9a80e0fc9fe03f Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 6 Feb 2019 14:16:26 -0800 Subject: [PATCH 13/23] Run windows tests on visual studio 2015 and 2017 I've noticed that it's possible to write code that works on one but not the other. --- .appveyor.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index ccc70ab7c..ff93085b6 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,3 +1,6 @@ +image: +- Visual Studio 2015 +- Visual Studio 2017 build: off init: From 1489879b80c1b1af9832936a410d5264f038d168 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Thu, 7 Feb 2019 17:03:41 -0800 Subject: [PATCH 14/23] Fix equals for FileCacheEntry The equals method didn't work exactly the way I thought. By delegating to the equivStamp object in sbt we can be more confident that it is actually comparing the stamp values and not object references or some other equals implementation. --- .../src/main/scala/sbt/internal/FileCacheEntry.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala b/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala index 417f49136..f7c0e71a4 100644 --- a/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala +++ b/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala @@ -36,6 +36,9 @@ object FileCacheEntry { } } + private implicit class Equiv(val xstamp: XStamp) extends AnyVal { + def equiv(that: XStamp): Boolean = Stamp.equivStamp.equiv(xstamp, that) + } private case class DelegateFileCacheEntry(private val stamp: XStamp) extends FileCacheEntry with XStamp { @@ -52,9 +55,9 @@ object FileCacheEntry { 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 + case DelegateFileCacheEntry(thatStamp) => this.stamp equiv thatStamp + case xStamp: XStamp => this.stamp equiv xStamp + case _ => false } override def hashCode: Int = stamp.hashCode override def toString: String = s"FileCacheEntry(hash = $hash, lastModified = $lastModified)" From 798145e81e6bb6e6fb404656d66eb71db4665a24 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Mon, 18 Feb 2019 14:00:48 -0800 Subject: [PATCH 15/23] Add sbt io to the sbt project Without this, the sbt io version is used by the compiler which means that apis added in later versions of io are not available. I don't understand why the transitive dependency on io is not used, but this fixes the issue. --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 1f13669c9..d761f7f1d 100644 --- a/build.sbt +++ b/build.sbt @@ -648,7 +648,7 @@ lazy val sbtProj = (project in file("sbt")) Test / run / outputStrategy := Some(StdoutOutput), Test / run / fork := true, ) - .configure(addSbtCompilerBridge) + .configure(addSbtIO, addSbtCompilerBridge) lazy val sbtBig = (project in file(".big")) .dependsOn(sbtProj) From fac6e0d9a01992ca7267a940ea5310a300183c13 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Mon, 18 Feb 2019 14:02:17 -0800 Subject: [PATCH 16/23] Make file repository apis private[sbt] I haven't fully settled on these interfaces yet so they shouldn't be publicly exposed in the sbt api. --- main-command/src/main/scala/sbt/Stamped.scala | 13 +++++++------ main/src/main/scala/sbt/internal/FileTree.scala | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/main-command/src/main/scala/sbt/Stamped.scala b/main-command/src/main/scala/sbt/Stamped.scala index 8e3f5fa26..32fad1465 100644 --- a/main-command/src/main/scala/sbt/Stamped.scala +++ b/main-command/src/main/scala/sbt/Stamped.scala @@ -29,25 +29,26 @@ private[sbt] trait Stamped { * Provides converter functions from TypedPath to [[Stamped]]. */ private[sbt] object Stamped { - type File = JFile with Stamped with TypedPath - def file(typedPath: TypedPath, entry: FileCacheEntry): JFile with Stamped with TypedPath = + type File = JFile with Stamped + def file(typedPath: TypedPath, entry: FileCacheEntry): JFile with Stamped = new StampedFileImpl(typedPath, entry.stamp) /** * Converts a TypedPath instance to a [[Stamped]] by calculating the file hash. */ - val sourceConverter: TypedPath => Stamp = tp => Stamper.forHash(tp.toPath.toFile) + private[sbt] val sourceConverter: TypedPath => Stamp = tp => Stamper.forHash(tp.toPath.toFile) /** * Converts a TypedPath instance to a [[Stamped]] using the last modified time. */ - val binaryConverter: TypedPath => Stamp = tp => Stamper.forLastModified(tp.toPath.toFile) + private[sbt] val binaryConverter: TypedPath => Stamp = tp => + Stamper.forLastModified(tp.toPath.toFile) /** * A combined convert that converts TypedPath instances representing *.jar and *.class files * using the last modified time and all other files using the file hash. */ - val converter: TypedPath => Stamp = (_: TypedPath) match { + private[sbt] val converter: TypedPath => Stamp = (_: TypedPath) match { case typedPath if !typedPath.exists => EmptyStamp case typedPath if typedPath.isDirectory => binaryConverter(typedPath) case typedPath => @@ -61,7 +62,7 @@ private[sbt] object Stamped { /** * Adds a default ordering that just delegates to the java.io.File.compareTo method. */ - implicit case object ordering extends Ordering[Stamped.File] { + private[sbt] implicit case object ordering extends Ordering[Stamped.File] { override def compare(left: Stamped.File, right: Stamped.File): Int = left.compareTo(right) } diff --git a/main/src/main/scala/sbt/internal/FileTree.scala b/main/src/main/scala/sbt/internal/FileTree.scala index ba2e63f4d..292515ae2 100644 --- a/main/src/main/scala/sbt/internal/FileTree.scala +++ b/main/src/main/scala/sbt/internal/FileTree.scala @@ -16,9 +16,9 @@ import sbt.io._ import scala.language.experimental.macros -object FileTree { - trait Repository extends sbt.internal.Repository[Seq, Glob, Entry[FileCacheEntry]] - object Repository { +private[sbt] object FileTree { + private[sbt] trait Repository extends sbt.internal.Repository[Seq, Glob, Entry[FileCacheEntry]] + private[sbt] object Repository { /** * Provide a default [[Repository]] that works within a task definition, e.g. Def.task. It's From 6a5f0f2af25ac479e231624526962bbb7cb734d5 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 20 Mar 2019 13:56:23 -0700 Subject: [PATCH 17/23] Make Stamped.file private[sbt] --- main-command/src/main/scala/sbt/Stamped.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main-command/src/main/scala/sbt/Stamped.scala b/main-command/src/main/scala/sbt/Stamped.scala index 32fad1465..9613e6d25 100644 --- a/main-command/src/main/scala/sbt/Stamped.scala +++ b/main-command/src/main/scala/sbt/Stamped.scala @@ -30,7 +30,7 @@ private[sbt] trait Stamped { */ private[sbt] object Stamped { type File = JFile with Stamped - def file(typedPath: TypedPath, entry: FileCacheEntry): JFile with Stamped = + private[sbt] def file(typedPath: TypedPath, entry: FileCacheEntry): JFile with Stamped = new StampedFileImpl(typedPath, entry.stamp) /** From 86200345e1cd2ca40b109b5b76f3c5d2fdf08b2e Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 20 Mar 2019 13:59:02 -0700 Subject: [PATCH 18/23] Don't expose TypedPath to users I've decided I don't like the TypedPath interface so I'm not going to expose it publicly. --- main-command/src/main/scala/sbt/Watched.scala | 14 +++++++------- main-command/src/test/scala/sbt/WatchedSpec.scala | 12 ++++++------ main/src/main/scala/sbt/Defaults.scala | 5 ++--- main/src/main/scala/sbt/Keys.scala | 3 ++- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index 6cf5b71c9..1af65fc2d 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -8,7 +8,7 @@ package sbt import java.io.{ File, InputStream } -import java.nio.file.FileSystems +import java.nio.file.{ FileSystems, Path } import sbt.BasicCommandStrings.{ ContinuousExecutePrefix, @@ -366,7 +366,7 @@ object Watched { action case (Trigger, Some(event)) => logger.debug(s"Triggered by ${event.entry.typedPath.toPath}") - config.triggeredMessage(event.entry.typedPath, count).foreach(info) + config.triggeredMessage(event.entry.typedPath.toPath, count).foreach(info) Trigger case (Reload, Some(event)) => logger.info(s"Reload triggered by ${event.entry.typedPath.toPath}") @@ -494,11 +494,11 @@ trait WatchConfig { /** * The optional message to log when a build is triggered. - * @param typedPath the path that triggered the build + * @param path the path that triggered the vuild * @param count the current iteration * @return an optional log message. */ - def triggeredMessage(typedPath: TypedPath, count: Int): Option[String] + def triggeredMessage(path: Path, count: Int): Option[String] /** * The optional message to log before each watch iteration. @@ -542,7 +542,7 @@ object WatchConfig { preWatch: (Int, Boolean) => Watched.Action, onWatchEvent: Event[FileCacheEntry] => Watched.Action, onWatchTerminated: (Watched.Action, String, State) => State, - triggeredMessage: (TypedPath, Int) => Option[String], + triggeredMessage: (Path, Int) => Option[String], watchingMessage: Int => Option[String] ): WatchConfig = { val l = logger @@ -562,8 +562,8 @@ object WatchConfig { 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] = - tm(typedPath, count) + override def triggeredMessage(path: Path, count: Int): Option[String] = + tm(path, count) override def watchingMessage(count: Int): Option[String] = wm(count) } } diff --git a/main-command/src/test/scala/sbt/WatchedSpec.scala b/main-command/src/test/scala/sbt/WatchedSpec.scala index e0f51afa2..f0555020b 100644 --- a/main-command/src/test/scala/sbt/WatchedSpec.scala +++ b/main-command/src/test/scala/sbt/WatchedSpec.scala @@ -8,7 +8,7 @@ package sbt import java.io.{ File, InputStream } -import java.nio.file.Files +import java.nio.file.{ Files, Path } import java.util.concurrent.atomic.AtomicBoolean import org.scalatest.{ FlatSpec, Matchers } @@ -32,7 +32,7 @@ class WatchedSpec extends FlatSpec with Matchers { handleInput: InputStream => Action = _ => Ignore, preWatch: (Int, Boolean) => Action = (_, _) => CancelWatch, onWatchEvent: Event[FileCacheEntry] => Action = _ => Ignore, - triggeredMessage: (TypedPath, Int) => Option[String] = (_, _) => None, + triggeredMessage: (Path, Int) => Option[String] = (_, _) => None, watchingMessage: Int => Option[String] = _ => None ): WatchConfig = { val monitor = fileEventMonitor.getOrElse { @@ -81,7 +81,7 @@ class WatchedSpec extends FlatSpec with Matchers { } it should "filter events" in IO.withTemporaryDirectory { dir => val realDir = dir.toRealPath - val queue = new mutable.Queue[TypedPath] + val queue = new mutable.Queue[Path] val foo = realDir.toPath.resolve("foo") val bar = realDir.toPath.resolve("bar") val config = Defaults.config( @@ -92,11 +92,11 @@ class WatchedSpec extends FlatSpec with Matchers { watchingMessage = _ => { Files.createFile(bar); Thread.sleep(5); Files.createFile(foo); None } ) Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch - queue.toIndexedSeq.map(_.toPath) shouldBe Seq(foo) + queue.toIndexedSeq shouldBe Seq(foo) } it should "enforce anti-entropy" in IO.withTemporaryDirectory { dir => val realDir = dir.toRealPath - val queue = new mutable.Queue[TypedPath] + val queue = new mutable.Queue[Path] val foo = realDir.toPath.resolve("foo") val bar = realDir.toPath.resolve("bar") val config = Defaults.config( @@ -116,7 +116,7 @@ class WatchedSpec extends FlatSpec with Matchers { } ) Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch - queue.toIndexedSeq.map(_.toPath) shouldBe Seq(bar, foo) + queue.toIndexedSeq shouldBe Seq(bar, foo) } it should "halt on error" in IO.withTemporaryDirectory { dir => val halted = new AtomicBoolean(false) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 46c43c360..ef8a9f61b 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -9,6 +9,7 @@ package sbt import java.io.{ File, PrintWriter } import java.net.{ URI, URL } +import java.nio.file.{ Path => NioPath } import java.util.Optional import java.util.concurrent.{ Callable, TimeUnit } @@ -641,9 +642,7 @@ object Defaults extends BuildCommon { .map(w => (count: Int) => Some(w(WatchState.empty(globs).withCount(count)))) .getOrElse(watchStartMessage.value) val tm = triggeredMessage.?.value - .map( - tm => (_: TypedPath, count: Int) => Some(tm(WatchState.empty(globs).withCount(count))) - ) + .map(tm => (_: NioPath, count: Int) => Some(tm(WatchState.empty(globs).withCount(count)))) .getOrElse(watchTriggeredMessage.value) val logger = watchLogger.value val repo = FileManagement.toMonitoringRepository(FileManagement.repo.value) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 9f144259f..6ad5b58bf 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -9,6 +9,7 @@ package sbt import java.io.{ File, InputStream } import java.net.URL +import java.nio.file.Path import org.apache.ivy.core.module.descriptor.ModuleDescriptor import org.apache.ivy.core.module.id.ModuleRevisionId @@ -110,7 +111,7 @@ object Keys { val watchSources = taskKey[Seq[Watched.WatchSource]]("Defines the sources in this project for continuous execution to watch for changes.").withRank(BMinusSetting) val watchStartMessage = settingKey[Int => Option[String]]("The message to show when triggered execution waits for sources to change. The parameter is the current watch iteration count.").withRank(DSetting) val watchTransitiveSources = taskKey[Seq[Watched.WatchSource]]("Defines the sources in all projects for continuous execution to watch.").withRank(CSetting) - val watchTriggeredMessage = settingKey[(TypedPath, Int) => Option[String]]("The message to show before triggered execution executes an action after sources change. The parameters are the path that triggered the build and the current watch iteration count.").withRank(DSetting) + val watchTriggeredMessage = settingKey[(Path, Int) => Option[String]]("The message to show before triggered execution executes an action after sources change. The parameters are the path that triggered the build and the current watch iteration count.").withRank(DSetting) @deprecated("Use watchStartMessage instead", "1.3.0") val watchingMessage = settingKey[WatchState => String]("The message to show when triggered execution waits for sources to change.").withRank(DSetting) @deprecated("Use watchTriggeredMessage instead", "1.3.0") From be94b25d680efd2ad2e7b5f0ab79c242b376922a Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Fri, 15 Mar 2019 11:09:29 -0700 Subject: [PATCH 19/23] Add Event trait to FileCacheEntry Rather than exposing the FileEventMonitor.Event types, which are under active development in the io repo, I am adding a new event trait to FileCacheEntry. This trait doesn't expose any internal implementation details. --- main-command/src/main/scala/sbt/Watched.scala | 32 ++++++++++--------- .../scala/sbt/internal/FileCacheEntry.scala | 32 +++++++++++++++++-- .../src/test/scala/sbt/WatchedSpec.scala | 5 ++- main/src/main/scala/sbt/Keys.scala | 3 +- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index 1af65fc2d..00ce14a05 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -23,7 +23,6 @@ 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 } @@ -145,13 +144,14 @@ object Watched { private[sbt] def onEvent( sources: Seq[WatchSource], projectSources: Seq[WatchSource] - ): Event[FileCacheEntry] => Watched.Action = + ): FileCacheEntry.Event => Watched.Action = event => - if (sources.exists(_.accept(event.entry.typedPath.toPath))) Watched.Trigger - else if (projectSources.exists(_.accept(event.entry.typedPath.toPath))) event match { - case Update(prev, cur, _) if prev.value != cur.value => Reload - case _: Creation[_] | _: Deletion[_] => Reload - case _ => Ignore + if (sources.exists(_.accept(event.path))) Watched.Trigger + else if (projectSources.exists(_.accept(event.path))) { + (event.previous, event.current) match { + case (Some(p), Some(c)) => if (c == p) Watched.Ignore else Watched.Reload + case _ => Watched.Trigger + } } else Ignore private[this] val reRun = if (isWin) "" else " or 'r' to re-run the command" @@ -333,7 +333,9 @@ object Watched { case action @ (CancelWatch | HandleError | Reload | _: Custom) => action case Trigger => Trigger case _ => - val events = config.fileEventMonitor.poll(10.millis) + val events = config.fileEventMonitor + .poll(10.millis) + .map(new FileCacheEntry.EventImpl(_)) val next = events match { case Seq() => (Ignore, None) case Seq(head, tail @ _*) => @@ -362,14 +364,14 @@ object Watched { if (action == HandleError) "error" else if (action.isInstanceOf[Custom]) action.toString else "cancellation" - logger.debug(s"Stopping watch due to $cause from ${event.entry.typedPath.toPath}") + logger.debug(s"Stopping watch due to $cause from ${event.path}") action case (Trigger, Some(event)) => - logger.debug(s"Triggered by ${event.entry.typedPath.toPath}") - config.triggeredMessage(event.entry.typedPath.toPath, count).foreach(info) + logger.debug(s"Triggered by ${event.path}") + config.triggeredMessage(event.path, count).foreach(info) Trigger case (Reload, Some(event)) => - logger.info(s"Reload triggered by ${event.entry.typedPath.toPath}") + logger.info(s"Reload triggered by ${event.path}") Reload case _ => nextAction() @@ -481,7 +483,7 @@ trait WatchConfig { * @param event the detected sbt.io.FileEventMonitor.Event. * @return the next [[Watched.Action Action]] to run. */ - def onWatchEvent(event: Event[FileCacheEntry]): Watched.Action + def onWatchEvent(event: FileCacheEntry.Event): Watched.Action /** * Transforms the state after the watch terminates. @@ -540,7 +542,7 @@ object WatchConfig { fileEventMonitor: FileEventMonitor[FileCacheEntry], handleInput: InputStream => Watched.Action, preWatch: (Int, Boolean) => Watched.Action, - onWatchEvent: Event[FileCacheEntry] => Watched.Action, + onWatchEvent: FileCacheEntry.Event => Watched.Action, onWatchTerminated: (Watched.Action, String, State) => State, triggeredMessage: (Path, Int) => Option[String], watchingMessage: Int => Option[String] @@ -559,7 +561,7 @@ object WatchConfig { 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[FileCacheEntry]): Watched.Action = owe(event) + override def onWatchEvent(event: FileCacheEntry.Event): Watched.Action = owe(event) override def onWatchTerminated(action: Watched.Action, command: String, state: State): State = owt(action, command, state) override def triggeredMessage(path: Path, count: Int): Option[String] = diff --git a/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala b/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala index f7c0e71a4..19c055929 100644 --- a/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala +++ b/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala @@ -8,10 +8,12 @@ package sbt package internal import java.lang +import java.nio.file.Path import java.util.Optional import sbt.internal.inc.{ EmptyStamp, LastModified, Stamp } -import sbt.io.TypedPath +import sbt.io.FileEventMonitor.{ Creation, Deletion, Update } +import sbt.io.{ FileEventMonitor, TypedPath } import xsbti.compile.analysis.{ Stamp => XStamp } /** @@ -23,7 +25,33 @@ trait FileCacheEntry { def lastModified: Option[Long] } object FileCacheEntry { - def default(typedPath: TypedPath): FileCacheEntry = + trait Event { + def path: Path + def previous: Option[FileCacheEntry] + def current: Option[FileCacheEntry] + } + private[sbt] class EventImpl(event: FileEventMonitor.Event[FileCacheEntry]) extends Event { + override def path: Path = event.entry.typedPath.toPath + override def previous: Option[FileCacheEntry] = event match { + case Deletion(entry, _) => entry.value.toOption + case Update(previous, _, _) => previous.value.toOption + case _ => None + } + override def current: Option[FileCacheEntry] = event match { + case Creation(entry, _) => entry.value.toOption + case Update(_, current, _) => current.value.toOption + case _ => None + } + override def equals(o: Any): Boolean = o match { + case that: Event => + this.path == that.path && this.previous == that.previous && this.current == that.current + case _ => false + } + override def hashCode(): Int = + ((path.hashCode * 31) ^ previous.hashCode() * 31) ^ current.hashCode() + override def toString: String = s"Event($path, $previous, $current)" + } + private[sbt] 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 { diff --git a/main-command/src/test/scala/sbt/WatchedSpec.scala b/main-command/src/test/scala/sbt/WatchedSpec.scala index f0555020b..60f73b7da 100644 --- a/main-command/src/test/scala/sbt/WatchedSpec.scala +++ b/main-command/src/test/scala/sbt/WatchedSpec.scala @@ -15,7 +15,6 @@ import org.scalatest.{ FlatSpec, Matchers } import sbt.Watched._ import sbt.WatchedSpec._ import sbt.internal.FileCacheEntry -import sbt.io.FileEventMonitor.Event import sbt.io._ import sbt.io.syntax._ import sbt.util.Logger @@ -31,7 +30,7 @@ class WatchedSpec extends FlatSpec with Matchers { logger: Logger = NullLogger, handleInput: InputStream => Action = _ => Ignore, preWatch: (Int, Boolean) => Action = (_, _) => CancelWatch, - onWatchEvent: Event[FileCacheEntry] => Action = _ => Ignore, + onWatchEvent: FileCacheEntry.Event => Action = _ => Ignore, triggeredMessage: (Path, Int) => Option[String] = (_, _) => None, watchingMessage: Int => Option[String] = _ => None ): WatchConfig = { @@ -87,7 +86,7 @@ class WatchedSpec extends FlatSpec with Matchers { val config = Defaults.config( globs = Seq(realDir ** AllPassFilter), preWatch = (count, _) => if (count == 2) CancelWatch else Ignore, - onWatchEvent = e => if (e.entry.typedPath.toPath == foo) Trigger else Ignore, + onWatchEvent = e => if (e.path == foo) Trigger else Ignore, triggeredMessage = (tp, _) => { queue += tp; None }, watchingMessage = _ => { Files.createFile(bar); Thread.sleep(5); Files.createFile(foo); None } ) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 6ad5b58bf..62033ed54 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -23,7 +23,6 @@ import sbt.internal.io.WatchState import sbt.internal.librarymanagement.{ CompatibilityWarningOptions, IvySbt } import sbt.internal.server.ServerHandler import sbt.internal.util.{ AttributeKey, SourcePosition } -import sbt.io.FileEventMonitor.Event import sbt.io._ import sbt.librarymanagement.Configurations.CompilerPlugin import sbt.librarymanagement.LibraryManagementCodec._ @@ -102,7 +101,7 @@ object Keys { 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[FileCacheEntry] => Watched.Action]("Determines how to handle a file event").withRank(BMinusSetting) + val watchOnEvent = taskKey[FileCacheEntry.Event => 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) From d231d7d9ece135b86f80194dafaf8c1aeac8a618 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 20 Mar 2019 10:38:26 -0700 Subject: [PATCH 20/23] Rename FileCacheEntry to FileAttributes I decided that FileCacheEntry was a bad name because the methods did not necessarily have anything to do with caching. Moreover, because it is exposed in a public interface, it shouldn't be in the internal package. --- main-command/src/main/scala/sbt/Stamped.scala | 4 +- main-command/src/main/scala/sbt/Watched.scala | 18 ++++----- ...eCacheEntry.scala => FileAttributes.scala} | 38 +++++++++---------- .../src/test/scala/sbt/WatchedSpec.scala | 8 ++-- main/src/main/scala/sbt/Keys.scala | 4 +- .../scala/sbt/internal/ExternalHooks.scala | 2 +- .../scala/sbt/internal/FileManagement.scala | 30 +++++++-------- .../main/scala/sbt/internal/FileTree.scala | 18 ++++----- 8 files changed, 61 insertions(+), 61 deletions(-) rename main-command/src/main/scala/sbt/internal/{FileCacheEntry.scala => FileAttributes.scala} (73%) diff --git a/main-command/src/main/scala/sbt/Stamped.scala b/main-command/src/main/scala/sbt/Stamped.scala index 9613e6d25..61592f488 100644 --- a/main-command/src/main/scala/sbt/Stamped.scala +++ b/main-command/src/main/scala/sbt/Stamped.scala @@ -10,7 +10,7 @@ package sbt import java.io.{ File => JFile } import java.nio.file.Path -import sbt.internal.FileCacheEntry +import sbt.internal.FileAttributes import sbt.internal.inc.{ EmptyStamp, Stamper } import sbt.io.TypedPath import xsbti.compile.analysis.Stamp @@ -30,7 +30,7 @@ private[sbt] trait Stamped { */ private[sbt] object Stamped { type File = JFile with Stamped - private[sbt] def file(typedPath: TypedPath, entry: FileCacheEntry): JFile with Stamped = + private[sbt] def file(typedPath: TypedPath, entry: FileAttributes): JFile with Stamped = new StampedFileImpl(typedPath, entry.stamp) /** diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index 00ce14a05..91b7dad18 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -22,7 +22,7 @@ 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.internal.{ FileAttributes, LegacyWatched } import sbt.io._ import sbt.util.{ Level, Logger } @@ -144,7 +144,7 @@ object Watched { private[sbt] def onEvent( sources: Seq[WatchSource], projectSources: Seq[WatchSource] - ): FileCacheEntry.Event => Watched.Action = + ): FileAttributes.Event => Watched.Action = event => if (sources.exists(_.accept(event.path))) Watched.Trigger else if (projectSources.exists(_.accept(event.path))) { @@ -335,7 +335,7 @@ object Watched { case _ => val events = config.fileEventMonitor .poll(10.millis) - .map(new FileCacheEntry.EventImpl(_)) + .map(new FileAttributes.EventImpl(_)) val next = events match { case Seq() => (Ignore, None) case Seq(head, tail @ _*) => @@ -460,7 +460,7 @@ trait WatchConfig { * * @return an sbt.io.FileEventMonitor instance. */ - def fileEventMonitor: FileEventMonitor[FileCacheEntry] + def fileEventMonitor: FileEventMonitor[FileAttributes] /** * A function that is periodically invoked to determine whether the watch should stop or @@ -483,7 +483,7 @@ trait WatchConfig { * @param event the detected sbt.io.FileEventMonitor.Event. * @return the next [[Watched.Action Action]] to run. */ - def onWatchEvent(event: FileCacheEntry.Event): Watched.Action + def onWatchEvent(event: FileAttributes.Event): Watched.Action /** * Transforms the state after the watch terminates. @@ -539,10 +539,10 @@ object WatchConfig { */ def default( logger: Logger, - fileEventMonitor: FileEventMonitor[FileCacheEntry], + fileEventMonitor: FileEventMonitor[FileAttributes], handleInput: InputStream => Watched.Action, preWatch: (Int, Boolean) => Watched.Action, - onWatchEvent: FileCacheEntry.Event => Watched.Action, + onWatchEvent: FileAttributes.Event => Watched.Action, onWatchTerminated: (Watched.Action, String, State) => State, triggeredMessage: (Path, Int) => Option[String], watchingMessage: Int => Option[String] @@ -557,11 +557,11 @@ object WatchConfig { val wm = watchingMessage new WatchConfig { override def logger: Logger = l - override def fileEventMonitor: FileEventMonitor[FileCacheEntry] = fem + override def fileEventMonitor: FileEventMonitor[FileAttributes] = 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: FileCacheEntry.Event): Watched.Action = owe(event) + override def onWatchEvent(event: FileAttributes.Event): Watched.Action = owe(event) override def onWatchTerminated(action: Watched.Action, command: String, state: State): State = owt(action, command, state) override def triggeredMessage(path: Path, count: Int): Option[String] = diff --git a/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala b/main-command/src/main/scala/sbt/internal/FileAttributes.scala similarity index 73% rename from main-command/src/main/scala/sbt/internal/FileCacheEntry.scala rename to main-command/src/main/scala/sbt/internal/FileAttributes.scala index 19c055929..2987313c1 100644 --- a/main-command/src/main/scala/sbt/internal/FileCacheEntry.scala +++ b/main-command/src/main/scala/sbt/internal/FileAttributes.scala @@ -5,39 +5,39 @@ * Licensed under Apache License 2.0 (see LICENSE) */ -package sbt -package internal +package sbt.internal + import java.lang import java.nio.file.Path import java.util.Optional +import sbt.Stamped import sbt.internal.inc.{ EmptyStamp, LastModified, Stamp } import sbt.io.FileEventMonitor.{ Creation, Deletion, Update } import sbt.io.{ FileEventMonitor, 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. + * Represents the FileAttributes of a file. This will be moved to io before 1.3.0 is released. */ -trait FileCacheEntry { +trait FileAttributes { def hash: Option[String] def lastModified: Option[Long] } -object FileCacheEntry { +object FileAttributes { trait Event { def path: Path - def previous: Option[FileCacheEntry] - def current: Option[FileCacheEntry] + def previous: Option[FileAttributes] + def current: Option[FileAttributes] } - private[sbt] class EventImpl(event: FileEventMonitor.Event[FileCacheEntry]) extends Event { + private[sbt] class EventImpl(event: FileEventMonitor.Event[FileAttributes]) extends Event { override def path: Path = event.entry.typedPath.toPath - override def previous: Option[FileCacheEntry] = event match { + override def previous: Option[FileAttributes] = event match { case Deletion(entry, _) => entry.value.toOption case Update(previous, _, _) => previous.value.toOption case _ => None } - override def current: Option[FileCacheEntry] = event match { + override def current: Option[FileAttributes] = event match { case Creation(entry, _) => entry.value.toOption case Update(_, current, _) => current.value.toOption case _ => None @@ -51,11 +51,11 @@ object FileCacheEntry { ((path.hashCode * 31) ^ previous.hashCode() * 31) ^ current.hashCode() override def toString: String = s"Event($path, $previous, $current)" } - private[sbt] def default(typedPath: TypedPath): FileCacheEntry = - DelegateFileCacheEntry(Stamped.converter(typedPath)) - private[sbt] implicit class FileCacheEntryOps(val e: FileCacheEntry) extends AnyVal { + private[sbt] def default(typedPath: TypedPath): FileAttributes = + DelegateFileAttributes(Stamped.converter(typedPath)) + private[sbt] implicit class FileAttributesOps(val e: FileAttributes) extends AnyVal { private[sbt] def stamp: XStamp = e match { - case DelegateFileCacheEntry(s) => s + case DelegateFileAttributes(s) => s case _ => e.hash .map(Stamp.fromString) @@ -67,8 +67,8 @@ object FileCacheEntry { private implicit class Equiv(val xstamp: XStamp) extends AnyVal { def equiv(that: XStamp): Boolean = Stamp.equivStamp.equiv(xstamp, that) } - private case class DelegateFileCacheEntry(private val stamp: XStamp) - extends FileCacheEntry + private case class DelegateFileAttributes(private val stamp: XStamp) + extends FileAttributes with XStamp { override def getValueId: Int = stamp.getValueId override def writeStamp(): String = stamp.writeStamp() @@ -83,11 +83,11 @@ object FileCacheEntry { case _ => None } override def equals(o: Any): Boolean = o match { - case DelegateFileCacheEntry(thatStamp) => this.stamp equiv thatStamp + case DelegateFileAttributes(thatStamp) => this.stamp equiv thatStamp case xStamp: XStamp => this.stamp equiv xStamp case _ => false } override def hashCode: Int = stamp.hashCode - override def toString: String = s"FileCacheEntry(hash = $hash, lastModified = $lastModified)" + override def toString: String = s"FileAttributes(hash = $hash, lastModified = $lastModified)" } } diff --git a/main-command/src/test/scala/sbt/WatchedSpec.scala b/main-command/src/test/scala/sbt/WatchedSpec.scala index 60f73b7da..5c66553d3 100644 --- a/main-command/src/test/scala/sbt/WatchedSpec.scala +++ b/main-command/src/test/scala/sbt/WatchedSpec.scala @@ -14,7 +14,7 @@ import java.util.concurrent.atomic.AtomicBoolean import org.scalatest.{ FlatSpec, Matchers } import sbt.Watched._ import sbt.WatchedSpec._ -import sbt.internal.FileCacheEntry +import sbt.internal.FileAttributes import sbt.io._ import sbt.io.syntax._ import sbt.util.Logger @@ -26,16 +26,16 @@ class WatchedSpec extends FlatSpec with Matchers { object Defaults { def config( globs: Seq[Glob], - fileEventMonitor: Option[FileEventMonitor[FileCacheEntry]] = None, + fileEventMonitor: Option[FileEventMonitor[FileAttributes]] = None, logger: Logger = NullLogger, handleInput: InputStream => Action = _ => Ignore, preWatch: (Int, Boolean) => Action = (_, _) => CancelWatch, - onWatchEvent: FileCacheEntry.Event => Action = _ => Ignore, + onWatchEvent: FileAttributes.Event => Action = _ => Ignore, triggeredMessage: (Path, Int) => Option[String] = (_, _) => None, watchingMessage: Int => Option[String] = _ => None ): WatchConfig = { val monitor = fileEventMonitor.getOrElse { - val fileTreeRepository = FileTreeRepository.default(FileCacheEntry.default) + val fileTreeRepository = FileTreeRepository.default(FileAttributes.default) globs.foreach(fileTreeRepository.register) FileEventMonitor.antiEntropy( fileTreeRepository, diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 62033ed54..5fdeeea03 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -101,7 +101,7 @@ object Keys { 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[FileCacheEntry.Event => Watched.Action]("Determines how to handle a file event").withRank(BMinusSetting) + val watchOnEvent = taskKey[FileAttributes.Event => 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) @@ -472,7 +472,7 @@ object Keys { @deprecated("No longer used", "1.3.0") private[sbt] val executeProgress = settingKey[State => TaskProgress]("Experimental task execution listener.").withRank(DTask) - private[sbt] val globalFileTreeRepository = AttributeKey[FileTreeRepository[FileCacheEntry]]( + private[sbt] val globalFileTreeRepository = AttributeKey[FileTreeRepository[FileAttributes]]( "global-file-tree-repository", "Provides a view into the file system that may or may not cache the tree in memory", 1000 diff --git a/main/src/main/scala/sbt/internal/ExternalHooks.scala b/main/src/main/scala/sbt/internal/ExternalHooks.scala index fc86c5303..e3c9c59cb 100644 --- a/main/src/main/scala/sbt/internal/ExternalHooks.scala +++ b/main/src/main/scala/sbt/internal/ExternalHooks.scala @@ -22,7 +22,7 @@ import scala.collection.mutable private[sbt] object ExternalHooks { private val javaHome = Option(System.getProperty("java.home")).map(Paths.get(_)) def apply(options: CompileOptions, repo: FileTree.Repository): DefaultExternalHooks = { - def listEntries(glob: Glob): Seq[Entry[FileCacheEntry]] = repo.get(glob) + def listEntries(glob: Glob): Seq[Entry[FileAttributes]] = repo.get(glob) import scala.collection.JavaConverters._ val sources = options.sources() val cachedSources = new java.util.HashMap[File, Stamp] diff --git a/main/src/main/scala/sbt/internal/FileManagement.scala b/main/src/main/scala/sbt/internal/FileManagement.scala index 348d933df..87f067e21 100644 --- a/main/src/main/scala/sbt/internal/FileManagement.scala +++ b/main/src/main/scala/sbt/internal/FileManagement.scala @@ -27,7 +27,7 @@ private[sbt] object FileManagement { private[sbt] def defaultFileTreeRepository( state: State, extracted: Extracted - ): FileTreeRepository[FileCacheEntry] = { + ): FileTreeRepository[FileAttributes] = { val pollingGlobs = extracted.getOpt(Keys.pollingGlobs).getOrElse(Nil) val remaining = state.remainingCommands.map(_.commandLine) // If the session is interactive or if the commands include a continuous build, then use @@ -47,38 +47,38 @@ private[sbt] object FileManagement { case _ => new WatchLogger { override def debug(msg: => Any): Unit = {} } } if (enableCache) { - if (pollingGlobs.isEmpty) FileTreeRepository.default(FileCacheEntry.default) + if (pollingGlobs.isEmpty) FileTreeRepository.default(FileAttributes.default) else - new HybridMonitoringRepository[FileCacheEntry]( - FileTreeRepository.hybrid(FileCacheEntry.default, pollingGlobs: _*), + new HybridMonitoringRepository[FileAttributes]( + FileTreeRepository.hybrid(FileAttributes.default, pollingGlobs: _*), pollInterval, watchLogger ) } else { - if (Util.isWindows) new PollingFileRepository(FileCacheEntry.default) + if (Util.isWindows) new PollingFileRepository(FileAttributes.default) else { val service = Watched.createWatchService(pollInterval) - FileTreeRepository.legacy(FileCacheEntry.default _, (_: Any) => {}, service) + FileTreeRepository.legacy(FileAttributes.default _, (_: Any) => {}, service) } } } private[sbt] def monitor( - repository: FileTreeRepository[FileCacheEntry], + repository: FileTreeRepository[FileAttributes], antiEntropy: FiniteDuration, logger: Logger - ): FileEventMonitor[FileCacheEntry] = { + ): FileEventMonitor[FileAttributes] = { // Forwards callbacks to the repository. The close method removes all of these // callbacks. - val copied: Observable[FileCacheEntry] = new Observable[FileCacheEntry] { - private[this] val observers = new Observers[FileCacheEntry] + val copied: Observable[FileAttributes] = new Observable[FileAttributes] { + private[this] val observers = new Observers[FileAttributes] val underlying = repository match { - case h: HybridPollingFileTreeRepository[FileCacheEntry] => + case h: HybridPollingFileTreeRepository[FileAttributes] => h.toPollingRepository(antiEntropy, (msg: Any) => logger.debug(msg.toString)) case r => r } private[this] val handle = underlying.addObserver(observers) - override def addObserver(observer: Observer[FileCacheEntry]): Int = + override def addObserver(observer: Observer[FileAttributes]): Int = observers.addObserver(observer) override def removeObserver(handle: Int): Unit = observers.removeObserver(handle) override def close(): Unit = { @@ -86,7 +86,7 @@ private[sbt] object FileManagement { underlying.close() } } - new FileEventMonitor[FileCacheEntry] { + new FileEventMonitor[FileAttributes] { val monitor = FileEventMonitor.antiEntropy( copied, @@ -95,13 +95,13 @@ private[sbt] object FileManagement { 50.millis, 10.minutes ) - override def poll(duration: Duration): Seq[FileEventMonitor.Event[FileCacheEntry]] = + override def poll(duration: Duration): Seq[FileEventMonitor.Event[FileAttributes]] = monitor.poll(duration) override def close(): Unit = monitor.close() } } - private[sbt] def repo: Def.Initialize[Task[FileTreeRepository[FileCacheEntry]]] = Def.task { + private[sbt] def repo: Def.Initialize[Task[FileTreeRepository[FileAttributes]]] = Def.task { lazy val msg = s"Tried to get FileTreeRepository for uninitialized state." state.value.get(Keys.globalFileTreeRepository).getOrElse(throw new IllegalStateException(msg)) } diff --git a/main/src/main/scala/sbt/internal/FileTree.scala b/main/src/main/scala/sbt/internal/FileTree.scala index 292515ae2..a939eb950 100644 --- a/main/src/main/scala/sbt/internal/FileTree.scala +++ b/main/src/main/scala/sbt/internal/FileTree.scala @@ -17,7 +17,7 @@ import sbt.io._ import scala.language.experimental.macros private[sbt] object FileTree { - private[sbt] trait Repository extends sbt.internal.Repository[Seq, Glob, Entry[FileCacheEntry]] + private[sbt] trait Repository extends sbt.internal.Repository[Seq, Glob, Entry[FileAttributes]] private[sbt] object Repository { /** @@ -28,26 +28,26 @@ private[sbt] object FileTree { */ implicit def default: FileTree.Repository = macro MacroDefaults.fileTreeRepository private[sbt] object polling extends Repository { - val view = FileTreeView.DEFAULT.asDataView(FileCacheEntry.default) - override def get(key: Glob): Seq[Entry[FileCacheEntry]] = view.listEntries(key) + val view = FileTreeView.DEFAULT.asDataView(FileAttributes.default) + override def get(key: Glob): Seq[Entry[FileAttributes]] = view.listEntries(key) override def close(): Unit = {} } } - private class ViewRepository(underlying: FileTreeDataView[FileCacheEntry]) extends Repository { - override def get(key: Glob): Seq[Entry[FileCacheEntry]] = underlying.listEntries(key) + private class ViewRepository(underlying: FileTreeDataView[FileAttributes]) extends Repository { + override def get(key: Glob): Seq[Entry[FileAttributes]] = underlying.listEntries(key) override def close(): Unit = {} } - private class CachingRepository(underlying: FileTreeRepository[FileCacheEntry]) + private class CachingRepository(underlying: FileTreeRepository[FileAttributes]) extends Repository { - override def get(key: Glob): Seq[Entry[FileCacheEntry]] = { + override def get(key: Glob): Seq[Entry[FileAttributes]] = { underlying.register(key) underlying.listEntries(key) } override def close(): Unit = underlying.close() } - private[sbt] def repository(underlying: FileTreeDataView[FileCacheEntry]): Repository = + private[sbt] def repository(underlying: FileTreeDataView[FileAttributes]): Repository = underlying match { - case r: FileTreeRepository[FileCacheEntry] => new CachingRepository(r) + case r: FileTreeRepository[FileAttributes] => new CachingRepository(r) case v => new ViewRepository(v) } } From 467ece1ba18150d6085d85c58949217f475ce756 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Thu, 21 Mar 2019 15:58:22 -0700 Subject: [PATCH 21/23] Temporarily make FileTree.Repository public This should not be exposed to users but will be for M1. --- main/src/main/scala/sbt/internal/FileTree.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/internal/FileTree.scala b/main/src/main/scala/sbt/internal/FileTree.scala index a939eb950..b998bcea6 100644 --- a/main/src/main/scala/sbt/internal/FileTree.scala +++ b/main/src/main/scala/sbt/internal/FileTree.scala @@ -17,7 +17,7 @@ import sbt.io._ import scala.language.experimental.macros private[sbt] object FileTree { - private[sbt] trait Repository extends sbt.internal.Repository[Seq, Glob, Entry[FileAttributes]] + trait Repository extends sbt.internal.Repository[Seq, Glob, Entry[FileAttributes]] private[sbt] object Repository { /** From c3e0e117e698ba0139e63174d579c03543b869e2 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Thu, 21 Mar 2019 15:49:28 -0700 Subject: [PATCH 22/23] Do not use caching repository by default The caching repository does not work universally so set the default to always poll. This is still faster than in sbt 1.2.x because of performance improvements that I added for listing directories. --- main/src/main/scala/sbt/Defaults.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index ef8a9f61b..9ddd1a1d3 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -282,10 +282,7 @@ object Defaults extends BuildCommon { None }, watchStartMessage := Watched.defaultStartWatch, - fileTreeRepository := state.value - .get(Keys.globalFileTreeRepository) - .map(FileTree.repository) - .getOrElse(FileTree.Repository.polling), + fileTreeRepository := FileTree.Repository.polling, externalHooks := { val repository = fileTreeRepository.value compileOptions => From f26afe6681da174e80e2ca6db8e638e1abf732cc Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 20 Mar 2019 21:06:10 -0700 Subject: [PATCH 23/23] Return (Path, FileAttributes) instead of Stamped.File I realized that Stamped.File was a bad interface that was really just an implementation detail of external hooks. I updated the GlobLister.{ all, unique } methods to return Seq[(Path, FileAttributes)] rather than Stamped.File which is a much more natural api and one I could see surviving the switch to nio based apis planned for 1.4.0/2.0.0. I also added a simple scripted test for glob listing. The GlobLister.all method is implicitly tested all over the place since the compile task uses it, but it's good to have an explicit test. --- main-command/src/main/scala/sbt/Stamped.scala | 17 ++++------ .../scala/sbt/internal/FileAttributes.scala | 22 +++++++++---- .../src/test/scala/sbt/WatchedSpec.scala | 8 ++--- main/src/main/scala/sbt/Defaults.scala | 6 ++-- .../scala/sbt/internal/ExternalHooks.scala | 22 ++++--------- .../main/scala/sbt/internal/FileTree.scala | 16 +++++---- .../main/scala/sbt/internal/GlobLister.scala | 25 +++++++------- sbt/src/sbt-test/io/glob/build.sbt | 1 + sbt/src/sbt-test/io/glob/files/bar.json | 0 sbt/src/sbt-test/io/glob/files/foo.txt | 0 sbt/src/sbt-test/io/glob/files/subdir/baz.yml | 0 sbt/src/sbt-test/io/glob/project/Build.scala | 33 +++++++++++++++++++ sbt/src/sbt-test/io/glob/test | 1 + 13 files changed, 92 insertions(+), 59 deletions(-) create mode 100644 sbt/src/sbt-test/io/glob/build.sbt create mode 100644 sbt/src/sbt-test/io/glob/files/bar.json create mode 100644 sbt/src/sbt-test/io/glob/files/foo.txt create mode 100644 sbt/src/sbt-test/io/glob/files/subdir/baz.yml create mode 100644 sbt/src/sbt-test/io/glob/project/Build.scala create mode 100644 sbt/src/sbt-test/io/glob/test diff --git a/main-command/src/main/scala/sbt/Stamped.scala b/main-command/src/main/scala/sbt/Stamped.scala index 61592f488..668c27510 100644 --- a/main-command/src/main/scala/sbt/Stamped.scala +++ b/main-command/src/main/scala/sbt/Stamped.scala @@ -30,8 +30,10 @@ private[sbt] trait Stamped { */ private[sbt] object Stamped { type File = JFile with Stamped - private[sbt] def file(typedPath: TypedPath, entry: FileAttributes): JFile with Stamped = - new StampedFileImpl(typedPath, entry.stamp) + private[sbt] val file: ((Path, FileAttributes)) => JFile with Stamped = { + case (path: Path, attributes: FileAttributes) => + new StampedFileImpl(path, attributes.stamp) + } /** * Converts a TypedPath instance to a [[Stamped]] by calculating the file hash. @@ -67,14 +69,7 @@ private[sbt] object Stamped { } private final class StampedImpl(override val stamp: Stamp) extends Stamped - private final class StampedFileImpl(typedPath: TypedPath, override val stamp: Stamp) - extends java.io.File(typedPath.toPath.toString) + private final class StampedFileImpl(path: Path, override val stamp: Stamp) + extends java.io.File(path.toString) with Stamped - with TypedPath { - override def exists: Boolean = typedPath.exists - override def isDirectory: Boolean = typedPath.isDirectory - override def isFile: Boolean = typedPath.isFile - override def isSymbolicLink: Boolean = typedPath.isSymbolicLink - override def toPath: Path = typedPath.toPath - } } diff --git a/main-command/src/main/scala/sbt/internal/FileAttributes.scala b/main-command/src/main/scala/sbt/internal/FileAttributes.scala index 2987313c1..1e7abefbe 100644 --- a/main-command/src/main/scala/sbt/internal/FileAttributes.scala +++ b/main-command/src/main/scala/sbt/internal/FileAttributes.scala @@ -23,6 +23,9 @@ import xsbti.compile.analysis.{ Stamp => XStamp } trait FileAttributes { def hash: Option[String] def lastModified: Option[Long] + def isRegularFile: Boolean + def isDirectory: Boolean + def isSymbolicLink: Boolean } object FileAttributes { trait Event { @@ -52,10 +55,10 @@ object FileAttributes { override def toString: String = s"Event($path, $previous, $current)" } private[sbt] def default(typedPath: TypedPath): FileAttributes = - DelegateFileAttributes(Stamped.converter(typedPath)) + DelegateFileAttributes(Stamped.converter(typedPath), typedPath) private[sbt] implicit class FileAttributesOps(val e: FileAttributes) extends AnyVal { private[sbt] def stamp: XStamp = e match { - case DelegateFileAttributes(s) => s + case DelegateFileAttributes(s, _) => s case _ => e.hash .map(Stamp.fromString) @@ -67,8 +70,10 @@ object FileAttributes { private implicit class Equiv(val xstamp: XStamp) extends AnyVal { def equiv(that: XStamp): Boolean = Stamp.equivStamp.equiv(xstamp, that) } - private case class DelegateFileAttributes(private val stamp: XStamp) - extends FileAttributes + private case class DelegateFileAttributes( + private val stamp: XStamp, + private val typedPath: TypedPath + ) extends FileAttributes with XStamp { override def getValueId: Int = stamp.getValueId override def writeStamp(): String = stamp.writeStamp() @@ -83,11 +88,14 @@ object FileAttributes { case _ => None } override def equals(o: Any): Boolean = o match { - case DelegateFileAttributes(thatStamp) => this.stamp equiv thatStamp - case xStamp: XStamp => this.stamp equiv xStamp - case _ => false + case DelegateFileAttributes(thatStamp, thatTypedPath) => + (this.stamp equiv thatStamp) && (this.typedPath == thatTypedPath) + case _ => false } override def hashCode: Int = stamp.hashCode override def toString: String = s"FileAttributes(hash = $hash, lastModified = $lastModified)" + override def isRegularFile: Boolean = typedPath.isFile + override def isDirectory: Boolean = typedPath.isDirectory + override def isSymbolicLink: Boolean = typedPath.isSymbolicLink } } diff --git a/main-command/src/test/scala/sbt/WatchedSpec.scala b/main-command/src/test/scala/sbt/WatchedSpec.scala index 5c66553d3..ed1170fa5 100644 --- a/main-command/src/test/scala/sbt/WatchedSpec.scala +++ b/main-command/src/test/scala/sbt/WatchedSpec.scala @@ -94,14 +94,14 @@ class WatchedSpec extends FlatSpec with Matchers { queue.toIndexedSeq shouldBe Seq(foo) } it should "enforce anti-entropy" in IO.withTemporaryDirectory { dir => - val realDir = dir.toRealPath + val realDir = dir.toRealPath.toPath val queue = new mutable.Queue[Path] - val foo = realDir.toPath.resolve("foo") - val bar = realDir.toPath.resolve("bar") + val foo = realDir.resolve("foo") + val bar = realDir.resolve("bar") val config = Defaults.config( globs = Seq(realDir ** AllPassFilter), preWatch = (count, _) => if (count == 3) CancelWatch else Ignore, - onWatchEvent = _ => Trigger, + onWatchEvent = e => if (e.path != realDir) Trigger else Ignore, triggeredMessage = (tp, _) => { queue += tp; None }, watchingMessage = count => { count match { diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 9ddd1a1d3..a355559bc 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -380,7 +380,7 @@ object Defaults extends BuildCommon { val filter = (includeFilter in unmanagedSources).value -- (excludeFilter in unmanagedSources).value val baseSources = if (sourcesInBase.value) baseDirectory.value * filter :: Nil else Nil - (unmanagedSourceDirectories.value.map(_ ** filter) ++ baseSources).all + (unmanagedSourceDirectories.value.map(_ ** filter) ++ baseSources).all.map(Stamped.file) }, watchSources in ConfigGlobal := (watchSources in ConfigGlobal).value ++ { val baseDir = baseDirectory.value @@ -419,7 +419,7 @@ object Defaults extends BuildCommon { unmanagedResources := { val filter = (includeFilter in unmanagedResources).value -- (excludeFilter in unmanagedResources).value - unmanagedResourceDirectories.value.map(_ ** filter).all + unmanagedResourceDirectories.value.map(_ ** filter).all.map(Stamped.file) }, watchSources in ConfigGlobal := (watchSources in ConfigGlobal).value ++ { val bases = unmanagedResourceDirectories.value @@ -1216,7 +1216,7 @@ object Defaults extends BuildCommon { exclude: ScopedTaskable[FileFilter] ): Initialize[Task[Seq[File]]] = Def.task { val filter = include.toTask.value -- exclude.toTask.value - dirs.toTask.value.map(_ ** filter).all + dirs.toTask.value.map(_ ** filter).all.map(Stamped.file) } def artifactPathSetting(art: SettingKey[Artifact]): Initialize[File] = Def.setting { diff --git a/main/src/main/scala/sbt/internal/ExternalHooks.scala b/main/src/main/scala/sbt/internal/ExternalHooks.scala index e3c9c59cb..df30bcbd3 100644 --- a/main/src/main/scala/sbt/internal/ExternalHooks.scala +++ b/main/src/main/scala/sbt/internal/ExternalHooks.scala @@ -6,14 +6,14 @@ */ package sbt.internal -import java.nio.file.Paths + +import java.nio.file.{ Path, Paths } import java.util.Optional -import sbt.Stamped import sbt.internal.inc.ExternalLookup -import sbt.io.FileTreeDataView.Entry import sbt.io.syntax._ import sbt.io.{ AllPassFilter, Glob, TypedPath } +import sbt.Stamped import xsbti.compile._ import xsbti.compile.analysis.Stamp @@ -22,7 +22,7 @@ import scala.collection.mutable private[sbt] object ExternalHooks { private val javaHome = Option(System.getProperty("java.home")).map(Paths.get(_)) def apply(options: CompileOptions, repo: FileTree.Repository): DefaultExternalHooks = { - def listEntries(glob: Glob): Seq[Entry[FileAttributes]] = repo.get(glob) + def listEntries(glob: Glob): Seq[(Path, FileAttributes)] = repo.get(glob) import scala.collection.JavaConverters._ val sources = options.sources() val cachedSources = new java.util.HashMap[File, Stamp] @@ -36,18 +36,10 @@ private[sbt] object ExternalHooks { case f if f.getName.endsWith(".jar") => // This gives us the entry for the path itself, which is necessary if the path is a jar file // rather than a directory. - listEntries(f.toGlob) foreach { e => - e.value match { - case Right(value) => allBinaries.put(e.typedPath.toPath.toFile, value.stamp) - case _ => - } - } + listEntries(f.toGlob) foreach { case (p, a) => allBinaries.put(p.toFile, a.stamp) } case f => - listEntries(f ** AllPassFilter) foreach { e => - e.value match { - case Right(value) => allBinaries.put(e.typedPath.toPath.toFile, value.stamp) - case _ => - } + listEntries(f ** AllPassFilter) foreach { + case (p, a) => allBinaries.put(p.toFile, a.stamp) } } diff --git a/main/src/main/scala/sbt/internal/FileTree.scala b/main/src/main/scala/sbt/internal/FileTree.scala index b998bcea6..a26bc0bec 100644 --- a/main/src/main/scala/sbt/internal/FileTree.scala +++ b/main/src/main/scala/sbt/internal/FileTree.scala @@ -8,7 +8,7 @@ package sbt package internal -import java.nio.file.{ WatchService => _ } +import java.nio.file.{ Path, WatchService => _ } import sbt.internal.util.appmacro.MacroDefaults import sbt.io.FileTreeDataView.Entry @@ -17,7 +17,9 @@ import sbt.io._ import scala.language.experimental.macros private[sbt] object FileTree { - trait Repository extends sbt.internal.Repository[Seq, Glob, Entry[FileAttributes]] + private def toPair(e: Entry[FileAttributes]): Option[(Path, FileAttributes)] = + e.value.toOption.map(a => e.typedPath.toPath -> a) + trait Repository extends sbt.internal.Repository[Seq, Glob, (Path, FileAttributes)] private[sbt] object Repository { /** @@ -29,19 +31,21 @@ private[sbt] object FileTree { implicit def default: FileTree.Repository = macro MacroDefaults.fileTreeRepository private[sbt] object polling extends Repository { val view = FileTreeView.DEFAULT.asDataView(FileAttributes.default) - override def get(key: Glob): Seq[Entry[FileAttributes]] = view.listEntries(key) + override def get(key: Glob): Seq[(Path, FileAttributes)] = + view.listEntries(key).flatMap(toPair) override def close(): Unit = {} } } private class ViewRepository(underlying: FileTreeDataView[FileAttributes]) extends Repository { - override def get(key: Glob): Seq[Entry[FileAttributes]] = underlying.listEntries(key) + override def get(key: Glob): Seq[(Path, FileAttributes)] = + underlying.listEntries(key).flatMap(toPair) override def close(): Unit = {} } private class CachingRepository(underlying: FileTreeRepository[FileAttributes]) extends Repository { - override def get(key: Glob): Seq[Entry[FileAttributes]] = { + override def get(key: Glob): Seq[(Path, FileAttributes)] = { underlying.register(key) - underlying.listEntries(key) + underlying.listEntries(key).flatMap(toPair) } override def close(): Unit = underlying.close() } diff --git a/main/src/main/scala/sbt/internal/GlobLister.scala b/main/src/main/scala/sbt/internal/GlobLister.scala index cb9016d3c..90d0aaa3e 100644 --- a/main/src/main/scala/sbt/internal/GlobLister.scala +++ b/main/src/main/scala/sbt/internal/GlobLister.scala @@ -8,14 +8,16 @@ package sbt package internal -import sbt.io.{ Glob, TypedPath } +import java.nio.file.Path + +import sbt.io.Glob /** * Retrieve files from a repository. This should usually be an extension class for * sbt.io.internal.Glob (or a Traversable collection of source instances) that allows us to * actually retrieve the files corresponding to those sources. */ -sealed trait GlobLister extends Any { +private[sbt] sealed trait GlobLister extends Any { /** * Get the sources described this [[GlobLister]]. @@ -23,7 +25,7 @@ sealed trait GlobLister extends Any { * @param repository the [[FileTree.Repository]] to delegate file i/o. * @return the files described by this [[GlobLister]]. */ - def all(implicit repository: FileTree.Repository): Seq[Stamped.File] + def all(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)] /** * Get the unique sources described this [[GlobLister]]. @@ -31,7 +33,7 @@ sealed trait GlobLister extends Any { * @param repository the [[FileTree.Repository]] to delegate file i/o. * @return the files described by this [[GlobLister]] with any duplicates removed. */ - def unique(implicit repository: FileTree.Repository): Seq[Stamped.File] + def unique(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)] } /** @@ -80,18 +82,15 @@ private[internal] object GlobListers { private def get[T0 <: Traversable[Glob]]( traversable: T0, repository: FileTree.Repository - ): Seq[Stamped.File] = + ): Seq[(Path, FileAttributes)] = traversable.flatMap { glob => - val sourceFilter: TypedPath => Boolean = glob.toTypedPathFilter - repository.get(glob).flatMap { - case e if sourceFilter(e.typedPath) => e.value.toOption.map(Stamped.file(e.typedPath, _)) - case _ => None - } - }.toIndexedSeq: Seq[Stamped.File] + val sourceFilter = glob.toFileFilter + repository.get(glob).filter { case (p, _) => sourceFilter.accept(p.toFile) } + }.toIndexedSeq - override def all(implicit repository: FileTree.Repository): Seq[Stamped.File] = + override def all(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)] = get(globs, repository) - override def unique(implicit repository: FileTree.Repository): Seq[Stamped.File] = + override def unique(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)] = get(globs.toSet[Glob], repository) } } diff --git a/sbt/src/sbt-test/io/glob/build.sbt b/sbt/src/sbt-test/io/glob/build.sbt new file mode 100644 index 000000000..a79335a34 --- /dev/null +++ b/sbt/src/sbt-test/io/glob/build.sbt @@ -0,0 +1 @@ +val root = Build.root \ No newline at end of file diff --git a/sbt/src/sbt-test/io/glob/files/bar.json b/sbt/src/sbt-test/io/glob/files/bar.json new file mode 100644 index 000000000..e69de29bb diff --git a/sbt/src/sbt-test/io/glob/files/foo.txt b/sbt/src/sbt-test/io/glob/files/foo.txt new file mode 100644 index 000000000..e69de29bb diff --git a/sbt/src/sbt-test/io/glob/files/subdir/baz.yml b/sbt/src/sbt-test/io/glob/files/subdir/baz.yml new file mode 100644 index 000000000..e69de29bb diff --git a/sbt/src/sbt-test/io/glob/project/Build.scala b/sbt/src/sbt-test/io/glob/project/Build.scala new file mode 100644 index 000000000..d0f9bbb71 --- /dev/null +++ b/sbt/src/sbt-test/io/glob/project/Build.scala @@ -0,0 +1,33 @@ +import java.nio.file.{ Path, Paths } +import sbt._ +import sbt.io.Glob +import sbt.Keys._ + +object Build { + val simpleTest = taskKey[Unit]("Check that glob file selectors work") + val relativeSubdir = Paths.get("subdir") + val relativeFiles = + Seq(Paths.get("foo.txt"), Paths.get("bar.json"), relativeSubdir.resolve("baz.yml")) + val files = taskKey[Path]("The files subdirectory") + val subdir = taskKey[Path]("The subdir path in the files subdirectory") + val allFiles = taskKey[Seq[Path]]("Returns all of the regular files in the files subdirectory") + private def check(actual: Any, expected: Any): Unit = + if (actual != expected) throw new IllegalStateException(s"$actual did not equal $expected") + val root = (project in file(".")) + .settings( + files := (baseDirectory.value / "files").toPath, + subdir := files.value.resolve("subdir"), + allFiles := { + val f = files.value + relativeFiles.map(f.resolve(_)) + }, + simpleTest := { + val allPaths: Glob = files.value.allPaths + val af = allFiles.value.toSet + val sub = subdir.value + check(allPaths.all.map(_._1).toSet, af + sub) + check(allPaths.all.filter(_._2.isRegularFile).map(_._1).toSet, af) + check(allPaths.all.filter(_._2.isDirectory).map(_._1).toSet, Set(sub)) + } + ) +} \ No newline at end of file diff --git a/sbt/src/sbt-test/io/glob/test b/sbt/src/sbt-test/io/glob/test new file mode 100644 index 000000000..3e26c171e --- /dev/null +++ b/sbt/src/sbt-test/io/glob/test @@ -0,0 +1 @@ +> simpleTest \ No newline at end of file