Merge pull request #4762 from eatkins/watch-commands

Watch commands
This commit is contained in:
eugene yokota 2019-06-02 16:41:56 -04:00 committed by GitHub
commit df24c0d8aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 116 additions and 39 deletions

View File

@ -9,7 +9,7 @@ package sbt
import java.io.{ File, IOException }
import java.net.URI
import java.util.concurrent.{ Executors, ForkJoinPool }
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.atomic.AtomicBoolean
import java.util.{ Locale, Properties }

View File

@ -177,21 +177,14 @@ object MainLoop {
}
/** This is the main function State transfer function of the sbt command processing. */
def processCommand(exec: Exec, state: State): State =
processCommand(exec, state, () => Command.process(exec.commandLine, state))
private[sbt] def processCommand(
exec: Exec,
state: State,
runCommand: () => State
): State = {
def processCommand(exec: Exec, state: State): State = {
val channelName = exec.source map (_.channelName)
StandardMain.exchange publishEventMessage
ExecStatusEvent("Processing", channelName, exec.execId, Vector())
try {
def process(): State = {
val newState = runCommand()
val newState = Command.process(exec.commandLine, state)
val doneEvent = ExecStatusEvent(
"Done",
channelName,

View File

@ -188,7 +188,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
inputs.foreach(i => repository.register(i.glob))
val watchSettings = new WatchSettings(scopedKey)
new Config(
scopedKey,
scopedKey.show,
() => dynamicInputs.toSeq.sorted,
watchSettings
)
@ -212,7 +212,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
val onFail = Command.command(failureCommandName)(identity)
// 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(
val s = state.copy(
onFailure = Some(Exec(failureCommandName, None)),
definedCommands = state.definedCommands :+ onFail
)
@ -226,22 +226,29 @@ private[sbt] object Continuous extends DeprecatedContinuous {
* 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, mutable.Set.empty[DynamicInput])
val task = Parser
.parse(cmd, Command.combine(newState.definedCommands)(newState))
.getOrElse(
throw new IllegalStateException(
"No longer able to parse command after transforming state"
)
)
val newState = s
.put(DynamicInputs, mutable.Set.empty[DynamicInput])
.copy(remainingCommands = Exec(cmd, None, None) :: Exec(FailureWall, None, None) :: Nil)
(
cmd,
newState,
() => {
MainLoop
.processCommand(Exec(cmd, None), newState, task)
.remainingCommands
.forall(_.commandLine != failureCommandName)
@tailrec
def impl(s: State): Boolean = {
s.remainingCommands match {
case exec :: rest =>
val updatedState = MainLoop.processCommand(exec, s.copy(remainingCommands = rest))
val remaining =
updatedState.remainingCommands.takeWhile(_.commandLine != FailureWall)
remaining match {
case Nil =>
updatedState.remainingCommands.forall(_.commandLine != failureCommandName)
case _ => impl(updatedState)
}
case Nil => true
}
}
impl(newState)
}
)
}
@ -256,7 +263,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
// Convert the command strings to runnable tasks, which are represented by
// () => Try[Boolean].
val taskParser = Command.combine(s.definedCommands)(s)
val taskParser = s.combinedParser
// 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) =
@ -378,6 +385,9 @@ private[sbt] object Continuous extends DeprecatedContinuous {
} finally repo.close()
}
// This is defined so we can assign a task key to a command to parse the WatchSettings.
private[this] val globalWatchSettingKey =
taskKey[Unit]("Internal task key. Not actually used.").withRank(KeyRanks.Invisible)
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.
@ -391,9 +401,13 @@ private[sbt] object Continuous extends DeprecatedContinuous {
val aliases = BasicCommands.allAliases(state)
aliases.collectFirst { case (`command`, aliased) => aliased } match {
case Some(aliased) => impl(aliased)
case _ =>
val msg = s"Error attempting to extract scope from $command: $e."
throw new IllegalStateException(msg)
case None =>
Parser.parse(command, state.combinedParser) match {
case Right(_) => globalWatchSettingKey.scopedKey :: Nil
case _ =>
val msg = s"Error attempting to extract scope from $command: $e."
throw new IllegalStateException(msg)
}
}
case _ => Nil: Seq[ScopedKey[_]]
}
@ -619,7 +633,7 @@ private[sbt] object Continuous extends DeprecatedContinuous {
configs.map { config =>
// Create a logger with a scoped key prefix so that we can tell from which task there
// were inputs that matched the event path.
val configLogger = logger.withPrefix(config.key.show)
val configLogger = logger.withPrefix(config.command)
observers.addObserver { e =>
if (config.inputs().exists(_.glob.matches(e.path))) {
configLogger.debug(s"Accepted event for ${e.path}")
@ -910,12 +924,12 @@ private[sbt] object Continuous extends DeprecatedContinuous {
* Container class for all of the components we need to setup a watch for a particular task or
* input task.
*
* @param key the [[ScopedKey]] instance for the task we will watch
* @param command the name of the command/task to run with each iteration
* @param inputs the transitive task inputs (see [[SettingsGraph]])
* @param watchSettings the [[WatchSettings]] instance for the task
*/
private final class Config private[internal] (
val key: ScopedKey[_],
val command: String,
val inputs: () => Seq[DynamicInput],
val watchSettings: WatchSettings
) {

View File

@ -0,0 +1,56 @@
import java.nio.file.Files
import scala.collection.JavaConverters._
val foo = taskKey[Unit]("foo")
foo := {
val fooTxt = baseDirectory.value / "foo.txt"
val _ = println(s"foo inputs: ${(foo / allInputFiles).value}")
IO.write(fooTxt, "foo")
println(s"foo wrote to $foo")
}
foo / fileInputs += baseDirectory.value.toGlob / "foo.txt"
Global / watchTriggers += baseDirectory.value.toGlob / "bar.txt"
commands ++= Seq(
Command.command("eval-foo") { s =>
Project.extract(s).runTask(foo, s)
s
},
Command.command("write-bar") { s =>
val bar = Project.extract(s).get(baseDirectory) / "bar.txt"
IO.write(bar, "bar")
println(s"write-bar wrote to $bar")
s
}
)
watchOnFileInputEvent := { (_, _) => sbt.nio.Watch.CancelWatch }
/*
* This test ensures that when watching a cross build that commands are run for multiple scala
* versions. It also checks that the fileInputs are automatically detected during task evaluation
* even though we can't directly inspect the fileInputs of the cross ('+') command.
*/
val expectFailure = taskKey[Unit]("expect failure")
expectFailure := {
val main = baseDirectory.value.toPath / "src" / "main"
val crossDir = main / (if (scalaVersion.value.startsWith("2.11")) "scala-2.11" else "scala-2.12")
(Compile / compile).result.value.toEither match {
case Left(_) =>
Files.write(crossDir / "Foo.scala", "class Foo".getBytes)
throw new IllegalStateException("Compilation failed.")
case Right(_) =>
if (!Files.walk(main).iterator.asScala.exists(_.getFileName.toString == "first.scala"))
Files.write(crossDir / "first.scala", "class first".getBytes)
else Files.write(crossDir / "second.scala", "class second".getBytes)
}
}
expectFailure / watchOnFileInputEvent := { (_, e) =>
if (e.path.getFileName.toString == "second.scala") sbt.nio.Watch.CancelWatch
else sbt.nio.Watch.Trigger
}
crossScalaVersions := Seq("2.11.12", "2.12.8")

View File

@ -0,0 +1 @@
class Foo {

View File

@ -0,0 +1 @@
class Foo {

View File

@ -0,0 +1 @@
class Bar

View File

@ -0,0 +1,6 @@
> ~ eval-foo
> ~ write-bar
> set watchOnFileInputEvent := (expectFailure / watchOnFileInputEvent).value
> ~ +expectFailure

View File

@ -1,8 +1,10 @@
import java.nio.file._
import java.nio.file.attribute.FileTime
import sbt.nio.Keys._
import sbt.nio._
import scala.concurrent.duration._
import StandardCopyOption.{ REPLACE_EXISTING => replace }
watchTriggeredMessage := { (i, path: Path, c) =>
val prev = watchTriggeredMessage.value
@ -16,14 +18,15 @@ watchOnIteration := { i: Int =>
val src =
base.resolve("src").resolve("main").resolve("scala").resolve("sbt").resolve("test")
val changes = base.resolve("changes")
Files.copy(changes.resolve("C.scala"), src.resolve("C.scala"), replace)
if (i < 5) {
def copy(fileName: String): Unit = {
val content =
new String(Files.readAllBytes(changes.resolve("A.scala"))) + "\n" + ("//" * i)
Files.write(src.resolve("A.scala"), content.getBytes)
} else {
Files.copy(changes.resolve("B.scala"), src.resolve("B.scala"), replace)
new String(Files.readAllBytes(changes.resolve(fileName))) + "\n" + ("//" * i)
Files.write(src.resolve(fileName), content.getBytes)
}
val c = src.resolve("C.scala")
Files.setLastModifiedTime(c, FileTime.fromMillis(Files.getLastModifiedTime(c).toMillis + 1111))
if (i < 5) copy("A.scala")
else copy("B.scala")
println(s"Waiting for changes...")
Watch.Ignore
}

View File

@ -232,7 +232,9 @@ final class ScriptedTests(
case "source-dependencies/linearization" => LauncherBased // sbt/Package$
case "source-dependencies/named" => LauncherBased // sbt/Package$
case "source-dependencies/specialized" => LauncherBased // sbt/Package$
case "watch/managed" => LauncherBased // sbt/Package$
case "watch/commands" =>
LauncherBased // java.lang.ClassNotFoundException: javax.tools.DiagnosticListener when run with java 11 and an old sbt launcher
case "watch/managed" => LauncherBased // sbt/Package$
case "tests/test-cross" =>
LauncherBased // the sbt metabuild classpath leaks into the test interface classloader in older versions of sbt
case _ => RunFromSourceBased