mirror of https://github.com/sbt/sbt.git
Merge branch 'develop' into configurableLibraryManagement
This commit is contained in:
commit
cd69a112bd
|
|
@ -41,7 +41,7 @@ env:
|
||||||
- SBT_CMD="scripted source-dependencies/*1of3"
|
- SBT_CMD="scripted source-dependencies/*1of3"
|
||||||
- SBT_CMD="scripted source-dependencies/*2of3"
|
- SBT_CMD="scripted source-dependencies/*2of3"
|
||||||
- SBT_CMD="scripted source-dependencies/*3of3"
|
- SBT_CMD="scripted source-dependencies/*3of3"
|
||||||
- SBT_CMD="scripted tests/*"
|
- SBT_CMD="scripted tests/* watch/*"
|
||||||
- SBT_CMD="repoOverrideTest:scripted dependency-management/*"
|
- SBT_CMD="repoOverrideTest:scripted dependency-management/*"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
|
|
|
||||||
|
|
@ -507,7 +507,8 @@ lazy val commandProj = (project in file("main-command"))
|
||||||
addSbtUtilLogging,
|
addSbtUtilLogging,
|
||||||
addSbtCompilerInterface,
|
addSbtCompilerInterface,
|
||||||
addSbtCompilerClasspath,
|
addSbtCompilerClasspath,
|
||||||
addSbtLmCore
|
addSbtLmCore,
|
||||||
|
addSbtZinc
|
||||||
)
|
)
|
||||||
|
|
||||||
// The core macro project defines the main logic of the DSL, abstracted
|
// The core macro project defines the main logic of the DSL, abstracted
|
||||||
|
|
|
||||||
|
|
@ -231,4 +231,8 @@ $AliasCommand name=
|
||||||
val ContinuousExecutePrefix = "~"
|
val ContinuousExecutePrefix = "~"
|
||||||
def continuousDetail = "Executes the specified command whenever source files change."
|
def continuousDetail = "Executes the specified command whenever source files change."
|
||||||
def continuousBriefHelp = (ContinuousExecutePrefix + " <command>", continuousDetail)
|
def continuousBriefHelp = (ContinuousExecutePrefix + " <command>", continuousDetail)
|
||||||
|
def FlushFileTreeRepository = "flushFileTreeRepository"
|
||||||
|
def FlushDetailed: String =
|
||||||
|
"Resets the global file repository in the event that the repository has become inconsistent " +
|
||||||
|
"with the file system."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ object BasicCommands {
|
||||||
call,
|
call,
|
||||||
early,
|
early,
|
||||||
exit,
|
exit,
|
||||||
continuous,
|
|
||||||
history,
|
history,
|
||||||
oldshell,
|
oldshell,
|
||||||
client,
|
client,
|
||||||
|
|
@ -254,6 +253,7 @@ object BasicCommands {
|
||||||
|
|
||||||
def exit: Command = Command.command(TerminateAction, exitBrief, exitBrief)(_ exit true)
|
def exit: Command = Command.command(TerminateAction, exitBrief, exitBrief)(_ exit true)
|
||||||
|
|
||||||
|
@deprecated("Replaced by BuiltInCommands.continuous", "1.3.0")
|
||||||
def continuous: Command =
|
def continuous: Command =
|
||||||
Command(ContinuousExecutePrefix, continuousBriefHelp, continuousDetail)(otherCommandParser) {
|
Command(ContinuousExecutePrefix, continuousBriefHelp, continuousDetail)(otherCommandParser) {
|
||||||
(s, arg) =>
|
(s, arg) =>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import java.io.File
|
||||||
import sbt.internal.util.AttributeKey
|
import sbt.internal.util.AttributeKey
|
||||||
import sbt.internal.inc.classpath.ClassLoaderCache
|
import sbt.internal.inc.classpath.ClassLoaderCache
|
||||||
import sbt.internal.server.ServerHandler
|
import sbt.internal.server.ServerHandler
|
||||||
|
import sbt.io.FileTreeDataView
|
||||||
import sbt.librarymanagement.ModuleID
|
import sbt.librarymanagement.ModuleID
|
||||||
import sbt.util.Level
|
import sbt.util.Level
|
||||||
|
|
||||||
|
|
@ -100,6 +101,11 @@ object BasicKeys {
|
||||||
"List of template resolver infos.",
|
"List of template resolver infos.",
|
||||||
1000
|
1000
|
||||||
)
|
)
|
||||||
|
private[sbt] val globalFileTreeView = AttributeKey[FileTreeDataView[StampedFile]](
|
||||||
|
"globalFileTreeView",
|
||||||
|
"provides a view into the file system that may or may not cache the tree in memory",
|
||||||
|
1000
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
case class TemplateResolverInfo(module: ModuleID, implementationClass: String)
|
case class TemplateResolverInfo(module: ModuleID, implementationClass: String)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
* sbt
|
||||||
|
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||||
|
* Copyright 2008 - 2010, Mark Harrah
|
||||||
|
* Licensed under Apache License 2.0 (see LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sbt
|
||||||
|
import sbt.Watched.WatchSource
|
||||||
|
import sbt.internal.io.{ HybridPollingFileTreeRepository, WatchServiceBackedObservable, WatchState }
|
||||||
|
import sbt.io._
|
||||||
|
import FileTreeDataView.{ Observable, Observer }
|
||||||
|
import sbt.util.Logger
|
||||||
|
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for viewing and monitoring the file system.
|
||||||
|
*/
|
||||||
|
final class FileTreeViewConfig private (
|
||||||
|
val newDataView: () => FileTreeDataView[StampedFile],
|
||||||
|
val newMonitor: (
|
||||||
|
FileTreeDataView[StampedFile],
|
||||||
|
Seq[WatchSource],
|
||||||
|
Logger
|
||||||
|
) => FileEventMonitor[StampedFile]
|
||||||
|
)
|
||||||
|
object FileTreeViewConfig {
|
||||||
|
private implicit class RepositoryOps(val repository: FileTreeRepository[StampedFile]) {
|
||||||
|
def register(sources: Seq[WatchSource]): Unit = sources foreach { s =>
|
||||||
|
repository.register(s.base.toPath, if (s.recursive) Integer.MAX_VALUE else 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new FileTreeViewConfig. This factory takes a generic parameter, T, that is bounded
|
||||||
|
* by {{{sbt.io.FileTreeDataView[StampedFile]}}}. The reason for this is to ensure that a
|
||||||
|
* sbt.io.FileTreeDataView that is instantiated by [[FileTreeViewConfig.newDataView]] can be
|
||||||
|
* passed into [[FileTreeViewConfig.newMonitor]] without constraining the type of view to be
|
||||||
|
* {{{sbt.io.FileTreeDataView[StampedFile]}}}.
|
||||||
|
* @param newDataView create a new sbt.io.FileTreeDataView. This value may be cached in a global
|
||||||
|
* attribute
|
||||||
|
* @param newMonitor create a new sbt.io.FileEventMonitor using the sbt.io.FileTreeDataView
|
||||||
|
* created by newDataView
|
||||||
|
* @tparam T the subtype of sbt.io.FileTreeDataView that is returned by [[FileTreeViewConfig.newDataView]]
|
||||||
|
* @return a [[FileTreeViewConfig]] instance.
|
||||||
|
*/
|
||||||
|
def apply[T <: FileTreeDataView[StampedFile]](
|
||||||
|
newDataView: () => T,
|
||||||
|
newMonitor: (T, Seq[WatchSource], Logger) => FileEventMonitor[StampedFile]
|
||||||
|
): FileTreeViewConfig =
|
||||||
|
new FileTreeViewConfig(
|
||||||
|
newDataView,
|
||||||
|
(view: FileTreeDataView[StampedFile], sources: Seq[WatchSource], logger: Logger) =>
|
||||||
|
newMonitor(view.asInstanceOf[T], sources, logger)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a [[FileTreeViewConfig]] with semantics as close as possible to sbt 1.2.0. This means
|
||||||
|
* that there is no file tree caching and the sbt.io.FileEventMonitor will use an
|
||||||
|
* sbt.io.WatchService for monitoring the file system.
|
||||||
|
* @param delay the maximum delay for which the background thread will poll the
|
||||||
|
* sbt.io.WatchService for file system events
|
||||||
|
* @param antiEntropy the duration of the period after a path triggers a build for which it is
|
||||||
|
* quarantined from triggering another build
|
||||||
|
* @return a [[FileTreeViewConfig]] instance.
|
||||||
|
*/
|
||||||
|
def sbt1_2_compat(
|
||||||
|
delay: FiniteDuration,
|
||||||
|
antiEntropy: FiniteDuration
|
||||||
|
): FileTreeViewConfig =
|
||||||
|
FileTreeViewConfig(
|
||||||
|
() => FileTreeView.DEFAULT.asDataView(StampedFile.converter),
|
||||||
|
(_: FileTreeDataView[StampedFile], sources, logger) => {
|
||||||
|
val ioLogger: sbt.io.WatchLogger = msg => logger.debug(msg.toString)
|
||||||
|
FileEventMonitor.antiEntropy(
|
||||||
|
new WatchServiceBackedObservable(
|
||||||
|
WatchState.empty(Watched.createWatchService(), sources),
|
||||||
|
delay,
|
||||||
|
StampedFile.converter,
|
||||||
|
closeService = true,
|
||||||
|
ioLogger
|
||||||
|
),
|
||||||
|
antiEntropy,
|
||||||
|
ioLogger
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a default [[FileTreeViewConfig]]. This view caches entries and solely relies on
|
||||||
|
* file system events from the operating system to update its internal representation of the
|
||||||
|
* file tree.
|
||||||
|
* @param antiEntropy the duration of the period after a path triggers a build for which it is
|
||||||
|
* quarantined from triggering another build
|
||||||
|
* @return a [[FileTreeViewConfig]] instance.
|
||||||
|
*/
|
||||||
|
def default(antiEntropy: FiniteDuration): FileTreeViewConfig =
|
||||||
|
FileTreeViewConfig(
|
||||||
|
() => FileTreeRepository.default(StampedFile.converter),
|
||||||
|
(repository: FileTreeRepository[StampedFile], sources: Seq[WatchSource], logger: Logger) => {
|
||||||
|
repository.register(sources)
|
||||||
|
val copied = new Observable[StampedFile] {
|
||||||
|
override def addObserver(observer: Observer[StampedFile]): Int =
|
||||||
|
repository.addObserver(observer)
|
||||||
|
override def removeObserver(handle: Int): Unit = repository.removeObserver(handle)
|
||||||
|
override def close(): Unit = {} // Don't close the underlying observable
|
||||||
|
}
|
||||||
|
FileEventMonitor.antiEntropy(copied, antiEntropy, msg => logger.debug(msg.toString))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a default [[FileTreeViewConfig]]. When the pollingSources argument is empty, it
|
||||||
|
* returns the same config as [[sbt.FileTreeViewConfig.default(antiEntropy:scala\.concurrent\.duration\.FiniteDuration)*]].
|
||||||
|
* Otherwise, it returns the same config as [[polling]].
|
||||||
|
* @param antiEntropy the duration of the period after a path triggers a build for which it is
|
||||||
|
* quarantined from triggering another build
|
||||||
|
* @param pollingInterval the frequency with which the sbt.io.FileEventMonitor polls the file
|
||||||
|
* system for the paths included in pollingSources
|
||||||
|
* @param pollingSources the sources that will not be cached in the sbt.io.FileTreeRepository and that
|
||||||
|
* will be periodically polled for changes during continuous builds.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
def default(
|
||||||
|
antiEntropy: FiniteDuration,
|
||||||
|
pollingInterval: FiniteDuration,
|
||||||
|
pollingSources: Seq[WatchSource]
|
||||||
|
): FileTreeViewConfig = {
|
||||||
|
if (pollingSources.isEmpty) default(antiEntropy)
|
||||||
|
else polling(antiEntropy, pollingInterval, pollingSources)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a polling [[FileTreeViewConfig]]. Unlike the view returned by newDataView in
|
||||||
|
* [[sbt.FileTreeViewConfig.default(antiEntropy:scala\.concurrent\.duration\.FiniteDuration)*]],
|
||||||
|
* the view returned by newDataView will not cache any portion of the file system tree that is is
|
||||||
|
* covered by the pollingSources parameter. The monitor that is generated by newMonitor, will
|
||||||
|
* poll these directories for changes rather than relying on file system events from the
|
||||||
|
* operating system. Any paths that are registered with the view that are not included in the
|
||||||
|
* pollingSources will be cached and monitored using file system events from the operating system
|
||||||
|
* in the same way that they are in the default view.
|
||||||
|
*
|
||||||
|
* @param antiEntropy the duration of the period after a path triggers a build for which it is
|
||||||
|
* quarantined from triggering another build
|
||||||
|
* @param pollingInterval the frequency with which the FileEventMonitor polls the file system
|
||||||
|
* for the paths included in pollingSources
|
||||||
|
* @param pollingSources the sources that will not be cached in the sbt.io.FileTreeRepository and that
|
||||||
|
* will be periodically polled for changes during continuous builds.
|
||||||
|
* @return a [[FileTreeViewConfig]] instance.
|
||||||
|
*/
|
||||||
|
def polling(
|
||||||
|
antiEntropy: FiniteDuration,
|
||||||
|
pollingInterval: FiniteDuration,
|
||||||
|
pollingSources: Seq[WatchSource],
|
||||||
|
): FileTreeViewConfig = FileTreeViewConfig(
|
||||||
|
() => FileTreeRepository.hybrid(StampedFile.converter, pollingSources: _*),
|
||||||
|
(
|
||||||
|
repository: HybridPollingFileTreeRepository[StampedFile],
|
||||||
|
sources: Seq[WatchSource],
|
||||||
|
logger: Logger
|
||||||
|
) => {
|
||||||
|
repository.register(sources)
|
||||||
|
FileEventMonitor
|
||||||
|
.antiEntropy(
|
||||||
|
repository.toPollingObservable(pollingInterval, sources, NullWatchLogger),
|
||||||
|
antiEntropy,
|
||||||
|
msg => logger.debug(msg.toString)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -7,20 +7,32 @@
|
||||||
|
|
||||||
package sbt
|
package sbt
|
||||||
|
|
||||||
import java.io.File
|
import java.io.{ File, InputStream }
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
|
|
||||||
import sbt.BasicCommandStrings.ClearOnFailure
|
import sbt.BasicCommandStrings.{
|
||||||
import sbt.State.FailureWall
|
ContinuousExecutePrefix,
|
||||||
|
FailureWall,
|
||||||
|
continuousBriefHelp,
|
||||||
|
continuousDetail
|
||||||
|
}
|
||||||
|
import sbt.BasicCommands.otherCommandParser
|
||||||
|
import sbt.internal.LegacyWatched
|
||||||
|
import sbt.internal.inc.Stamper
|
||||||
import sbt.internal.io.{ EventMonitor, Source, WatchState }
|
import sbt.internal.io.{ EventMonitor, Source, WatchState }
|
||||||
import sbt.internal.util.AttributeKey
|
|
||||||
import sbt.internal.util.Types.const
|
import sbt.internal.util.Types.const
|
||||||
|
import sbt.internal.util.complete.DefaultParsers
|
||||||
|
import sbt.internal.util.{ AttributeKey, JLine }
|
||||||
|
import sbt.io.FileEventMonitor.Event
|
||||||
import sbt.io._
|
import sbt.io._
|
||||||
|
import sbt.util.{ Level, Logger }
|
||||||
|
import xsbti.compile.analysis.Stamp
|
||||||
|
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
import scala.util.Properties
|
import scala.util.Properties
|
||||||
|
|
||||||
|
@deprecated("Watched is no longer used to implement continuous execution", "1.3.0")
|
||||||
trait Watched {
|
trait Watched {
|
||||||
|
|
||||||
/** The files watched when an action is run with a proceeding ~ */
|
/** The files watched when an action is run with a proceeding ~ */
|
||||||
|
|
@ -50,18 +62,102 @@ trait Watched {
|
||||||
}
|
}
|
||||||
|
|
||||||
object Watched {
|
object Watched {
|
||||||
val defaultWatchingMessage: WatchState => String = ws =>
|
|
||||||
s"${ws.count}. Waiting for source changes... (press enter to interrupt)"
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This trait is used to communicate what the watch should do next at various points in time. It
|
||||||
|
* is heavily linked to a number of callbacks in [[WatchConfig]]. For example, when the
|
||||||
|
* sbt.io.FileEventMonitor created by [[FileTreeViewConfig.newMonitor]] detects a changed source
|
||||||
|
* file, then we expect [[WatchConfig.onWatchEvent]] to return [[Trigger]].
|
||||||
|
*/
|
||||||
|
sealed trait Action
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action that indicates that the watch should stop.
|
||||||
|
*/
|
||||||
|
case object CancelWatch extends Action
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action that indicates that an error has occurred. The watch will be terminated when this action
|
||||||
|
* is produced.
|
||||||
|
*/
|
||||||
|
case object HandleError extends Action
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action that indicates that the watch should continue as though nothing happened. This may be
|
||||||
|
* because, for example, no user input was yet available in [[WatchConfig.handleInput]].
|
||||||
|
*/
|
||||||
|
case object Ignore extends Action
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action that indicates that the watch should pause while the build is reloaded. This is used to
|
||||||
|
* automatically reload the project when the build files (e.g. build.sbt) are changed.
|
||||||
|
*/
|
||||||
|
case object Reload extends Action
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action that indicates that the watch process should re-run the command.
|
||||||
|
*/
|
||||||
|
case object Trigger extends Action
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A user defined Action. It is not sealed so that the user can create custom instances. If any
|
||||||
|
* of the [[WatchConfig]] callbacks, e.g. [[WatchConfig.onWatchEvent]], return an instance of
|
||||||
|
* [[Custom]], the watch will terminate.
|
||||||
|
*/
|
||||||
|
trait Custom extends Action
|
||||||
|
|
||||||
|
type WatchSource = Source
|
||||||
|
def terminateWatch(key: Int): Boolean = Watched.isEnter(key)
|
||||||
|
|
||||||
|
private def withCharBufferedStdIn[R](f: InputStream => R): R = JLine.usingTerminal { terminal =>
|
||||||
|
val in = terminal.wrapInIfNeeded(System.in)
|
||||||
|
try {
|
||||||
|
while (in.available > 0) in.read()
|
||||||
|
terminal.init()
|
||||||
|
f(in)
|
||||||
|
} finally {
|
||||||
|
while (in.available > 0) in.read()
|
||||||
|
terminal.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private[sbt] final val handleInput: InputStream => Action = in => {
|
||||||
|
@tailrec
|
||||||
|
def scanInput(): Action = {
|
||||||
|
if (in.available > 0) {
|
||||||
|
in.read() match {
|
||||||
|
case key if isEnter(key) => CancelWatch
|
||||||
|
case key if isR(key) => Trigger
|
||||||
|
case key if key >= 0 => scanInput()
|
||||||
|
case _ => Ignore
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scanInput()
|
||||||
|
}
|
||||||
|
private def waitMessage(project: String): String =
|
||||||
|
s"Waiting for source changes$project... (press enter to interrupt or 'r' to re-run the command)"
|
||||||
|
val defaultStartWatch: Int => Option[String] = count => Some(s"$count. ${waitMessage("")}")
|
||||||
|
@deprecated("Use defaultStartWatch in conjunction with the watchStartMessage key", "1.3.0")
|
||||||
|
val defaultWatchingMessage: WatchState => String = ws => defaultStartWatch(ws.count).get
|
||||||
def projectWatchingMessage(projectId: String): WatchState => String =
|
def projectWatchingMessage(projectId: String): WatchState => String =
|
||||||
ws =>
|
ws => projectOnWatchMessage(projectId)(ws.count).get
|
||||||
s"${ws.count}. Waiting for source changes in project $projectId... (press enter to interrupt)"
|
def projectOnWatchMessage(project: String): Int => Option[String] =
|
||||||
|
count => Some(s"$count. ${waitMessage(s" in project $project")}")
|
||||||
|
|
||||||
|
val defaultOnTriggerMessage: Int => Option[String] = _ => None
|
||||||
|
@deprecated(
|
||||||
|
"Use defaultOnTriggerMessage in conjunction with the watchTriggeredMessage key",
|
||||||
|
"1.3.0"
|
||||||
|
)
|
||||||
val defaultTriggeredMessage: WatchState => String = const("")
|
val defaultTriggeredMessage: WatchState => String = const("")
|
||||||
|
val clearOnTrigger: Int => Option[String] = _ => Some(clearScreen)
|
||||||
|
@deprecated("Use clearOnTrigger in conjunction with the watchTriggeredMessage key", "1.3.0")
|
||||||
val clearWhenTriggered: WatchState => String = const(clearScreen)
|
val clearWhenTriggered: WatchState => String = const(clearScreen)
|
||||||
def clearScreen: String = "\u001b[2J\u001b[0;0H"
|
def clearScreen: String = "\u001b[2J\u001b[0;0H"
|
||||||
|
|
||||||
type WatchSource = Source
|
|
||||||
object WatchSource {
|
object WatchSource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -85,67 +181,209 @@ object Watched {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@deprecated("This method is not used and may be removed in a future version of sbt", "1.3.0")
|
||||||
private[this] class AWatched extends Watched
|
private[this] class AWatched extends Watched
|
||||||
|
|
||||||
|
@deprecated("This method is not used and may be removed in a future version of sbt", "1.3.0")
|
||||||
def multi(base: Watched, paths: Seq[Watched]): Watched =
|
def multi(base: Watched, paths: Seq[Watched]): Watched =
|
||||||
new AWatched {
|
new AWatched {
|
||||||
override def watchSources(s: State) = (base.watchSources(s) /: paths)(_ ++ _.watchSources(s))
|
override def watchSources(s: State): Seq[Watched.WatchSource] =
|
||||||
|
(base.watchSources(s) /: paths)(_ ++ _.watchSources(s))
|
||||||
override def terminateWatch(key: Int): Boolean = base.terminateWatch(key)
|
override def terminateWatch(key: Int): Boolean = base.terminateWatch(key)
|
||||||
override val pollInterval = (base +: paths).map(_.pollInterval).min
|
override val pollInterval: FiniteDuration = (base +: paths).map(_.pollInterval).min
|
||||||
override val antiEntropy = (base +: paths).map(_.antiEntropy).min
|
override val antiEntropy: FiniteDuration = (base +: paths).map(_.antiEntropy).min
|
||||||
override def watchingMessage(s: WatchState) = base.watchingMessage(s)
|
override def watchingMessage(s: WatchState): String = base.watchingMessage(s)
|
||||||
override def triggeredMessage(s: WatchState) = base.triggeredMessage(s)
|
override def triggeredMessage(s: WatchState): String = base.triggeredMessage(s)
|
||||||
}
|
}
|
||||||
|
@deprecated("This method is not used and may be removed in a future version of sbt", "1.3.0")
|
||||||
def empty: Watched = new AWatched
|
def empty: Watched = new AWatched
|
||||||
|
|
||||||
val PollDelay: FiniteDuration = 500.milliseconds
|
val PollDelay: FiniteDuration = 500.milliseconds
|
||||||
val AntiEntropy: FiniteDuration = 40.milliseconds
|
val AntiEntropy: FiniteDuration = 40.milliseconds
|
||||||
def isEnter(key: Int): Boolean = key == 10 || key == 13
|
def isEnter(key: Int): Boolean = key == 10 || key == 13
|
||||||
def printIfDefined(msg: String) = if (!msg.isEmpty) System.out.println(msg)
|
def isR(key: Int): Boolean = key == 82 || key == 114
|
||||||
|
def printIfDefined(msg: String): Unit = if (!msg.isEmpty) System.out.println(msg)
|
||||||
|
|
||||||
def executeContinuously(watched: Watched, s: State, next: String, repeat: String): State = {
|
private type RunCommand = () => State
|
||||||
@tailrec def shouldTerminate: Boolean =
|
private type WatchSetup = (State, String) => (State, WatchConfig, RunCommand => State)
|
||||||
(System.in.available > 0) && (watched.terminateWatch(System.in.read()) || shouldTerminate)
|
|
||||||
val log = s.log
|
/**
|
||||||
val logger = new EventMonitor.Logger {
|
* Provides the '~' continuous execution command.
|
||||||
override def debug(msg: => Any): Unit = log.debug(msg.toString)
|
* @param setup a function that provides a logger and a function from (() => State) => State.
|
||||||
|
* @return the '~' command.
|
||||||
|
*/
|
||||||
|
def continuous(setup: WatchSetup): Command =
|
||||||
|
Command(ContinuousExecutePrefix, continuousBriefHelp, continuousDetail)(otherCommandParser) {
|
||||||
|
(state, command) =>
|
||||||
|
Watched.executeContinuously(state, command, setup)
|
||||||
}
|
}
|
||||||
s get ContinuousEventMonitor match {
|
|
||||||
case None =>
|
/**
|
||||||
// This is the first iteration, so run the task and create a new EventMonitor
|
* Default handler to transform the state when the watch terminates. When the [[Watched.Action]] is
|
||||||
(ClearOnFailure :: next :: FailureWall :: repeat :: s)
|
* [[Reload]], the handler will prepend the original command (prefixed by ~) to the
|
||||||
.put(
|
* [[State.remainingCommands]] and then invoke the [[StateOps.reload]] method. When the
|
||||||
ContinuousEventMonitor,
|
* [[Watched.Action]] is [[HandleError]], the handler returns the result of [[StateOps.fail]]. Otherwise
|
||||||
EventMonitor(
|
* the original state is returned.
|
||||||
WatchState.empty(watched.watchService(), watched.watchSources(s)),
|
*/
|
||||||
watched.pollInterval,
|
private[sbt] val onTermination: (Action, String, State) => State = (action, command, state) =>
|
||||||
watched.antiEntropy,
|
action match {
|
||||||
shouldTerminate,
|
case Reload =>
|
||||||
logger
|
val continuousCommand = Exec(ContinuousExecutePrefix + command, None)
|
||||||
)
|
state.copy(remainingCommands = continuousCommand +: state.remainingCommands).reload
|
||||||
)
|
case HandleError => state.fail
|
||||||
case Some(eventMonitor) =>
|
case _ => state
|
||||||
printIfDefined(watched watchingMessage eventMonitor.state)
|
}
|
||||||
val triggered = try eventMonitor.awaitEvent()
|
|
||||||
catch {
|
/**
|
||||||
case e: Exception =>
|
* Implements continuous execution. It works by first parsing the command and generating a task to
|
||||||
log.error(
|
* run with each build. It can run multiple commands that are separated by ";" in the command
|
||||||
"Error occurred obtaining files to watch. Terminating continuous execution..."
|
* input. If any of these commands are invalid, the watch will immediately exit.
|
||||||
)
|
* @param state the initial state
|
||||||
s.handleError(e)
|
* @param command the command(s) to repeatedly apply
|
||||||
false
|
* @param setup function to generate a logger and a transformation of the resultant state. The
|
||||||
}
|
* purpose of the transformation is to preserve the logging semantics that existed
|
||||||
if (triggered) {
|
* in the legacy version of this function in which the task would be run through
|
||||||
printIfDefined(watched triggeredMessage eventMonitor.state)
|
* MainLoop.processCommand, which is unavailable in the main-command project
|
||||||
ClearOnFailure :: next :: FailureWall :: repeat :: s
|
* @return the initial state if all of the input commands are valid. Otherwise, returns the
|
||||||
} else {
|
* initial state with the failure transformation.
|
||||||
while (System.in.available() > 0) System.in.read()
|
*/
|
||||||
eventMonitor.close()
|
private[sbt] def executeContinuously(
|
||||||
s.remove(ContinuousEventMonitor)
|
state: State,
|
||||||
}
|
command: String,
|
||||||
|
setup: WatchSetup,
|
||||||
|
): State = withCharBufferedStdIn { in =>
|
||||||
|
val (s0, config, newState) = setup(state, command)
|
||||||
|
val failureCommandName = "SbtContinuousWatchOnFail"
|
||||||
|
val onFail = Command.command(failureCommandName)(identity)
|
||||||
|
val s = (FailureWall :: s0).copy(
|
||||||
|
onFailure = Some(Exec(failureCommandName, None)),
|
||||||
|
definedCommands = s0.definedCommands :+ onFail
|
||||||
|
)
|
||||||
|
val commands = command.split(";") match {
|
||||||
|
case Array("", rest @ _*) => rest
|
||||||
|
case Array(cmd) => Seq(cmd)
|
||||||
|
}
|
||||||
|
val parser = Command.combine(s.definedCommands)(s)
|
||||||
|
val tasks = commands.foldLeft(Nil: Seq[Either[String, () => Either[Exception, Boolean]]]) {
|
||||||
|
(t, cmd) =>
|
||||||
|
t :+ (DefaultParsers.parse(cmd, parser) match {
|
||||||
|
case Right(task) =>
|
||||||
|
Right { () =>
|
||||||
|
try {
|
||||||
|
Right(newState(task).remainingCommands.forall(_.commandLine != failureCommandName))
|
||||||
|
} catch { case e: Exception => Left(e) }
|
||||||
|
}
|
||||||
|
case Left(_) => Left(cmd)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
val (valid, invalid) = tasks.partition(_.isRight)
|
||||||
|
if (invalid.isEmpty) {
|
||||||
|
val task = () =>
|
||||||
|
valid.foldLeft(Right(true): Either[Exception, Boolean]) {
|
||||||
|
case (status, Right(t)) => if (status.getOrElse(true)) t() else status
|
||||||
|
case _ => throw new IllegalStateException("Should be unreachable")
|
||||||
|
}
|
||||||
|
val terminationAction = watch(in, task, config)
|
||||||
|
config.onWatchTerminated(terminationAction, command, state)
|
||||||
|
} else {
|
||||||
|
config.logger.error(
|
||||||
|
s"Terminating watch due to invalid command(s): ${invalid.mkString("'", "', '", "'")}"
|
||||||
|
)
|
||||||
|
state.fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private[sbt] def watch(
|
||||||
|
in: InputStream,
|
||||||
|
task: () => Either[Exception, Boolean],
|
||||||
|
config: WatchConfig
|
||||||
|
): Action = {
|
||||||
|
val logger = config.logger
|
||||||
|
def info(msg: String): Unit = if (msg.nonEmpty) logger.info(msg)
|
||||||
|
|
||||||
|
@tailrec
|
||||||
|
def impl(count: Int): Action = {
|
||||||
|
@tailrec
|
||||||
|
def nextAction(): Action = {
|
||||||
|
config.handleInput(in) match {
|
||||||
|
case action @ (CancelWatch | HandleError | Reload | _: Custom) => action
|
||||||
|
case Trigger => Trigger
|
||||||
|
case _ =>
|
||||||
|
val events = config.fileEventMonitor.poll(10.millis)
|
||||||
|
val next = events match {
|
||||||
|
case Seq() => (Ignore, None)
|
||||||
|
case Seq(head, tail @ _*) =>
|
||||||
|
/*
|
||||||
|
* We traverse all of the events and find the one for which we give the highest
|
||||||
|
* weight.
|
||||||
|
* Custom > HandleError > CancelWatch > Reload > Trigger > Ignore
|
||||||
|
*/
|
||||||
|
tail.foldLeft((config.onWatchEvent(head), Some(head))) {
|
||||||
|
case (current @ (_: Custom, _), _) => current
|
||||||
|
case (current @ (action, _), event) =>
|
||||||
|
config.onWatchEvent(event) match {
|
||||||
|
case HandleError => (HandleError, Some(event))
|
||||||
|
case CancelWatch if action != HandleError => (CancelWatch, Some(event))
|
||||||
|
case Reload if action != HandleError && action != CancelWatch =>
|
||||||
|
(Reload, Some(event))
|
||||||
|
case Trigger if action == Ignore => (Trigger, Some(event))
|
||||||
|
case _ => current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Note that nextAction should never return Ignore.
|
||||||
|
next match {
|
||||||
|
case (action @ (HandleError | CancelWatch | _: Custom), Some(event)) =>
|
||||||
|
val cause =
|
||||||
|
if (action == HandleError) "error"
|
||||||
|
else if (action.isInstanceOf[Custom]) action.toString
|
||||||
|
else "cancellation"
|
||||||
|
logger.debug(s"Stopping watch due to $cause from ${event.entry.typedPath.getPath}")
|
||||||
|
action
|
||||||
|
case (Trigger, Some(event)) =>
|
||||||
|
logger.debug(s"Triggered by ${event.entry.typedPath.getPath}")
|
||||||
|
config.triggeredMessage(event.entry.typedPath, count).foreach(info)
|
||||||
|
Trigger
|
||||||
|
case (Reload, Some(event)) =>
|
||||||
|
logger.info(s"Reload triggered by ${event.entry.typedPath.getPath}")
|
||||||
|
Reload
|
||||||
|
case _ =>
|
||||||
|
nextAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task() match {
|
||||||
|
case Right(status) =>
|
||||||
|
config.preWatch(count, status) match {
|
||||||
|
case Ignore =>
|
||||||
|
config.watchingMessage(count).foreach(info)
|
||||||
|
nextAction() match {
|
||||||
|
case action @ (CancelWatch | HandleError | Reload | _: Custom) => action
|
||||||
|
case _ => impl(count + 1)
|
||||||
|
}
|
||||||
|
case Trigger => impl(count + 1)
|
||||||
|
case action @ (CancelWatch | HandleError | Reload | _: Custom) => action
|
||||||
|
}
|
||||||
|
case Left(e) =>
|
||||||
|
logger.error(s"Terminating watch due to Unexpected error: $e")
|
||||||
|
HandleError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try impl(count = 1)
|
||||||
|
finally config.fileEventMonitor.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@deprecated("Replaced by Watched.command", "1.3.0")
|
||||||
|
def executeContinuously(watched: Watched, s: State, next: String, repeat: String): State =
|
||||||
|
LegacyWatched.executeContinuously(watched, s, next, repeat)
|
||||||
|
|
||||||
|
private[sbt] object NullLogger extends Logger {
|
||||||
|
override def trace(t: => Throwable): Unit = {}
|
||||||
|
override def success(message: => String): Unit = {}
|
||||||
|
override def log(level: Level.Value, message: => String): Unit = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@deprecated("ContinuousEventMonitor attribute is not used by Watched.command", "1.3.0")
|
||||||
val ContinuousEventMonitor =
|
val ContinuousEventMonitor =
|
||||||
AttributeKey[EventMonitor](
|
AttributeKey[EventMonitor](
|
||||||
"watch event monitor",
|
"watch event monitor",
|
||||||
|
|
@ -178,3 +416,155 @@ object Watched {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a number of configuration options for continuous execution.
|
||||||
|
*/
|
||||||
|
trait WatchConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A logger.
|
||||||
|
* @return a logger
|
||||||
|
*/
|
||||||
|
def logger: Logger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sbt.io.FileEventMonitor that is used to monitor the file system.
|
||||||
|
*
|
||||||
|
* @return an sbt.io.FileEventMonitor instance.
|
||||||
|
*/
|
||||||
|
def fileEventMonitor: FileEventMonitor[StampedFile]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that is periodically invoked to determine whether the watch should stop or
|
||||||
|
* trigger. Usually this will read from System.in to react to user input.
|
||||||
|
* @return an [[Watched.Action Action]] that will determine the next step in the watch.
|
||||||
|
*/
|
||||||
|
def handleInput(inputStream: InputStream): Watched.Action
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is run before each watch iteration and if it returns true, the watch is terminated.
|
||||||
|
* @param count The current number of watch iterations.
|
||||||
|
* @param lastStatus true if the previous task execution completed successfully
|
||||||
|
* @return the Action to apply
|
||||||
|
*/
|
||||||
|
def preWatch(count: Int, lastStatus: Boolean): Watched.Action
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback that is invoked whenever a file system vent is detected. The next step of the watch
|
||||||
|
* is determined by the [[Watched.Action Action]] returned by the callback.
|
||||||
|
* @param event the detected sbt.io.FileEventMonitor.Event.
|
||||||
|
* @return the next [[Watched.Action Action]] to run.
|
||||||
|
*/
|
||||||
|
def onWatchEvent(event: Event[StampedFile]): Watched.Action
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the state after the watch terminates.
|
||||||
|
* @param action the [[Watched.Action Action]] that caused the build to terminate
|
||||||
|
* @param command the command that the watch was repeating
|
||||||
|
* @param state the initial state prior to the start of continuous execution
|
||||||
|
* @return the updated state.
|
||||||
|
*/
|
||||||
|
def onWatchTerminated(action: Watched.Action, command: String, state: State): State
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The optional message to log when a build is triggered.
|
||||||
|
* @param typedPath the path that triggered the build
|
||||||
|
* @param count the current iteration
|
||||||
|
* @return an optional log message.
|
||||||
|
*/
|
||||||
|
def triggeredMessage(typedPath: TypedPath, count: Int): Option[String]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The optional message to log before each watch iteration.
|
||||||
|
* @param count the current iteration
|
||||||
|
* @return an optional log message.
|
||||||
|
*/
|
||||||
|
def watchingMessage(count: Int): Option[String]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a default implementation of [[WatchConfig]].
|
||||||
|
*/
|
||||||
|
object WatchConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of [[WatchConfig]].
|
||||||
|
* @param logger logger for watch events
|
||||||
|
* @param fileEventMonitor the monitor for file system events.
|
||||||
|
* @param handleInput callback that is periodically invoked to check whether to continue or
|
||||||
|
* terminate the watch based on user input. It is also possible to, for
|
||||||
|
* example time out the watch using this callback.
|
||||||
|
* @param preWatch callback to invoke before waiting for updates from the sbt.io.FileEventMonitor.
|
||||||
|
* The input parameters are the current iteration count and whether or not
|
||||||
|
* the last invocation of the command was successful. Typical uses would be to
|
||||||
|
* terminate the watch after a fixed number of iterations or to terminate the
|
||||||
|
* watch if the command was unsuccessful.
|
||||||
|
* @param onWatchEvent callback that is invoked when
|
||||||
|
* @param onWatchTerminated callback that is invoked to update the state after the watch
|
||||||
|
* terminates.
|
||||||
|
* @param triggeredMessage optional message that will be logged when a new build is triggered.
|
||||||
|
* The input parameters are the sbt.io.TypedPath that triggered the new
|
||||||
|
* build and the current iteration count.
|
||||||
|
* @param watchingMessage optional message that is printed before each watch iteration begins.
|
||||||
|
* The input parameter is the current iteration count.
|
||||||
|
* @return a [[WatchConfig]] instance.
|
||||||
|
*/
|
||||||
|
def default(
|
||||||
|
logger: Logger,
|
||||||
|
fileEventMonitor: FileEventMonitor[StampedFile],
|
||||||
|
handleInput: InputStream => Watched.Action,
|
||||||
|
preWatch: (Int, Boolean) => Watched.Action,
|
||||||
|
onWatchEvent: Event[StampedFile] => Watched.Action,
|
||||||
|
onWatchTerminated: (Watched.Action, String, State) => State,
|
||||||
|
triggeredMessage: (TypedPath, Int) => Option[String],
|
||||||
|
watchingMessage: Int => Option[String]
|
||||||
|
): WatchConfig = {
|
||||||
|
val l = logger
|
||||||
|
val fem = fileEventMonitor
|
||||||
|
val hi = handleInput
|
||||||
|
val pw = preWatch
|
||||||
|
val owe = onWatchEvent
|
||||||
|
val owt = onWatchTerminated
|
||||||
|
val tm = triggeredMessage
|
||||||
|
val wm = watchingMessage
|
||||||
|
new WatchConfig {
|
||||||
|
override def logger: Logger = l
|
||||||
|
override def fileEventMonitor: FileEventMonitor[StampedFile] = fem
|
||||||
|
override def handleInput(inputStream: InputStream): Watched.Action = hi(inputStream)
|
||||||
|
override def preWatch(count: Int, lastResult: Boolean): Watched.Action =
|
||||||
|
pw(count, lastResult)
|
||||||
|
override def onWatchEvent(event: Event[StampedFile]): Watched.Action = owe(event)
|
||||||
|
override def onWatchTerminated(action: Watched.Action, command: String, state: State): State =
|
||||||
|
owt(action, command, state)
|
||||||
|
override def triggeredMessage(typedPath: TypedPath, count: Int): Option[String] =
|
||||||
|
tm(typedPath, count)
|
||||||
|
override def watchingMessage(count: Int): Option[String] = wm(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait StampedFile extends File {
|
||||||
|
def stamp: Stamp
|
||||||
|
}
|
||||||
|
object StampedFile {
|
||||||
|
val sourceConverter: TypedPath => StampedFile =
|
||||||
|
new StampedFileImpl(_: TypedPath, forceLastModified = false)
|
||||||
|
val binaryConverter: TypedPath => StampedFile =
|
||||||
|
new StampedFileImpl(_: TypedPath, forceLastModified = true)
|
||||||
|
val converter: TypedPath => StampedFile = (tp: TypedPath) =>
|
||||||
|
tp.getPath.toString match {
|
||||||
|
case s if s.endsWith(".jar") => binaryConverter(tp)
|
||||||
|
case s if s.endsWith(".class") => binaryConverter(tp)
|
||||||
|
case _ => sourceConverter(tp)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class StampedFileImpl(typedPath: TypedPath, forceLastModified: Boolean)
|
||||||
|
extends java.io.File(typedPath.getPath.toString)
|
||||||
|
with StampedFile {
|
||||||
|
override val stamp: Stamp =
|
||||||
|
if (forceLastModified || typedPath.isDirectory)
|
||||||
|
Stamper.forLastModified(typedPath.getPath.toFile)
|
||||||
|
else Stamper.forHash(typedPath.getPath.toFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* sbt
|
||||||
|
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||||
|
* Copyright 2008 - 2010, Mark Harrah
|
||||||
|
* Licensed under Apache License 2.0 (see LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sbt.internal
|
||||||
|
|
||||||
|
import sbt.BasicCommandStrings.{ ClearOnFailure, FailureWall }
|
||||||
|
import sbt.internal.io.{ EventMonitor, WatchState }
|
||||||
|
import sbt.{ State, Watched }
|
||||||
|
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
import Watched.ContinuousEventMonitor
|
||||||
|
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
|
||||||
|
private[sbt] object LegacyWatched {
|
||||||
|
@deprecated("Replaced by Watched.command", "1.3.0")
|
||||||
|
def executeContinuously(watched: Watched, s: State, next: String, repeat: String): State = {
|
||||||
|
@tailrec def shouldTerminate: Boolean =
|
||||||
|
(System.in.available > 0) && (watched.terminateWatch(System.in.read()) || shouldTerminate)
|
||||||
|
val log = s.log
|
||||||
|
val logger = new EventMonitor.Logger {
|
||||||
|
override def debug(msg: => Any): Unit = log.debug(msg.toString)
|
||||||
|
}
|
||||||
|
s get ContinuousEventMonitor match {
|
||||||
|
case None =>
|
||||||
|
// This is the first iteration, so run the task and create a new EventMonitor
|
||||||
|
(ClearOnFailure :: next :: FailureWall :: repeat :: s)
|
||||||
|
.put(
|
||||||
|
ContinuousEventMonitor,
|
||||||
|
EventMonitor(
|
||||||
|
WatchState.empty(watched.watchService(), watched.watchSources(s)),
|
||||||
|
watched.pollInterval,
|
||||||
|
watched.antiEntropy,
|
||||||
|
shouldTerminate,
|
||||||
|
logger
|
||||||
|
)
|
||||||
|
)
|
||||||
|
case Some(eventMonitor) =>
|
||||||
|
Watched.printIfDefined(watched watchingMessage eventMonitor.state)
|
||||||
|
val triggered = try eventMonitor.awaitEvent()
|
||||||
|
catch {
|
||||||
|
case NonFatal(e) =>
|
||||||
|
log.error(
|
||||||
|
"Error occurred obtaining files to watch. Terminating continuous execution..."
|
||||||
|
)
|
||||||
|
s.handleError(e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
if (triggered) {
|
||||||
|
Watched.printIfDefined(watched triggeredMessage eventMonitor.state)
|
||||||
|
ClearOnFailure :: next :: FailureWall :: repeat :: s
|
||||||
|
} else {
|
||||||
|
while (System.in.available() > 0) System.in.read()
|
||||||
|
eventMonitor.close()
|
||||||
|
s.remove(ContinuousEventMonitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* 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, InputStream }
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
import org.scalatest.{ FlatSpec, Matchers }
|
||||||
|
import sbt.Watched._
|
||||||
|
import sbt.WatchedSpec._
|
||||||
|
import sbt.io.FileEventMonitor.Event
|
||||||
|
import sbt.io.{ FileEventMonitor, IO, TypedPath }
|
||||||
|
import sbt.util.Logger
|
||||||
|
|
||||||
|
import scala.collection.mutable
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
|
class WatchedSpec extends FlatSpec with Matchers {
|
||||||
|
object Defaults {
|
||||||
|
private val fileTreeViewConfig = FileTreeViewConfig.default(50.millis)
|
||||||
|
def config(
|
||||||
|
sources: Seq[WatchSource],
|
||||||
|
fileEventMonitor: Option[FileEventMonitor[StampedFile]] = None,
|
||||||
|
logger: Logger = NullLogger,
|
||||||
|
handleInput: InputStream => Action = _ => Ignore,
|
||||||
|
preWatch: (Int, Boolean) => Action = (_, _) => CancelWatch,
|
||||||
|
onWatchEvent: Event[StampedFile] => Action = _ => Ignore,
|
||||||
|
triggeredMessage: (TypedPath, Int) => Option[String] = (_, _) => None,
|
||||||
|
watchingMessage: Int => Option[String] = _ => None
|
||||||
|
): WatchConfig = {
|
||||||
|
val monitor = fileEventMonitor.getOrElse(
|
||||||
|
fileTreeViewConfig.newMonitor(fileTreeViewConfig.newDataView(), sources, logger)
|
||||||
|
)
|
||||||
|
WatchConfig.default(
|
||||||
|
logger = logger,
|
||||||
|
monitor,
|
||||||
|
handleInput,
|
||||||
|
preWatch,
|
||||||
|
onWatchEvent,
|
||||||
|
(_, _, state) => state,
|
||||||
|
triggeredMessage,
|
||||||
|
watchingMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object NullInputStream extends InputStream {
|
||||||
|
override def available(): Int = 0
|
||||||
|
override def read(): Int = -1
|
||||||
|
}
|
||||||
|
"Watched.watch" should "stop" in IO.withTemporaryDirectory { dir =>
|
||||||
|
val config = Defaults.config(sources = Seq(WatchSource(dir.toRealPath)))
|
||||||
|
Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch
|
||||||
|
}
|
||||||
|
it should "trigger" in IO.withTemporaryDirectory { dir =>
|
||||||
|
val triggered = new AtomicBoolean(false)
|
||||||
|
val config = Defaults.config(
|
||||||
|
sources = Seq(WatchSource(dir.toRealPath)),
|
||||||
|
preWatch = (count, _) => if (count == 2) CancelWatch else Ignore,
|
||||||
|
onWatchEvent = _ => { triggered.set(true); Trigger },
|
||||||
|
watchingMessage = _ => {
|
||||||
|
new File(dir, "file").createNewFile; None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch
|
||||||
|
assert(triggered.get())
|
||||||
|
}
|
||||||
|
it should "filter events" in IO.withTemporaryDirectory { dir =>
|
||||||
|
val realDir = dir.toRealPath
|
||||||
|
val queue = new mutable.Queue[TypedPath]
|
||||||
|
val foo = realDir.toPath.resolve("foo")
|
||||||
|
val bar = realDir.toPath.resolve("bar")
|
||||||
|
val config = Defaults.config(
|
||||||
|
sources = Seq(WatchSource(realDir)),
|
||||||
|
preWatch = (count, _) => if (count == 2) CancelWatch else Ignore,
|
||||||
|
onWatchEvent = e => if (e.entry.typedPath.getPath == foo) Trigger else Ignore,
|
||||||
|
triggeredMessage = (tp, _) => { queue += tp; None },
|
||||||
|
watchingMessage = _ => { Files.createFile(bar); Thread.sleep(5); Files.createFile(foo); None }
|
||||||
|
)
|
||||||
|
Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch
|
||||||
|
queue.toIndexedSeq.map(_.getPath) shouldBe Seq(foo)
|
||||||
|
}
|
||||||
|
it should "enforce anti-entropy" in IO.withTemporaryDirectory { dir =>
|
||||||
|
val realDir = dir.toRealPath
|
||||||
|
val queue = new mutable.Queue[TypedPath]
|
||||||
|
val foo = realDir.toPath.resolve("foo")
|
||||||
|
val bar = realDir.toPath.resolve("bar")
|
||||||
|
val config = Defaults.config(
|
||||||
|
sources = Seq(WatchSource(realDir)),
|
||||||
|
preWatch = (count, _) => if (count == 3) CancelWatch else Ignore,
|
||||||
|
onWatchEvent = _ => Trigger,
|
||||||
|
triggeredMessage = (tp, _) => { queue += tp; None },
|
||||||
|
watchingMessage = count => {
|
||||||
|
count match {
|
||||||
|
case 1 => Files.createFile(bar)
|
||||||
|
case 2 =>
|
||||||
|
bar.toFile.setLastModified(5000)
|
||||||
|
Files.createFile(foo)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch
|
||||||
|
queue.toIndexedSeq.map(_.getPath) shouldBe Seq(bar, foo)
|
||||||
|
}
|
||||||
|
it should "halt on error" in IO.withTemporaryDirectory { dir =>
|
||||||
|
val halted = new AtomicBoolean(false)
|
||||||
|
val config = Defaults.config(
|
||||||
|
sources = Seq(WatchSource(dir.toRealPath)),
|
||||||
|
preWatch = (_, lastStatus) => if (lastStatus) Ignore else { halted.set(true); HandleError }
|
||||||
|
)
|
||||||
|
Watched.watch(NullInputStream, () => Right(false), config) shouldBe HandleError
|
||||||
|
assert(halted.get())
|
||||||
|
}
|
||||||
|
it should "reload" in IO.withTemporaryDirectory { dir =>
|
||||||
|
val config = Defaults.config(
|
||||||
|
sources = Seq(WatchSource(dir.toRealPath)),
|
||||||
|
preWatch = (_, _) => Ignore,
|
||||||
|
onWatchEvent = _ => Reload,
|
||||||
|
watchingMessage = _ => { new File(dir, "file").createNewFile(); None }
|
||||||
|
)
|
||||||
|
Watched.watch(NullInputStream, () => Right(true), config) shouldBe Reload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object WatchedSpec {
|
||||||
|
implicit class FileOps(val f: File) {
|
||||||
|
def toRealPath: File = f.toPath.toRealPath().toFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,16 +40,18 @@ import sbt.internal.util.Types._
|
||||||
import sbt.io.syntax._
|
import sbt.io.syntax._
|
||||||
import sbt.io.{
|
import sbt.io.{
|
||||||
AllPassFilter,
|
AllPassFilter,
|
||||||
|
DirectoryFilter,
|
||||||
FileFilter,
|
FileFilter,
|
||||||
|
FileTreeView,
|
||||||
GlobFilter,
|
GlobFilter,
|
||||||
|
Hash,
|
||||||
HiddenFileFilter,
|
HiddenFileFilter,
|
||||||
IO,
|
IO,
|
||||||
NameFilter,
|
NameFilter,
|
||||||
NothingFilter,
|
NothingFilter,
|
||||||
Path,
|
Path,
|
||||||
PathFinder,
|
PathFinder,
|
||||||
DirectoryFilter,
|
TypedPath
|
||||||
Hash
|
|
||||||
}, Path._
|
}, Path._
|
||||||
import sbt.librarymanagement.Artifact.{ DocClassifier, SourceClassifier }
|
import sbt.librarymanagement.Artifact.{ DocClassifier, SourceClassifier }
|
||||||
import sbt.librarymanagement.Configurations.{
|
import sbt.librarymanagement.Configurations.{
|
||||||
|
|
@ -248,7 +250,9 @@ object Defaults extends BuildCommon {
|
||||||
extraLoggers :== { _ =>
|
extraLoggers :== { _ =>
|
||||||
Nil
|
Nil
|
||||||
},
|
},
|
||||||
|
pollingDirectories :== Nil,
|
||||||
watchSources :== Nil,
|
watchSources :== Nil,
|
||||||
|
watchProjectSources :== Nil,
|
||||||
skip :== false,
|
skip :== false,
|
||||||
taskTemporaryDirectory := { val dir = IO.createTemporaryDirectory; dir.deleteOnExit(); dir },
|
taskTemporaryDirectory := { val dir = IO.createTemporaryDirectory; dir.deleteOnExit(); dir },
|
||||||
onComplete := {
|
onComplete := {
|
||||||
|
|
@ -264,7 +268,21 @@ object Defaults extends BuildCommon {
|
||||||
concurrentRestrictions := defaultRestrictions.value,
|
concurrentRestrictions := defaultRestrictions.value,
|
||||||
parallelExecution :== true,
|
parallelExecution :== true,
|
||||||
pollInterval :== new FiniteDuration(500, TimeUnit.MILLISECONDS),
|
pollInterval :== new FiniteDuration(500, TimeUnit.MILLISECONDS),
|
||||||
watchAntiEntropy :== new FiniteDuration(40, TimeUnit.MILLISECONDS),
|
watchTriggeredMessage := { (_, _) =>
|
||||||
|
None
|
||||||
|
},
|
||||||
|
watchStartMessage := Watched.defaultStartWatch,
|
||||||
|
fileTreeViewConfig := FileManagement.defaultFileTreeView.value,
|
||||||
|
fileTreeView := state.value
|
||||||
|
.get(BasicKeys.globalFileTreeView)
|
||||||
|
.getOrElse(FileTreeView.DEFAULT.asDataView(StampedFile.converter)),
|
||||||
|
externalHooks := {
|
||||||
|
val view = fileTreeView.value
|
||||||
|
compileOptions =>
|
||||||
|
Some(ExternalHooks(compileOptions, view))
|
||||||
|
},
|
||||||
|
watchAntiEntropy :== new FiniteDuration(500, TimeUnit.MILLISECONDS),
|
||||||
|
watchLogger := streams.value.log,
|
||||||
watchService :== { () =>
|
watchService :== { () =>
|
||||||
Watched.createWatchService()
|
Watched.createWatchService()
|
||||||
},
|
},
|
||||||
|
|
@ -351,12 +369,14 @@ object Defaults extends BuildCommon {
|
||||||
crossPaths.value
|
crossPaths.value
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
unmanagedSources := collectFiles(
|
unmanagedSources := FileManagement
|
||||||
unmanagedSourceDirectories,
|
.collectFiles(
|
||||||
includeFilter in unmanagedSources,
|
unmanagedSourceDirectories,
|
||||||
excludeFilter in unmanagedSources
|
includeFilter in unmanagedSources,
|
||||||
).value,
|
excludeFilter in unmanagedSources
|
||||||
watchSources in ConfigGlobal ++= {
|
)
|
||||||
|
.value,
|
||||||
|
watchSources in ConfigGlobal := (watchSources in ConfigGlobal).value ++ {
|
||||||
val baseDir = baseDirectory.value
|
val baseDir = baseDirectory.value
|
||||||
val bases = unmanagedSourceDirectories.value
|
val bases = unmanagedSourceDirectories.value
|
||||||
val include = (includeFilter in unmanagedSources).value
|
val include = (includeFilter in unmanagedSources).value
|
||||||
|
|
@ -377,6 +397,13 @@ object Defaults extends BuildCommon {
|
||||||
else Nil
|
else Nil
|
||||||
bases.map(b => new Source(b, include, exclude)) ++ baseSources
|
bases.map(b => new Source(b, include, exclude)) ++ baseSources
|
||||||
},
|
},
|
||||||
|
watchProjectSources in ConfigGlobal := (watchProjectSources in ConfigGlobal).value ++ {
|
||||||
|
val baseDir = baseDirectory.value
|
||||||
|
Seq(
|
||||||
|
new Source(baseDir, "*.sbt", HiddenFileFilter, recursive = false),
|
||||||
|
new Source(baseDir / "project", "*.sbt" || "*.scala", HiddenFileFilter, recursive = true)
|
||||||
|
)
|
||||||
|
},
|
||||||
managedSourceDirectories := Seq(sourceManaged.value),
|
managedSourceDirectories := Seq(sourceManaged.value),
|
||||||
managedSources := generate(sourceGenerators).value,
|
managedSources := generate(sourceGenerators).value,
|
||||||
sourceGenerators :== Nil,
|
sourceGenerators :== Nil,
|
||||||
|
|
@ -393,12 +420,14 @@ object Defaults extends BuildCommon {
|
||||||
resourceDirectories := Classpaths
|
resourceDirectories := Classpaths
|
||||||
.concatSettings(unmanagedResourceDirectories, managedResourceDirectories)
|
.concatSettings(unmanagedResourceDirectories, managedResourceDirectories)
|
||||||
.value,
|
.value,
|
||||||
unmanagedResources := collectFiles(
|
unmanagedResources := FileManagement
|
||||||
unmanagedResourceDirectories,
|
.collectFiles(
|
||||||
includeFilter in unmanagedResources,
|
unmanagedResourceDirectories,
|
||||||
excludeFilter in unmanagedResources
|
includeFilter in unmanagedResources,
|
||||||
).value,
|
excludeFilter in unmanagedResources
|
||||||
watchSources in ConfigGlobal ++= {
|
)
|
||||||
|
.value,
|
||||||
|
watchSources in ConfigGlobal := (watchSources in ConfigGlobal).value ++ {
|
||||||
val bases = unmanagedResourceDirectories.value
|
val bases = unmanagedResourceDirectories.value
|
||||||
val include = (includeFilter in unmanagedResources).value
|
val include = (includeFilter in unmanagedResources).value
|
||||||
val exclude = (excludeFilter in unmanagedResources).value
|
val exclude = (excludeFilter in unmanagedResources).value
|
||||||
|
|
@ -411,19 +440,11 @@ object Defaults extends BuildCommon {
|
||||||
managedResources := generate(resourceGenerators).value,
|
managedResources := generate(resourceGenerators).value,
|
||||||
resources := Classpaths.concat(managedResources, unmanagedResources).value
|
resources := Classpaths.concat(managedResources, unmanagedResources).value
|
||||||
)
|
)
|
||||||
|
def addBaseSources = FileManagement.appendBaseSources
|
||||||
lazy val outputConfigPaths = Seq(
|
lazy val outputConfigPaths = Seq(
|
||||||
classDirectory := crossTarget.value / (prefix(configuration.value.name) + "classes"),
|
classDirectory := crossTarget.value / (prefix(configuration.value.name) + "classes"),
|
||||||
target in doc := crossTarget.value / (prefix(configuration.value.name) + "api")
|
target in doc := crossTarget.value / (prefix(configuration.value.name) + "api")
|
||||||
)
|
)
|
||||||
def addBaseSources = Seq(
|
|
||||||
unmanagedSources := {
|
|
||||||
val srcs = unmanagedSources.value
|
|
||||||
val f = (includeFilter in unmanagedSources).value
|
|
||||||
val excl = (excludeFilter in unmanagedSources).value
|
|
||||||
val baseDir = baseDirectory.value
|
|
||||||
if (sourcesInBase.value) (srcs +++ baseDir * (f -- excl)).get else srcs
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is included into JvmPlugin.projectSettings
|
// This is included into JvmPlugin.projectSettings
|
||||||
def compileBase = inTask(console)(compilersSetting :: Nil) ++ compileBaseGlobal ++ Seq(
|
def compileBase = inTask(console)(compilersSetting :: Nil) ++ compileBaseGlobal ++ Seq(
|
||||||
|
|
@ -599,17 +620,62 @@ object Defaults extends BuildCommon {
|
||||||
clean := (Def.task { IO.delete(cleanFiles.value) } tag (Tags.Clean)).value,
|
clean := (Def.task { IO.delete(cleanFiles.value) } tag (Tags.Clean)).value,
|
||||||
consoleProject := consoleProjectTask.value,
|
consoleProject := consoleProjectTask.value,
|
||||||
watchTransitiveSources := watchTransitiveSourcesTask.value,
|
watchTransitiveSources := watchTransitiveSourcesTask.value,
|
||||||
watchingMessage := Watched.projectWatchingMessage(thisProjectRef.value.project),
|
watchProjectTransitiveSources := watchTransitiveSourcesTaskImpl(watchProjectSources).value,
|
||||||
watch := watchSetting.value
|
watchOnEvent := {
|
||||||
|
val sources = watchTransitiveSources.value
|
||||||
|
val projectSources = watchProjectTransitiveSources.value
|
||||||
|
e =>
|
||||||
|
if (sources.exists(_.accept(e.entry.typedPath.getPath))) Watched.Trigger
|
||||||
|
else if (projectSources.exists(_.accept(e.entry.typedPath.getPath))) Watched.Reload
|
||||||
|
else Watched.Ignore
|
||||||
|
},
|
||||||
|
watchHandleInput := Watched.handleInput,
|
||||||
|
watchPreWatch := { (_, _) =>
|
||||||
|
Watched.Ignore
|
||||||
|
},
|
||||||
|
watchOnTermination := Watched.onTermination,
|
||||||
|
watchConfig := {
|
||||||
|
val sources = watchTransitiveSources.value ++ watchProjectTransitiveSources.value
|
||||||
|
val extracted = Project.extract(state.value)
|
||||||
|
val wm = extracted
|
||||||
|
.getOpt(watchingMessage)
|
||||||
|
.map(w => (count: Int) => Some(w(WatchState.empty(sources).withCount(count))))
|
||||||
|
.getOrElse(watchStartMessage.value)
|
||||||
|
val tm = extracted
|
||||||
|
.getOpt(triggeredMessage)
|
||||||
|
.map(
|
||||||
|
tm => (_: TypedPath, count: Int) => Some(tm(WatchState.empty(sources).withCount(count)))
|
||||||
|
)
|
||||||
|
.getOrElse(watchTriggeredMessage.value)
|
||||||
|
val logger = watchLogger.value
|
||||||
|
WatchConfig.default(
|
||||||
|
logger,
|
||||||
|
fileTreeViewConfig.value.newMonitor(fileTreeView.value, sources, logger),
|
||||||
|
watchHandleInput.value,
|
||||||
|
watchPreWatch.value,
|
||||||
|
watchOnEvent.value,
|
||||||
|
watchOnTermination.value,
|
||||||
|
tm,
|
||||||
|
wm
|
||||||
|
)
|
||||||
|
},
|
||||||
|
watchStartMessage := Watched.projectOnWatchMessage(thisProjectRef.value.project),
|
||||||
|
watch := watchSetting.value,
|
||||||
|
fileTreeViewConfig := FileManagement.defaultFileTreeView.value
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate(generators: SettingKey[Seq[Task[Seq[File]]]]): Initialize[Task[Seq[File]]] =
|
def generate(generators: SettingKey[Seq[Task[Seq[File]]]]): Initialize[Task[Seq[File]]] =
|
||||||
generators { _.join.map(_.flatten) }
|
generators { _.join.map(_.flatten) }
|
||||||
|
|
||||||
def watchTransitiveSourcesTask: Initialize[Task[Seq[Source]]] = {
|
def watchTransitiveSourcesTask: Initialize[Task[Seq[Source]]] =
|
||||||
|
watchTransitiveSourcesTaskImpl(watchSources)
|
||||||
|
|
||||||
|
private def watchTransitiveSourcesTaskImpl(
|
||||||
|
key: TaskKey[Seq[Source]]
|
||||||
|
): Initialize[Task[Seq[Source]]] = {
|
||||||
import ScopeFilter.Make.{ inDependencies => inDeps, _ }
|
import ScopeFilter.Make.{ inDependencies => inDeps, _ }
|
||||||
val selectDeps = ScopeFilter(inAggregates(ThisProject) || inDeps(ThisProject))
|
val selectDeps = ScopeFilter(inAggregates(ThisProject) || inDeps(ThisProject))
|
||||||
val allWatched = (watchSources ?? Nil).all(selectDeps)
|
val allWatched = (key ?? Nil).all(selectDeps)
|
||||||
Def.task { allWatched.value.flatten }
|
Def.task { allWatched.value.flatten }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -621,6 +687,7 @@ object Defaults extends BuildCommon {
|
||||||
Def.task { allUpdates.value.flatten ++ globalPluginUpdate.?.value }
|
Def.task { allUpdates.value.flatten ++ globalPluginUpdate.?.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@deprecated("This is no longer used to implement continuous execution", "1.3.0")
|
||||||
def watchSetting: Initialize[Watched] =
|
def watchSetting: Initialize[Watched] =
|
||||||
Def.setting {
|
Def.setting {
|
||||||
val getService = watchService.value
|
val getService = watchService.value
|
||||||
|
|
@ -1127,10 +1194,7 @@ object Defaults extends BuildCommon {
|
||||||
dirs: ScopedTaskable[Seq[File]],
|
dirs: ScopedTaskable[Seq[File]],
|
||||||
filter: ScopedTaskable[FileFilter],
|
filter: ScopedTaskable[FileFilter],
|
||||||
excludes: ScopedTaskable[FileFilter]
|
excludes: ScopedTaskable[FileFilter]
|
||||||
): Initialize[Task[Seq[File]]] =
|
): Initialize[Task[Seq[File]]] = FileManagement.collectFiles(dirs, filter, excludes)
|
||||||
Def.task {
|
|
||||||
dirs.toTask.value.descendantsExcept(filter.toTask.value, excludes.toTask.value).get
|
|
||||||
}
|
|
||||||
def artifactPathSetting(art: SettingKey[Artifact]): Initialize[File] =
|
def artifactPathSetting(art: SettingKey[Artifact]): Initialize[File] =
|
||||||
Def.setting {
|
Def.setting {
|
||||||
val f = artifactName.value
|
val f = artifactName.value
|
||||||
|
|
@ -1587,12 +1651,22 @@ object Defaults extends BuildCommon {
|
||||||
foldMappers(sourcePositionMappers.value)
|
foldMappers(sourcePositionMappers.value)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
compileInputs := Inputs.of(
|
compileInputs := {
|
||||||
compilers.value,
|
val options = compileOptions.value
|
||||||
compileOptions.value,
|
val setup = compileIncSetup.value
|
||||||
compileIncSetup.value,
|
Inputs.of(
|
||||||
previousCompile.value
|
compilers.value,
|
||||||
)
|
options,
|
||||||
|
externalHooks
|
||||||
|
.value(options)
|
||||||
|
.map { hooks =>
|
||||||
|
val newOptions = setup.incrementalCompilerOptions.withExternalHooks(hooks)
|
||||||
|
setup.withIncrementalCompilerOptions(newOptions)
|
||||||
|
}
|
||||||
|
.getOrElse(setup),
|
||||||
|
previousCompile.value
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1702,7 +1776,7 @@ object Defaults extends BuildCommon {
|
||||||
|
|
||||||
lazy val compileSettings: Seq[Setting[_]] =
|
lazy val compileSettings: Seq[Setting[_]] =
|
||||||
configSettings ++
|
configSettings ++
|
||||||
(mainBgRunMainTask +: mainBgRunTask +: addBaseSources) ++
|
(mainBgRunMainTask +: mainBgRunTask +: FileManagement.appendBaseSources) ++
|
||||||
Classpaths.addUnmanagedLibrary
|
Classpaths.addUnmanagedLibrary
|
||||||
|
|
||||||
lazy val testSettings: Seq[Setting[_]] = configSettings ++ testTasks
|
lazy val testSettings: Seq[Setting[_]] = configSettings ++ testTasks
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
package sbt
|
package sbt
|
||||||
|
|
||||||
import java.io.File
|
import java.io.{ File, InputStream }
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import scala.concurrent.duration.{ FiniteDuration, Duration }
|
import scala.concurrent.duration.{ FiniteDuration, Duration }
|
||||||
import Def.ScopedKey
|
import Def.ScopedKey
|
||||||
|
|
@ -20,6 +20,7 @@ import xsbti.compile.{
|
||||||
CompileOrder,
|
CompileOrder,
|
||||||
Compilers,
|
Compilers,
|
||||||
CompileResult,
|
CompileResult,
|
||||||
|
ExternalHooks,
|
||||||
GlobalsCache,
|
GlobalsCache,
|
||||||
IncOptions,
|
IncOptions,
|
||||||
Inputs,
|
Inputs,
|
||||||
|
|
@ -40,7 +41,8 @@ import sbt.internal.{
|
||||||
SessionSettings,
|
SessionSettings,
|
||||||
LogManager
|
LogManager
|
||||||
}
|
}
|
||||||
import sbt.io.{ FileFilter, WatchService }
|
import sbt.io.{ FileFilter, FileTreeDataView, TypedPath, WatchService }
|
||||||
|
import sbt.io.FileEventMonitor.Event
|
||||||
import sbt.internal.io.WatchState
|
import sbt.internal.io.WatchState
|
||||||
import sbt.internal.server.ServerHandler
|
import sbt.internal.server.ServerHandler
|
||||||
import sbt.internal.util.{ AttributeKey, SourcePosition }
|
import sbt.internal.util.{ AttributeKey, SourcePosition }
|
||||||
|
|
@ -141,15 +143,31 @@ object Keys {
|
||||||
val serverHandlers = settingKey[Seq[ServerHandler]]("User-defined server handlers.")
|
val serverHandlers = settingKey[Seq[ServerHandler]]("User-defined server handlers.")
|
||||||
|
|
||||||
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)
|
||||||
|
@deprecated("This is no longer used for continuous execution", "1.3.0")
|
||||||
val watch = SettingKey(BasicKeys.watch)
|
val watch = SettingKey(BasicKeys.watch)
|
||||||
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 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 fileTreeView = taskKey[FileTreeDataView[StampedFile]]("A view of the file system")
|
||||||
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 pollingDirectories = settingKey[Seq[Watched.WatchSource]]("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 watchConfig = taskKey[WatchConfig]("The configuration for continuous execution.").withRank(BMinusSetting)
|
||||||
|
val watchLogger = taskKey[Logger]("A logger that reports watch events.").withRank(DSetting)
|
||||||
|
val watchHandleInput = settingKey[InputStream => Watched.Action]("Function that is periodically invoked to determine if the continous build should be stopped or if a build should be triggered. It will usually read from stdin to respond to user commands.").withRank(BMinusSetting)
|
||||||
|
val watchOnEvent = taskKey[Event[StampedFile] => Watched.Action]("Determines how to handle a file event").withRank(BMinusSetting)
|
||||||
|
val watchOnTermination = taskKey[(Watched.Action, String, State) => State]("Transforms the input state after the continuous build completes.").withRank(BMinusSetting)
|
||||||
val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting)
|
val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting)
|
||||||
|
val watchProjectSources = taskKey[Seq[Watched.WatchSource]]("Defines the sources for the sbt meta project to watch to trigger a reload.").withRank(CSetting)
|
||||||
|
val watchProjectTransitiveSources = taskKey[Seq[Watched.WatchSource]]("Defines the sources in all projects for the sbt meta project to watch to trigger a reload.").withRank(CSetting)
|
||||||
|
val watchPreWatch = settingKey[(Int, Boolean) => Watched.Action]("Function that may terminate a continuous build based on the number of iterations and the last result").withRank(BMinusSetting)
|
||||||
val watchSources = taskKey[Seq[Watched.WatchSource]]("Defines the sources in this project for continuous execution to watch for changes.").withRank(BMinusSetting)
|
val watchSources = taskKey[Seq[Watched.WatchSource]]("Defines the sources in this project for continuous execution to watch for changes.").withRank(BMinusSetting)
|
||||||
|
val watchStartMessage = settingKey[Int => Option[String]]("The message to show when triggered execution waits for sources to change. The parameter is the current watch iteration count.").withRank(DSetting)
|
||||||
val watchTransitiveSources = taskKey[Seq[Watched.WatchSource]]("Defines the sources in all projects for continuous execution to watch.").withRank(CSetting)
|
val watchTransitiveSources = taskKey[Seq[Watched.WatchSource]]("Defines the sources in all projects for continuous execution to watch.").withRank(CSetting)
|
||||||
|
val watchTriggeredMessage = settingKey[(TypedPath, Int) => 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("Use watchStartMessage instead", "1.3.0")
|
||||||
val watchingMessage = settingKey[WatchState => String]("The message to show when triggered execution waits for sources to change.").withRank(DSetting)
|
val watchingMessage = settingKey[WatchState => String]("The message to show when triggered execution waits for sources to change.").withRank(DSetting)
|
||||||
|
@deprecated("Use watchTriggeredMessage instead", "1.3.0")
|
||||||
val triggeredMessage = settingKey[WatchState => String]("The message to show before triggered execution executes an action after sources change.").withRank(DSetting)
|
val triggeredMessage = settingKey[WatchState => String]("The message to show before triggered execution executes an action after sources change.").withRank(DSetting)
|
||||||
|
val fileTreeViewConfig = taskKey[FileTreeViewConfig]("Configures how sbt will traverse and monitor the file system.").withRank(BMinusSetting)
|
||||||
|
|
||||||
// Path Keys
|
// Path Keys
|
||||||
val baseDirectory = settingKey[File]("The base directory. Depending on the scope, this is the base directory for the build, project, configuration, or task.").withRank(AMinusSetting)
|
val baseDirectory = settingKey[File]("The base directory. Depending on the scope, this is the base directory for the build, project, configuration, or task.").withRank(AMinusSetting)
|
||||||
|
|
@ -244,6 +262,7 @@ object Keys {
|
||||||
val copyResources = taskKey[Seq[(File, File)]]("Copies resources to the output directory.").withRank(AMinusTask)
|
val copyResources = taskKey[Seq[(File, File)]]("Copies resources to the output directory.").withRank(AMinusTask)
|
||||||
val aggregate = settingKey[Boolean]("Configures task aggregation.").withRank(BMinusSetting)
|
val aggregate = settingKey[Boolean]("Configures task aggregation.").withRank(BMinusSetting)
|
||||||
val sourcePositionMappers = taskKey[Seq[xsbti.Position => Option[xsbti.Position]]]("Maps positions in generated source files to the original source it was generated from").withRank(DTask)
|
val sourcePositionMappers = taskKey[Seq[xsbti.Position => Option[xsbti.Position]]]("Maps positions in generated source files to the original source it was generated from").withRank(DTask)
|
||||||
|
val externalHooks = taskKey[CompileOptions => Option[ExternalHooks]]("External hooks for modifying the internal behavior of the incremental compiler.").withRank(BMinusSetting)
|
||||||
|
|
||||||
// package keys
|
// package keys
|
||||||
val packageBin = taskKey[File]("Produces a main artifact, such as a binary jar.").withRank(ATask)
|
val packageBin = taskKey[File]("Produces a main artifact, such as a binary jar.").withRank(ATask)
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ import Project.LoadAction
|
||||||
import xsbti.compile.CompilerCache
|
import xsbti.compile.CompilerCache
|
||||||
|
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
import sbt.io.IO
|
import sbt.io.{ FileTreeDataView, IO }
|
||||||
import sbt.io.syntax._
|
import sbt.io.syntax._
|
||||||
import java.io.{ File, IOException }
|
import java.io.{ File, IOException }
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
@ -241,7 +241,9 @@ object BuiltinCommands {
|
||||||
export,
|
export,
|
||||||
boot,
|
boot,
|
||||||
initialize,
|
initialize,
|
||||||
act
|
act,
|
||||||
|
continuous,
|
||||||
|
flushFileTreeRepository
|
||||||
) ++ allBasicCommands
|
) ++ allBasicCommands
|
||||||
|
|
||||||
def DefaultBootCommands: Seq[String] =
|
def DefaultBootCommands: Seq[String] =
|
||||||
|
|
@ -446,6 +448,14 @@ object BuiltinCommands {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def continuous: Command = Watched.continuous { (state: State, command: String) =>
|
||||||
|
val extracted = Project.extract(state)
|
||||||
|
val (s, watchConfig) = extracted.runTask(Keys.watchConfig, state)
|
||||||
|
val updateState =
|
||||||
|
(runCommand: () => State) => MainLoop.processCommand(Exec(command, None), s, runCommand)
|
||||||
|
(s, watchConfig, updateState)
|
||||||
|
}
|
||||||
|
|
||||||
private[this] def loadedEval(s: State, arg: String): Unit = {
|
private[this] def loadedEval(s: State, arg: String): Unit = {
|
||||||
val extracted = Project extract s
|
val extracted = Project extract s
|
||||||
import extracted._
|
import extracted._
|
||||||
|
|
@ -849,7 +859,7 @@ object BuiltinCommands {
|
||||||
|
|
||||||
val session = Load.initialSession(structure, eval, s0)
|
val session = Load.initialSession(structure, eval, s0)
|
||||||
SessionSettings.checkSession(session, s)
|
SessionSettings.checkSession(session, s)
|
||||||
Project.setProject(session, structure, s)
|
registerGlobalFileRepository(Project.setProject(session, structure, s))
|
||||||
}
|
}
|
||||||
|
|
||||||
def registerCompilerCache(s: State): State = {
|
def registerCompilerCache(s: State): State = {
|
||||||
|
|
@ -867,6 +877,27 @@ object BuiltinCommands {
|
||||||
}
|
}
|
||||||
s.put(Keys.stateCompilerCache, cache)
|
s.put(Keys.stateCompilerCache, cache)
|
||||||
}
|
}
|
||||||
|
def registerGlobalFileRepository(s: State): State = {
|
||||||
|
val extracted = Project.extract(s)
|
||||||
|
try {
|
||||||
|
val (_, config: FileTreeViewConfig) = extracted.runTask(Keys.fileTreeViewConfig, s)
|
||||||
|
val view: FileTreeDataView[StampedFile] = config.newDataView()
|
||||||
|
val newState = s.addExitHook {
|
||||||
|
view.close()
|
||||||
|
s.attributes.remove(BasicKeys.globalFileTreeView)
|
||||||
|
()
|
||||||
|
}
|
||||||
|
newState.get(BasicKeys.globalFileTreeView).foreach(_.close())
|
||||||
|
newState.put(BasicKeys.globalFileTreeView, view)
|
||||||
|
} catch {
|
||||||
|
case NonFatal(_) => s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def flushFileTreeRepository: Command = {
|
||||||
|
val help = Help.more(FlushFileTreeRepository, FlushDetailed)
|
||||||
|
Command.command(FlushFileTreeRepository, help)(registerGlobalFileRepository)
|
||||||
|
}
|
||||||
|
|
||||||
def shell: Command = Command.command(Shell, Help.more(Shell, ShellDetailed)) { s0 =>
|
def shell: Command = Command.command(Shell, Help.more(Shell, ShellDetailed)) { s0 =>
|
||||||
import sbt.internal.{ ConsolePromptEvent, ConsoleUnpromptEvent }
|
import sbt.internal.{ ConsolePromptEvent, ConsoleUnpromptEvent }
|
||||||
|
|
|
||||||
|
|
@ -144,13 +144,20 @@ object MainLoop {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This is the main function State transfer function of the sbt command processing. */
|
/** This is the main function State transfer function of the sbt command processing. */
|
||||||
def processCommand(exec: Exec, state: State): State = {
|
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 = {
|
||||||
val channelName = exec.source map (_.channelName)
|
val channelName = exec.source map (_.channelName)
|
||||||
StandardMain.exchange publishEventMessage
|
StandardMain.exchange publishEventMessage
|
||||||
ExecStatusEvent("Processing", channelName, exec.execId, Vector())
|
ExecStatusEvent("Processing", channelName, exec.execId, Vector())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val newState = Command.process(exec.commandLine, state)
|
val newState = runCommand()
|
||||||
val doneEvent = ExecStatusEvent(
|
val doneEvent = ExecStatusEvent(
|
||||||
"Done",
|
"Done",
|
||||||
channelName,
|
channelName,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* sbt
|
||||||
|
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||||
|
* Copyright 2008 - 2010, Mark Harrah
|
||||||
|
* Licensed under Apache License 2.0 (see LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sbt.internal
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
import sbt.StampedFile
|
||||||
|
import sbt.internal.inc.ExternalLookup
|
||||||
|
import sbt.io.syntax.File
|
||||||
|
import sbt.io.{ FileTreeRepository, FileTreeDataView, TypedPath }
|
||||||
|
import xsbti.compile._
|
||||||
|
import xsbti.compile.analysis.Stamp
|
||||||
|
|
||||||
|
import scala.collection.mutable
|
||||||
|
|
||||||
|
private[sbt] object ExternalHooks {
|
||||||
|
private val javaHome = Option(System.getProperty("java.home")).map(Paths.get(_))
|
||||||
|
def apply(options: CompileOptions, view: FileTreeDataView[StampedFile]): DefaultExternalHooks = {
|
||||||
|
import scala.collection.JavaConverters._
|
||||||
|
val sources = options.sources()
|
||||||
|
val cachedSources = new java.util.HashMap[File, Stamp]
|
||||||
|
val converter: File => Stamp = f => StampedFile.sourceConverter(TypedPath(f.toPath)).stamp
|
||||||
|
sources.foreach {
|
||||||
|
case sf: StampedFile => cachedSources.put(sf, sf.stamp)
|
||||||
|
case f: File => cachedSources.put(f, converter(f))
|
||||||
|
}
|
||||||
|
view match {
|
||||||
|
case r: FileTreeRepository[StampedFile] =>
|
||||||
|
r.register(options.classesDirectory.toPath, Integer.MAX_VALUE)
|
||||||
|
options.classpath.foreach { f =>
|
||||||
|
r.register(f.toPath, Integer.MAX_VALUE)
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
val allBinaries = new java.util.HashMap[File, Stamp]
|
||||||
|
options.classpath.foreach { f =>
|
||||||
|
view.listEntries(f.toPath, Integer.MAX_VALUE, _ => true) foreach { e =>
|
||||||
|
e.value match {
|
||||||
|
case Right(value) => allBinaries.put(e.typedPath.getPath.toFile, value.stamp)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This gives us the entry for the path itself, which is necessary if the path is a jar file
|
||||||
|
// rather than a directory.
|
||||||
|
view.listEntries(f.toPath, -1, _ => true) foreach { e =>
|
||||||
|
e.value match {
|
||||||
|
case Right(value) => allBinaries.put(e.typedPath.getPath.toFile, value.stamp)
|
||||||
|
case _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val lookup = new ExternalLookup {
|
||||||
|
override def changedSources(previousAnalysis: CompileAnalysis): Option[Changes[File]] = Some {
|
||||||
|
new Changes[File] {
|
||||||
|
val getAdded: java.util.Set[File] = new java.util.HashSet[File]
|
||||||
|
val getRemoved: java.util.Set[File] = new java.util.HashSet[File]
|
||||||
|
val getChanged: java.util.Set[File] = new java.util.HashSet[File]
|
||||||
|
val getUnmodified: java.util.Set[File] = new java.util.HashSet[File]
|
||||||
|
override def isEmpty: java.lang.Boolean =
|
||||||
|
getAdded.isEmpty && getRemoved.isEmpty && getChanged.isEmpty
|
||||||
|
val prevSources: mutable.Map[File, Stamp] =
|
||||||
|
previousAnalysis.readStamps().getAllSourceStamps.asScala
|
||||||
|
prevSources.foreach {
|
||||||
|
case (file: File, s: Stamp) =>
|
||||||
|
cachedSources.get(file) match {
|
||||||
|
case null =>
|
||||||
|
getRemoved.add(file)
|
||||||
|
case stamp =>
|
||||||
|
if ((stamp.getHash.orElse("") == s.getHash.orElse("")) && (stamp.getLastModified
|
||||||
|
.orElse(-1L) == s.getLastModified.orElse(-1L))) {
|
||||||
|
getUnmodified.add(file)
|
||||||
|
} else {
|
||||||
|
getChanged.add(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sources.foreach(file => if (!prevSources.contains(file)) getAdded.add(file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def shouldDoIncrementalCompilation(
|
||||||
|
set: Set[String],
|
||||||
|
compileAnalysis: CompileAnalysis
|
||||||
|
): Boolean = true
|
||||||
|
|
||||||
|
// This could use the cache as well, but it would complicate the cache implementation.
|
||||||
|
override def hashClasspath(files: Array[File]): Optional[Array[FileHash]] =
|
||||||
|
Optional.empty[Array[FileHash]]
|
||||||
|
|
||||||
|
override def changedBinaries(previousAnalysis: CompileAnalysis): Option[Set[File]] = {
|
||||||
|
Some(previousAnalysis.readStamps.getAllBinaryStamps.asScala.flatMap {
|
||||||
|
case (file, stamp) =>
|
||||||
|
allBinaries.get(file) match {
|
||||||
|
case null =>
|
||||||
|
javaHome match {
|
||||||
|
case Some(h) if file.toPath.startsWith(h) => None
|
||||||
|
case _ => Some(file)
|
||||||
|
}
|
||||||
|
case cachedStamp if stamp == cachedStamp => None
|
||||||
|
case _ => Some(file)
|
||||||
|
}
|
||||||
|
}.toSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def removedProducts(previousAnalysis: CompileAnalysis): Option[Set[File]] = {
|
||||||
|
Some(previousAnalysis.readStamps.getAllProductStamps.asScala.flatMap {
|
||||||
|
case (file, s) =>
|
||||||
|
allBinaries get file match {
|
||||||
|
case null => Some(file)
|
||||||
|
case stamp if stamp.getLastModified.orElse(0L) != s.getLastModified.orElse(0L) =>
|
||||||
|
Some(file)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}.toSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new DefaultExternalHooks(Optional.of(lookup), Optional.empty[ClassFileManager])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* sbt
|
||||||
|
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||||
|
* Copyright 2008 - 2010, Mark Harrah
|
||||||
|
* Licensed under Apache License 2.0 (see LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sbt.internal
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
import sbt.Keys._
|
||||||
|
import sbt.io.FileTreeDataView.Entry
|
||||||
|
import sbt.io.syntax.File
|
||||||
|
import sbt.io.{ FileFilter, FileTreeDataView, FileTreeRepository }
|
||||||
|
import sbt._
|
||||||
|
import BasicCommandStrings.ContinuousExecutePrefix
|
||||||
|
|
||||||
|
private[sbt] object FileManagement {
|
||||||
|
private[sbt] def defaultFileTreeView: Def.Initialize[Task[FileTreeViewConfig]] = Def.task {
|
||||||
|
val remaining = state.value.remainingCommands.map(_.commandLine.trim)
|
||||||
|
// If the session is interactive or if the commands include a continuous build, then use
|
||||||
|
// the default configuration. Otherwise, use the sbt1_2_compat config, which does not cache
|
||||||
|
// anything, which makes it less likely to cause issues with CI.
|
||||||
|
val interactive = remaining.contains("shell") && !remaining.contains("setUpScripted")
|
||||||
|
val continuous = remaining.exists(_.startsWith(ContinuousExecutePrefix))
|
||||||
|
if (interactive || continuous) {
|
||||||
|
FileTreeViewConfig
|
||||||
|
.default(watchAntiEntropy.value, pollInterval.value, pollingDirectories.value)
|
||||||
|
} else FileTreeViewConfig.sbt1_2_compat(pollInterval.value, watchAntiEntropy.value)
|
||||||
|
}
|
||||||
|
private[sbt] implicit class FileTreeDataViewOps[+T](val fileTreeDataView: FileTreeDataView[T]) {
|
||||||
|
def register(path: Path, maxDepth: Int): Either[IOException, Boolean] = {
|
||||||
|
fileTreeDataView match {
|
||||||
|
case r: FileTreeRepository[T] => r.register(path, maxDepth)
|
||||||
|
case _ => Right(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private[sbt] def collectFiles(
|
||||||
|
dirs: ScopedTaskable[Seq[File]],
|
||||||
|
filter: ScopedTaskable[FileFilter],
|
||||||
|
excludes: ScopedTaskable[FileFilter]
|
||||||
|
): Def.Initialize[Task[Seq[File]]] =
|
||||||
|
Def.task {
|
||||||
|
val sourceDirs = dirs.toTask.value
|
||||||
|
val view = fileTreeView.value
|
||||||
|
val include = filter.toTask.value
|
||||||
|
val ex = excludes.toTask.value
|
||||||
|
val sourceFilter: Entry[StampedFile] => Boolean = (entry: Entry[StampedFile]) => {
|
||||||
|
entry.value match {
|
||||||
|
case Right(sf) => include.accept(sf) && !ex.accept(sf)
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceDirs.flatMap { dir =>
|
||||||
|
view.register(dir.toPath, maxDepth = Integer.MAX_VALUE)
|
||||||
|
view
|
||||||
|
.listEntries(dir.toPath, maxDepth = Integer.MAX_VALUE, sourceFilter)
|
||||||
|
.map(e => e.value.getOrElse(e.typedPath.getPath.toFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private[sbt] def appendBaseSources: Seq[Def.Setting[Task[Seq[File]]]] = Seq(
|
||||||
|
unmanagedSources := {
|
||||||
|
val sources = unmanagedSources.value
|
||||||
|
val f = (includeFilter in unmanagedSources).value
|
||||||
|
val excl = (excludeFilter in unmanagedSources).value
|
||||||
|
val baseDir = baseDirectory.value
|
||||||
|
val view = fileTreeView.value
|
||||||
|
if (sourcesInBase.value) {
|
||||||
|
view.register(baseDir.toPath, maxDepth = 0)
|
||||||
|
sources ++
|
||||||
|
view
|
||||||
|
.listEntries(
|
||||||
|
baseDir.toPath,
|
||||||
|
maxDepth = 0,
|
||||||
|
e => {
|
||||||
|
val tp = e.typedPath
|
||||||
|
/*
|
||||||
|
* The TypedPath has the isDirectory and isFile properties embedded. By overriding
|
||||||
|
* these methods in java.io.File, FileFilters may be applied without needing to
|
||||||
|
* stat the file (which is expensive) for isDirectory and isFile checks.
|
||||||
|
*/
|
||||||
|
val file = new java.io.File(tp.getPath.toString) {
|
||||||
|
override def isDirectory: Boolean = tp.isDirectory
|
||||||
|
override def isFile: Boolean = tp.isFile
|
||||||
|
}
|
||||||
|
f.accept(file) && !excl.accept(file)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.flatMap(_.value.toOption)
|
||||||
|
} else sources
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -113,6 +113,7 @@ private[sbt] object Load {
|
||||||
val delegates = defaultDelegates
|
val delegates = defaultDelegates
|
||||||
val pluginMgmt = PluginManagement(loader)
|
val pluginMgmt = PluginManagement(loader)
|
||||||
val inject = InjectSettings(injectGlobal(state), Nil, const(Nil))
|
val inject = InjectSettings(injectGlobal(state), Nil, const(Nil))
|
||||||
|
System.setProperty("swoval.tmpdir", System.getProperty("swoval.tmpdir", globalBase.toString))
|
||||||
LoadBuildConfiguration(
|
LoadBuildConfiguration(
|
||||||
stagingDirectory,
|
stagingDirectory,
|
||||||
classpath,
|
classpath,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
val checkCount = inputKey[Unit]("check that compile has run a specified number of times")
|
||||||
|
val checkReloadCount = inputKey[Unit]("check whether the project was reloaded")
|
||||||
|
val failingTask = taskKey[Unit]("should always fail")
|
||||||
|
val maybeReload = settingKey[(Int, Boolean) => Watched.Action]("possibly reload")
|
||||||
|
val resetCount = taskKey[Unit]("reset compile count")
|
||||||
|
val reloadFile = settingKey[File]("get the current reload file")
|
||||||
|
|
||||||
|
checkCount := {
|
||||||
|
val expected = Def.spaceDelimited().parsed.head.toInt
|
||||||
|
if (Count.get != expected)
|
||||||
|
throw new IllegalStateException(s"Expected ${expected} compilation runs, got ${Count.get}")
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeReload := { (_, _) =>
|
||||||
|
if (Count.reloadCount(reloadFile.value) == 0) Watched.Reload else Watched.CancelWatch
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadFile := baseDirectory.value / "reload-count"
|
||||||
|
|
||||||
|
resetCount := {
|
||||||
|
Count.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
failingTask := {
|
||||||
|
throw new IllegalStateException("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
watchPreWatch := maybeReload.value
|
||||||
|
|
||||||
|
checkReloadCount := {
|
||||||
|
val expected = Def.spaceDelimited().parsed.head.toInt
|
||||||
|
assert(Count.reloadCount(reloadFile.value) == expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
val addReloadShutdownHook = Command.command("addReloadShutdownHook") { state =>
|
||||||
|
state.addExitHook {
|
||||||
|
val base = Project.extract(state).get(baseDirectory)
|
||||||
|
val file = base / "reload-count"
|
||||||
|
val currentCount = Try(Count.reloadCount(file)).getOrElse(0)
|
||||||
|
IO.write(file, s"${currentCount + 1}".getBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commands += addReloadShutdownHook
|
||||||
|
|
||||||
|
Compile / compile := {
|
||||||
|
Count.increment()
|
||||||
|
// Trigger a new build by updating the last modified time
|
||||||
|
((Compile / scalaSource).value / "A.scala").setLastModified(5000)
|
||||||
|
(Compile / compile).value
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import sbt._
|
||||||
|
import scala.util.Try
|
||||||
|
|
||||||
|
object Count {
|
||||||
|
private var count = 0
|
||||||
|
def get: Int = count
|
||||||
|
def increment(): Unit = count += 1
|
||||||
|
def reset(): Unit = count = 0
|
||||||
|
def reloadCount(file: File): Int = Try(IO.read(file).toInt).getOrElse(0)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package a
|
||||||
|
|
||||||
|
class A
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# verify that reloading occurs if watchPreWatch returns Watched.Reload
|
||||||
|
> addReloadShutdownHook
|
||||||
|
> checkReloadCount 0
|
||||||
|
> ~compile
|
||||||
|
> checkReloadCount 1
|
||||||
|
|
||||||
|
# verify that the watch terminates when we reach the specified count
|
||||||
|
> resetCount
|
||||||
|
> set watchPreWatch := { (count: Int, _) => if (count == 2) Watched.CancelWatch else Watched.Ignore }
|
||||||
|
> ~compile
|
||||||
|
> checkCount 2
|
||||||
|
|
||||||
|
# verify that the watch terminates and returns an error when we reach the specified count
|
||||||
|
> resetCount
|
||||||
|
> set watchPreWatch := { (count: Int, _) => if (count == 2) Watched.HandleError else Watched.Ignore }
|
||||||
|
# Returning Watched.HandleError causes the '~' command to fail
|
||||||
|
-> ~compile
|
||||||
|
> checkCount 2
|
||||||
|
|
||||||
|
# verify that a re-build is triggered when we reach the specified count
|
||||||
|
> resetCount
|
||||||
|
> set watchPreWatch := { (count: Int, _) => if (count == 2) Watched.Trigger else if (count == 3) Watched.CancelWatch else Watched.Ignore }
|
||||||
|
> ~compile
|
||||||
|
> checkCount 3
|
||||||
|
|
||||||
|
# verify that the watch exits and returns an error if the task fails
|
||||||
|
> set watchPreWatch := { (_, lastStatus: Boolean) => if (lastStatus) Watched.Ignore else Watched.HandleError }
|
||||||
|
-> ~failingTask
|
||||||
Loading…
Reference in New Issue