The newest version of io repackages a number of classes into the
sbt.nio.* packages. It also changes some of the semantics of glob
related apis. This commit updates all of the usages of the updated apis
within sbt but should have no functional difference.
This commit is contained in:
Ethan Atkins 2019-04-03 16:50:00 -07:00
parent 20b0ef786b
commit 2deac62b00
34 changed files with 567 additions and 601 deletions

View File

@ -11,18 +11,6 @@ import scala.reflect.macros.blackbox
object MacroDefaults { 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"
}
/** /**
* Macro to generated default file tree repository. It must be defined as an untyped tree because * 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 * sbt.Keys is not available in this project. This is meant for internal use only, but must be

View File

@ -8,8 +8,8 @@
package sbt package sbt
import sbt.Tests.{ Output, Summary } import sbt.Tests.{ Output, Summary }
import sbt.util.{ Level, Logger }
import sbt.protocol.testing.TestResult import sbt.protocol.testing.TestResult
import sbt.util.{ Level, Logger }
/** /**
* Logs information about tests after they finish. * Logs information about tests after they finish.

View File

@ -1,75 +0,0 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
import java.io.{ File => JFile }
import java.nio.file.Path
import sbt.internal.FileAttributes
import sbt.internal.inc.{ EmptyStamp, Stamper }
import sbt.io.TypedPath
import xsbti.compile.analysis.Stamp
/**
* A File that has a compile analysis Stamp value associated with it. In general, the stamp method
* should be a cached value that can be read without doing any io. This can be used to improve
* performance anywhere where we need to check if files have changed before doing potentially
* expensive work.
*/
private[sbt] trait Stamped {
private[sbt] def stamp: Stamp
}
/**
* Provides converter functions from TypedPath to [[Stamped]].
*/
private[sbt] object Stamped {
type File = JFile with Stamped
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.
*/
private[sbt] val sourceConverter: TypedPath => Stamp = tp => Stamper.forHash(tp.toPath.toFile)
/**
* Converts a TypedPath instance to a [[Stamped]] using the last modified time.
*/
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.
*/
private[sbt] 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)
}
}
/**
* Adds a default ordering that just delegates to the java.io.File.compareTo method.
*/
private[sbt] implicit case object ordering extends Ordering[Stamped.File] {
override def compare(left: Stamped.File, right: Stamped.File): Int = left.compareTo(right)
}
private final class StampedImpl(override val stamp: Stamp) extends Stamped
private final class StampedFileImpl(path: Path, override val stamp: Stamp)
extends java.io.File(path.toString)
with Stamped
}

View File

@ -1,101 +0,0 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
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 the FileAttributes of a file. This will be moved to io before 1.3.0 is released.
*/
trait FileAttributes {
def hash: Option[String]
def lastModified: Option[Long]
def isRegularFile: Boolean
def isDirectory: Boolean
def isSymbolicLink: Boolean
}
object FileAttributes {
trait Event {
def path: Path
def previous: Option[FileAttributes]
def current: Option[FileAttributes]
}
private[sbt] class EventImpl(event: FileEventMonitor.Event[FileAttributes]) extends Event {
override def path: Path = event.entry.typedPath.toPath
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[FileAttributes] = 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): FileAttributes =
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 _ =>
e.hash
.map(Stamp.fromString)
.orElse(e.lastModified.map(new LastModified(_)))
.getOrElse(EmptyStamp)
}
}
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,
private val typedPath: TypedPath
) extends FileAttributes
with XStamp {
override def getValueId: Int = stamp.getValueId
override def writeStamp(): String = stamp.writeStamp()
override def getHash: Optional[String] = stamp.getHash
override def getLastModified: Optional[lang.Long] = stamp.getLastModified
override def hash: Option[String] = getHash match {
case h if h.isPresent => Some(h.get)
case _ => None
}
override def lastModified: Option[Long] = getLastModified match {
case l if l.isPresent => Some(l.get)
case _ => None
}
override def equals(o: Any): Boolean = o match {
case 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
}
}

View File

@ -8,12 +8,13 @@
package sbt.internal package sbt.internal
import sbt.BasicCommandStrings.{ ClearOnFailure, FailureWall } import sbt.BasicCommandStrings.{ ClearOnFailure, FailureWall }
import sbt.Watched.ContinuousEventMonitor
import sbt.internal.io.{ EventMonitor, WatchState } import sbt.internal.io.{ EventMonitor, WatchState }
import sbt.internal.nio.{ FileEventMonitor, FileTreeRepository, WatchLogger }
import sbt.{ State, Watched } import sbt.{ State, Watched }
import scala.annotation.tailrec import scala.annotation.tailrec
import Watched.ContinuousEventMonitor import scala.concurrent.duration._
import scala.util.control.NonFatal import scala.util.control.NonFatal
private[sbt] object LegacyWatched { private[sbt] object LegacyWatched {
@ -22,43 +23,63 @@ private[sbt] object LegacyWatched {
@tailrec def shouldTerminate: Boolean = @tailrec def shouldTerminate: Boolean =
(System.in.available > 0) && (watched.terminateWatch(System.in.read()) || shouldTerminate) (System.in.available > 0) && (watched.terminateWatch(System.in.read()) || shouldTerminate)
val log = s.log val log = s.log
val logger = new EventMonitor.Logger {
override def debug(msg: => Any): Unit = log.debug(msg.toString)
}
s get ContinuousEventMonitor match { s get ContinuousEventMonitor match {
case None => case None =>
val watchState = WatchState.empty(watched.watchService(), watched.watchSources(s))
// This is the first iteration, so run the task and create a new EventMonitor // This is the first iteration, so run the task and create a new EventMonitor
val logger: WatchLogger = (a: Any) => log.debug(a.toString)
val repo = FileTreeRepository.legacy(logger, watched.watchService())
val fileEventMonitor = FileEventMonitor.antiEntropy(
repo,
watched.antiEntropy,
logger,
watched.antiEntropy,
10.minutes
)
val monitor = new EventMonitor {
override def awaitEvent(): Boolean = fileEventMonitor.poll(2.millis).nonEmpty
override def state(): WatchState = watchState
override def close(): Unit = watchState.close()
}
(ClearOnFailure :: next :: FailureWall :: repeat :: s) (ClearOnFailure :: next :: FailureWall :: repeat :: s)
.put( .put(ContinuousEventMonitor, monitor)
ContinuousEventMonitor,
EventMonitor(
WatchState.empty(watched.watchService(), watched.watchSources(s)),
watched.pollInterval,
watched.antiEntropy,
shouldTerminate,
logger
)
)
case Some(eventMonitor) => case Some(eventMonitor) =>
Watched.printIfDefined(watched watchingMessage eventMonitor.state) Watched.printIfDefined(watched watchingMessage eventMonitor.state)
val triggered = try eventMonitor.awaitEvent() @tailrec def impl(): State = {
catch { val triggered = try eventMonitor.awaitEvent()
case NonFatal(e) => catch {
log.error( case NonFatal(e) =>
"Error occurred obtaining files to watch. Terminating continuous execution..." log.error(
) "Error occurred obtaining files to watch. Terminating continuous execution..."
s.handleError(e) )
false s.handleError(e)
} false
if (triggered) { }
Watched.printIfDefined(watched triggeredMessage eventMonitor.state) if (triggered) {
ClearOnFailure :: next :: FailureWall :: repeat :: s Watched.printIfDefined(watched triggeredMessage eventMonitor.state)
} else { ClearOnFailure :: next :: FailureWall :: repeat :: s
while (System.in.available() > 0) System.in.read() } else if (shouldTerminate) {
eventMonitor.close() while (System.in.available() > 0) System.in.read()
s.remove(ContinuousEventMonitor) eventMonitor.close()
s.remove(ContinuousEventMonitor)
} else {
impl()
}
} }
impl()
} }
} }
} }
package io {
@deprecated("No longer used", "1.3.0")
private[sbt] trait EventMonitor extends AutoCloseable {
/** Block indefinitely until the monitor receives a file event or the user stops the watch. */
def awaitEvent(): Boolean
/** A snapshot of the WatchState that includes the number of build triggers and watch sources. */
def state(): WatchState
}
}

View File

