diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 21c02c79a..e8784039a 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -155,10 +155,10 @@ object Defaults extends BuildCommon { watchForceTriggerOnAnyChange :== true, watchTriggers :== Nil, clean := { () }, - sbt.nio.Keys.fileAttributeMap := { + sbt.nio.Keys.fileStampCache := { state.value - .get(sbt.nio.Keys.persistentFileAttributeMap) - .getOrElse(new sbt.nio.Keys.FileAttributeMap) + .get(sbt.nio.Keys.persistentFileStampCache) + .getOrElse(new sbt.nio.FileStamp.Cache) }, ) ++ TaskRepository .proxy(GlobalScope / classLoaderCache, ClassLoaderCache(4)) ++ globalIvyCore ++ globalJvmCore @@ -1621,7 +1621,7 @@ object Defaults extends BuildCommon { val contents = AnalysisContents.create(analysisResult.analysis(), analysisResult.setup()) store.set(contents) } - val map = sbt.nio.Keys.fileAttributeMap.value + val map = sbt.nio.Keys.fileStampCache.value val analysis = analysisResult.analysis import scala.collection.JavaConverters._ analysis.readStamps.getAllProductStamps.asScala.foreach { diff --git a/main/src/main/scala/sbt/internal/Continuous.scala b/main/src/main/scala/sbt/internal/Continuous.scala index 4499488b0..5aa62184a 100644 --- a/main/src/main/scala/sbt/internal/Continuous.scala +++ b/main/src/main/scala/sbt/internal/Continuous.scala @@ -125,8 +125,10 @@ private[sbt] object Continuous extends DeprecatedContinuous { private[sbt] val dynamicInputs = taskKey[Option[mutable.Set[DynamicInput]]]( "The input globs found during task evaluation that are used in watch." ) + private[sbt] def dynamicInputsImpl: Def.Initialize[Task[Option[mutable.Set[DynamicInput]]]] = Def.task(Keys.state.value.get(DynamicInputs)) + private[sbt] val DynamicInputs = AttributeKey[mutable.Set[DynamicInput]]( "dynamic-inputs", @@ -136,6 +138,7 @@ private[sbt] object Continuous extends DeprecatedContinuous { private[this] val continuousParser: State => Parser[(Int, String)] = { def toInt(s: String): Int = Try(s.toInt).getOrElse(0) + // This allows us to re-enter the watch with the previous count. val digitParser: Parser[Int] = (Parsers.Space.* ~> matched(Parsers.Digit.+) <~ Parsers.Space.*).map(toInt) @@ -189,6 +192,7 @@ private[sbt] object Continuous extends DeprecatedContinuous { watchSettings ) } + private def getRepository(state: State): FileTreeRepository[FileAttributes] = { lazy val exception = new IllegalStateException("Tried to access FileTreeRepository for uninitialized state") @@ -287,17 +291,20 @@ private[sbt] object Continuous extends DeprecatedContinuous { } else { FileTreeRepository.default } + val attributeMap = new FileStamp.Cache + repo.addObserver(t => attributeMap.invalidate(t.path)) try { val stateWithRepo = state .put(globalFileTreeRepository, repo) - .put(persistentFileAttributeMap, new sbt.nio.Keys.FileAttributeMap) + .put(persistentFileStampCache, attributeMap) setup(stateWithRepo, command) { (commands, s, valid, invalid) => EvaluateTask.withStreams(extracted.structure, s)(_.use(streams in Global) { streams => implicit val logger: Logger = streams.log if (invalid.isEmpty) { val currentCount = new AtomicInteger(count) val configs = getAllConfigs(valid.map(v => v._1 -> v._2)) - val callbacks = aggregate(configs, logger, in, s, currentCount, isCommand, commands) + val callbacks = + aggregate(configs, logger, in, s, currentCount, isCommand, commands, attributeMap) val task = () => { currentCount.getAndIncrement() // abort as soon as one of the tasks fails @@ -311,6 +318,12 @@ private[sbt] object Continuous extends DeprecatedContinuous { // additional times whenever the state transition callbacks return Watched.Trigger. try { val terminationAction = Watch(task, callbacks.onStart, callbacks.nextEvent) + terminationAction match { + case e: Watch.HandleUnexpectedError => + System.err.println("Caught unexpected error running continuous build:") + e.throwable.printStackTrace(System.err) + case _ => + } callbacks.onTermination(terminationAction, command, currentCount.get(), state) } finally { callbacks.onExit() @@ -340,6 +353,7 @@ private[sbt] object Continuous extends DeprecatedContinuous { case _ => Nil: Seq[ScopedKey[_]] } } + private def getAllConfigs( inputs: Seq[(String, State)] )(implicit extracted: Extracted, logger: Logger): Seq[Config] = { @@ -386,7 +400,8 @@ private[sbt] object Continuous extends DeprecatedContinuous { state: State, count: AtomicInteger, isCommand: Boolean, - commands: Seq[String] + commands: Seq[String], + attributeMap: FileStamp.Cache )( implicit extracted: Extracted ): Callbacks = { @@ -396,7 +411,7 @@ private[sbt] object Continuous extends DeprecatedContinuous { val onStart: () => Watch.Action = getOnStart(project, commands, configs, rawLogger, count) val nextInputEvent: () => Watch.Action = parseInputEvents(configs, state, inputStream, logger) val (nextFileEvent, cleanupFileMonitor): (() => Option[(Watch.Event, Watch.Action)], () => Unit) = - getFileEvents(configs, rawLogger, state, count, commands) + getFileEvents(configs, rawLogger, state, count, commands, attributeMap) val nextEvent: () => Watch.Action = combineInputAndFileEvents(nextInputEvent, nextFileEvent, logger) val onExit = () => { @@ -454,14 +469,15 @@ private[sbt] object Continuous extends DeprecatedContinuous { res } } + private def getFileEvents( configs: Seq[Config], logger: Logger, state: State, count: AtomicInteger, - commands: Seq[String] + commands: Seq[String], + attributeMap: FileStamp.Cache )(implicit extracted: Extracted): (() => Option[(Watch.Event, Watch.Action)], () => Unit) = { - val attributeMap = state.get(persistentFileAttributeMap).get val trackMetaBuild = configs.forall(_.watchSettings.trackMetaBuild) val buildGlobs = if (trackMetaBuild) extracted.getOpt(fileInputs in settingsData).getOrElse(Nil) @@ -471,24 +487,38 @@ private[sbt] object Continuous extends DeprecatedContinuous { val quarantinePeriod = configs.map(_.watchSettings.deletionQuarantinePeriod).max val onEvent: Event => Seq[(Watch.Event, Watch.Action)] = event => { val path = event.path + def watchEvent(stamper: FileStamper, forceTrigger: Boolean): Option[Watch.Event] = { - val stamp = FileStamp(path, stamper) if (!event.exists) { + Some(Deletion(event)) attributeMap.remove(event.path) match { case null => None case _ => Some(Deletion(event)) } } else { - import sbt.internal.inc.Stamp.equivStamp - attributeMap.put(path, stamp) match { - case null => Some(Creation(event)) - case s => - if (forceTrigger || !equivStamp.equiv(s.stamp, stamp.stamp)) + fileStampCache.update(path, stamper) match { + case (None, Some(_)) => Some(Creation(event)) + case (Some(_), None) => Some(Deletion(event)) + case (Some(p), Some(c)) => + if (forceTrigger) { + val msg = + s"Creating forced update event for path $path (previous stamp: $p, current stamp: $c)" + logger.debug(msg) Some(Update(event)) - else None + } else if (p == c) { + logger.debug(s"Dropping event for unmodified path $path") + None + } else { + val msg = + s"Creating update event for modified $path (previous stamp: $p, current stamp: $c)" + logger.debug(msg) + Some(Update(event)) + } + case _ => None } } } + if (buildGlobs.exists(_.matches(path))) { watchEvent(FileStamper.Hash, forceTrigger = false).map(e => e -> Watch.Reload).toSeq } else { @@ -519,6 +549,7 @@ private[sbt] object Continuous extends DeprecatedContinuous { private implicit class WatchLogger(val l: Logger) extends sbt.internal.nio.WatchLogger { override def debug(msg: Any): Unit = l.debug(msg.toString) } + // TODO make this a normal monitor private[this] val monitors: Seq[FileEventMonitor[Event]] = configs.map { config => @@ -543,11 +574,13 @@ private[sbt] object Continuous extends DeprecatedContinuous { retentionPeriod ) :: Nil } else Nil) + override def poll(duration: Duration, filter: Event => Boolean): Seq[Event] = { val res = monitors.flatMap(_.poll(0.millis, filter)).toSet.toVector if (res.isEmpty) Thread.sleep(duration.toMillis) res } + override def close(): Unit = monitors.foreach(_.close()) } val watchLogger: WatchLogger = msg => logger.debug(msg.toString) @@ -640,7 +673,9 @@ private[sbt] object Continuous extends DeprecatedContinuous { val parser = any ~> inputParser ~ matched(any) // Each parser gets its own copy of System.in that it can modify while parsing. val systemInBuilder = new StringBuilder + def inputStream(string: String): InputStream = new ByteArrayInputStream(string.getBytes) + // This string is provided in the closure below by reading from System.in val default: String => Watch.Action = string => parse(inputStream(string), systemInBuilder, parser) @@ -726,7 +761,9 @@ private[sbt] object Continuous extends DeprecatedContinuous { */ new Logger { override def trace(t: => Throwable): Unit = logger.trace(t) + override def success(message: => String): Unit = logger.success(message) + override def log(level: Level.Value, message: => String): Unit = { val levelString = if (level < delegateLevel) s"[$level] " else "" val newMessage = s"[watch] $levelString$message" @@ -812,12 +849,15 @@ private[sbt] object Continuous extends DeprecatedContinuous { ) { private[sbt] def watchState(count: Int): DeprecatedWatchState = WatchState.empty(inputs().map(_.glob)).withCount(count) + def arguments(logger: Logger): Arguments = new Arguments(logger, inputs()) } + private def getStartMessage(key: ScopedKey[_])(implicit e: Extracted): StartMessage = Some { lazy val default = key.get(watchStartMessage).getOrElse(Watch.defaultStartWatch) key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default)) } + private def getTriggerMessage( key: ScopedKey[_] )(implicit e: Extracted): TriggerMessage = { @@ -926,9 +966,12 @@ private[sbt] object Continuous extends DeprecatedContinuous { */ def withPrefix(prefix: String): Logger = new Logger { override def trace(t: => Throwable): Unit = logger.trace(t) + override def success(message: => String): Unit = logger.success(message) + override def log(level: Level.Value, message: => String): Unit = logger.log(level, s"$prefix - $message") } } + } diff --git a/main/src/main/scala/sbt/internal/ExternalHooks.scala b/main/src/main/scala/sbt/internal/ExternalHooks.scala index 400ced085..ffc675f5a 100644 --- a/main/src/main/scala/sbt/internal/ExternalHooks.scala +++ b/main/src/main/scala/sbt/internal/ExternalHooks.scala @@ -12,11 +12,13 @@ import java.util.Optional import sbt.Def import sbt.Keys._ -import sbt.internal.inc.{ EmptyStamp, ExternalLookup, Stamper } +import sbt.internal.inc.ExternalLookup +import sbt.internal.inc.Stamp.equivStamp.equiv import sbt.io.syntax._ import sbt.nio.Keys._ import sbt.nio.file.RecursiveGlob import sbt.nio.file.syntax._ +import sbt.nio.{ FileStamp, FileStamper } import xsbti.compile._ import xsbti.compile.analysis.Stamp @@ -25,37 +27,22 @@ import scala.collection.mutable private[sbt] object ExternalHooks { private val javaHome = Option(System.getProperty("java.home")).map(Paths.get(_)) - private[this] implicit class StampOps(val s: Stamp) extends AnyVal { - def hash: String = s.getHash.orElse("") - def lastModified: Long = s.getLastModified.orElse(-1L) - } def default: Def.Initialize[sbt.Task[ExternalHooks]] = Def.task { - val attributeMap = fileAttributeMap.value + val cache = fileStampCache.value val cp = dependencyClasspath.value.map(_.data) cp.foreach { file => val path = file.toPath - attributeMap.get(path) match { - case null => attributeMap.put(path, sbt.nio.FileStamp.lastModified(path)) - case _ => - } + cache.getOrElseUpdate(path, FileStamper.LastModified) } val classGlob = classDirectory.value.toGlob / RecursiveGlob / "*.class" fileTreeView.value.list(classGlob).foreach { - case (path, _) => attributeMap.put(path, sbt.nio.FileStamp.lastModified(path)) + case (path, _) => cache.update(path, FileStamper.LastModified) } - apply( - (compileOptions in compile).value, - (file: File) => { - attributeMap.get(file.toPath) match { - case null => EmptyStamp - case s => s.stamp - } - } - ) + apply((compileOptions in compile).value, cache) } private def apply( options: CompileOptions, - attributeMap: File => Stamp + fileStampCache: FileStamp.Cache ): DefaultExternalHooks = { val lookup = new ExternalLookup { override def changedSources(previousAnalysis: CompileAnalysis): Option[Changes[File]] = Some { @@ -70,16 +57,10 @@ private[sbt] object ExternalHooks { previousAnalysis.readStamps().getAllSourceStamps.asScala prevSources.foreach { case (file: File, s: Stamp) => - attributeMap(file) match { - case null => - getRemoved.add(file) - case stamp => - val hash = (if (stamp.getHash.isPresent) stamp else Stamper.forHash(file)).hash - if (hash == s.hash) { - getUnmodified.add(file) - } else { - getChanged.add(file) - } + fileStampCache.getOrElseUpdate(file.toPath, FileStamper.Hash) match { + case None => getRemoved.add(file) + case Some(stamp) => + if (equiv(stamp.stamp, s)) getUnmodified.add(file) else getChanged.add(file) } } options.sources.foreach(file => if (!prevSources.contains(file)) getAdded.add(file)) @@ -98,8 +79,8 @@ private[sbt] object ExternalHooks { override def changedBinaries(previousAnalysis: CompileAnalysis): Option[Set[File]] = { Some(previousAnalysis.readStamps.getAllBinaryStamps.asScala.flatMap { case (file, stamp) => - attributeMap(file) match { - case cachedStamp if stamp.getLastModified == cachedStamp.getLastModified => None + fileStampCache.get(file.toPath) match { + case Some(cachedStamp) if equiv(cachedStamp.stamp, stamp) => None case _ => javaHome match { case Some(h) if file.toPath.startsWith(h) => None @@ -112,9 +93,9 @@ private[sbt] object ExternalHooks { override def removedProducts(previousAnalysis: CompileAnalysis): Option[Set[File]] = { Some(previousAnalysis.readStamps.getAllProductStamps.asScala.flatMap { case (file, stamp) => - attributeMap(file) match { - case s if s.getLastModified == stamp.getLastModified => None - case _ => Some(file) + fileStampCache.get(file.toPath) match { + case Some(s) if equiv(s.stamp, stamp) => None + case _ => Some(file) } }.toSet) } diff --git a/main/src/main/scala/sbt/nio/FileStamp.scala b/main/src/main/scala/sbt/nio/FileStamp.scala index b6a9d99e0..dafdca57f 100644 --- a/main/src/main/scala/sbt/nio/FileStamp.scala +++ b/main/src/main/scala/sbt/nio/FileStamp.scala @@ -16,8 +16,6 @@ import sbt.nio.file.FileAttributes import sjsonnew.{ Builder, JsonFormat, Unbuilder, deserializationError } import xsbti.compile.analysis.{ Stamp => XStamp } -import scala.util.Try - sealed trait FileStamper object FileStamper { case object Hash extends FileStamper @@ -36,12 +34,11 @@ private[sbt] object FileStamp { } } - private[sbt] val converter: (Path, FileAttributes) => Try[FileStamp] = (p, a) => Try(apply(p, a)) - def apply(path: Path, fileStamper: FileStamper): FileStamp = fileStamper match { + def apply(path: Path, fileStamper: FileStamper): Option[FileStamp] = fileStamper match { case FileStamper.Hash => hash(path) case FileStamper.LastModified => lastModified(path) } - def apply(path: Path, fileAttributes: FileAttributes): FileStamp = + def apply(path: Path, fileAttributes: FileAttributes): Option[FileStamp] = try { if (fileAttributes.isDirectory) lastModified(path) else @@ -51,11 +48,17 @@ private[sbt] object FileStamp { case _ => hash(path) } } catch { - case e: IOException => Error(e) + case e: IOException => Some(Error(e)) } def hash(string: String): Hash = new FileHashImpl(sbt.internal.inc.Hash.unsafeFromString(string)) - def hash(path: Path): Hash = new FileHashImpl(Stamper.forHash(path.toFile)) - def lastModified(path: Path): LastModified = LastModified(IO.getModifiedTimeOrZero(path.toFile)) + def hash(path: Path): Option[Hash] = Stamper.forHash(path.toFile) match { + case EmptyStamp => None + case s => Some(new FileHashImpl(s)) + } + def lastModified(path: Path): Option[LastModified] = IO.getModifiedTimeOrZero(path.toFile) match { + case 0 => None + case l => Some(LastModified(l)) + } private[this] class FileHashImpl(val xstamp: XStamp) extends Hash(xstamp.getHash.orElse("")) sealed abstract case class Hash private[sbt] (hex: String) extends FileStamp final case class LastModified private[sbt] (time: Long) extends FileStamp @@ -209,4 +212,65 @@ private[sbt] object FileStamp { } } + private implicit class EitherOps(val e: Either[FileStamp, FileStamp]) extends AnyVal { + def value: Option[FileStamp] = if (e == null) None else Some(e.fold(identity, identity)) + } + + private[sbt] class Cache { + private[this] val underlying = new java.util.HashMap[Path, Either[FileStamp, FileStamp]] + + /** + * Invalidate the cache entry, but don't re-stamp the file until it's actually used + * in a call to get or update. + * + * @param path the file whose stamp we are invalidating + */ + def invalidate(path: Path): Unit = underlying.get(path) match { + case Right(s) => + underlying.put(path, Left(s)) + () + case _ => () + } + def get(path: Path): Option[FileStamp] = + underlying.get(path) match { + case null => None + case Left(v) => updateImpl(path, fileStampToStamper(v)) + case Right(v) => Some(v) + } + def getOrElseUpdate(path: Path, stamper: FileStamper): Option[FileStamp] = + underlying.get(path) match { + case null => updateImpl(path, stamper) + case Left(v) => updateImpl(path, stamper) + case Right(v) => Some(v) + } + def remove(key: Path): Option[FileStamp] = { + underlying.remove(key).value + } + def put(key: Path, fileStamp: FileStamp): Option[FileStamp] = + underlying.put(key, Right(fileStamp)) match { + case null => None + case e => e.value + } + def update(key: Path, stamper: FileStamper): (Option[FileStamp], Option[FileStamp]) = { + underlying.get(key) match { + case null => (None, updateImpl(key, stamper)) + case v => (v.value, updateImpl(key, stamper)) + } + } + + private def fileStampToStamper(stamp: FileStamp): FileStamper = stamp match { + case _: Hash => FileStamper.Hash + case _ => FileStamper.LastModified + } + + private def updateImpl(path: Path, stamper: FileStamper): Option[FileStamp] = { + val stamp = FileStamp(path, stamper) + stamp match { + case None => underlying.remove(path) + case Some(s) => underlying.put(path, Right(s)) + } + stamp + } + + } } diff --git a/main/src/main/scala/sbt/nio/Keys.scala b/main/src/main/scala/sbt/nio/Keys.scala index 36e4d6c38..d7577d53c 100644 --- a/main/src/main/scala/sbt/nio/Keys.scala +++ b/main/src/main/scala/sbt/nio/Keys.scala @@ -124,16 +124,16 @@ object Keys { taskKey[Seq[(Path, FileStamp)]]("Retrieves the hashes for a set of task output files") .withRank(Invisible) private[sbt] type FileAttributeMap = - java.util.HashMap[Path, FileStamp] - private[sbt] val persistentFileAttributeMap = - AttributeKey[FileAttributeMap]("persistent-file-attribute-map", Int.MaxValue) + java.util.Map[Path, FileStamp] + private[sbt] val persistentFileStampCache = + AttributeKey[FileStamp.Cache]("persistent-file-stamp-cache", Int.MaxValue) private[sbt] val allInputPathsAndAttributes = taskKey[Seq[(Path, FileAttributes)]]("Get all of the file inputs for a task") .withRank(Invisible) - private[sbt] val fileAttributeMap = taskKey[FileAttributeMap]( + private[sbt] val fileStampCache = taskKey[FileStamp.Cache]( "Map of file stamps that may be cleared between task evaluation runs." ).withRank(Invisible) - private[sbt] val pathToFileStamp = taskKey[Path => FileStamp]( + private[sbt] val pathToFileStamp = taskKey[Path => Option[FileStamp]]( "A function that computes a file stamp for a path. It may have the side effect of updating a cache." ).withRank(Invisible) } diff --git a/main/src/main/scala/sbt/nio/Settings.scala b/main/src/main/scala/sbt/nio/Settings.scala index 97b122b41..a21a89016 100644 --- a/main/src/main/scala/sbt/nio/Settings.scala +++ b/main/src/main/scala/sbt/nio/Settings.scala @@ -319,8 +319,9 @@ private[sbt] object Settings { private[sbt] def fileStamps(scopedKey: Def.ScopedKey[_]): Def.Setting[_] = addTaskDefinition(Keys.inputFileStamps in scopedKey.scope := { val stamper = (Keys.pathToFileStamp in scopedKey.scope).value - (Keys.allInputPathsAndAttributes in scopedKey.scope).value.collect { - case (p, a) if a.isRegularFile && !Files.isHidden(p) => p -> stamper(p) + (Keys.allInputPathsAndAttributes in scopedKey.scope).value.flatMap { + case (p, a) if a.isRegularFile && !Files.isHidden(p) => stamper(p).map(p -> _) + case _ => None } }) private[this] def outputsAndStamps[T: JsonFormat: ToSeqPath]( @@ -340,11 +341,11 @@ private[sbt] object Settings { }) private[this] def outputFileStampsImpl(scope: Scope): Def.Setting[_] = addTaskDefinition(outputFileStamps in scope := { - val stamper: Path => FileStamp = (outputFileStamper in scope).value match { + val stamper: Path => Option[FileStamp] = (outputFileStamper in scope).value match { case LastModified => FileStamp.lastModified case Hash => FileStamp.hash } - (allOutputFiles in scope).value.map(p => p -> stamper(p)) + (allOutputFiles in scope).value.flatMap(p => stamper(p).map(p -> _)) }) /** @@ -356,18 +357,8 @@ private[sbt] object Settings { */ private[this] def stamper(scopedKey: Def.ScopedKey[_]): Def.Setting[_] = addTaskDefinition((Keys.pathToFileStamp in scopedKey.scope) := { - val attributeMap = Keys.fileAttributeMap.value + val attributeMap = Keys.fileStampCache.value val stamper = (Keys.inputFileStamper in scopedKey.scope).value - path: Path => - attributeMap.get(path) match { - case null => - val stamp = stamper match { - case Hash => FileStamp.hash(path) - case LastModified => FileStamp.lastModified(path) - } - attributeMap.put(path, stamp) - stamp - case s => s - } + path: Path => attributeMap.getOrElseUpdate(path, stamper) }) } diff --git a/main/src/main/scala/sbt/nio/Watch.scala b/main/src/main/scala/sbt/nio/Watch.scala index c3d22ad6f..3091fa285 100644 --- a/main/src/main/scala/sbt/nio/Watch.scala +++ b/main/src/main/scala/sbt/nio/Watch.scala @@ -217,7 +217,7 @@ object Watch { * Action that indicates that an error has occurred. The watch will be terminated when this action * is produced. */ - final class HandleError(val throwable: Throwable) extends CancelWatch { + sealed class HandleError(val throwable: Throwable) extends CancelWatch { override def equals(o: Any): Boolean = o match { case that: HandleError => this.throwable == that.throwable case _ => false @@ -226,6 +226,15 @@ object Watch { override def toString: String = s"HandleError($throwable)" } + /** + * Action that indicates that an error has occurred. The watch will be terminated when this action + * is produced. + */ + private[sbt] final class HandleUnexpectedError(override val throwable: Throwable) + extends HandleError(throwable) { + override def toString: String = s"HandleUnexpectedError($throwable)" + } + /** * Action that indicates that the watch should continue as though nothing happened. This may be * because, for example, no user input was yet available. @@ -279,7 +288,12 @@ object Watch { def apply(task: () => Unit, onStart: NextAction, nextAction: NextAction): Watch.Action = { def safeNextAction(delegate: NextAction): Watch.Action = try delegate() - catch { case NonFatal(t) => new HandleError(t) } + catch { + case NonFatal(t) => + System.err.println(s"Watch caught unexpected error:") + t.printStackTrace(System.err) + new HandleError(t) + } @tailrec def next(): Watch.Action = safeNextAction(nextAction) match { // This should never return Ignore due to this condition. case Ignore => next() diff --git a/main/src/test/scala/sbt/internal/FileStampJsonSpec.scala b/main/src/test/scala/sbt/internal/FileStampJsonSpec.scala index bcf002e77..75cb961fd 100644 --- a/main/src/test/scala/sbt/internal/FileStampJsonSpec.scala +++ b/main/src/test/scala/sbt/internal/FileStampJsonSpec.scala @@ -45,6 +45,6 @@ class FileStampJsonSpec extends FlatSpec { val both: Seq[(Path, FileStamp)] = hashes ++ lastModifiedTimes val json = Converter.toJsonUnsafe(both)(fileStampJsonFormatter) val deserialized = Converter.fromJsonUnsafe(json)(fileStampJsonFormatter) - assert(both.sameElements(deserialized)) + assert(both == deserialized) } } diff --git a/sbt/src/sbt-test/nio/glob-dsl/build.sbt b/sbt/src/sbt-test/nio/glob-dsl/build.sbt index c2454aaf8..0f519f646 100644 --- a/sbt/src/sbt-test/nio/glob-dsl/build.sbt +++ b/sbt/src/sbt-test/nio/glob-dsl/build.sbt @@ -14,13 +14,13 @@ checkFoo := assert(foo.value == Seq(baseDirectory.value / "base/subdir/nested-su // Check that we can correctly extract Bar.md with a non-recursive source val bar = taskKey[Seq[File]]("Retrieve Bar.md") -bar / fileInputs += baseDirectory.value.toGlob / "base/subdir/nested-subdir/*.md" +bar / fileInputs += baseDirectory.value.toGlob / "base" / "subdir" / "nested-subdir" / "*.md" bar := (bar / allInputFiles).value.map(_.toFile) val checkBar = taskKey[Unit]("Check that the Bar.md file is retrieved") -checkBar := assert(bar.value == Seq(baseDirectory.value / "base/subdir/nested-subdir/Bar.md")) +checkBar := assert(bar.value == Seq(baseDirectory.value / "base" / "subdir" / "nested-subdir" / "Bar.md")) // Check that we can correctly extract Bar.md and Foo.md with a non-recursive source val all = taskKey[Seq[File]]("Retrieve all files") @@ -31,7 +31,7 @@ val checkAll = taskKey[Unit]("Check that the Bar.md file is retrieved") checkAll := { import sbt.dsl.LinterLevel.Ignore - val expected = Set("Foo.txt", "Bar.md").map(baseDirectory.value / "base/subdir/nested-subdir" / _) + val expected = Set("Foo.txt", "Bar.md").map(baseDirectory.value / "base" / "subdir" / "nested-subdir" / _) val actual = (all / allInputFiles).value.map(_.toFile).toSet assert(actual == expected) } diff --git a/sbt/src/sbt-test/watch/on-change/build.sbt b/sbt/src/sbt-test/watch/on-change/build.sbt index e69de29bb..b1ccae48d 100644 --- a/sbt/src/sbt-test/watch/on-change/build.sbt +++ b/sbt/src/sbt-test/watch/on-change/build.sbt @@ -0,0 +1,36 @@ +import java.nio.file._ +import sbt.nio.Keys._ +import sbt.nio._ +import scala.concurrent.duration._ +import StandardCopyOption.{ REPLACE_EXISTING => replace } + +watchTriggeredMessage := { (i, path: Path, c) => + val prev = watchTriggeredMessage.value + if (path.getFileName.toString == "C.scala") + throw new IllegalStateException("C.scala should not trigger") + prev(i, path, c) +} + +watchOnIteration := { i: Int => + val base = baseDirectory.value.toPath + val src = + base.resolve("src").resolve("main").resolve("scala").resolve("sbt").resolve("test") + val changes = base.resolve("changes") + Files.copy(changes.resolve("C.scala"), src.resolve("C.scala"), replace) + if (i < 5) { + val content = + new String(Files.readAllBytes(changes.resolve("A.scala"))) + "\n" + ("//" * i) + Files.write(src.resolve("A.scala"), content.getBytes) + } else { + Files.copy(changes.resolve("B.scala"), src.resolve("B.scala"), replace) + } + println(s"Waiting for changes...") + Watch.Ignore +} + +watchOnFileInputEvent := { (_, event: Watch.Event) => + if (event.path.getFileName.toString == "B.scala") Watch.CancelWatch + else Watch.Trigger +} + +watchAntiEntropy := 0.millis \ No newline at end of file diff --git a/sbt/src/sbt-test/watch/on-change/changes/A.scala b/sbt/src/sbt-test/watch/on-change/changes/A.scala index e69de29bb..c0f3723e9 100644 --- a/sbt/src/sbt-test/watch/on-change/changes/A.scala +++ b/sbt/src/sbt-test/watch/on-change/changes/A.scala @@ -0,0 +1,3 @@ +package sbt.test + +class A \ No newline at end of file diff --git a/sbt/src/sbt-test/watch/on-change/changes/B.scala b/sbt/src/sbt-test/watch/on-change/changes/B.scala index e69de29bb..b09c653dd 100644 --- a/sbt/src/sbt-test/watch/on-change/changes/B.scala +++ b/sbt/src/sbt-test/watch/on-change/changes/B.scala @@ -0,0 +1,3 @@ +package sbt.test + +class B \ No newline at end of file diff --git a/sbt/src/sbt-test/watch/on-change/changes/C.scala b/sbt/src/sbt-test/watch/on-change/changes/C.scala index e69de29bb..c4f0b26b3 100644 --- a/sbt/src/sbt-test/watch/on-change/changes/C.scala +++ b/sbt/src/sbt-test/watch/on-change/changes/C.scala @@ -0,0 +1,3 @@ +package sbt.test + +class C \ No newline at end of file diff --git a/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/A.scala b/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/A.scala index e69de29bb..8a803930e 100644 --- a/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/A.scala +++ b/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/A.scala @@ -0,0 +1,3 @@ +package sbt.test + +class A { \ No newline at end of file diff --git a/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/B.scala b/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/B.scala index e69de29bb..dc7d2140d 100644 --- a/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/B.scala +++ b/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/B.scala @@ -0,0 +1,3 @@ +package sbt.test + +class B { \ No newline at end of file diff --git a/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/C.scala b/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/C.scala index e69de29bb..c4f0b26b3 100644 --- a/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/C.scala +++ b/sbt/src/sbt-test/watch/on-change/src/main/scala/sbt/test/C.scala @@ -0,0 +1,3 @@ +package sbt.test + +class C \ No newline at end of file diff --git a/sbt/src/sbt-test/watch/on-change/test b/sbt/src/sbt-test/watch/on-change/test index e69de29bb..d2a71e6d9 100644 --- a/sbt/src/sbt-test/watch/on-change/test +++ b/sbt/src/sbt-test/watch/on-change/test @@ -0,0 +1,3 @@ +> ~compile + +> compile \ No newline at end of file diff --git a/sbt/src/sbt-test/watch/on-start-watch/build.sbt b/sbt/src/sbt-test/watch/on-start-watch/build.sbt index b66bb6199..5fbef8dd3 100644 --- a/sbt/src/sbt-test/watch/on-start-watch/build.sbt +++ b/sbt/src/sbt-test/watch/on-start-watch/build.sbt @@ -19,6 +19,7 @@ failingTask := { Compile / compile := { Count.increment() // Trigger a new build by updating the last modified time - ((Compile / scalaSource).value / "A.scala").setLastModified(5000) + val file = (Compile / scalaSource).value / "A.scala" + IO.write(file, IO.read(file) + ("\n" * Count.get)) (Compile / compile).value }