mirror of https://github.com/sbt/sbt.git
Support inputs in dynamic tasks
Prior to this commit, it was necessary to add breadcrumbs for every input that is used within a dynamic task. In this commit, I rework the watch setup so that we can track the dynamic inputs that are used. To simplify the discussion, I'm going to ignore aggregation and multi-commands, but they are both supported. To implement this change, I update the GlobLister.all method to take a second implicit argument: DynamicInputs. This is effectively a mutable Set of Globs that is updated every time a task looks up files from a glob. The repository.get method should already register the glob with the repository. The set of globs are necessary because the repository may not do any file filtering so the file event monitor needs to check the input globs to ensure that the file event is for a file that actually requested by a task during evaluation. * Long term, I plan to add support for lifting tasks into a dynamic task in a way that records _all_ of the possible dependencies for the task through each of the dynamic code paths. We should revisit this change to determine if its still necessary after that change.
This commit is contained in:
parent
7c2607b1ae
commit
c72005fd2b
|
|
@ -22,4 +22,16 @@ object MacroDefaults {
|
|||
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
|
||||
* 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 dynamicInputs(c: blackbox.Context): c.Tree = {
|
||||
import c.universe._
|
||||
q"sbt.internal.Continuous.dynamicInputs.value: @sbtUnchecked"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ object Defaults extends BuildCommon {
|
|||
case Some(r) => r
|
||||
case None => FileTreeView.DEFAULT.asDataView(FileAttributes.default)
|
||||
}),
|
||||
Continuous.dynamicInputs := Continuous.dynamicInputsImpl.value,
|
||||
externalHooks := {
|
||||
val repository = fileTreeRepository.value
|
||||
compileOptions =>
|
||||
|
|
|
|||
|
|
@ -134,6 +134,18 @@ object Continuous extends DeprecatedContinuous {
|
|||
"Receives a copy of all of the bytes from System.in.",
|
||||
10000
|
||||
)
|
||||
val dynamicInputs = taskKey[FileTree.DynamicInputs](
|
||||
"The input globs found during task evaluation that are used in watch."
|
||||
)
|
||||
def dynamicInputsImpl: Def.Initialize[Task[FileTree.DynamicInputs]] = Def.task {
|
||||
Keys.state.value.get(DynamicInputs).getOrElse(FileTree.DynamicInputs.none)
|
||||
}
|
||||
private[sbt] val DynamicInputs =
|
||||
AttributeKey[FileTree.DynamicInputs](
|
||||
"dynamic-inputs",
|
||||
"Stores the inputs (dynamic and regular) for a task",
|
||||
10000
|
||||
)
|
||||
|
||||
private[this] val continuousParser: State => Parser[(Int, String)] = {
|
||||
def toInt(s: String): Int = Try(s.toInt).getOrElse(0)
|
||||
|
|
@ -175,12 +187,14 @@ object Continuous extends DeprecatedContinuous {
|
|||
}
|
||||
|
||||
val repository = getRepository(state)
|
||||
(inputs ++ triggers).foreach(repository.register)
|
||||
val registeringSet = state.get(DynamicInputs).get
|
||||
registeringSet.value.foreach(_ ++= inputs)
|
||||
(inputs ++ triggers).foreach(repository.register(_: Glob))
|
||||
val watchSettings = new WatchSettings(scopedKey)
|
||||
new Config(
|
||||
scopedKey,
|
||||
repository,
|
||||
inputs,
|
||||
() => registeringSet.value.fold(Nil: Seq[Glob])(_.toSeq).sorted,
|
||||
triggers,
|
||||
watchSettings
|
||||
)
|
||||
|
|
@ -195,25 +209,13 @@ object Continuous extends DeprecatedContinuous {
|
|||
}
|
||||
|
||||
private[sbt] def setup[R](state: State, command: String)(
|
||||
f: (State, Seq[String], Seq[() => Boolean], Seq[String]) => R
|
||||
f: (State, Seq[(String, State, () => Boolean)], Seq[String]) => R
|
||||
): R = {
|
||||
// First set up the state so that we can capture whether or not a task completed successfully
|
||||
// or if it threw an Exception (we lose the actual exception, but that should still be printed
|
||||
// to the console anyway).
|
||||
val failureCommandName = "SbtContinuousWatchOnFail"
|
||||
val onFail = Command.command(failureCommandName)(identity)
|
||||
/*
|
||||
* Takes a task string and converts it to an EitherTask. We cannot preserve either
|
||||
* the value returned by the task or any exception thrown by the task, but we can determine
|
||||
* whether or not the task ran successfully using the onFail command defined above.
|
||||
*/
|
||||
def makeTask(cmd: String)(task: () => State): () => Boolean = { () =>
|
||||
MainLoop
|
||||
.processCommand(Exec(cmd, None), state, task)
|
||||
.remainingCommands
|
||||
.forall(_.commandLine != failureCommandName)
|
||||
}
|
||||
|
||||
// This adds the "SbtContinuousWatchOnFail" onFailure handler which allows us to determine
|
||||
// whether or not the last task successfully ran. It is used in the makeTask method below.
|
||||
val s = (FailureWall :: state).copy(
|
||||
|
|
@ -221,6 +223,35 @@ object Continuous extends DeprecatedContinuous {
|
|||
definedCommands = state.definedCommands :+ onFail
|
||||
)
|
||||
|
||||
/*
|
||||
* Takes a task string and converts it to an EitherTask. We cannot preserve either
|
||||
* the value returned by the task or any exception thrown by the task, but we can determine
|
||||
* whether or not the task ran successfully using the onFail command defined above. Each
|
||||
* task gets its own state with its own file tree repository. This is so that we can keep
|
||||
* track of what globs are actually used by the task to ensure that we monitor them, even
|
||||
* if they are not visible in the input graph due to the use of Def.taskDyn.
|
||||
*/
|
||||
def makeTask(cmd: String): (String, State, () => Boolean) = {
|
||||
val newState = s.put(DynamicInputs, FileTree.DynamicInputs.empty)
|
||||
val task = Parser
|
||||
.parse(cmd, Command.combine(newState.definedCommands)(newState))
|
||||
.getOrElse(
|
||||
throw new IllegalStateException(
|
||||
"No longer able to parse command after transforming state"
|
||||
)
|
||||
)
|
||||
(
|
||||
cmd,
|
||||
newState,
|
||||
() => {
|
||||
MainLoop
|
||||
.processCommand(Exec(cmd, None), newState, task)
|
||||
.remainingCommands
|
||||
.forall(_.commandLine != failureCommandName)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// We support multiple commands in watch, so it's necessary to run the command string through
|
||||
// the multi parser.
|
||||
val trimmed = command.trim
|
||||
|
|
@ -234,14 +265,15 @@ object Continuous extends DeprecatedContinuous {
|
|||
val taskParser = Command.combine(s.definedCommands)(s)
|
||||
// This specified either the task corresponding to a command or the command itself if the
|
||||
// the command cannot be converted to a task.
|
||||
val (invalid, valid) = commands.foldLeft((Nil: Seq[String], Nil: Seq[() => Boolean])) {
|
||||
case ((i, v), cmd) =>
|
||||
Parser.parse(cmd, taskParser) match {
|
||||
case Right(task) => (i, v :+ makeTask(cmd)(task))
|
||||
case Left(c) => (i :+ c, v)
|
||||
}
|
||||
}
|
||||
f(s, commands, valid, invalid)
|
||||
val (invalid, valid) =
|
||||
commands.foldLeft((Nil: Seq[String], Nil: Seq[(String, State, () => Boolean)])) {
|
||||
case ((i, v), cmd) =>
|
||||
Parser.parse(cmd, taskParser) match {
|
||||
case Right(_) => (i, v :+ makeTask(cmd))
|
||||
case Left(c) => (i :+ c, v)
|
||||
}
|
||||
}
|
||||
f(s, valid, invalid)
|
||||
}
|
||||
|
||||
private[sbt] def runToTermination(
|
||||
|
|
@ -251,18 +283,18 @@ object Continuous extends DeprecatedContinuous {
|
|||
isCommand: Boolean
|
||||
): State = Watch.withCharBufferedStdIn { in =>
|
||||
val duped = new DupedInputStream(in)
|
||||
setup(state.put(DupedSystemIn, duped), command) { (s, commands, valid, invalid) =>
|
||||
setup(state.put(DupedSystemIn, duped), command) { (s, valid, invalid) =>
|
||||
implicit val extracted: Extracted = Project.extract(s)
|
||||
EvaluateTask.withStreams(extracted.structure, s)(_.use(Keys.streams in Global) { streams =>
|
||||
implicit val logger: Logger = streams.log
|
||||
if (invalid.isEmpty) {
|
||||
val currentCount = new AtomicInteger(count)
|
||||
val callbacks =
|
||||
aggregate(getAllConfigs(s, commands), logger, in, state, currentCount, isCommand)
|
||||
val configs = getAllConfigs(valid.map(v => v._1 -> v._2))
|
||||
val callbacks = aggregate(configs, logger, in, s, currentCount, isCommand)
|
||||
val task = () => {
|
||||
currentCount.getAndIncrement()
|
||||
// abort as soon as one of the tasks fails
|
||||
valid.takeWhile(_.apply())
|
||||
valid.takeWhile(_._3.apply())
|
||||
()
|
||||
}
|
||||
callbacks.onEnter()
|
||||
|
|
@ -273,7 +305,10 @@ object Continuous extends DeprecatedContinuous {
|
|||
try {
|
||||
val terminationAction = Watch(task, callbacks.onStart, callbacks.nextEvent)
|
||||
callbacks.onTermination(terminationAction, command, currentCount.get(), state)
|
||||
} finally callbacks.onExit()
|
||||
} finally {
|
||||
configs.foreach(_.repository.close())
|
||||
callbacks.onExit()
|
||||
}
|
||||
} else {
|
||||
// At least one of the commands in the multi command string could not be parsed, so we
|
||||
// log an error and exit.
|
||||
|
|
@ -285,28 +320,27 @@ object Continuous extends DeprecatedContinuous {
|
|||
}
|
||||
}
|
||||
|
||||
private def parseCommands(state: State, commands: Seq[String]): Seq[ScopedKey[_]] = {
|
||||
private def parseCommand(command: String, state: State): Seq[ScopedKey[_]] = {
|
||||
// Collect all of the scoped keys that are used to delegate the multi commands. These are
|
||||
// necessary to extract all of the transitive globs that we need to monitor during watch.
|
||||
// We have to add the <~ Parsers.any.* to ensure that we're able to extract the input key
|
||||
// from input tasks.
|
||||
val scopedKeyParser: Parser[Seq[ScopedKey[_]]] = Act.aggregatedKeyParser(state) <~ Parsers.any.*
|
||||
commands.flatMap { cmd: String =>
|
||||
Parser.parse(cmd, scopedKeyParser) match {
|
||||
case Right(scopedKeys: Seq[ScopedKey[_]]) => scopedKeys
|
||||
case Left(e) =>
|
||||
throw new IllegalStateException(s"Error attempting to extract scope from $cmd: $e.")
|
||||
case _ => Nil: Seq[ScopedKey[_]]
|
||||
}
|
||||
Parser.parse(command, scopedKeyParser) match {
|
||||
case Right(scopedKeys: Seq[ScopedKey[_]]) => scopedKeys
|
||||
case Left(e) =>
|
||||
throw new IllegalStateException(s"Error attempting to extract scope from $command: $e.")
|
||||
case _ => Nil: Seq[ScopedKey[_]]
|
||||
}
|
||||
}
|
||||
private def getAllConfigs(
|
||||
state: State,
|
||||
commands: Seq[String]
|
||||
inputs: Seq[(String, State)]
|
||||
)(implicit extracted: Extracted, logger: Logger): Seq[Config] = {
|
||||
val commandKeys = parseCommands(state, commands)
|
||||
val commandKeys = inputs.map { case (c, s) => s -> parseCommand(c, s) }
|
||||
val compiledMap = InputGraph.compile(extracted.structure)
|
||||
commandKeys.map((scopedKey: ScopedKey[_]) => getConfig(state, scopedKey, compiledMap))
|
||||
commandKeys.flatMap {
|
||||
case (s, scopedKeys) => scopedKeys.map(getConfig(s, _, compiledMap))
|
||||
}
|
||||
}
|
||||
|
||||
private class Callbacks(
|
||||
|
|
@ -430,16 +464,16 @@ object Continuous extends DeprecatedContinuous {
|
|||
val ws = params.watchSettings
|
||||
ws.onTrigger
|
||||
.map(_.apply(params.arguments(logger)))
|
||||
.getOrElse {
|
||||
val globFilter = (params.inputs ++ params.triggers).toEntryFilter
|
||||
event: Event =>
|
||||
if (globFilter(event.entry)) {
|
||||
ws.triggerMessage match {
|
||||
case Some(Left(tm)) => logger.info(tm(params.watchState(count.get())))
|
||||
case Some(Right(tm)) => tm(count.get(), event).foreach(logger.info(_))
|
||||
case None => // By default don't print anything
|
||||
}
|
||||
.getOrElse { event: Event =>
|
||||
val globFilter =
|
||||
(params.inputs() ++ params.triggers).toEntryFilter
|
||||
if (globFilter(event.entry)) {
|
||||
ws.triggerMessage match {
|
||||
case Some(Left(tm)) => logger.info(tm(params.watchState(count.get())))
|
||||
case Some(Right(tm)) => tm(count.get(), event).foreach(logger.info(_))
|
||||
case None => // By default don't print anything
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
event: Event =>
|
||||
|
|
@ -457,14 +491,16 @@ object Continuous extends DeprecatedContinuous {
|
|||
val onInputEvent = ws.onInputEvent.getOrElse(defaultTrigger)
|
||||
val onTriggerEvent = ws.onTriggerEvent.getOrElse(defaultTrigger)
|
||||
val onMetaBuildEvent = ws.onMetaBuildEvent.getOrElse(Watch.ifChanged(Watch.Reload))
|
||||
val inputFilter = params.inputs.toEntryFilter
|
||||
val triggerFilter = params.triggers.toEntryFilter
|
||||
val excludedBuildFilter = buildFilter
|
||||
event: Event =>
|
||||
val inputFilter = params.inputs().toEntryFilter
|
||||
val c = count.get()
|
||||
val entry = event.entry
|
||||
Seq[Watch.Action](
|
||||
if (inputFilter(event.entry)) onInputEvent(c, event) else Watch.Ignore,
|
||||
if (triggerFilter(event.entry)) onTriggerEvent(c, event) else Watch.Ignore,
|
||||
if (buildFilter(event.entry)) onMetaBuildEvent(c, event) else Watch.Ignore
|
||||
if (inputFilter(entry)) onInputEvent(c, event) else Watch.Ignore,
|
||||
if (triggerFilter(entry)) onTriggerEvent(c, event) else Watch.Ignore,
|
||||
if (excludedBuildFilter(entry)) onMetaBuildEvent(c, event) else Watch.Ignore
|
||||
).min
|
||||
}
|
||||
event: Event =>
|
||||
|
|
@ -477,18 +513,26 @@ object Continuous extends DeprecatedContinuous {
|
|||
f.view.map(_.apply(event)).minBy(_._2)
|
||||
}
|
||||
val monitor: FileEventMonitor[FileAttributes] = new FileEventMonitor[FileAttributes] {
|
||||
private def setup(
|
||||
|
||||
/**
|
||||
* Create a filtered monitor that only accepts globs that have been registered for the
|
||||
* task at hand.
|
||||
* @param monitor the file event monitor to filter
|
||||
* @param globs the globs to accept. This must be a function because we want to be able
|
||||
* to accept globs that are added dynamically as part of task evaluation.
|
||||
* @return the filtered FileEventMonitor.
|
||||
*/
|
||||
private def filter(
|
||||
monitor: FileEventMonitor[FileAttributes],
|
||||
globs: Seq[Glob]
|
||||
globs: () => Seq[Glob]
|
||||
): FileEventMonitor[FileAttributes] = {
|
||||
val globFilters = globs.toEntryFilter
|
||||
val filter: Event => Boolean = (event: Event) => globFilters(event.entry)
|
||||
new FileEventMonitor[FileAttributes] {
|
||||
override def poll(duration: Duration): Seq[FileEventMonitor.Event[FileAttributes]] =
|
||||
monitor.poll(duration).filter(filter)
|
||||
monitor.poll(duration).filter(e => globs().toEntryFilter(e.entry))
|
||||
override def close(): Unit = monitor.close()
|
||||
}
|
||||
}
|
||||
// TODO make this a normal monitor
|
||||
private[this] val monitors: Seq[FileEventMonitor[FileAttributes]] =
|
||||
configs.map { config =>
|
||||
// Create a logger with a scoped key prefix so that we can tell from which
|
||||
|
|
@ -496,17 +540,20 @@ object Continuous extends DeprecatedContinuous {
|
|||
val l = logger.withPrefix(config.key.show)
|
||||
val monitor: FileEventMonitor[FileAttributes] =
|
||||
FileManagement.monitor(config.repository, config.watchSettings.antiEntropy, l)
|
||||
val allGlobs = (config.inputs ++ config.triggers).distinct.sorted
|
||||
setup(monitor, allGlobs)
|
||||
val allGlobs: () => Seq[Glob] = () => (config.inputs() ++ config.triggers).distinct.sorted
|
||||
filter(monitor, allGlobs)
|
||||
} ++ (if (trackMetaBuild) {
|
||||
val l = logger.withPrefix("meta-build")
|
||||
val antiEntropy = configs.map(_.watchSettings.antiEntropy).min
|
||||
setup(FileManagement.monitor(getRepository(state), antiEntropy, l), buildGlobs) :: Nil
|
||||
val antiEntropy = configs.map(_.watchSettings.antiEntropy).max
|
||||
val repo = getRepository(state)
|
||||
buildGlobs.foreach(repo.register)
|
||||
val monitor = FileManagement.monitor(repo, antiEntropy, l)
|
||||
filter(monitor, () => buildGlobs) :: Nil
|
||||
} else Nil)
|
||||
override def poll(duration: Duration): Seq[FileEventMonitor.Event[FileAttributes]] = {
|
||||
// The call to .par allows us to poll all of the monitors in parallel.
|
||||
// This should be cheap because poll just blocks on a queue until an event is added.
|
||||
monitors.par.flatMap(_.poll(duration)).toSet.toVector
|
||||
val res = monitors.flatMap(_.poll(0.millis)).toSet.toVector
|
||||
if (res.isEmpty) Thread.sleep(duration.toMillis)
|
||||
res
|
||||
}
|
||||
override def close(): Unit = monitors.foreach(_.close())
|
||||
}
|
||||
|
|
@ -745,13 +792,13 @@ object Continuous extends DeprecatedContinuous {
|
|||
private final class Config private[internal] (
|
||||
val key: ScopedKey[_],
|
||||
val repository: FileTreeRepository[FileAttributes],
|
||||
val inputs: Seq[Glob],
|
||||
val inputs: () => Seq[Glob],
|
||||
val triggers: Seq[Glob],
|
||||
val watchSettings: WatchSettings
|
||||
) {
|
||||
private[sbt] def watchState(count: Int): DeprecatedWatchState =
|
||||
WatchState.empty(inputs ++ triggers).withCount(count)
|
||||
def arguments(logger: Logger): Arguments = new Arguments(logger, inputs, triggers)
|
||||
WatchState.empty(inputs() ++ triggers).withCount(count)
|
||||
def arguments(logger: Logger): Arguments = new Arguments(logger, inputs(), triggers)
|
||||
}
|
||||
private def getStartMessage(key: ScopedKey[_])(implicit e: Extracted): StartMessage = Some {
|
||||
lazy val default = key.get(Keys.watchStartMessage).getOrElse(Watch.defaultStartWatch)
|
||||
|
|
@ -865,5 +912,4 @@ object Continuous extends DeprecatedContinuous {
|
|||
logger.log(level, s"$prefix - $message")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
package sbt.internal
|
||||
|
||||
import java.nio.file.{ Path, Paths }
|
||||
import java.nio.file.Paths
|
||||
import java.util.Optional
|
||||
|
||||
import sbt.Stamped
|
||||
import sbt.internal.inc.ExternalLookup
|
||||
import sbt.io.syntax._
|
||||
import sbt.io.{ AllPassFilter, Glob, TypedPath }
|
||||
import sbt.Stamped
|
||||
import sbt.io.{ AllPassFilter, TypedPath }
|
||||
import xsbti.compile._
|
||||
import xsbti.compile.analysis.Stamp
|
||||
|
||||
|
|
@ -22,7 +22,6 @@ import scala.collection.mutable
|
|||
private[sbt] object ExternalHooks {
|
||||
private val javaHome = Option(System.getProperty("java.home")).map(Paths.get(_))
|
||||
def apply(options: CompileOptions, repo: FileTree.Repository): DefaultExternalHooks = {
|
||||
def listEntries(glob: Glob): Seq[(Path, FileAttributes)] = repo.get(glob)
|
||||
import scala.collection.JavaConverters._
|
||||
val sources = options.sources()
|
||||
val cachedSources = new java.util.HashMap[File, Stamp]
|
||||
|
|
@ -34,13 +33,9 @@ private[sbt] object ExternalHooks {
|
|||
val allBinaries = new java.util.HashMap[File, Stamp]
|
||||
options.classpath.foreach {
|
||||
case f if f.getName.endsWith(".jar") =>
|
||||
// This gives us the entry for the path itself, which is necessary if the path is a jar file
|
||||
// rather than a directory.
|
||||
listEntries(f.toGlob) foreach { case (p, a) => allBinaries.put(p.toFile, a.stamp) }
|
||||
repo.get(f.toGlob) foreach { case (p, a) => allBinaries.put(p.toFile, a.stamp) }
|
||||
case f =>
|
||||
listEntries(f ** AllPassFilter) foreach {
|
||||
case (p, a) => allBinaries.put(p.toFile, a.stamp)
|
||||
}
|
||||
repo.get(f ** AllPassFilter) foreach { case (p, a) => allBinaries.put(p.toFile, a.stamp) }
|
||||
}
|
||||
|
||||
val lookup = new ExternalLookup {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ private[sbt] object FileManagement {
|
|||
override def close(): Unit = monitor.close()
|
||||
}
|
||||
}
|
||||
|
||||
private[sbt] class HybridMonitoringRepository[T](
|
||||
underlying: HybridPollingFileTreeRepository[T],
|
||||
delay: FiniteDuration,
|
||||
|
|
|
|||
|
|
@ -14,12 +14,22 @@ import sbt.internal.util.appmacro.MacroDefaults
|
|||
import sbt.io.FileTreeDataView.Entry
|
||||
import sbt.io._
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.language.experimental.macros
|
||||
|
||||
object FileTree {
|
||||
private def toPair(e: Entry[FileAttributes]): Option[(Path, FileAttributes)] =
|
||||
e.value.toOption.map(a => e.typedPath.toPath -> a)
|
||||
trait Repository extends sbt.internal.Repository[Seq, Glob, (Path, FileAttributes)]
|
||||
private[sbt] trait DynamicInputs {
|
||||
def value: Option[mutable.Set[Glob]]
|
||||
}
|
||||
private[sbt] object DynamicInputs {
|
||||
def empty: DynamicInputs = new impl(Some(mutable.Set.empty[Glob]))
|
||||
final val none: DynamicInputs = new impl(None)
|
||||
private final class impl(override val value: Option[mutable.Set[Glob]]) extends DynamicInputs
|
||||
implicit def default: DynamicInputs = macro MacroDefaults.dynamicInputs
|
||||
}
|
||||
private[sbt] object Repository {
|
||||
|
||||
/**
|
||||
|
|
@ -45,7 +55,8 @@ object FileTree {
|
|||
extends Repository {
|
||||
override def get(key: Glob): Seq[(Path, FileAttributes)] = {
|
||||
underlying.register(key)
|
||||
underlying.listEntries(key).flatMap(toPair)
|
||||
//underlying.listEntries(key).flatMap(toPair).distinct
|
||||
Repository.polling.get(key)
|
||||
}
|
||||
override def close(): Unit = underlying.close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,14 @@
|
|||
package sbt
|
||||
package internal
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.ConcurrentSkipListMap
|
||||
|
||||
import sbt.io.Glob
|
||||
import sbt.io.{ FileFilter, Glob, SimpleFileFilter }
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.collection.mutable
|
||||
|
||||
/**
|
||||
* Retrieve files from a repository. This should usually be an extension class for
|
||||
|
|
@ -19,21 +24,21 @@ import sbt.io.Glob
|
|||
*/
|
||||
private[sbt] sealed trait GlobLister extends Any {
|
||||
|
||||
/**
|
||||
* Get the sources described this `GlobLister`.
|
||||
*
|
||||
* @param repository the [[FileTree.Repository]] to delegate file i/o.
|
||||
* @return the files described by this `GlobLister`.
|
||||
*/
|
||||
def all(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)]
|
||||
final def all(repository: FileTree.Repository): Seq[(Path, FileAttributes)] =
|
||||
all(repository, FileTree.DynamicInputs.empty)
|
||||
|
||||
/**
|
||||
* Get the unique sources described this `GlobLister`.
|
||||
* Get the sources described this `GlobLister`. The results should not return any duplicate
|
||||
* entries for each path in the result set.
|
||||
*
|
||||
* @param repository the [[FileTree.Repository]] to delegate file i/o.
|
||||
* @return the files described by this `GlobLister` with any duplicates removed.
|
||||
* @param repository the file tree repository for retrieving the files for a given glob.
|
||||
* @param dynamicInputs the task dynamic inputs to track for watch.
|
||||
* @return the files described by this `GlobLister`.
|
||||
*/
|
||||
def unique(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)]
|
||||
def all(
|
||||
implicit repository: FileTree.Repository,
|
||||
dynamicInputs: FileTree.DynamicInputs
|
||||
): Seq[(Path, FileAttributes)]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -57,10 +62,7 @@ private[sbt] trait GlobListers {
|
|||
implicit def fromGlob(source: Glob): GlobLister = new impl(source :: Nil)
|
||||
|
||||
/**
|
||||
* Generate a GlobLister given a collection of Globs. If the input collection type
|
||||
* preserves uniqueness, e.g. `Set[Glob]`, then the output of `GlobLister.all` will be
|
||||
* the unique source list. Otherwise duplicates are possible in all and it is necessary to call
|
||||
* `GlobLister.unique` to de-duplicate the files.
|
||||
* Generate a GlobLister given a collection of Globs.
|
||||
*
|
||||
* @param sources the collection of sources
|
||||
* @tparam T the source collection type
|
||||
|
|
@ -69,6 +71,34 @@ private[sbt] trait GlobListers {
|
|||
new impl(sources)
|
||||
}
|
||||
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
|
||||
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
|
||||
|
|
@ -79,18 +109,15 @@ private[internal] object GlobListers {
|
|||
* @tparam T the collection type
|
||||
*/
|
||||
private class impl[T <: Traversable[Glob]](val globs: T) extends AnyVal with GlobLister {
|
||||
private def get[T0 <: Traversable[Glob]](
|
||||
traversable: T0,
|
||||
repository: FileTree.Repository
|
||||
): Seq[(Path, FileAttributes)] =
|
||||
traversable.flatMap { glob =>
|
||||
val sourceFilter = glob.toFileFilter
|
||||
repository.get(glob).filter { case (p, _) => sourceFilter.accept(p.toFile) }
|
||||
override def all(
|
||||
implicit repository: FileTree.Repository,
|
||||
dynamicInputs: FileTree.DynamicInputs
|
||||
): Seq[(Path, FileAttributes)] = {
|
||||
aggregate(globs).flatMap {
|
||||
case (glob, allGlobs) =>
|
||||
dynamicInputs.value.foreach(_ ++= allGlobs)
|
||||
repository.get(glob)
|
||||
}.toIndexedSeq
|
||||
|
||||
override def all(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)] =
|
||||
get(globs, repository)
|
||||
override def unique(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)] =
|
||||
get(globs.toSet[Glob], repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,12 +46,9 @@ val checkSet = taskKey[Unit]("Verify that redundant sources are handled")
|
|||
|
||||
checkSet := {
|
||||
val redundant = (set / fileInputs).value.all.map(_._1.toFile)
|
||||
assert(redundant.size == 4) // It should get Foo.txt and Bar.md twice
|
||||
assert(redundant.size == 2)
|
||||
|
||||
val deduped = (set / 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)
|
||||
|
||||
val altDeduped = (set / fileInputs).value.unique.map(_._1.toFile)
|
||||
assert(altDeduped.sorted == expected)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import java.nio.file.Path
|
||||
|
||||
import sbt.internal.{FileAttributes, FileTree}
|
||||
import sbt.io.FileTreeDataView
|
||||
import xsbti.compile.analysis.Stamp
|
||||
|
||||
val allInputs = taskKey[Seq[File]]("")
|
||||
val allInputsExplicit = taskKey[Seq[File]]("")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import sbt.watch.task.Build
|
||||
|
||||
val root = Build.root
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package sbt.watch.task
|
||||
|
||||
import sbt._
|
||||
import Keys._
|
||||
import sbt.internal.FileTree
|
||||
|
||||
object Build {
|
||||
val reloadFile = settingKey[File]("file to toggle whether or not to reload")
|
||||
val setStringValue = taskKey[Unit]("set a global string to a value")
|
||||
val checkStringValue = inputKey[Unit]("check the value of a global")
|
||||
val foo = taskKey[Unit]("foo")
|
||||
def setStringValueImpl: Def.Initialize[Task[Unit]] = Def.task {
|
||||
val i = (setStringValue / fileInputs).value
|
||||
val (stringFile, string) = ("foo.txt", "bar")
|
||||
IO.write(file(stringFile), string)
|
||||
}
|
||||
def checkStringValueImpl: Def.Initialize[InputTask[Unit]] = Def.inputTask {
|
||||
val Seq(stringFile, string) = Def.spaceDelimited().parsed
|
||||
assert(IO.read(file(stringFile)) == string)
|
||||
}
|
||||
lazy val root = (project in file(".")).settings(
|
||||
reloadFile := baseDirectory.value / "reload",
|
||||
foo / fileInputs += baseDirectory.value * "foo.txt",
|
||||
setStringValue := Def.taskDyn {
|
||||
// This hides foo / fileInputs from the input graph
|
||||
Def.taskDyn {
|
||||
val _ = (foo / fileInputs).value.all
|
||||
// By putting setStringValueImpl.value inside a Def.task, we ensure that
|
||||
// (foo / fileInputs).value is registered with the file repository before modifying the file.
|
||||
Def.task(setStringValueImpl.value)
|
||||
}
|
||||
}.value,
|
||||
checkStringValue := checkStringValueImpl.evaluated,
|
||||
watchOnInputEvent := { (_, _) => Watch.CancelWatch },
|
||||
watchOnTriggerEvent := { (_, _) => Watch.CancelWatch },
|
||||
watchTasks := Def.inputTask {
|
||||
val prev = watchTasks.evaluated
|
||||
new StateTransform(prev.state.fail)
|
||||
}.evaluated
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# This tests that we can override the state transformation in the watch task
|
||||
# In the build, watchOnEvent should return CancelWatch which should be successful, but we
|
||||
# override watchTasks to fail the state instead
|
||||
|
||||
-> watch root / setStringValue
|
||||
|
||||
> checkStringValue foo.txt bar
|
||||
Loading…
Reference in New Issue