Clean up nio apis

This commit refactors things so that the nio apis are located primarily
in the nio package. Because the nio keys are a first class sbt feature,
I had to add import sbt.nio._ and sbt.nio.Keys._ to the autoimports in
BuildUtil.scala
This commit is contained in:
Ethan Atkins 2019-04-17 18:43:08 -07:00
parent 4f6e38c805
commit a5cefd45be
28 changed files with 246 additions and 175 deletions

View File

@ -64,6 +64,7 @@ import sbt.librarymanagement.CrossVersion.{ binarySbtVersion, binaryScalaVersion
import sbt.librarymanagement._
import sbt.librarymanagement.ivy._
import sbt.librarymanagement.syntax._
import sbt.nio.Watch
import sbt.nio.Keys._
import sbt.nio.file.FileTreeView
import sbt.nio.file.syntax._
@ -421,7 +422,7 @@ object Defaults extends BuildCommon {
unmanagedSources := (unmanagedSources / fileStamps).value.map(_._1.toFile),
managedSourceDirectories := Seq(sourceManaged.value),
managedSources := {
val stamper = sbt.nio.Keys.stamper.value
val stamper = sbt.nio.Keys.pathToFileStamp.value
val res = generate(sourceGenerators).value
res.foreach(f => stamper(f.toPath))
res
@ -654,7 +655,7 @@ object Defaults extends BuildCommon {
watchTransitiveSources := watchTransitiveSourcesTask.value,
watch := watchSetting.value,
fileOutputs += target.value ** AllPassFilter,
TransitiveDynamicInputs.transitiveDynamicInputs := InputGraph.task.value,
transitiveDynamicInputs := InputGraph.task.value,
)
def generate(generators: SettingKey[Seq[Task[Seq[File]]]]): Initialize[Task[Seq[File]]] =
@ -2097,7 +2098,7 @@ object Classpaths {
shellPrompt := shellPromptFromState,
dynamicDependency := { (): Unit },
transitiveClasspathDependency := { (): Unit },
TransitiveDynamicInputs.transitiveDynamicInputs :== Nil,
transitiveDynamicInputs :== Nil,
)
)

View File

@ -16,9 +16,8 @@ import sbt.Project.richInitializeTask
import sbt.Scope.Global
import sbt.internal.Aggregation.KeyValue
import sbt.internal.TaskName._
import sbt.internal.TransitiveDynamicInputs._
import sbt.internal.util._
import sbt.internal.{ BuildStructure, GCUtil, Load, TaskProgress, TaskTimings, TaskTraceEvent, _ }
import sbt.internal._
import sbt.librarymanagement.{ Resolver, UpdateReport }
import sbt.std.Transform.DummyTaskMap
import sbt.util.{ Logger, Show }
@ -572,31 +571,6 @@ object EvaluateTask {
stream
}).value
})
} else if (scoped.key == transitiveDynamicInputs.key) {
scoped.scope.task.toOption.toSeq.map { key =>
val updatedKey = ScopedKey(scoped.scope.copy(task = Zero), key)
transitiveDynamicInputs in scoped.scope := InputGraph.task(updatedKey).value
}
} else if (scoped.key == dynamicDependency.key) {
(dynamicDependency in scoped.scope := {
()
}) :: Nil
} else if (scoped.key == transitiveClasspathDependency.key) {
(transitiveClasspathDependency in scoped.scope := {
()
}) :: Nil
} else if (scoped.key == sbt.nio.Keys.allFiles.key) {
sbt.nio.Settings.allFiles(scoped) :: Nil
} else if (scoped.key == sbt.nio.Keys.allPaths.key) {
sbt.nio.Settings.allPaths(scoped) :: Nil
} else if (scoped.key == sbt.nio.Keys.changedFiles.key) {
sbt.nio.Settings.changedFiles(scoped)
} else if (scoped.key == sbt.nio.Keys.modifiedFiles.key) {
sbt.nio.Settings.modifiedFiles(scoped)
} else if (scoped.key == sbt.nio.Keys.removedFiles.key) {
sbt.nio.Settings.removedFiles(scoped) :: Nil
} else if (scoped.key == sbt.nio.Keys.stamper.key) {
sbt.nio.Settings.stamper(scoped) :: Nil
} else {
Nil
}

View File

