sbt/project/SbtLauncherPlugin.scala

56 lines
2.1 KiB
Scala
Raw Normal View History

import sbt.Keys._
import sbt._
2017-07-20 04:00:42 +02:00
import sbt.io.CopyOptions
object SbtLauncherPlugin extends AutoPlugin {
override def requires = plugins.IvyPlugin
object autoImport {
val SbtLaunchConfiguration = config("sbt-launch")
val sbtLaunchJar = taskKey[File]("constructs an sbt-launch.jar for this version of sbt.")
2017-04-21 09:14:31 +02:00
val rawSbtLaunchJar =
taskKey[File]("The released version of the sbt-launcher we use to bundle this application.")
}
import autoImport._
override def projectConfigurations: Seq[Configuration] = Seq(SbtLaunchConfiguration)
override def projectSettings: Seq[Setting[_]] = Seq(
libraryDependencies += Dependencies.rawLauncher % SbtLaunchConfiguration.name,
rawSbtLaunchJar := {
Classpaths.managedJars(SbtLaunchConfiguration, Set("jar"), update.value).headOption match {
case Some(jar) => jar.data
2017-04-21 09:14:31 +02:00
case None =>
sys.error(
Refactor Watched This is a huge refactor of Watched. I produced this through multiple rewrite iterations and it was too difficult to separate all of the changes into small individual commits so I, unfortunately, had to make a massive commit. In general, I have tried to document the source code extensively both to facilitate reading this commit and to help with future maintenance. These changes are quite complicated because they provided a built-in like api to a feature that is implemented like a plugin. In particular, we have to manually do a lot of parsing as well as roll our own task/setting evaluation because we cannot infer the watch settings at project build time because we do not know a priori what commands the user may watch in a given session. The dynamic setting and task evaluation is mostly confined to the WatchSettings class in Continuous. It feels dirty to do all of this extraction by hand, but it does seem to work correctly with scopes. At a high level this commit does four things: 1) migrate the watch implementation to using the InputGraph to collect the globs that it needs to monitor during the watch 2) simplify WatchConfig to make it easier for plugin authors to write their own custom watch implementations 3) allow configuration of the watch settings based on the task(s) that is/are being run 4) adds an InputTask implemenation of watch. Point #1 is mostly handled by Point #3 since I had to overhaul how _all_ of the watch settings are generated. InputGraph already handles both transitive inputs and triggers as well as legacy watchSources so not much additional logic is needed beyond passing the correct scoped keys into InputGraph. Point #3 require some structural changes. The watch settings cannot in general be defined statically because we don't know a priori what tasks the user will try and watch. To address this, I added code that will extract the task keys for all of the commands that we are running. I then manually extract the relevant settings for each command. Finally, I aggregate those settings into a single WatchConfig that can be used to actually implement the watch. The aggregation is generally straightforward: we run all of the callbacks for each task and choose the next watch state based on the highest priority Action that is returned by any of the callbacks. Because I needed Extracted to pull out the necessary settings, I was forced to move a lot of logic out of Watched and into a new singleton, Continuous, that exists in the main project (Watched is in the command project). The public footprint of Continuous is tiny. Even though I want to make the watch feature flexible for plugin authors, the implementation and api remain a moving target so I do not want to be limited by future binary compatibility requirements. Anyone who wants to live dangerously can access the private[sbt] apis via reflection or by adding custom code to the sbt package in their plugin (a technique I've used in CloseWatch). Point #2 is addressed by removing the count and lastStatus from the WatchConfig callbacks. While these parameters can be useful, they are not necessary to implement the semantics of a watch. Moreover, a status boolean isn't really that useful and the sbt task engine makes it very difficult to actually extract the previous result of the tasks that were run. After this refactor, WatchConfig has a simpler api. There are fewer callbacks to implement and the signatures are simpler. To preserve the _functionality_ of making the count accessible to the user specifiable callbacks, I still provided settings like watchOnInputEvent that accept a count parameter, but the count is actually tracked externally to Watched.watch and incremented every time the task is run. Moreover, there are a few parameters of the watch: the logger and transitive globs, that cannot be provided via settings. I provide callback settings like watchOnStart that mirror the WatchConfig callbacks except that they return a function from Continuous.Arguments to the needed callback. The Continuous.aggregate function will check if the watchOnStart setting is set and if it is, will pass in the needed arguments. Otherwise it will use the default watchOnStart implementation which simulates the existing behavior by tracking the iteration count in an AtomicInteger and passing the current count into the user provided callback. In this way, we are able to provide a number of apis to the watch process while preserving the default behavior. To implement #4, I had to change the label of the `watch` attribute key from "watch" to "watched". This allows `watch compile` to work at the sbt command line even thought it maps to the watchTasks key. The actual implementation is almost trivial. The difference between an InputTask[Unit] and a command is very small. The tricky part is that the actual implementation requires applying mapTask to a delegate task that overrides the Task's info.postTransform value (which is used to transform the state after task evaluation). The actual postTransform function can be shared by the continuous task and continuous command. There is just a slightly different mechanism for getting to the state transformation function.
2019-01-31 20:18:27 +01:00
s"Could not resolve sbt launcher!, dependencies := ${libraryDependencies.value}"
)
}
},
sbtLaunchJar := {
val propFiles = (resources in Compile).value
val propFileLocations =
2017-04-21 09:14:31 +02:00
for (file <- propFiles; if file.getName != "resources") yield {
if (file.getName == "sbt.boot.properties") "sbt/sbt.boot.properties" -> file
else file.getName -> file
}
// TODO - We need to inject the appropriate boot.properties file for this version of sbt.
rebundle(rawSbtLaunchJar.value, propFileLocations.toMap, target.value / "sbt-launch.jar")
}
)
def rebundle(jar: File, overrides: Map[String, File], target: File): File = {
// TODO - Check if we should rebuild the jar or not....
IO.withTemporaryDirectory { dir =>
IO.unzip(jar, dir)
2017-07-20 04:00:42 +02:00
IO.copy(overrides.map({ case (n, f) => (f, dir / n) }), CopyOptions().withOverwrite(true))
// TODO - is the ok for creating a jar?
val rebase: File => Seq[(File, String)] = {
val path = dir.toPath
f => if (f != dir) f -> path.relativize(f.toPath).toString :: Nil else Nil
}
IO.zip(dir.allPaths.get().flatMap(rebase), target)
}
target
}
2015-07-10 11:53:48 +02:00
}