@ -9,6 +9,7 @@ package sbt
import java.io.{ File, PrintWriter } import java.io.{ File, PrintWriter }
import java.net.{ URI, URL, URLClassLoader } import java.net.{ URI, URL, URLClassLoader }
import java.nio.file.{ Path => NioPath }
import java.util.Optional import java.util.Optional
import java.util.concurrent.{ Callable, TimeUnit } import java.util.concurrent.{ Callable, TimeUnit }
@ -27,6 +28,7 @@ import sbt.Project.{
} }
import sbt.Scope.{ GlobalScope, ThisScope, fillTaskAxis } import sbt.Scope.{ GlobalScope, ThisScope, fillTaskAxis }
import sbt.internal.CommandStrings.ExportStream import sbt.internal.CommandStrings.ExportStream
import sbt.internal.TransitiveGlobs._
import sbt.internal._ import sbt.internal._
import sbt.internal.inc.JavaInterfaceUtil._ import sbt.internal.inc.JavaInterfaceUtil._
import sbt.internal.inc.{ ZincLmUtil, ZincUtil } import sbt.internal.inc.{ ZincLmUtil, ZincUtil }
@ -43,7 +45,6 @@ import sbt.internal.server.{
ServerHandler ServerHandler
} }
import sbt.internal.testing.TestLogger import sbt.internal.testing.TestLogger
import sbt.internal.TransitiveGlobs._
import sbt.internal.util.Attributed.data import sbt.internal.util.Attributed.data
import sbt.internal.util.Types._ import sbt.internal.util.Types._
import sbt.internal.util._ import sbt.internal.util._
@ -65,6 +66,10 @@ import sbt.librarymanagement.CrossVersion.{ binarySbtVersion, binaryScalaVersion
import sbt.librarymanagement._ import sbt.librarymanagement._
import sbt.librarymanagement.ivy._ import sbt.librarymanagement.ivy._
import sbt.librarymanagement.syntax._ import sbt.librarymanagement.syntax._
import sbt.nio.FileStamp
import sbt.nio.Keys._
import sbt.nio.file.{ FileTreeView, Glob }
import sbt.nio.file.syntax._
import sbt.std.TaskExtra._ import sbt.std.TaskExtra._
import sbt.testing.{ AnnotatedFingerprint, Framework, Runner, SubclassFingerprint } import sbt.testing.{ AnnotatedFingerprint, Framework, Runner, SubclassFingerprint }
import sbt.util.CacheImplicits._ import sbt.util.CacheImplicits._
@ -146,6 +151,9 @@ object Defaults extends BuildCommon {
classLoaderCache := ClassLoaderCache(4), classLoaderCache := ClassLoaderCache(4),
fileInputs :== Nil, fileInputs :== Nil,
watchTriggers :== Nil, watchTriggers :== Nil,
sbt.nio.Keys.fileAttributeMap := {
new java.util.HashMap[NioPath, (Option[FileStamp.Hash], Option[FileStamp.LastModified])]()
},
) ++ TaskRepository ) ++ TaskRepository
.proxy(GlobalScope / classLoaderCache, ClassLoaderCache(4)) ++ globalIvyCore ++ globalJvmCore .proxy(GlobalScope / classLoaderCache, ClassLoaderCache(4)) ++ globalIvyCore ++ globalJvmCore
) ++ globalSbtCore ) ++ globalSbtCore
@ -191,7 +199,7 @@ object Defaults extends BuildCommon {
ps := psTask.value, ps := psTask.value,
bgStop := bgStopTask.evaluated, bgStop := bgStopTask.evaluated,
bgWaitFor := bgWaitForTask.evaluated, bgWaitFor := bgWaitForTask.evaluated,
bgCopyClasspath :== true bgCopyClasspath :== true,
) )
private[sbt] lazy val globalIvyCore: Seq[Setting[_]] = private[sbt] lazy val globalIvyCore: Seq[Setting[_]] =
@ -243,9 +251,13 @@ object Defaults extends BuildCommon {
settingsData / fileInputs := { settingsData / fileInputs := {
val baseDir = file(".").getCanonicalFile val baseDir = file(".").getCanonicalFile
val sourceFilter = ("*.sbt" || "*.scala" || "*.java") -- HiddenFileFilter val sourceFilter = ("*.sbt" || "*.scala" || "*.java") -- HiddenFileFilter
val projectDir = baseDir / "project"
Seq( Seq(
Glob(baseDir, "*.sbt" -- HiddenFileFilter, 0), baseDir * ("*.sbt" -- HiddenFileFilter),
Glob(baseDir / "project", sourceFilter, Int.MaxValue) projectDir * sourceFilter,
// We only want to recursively look in source because otherwise we have to search
// the project target directories which is expensive.
projectDir / "src" ** sourceFilter,
) )
}, },
trapExit :== true, trapExit :== true,
@ -296,13 +308,10 @@ object Defaults extends BuildCommon {
Previous.references :== new Previous.References, Previous.references :== new Previous.References,
concurrentRestrictions := defaultRestrictions.value, concurrentRestrictions := defaultRestrictions.value,
parallelExecution :== true, parallelExecution :== true,
fileTreeRepository := state.value fileTreeView :== FileTreeView.default,
.get(globalFileTreeRepository)
.map(FileTree.repository)
.getOrElse(FileTree.Repository.polling),
Continuous.dynamicInputs := Continuous.dynamicInputsImpl.value, Continuous.dynamicInputs := Continuous.dynamicInputsImpl.value,
externalHooks := { externalHooks := {
val repository = fileTreeRepository.value val repository = fileTreeView.value
compileOptions => Some(ExternalHooks(compileOptions, repository)) compileOptions => Some(ExternalHooks(compileOptions, repository))
}, },
logBuffered :== false, logBuffered :== false,
@ -411,7 +420,9 @@ object Defaults extends BuildCommon {
val baseSources = if (sourcesInBase.value) baseDirectory.value * filter :: Nil else Nil val baseSources = if (sourcesInBase.value) baseDirectory.value * filter :: Nil else Nil
unmanagedSourceDirectories.value.map(_ ** filter) ++ baseSources unmanagedSourceDirectories.value.map(_ ** filter) ++ baseSources
}, },
unmanagedSources := (unmanagedSources / fileInputs).value.all.map(Stamped.file), unmanagedSources := (unmanagedSources / fileInputs).value
.all(fileTreeView.value)
.map(FileStamp.stampedFile),
managedSourceDirectories := Seq(sourceManaged.value), managedSourceDirectories := Seq(sourceManaged.value),
managedSources := generate(sourceGenerators).value, managedSources := generate(sourceGenerators).value,
sourceGenerators :== Nil, sourceGenerators :== Nil,
@ -434,7 +445,8 @@ object Defaults extends BuildCommon {
(includeFilter in unmanagedResources).value -- (excludeFilter in unmanagedResources).value (includeFilter in unmanagedResources).value -- (excludeFilter in unmanagedResources).value
unmanagedResourceDirectories.value.map(_ ** filter) unmanagedResourceDirectories.value.map(_ ** filter)
}, },
unmanagedResources := (unmanagedResources / fileInputs).value.all.map(Stamped.file), unmanagedResources :=
(unmanagedResources / fileInputs).value.all(fileTreeView.value).map(FileStamp.stampedFile),
resourceGenerators :== Nil, resourceGenerators :== Nil,
resourceGenerators += Def.task { resourceGenerators += Def.task {
PluginDiscovery.writeDescriptors(discoveredSbtPlugins.value, resourceManaged.value) PluginDiscovery.writeDescriptors(discoveredSbtPlugins.value, resourceManaged.value)
@ -1228,7 +1240,7 @@ object Defaults extends BuildCommon {
exclude: ScopedTaskable[FileFilter] exclude: ScopedTaskable[FileFilter]
): Initialize[Task[Seq[File]]] = Def.task { ): Initialize[Task[Seq[File]]] = Def.task {
val filter = include.toTask.value -- exclude.toTask.value val filter = include.toTask.value -- exclude.toTask.value
dirs.toTask.value.map(_ ** filter).all.map(Stamped.file) dirs.toTask.value.map(_ ** filter).all(fileTreeView.value).map(FileStamp.stampedFile)
} }
def artifactPathSetting(art: SettingKey[Artifact]): Initialize[File] = def artifactPathSetting(art: SettingKey[Artifact]): Initialize[File] =
Def.setting { Def.setting {

View File

@ -591,6 +591,14 @@ object EvaluateTask {
(dynamicDependency in scoped.scope := { () }) :: Nil (dynamicDependency in scoped.scope := { () }) :: Nil
} else if (scoped.key == transitiveClasspathDependency.key) { } else if (scoped.key == transitiveClasspathDependency.key) {
(transitiveClasspathDependency in scoped.scope := { () }) :: Nil (transitiveClasspathDependency in scoped.scope := { () }) :: Nil
} else if (scoped.key == sbt.nio.Keys.fileInputs.key) {
(sbt.nio.Keys.fileHashes in scoped.scope) := {
import GlobLister._
val map = sbt.nio.FileStamp.fileHashMap.value
(sbt.nio.Keys.fileInputs in scoped.scope).value.all(fileTreeView.value).collect {
case (p, a) if a.isRegularFile => p -> map.get(p)
}
}
} else { } else {
Nil Nil
} }

View File

@ -9,6 +9,7 @@ package sbt
import java.io.{ File, InputStream } import java.io.{ File, InputStream }
import java.net.URL import java.net.URL
import java.nio.file.{ Path => NioPath }
import org.apache.ivy.core.module.descriptor.ModuleDescriptor import org.apache.ivy.core.module.descriptor.ModuleDescriptor
import org.apache.ivy.core.module.id.ModuleRevisionId import org.apache.ivy.core.module.id.ModuleRevisionId
@ -20,15 +21,16 @@ import sbt.internal._
import sbt.internal.inc.ScalaInstance import sbt.internal.inc.ScalaInstance
import sbt.internal.io.WatchState import sbt.internal.io.WatchState
import sbt.internal.librarymanagement.{ CompatibilityWarningOptions, IvySbt } import sbt.internal.librarymanagement.{ CompatibilityWarningOptions, IvySbt }
import sbt.internal.nio.FileTreeRepository
import sbt.internal.server.ServerHandler import sbt.internal.server.ServerHandler
import sbt.internal.util.complete.Parser import sbt.internal.util.complete.Parser
import sbt.internal.util.{ AttributeKey, SourcePosition } import sbt.internal.util.{ AttributeKey, SourcePosition }
import sbt.io.FileEventMonitor.Event
import sbt.io._ import sbt.io._
import sbt.librarymanagement.Configurations.CompilerPlugin import sbt.librarymanagement.Configurations.CompilerPlugin
import sbt.librarymanagement.LibraryManagementCodec._ import sbt.librarymanagement.LibraryManagementCodec._
import sbt.librarymanagement._ import sbt.librarymanagement._
import sbt.librarymanagement.ivy.{ Credentials, IvyConfiguration, IvyPaths, UpdateOptions } import sbt.librarymanagement.ivy.{ Credentials, IvyConfiguration, IvyPaths, UpdateOptions }
import sbt.nio.file.{ FileAttributes, FileTreeView, Glob }
import sbt.testing.Framework import sbt.testing.Framework
import sbt.util.{ Level, Logger } import sbt.util.{ Level, Logger }
import xsbti.compile._ import xsbti.compile._
@ -94,7 +96,7 @@ object Keys {
val analysis = AttributeKey[CompileAnalysis]("analysis", "Analysis of compilation, including dependencies and generated outputs.", DSetting) val analysis = AttributeKey[CompileAnalysis]("analysis", "Analysis of compilation, including dependencies and generated outputs.", DSetting)
val suppressSbtShellNotification = settingKey[Boolean]("""True to suppress the "Executing in batch mode.." message.""").withRank(CSetting) val suppressSbtShellNotification = settingKey[Boolean]("""True to suppress the "Executing in batch mode.." message.""").withRank(CSetting)
val fileTreeRepository = taskKey[FileTree.Repository]("A repository of the file system.").withRank(DSetting) val fileTreeView = taskKey[FileTreeView[(NioPath, FileAttributes)]]("A view of the file system.").withRank(DSetting)
val pollInterval = settingKey[FiniteDuration]("Interval between checks for modified sources by the continuous execution command.").withRank(BMinusSetting) 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 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 watchAntiEntropy = settingKey[FiniteDuration]("Duration for which the watch EventMonitor will ignore events for a file after that file has triggered a build.").withRank(BMinusSetting)
@ -106,12 +108,12 @@ object Keys {
val watchInputParser = settingKey[Parser[Watch.Action]]("A parser of user input that can be used to trigger or exit a continuous build").withRank(DSetting) val watchInputParser = settingKey[Parser[Watch.Action]]("A parser of user input that can be used to trigger or exit a continuous build").withRank(DSetting)
val watchOnEnter = settingKey[() => Unit]("Function to run prior to beginning a continuous build. This will run before the continuous task(s) is(are) first evaluated.").withRank(DSetting) val watchOnEnter = settingKey[() => Unit]("Function to run prior to beginning a continuous build. This will run before the continuous task(s) is(are) first evaluated.").withRank(DSetting)
val watchOnExit = settingKey[() => Unit]("Function to run upon exit of a continuous build. It can be used to cleanup resources used during the watch.").withRank(DSetting) val watchOnExit = settingKey[() => Unit]("Function to run upon exit of a continuous build. It can be used to cleanup resources used during the watch.").withRank(DSetting)
val watchOnInputEvent = settingKey[(Int, Event[FileAttributes]) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive inputs. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting) val watchOnInputEvent = settingKey[(Int, Watch.Event) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive inputs. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting)
val watchOnEvent = settingKey[Continuous.Arguments => Event[FileAttributes] => Watch.Action]("Determines how to handle a file event. The Seq[Glob] contains all of the transitive inputs for the task(s) being run by the continuous build.").withRank(DSetting) val watchOnEvent = settingKey[Continuous.Arguments => Watch.Event => Watch.Action]("Determines how to handle a file event. The Seq[Glob] contains all of the transitive inputs for the task(s) being run by the continuous build.").withRank(DSetting)
val watchOnMetaBuildEvent = settingKey[(Int, Event[FileAttributes]) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the meta build triggers.").withRank(DSetting) val watchOnMetaBuildEvent = settingKey[(Int, Watch.Event) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the meta build triggers.").withRank(DSetting)
val watchOnTermination = settingKey[(Watch.Action, String, Int, State) => State]("Transforms the state upon completion of a watch. The String argument is the command that was run during the watch. The Int parameter specifies how many times the command was run during the watch.").withRank(DSetting) val watchOnTermination = settingKey[(Watch.Action, String, Int, State) => State]("Transforms the state upon completion of a watch. The String argument is the command that was run during the watch. The Int parameter specifies how many times the command was run during the watch.").withRank(DSetting)
val watchOnTrigger = settingKey[Continuous.Arguments => Event[FileAttributes] => Unit]("Callback to invoke when a continuous build triggers. The first parameter is the number of previous watch task invocations. The second parameter is the Event that triggered this build").withRank(DSetting) val watchOnTrigger = settingKey[Continuous.Arguments => Watch.Event => Unit]("Callback to invoke when a continuous build triggers. The first parameter is the number of previous watch task invocations. The second parameter is the Event that triggered this build").withRank(DSetting)
val watchOnTriggerEvent = settingKey[(Int, Event[FileAttributes]) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive triggers. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting) val watchOnTriggerEvent = settingKey[(Int, Watch.Event) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive triggers. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting)
val watchOnIteration = settingKey[Int => Watch.Action]("Function that is invoked before waiting for file system events or user input events. This is only invoked if watchOnStart is not explicitly set.").withRank(DSetting) val watchOnIteration = settingKey[Int => Watch.Action]("Function that is invoked before waiting for file system events or user input events. This is only invoked if watchOnStart is not explicitly set.").withRank(DSetting)
val watchOnStart = settingKey[Continuous.Arguments => () => Watch.Action]("Function is invoked before waiting for file system or input events. The returned Action is used to either trigger the build, terminate the watch or wait for events.").withRank(DSetting) val watchOnStart = settingKey[Continuous.Arguments => () => Watch.Action]("Function is invoked before waiting for file system or input events. The returned Action is used to either trigger the build, terminate the watch or wait for events.").withRank(DSetting)
val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting).withRank(DSetting) val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting).withRank(DSetting)
@ -119,7 +121,7 @@ object Keys {
// The watchTasks key should really be named watch, but that is already taken by the deprecated watch key. I'd be surprised if there are any plugins that use it so I think we should consider breaking binary compatibility to rename this task. // The watchTasks key should really be named watch, but that is already taken by the deprecated watch key. I'd be surprised if there are any plugins that use it so I think we should consider breaking binary compatibility to rename this task.
val watchTasks = InputKey[StateTransform]("watch", "Watch a task (or multiple tasks) and rebuild when its file inputs change or user input is received. The semantics are more or less the same as the `~` command except that it cannot transform the state on exit. This means that it cannot be used to reload the build.").withRank(DSetting) val watchTasks = InputKey[StateTransform]("watch", "Watch a task (or multiple tasks) and rebuild when its file inputs change or user input is received. The semantics are more or less the same as the `~` command except that it cannot transform the state on exit. This means that it cannot be used to reload the build.").withRank(DSetting)
val watchTrackMetaBuild = settingKey[Boolean]("Toggles whether or not changing the build files (e.g. **/*.sbt, project/**/(*.scala | *.java)) should automatically trigger a project reload").withRank(DSetting) val watchTrackMetaBuild = settingKey[Boolean]("Toggles whether or not changing the build files (e.g. **/*.sbt, project/**/(*.scala | *.java)) should automatically trigger a project reload").withRank(DSetting)
val watchTriggeredMessage = settingKey[(Int, Event[FileAttributes], Seq[String]) => 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[(Int, Watch.Event, Seq[String]) => 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 watch apis // Deprecated watch apis
@deprecated("This is no longer used for continuous execution", "1.3.0") @deprecated("This is no longer used for continuous execution", "1.3.0")
@ -149,7 +151,6 @@ object Keys {
val managedSources = taskKey[Seq[File]]("Sources generated by the build.").withRank(BTask) val managedSources = taskKey[Seq[File]]("Sources generated by the build.").withRank(BTask)
val sources = taskKey[Seq[File]]("All sources, both managed and unmanaged.").withRank(BTask) val sources = taskKey[Seq[File]]("All sources, both managed and unmanaged.").withRank(BTask)
val sourcesInBase = settingKey[Boolean]("If true, sources from the project's base directory are included as main sources.") val sourcesInBase = settingKey[Boolean]("If true, sources from the project's base directory are included as main sources.")
val fileInputs = settingKey[Seq[Glob]]("The file globs that are used by a task. This setting will generally be scoped per task. It will also be used to determine the sources to watch during continuous execution.")
val watchTriggers = settingKey[Seq[Glob]]("Describes files that should trigger a new continuous build.") val watchTriggers = settingKey[Seq[Glob]]("Describes files that should trigger a new continuous build.")
// Filters // Filters
@ -175,7 +176,6 @@ object Keys {
val cleanKeepGlobs = settingKey[Seq[Glob]]("Globs 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 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 taskTemporaryDirectory = settingKey[File]("Directory used for temporary files for tasks that is deleted after each task execution.").withRank(DSetting)
val fileOutputs = taskKey[Seq[Glob]]("Describes the output files of a task")
// Generators // Generators
val sourceGenerators = settingKey[Seq[Task[Seq[File]]]]("List of tasks that generate sources.").withRank(CSetting) val sourceGenerators = settingKey[Seq[Task[Seq[File]]]]("List of tasks that generate sources.").withRank(CSetting)

View File

@ -18,14 +18,15 @@ import sbt.Project.LoadAction
import sbt.compiler.EvalImports import sbt.compiler.EvalImports
import sbt.internal.Aggregation.AnyKeys import sbt.internal.Aggregation.AnyKeys
import sbt.internal.CommandStrings.BootCommand import sbt.internal.CommandStrings.BootCommand
import sbt.internal.FileManagement.CopiedFileTreeRepository
import sbt.internal._ import sbt.internal._
import sbt.internal.inc.ScalaInstance import sbt.internal.inc.ScalaInstance
import sbt.internal.nio.FileTreeRepository
import sbt.internal.util.Types.{ const, idFun } import sbt.internal.util.Types.{ const, idFun }
import sbt.internal.util._ import sbt.internal.util._
import sbt.internal.util.complete.Parser import sbt.internal.util.complete.Parser
import sbt.io._ import sbt.io._
import sbt.io.syntax._ import sbt.io.syntax._
import sbt.nio.file.FileAttributes
import sbt.util.{ Level, Logger, Show } import sbt.util.{ Level, Logger, Show }
import xsbti.compile.CompilerCache import xsbti.compile.CompilerCache
import xsbti.{ AppMain, AppProvider, ComponentProvider, ScalaProvider } import xsbti.{ AppMain, AppProvider, ComponentProvider, ScalaProvider }
@ -907,17 +908,14 @@ object BuiltinCommands {
() ()
} }
cleanup() cleanup()
val fileTreeRepository = FileTreeRepository.default(FileAttributes.default) val fileTreeRepository = FileTreeRepository.default
val fileCache = System.getProperty("sbt.io.filecache", "validate") val fileCache = System.getProperty("sbt.io.filecache", "validate")
val newState = s val newState = s
.addExitHook(if (cleanedUp.compareAndSet(false, true)) cleanup()) .addExitHook(if (cleanedUp.compareAndSet(false, true)) cleanup())
.put(Keys.taskRepository, new TaskRepository.Repr) .put(Keys.taskRepository, new TaskRepository.Repr)
.put(rawGlobalFileTreeRepository, fileTreeRepository) .put(rawGlobalFileTreeRepository, fileTreeRepository)
if (fileCache == "false" || (fileCache != "true" && Util.isWindows)) newState if (fileCache == "false" || (fileCache != "true" && Util.isWindows)) newState
else { else newState.put(Keys.globalFileTreeRepository, FileManagement.copy(fileTreeRepository))
val copied = new CopiedFileTreeRepository(fileTreeRepository)
newState.put(Keys.globalFileTreeRepository, copied)
}
} catch { } catch {
case NonFatal(_) => s case NonFatal(_) => s
} }

View File

@ -7,14 +7,16 @@
package sbt package sbt
import java.io.InputStream import java.io.InputStream
import java.nio.file.Path
import java.util.concurrent.TimeUnit
import sbt.BasicCommandStrings.ContinuousExecutePrefix import sbt.BasicCommandStrings.ContinuousExecutePrefix
import sbt.internal.FileAttributes
import sbt.internal.LabeledFunctions._ import sbt.internal.LabeledFunctions._
import sbt.internal.util.{ JLine, Util } import sbt.internal.nio.FileEvent
import sbt.internal.util.complete.Parser import sbt.internal.util.complete.Parser
import sbt.internal.util.complete.Parser._ import sbt.internal.util.complete.Parser._
import sbt.io.FileEventMonitor.{ Creation, Deletion, Event, Update } import sbt.internal.util.{ JLine, Util }
import sbt.nio.file.FileAttributes
import sbt.util.{ Level, Logger } import sbt.util.{ Level, Logger }
import scala.annotation.tailrec import scala.annotation.tailrec
@ -22,6 +24,68 @@ import scala.concurrent.duration._
import scala.util.control.NonFatal import scala.util.control.NonFatal
object Watch { object Watch {
sealed trait Event {
def path: Path
def previousAttributes: Option[FileAttributes]
def attributes: Option[FileAttributes]
def occurredAt: FiniteDuration
}
private[sbt] object Event {
private implicit class DurationOps(val d: Duration) extends AnyVal {
def finite: FiniteDuration = d match {
case f: FiniteDuration => f
case _ => new FiniteDuration(Long.MaxValue, TimeUnit.MILLISECONDS)
}
}
def fromIO(fileEvent: FileEvent[FileAttributes]): Watch.Event = fileEvent match {
case c @ FileEvent.Creation(p, a) => new Watch.Creation(p, a, c.occurredAt.value.finite)
case d @ FileEvent.Deletion(p, a) => new Watch.Deletion(p, a, d.occurredAt.value.finite)
case u @ FileEvent.Update(p, prev, attrs) =>
new Watch.Update(p, prev, attrs, u.occurredAt.value.finite)
}
}
final class Deletion private[sbt] (
override val path: Path,
private[this] val attrs: FileAttributes,
override val occurredAt: FiniteDuration
) extends Event {
override def previousAttributes: Option[FileAttributes] = Some(attrs)
override def attributes: Option[FileAttributes] = None
}
object Deletion {
def unapply(deletion: Deletion): Option[(Path, FileAttributes)] =
deletion.previousAttributes.map(a => deletion.path -> a)
}
final class Creation private[sbt] (
override val path: Path,
private[this] val attrs: FileAttributes,
override val occurredAt: FiniteDuration
) extends Event {
override def attributes: Option[FileAttributes] = Some(attrs)
override def previousAttributes: Option[FileAttributes] = None
}
object Creation {
def unapply(creation: Creation): Option[(Path, FileAttributes)] =
creation.attributes.map(a => creation.path -> a)
}
final class Update private[sbt] (
override val path: Path,
private[this] val prevAttrs: FileAttributes,
private[this] val attrs: FileAttributes,
override val occurredAt: FiniteDuration
) extends Event {
override def previousAttributes: Option[FileAttributes] = Some(prevAttrs)
override def attributes: Option[FileAttributes] = Some(attrs)
}
object Update {
def unapply(update: Update): Option[(Path, FileAttributes, FileAttributes)] =
update.previousAttributes
.zip(update.attributes)
.map {
case (previous, current) => (update.path, previous, current)
}
.headOption
}
/** /**
* This trait is used to control the state of [[Watch.apply]]. The [[Watch.Trigger]] action * This trait is used to control the state of [[Watch.apply]]. The [[Watch.Trigger]] action
@ -227,8 +291,8 @@ object Watch {
*/ */
@inline @inline
private[sbt] def aggregate( private[sbt] def aggregate(
events: Seq[(Action, Event[FileAttributes])] events: Seq[(Action, Event)]
): Option[(Action, Event[FileAttributes])] = ): Option[(Action, Event)] =
if (events.isEmpty) None else Some(events.minBy(_._1)) if (events.isEmpty) None else Some(events.minBy(_._1))
private implicit class StringToExec(val s: String) extends AnyVal { private implicit class StringToExec(val s: String) extends AnyVal {
@ -250,17 +314,16 @@ object Watch {
/** /**
* A constant function that returns [[Trigger]]. * A constant function that returns [[Trigger]].
*/ */
final val trigger: (Int, Event[FileAttributes]) => Watch.Action = { final val trigger: (Int, Event) => Watch.Action = { (_: Int, _: Event) =>
(_: Int, _: Event[FileAttributes]) => Trigger
Trigger
}.label("Watched.trigger") }.label("Watched.trigger")
def ifChanged(action: Action): (Int, Event[FileAttributes]) => Watch.Action = def ifChanged(action: Action): (Int, Event) => Watch.Action =
(_: Int, event: Event[FileAttributes]) => (_: Int, event: Event) =>
event match { event match {
case Update(prev, cur, _) if prev.value != cur.value => action case Update(_, previousAttributes, attributes) if previousAttributes != attributes => action
case _: Creation[_] | _: Deletion[_] => action case _: Creation | _: Deletion => action
case _ => Ignore case _ => Ignore
} }
/** /**
@ -369,14 +432,14 @@ object Watch {
* `Keys.watchTriggeredMessage := Watched.defaultOnTriggerMessage`, then nothing is logged when * `Keys.watchTriggeredMessage := Watched.defaultOnTriggerMessage`, then nothing is logged when
* a build is triggered. * a build is triggered.
*/ */
final val defaultOnTriggerMessage: (Int, Event[FileAttributes], Seq[String]) => Option[String] = final val defaultOnTriggerMessage: (Int, Event, Seq[String]) => Option[String] =
((_: Int, e: Event[FileAttributes], commands: Seq[String]) => { ((_: Int, e: Event, commands: Seq[String]) => {
val msg = s"Build triggered by ${e.entry.typedPath.toPath}. " + val msg = s"Build triggered by ${e.path}. " +
s"Running ${commands.mkString("'", "; ", "'")}." s"Running ${commands.mkString("'", "; ", "'")}."
Some(msg) Some(msg)
}).label("Watched.defaultOnTriggerMessage") }).label("Watched.defaultOnTriggerMessage")
final val noTriggerMessage: (Int, Event[FileAttributes], Seq[String]) => Option[String] = final val noTriggerMessage: (Int, Event, Seq[String]) => Option[String] =
(_, _, _) => None (_, _, _) => None
/** /**

View File

@ -9,43 +9,64 @@ package sbt
package internal package internal
import java.io.IOException import java.io.IOException
import java.nio.file.{ DirectoryNotEmptyException, Files } import java.nio.file.{ DirectoryNotEmptyException, Files, Path }
import sbt.Def._ import sbt.Def._
import sbt.Keys._ import sbt.Keys._
import sbt.Project.richInitializeTask import sbt.Project.richInitializeTask
import sbt.io.AllPassFilter
import sbt.io.syntax._ import sbt.io.syntax._
import sbt.io.{ AllPassFilter, FileTreeView, TypedPath } import sbt.nio.Keys._
import sbt.nio.file.{ AnyPath, FileAttributes, FileTreeView, Glob }
import sbt.util.Level import sbt.util.Level
object Clean { object Clean {
def deleteContents(file: File, exclude: TypedPath => Boolean): Unit = def deleteContents(file: File, exclude: File => Boolean): Unit =
deleteContents(file, exclude, FileTreeView.DEFAULT, tryDelete((_: String) => {})) deleteContents(
def deleteContents( file.toPath,
file: File, path => exclude(path.toFile),
exclude: TypedPath => Boolean, FileTreeView.default,
view: FileTreeView, tryDelete((_: String) => {})
delete: File => Unit )
private[sbt] def deleteContents(
path: Path,
exclude: Path => Boolean,
view: FileTreeView.Nio[FileAttributes],
delete: Path => Unit
): Unit = { ): Unit = {
def deleteRecursive(file: File): Unit = { def deleteRecursive(path: Path): Unit = {
view.list(file * AllPassFilter).filterNot(exclude).foreach { view
case dir if dir.isDirectory => .list(Glob(path, AnyPath))
deleteRecursive(dir.toPath.toFile) .filterNot { case (p, _) => exclude(p) }
delete(dir.toPath.toFile) .foreach {
case f => delete(f.toPath.toFile) case (dir, attrs) if attrs.isDirectory =>
} deleteRecursive(dir)
delete(dir)
case (file, _) => delete(file)
}
} }
deleteRecursive(file) deleteRecursive(path)
} }
/** private[this] def cleanFilter(scope: Scope): Def.Initialize[Task[Path => Boolean]] = Def.task {
* Provides an implementation for the clean task. It delegates to [[taskIn]] using the val excludes = (cleanKeepFiles in scope).value.map {
* resolvedScoped key to set the scope. // This mimics the legacy behavior of cleanFilesTask
* @return the clean task definition. case f if f.isDirectory => f * AllPassFilter
*/ case f => f.toGlob
def task: Def.Initialize[Task[Unit]] = } ++ (cleanKeepGlobs in scope).value
Def.taskDyn(taskIn(Keys.resolvedScoped.value.scope)) tag Tags.Clean p: Path => excludes.exists(_.matches(p))
}
private[this] def cleanDelete(scope: Scope): Def.Initialize[Task[Path => Unit]] = Def.task {
// Don't use a regular logger because the logger actually writes to the target directory.
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) => {}
}
tryDelete(debug)
}
/** /**
* Implements the clean task in a given scope. It uses the outputs task value in the provided * Implements the clean task in a given scope. It uses the outputs task value in the provided
@ -58,9 +79,9 @@ object Clean {
val excludes = cleanKeepFiles.value.map { val excludes = cleanKeepFiles.value.map {
// This mimics the legacy behavior of cleanFilesTask // This mimics the legacy behavior of cleanFilesTask
case f if f.isDirectory => f * AllPassFilter case f if f.isDirectory => f * AllPassFilter
case f => f.toGlob case f => f.glob
} ++ cleanKeepGlobs.value } ++ cleanKeepGlobs.value
val excludeFilter: TypedPath => Boolean = excludes.toTypedPathFilter val excludeFilter: Path => Boolean = p => excludes.exists(_.matches(p))
// Don't use a regular logger because the logger actually writes to the target directory. // Don't use a regular logger because the logger actually writes to the target directory.
val debug = (logLevel in scope).?.value.orElse(state.value.get(logLevel.key)) match { val debug = (logLevel in scope).?.value.orElse(state.value.get(logLevel.key)) match {
case Some(Level.Debug) => case Some(Level.Debug) =>
@ -69,26 +90,25 @@ object Clean {
(_: String) => {} (_: String) => {}
} }
val delete = tryDelete(debug) val delete = tryDelete(debug)
cleanFiles.value.sorted.reverseIterator.foreach(delete) cleanFiles.value.sorted.reverseIterator.foreach(f => delete(f.toPath))
(fileOutputs in scope).value.foreach { g => (fileOutputs in scope).value.foreach { g =>
val filter: TypedPath => Boolean = { val filter: Path => Boolean = { path =>
val globFilter = g.toTypedPathFilter !g.matches(path) || excludeFilter(path)
tp => !globFilter(tp) || excludeFilter(tp)
} }
deleteContents(g.base.toFile, filter, FileTreeView.DEFAULT, delete) deleteContents(g.base, filter, FileTreeView.default, delete)
delete(g.base.toFile) delete(g.base)
} }
} tag Tags.Clean } tag Tags.Clean
private def tryDelete(debug: String => Unit): File => Unit = file => { private def tryDelete(debug: String => Unit): Path => Unit = path => {
try { try {
debug(s"clean -- deleting file $file") debug(s"clean -- deleting file $path")
Files.deleteIfExists(file.toPath) Files.deleteIfExists(path)
() ()
} catch { } catch {
case _: DirectoryNotEmptyException => case _: DirectoryNotEmptyException =>
debug(s"clean -- unable to delete non-empty directory $file") debug(s"clean -- unable to delete non-empty directory $path")
case e: IOException => case e: IOException =>
debug(s"Caught unexpected exception $e deleting $file") debug(s"Caught unexpected exception $e deleting $path")
} }
} }
} }

View File

@ -8,7 +8,8 @@
package sbt package sbt
package internal package internal
import java.io.{ ByteArrayInputStream, InputStream } import java.io.{ ByteArrayInputStream, InputStream, File => _, _ }
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import sbt.BasicCommandStrings.{ import sbt.BasicCommandStrings.{
@ -20,13 +21,14 @@ import sbt.BasicCommandStrings.{
import sbt.BasicCommands.otherCommandParser import sbt.BasicCommands.otherCommandParser
import sbt.Def._ import sbt.Def._
import sbt.Scope.Global import sbt.Scope.Global
import sbt.internal.FileManagement.CopiedFileTreeRepository
import sbt.internal.LabeledFunctions._ import sbt.internal.LabeledFunctions._
import sbt.internal.io.WatchState import sbt.internal.io.WatchState
import sbt.internal.nio._
import sbt.internal.util.complete.Parser._ import sbt.internal.util.complete.Parser._
import sbt.internal.util.complete.{ Parser, Parsers } import sbt.internal.util.complete.{ Parser, Parsers }
import sbt.internal.util.{ AttributeKey, Util } import sbt.internal.util.{ AttributeKey, JLine, Util }
import sbt.io._ import sbt.nio.Keys.fileInputs
import sbt.nio.file.{ FileAttributes, Glob }
import sbt.util.{ Level, _ } import sbt.util.{ Level, _ }
import scala.annotation.tailrec import scala.annotation.tailrec
@ -61,6 +63,7 @@ import scala.util.Try
* *
*/ */
object Continuous extends DeprecatedContinuous { object Continuous extends DeprecatedContinuous {
private type Event = FileEvent[FileAttributes]
/** /**
* Provides the dynamic inputs to the continuous build callbacks that cannot be stored as * Provides the dynamic inputs to the continuous build callbacks that cannot be stored as
@ -276,12 +279,23 @@ object Continuous extends DeprecatedContinuous {
f(commands, s, valid, invalid) f(commands, s, valid, invalid)
} }
private[this] def withCharBufferedStdIn[R](f: InputStream => R): R = {
val unwrapped = new FileInputStream(FileDescriptor.in) {
override def close(): Unit = {
getChannel.close() // We don't want to close the System.in file descriptor
}
}
val in = if (Util.isWindows) unwrapped else JLine.terminal.wrapInIfNeeded(unwrapped)
try f(in)
finally in.close()
}
private[sbt] def runToTermination( private[sbt] def runToTermination(
state: State, state: State,
command: String, command: String,
count: Int, count: Int,
isCommand: Boolean isCommand: Boolean
): State = Watch.withCharBufferedStdIn { in => ): State = withCharBufferedStdIn { in =>
val duped = new DupedInputStream(in) val duped = new DupedInputStream(in)
implicit val extracted: Extracted = Project.extract(state) implicit val extracted: Extracted = Project.extract(state)
val (stateWithRepo, repo) = state.get(Keys.globalFileTreeRepository) match { val (stateWithRepo, repo) = state.get(Keys.globalFileTreeRepository) match {
@ -290,12 +304,13 @@ object Continuous extends DeprecatedContinuous {
val repo = if ("polling" == System.getProperty("sbt.watch.mode")) { val repo = if ("polling" == System.getProperty("sbt.watch.mode")) {
val service = val service =
new PollingWatchService(extracted.getOpt(Keys.pollInterval).getOrElse(500.millis)) new PollingWatchService(extracted.getOpt(Keys.pollInterval).getOrElse(500.millis))
FileTreeRepository.legacy(FileAttributes.default _, (_: Any) => {}, service) FileTreeRepository
.legacy((_: Any) => {}, service)
} else { } else {
state state
.get(BuiltinCommands.rawGlobalFileTreeRepository) .get(BuiltinCommands.rawGlobalFileTreeRepository)
.map(new CopiedFileTreeRepository(_)) .map(FileManagement.copy)
.getOrElse(FileTreeRepository.default(FileAttributes.default)) .getOrElse(FileTreeRepository.default)
} }
(state.put(Keys.globalFileTreeRepository, repo), repo) (state.put(Keys.globalFileTreeRepository, repo), repo)
} }
@ -372,8 +387,8 @@ object Continuous extends DeprecatedContinuous {
* Aggregates a collection of [[Config]] instances into a single instance of [[Callbacks]]. * Aggregates a collection of [[Config]] instances into a single instance of [[Callbacks]].
* This allows us to monitor and respond to changes for all of * This allows us to monitor and respond to changes for all of
* the inputs and triggers for each of the tasks that we are monitoring in the continuous build. * the inputs and triggers for each of the tasks that we are monitoring in the continuous build.
* To monitor all of the inputs and triggers, it creates a [[FileEventMonitor]] for each task * To monitor all of the inputs and triggers, it creates a monitor for each task
* and then aggregates each of the individual [[FileEventMonitor]] instances into an aggregated * and then aggregates each of the individual monitor instances into an aggregated
* instance. It aggregates all of the event callbacks into a single callback that delegates * instance. It aggregates all of the event callbacks into a single callback that delegates
* to each of the individual callbacks. For the callbacks that return a [[Watch.Action]], * to each of the individual callbacks. For the callbacks that return a [[Watch.Action]],
* the aggregated callback will select the minimum [[Watch.Action]] returned where the ordering * the aggregated callback will select the minimum [[Watch.Action]] returned where the ordering
@ -405,7 +420,7 @@ object Continuous extends DeprecatedContinuous {
val onEnter = () => configs.foreach(_.watchSettings.onEnter()) val onEnter = () => configs.foreach(_.watchSettings.onEnter())
val onStart: () => Watch.Action = getOnStart(project, commands, configs, rawLogger, count) val onStart: () => Watch.Action = getOnStart(project, commands, configs, rawLogger, count)
val nextInputEvent: () => Watch.Action = parseInputEvents(configs, state, inputStream, logger) val nextInputEvent: () => Watch.Action = parseInputEvents(configs, state, inputStream, logger)
val (nextFileEvent, cleanupFileMonitor): (() => Option[(Event, Watch.Action)], () => Unit) = val (nextFileEvent, cleanupFileMonitor): (() => Option[(Watch.Event, Watch.Action)], () => Unit) =
getFileEvents(configs, rawLogger, state, count, commands) getFileEvents(configs, rawLogger, state, count, commands)
val nextEvent: () => Watch.Action = val nextEvent: () => Watch.Action =
combineInputAndFileEvents(nextInputEvent, nextFileEvent, logger) combineInputAndFileEvents(nextInputEvent, nextFileEvent, logger)
@ -460,25 +475,32 @@ object Continuous extends DeprecatedContinuous {
val res = f.view.map(_()).min val res = f.view.map(_()).min
// Print the default watch message if there are multiple tasks // Print the default watch message if there are multiple tasks
if (configs.size > 1) if (configs.size > 1)
Watch.defaultStartWatch(count.get(), project, commands).foreach(logger.info(_)) Watch
.defaultStartWatch(count.get(), project, commands)
.foreach(logger.info(_))
res res
} }
} }
private implicit class TraversableGlobOps(val t: Traversable[Glob]) extends AnyVal {
def toFilter: Path => Boolean = p => t.exists(_.matches(p))
}
private def getFileEvents( private def getFileEvents(
configs: Seq[Config], configs: Seq[Config],
logger: Logger, logger: Logger,
state: State, state: State,
count: AtomicInteger, count: AtomicInteger,
commands: Seq[String] commands: Seq[String]
)(implicit extracted: Extracted): (() => Option[(Event, Watch.Action)], () => Unit) = { )(implicit extracted: Extracted): (() => Option[(Watch.Event, Watch.Action)], () => Unit) = {
val trackMetaBuild = configs.forall(_.watchSettings.trackMetaBuild) val trackMetaBuild = configs.forall(_.watchSettings.trackMetaBuild)
val buildGlobs = val buildGlobs =
if (trackMetaBuild) extracted.getOpt(Keys.fileInputs in Keys.settingsData).getOrElse(Nil) if (trackMetaBuild) extracted.getOpt(fileInputs in Keys.settingsData).getOrElse(Nil)
else Nil else Nil
val buildFilter = buildGlobs.toEntryFilter val buildFilter: Path => Boolean = buildGlobs.toFilter
val defaultTrigger = if (Util.isWindows) Watch.ifChanged(Watch.Trigger) else Watch.trigger val defaultTrigger = if (Util.isWindows) Watch.ifChanged(Watch.Trigger) else Watch.trigger
val onEvent: Event => (Event, Watch.Action) = { val retentionPeriod = configs.map(_.watchSettings.antiEntropyRetentionPeriod).max
val quarantinePeriod = configs.map(_.watchSettings.deletionQuarantinePeriod).max
val onEvent: Event => (Watch.Event, Watch.Action) = {
val f = configs.map { params => val f = configs.map { params =>
val ws = params.watchSettings val ws = params.watchSettings
val oe = ws.onEvent val oe = ws.onEvent
@ -487,23 +509,25 @@ object Continuous extends DeprecatedContinuous {
val onInputEvent = ws.onInputEvent.getOrElse(defaultTrigger) val onInputEvent = ws.onInputEvent.getOrElse(defaultTrigger)
val onTriggerEvent = ws.onTriggerEvent.getOrElse(defaultTrigger) val onTriggerEvent = ws.onTriggerEvent.getOrElse(defaultTrigger)
val onMetaBuildEvent = ws.onMetaBuildEvent.getOrElse(Watch.ifChanged(Watch.Reload)) val onMetaBuildEvent = ws.onMetaBuildEvent.getOrElse(Watch.ifChanged(Watch.Reload))
val triggerFilter = params.triggers.toEntryFilter val triggerFilter = params.triggers.toFilter
val excludedBuildFilter = buildFilter val excludedBuildFilter = buildFilter
event: Event => event: Watch.Event =>
val inputFilter = params.inputs().toEntryFilter val inputFilter = params.inputs().toFilter
val c = count.get() val c = count.get()
val entry = event.entry
Seq[Watch.Action]( Seq[Watch.Action](
if (inputFilter(entry)) onInputEvent(c, event) else Watch.Ignore, if (inputFilter(event.path)) onInputEvent(c, event) else Watch.Ignore,
if (triggerFilter(entry)) onTriggerEvent(c, event) else Watch.Ignore, if (triggerFilter(event.path)) onTriggerEvent(c, event) else Watch.Ignore,
if (excludedBuildFilter(entry)) onMetaBuildEvent(c, event) else Watch.Ignore if (excludedBuildFilter(event.path)) onMetaBuildEvent(c, event)
else Watch.Ignore
).min ).min
} }
event: Event => event -> oe(event) event: Event =>
val watchEvent = Watch.Event.fromIO(event)
watchEvent -> oe(watchEvent)
} }
event: Event => f.view.map(_.apply(event)).minBy(_._2) event: Event => f.view.map(_.apply(event)).minBy(_._2)
} }
val monitor: FileEventMonitor[FileAttributes] = new FileEventMonitor[FileAttributes] { val monitor: FileEventMonitor[Event] = new FileEventMonitor[Event] {
/** /**
* Create a filtered monitor that only accepts globs that have been registered for the * Create a filtered monitor that only accepts globs that have been registered for the
@ -514,44 +538,60 @@ object Continuous extends DeprecatedContinuous {
* @return the filtered FileEventMonitor. * @return the filtered FileEventMonitor.
*/ */
private def filter( private def filter(
monitor: FileEventMonitor[FileAttributes], monitor: FileEventMonitor[Event],
globs: () => Seq[Glob] globs: () => Seq[Glob]
): FileEventMonitor[FileAttributes] = { ): FileEventMonitor[Event] = {
new FileEventMonitor[FileAttributes] { new FileEventMonitor[Event] {
override def poll(duration: Duration): Seq[FileEventMonitor.Event[FileAttributes]] = override def poll(
monitor.poll(duration).filter(e => globs().toEntryFilter(e.entry)) duration: Duration,
filter: Event => Boolean
): Seq[Event] = monitor.poll(duration, filter).filter(e => globs().toFilter(e.path))
override def close(): Unit = monitor.close() override def close(): Unit = monitor.close()
} }
} }
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 // TODO make this a normal monitor
private[this] val monitors: Seq[FileEventMonitor[FileAttributes]] = private[this] val monitors: Seq[FileEventMonitor[Event]] =
configs.map { config => configs.map { config =>
// Create a logger with a scoped key prefix so that we can tell from which // Create a logger with a scoped key prefix so that we can tell from which
// monitor events occurred. // monitor events occurred.
val l = logger.withPrefix(config.key.show) val l = logger.withPrefix(config.key.show)
val monitor: FileEventMonitor[FileAttributes] = val monitor: FileEventMonitor[Event] =
FileManagement.monitor(config.repository, config.watchSettings.antiEntropy, l) FileEventMonitor.antiEntropy(
val allGlobs: () => Seq[Glob] = () => (config.inputs() ++ config.triggers).distinct.sorted config.repository,
config.watchSettings.antiEntropy,
l,
config.watchSettings.deletionQuarantinePeriod,
config.watchSettings.antiEntropyRetentionPeriod
)
val allGlobs: () => Seq[Glob] =
() => (config.inputs() ++ config.triggers).distinct.sorted
filter(monitor, allGlobs) filter(monitor, allGlobs)
} ++ (if (trackMetaBuild) { } ++ (if (trackMetaBuild) {
val l = logger.withPrefix("meta-build") val l = logger.withPrefix("meta-build")
val antiEntropy = configs.map(_.watchSettings.antiEntropy).max val antiEntropy = configs.map(_.watchSettings.antiEntropy).max
val repo = getRepository(state) val repo = getRepository(state)
buildGlobs.foreach(repo.register) buildGlobs.foreach(repo.register)
val monitor = FileManagement.monitor(repo, antiEntropy, l) val monitor = FileEventMonitor.antiEntropy(
repo,
antiEntropy,
l,
quarantinePeriod,
retentionPeriod
)
filter(monitor, () => buildGlobs) :: Nil filter(monitor, () => buildGlobs) :: Nil
} else Nil) } else Nil)
override def poll(duration: Duration): Seq[FileEventMonitor.Event[FileAttributes]] = { override def poll(duration: Duration, filter: Event => Boolean): Seq[Event] = {
val res = monitors.flatMap(_.poll(0.millis)).toSet.toVector val res = monitors.flatMap(_.poll(0.millis, filter)).toSet.toVector
if (res.isEmpty) Thread.sleep(duration.toMillis) if (res.isEmpty) Thread.sleep(duration.toMillis)
res res
} }
override def close(): Unit = monitors.foreach(_.close()) override def close(): Unit = monitors.foreach(_.close())
} }
val watchLogger: WatchLogger = msg => logger.debug(msg.toString) val watchLogger: WatchLogger = msg => logger.debug(msg.toString)
val retentionPeriod = configs.map(_.watchSettings.antiEntropyRetentionPeriod).max
val antiEntropy = configs.map(_.watchSettings.antiEntropy).max val antiEntropy = configs.map(_.watchSettings.antiEntropy).max
val quarantinePeriod = configs.map(_.watchSettings.deletionQuarantinePeriod).max
val antiEntropyMonitor = FileEventMonitor.antiEntropy( val antiEntropyMonitor = FileEventMonitor.antiEntropy(
monitor, monitor,
antiEntropy, antiEntropy,
@ -564,7 +604,7 @@ object Continuous extends DeprecatedContinuous {
* motivation is to allow the user to specify this callback via setting so that, for example, * motivation is to allow the user to specify this callback via setting so that, for example,
* they can clear the screen when the build triggers. * they can clear the screen when the build triggers.
*/ */
val onTrigger: Event => Unit = { event: Event => val onTrigger: Watch.Event => Unit = { event: Watch.Event =>
configs.foreach { params => configs.foreach { params =>
params.watchSettings.onTrigger.foreach(ot => ot(params.arguments(logger))(event)) params.watchSettings.onTrigger.foreach(ot => ot(params.arguments(logger))(event))
} }
@ -586,7 +626,7 @@ object Continuous extends DeprecatedContinuous {
val min = actions.minBy { val min = actions.minBy {
case (e, a) => case (e, a) =>
if (builder.nonEmpty) builder.append(", ") if (builder.nonEmpty) builder.append(", ")
val path = e.entry.typedPath.toPath.toString val path = e.path
builder.append(path) builder.append(path)
builder.append(" -> ") builder.append(" -> ")
builder.append(a.toString) builder.append(a.toString)
@ -672,10 +712,10 @@ object Continuous extends DeprecatedContinuous {
private def combineInputAndFileEvents( private def combineInputAndFileEvents(
nextInputAction: () => Watch.Action, nextInputAction: () => Watch.Action,
nextFileEvent: () => Option[(Event, Watch.Action)], nextFileEvent: () => Option[(Watch.Event, Watch.Action)],
logger: Logger logger: Logger
): () => Watch.Action = () => { ): () => Watch.Action = () => {
val (inputAction: Watch.Action, fileEvent: Option[(Event, Watch.Action)] @unchecked) = val (inputAction: Watch.Action, fileEvent: Option[(Watch.Event, Watch.Action)] @unchecked) =
Seq(nextInputAction, nextFileEvent).map(_.apply()).toIndexedSeq match { Seq(nextInputAction, nextFileEvent).map(_.apply()).toIndexedSeq match {
case Seq(ia: Watch.Action, fe @ Some(_)) => (ia, fe) case Seq(ia: Watch.Action, fe @ Some(_)) => (ia, fe)
case Seq(ia: Watch.Action, None) => (ia, None) case Seq(ia: Watch.Action, None) => (ia, None)
@ -688,7 +728,7 @@ object Continuous extends DeprecatedContinuous {
fileEvent fileEvent
.collect { .collect {
case (event, action) if action != Watch.Ignore => case (event, action) if action != Watch.Ignore =>
s"Received file event $action for ${event.entry.typedPath.toPath}." + s"Received file event $action for ${event.path}." +
(if (action != min) s" Dropping in favor of input event: $min" else "") (if (action != min) s" Dropping in favor of input event: $min" else "")
} }
.foreach(logger.debug(_)) .foreach(logger.debug(_))
@ -738,7 +778,7 @@ object Continuous extends DeprecatedContinuous {
} }
} }
private type WatchOnEvent = (Int, Event) => Watch.Action private type WatchOnEvent = (Int, Watch.Event) => Watch.Action
/** /**
* Contains all of the user defined settings that will be used to build a [[Callbacks]] * Contains all of the user defined settings that will be used to build a [[Callbacks]]
@ -781,7 +821,7 @@ object Continuous extends DeprecatedContinuous {
key.get(Keys.watchInputParser).getOrElse(Watch.defaultInputParser) key.get(Keys.watchInputParser).getOrElse(Watch.defaultInputParser)
val logLevel: Level.Value = key.get(Keys.watchLogLevel).getOrElse(Level.Info) val logLevel: Level.Value = key.get(Keys.watchLogLevel).getOrElse(Level.Info)
val onEnter: () => Unit = key.get(Keys.watchOnEnter).getOrElse(() => {}) val onEnter: () => Unit = key.get(Keys.watchOnEnter).getOrElse(() => {})
val onEvent: Option[Arguments => Event => Watch.Action] = key.get(Keys.watchOnEvent) val onEvent: Option[Arguments => Watch.Event => Watch.Action] = key.get(Keys.watchOnEvent)
val onExit: () => Unit = key.get(Keys.watchOnExit).getOrElse(() => {}) val onExit: () => Unit = key.get(Keys.watchOnExit).getOrElse(() => {})
val onInputEvent: Option[WatchOnEvent] = key.get(Keys.watchOnInputEvent) val onInputEvent: Option[WatchOnEvent] = key.get(Keys.watchOnInputEvent)
val onIteration: Option[Int => Watch.Action] = key.get(Keys.watchOnIteration) val onIteration: Option[Int => Watch.Action] = key.get(Keys.watchOnIteration)
@ -789,11 +829,11 @@ object Continuous extends DeprecatedContinuous {
val onStart: Option[Arguments => () => Watch.Action] = key.get(Keys.watchOnStart) val onStart: Option[Arguments => () => Watch.Action] = key.get(Keys.watchOnStart)
val onTermination: Option[(Watch.Action, String, Int, State) => State] = val onTermination: Option[(Watch.Action, String, Int, State) => State] =
key.get(Keys.watchOnTermination) key.get(Keys.watchOnTermination)
val onTrigger: Option[Arguments => Event => Unit] = key.get(Keys.watchOnTrigger) val onTrigger: Option[Arguments => Watch.Event => Unit] = key.get(Keys.watchOnTrigger)
val onTriggerEvent: Option[WatchOnEvent] = key.get(Keys.watchOnTriggerEvent) val onTriggerEvent: Option[WatchOnEvent] = key.get(Keys.watchOnTriggerEvent)
val startMessage: StartMessage = getStartMessage(key) val startMessage: StartMessage = getStartMessage(key)
val trackMetaBuild: Boolean = key.get(Keys.watchTrackMetaBuild).getOrElse(true) val trackMetaBuild: Boolean = key.get(Keys.watchTrackMetaBuild).getOrElse(true)
val triggerMessage: TriggerMessage = getTriggerMessage(key) val triggerMessage: TriggerMessage[Watch.Event] = getTriggerMessage(key)
// Unlike the rest of the settings, InputStream is a TaskKey which means that if it is set, // Unlike the rest of the settings, InputStream is a TaskKey which means that if it is set,
// we have to use Extracted.runTask to get the value. The reason for this is because it is // we have to use Extracted.runTask to get the value. The reason for this is because it is
@ -827,7 +867,9 @@ object Continuous extends DeprecatedContinuous {
lazy val default = key.get(Keys.watchStartMessage).getOrElse(Watch.defaultStartWatch) lazy val default = key.get(Keys.watchStartMessage).getOrElse(Watch.defaultStartWatch)
key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default)) key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default))
} }
private def getTriggerMessage(key: ScopedKey[_])(implicit e: Extracted): TriggerMessage = { private def getTriggerMessage(
key: ScopedKey[_]
)(implicit e: Extracted): TriggerMessage[Watch.Event] = {
lazy val default = lazy val default =
key.get(Keys.watchTriggeredMessage).getOrElse(Watch.defaultOnTriggerMessage) key.get(Keys.watchTriggeredMessage).getOrElse(Watch.defaultOnTriggerMessage)
key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default)) key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default))

View File

@ -10,10 +10,10 @@ package sbt.internal
import sbt.internal.io.{ WatchState => WS } import sbt.internal.io.{ WatchState => WS }
private[internal] trait DeprecatedContinuous { private[internal] trait DeprecatedContinuous {
protected type Event = sbt.io.FileEventMonitor.Event[FileAttributes]
protected type StartMessage = protected type StartMessage =
Option[Either[WS => String, (Int, String, Seq[String]) => Option[String]]] Option[Either[WS => String, (Int, String, Seq[String]) => Option[String]]]
protected type TriggerMessage = Either[WS => String, (Int, Event, Seq[String]) => Option[String]] protected type TriggerMessage[Event] =
Either[WS => String, (Int, Event, Seq[String]) => Option[String]]
protected type DeprecatedWatchState = WS protected type DeprecatedWatchState = WS
protected val deprecatedWatchingMessage = sbt.Keys.watchingMessage protected val deprecatedWatchingMessage = sbt.Keys.watchingMessage
protected val deprecatedTriggeredMessage = sbt.Keys.triggeredMessage protected val deprecatedTriggeredMessage = sbt.Keys.triggeredMessage

View File

@ -10,10 +10,13 @@ package sbt.internal
import java.nio.file.Paths import java.nio.file.Paths
import java.util.Optional import java.util.Optional
import sbt.Stamped
import sbt.internal.inc.ExternalLookup import sbt.internal.inc.ExternalLookup
import sbt.io.AllPassFilter
import sbt.io.syntax._ import sbt.io.syntax._
import sbt.io.{ AllPassFilter, TypedPath } import sbt.nio.FileStamp
import sbt.nio.FileStamp.StampedFile
import sbt.nio.file.syntax._
import sbt.nio.file.{ FileAttributes, FileTreeView }
import xsbti.compile._ import xsbti.compile._
import xsbti.compile.analysis.Stamp import xsbti.compile.analysis.Stamp
@ -21,21 +24,27 @@ import scala.collection.mutable
private[sbt] object ExternalHooks { private[sbt] object ExternalHooks {
private val javaHome = Option(System.getProperty("java.home")).map(Paths.get(_)) private val javaHome = Option(System.getProperty("java.home")).map(Paths.get(_))
def apply(options: CompileOptions, repo: FileTree.Repository): DefaultExternalHooks = { def apply(
options: CompileOptions,
view: FileTreeView.Nio[FileAttributes]
): DefaultExternalHooks = {
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
val sources = options.sources() val sources = options.sources()
val cachedSources = new java.util.HashMap[File, Stamp] val cachedSources = new java.util.HashMap[File, Stamp]
val converter: File => Stamp = f => Stamped.sourceConverter(TypedPath(f.toPath))
sources.foreach { sources.foreach {
case sf: Stamped => cachedSources.put(sf, sf.stamp) case sf: StampedFile => cachedSources.put(sf, sf.stamp)
case f: File => cachedSources.put(f, converter(f)) case f: File => cachedSources.put(f, FileStamp.stamped(f))
} }
val allBinaries = new java.util.HashMap[File, Stamp] val allBinaries = new java.util.HashMap[File, Stamp]
options.classpath.foreach { options.classpath.foreach {
case f if f.getName.endsWith(".jar") => case f if f.getName.endsWith(".jar") =>
repo.get(f.toGlob) foreach { case (p, a) => allBinaries.put(p.toFile, a.stamp) } view.list(f.toGlob) foreach {
case (p, a) => allBinaries.put(p.toFile, FileStamp(p, a).stamp)
}
case f => case f =>
repo.get(f ** AllPassFilter) foreach { case (p, a) => allBinaries.put(p.toFile, a.stamp) } view.list(f ** AllPassFilter) foreach {
case (p, a) => allBinaries.put(p.toFile, FileStamp(p, a).stamp)
}
} }
val lookup = new ExternalLookup { val lookup = new ExternalLookup {

View File

@ -9,61 +9,22 @@ package sbt
package internal package internal
import java.io.IOException import java.io.IOException
import java.nio.file.Path
import sbt.internal.io.HybridPollingFileTreeRepository import sbt.internal.nio.{ FileEvent, FileTreeRepository, Observable, Observer }
import sbt.io.FileTreeDataView.{ Entry, Observable, Observer, Observers } import sbt.nio.file.Glob
import sbt.io.{ FileTreeRepository, _ }
import sbt.util.Logger
import scala.concurrent.duration._
private[sbt] object FileManagement { private[sbt] object FileManagement {
private[sbt] def monitor( private[sbt] def copy[T](fileTreeRepository: FileTreeRepository[T]): FileTreeRepository[T] =
repository: FileTreeRepository[FileAttributes], new CopiedFileTreeRepository[T](fileTreeRepository)
antiEntropy: FiniteDuration, private[this] class CopiedFileTreeRepository[T](underlying: FileTreeRepository[T])
logger: Logger
): FileEventMonitor[FileAttributes] = {
// Forwards callbacks to the repository. The close method removes all of these
// callbacks.
val copied: Observable[FileAttributes] = new Observable[FileAttributes] {
private[this] val observers = new Observers[FileAttributes]
val underlying = repository match {
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[FileAttributes]): Int =
observers.addObserver(observer)
override def removeObserver(handle: Int): Unit = observers.removeObserver(handle)
override def close(): Unit = {
underlying.removeObserver(handle)
underlying.close()
}
}
new FileEventMonitor[FileAttributes] {
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[FileAttributes]] =
monitor.poll(duration)
override def close(): Unit = monitor.close()
}
}
private[sbt] class CopiedFileTreeRepository[T](underlying: FileTreeRepository[T])
extends FileTreeRepository[T] { extends FileTreeRepository[T] {
def addObserver(observer: Observer[T]) = underlying.addObserver(observer) override def list(path: Path): Seq[(Path, T)] = underlying.list(path)
def close(): Unit = {} // Don't close the underlying observable override def close(): Unit = {}
def list(glob: Glob): Seq[TypedPath] = underlying.list(glob) override def register(glob: Glob): Either[IOException, Observable[FileEvent[T]]] =
def listEntries(glob: Glob): Seq[Entry[T]] = underlying.listEntries(glob) underlying.register(glob)
def removeObserver(handle: Int): Unit = underlying.removeObserver(handle) override def addObserver(observer: Observer[FileEvent[T]]): AutoCloseable =
def register(glob: Glob): Either[IOException, Boolean] = underlying.register(glob) underlying.addObserver(observer)
def unregister(glob: Glob): Unit = underlying.unregister(glob) override def toString: String = s"CopiedFileTreeRepository($underlying)"
} }
} }

View File

@ -8,25 +8,15 @@
package sbt package sbt
package internal package internal
import java.nio.file.{ Path, WatchService => _ } import java.nio.file.{ WatchService => _ }
import sbt.internal.util.appmacro.MacroDefaults import sbt.internal.util.appmacro.MacroDefaults
import sbt.io.FileTreeDataView.Entry import sbt.nio.file.Glob
import sbt.io._
import scala.collection.mutable import scala.collection.mutable
import scala.language.experimental.macros import scala.language.experimental.macros
object FileTree { object FileTree {
private sealed trait CacheOptions
private case object NoCache extends CacheOptions
private case object UseCache extends CacheOptions
private case object Validate extends CacheOptions
private def toPair(
filter: Entry[FileAttributes] => Boolean
)(e: Entry[FileAttributes]): Option[(Path, FileAttributes)] =
e.value.toOption.flatMap(a => if (filter(e)) Some(e.typedPath.toPath -> a) else None)
trait Repository extends sbt.internal.Repository[Seq, Glob, (Path, FileAttributes)]
private[sbt] trait DynamicInputs { private[sbt] trait DynamicInputs {
def value: Option[mutable.Set[Glob]] def value: Option[mutable.Set[Glob]]
} }
@ -36,69 +26,4 @@ object FileTree {
private final class impl(override val value: Option[mutable.Set[Glob]]) extends DynamicInputs private final class impl(override val value: Option[mutable.Set[Glob]]) extends DynamicInputs
implicit def default: DynamicInputs = macro MacroDefaults.dynamicInputs implicit def default: DynamicInputs = macro MacroDefaults.dynamicInputs
} }
private[sbt] 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(FileAttributes.default)
override def get(key: Glob): Seq[(Path, FileAttributes)] =
view.listEntries(key).flatMap(toPair(key.toEntryFilter))
override def close(): Unit = {}
}
}
private class CachingRepository(underlying: FileTreeRepository[FileAttributes])
extends Repository {
lazy val cacheOptions = System.getProperty("sbt.io.filecache") match {
case "true" => UseCache
case "validate" => Validate
case _ => NoCache
}
override def get(key: Glob): Seq[(Path, FileAttributes)] = {
underlying.register(key)
cacheOptions match {
case Validate =>
val res = Repository.polling.get(key)
val filter = key.toEntryFilter
val cacheRes = underlying
.listEntries(key)
.flatMap(e => if (filter(e)) Some(e.typedPath.toPath) else None)
.toSet
val resSet = res.map(_._1).toSet
if (cacheRes != resSet) {
val msg = "Warning: got different files when using the internal file cache compared " +
s"to polling the file system for key: $key.\n"
val fileDiff = cacheRes diff resSet match {
case d if d.nonEmpty =>
new Exception("hmm").printStackTrace()
s"Cache had files not found in the file system:\n${d.mkString("\n")}.\n"
case _ => ""
}
val cacheDiff = resSet diff cacheRes match {
case d if d.nonEmpty =>
(if (fileDiff.isEmpty) "" else " ") +
s"File system had files not in the cache:\n${d.mkString("\n")}.\n"
case _ => ""
}
val diff = fileDiff + cacheDiff
val instructions = "Please open an issue at https://github.com/sbt/sbt. To disable " +
"this warning, run sbt with -Dsbt.io.filecache=false"
System.err.println(msg + diff + instructions)
}
res
case UseCache =>
underlying.listEntries(key).flatMap(toPair(key.toEntryFilter))
case NoCache =>
Repository.polling.get(key)
}
}
override def close(): Unit = underlying.close()
}
private[sbt] def repository(underlying: FileTreeRepository[FileAttributes]): Repository =
new CachingRepository(underlying)
} }

View File

@ -8,14 +8,9 @@
package sbt package sbt
package internal package internal
import java.io.File
import java.nio.file.Path import java.nio.file.Path
import java.util.concurrent.ConcurrentSkipListMap
import sbt.io.{ FileFilter, Glob, SimpleFileFilter } import sbt.nio.file.{ FileAttributes, FileTreeView, Glob }
import scala.collection.JavaConverters._
import scala.collection.mutable
/** /**
* Retrieve files from a repository. This should usually be an extension class for * Retrieve files from a repository. This should usually be an extension class for
@ -24,19 +19,20 @@ import scala.collection.mutable
*/ */
private[sbt] sealed trait GlobLister extends Any { private[sbt] sealed trait GlobLister extends Any {
final def all(repository: FileTree.Repository): Seq[(Path, FileAttributes)] = final def all(view: FileTreeView.Nio[FileAttributes]): Seq[(Path, FileAttributes)] = {
all(repository, FileTree.DynamicInputs.empty) all(view, FileTree.DynamicInputs.empty)
}
/** /**
* Get the sources described this `GlobLister`. The results should not return any duplicate * Get the sources described this `GlobLister`. The results should not return any duplicate
* entries for each path in the result set. * entries for each path in the result set.
* *
* @param repository the file tree repository for retrieving the files for a given glob. * @param view the file tree view
* @param dynamicInputs the task dynamic inputs to track for watch. * @param dynamicInputs the task dynamic inputs to track for watch.
* @return the files described by this `GlobLister`. * @return the files described by this `GlobLister`.
*/ */
def all( def all(
implicit repository: FileTree.Repository, implicit view: FileTreeView.Nio[FileAttributes],
dynamicInputs: FileTree.DynamicInputs dynamicInputs: FileTree.DynamicInputs
): Seq[(Path, FileAttributes)] ): Seq[(Path, FileAttributes)]
} }
@ -55,7 +51,7 @@ private[sbt] trait GlobListers {
import GlobListers._ import GlobListers._
/** /**
* Generate a GlobLister given a particular [[Glob]]s. * Generate a GlobLister given a particular `Glob`s.
* *
* @param source the input Glob * @param source the input Glob
*/ */
@ -71,34 +67,6 @@ private[sbt] trait GlobListers {
new impl(sources) new impl(sources)
} }
private[internal] object GlobListers { private[internal] object GlobListers {
private def covers(left: Glob, right: Glob): Boolean = {
right.base.startsWith(left.base) && {
left.depth == Int.MaxValue || {
val depth = left.base.relativize(right.base).getNameCount - 1
depth <= left.depth - right.depth
}
}
}
private def aggregate(globs: Traversable[Glob]): Seq[(Glob, Traversable[Glob])] = {
val sorted = globs.toSeq.sorted
val map = new ConcurrentSkipListMap[Path, (Glob, mutable.Set[Glob])]
if (sorted.size > 1) {
sorted.foreach { glob =>
map.subMap(glob.base.getRoot, glob.base.resolve(Char.MaxValue.toString)).asScala.find {
case (_, (g, _)) => covers(g, glob)
} match {
case Some((_, (_, globs))) => globs += glob
case None =>
val globs = mutable.Set(glob)
val filter: FileFilter = new SimpleFileFilter((file: File) => {
globs.exists(_.toFileFilter.accept(file))
})
map.put(glob.base, (Glob(glob.base, filter, glob.depth), globs))
}
}
map.asScala.values.toIndexedSeq
} else sorted.map(g => g -> (g :: Nil))
}
/** /**
* Implements `GlobLister` given a collection of Globs. If the input collection type * Implements `GlobLister` given a collection of Globs. If the input collection type
@ -110,14 +78,11 @@ private[internal] object GlobListers {
*/ */
private class impl[T <: Traversable[Glob]](val globs: T) extends AnyVal with GlobLister { private class impl[T <: Traversable[Glob]](val globs: T) extends AnyVal with GlobLister {
override def all( override def all(
implicit repository: FileTree.Repository, implicit view: FileTreeView.Nio[FileAttributes],
dynamicInputs: FileTree.DynamicInputs dynamicInputs: FileTree.DynamicInputs
): Seq[(Path, FileAttributes)] = { ): Seq[(Path, FileAttributes)] = {
aggregate(globs).flatMap { dynamicInputs.value.foreach(_ ++= globs)
case (glob, allGlobs) => view.list(globs)
dynamicInputs.value.foreach(_ ++= allGlobs)
repository.get(glob)
}.toIndexedSeq
} }
} }
} }

View File

@ -14,7 +14,9 @@ import sbt._
import sbt.internal.io.Source import sbt.internal.io.Source
import sbt.internal.util.AttributeMap import sbt.internal.util.AttributeMap
import sbt.internal.util.complete.Parser import sbt.internal.util.complete.Parser
import sbt.io.Glob import sbt.io.syntax._
import sbt.nio.Keys._
import sbt.nio.file.Glob
import scala.annotation.tailrec import scala.annotation.tailrec
@ -25,14 +27,11 @@ object TransitiveGlobs {
Def.taskKey[(Seq[Glob], Seq[Glob])]("The transitive inputs and triggers for a key") Def.taskKey[(Seq[Glob], Seq[Glob])]("The transitive inputs and triggers for a key")
} }
private[sbt] object InputGraph { private[sbt] object InputGraph {
@deprecated("Source is also deprecated.", "1.3.0")
private implicit class SourceOps(val source: Source) { private implicit class SourceOps(val source: Source) {
def toGlob: Glob = def toGlob: Glob = {
Glob( val filter = source.includeFilter -- source.excludeFilter
source.base, if (source.recursive) source.base ** filter else source.base * filter
source.includeFilter -- source.excludeFilter, }
if (source.recursive) Int.MaxValue else 0
)
} }
private[sbt] def inputsTask: Def.Initialize[Task[Seq[Glob]]] = private[sbt] def inputsTask: Def.Initialize[Task[Seq[Glob]]] =
Def.task(transitiveGlobs(arguments.value)._1.sorted) Def.task(transitiveGlobs(arguments.value)._1.sorted)

View File

@ -35,6 +35,7 @@ import scala.collection.JavaConverters._
*/ */
trait Repository[M[_], K, V] extends AutoCloseable { trait Repository[M[_], K, V] extends AutoCloseable {
def get(key: K): M[V] def get(key: K): M[V]
override def close(): Unit = {}
} }
private[sbt] final class MutableRepository[K, V] extends Repository[Option, K, V] { private[sbt] final class MutableRepository[K, V] extends Repository[Option, K, V] {

View File

@ -0,0 +1,83 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.nio
import java.io.{ File, IOException }
import java.nio.file.Path
import java.util
import sbt.internal.Repository
import sbt.internal.inc.{ EmptyStamp, Stamper, LastModified => IncLastModified }
import sbt.internal.util.AttributeKey
import sbt.io.IO
import sbt.nio.file.FileAttributes
import sbt.{ Def, Task }
import xsbti.compile.analysis.Stamp
import scala.util.Try
sealed trait FileStamp
object FileStamp {
private[nio] type Id[T] = T
private[nio] val attributeMapKey =
AttributeKey[util.HashMap[Path, (Option[Hash], Option[LastModified])]]("task-attribute-map")
private[sbt] def fileHashMap: Def.Initialize[Task[Repository[Id, Path, Hash]]] = Def.task {
val attributeMap = Keys.fileAttributeMap.value
path: Path =>
attributeMap.get(path) match {
case null =>
val h = hash(path)
attributeMap.put(path, (Some(h), None))
h
case (Some(h), _) => h
case (None, lm) =>
val h = hash(path)
attributeMap.put(path, (Some(h), lm))
h
}
}
private[sbt] final class StampedFile(path: Path, val stamp: Stamp)
extends java.io.File(path.toString)
private[sbt] val stampedFile: ((Path, FileAttributes)) => File = {
case (p: Path, a: FileAttributes) => new StampedFile(p, apply(p, a).stamp)
}
private[sbt] val stamped: File => Stamp = file => {
val path = file.toPath
FileAttributes(path).map(apply(path, _).stamp).getOrElse(EmptyStamp)
}
private[sbt] implicit class Ops(val fileStamp: FileStamp) {
private[sbt] def stamp: Stamp = fileStamp match {
case f: FileHashImpl => f.xstamp
case LastModified(time) => new IncLastModified(time)
case _ => EmptyStamp
}
}
private[sbt] val extractor: Try[FileStamp] => FileStamp = (_: Try[FileStamp]).getOrElse(Empty)
private[sbt] val converter: (Path, FileAttributes) => Try[FileStamp] = (p, a) => Try(apply(p, a))
def apply(path: Path, fileAttributes: FileAttributes): FileStamp =
try {
if (fileAttributes.isDirectory) lastModified(path)
else
path.toString match {
case s if s.endsWith(".jar") => lastModified(path)
case s if s.endsWith(".class") => lastModified(path)
case _ => hash(path)
}
} catch {
case e: IOException => Error(e)
}
def hash(path: Path): Hash = new FileHashImpl(Stamper.forHash(path.toFile))
def lastModified(path: Path): LastModified = LastModified(IO.getModifiedTimeOrZero(path.toFile))
private[this] class FileHashImpl(val xstamp: Stamp) extends Hash(xstamp.getHash.orElse(""))
sealed abstract case class Hash private[sbt] (hex: String) extends FileStamp
case class LastModified private[sbt] (time: Long) extends FileStamp
case class Error(exception: IOException) extends FileStamp
case object Empty extends FileStamp
}

View File

@ -0,0 +1,28 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.nio
import java.nio.file.Path
import sbt.BuildSyntax.{ settingKey, taskKey }
import sbt.nio.file.Glob
object Keys {
val fileInputs = settingKey[Seq[Glob]](
"The file globs that are used by a task. This setting will generally be scoped per task. It will also be used to determine the sources to watch during continuous execution."
)
val fileOutputs = taskKey[Seq[Glob]]("Describes the output files of a task")
val fileHashes = taskKey[Seq[(Path, FileStamp.Hash)]]("Retrieves the hashes for a set of files")
val fileLastModifiedTimes = taskKey[Seq[(Path, FileStamp.LastModified)]](
"Retrieves the last modified times for a set of files"
)
private[sbt] val fileAttributeMap =
taskKey[java.util.HashMap[Path, (Option[FileStamp.Hash], Option[FileStamp.LastModified])]](
"Map of file stamps that may be cleared between task evaluation runs."
)
}

View File

@ -14,10 +14,10 @@ import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }
import org.scalatest.{ FlatSpec, Matchers } import org.scalatest.{ FlatSpec, Matchers }
import sbt.Watch.{ NullLogger, _ } import sbt.Watch.{ NullLogger, _ }
import sbt.WatchSpec._ import sbt.WatchSpec._
import sbt.internal.FileAttributes import sbt.internal.nio.{ FileEvent, FileEventMonitor, FileTreeRepository }
import sbt.io.FileEventMonitor.Event
import sbt.io._ import sbt.io._
import sbt.io.syntax._ import sbt.io.syntax._
import sbt.nio.file.{ FileAttributes, Glob }
import sbt.util.Logger import sbt.util.Logger
import scala.collection.mutable import scala.collection.mutable
@ -30,31 +30,26 @@ class WatchSpec extends FlatSpec with Matchers {
object TestDefaults { object TestDefaults {
def callbacks( def callbacks(
inputs: Seq[Glob], inputs: Seq[Glob],
fileEventMonitor: Option[FileEventMonitor[FileAttributes]] = None, fileEventMonitor: Option[FileEventMonitor[FileEvent[FileAttributes]]] = None,
logger: Logger = NullLogger, logger: Logger = NullLogger,
parseEvent: () => Watch.Action = () => Ignore, parseEvent: () => Watch.Action = () => Ignore,
onStartWatch: () => Watch.Action = () => CancelWatch: Watch.Action, onStartWatch: () => Watch.Action = () => CancelWatch: Watch.Action,
onWatchEvent: Event[FileAttributes] => Watch.Action = _ => Ignore, onWatchEvent: FileEvent[FileAttributes] => Watch.Action = _ => Ignore,
triggeredMessage: Event[FileAttributes] => Option[String] = _ => None, triggeredMessage: FileEvent[FileAttributes] => Option[String] = _ => None,
watchingMessage: () => Option[String] = () => None watchingMessage: () => Option[String] = () => None
): (NextAction, NextAction) = { ): (NextAction, NextAction) = {
val monitor = fileEventMonitor.getOrElse { val monitor: FileEventMonitor[FileEvent[FileAttributes]] = fileEventMonitor.getOrElse {
val fileTreeRepository = FileTreeRepository.default(FileAttributes.default) val fileTreeRepository = FileTreeRepository.default
inputs.foreach(fileTreeRepository.register) inputs.foreach(fileTreeRepository.register)
val m = FileEventMonitor.antiEntropy(
FileEventMonitor.antiEntropy( fileTreeRepository,
fileTreeRepository, 50.millis,
50.millis, m => logger.debug(m.toString),
m => logger.debug(m.toString), 50.millis,
50.millis, 10.minutes
10.minutes )
)
new FileEventMonitor[FileAttributes] {
override def poll(duration: Duration): Seq[Event[FileAttributes]] = m.poll(duration)
override def close(): Unit = m.close()
}
} }
val onTrigger: Event[FileAttributes] => Unit = event => { val onTrigger: FileEvent[FileAttributes] => Unit = event => {
triggeredMessage(event).foreach(logger.info(_)) triggeredMessage(event).foreach(logger.info(_))
} }
val onStart: () => Watch.Action = () => { val onStart: () => Watch.Action = () => {
@ -63,7 +58,7 @@ class WatchSpec extends FlatSpec with Matchers {
} }
val nextAction: NextAction = () => { val nextAction: NextAction = () => {
val inputAction = parseEvent() val inputAction = parseEvent()
val fileActions = monitor.poll(10.millis).map { e: Event[FileAttributes] => val fileActions = monitor.poll(10.millis).map { e: FileEvent[FileAttributes] =>
onWatchEvent(e) match { onWatchEvent(e) match {
case Trigger => onTrigger(e); Trigger case Trigger => onTrigger(e); Trigger
case action => action case action => action
@ -113,8 +108,8 @@ class WatchSpec extends FlatSpec with Matchers {
val callbacks = TestDefaults.callbacks( val callbacks = TestDefaults.callbacks(
inputs = Seq(realDir ** AllPassFilter), inputs = Seq(realDir ** AllPassFilter),
onStartWatch = () => if (task.getCount == 2) CancelWatch else Ignore, onStartWatch = () => if (task.getCount == 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 = e => { queue += e.entry.typedPath.toPath; None }, triggeredMessage = e => { queue += e.path; None },
watchingMessage = () => { watchingMessage = () => {
IO.touch(bar.toFile); Thread.sleep(5); IO.touch(foo.toFile) IO.touch(bar.toFile); Thread.sleep(5); IO.touch(foo.toFile)
None None
@ -132,8 +127,8 @@ class WatchSpec extends FlatSpec with Matchers {
val callbacks = TestDefaults.callbacks( val callbacks = TestDefaults.callbacks(
inputs = Seq(realDir ** AllPassFilter), inputs = Seq(realDir ** AllPassFilter),
onStartWatch = () => if (task.getCount == 3) CancelWatch else Ignore, onStartWatch = () => if (task.getCount == 3) CancelWatch else Ignore,
onWatchEvent = _ => Trigger, onWatchEvent = e => if (e.path != realDir.toPath) Trigger else Ignore,
triggeredMessage = e => { queue += e.entry.typedPath.toPath; None }, triggeredMessage = e => { queue += e.path; None },
watchingMessage = () => { watchingMessage = () => {
task.getCount match { task.getCount match {
case 1 => Files.createFile(bar) case 1 => Files.createFile(bar)

View File

@ -9,7 +9,7 @@ object Dependencies {
val baseScalaVersion = scala212 val baseScalaVersion = scala212
// sbt modules // sbt modules
private val ioVersion = "1.3.0-M7" private val ioVersion = "1.3.0-M9"
private val utilVersion = "1.3.0-M6" private val utilVersion = "1.3.0-M6"
private val lmVersion = private val lmVersion =
sys.props.get("sbt.build.lm.version") match { sys.props.get("sbt.build.lm.version") match {

View File

@ -42,7 +42,6 @@ trait Import {
val ExistsFileFilter = sbt.io.ExistsFileFilter val ExistsFileFilter = sbt.io.ExistsFileFilter
val FileFilter = sbt.io.FileFilter val FileFilter = sbt.io.FileFilter
type FileFilter = sbt.io.FileFilter type FileFilter = sbt.io.FileFilter
type Glob = sbt.io.Glob
val GlobFilter = sbt.io.GlobFilter val GlobFilter = sbt.io.GlobFilter
val Hash = sbt.io.Hash val Hash = sbt.io.Hash
val HiddenFileFilter = sbt.io.HiddenFileFilter val HiddenFileFilter = sbt.io.HiddenFileFilter

View File

@ -1,6 +1,10 @@
import sbt.nio.file.Glob
cleanKeepFiles ++= Seq( cleanKeepFiles ++= Seq(
target.value / "keep", target.value / "keep",
target.value / "keepfile" target.value / "keepfile"
) )
cleanKeepGlobs += target.value / "keepdir" ** AllPassFilter cleanKeepGlobs += target.value / "keepdir" ** AllPassFilter
// This is necessary because recursive globs do not include the base directory.
cleanKeepGlobs += Glob(target.value / "keepdir")

View File

@ -1,3 +1,5 @@
import sbt.nio.file.syntax._
Compile / sourceGenerators += Def.task { Compile / sourceGenerators += Def.task {
val files = Seq(sourceManaged.value / "foo.txt", sourceManaged.value / "bar.txt") val files = Seq(sourceManaged.value / "foo.txt", sourceManaged.value / "bar.txt")
files.foreach(IO.touch(_)) files.foreach(IO.touch(_))

View File

@ -1,2 +1,4 @@
import sbt.nio.file.syntax._
cleanKeepGlobs in Compile += cleanKeepGlobs in Compile +=
((classDirectory in Compile in compile).value / "X.class").toGlob ((classDirectory in Compile in compile).value / "X.class").toGlob

View File

@ -16,8 +16,8 @@ val snapshot = (project in file(".")).settings(
libraryDependencies += "sbt" %% "foo-lib" % "0.1.0-SNAPSHOT", libraryDependencies += "sbt" %% "foo-lib" % "0.1.0-SNAPSHOT",
rewriteIvy := { rewriteIvy := {
val dir = Def.spaceDelimited().parsed.head val dir = Def.spaceDelimited().parsed.head
sbt.IO.delete(file("ivy")) sbt.IO.delete(baseDirectory.value / "ivy")
sbt.IO.copyDirectory(file(s"libraries/library-$dir/ivy"), file("ivy")) sbt.IO.copyDirectory(baseDirectory.value / s"libraries/library-$dir/ivy", baseDirectory.value / "ivy")
Files.walk(file("ivy").getCanonicalFile.toPath).iterator.asScala.foreach { f => Files.walk(file("ivy").getCanonicalFile.toPath).iterator.asScala.foreach { f =>
Files.setLastModifiedTime(f, FileTime.fromMillis(System.currentTimeMillis + 3000)) Files.setLastModifiedTime(f, FileTime.fromMillis(System.currentTimeMillis + 3000))
} }

View File

@ -1,3 +1,8 @@
import java.nio.file._
import sbt.nio.Keys._
import sbt.nio.file._
// The project contains two files: { Foo.txt, Bar.md } in the subdirector base/subdir/nested-subdir // The project contains two files: { Foo.txt, Bar.md } in the subdirector base/subdir/nested-subdir
// Check that we can correctly extract Foo.txt with a recursive source // Check that we can correctly extract Foo.txt with a recursive source
@ -5,7 +10,7 @@ val foo = taskKey[Seq[File]]("Retrieve Foo.txt")
foo / fileInputs += baseDirectory.value ** "*.txt" foo / fileInputs += baseDirectory.value ** "*.txt"
foo := (foo / fileInputs).value.all.map(_._1.toFile) foo := (foo / fileInputs).value.all(fileTreeView.value).map(_._1.toFile)
val checkFoo = taskKey[Unit]("Check that the Foo.txt file is retrieved") val checkFoo = taskKey[Unit]("Check that the Foo.txt file is retrieved")
@ -16,7 +21,7 @@ val bar = taskKey[Seq[File]]("Retrieve Bar.md")
bar / fileInputs += baseDirectory.value / "base/subdir/nested-subdir" * "*.md" bar / fileInputs += baseDirectory.value / "base/subdir/nested-subdir" * "*.md"
bar := (bar / fileInputs).value.all.map(_._1.toFile) bar := (bar / fileInputs).value.all(fileTreeView.value).map(_._1.toFile)
val checkBar = taskKey[Unit]("Check that the Bar.md file is retrieved") val checkBar = taskKey[Unit]("Check that the Bar.md file is retrieved")
@ -32,7 +37,8 @@ val checkAll = taskKey[Unit]("Check that the Bar.md file is retrieved")
checkAll := { checkAll := {
import sbt.dsl.LinterLevel.Ignore 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" / _)
assert((all / fileInputs).value.all.map(_._1.toFile).toSet == expected) val actual = (all / fileInputs).value.all(fileTreeView.value).filter(_._2.isRegularFile).map(_._1.toFile).toSet
assert(actual == expected)
} }
val set = taskKey[Seq[File]]("Specify redundant sources in a set") val set = taskKey[Seq[File]]("Specify redundant sources in a set")
@ -45,10 +51,10 @@ set / fileInputs ++= Seq(
val checkSet = taskKey[Unit]("Verify that redundant sources are handled") val checkSet = taskKey[Unit]("Verify that redundant sources are handled")
checkSet := { checkSet := {
val redundant = (set / fileInputs).value.all.map(_._1.toFile) val redundant = (set / fileInputs).value.all(fileTreeView.value).map(_._1.toFile)
assert(redundant.size == 2) assert(redundant.size == 2)
val deduped = (set / fileInputs).value.toSet[Glob].all.map(_._1.toFile) val deduped = (set / fileInputs).value.toSet[Glob].all(fileTreeView.value).map(_._1.toFile)
val expected = Seq("Bar.md", "Foo.txt").map(baseDirectory.value / "base/subdir/nested-subdir" / _) val expected = Seq("Bar.md", "Foo.txt").map(baseDirectory.value / "base/subdir/nested-subdir" / _)
assert(deduped.sorted == expected) assert(deduped.sorted == expected)
} }
@ -56,16 +62,15 @@ checkSet := {
val depth = taskKey[Seq[File]]("Specify redundant sources with limited depth") val depth = taskKey[Seq[File]]("Specify redundant sources with limited depth")
val checkDepth = taskKey[Unit]("Check that the Bar.md file is retrieved") val checkDepth = taskKey[Unit]("Check that the Bar.md file is retrieved")
depth / fileInputs ++= Seq( depth / fileInputs ++= {
sbt.io.Glob(baseDirectory.value / "base", -DirectoryFilter, 2), Seq(
sbt.io.Glob(baseDirectory.value / "base" / "subdir", -DirectoryFilter, 1) Glob(baseDirectory.value / "base", AnyPath / AnyPath / "*.md"),
) Glob(baseDirectory.value / "base" / "subdir", AnyPath / "*.md"),
)
}
checkDepth := { checkDepth := {
val redundant = (depth / fileInputs).value.all.map(_._1.toFile) val expected = Seq("Bar.md").map(baseDirectory.value / "base/subdir/nested-subdir" / _)
assert(redundant.size == 2) val actual = (depth / fileInputs).value.all(fileTreeView.value).map(_._1.toFile)
assert(actual == expected)
val deduped = (depth / fileInputs).value.toSet[Glob].all.map(_._1.toFile)
val expected = Seq("Bar.md", "Foo.txt").map(baseDirectory.value / "base/subdir/nested-subdir" / _)
assert(deduped.sorted == expected)
} }

View File

@ -1,6 +1,8 @@
import java.nio.file.Path import java.nio.file.Path
import sbt.internal.{FileAttributes, FileTree} import sbt.Keys._
import sbt.nio.file._
import sbt.nio.Keys._
val allInputs = taskKey[Seq[File]]("") val allInputs = taskKey[Seq[File]]("")
val allInputsExplicit = taskKey[Seq[File]]("") val allInputsExplicit = taskKey[Seq[File]]("")
@ -8,7 +10,7 @@ val allInputsExplicit = taskKey[Seq[File]]("")
val checkInputs = inputKey[Unit]("") val checkInputs = inputKey[Unit]("")
val checkInputsExplicit = inputKey[Unit]("") val checkInputsExplicit = inputKey[Unit]("")
allInputs := (Compile / unmanagedSources / fileInputs).value.all.map(_._1.toFile) allInputs := (Compile / unmanagedSources / fileInputs).value.all(fileTreeView.value).map(_._1.toFile)
checkInputs := { checkInputs := {
val res = allInputs.value val res = allInputs.value
@ -20,17 +22,16 @@ checkInputs := {
// In this test we override the FileTree.Repository used by the all method. // In this test we override the FileTree.Repository used by the all method.
allInputsExplicit := { allInputsExplicit := {
val files = scala.collection.mutable.Set.empty[File] val files = scala.collection.mutable.Set.empty[File]
val underlying = implicitly[FileTree.Repository] val underlying = fileTreeView.value
val repo = new FileTree.Repository { val view = new FileTreeView[(Path, FileAttributes)] {
override def get(glob: Glob): Seq[(Path, FileAttributes)] = { override def list(path: Path): Seq[(Path, FileAttributes)] = {
val res = underlying.get(glob) val res = underlying.list(path)
files ++= res.map(_._1.toFile) files ++= res.map(_._1.toFile)
res res
} }
override def close(): Unit = {}
} }
val include = (Compile / unmanagedSources / includeFilter).value val include = (Compile / unmanagedSources / includeFilter).value
val _ = (Compile / unmanagedSources / fileInputs).value.all(repo).map(_._1.toFile).toSet val _ = (Compile / unmanagedSources / fileInputs).value.all(view).map(_._1.toFile).toSet
files.filter(include.accept).toSeq files.filter(include.accept).toSeq
} }

View File

@ -1,4 +1,7 @@
import sbt.internal.TransitiveGlobs._ import sbt.internal.TransitiveGlobs._
import sbt.nio.Keys._
val cached = settingKey[Unit]("") val cached = settingKey[Unit]("")
val newInputs = settingKey[Unit]("") val newInputs = settingKey[Unit]("")
Compile / cached / fileInputs := (Compile / unmanagedSources / fileInputs).value ++ Compile / cached / fileInputs := (Compile / unmanagedSources / fileInputs).value ++

View File

@ -2,6 +2,7 @@ package sbt.input.aggregation
import sbt._ import sbt._
import Keys._ import Keys._
import sbt.nio.Keys._
object Build { object Build {
val setStringValue = inputKey[Unit]("set a global string to a value") val setStringValue = inputKey[Unit]("set a global string to a value")

View File

@ -2,7 +2,7 @@ package sbt.watch.task
import sbt._ import sbt._
import Keys._ import Keys._
import sbt.internal.FileTree import sbt.nio.Keys._
object Build { object Build {
val reloadFile = settingKey[File]("file to toggle whether or not to reload") val reloadFile = settingKey[File]("file to toggle whether or not to reload")
@ -25,15 +25,20 @@ object Build {
setStringValue := Def.taskDyn { setStringValue := Def.taskDyn {
// This hides foo / fileInputs from the input graph // This hides foo / fileInputs from the input graph
Def.taskDyn { Def.taskDyn {
val _ = (foo / fileInputs).value.all val _ = (foo / fileInputs).value
.all(fileTreeView.value, sbt.internal.Continuous.dynamicInputs.value)
// By putting setStringValueImpl.value inside a Def.task, we ensure that // By putting setStringValueImpl.value inside a Def.task, we ensure that
// (foo / fileInputs).value is registered with the file repository before modifying the file. // (foo / fileInputs).value is registered with the file repository before modifying the file.
Def.task(setStringValueImpl.value) Def.task(setStringValueImpl.value)
} }
}.value, }.value,
checkStringValue := checkStringValueImpl.evaluated, checkStringValue := checkStringValueImpl.evaluated,
watchOnInputEvent := { (_, _) => Watch.CancelWatch }, watchOnInputEvent := { (_, _) =>
watchOnTriggerEvent := { (_, _) => Watch.CancelWatch }, Watch.CancelWatch
},
watchOnTriggerEvent := { (_, _) =>
Watch.CancelWatch
},
watchTasks := Def.inputTask { watchTasks := Def.inputTask {
val prev = watchTasks.evaluated val prev = watchTasks.evaluated
new StateTransform(prev.state.fail) new StateTransform(prev.state.fail)

View File

@ -1,8 +1,10 @@
package sbt.input.aggregation package sbt.input.aggregation
import sbt.Keys._
import sbt._ import sbt._
import Keys._
import sbt.internal.TransitiveGlobs._ import sbt.internal.TransitiveGlobs._
import sbt.nio.Keys._
import sbt.nio.file._
object Build { object Build {
val setStringValue = inputKey[Unit]("set a global string to a value") val setStringValue = inputKey[Unit]("set a global string to a value")