@ -7,9 +7,8 @@
package sbt
import java.io.{ File, InputStream }
import java.io.File
import java.net.URL
import java.nio.file.{ Path => NioPath }
import org.apache.ivy.core.module.descriptor.ModuleDescriptor
import org.apache.ivy.core.module.id.ModuleRevisionId
@ -21,16 +20,14 @@ import sbt.internal._
import sbt.internal.inc.ScalaInstance
import sbt.internal.io.WatchState
import sbt.internal.librarymanagement.{ CompatibilityWarningOptions, IvySbt }
import sbt.internal.nio.FileTreeRepository
import sbt.internal.server.ServerHandler
import sbt.internal.util.complete.Parser
import sbt.internal.util.{ AttributeKey, SourcePosition }
import sbt.io._
import sbt.librarymanagement.Configurations.CompilerPlugin
import sbt.librarymanagement.LibraryManagementCodec._
import sbt.librarymanagement._
import sbt.librarymanagement.ivy.{ Credentials, IvyConfiguration, IvyPaths, UpdateOptions }
import sbt.nio.file.{ FileAttributes, Glob }
import sbt.nio.file.Glob
import sbt.testing.Framework
import sbt.util.{ Level, Logger }
import xsbti.compile._
@ -98,24 +95,7 @@ object Keys {
val suppressSbtShellNotification = settingKey[Boolean]("""True to suppress the "Executing in batch mode.." message.""").withRank(CSetting)
val pollInterval = settingKey[FiniteDuration]("Interval between checks for modified sources by the continuous execution command.").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)
val watchAntiEntropyRetentionPeriod = settingKey[FiniteDuration]("Wall clock Duration for which a FileEventMonitor will store anti-entropy events. This prevents spurious triggers when a task takes a long time to run. Higher values will consume more memory but make spurious triggers less likely.").withRank(BMinusSetting)
val watchDeletionQuarantinePeriod = settingKey[FiniteDuration]("Period for which deletion events will be quarantined. This is to prevent spurious builds when a file is updated with a rename which manifests as a file deletion followed by a file creation. The higher this value is set, the longer the delay will be between a file deletion and a build trigger but the less likely it is for a spurious trigger.").withRank(DSetting)
val watchLogLevel = settingKey[sbt.util.Level.Value]("Transform the default logger in continuous builds.").withRank(DSetting)
val watchInputHandler = settingKey[InputStream => Watch.Action]("Function that is periodically invoked to determine if the continuous build should be stopped or if a build should be triggered. It will usually read from stdin to respond to user commands. This is only invoked if watchInputStream is set.").withRank(DSetting)
val watchForceTriggerOnAnyChange = settingKey[Boolean]("Force the watch process to rerun the current task(s) if any relevant source change is detected regardless of whether or not the underlying file has actually changed.").withRank(DSetting)
val watchInputStream = taskKey[InputStream]("The input stream to read for user input events. This will usually be System.in").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 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 watchOnFileInputEvent = 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 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 watchOnIteration = settingKey[Int => Watch.Action]("Function that is invoked before waiting for file system events or user input events.").withRank(DSetting)
val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting).withRank(DSetting)
val watchStartMessage = settingKey[(Int, String, Seq[String]) => Option[String]]("The message to show when triggered execution waits for sources to change. The parameters are the current watch iteration count, the current project name and the tasks that are being run with each build.").withRank(DSetting)
// 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 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, NioPath, 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("This is no longer used for continuous execution", "1.3.0")
@ -498,14 +478,6 @@ object Keys {
@deprecated("No longer used", "1.3.0")
private[sbt] val executeProgress = settingKey[State => TaskProgress]("Experimental task execution listener.").withRank(DTask)
private[sbt] val globalFileTreeRepository = AttributeKey[FileTreeRepository[FileAttributes]](
"global-file-tree-repository",
"Provides a view into the file system that may or may not cache the tree in memory",
1000
)
private[sbt] val dynamicDependency = settingKey[Unit]("Leaves a breadcrumb that the scoped task is evaluated inside of a dynamic task")
private[sbt] val transitiveClasspathDependency = settingKey[Unit]("Leaves a breadcrumb that the scoped task has transitive classpath dependencies")
val stateStreams = AttributeKey[Streams]("stateStreams", "Streams manager, which provides streams for different contexts. Setting this on State will override the default Streams implementation.")
val resolvedScoped = Def.resolvedScoped
val pluginData = taskKey[PluginData]("Information from the plugin build needed in the main build definition.").withRank(DTask)

View File

@ -86,7 +86,7 @@ object BuildUtil {
}
def baseImports: Seq[String] =
"import _root_.scala.xml.{TopScope=>$scope}" :: "import _root_.sbt._" :: "import _root_.sbt.Keys._" :: Nil
"import _root_.scala.xml.{TopScope=>$scope}" :: "import _root_.sbt._" :: "import _root_.sbt.Keys._" :: "import _root_.sbt.nio.Keys._" :: Nil
def getImports(unit: BuildUnit): Seq[String] =
unit.plugins.detected.imports ++ unit.definitions.dslDefinitions.imports

View File

@ -14,7 +14,7 @@ import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic._
import sbt.BasicKeys._
import sbt.Watch.NullLogger
import sbt.nio.Watch.NullLogger
import sbt.internal.langserver.{ LogMessageParams, MessageType }
import sbt.internal.server._
import sbt.internal.util.codec.JValueFormats

View File

@ -19,17 +19,18 @@ import sbt.BasicCommandStrings.{
}
import sbt.BasicCommands.otherCommandParser
import sbt.Def._
import sbt.Keys._
import sbt.Scope.Global
import sbt.Watch.{ Creation, Deletion, Update }
import sbt.internal.LabeledFunctions._
import sbt.internal.io.WatchState
import sbt.internal.nio._
import sbt.internal.util.complete.Parser._
import sbt.internal.util.complete.{ Parser, Parsers }
import sbt.internal.util.{ AttributeKey, JLine, Util }
import sbt.nio.Keys.fileInputs
import sbt.nio.Keys.{ fileInputs, _ }
import sbt.nio.Watch.{ Creation, Deletion, Update }
import sbt.nio.file.FileAttributes
import sbt.nio.{ FileStamp, FileStamper }
import sbt.nio.{ FileStamp, FileStamper, Watch }
import sbt.util.{ Level, _ }
import scala.annotation.tailrec
@ -103,8 +104,8 @@ private[sbt] object Continuous extends DeprecatedContinuous {
*/
private[sbt] def continuous: Command =
Command(ContinuousExecutePrefix, continuousBriefHelp, continuousDetail)(continuousParser) {
case (state, (initialCount, command)) =>
runToTermination(state, command, initialCount, isCommand = true)
case (s, (initialCount, command)) =>
runToTermination(s, command, initialCount, isCommand = true)
}
/**
@ -117,7 +118,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
Def.inputTask {
val (initialCount, command) = continuousParser.parsed
new StateTransform(
runToTermination(Keys.state.value, command, initialCount, isCommand = false)
runToTermination(state.value, command, initialCount, isCommand = false)
)
}
@ -165,7 +166,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
// Extract all of the globs that we will monitor during the continuous build.
val inputs = {
val configs = scopedKey.get(Keys.internalDependencyConfigurations).getOrElse(Nil)
val configs = scopedKey.get(internalDependencyConfigurations).getOrElse(Nil)
val args = new InputGraph.Arguments(scopedKey, extracted, compiledMap, logger, configs, state)
InputGraph.transitiveDynamicInputs(args)
}
@ -191,7 +192,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
lazy val exception =
new IllegalStateException("Tried to access FileTreeRepository for uninitialized state")
state
.get(Keys.globalFileTreeRepository)
.get(globalFileTreeRepository)
.getOrElse(throw exception)
}
@ -279,18 +280,18 @@ private[sbt] object Continuous extends DeprecatedContinuous {
): State = withCharBufferedStdIn { in =>
implicit val extracted: Extracted = Project.extract(state)
val repo = if ("polling" == System.getProperty("sbt.watch.mode")) {
val service =
new PollingWatchService(extracted.getOpt(Keys.pollInterval).getOrElse(500.millis))
FileTreeRepository.legacy((_: Any) => {}, service)
val service = new PollingWatchService(extracted.getOpt(pollInterval).getOrElse(500.millis))
FileTreeRepository
.legacy((_: Any) => {}, service)
} else {
FileTreeRepository.default
}
try {
val stateWithRepo = state
.put(Keys.globalFileTreeRepository, repo)
.put(sbt.nio.Keys.persistentFileAttributeMap, new sbt.nio.Keys.FileAttributeMap)
.put(globalFileTreeRepository, repo)
.put(persistentFileAttributeMap, new sbt.nio.Keys.FileAttributeMap)
setup(stateWithRepo, command) { (commands, s, valid, invalid) =>
EvaluateTask.withStreams(extracted.structure, s)(_.use(Keys.streams in Global) { streams =>
EvaluateTask.withStreams(extracted.structure, s)(_.use(streams in Global) { streams =>
implicit val logger: Logger = streams.log
if (invalid.isEmpty) {
val currentCount = new AtomicInteger(count)
@ -459,10 +460,10 @@ private[sbt] object Continuous extends DeprecatedContinuous {
count: AtomicInteger,
commands: Seq[String]
)(implicit extracted: Extracted): (() => Option[(Watch.Event, Watch.Action)], () => Unit) = {
val attributeMap = state.get(sbt.nio.Keys.persistentFileAttributeMap).get
val attributeMap = state.get(persistentFileAttributeMap).get
val trackMetaBuild = configs.forall(_.watchSettings.trackMetaBuild)
val buildGlobs =
if (trackMetaBuild) extracted.getOpt(fileInputs in Keys.settingsData).getOrElse(Nil)
if (trackMetaBuild) extracted.getOpt(fileInputs in settingsData).getOrElse(Nil)
else Nil
val retentionPeriod = configs.map(_.watchSettings.antiEntropyRetentionPeriod).max
@ -765,26 +766,26 @@ private[sbt] object Continuous extends DeprecatedContinuous {
implicit extracted: Extracted
) {
val antiEntropy: FiniteDuration =
key.get(Keys.watchAntiEntropy).getOrElse(Watch.defaultAntiEntropy)
key.get(watchAntiEntropy).getOrElse(Watch.defaultAntiEntropy)
val antiEntropyRetentionPeriod: FiniteDuration =
key
.get(Keys.watchAntiEntropyRetentionPeriod)
.get(watchAntiEntropyRetentionPeriod)
.getOrElse(Watch.defaultAntiEntropyRetentionPeriod)
val deletionQuarantinePeriod: FiniteDuration =
key.get(Keys.watchDeletionQuarantinePeriod).getOrElse(Watch.defaultDeletionQuarantinePeriod)
val inputHandler: Option[InputStream => Watch.Action] = key.get(Keys.watchInputHandler)
key.get(watchDeletionQuarantinePeriod).getOrElse(Watch.defaultDeletionQuarantinePeriod)
val inputHandler: Option[InputStream => Watch.Action] = key.get(watchInputHandler)
val inputParser: Parser[Watch.Action] =
key.get(Keys.watchInputParser).getOrElse(Watch.defaultInputParser)
val logLevel: Level.Value = key.get(Keys.watchLogLevel).getOrElse(Level.Info)
val onEnter: () => Unit = key.get(Keys.watchOnEnter).getOrElse(() => {})
val onExit: () => Unit = key.get(Keys.watchOnExit).getOrElse(() => {})
key.get(watchInputParser).getOrElse(Watch.defaultInputParser)
val logLevel: Level.Value = key.get(watchLogLevel).getOrElse(Level.Info)
val onEnter: () => Unit = key.get(watchOnEnter).getOrElse(() => {})
val onExit: () => Unit = key.get(watchOnExit).getOrElse(() => {})
val onFileInputEvent: WatchOnEvent =
key.get(Keys.watchOnFileInputEvent).getOrElse(Watch.trigger)
val onIteration: Option[Int => Watch.Action] = key.get(Keys.watchOnIteration)
key.get(watchOnFileInputEvent).getOrElse(Watch.trigger)
val onIteration: Option[Int => Watch.Action] = key.get(watchOnIteration)
val onTermination: Option[(Watch.Action, String, Int, State) => State] =
key.get(Keys.watchOnTermination)
key.get(watchOnTermination)
val startMessage: StartMessage = getStartMessage(key)
val trackMetaBuild: Boolean = key.get(Keys.watchTrackMetaBuild).getOrElse(true)
val trackMetaBuild: Boolean = key.get(watchTrackMetaBuild).getOrElse(true)
val triggerMessage: TriggerMessage = getTriggerMessage(key)
// Unlike the rest of the settings, InputStream is a TaskKey which means that if it is set,
@ -792,7 +793,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
// logical that users may want to use a different InputStream on each task invocation. The
// alternative would be SettingKey[() => InputStream], but that doesn't feel right because
// one might want the InputStream to depend on other tasks.
val inputStream: Option[TaskKey[InputStream]] = key.get(Keys.watchInputStream)
val inputStream: Option[TaskKey[InputStream]] = key.get(watchInputStream)
}
/**
@ -813,14 +814,14 @@ private[sbt] object Continuous extends DeprecatedContinuous {
def arguments(logger: Logger): Arguments = new Arguments(logger, inputs())
}
private def getStartMessage(key: ScopedKey[_])(implicit e: Extracted): StartMessage = Some {
lazy val default = key.get(Keys.watchStartMessage).getOrElse(Watch.defaultStartWatch)
lazy val default = key.get(watchStartMessage).getOrElse(Watch.defaultStartWatch)
key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default))
}
private def getTriggerMessage(
key: ScopedKey[_]
)(implicit e: Extracted): TriggerMessage = {
lazy val default =
key.get(Keys.watchTriggeredMessage).getOrElse(Watch.defaultOnTriggerMessage)
key.get(watchTriggeredMessage).getOrElse(Watch.defaultOnTriggerMessage)
key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default))
}

View File

@ -21,10 +21,6 @@ import sbt.nio.Keys._
import scala.annotation.tailrec
private[sbt] object TransitiveDynamicInputs {
val transitiveDynamicInputs =
Def.taskKey[Seq[DynamicInput]]("The transitive inputs and triggers for a key")
}
private[sbt] object InputGraph {
private implicit class SourceOps(val source: Source) {
def toGlob: Glob = {

View File

@ -107,7 +107,7 @@ private[sbt] object Load {
compilers,
evalPluginDef,
delegates,
EvaluateTask.injectStreams,
s => EvaluateTask.injectStreams(s) ++ Settings.inject(s),
pluginMgmt,
inject,
None,

View File

@ -7,11 +7,19 @@
package sbt.nio
import java.io.InputStream
import java.nio.file.Path
import sbt.BuildSyntax.{ settingKey, taskKey }
import sbt.KeyRanks.{ BMinusSetting, DSetting }
import sbt.internal.DynamicInput
import sbt.internal.nio.FileTreeRepository
import sbt.internal.util.AttributeKey
import sbt.internal.util.complete.Parser
import sbt.nio.file.{ FileAttributes, FileTreeView, Glob }
import sbt.{ Def, InputKey, State, StateTransform }
import scala.concurrent.duration.FiniteDuration
object Keys {
val allPaths = taskKey[Seq[Path]](
@ -49,7 +57,75 @@ object Keys {
private[sbt] val fileAttributeMap = taskKey[FileAttributeMap](
"Map of file stamps that may be cleared between task evaluation runs."
)
private[sbt] val stamper = taskKey[Path => FileStamp](
private[sbt] val pathToFileStamp = taskKey[Path => FileStamp](
"A function that computes a file stamp for a path. It may have the side effect of updating a cache."
)
val watchAntiEntropyRetentionPeriod = settingKey[FiniteDuration](
"Wall clock Duration for which a FileEventMonitor will store anti-entropy events. This prevents spurious triggers when a task takes a long time to run. Higher values will consume more memory but make spurious triggers less likely."
).withRank(BMinusSetting)
val watchDeletionQuarantinePeriod = settingKey[FiniteDuration](
"Period for which deletion events will be quarantined. This is to prevent spurious builds when a file is updated with a rename which manifests as a file deletion followed by a file creation. The higher this value is set, the longer the delay will be between a file deletion and a build trigger but the less likely it is for a spurious trigger."
).withRank(DSetting)
private[this] val forceTriggerOnAnyChangeMessage =
"Force the watch process to rerun the current task(s) if any relevant source change is " +
"detected regardless of whether or not the underlying file has actually changed."
val watchForceTriggerOnAnyChange =
Def.settingKey[Boolean](forceTriggerOnAnyChangeMessage).withRank(DSetting)
val watchLogLevel =
settingKey[sbt.util.Level.Value]("Transform the default logger in continuous builds.")
.withRank(DSetting)
val watchInputHandler = settingKey[InputStream => Watch.Action](
"Function that is periodically invoked to determine if the continuous build should be stopped or if a build should be triggered. It will usually read from stdin to respond to user commands. This is only invoked if watchInputStream is set."
).withRank(DSetting)
val watchInputStream = taskKey[InputStream](
"The input stream to read for user input events. This will usually be System.in"
).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 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 watchOnFileInputEvent = settingKey[(Int, Watch.Event) => Watch.Action](
"Callback to invoke if an event is triggered in a continuous build by one of the files matching an fileInput glob for the task and its transitive dependencies"
).withRank(DSetting)
val watchOnIteration = settingKey[Int => Watch.Action](
"Function that is invoked before waiting for file system events or user input events."
).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 watchStartMessage = settingKey[(Int, String, Seq[String]) => Option[String]](
"The message to show when triggered execution waits for sources to change. The parameters are the current watch iteration count, the current project name and the tasks that are being run with each build."
).withRank(DSetting)
// 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 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, Path, 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)
private[sbt] val globalFileTreeRepository = AttributeKey[FileTreeRepository[FileAttributes]](
"global-file-tree-repository",
"Provides a view into the file system that may or may not cache the tree in memory",
1000
)
private[sbt] val dynamicDependency = settingKey[Unit](
"Leaves a breadcrumb that the scoped task is evaluated inside of a dynamic task"
)
private[sbt] val transitiveClasspathDependency = settingKey[Unit](
"Leaves a breadcrumb that the scoped task has transitive classpath dependencies"
)
private[sbt] val transitiveDynamicInputs =
taskKey[Seq[DynamicInput]]("The transitive inputs and triggers for a key")
}

View File

@ -10,13 +10,38 @@ package nio
import java.nio.file.{ Files, Path }
import sbt.Keys._
import sbt.internal.{ Continuous, DynamicInput }
import sbt.internal.{ Continuous, DynamicInput, InputGraph }
import sbt.nio.FileStamp.{ fileStampJsonFormatter, pathJsonFormatter }
import sbt.nio.FileStamper.{ Hash, LastModified }
import sbt.nio.Keys._
private[sbt] object Settings {
private[sbt] val inject: Def.ScopedKey[_] => Seq[Def.Setting[_]] = scopedKey => {
if (scopedKey.key == transitiveDynamicInputs.key) {
scopedKey.scope.task.toOption.toSeq.map { key =>
val updatedKey = Def.ScopedKey(scopedKey.scope.copy(task = Zero), key)
transitiveDynamicInputs in scopedKey.scope := InputGraph.task(updatedKey).value
}
} else if (scopedKey.key == dynamicDependency.key) {
(dynamicDependency in scopedKey.scope := { () }) :: Nil
} else if (scopedKey.key == transitiveClasspathDependency.key) {
(transitiveClasspathDependency in scopedKey.scope := { () }) :: Nil
} else if (scopedKey.key == allFiles.key) {
allFilesImpl(scopedKey) :: Nil
} else if (scopedKey.key == allPaths.key) {
allPathsImpl(scopedKey) :: Nil
} else if (scopedKey.key == changedFiles.key) {
changedFilesImpl(scopedKey)
} else if (scopedKey.key == modifiedFiles.key) {
modifiedFilesImpl(scopedKey)
} else if (scopedKey.key == removedFiles.key) {
removedFilesImpl(scopedKey) :: Nil
} else if (scopedKey.key == pathToFileStamp.key) {
stamper(scopedKey) :: Nil
} else {
Nil
}
}
/**
* This adds the [[sbt.Keys.taskDefinitionKey]] to the work for each [[Task]]. Without
@ -58,7 +83,7 @@ private[sbt] object Settings {
* @param scopedKey the key whose file inputs we are seeking
* @return a task definition that retrieves the input files and their attributes scoped to a particular task.
*/
private[sbt] def allPaths(scopedKey: Def.ScopedKey[_]): Def.Setting[_] =
private[this] def allPathsImpl(scopedKey: Def.ScopedKey[_]): Def.Setting[_] =
addTaskDefinition(Keys.allPaths in scopedKey.scope := {
(Keys.allPathsAndAttributes in scopedKey.scope).value.map(_._1)
})
@ -70,7 +95,7 @@ private[sbt] object Settings {
* @param scopedKey the key whose file inputs we are seeking
* @return a task definition that retrieves all of the input paths scoped to the input key.
*/
private[sbt] def allFiles(scopedKey: Def.ScopedKey[_]): Def.Setting[_] =
private[this] def allFilesImpl(scopedKey: Def.ScopedKey[_]): Def.Setting[_] =
addTaskDefinition(Keys.allFiles in scopedKey.scope := {
(Keys.allPathsAndAttributes in scopedKey.scope).value.collect {
case (p, a) if a.isRegularFile && !Files.isHidden(p) => p
@ -86,7 +111,7 @@ private[sbt] object Settings {
* @param scopedKey the key whose fileInputs we are seeking
* @return a task definition that retrieves the changed input files scoped to the key.
*/
private[sbt] def changedFiles(scopedKey: Def.ScopedKey[_]): Seq[Def.Setting[_]] =
private[this] def changedFilesImpl(scopedKey: Def.ScopedKey[_]): Seq[Def.Setting[_]] =
addTaskDefinition(Keys.changedFiles in scopedKey.scope := {
val current = (Keys.fileStamps in scopedKey.scope).value
(Keys.fileStamps in scopedKey.scope).previous match {
@ -110,7 +135,7 @@ private[sbt] object Settings {
*/
private[sbt] def fileStamps(scopedKey: Def.ScopedKey[_]): Def.Setting[_] =
addTaskDefinition(Keys.fileStamps in scopedKey.scope := {
val stamper = (Keys.stamper in scopedKey.scope).value
val stamper = (Keys.pathToFileStamp in scopedKey.scope).value
(Keys.allPathsAndAttributes in scopedKey.scope).value.collect {
case (p, a) if a.isRegularFile && !Files.isHidden(p) => p -> stamper(p)
}
@ -125,7 +150,7 @@ private[sbt] object Settings {
* @param scopedKey the key whose modified files we are seeking
* @return a task definition that retrieves the changed input files scoped to the key.
*/
private[sbt] def modifiedFiles(scopedKey: Def.ScopedKey[_]): Seq[Def.Setting[_]] =
private[this] def modifiedFilesImpl(scopedKey: Def.ScopedKey[_]): Seq[Def.Setting[_]] =
(Keys.modifiedFiles in scopedKey.scope := {
val current = (Keys.fileStamps in scopedKey.scope).value
(Keys.fileStamps in scopedKey.scope).previous match {
@ -151,7 +176,7 @@ private[sbt] object Settings {
* @param scopedKey the key whose removed files we are seeking
* @return a task definition that retrieves the changed input files scoped to the key.
*/
private[sbt] def removedFiles(scopedKey: Def.ScopedKey[_]): Def.Setting[_] =
private[this] def removedFilesImpl(scopedKey: Def.ScopedKey[_]): Def.Setting[_] =
addTaskDefinition(Keys.removedFiles in scopedKey.scope := {
val current = (Keys.allFiles in scopedKey.scope).value
(Keys.allFiles in scopedKey.scope).previous match {
@ -167,8 +192,8 @@ private[sbt] object Settings {
*
* @return a task definition for a function from `Path` to [[FileStamp]].
*/
private[sbt] def stamper(scopedKey: Def.ScopedKey[_]): Def.Setting[_] =
addTaskDefinition((Keys.stamper in scopedKey.scope) := {
private[this] def stamper(scopedKey: Def.ScopedKey[_]): Def.Setting[_] =
addTaskDefinition((Keys.pathToFileStamp in scopedKey.scope) := {
val attributeMap = Keys.fileAttributeMap.value
val stamper = (Keys.fileStamper in scopedKey.scope).value
path: Path =>

View File

@ -5,7 +5,7 @@
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package sbt.nio
import java.nio.file.Path
import java.time.format.{ DateTimeFormatter, TextStyle }
@ -14,6 +14,7 @@ import java.util.Locale
import java.util.concurrent.TimeUnit
import sbt.BasicCommandStrings.ContinuousExecutePrefix
import sbt._
import sbt.internal.LabeledFunctions._
import sbt.internal.nio.FileEvent
import sbt.internal.util.Util

View File

@ -12,11 +12,12 @@ import java.nio.file.{ Files, Path }
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }
import org.scalatest.{ FlatSpec, Matchers }
import sbt.Watch.{ NullLogger, _ }
import sbt.WatchSpec._
import sbt.internal.nio.{ FileEvent, FileEventMonitor, FileTreeRepository }
import sbt.io._
import sbt.io.syntax._
import sbt.nio.Watch
import sbt.nio.Watch.{ NullLogger, _ }
import sbt.nio.file.{ FileAttributes, Glob }
import sbt.util.Logger

View File

@ -60,6 +60,16 @@ trait Import {
type WatchSource = sbt.internal.io.Source
val WatchSource = sbt.internal.io.Source
// sbt.nio
val ** = sbt.nio.file.**
val * = sbt.nio.file.*
val AnyPath = sbt.nio.file.AnyPath
type Glob = sbt.nio.file.Glob
val Glob = sbt.nio.file.Glob
type RelativeGlob = sbt.nio.file.RelativeGlob
val RelativeGlob = sbt.nio.file.RelativeGlob
val RecursiveGlob = sbt.nio.file.RecursiveGlob
// sbt.util
type AbstractLogger = sbt.util.AbstractLogger
type BasicCache[I, O] = sbt.util.BasicCache[I, O]

View File

@ -1,5 +1,4 @@
import sbt.nio.Keys._
import sbt.nio.file._
val fileInputTask = taskKey[Unit]("task with file inputs")

View File

@ -1,9 +1,3 @@
import java.nio.file._
import sbt.nio.Keys._
import sbt.nio.file._
import sbt.Keys._
// 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

View File

@ -9,7 +9,6 @@ import sbt.nio.Keys._
* private apis.
*/
object Build {
import sbt.internal.TransitiveDynamicInputs._
val cached = settingKey[Unit]("")
val newInputs = settingKey[Unit]("")

View File

@ -10,4 +10,4 @@ checkStringValue := checkStringValueImpl.evaluated
setStringValue / watchTriggers := baseDirectory.value * "string.txt" :: Nil
watchOnFileInputEvent := { (_, _) => Watch.CancelWatch }
watchOnFileInputEvent := { (_, _) => sbt.nio.Watch.CancelWatch }

View File

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

View File

@ -4,6 +4,7 @@ import java.nio.file.Path
import sbt._
import Keys._
import sbt.nio.Keys._
import sbt.nio.Watch
object Build {
val reloadFile = settingKey[File]("file to toggle whether or not to reload")

View File

@ -1,22 +1,23 @@
package sbt
package input.aggregation
import java.nio.file.Paths
import sbt.Keys._
import sbt.internal.DynamicInput
import sbt.internal.TransitiveDynamicInputs._
import sbt.nio.{ file => _, _ }
import sbt.nio.Keys._
import sbt.nio.file.Glob
import java.nio.file.Paths
/**
* This test is for internal logic so it must be in the sbt package because it uses package
* private apis.
*/
* This test is for internal logic so it must be in the sbt package because it uses package
* private apis.
*/
object Build {
val setStringValue = inputKey[Unit]("set a global string to a value")
val checkStringValue = inputKey[Unit]("check the value of a global")
val checkTriggers = taskKey[Unit]("Check that the triggers are correctly aggregated.")
val checkGlobs = taskKey[Unit]("Check that the globs are correctly aggregated and that the globs are the union of the inputs and the triggers")
val checkGlobs = taskKey[Unit](
"Check that the globs are correctly aggregated and that the globs are the union of the inputs and the triggers"
)
def setStringValueImpl: Def.Initialize[InputTask[Unit]] = Def.inputTask {
val Seq(stringFile, string) = Def.spaceDelimited().parsed.map(_.trim)
IO.write(file(stringFile), string)
@ -25,43 +26,49 @@ object Build {
val Seq(stringFile, string) = Def.spaceDelimited().parsed
assert(IO.read(file(stringFile)) == string)
}
// This is a hack to exclude the default compile file inputs
def triggers(t: Seq[DynamicInput]): Seq[Glob] = t.collect {
// This is a hack to exclude the default compile and resource file inputs
case i if !i.glob.toString.contains("*") => i.glob
}
lazy val foo = project.settings(
setStringValue := {
val _ = (fileInputs in (bar, setStringValue)).value
setStringValueImpl.evaluated
},
checkStringValue := checkStringValueImpl.evaluated,
watchOnFileInputEvent := { (_, _) => Watch.CancelWatch },
Compile / compile / watchOnIteration := { _ => Watch.CancelWatch },
checkTriggers := {
val actual = triggers((Compile / compile / transitiveDynamicInputs).value).toSet
val base = baseDirectory.value.getParentFile
// This checks that since foo depends on bar there is a transitive trigger generated
// for the "bar.txt" trigger added to bar / Compile / unmanagedResources (which is a
// transitive dependency of
val expected: Set[Glob] = Set(base * "baz.txt", (base / "bar") * "bar.txt")
assert(actual == expected)
},
Test / test / watchTriggers += baseDirectory.value * "test.txt",
Test / checkTriggers := {
val testTriggers = triggers((Test / test / transitiveDynamicInputs).value).toSet
// This validates that since the "test.txt" trigger is only added to the Test / test task,
// that the Test / compile does not pick it up. Both of them pick up the the triggers that
// are found in the test above for the compile configuration because of the transitive
// classpath dependency that is added in Defaults.internalDependencies.
val compileTriggers = triggers((Test / compile / transitiveDynamicInputs).value).toSet
val base = baseDirectory.value.getParentFile
val expected: Set[Glob] = Set(
base * "baz.txt", (base / "bar") * "bar.txt", (base / "foo") * "test.txt")
assert(testTriggers == expected)
assert((testTriggers - ((base / "foo") * "test.txt")) == compileTriggers)
},
).dependsOn(bar)
lazy val foo = project
.settings(
setStringValue := {
val _ = (fileInputs in (bar, setStringValue)).value
setStringValueImpl.evaluated
},
checkStringValue := checkStringValueImpl.evaluated,
watchOnFileInputEvent := { (_, _) =>
Watch.CancelWatch
},
Compile / compile / watchOnIteration := { _ =>
Watch.CancelWatch
},
checkTriggers := {
val actual = triggers((Compile / compile / transitiveDynamicInputs).value).toSet
val base = baseDirectory.value.getParentFile
// This checks that since foo depends on bar there is a transitive trigger generated
// for the "bar.txt" trigger added to bar / Compile / unmanagedResources (which is a
// transitive dependency of
val expected: Set[Glob] = Set(base * "baz.txt", (base / "bar") * "bar.txt")
assert(actual == expected)
},
Test / test / watchTriggers += (baseDirectory.value / "test.txt").toGlob,
Test / checkTriggers := {
val testTriggers = triggers((Test / test / transitiveDynamicInputs).value).toSet
// This validates that since the "test.txt" trigger is only added to the Test / test task,
// that the Test / compile does not pick it up. Both of them pick up the the triggers that
// are found in the test above for the compile configuration because of the transitive
// classpath dependency that is added in Defaults.internalDependencies.
val compileTriggers = triggers((Test / compile / transitiveDynamicInputs).value).toSet
val base = baseDirectory.value.getParentFile
val expected: Set[Glob] =
Set(base * "baz.txt", (base / "bar") * "bar.txt", (base / "foo") * "test.txt")
assert(testTriggers == expected)
assert((testTriggers - ((base / "foo") * "test.txt")) == compileTriggers)
},
)
.dependsOn(bar)
lazy val bar = project.settings(
fileInputs in setStringValue += baseDirectory.value * "foo.txt",
@ -80,18 +87,22 @@ object Build {
val testTriggers = triggers((Test / test / transitiveDynamicInputs).value).toSet
val compileTriggers = triggers((Test / compile / transitiveDynamicInputs).value).toSet
val base = baseDirectory.value.getParentFile
val expected: Set[Glob] = Set(
base * "baz.txt", (base / "bar") * "bar.txt", (base / "bar") * "bar-test.txt")
val expected: Set[Glob] =
Set(base * "baz.txt", (base / "bar") * "bar.txt", (base / "bar") * "bar-test.txt")
assert(testTriggers == expected)
assert(testTriggers == compileTriggers)
},
)
lazy val root = (project in file(".")).aggregate(foo, bar).settings(
watchOnFileInputEvent := { (_, _) => Watch.CancelWatch },
checkTriggers := {
val actual = triggers((Compile / compile / transitiveDynamicInputs).value)
val expected: Seq[Glob] = baseDirectory.value * "baz.txt" :: Nil
assert(actual == expected)
},
)
lazy val root = (project in file("."))
.aggregate(foo, bar)
.settings(
watchOnFileInputEvent := { (_, _) =>
Watch.CancelWatch
},
checkTriggers := {
val actual = triggers((Compile / compile / transitiveDynamicInputs).value)
val expected: Seq[Glob] = baseDirectory.value * "baz.txt" :: Nil
assert(actual == expected)
},
)
}

View File

@ -6,6 +6,8 @@ import complete.Parser._
import java.io.{ PipedInputStream, PipedOutputStream }
import Keys._
import sbt.nio.Watch
import sbt.nio.Keys._
object Build {
val root = (project in file(".")).settings(

View File

@ -1,4 +1,5 @@
import sbt.legacy.sources.Build._
import sbt.nio.Watch
Global / watchSources += new sbt.internal.io.Source(baseDirectory.value, "global.txt", NothingFilter, false)

View File

@ -1,4 +1,4 @@
val checkReloaded = taskKey[Unit]("Asserts that the build was reloaded")
checkReloaded := { () }
watchOnIteration := { _ => Watch.CancelWatch }
watchOnIteration := { _ => sbt.nio.Watch.CancelWatch }

View File

@ -1 +1 @@
watchOnIteration := { _ => Watch.Reload }
watchOnIteration := { _ => sbt.nio.Watch.Reload }

View File

@ -6,19 +6,19 @@ $ copy-file changes/extra.sbt extra.sbt
# verify that the watch terminates when we reach the specified count
> resetCount
> set watchOnIteration := { (count: Int) => if (count == 2) Watch.CancelWatch else Watch.Ignore }
> set watchOnIteration := { (count: Int) => if (count == 2) sbt.nio.Watch.CancelWatch else sbt.nio.Watch.Ignore }
> ~compile
> checkCount 2
# verify that the watch terminates and returns an error when we reach the specified count
> resetCount
> set watchOnIteration := { (count: Int) => if (count == 2) new Watch.HandleError(new Exception("")) else Watch.Ignore }
> set watchOnIteration := { (count: Int) => if (count == 2) new sbt.nio.Watch.HandleError(new Exception("")) else sbt.nio.Watch.Ignore }
# Returning Watch.HandleError causes the '~' command to fail
-> ~compile
> checkCount 2
# verify that a re-build is triggered when we reach the specified count
> resetCount
> set watchOnIteration := { (count: Int) => if (count == 2) Watch.Trigger else if (count == 3) Watch.CancelWatch else Watch.Ignore }
> set watchOnIteration := { (count: Int) => if (count == 2) sbt.nio.Watch.Trigger else if (count == 3) sbt.nio.Watch.CancelWatch else sbt.nio.Watch.Ignore }
> ~compile
> checkCount 3

View File

@ -2,6 +2,8 @@ package sbt.watch.task
import sbt._
import Keys._
import sbt.nio.Watch
import sbt.nio.Keys._
object Build {
val reloadFile = settingKey[File]("file to toggle whether or not to reload")

View File

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

View File

@ -2,6 +2,8 @@ package sbt.watch.task
import sbt._
import Keys._
import sbt.nio.Keys._
import sbt.nio.Watch
object Build {
val reloadFile = settingKey[File]("file to toggle whether or not to reload")