Run custom task automatically before/after standard task - scala

I often want to do some customization before one of the standard tasks are run. I realize I can make new tasks that executes existing tasks in the order I want, but I find that cumbersome and the chance that a developer misses that he is supposed to run my-compile instead of compile is big and leads to hard to fix errors.
So I want to define a custom task (say prepare-app) and inject it into the dependency tree of the existing tasks (say package-bin) so that every time someone invokes package-bin my custom tasks is run right before it.
I tried doing this
def mySettings = {
inConfig(Compile)(Seq(prepareAppTask <<= packageBin in Compile map { (pkg: File) =>
// fiddle with the /target folder before package-bin makes it into a jar
})) ++
Seq(name := "my project", version := "1.0")
}
lazy val prepareAppTask = TaskKey[Unit]("prepare-app")
but it's not executed automatically by package-bin right before it packages the compile output into a jar. So how do I alter the above code to be run at the right time ?
More generally where do I find info about hooking into other tasks like compile and is there a general way to ensure that your own tasks are run before and after a standard tasks are invoked ?.

Extending an existing task is documented the SBT documentation for Tasks (look at the section Modifying an Existing Task).
Something like this:
compile in Compile <<= (compile in Compile) map { _ =>
// what you want to happen after compile goes here
}
Actually, there is another way - define your task to depend on compile
prepareAppTask := (whatever you want to do) dependsOn compile
and then modify packageBin to depend on that:
packageBin <<= packageBin dependsOn prepareAppTask
(all of the above non-tested, but the general thrust should work, I hope).

As an update for the previous answer by #Paul Butcher, this could be done in a bit different way in SBT 1.x versions since <<== is no longer supported. I took an example of a sample task to run before the compilation that I use in one of my projects:
lazy val wsdlImport = TaskKey[Unit]("wsdlImport", "Generates Java classes from WSDL")
wsdlImport := {
import sys.process._
"./wsdl/bin/wsdl_import.sh" !
// or do whatever stuff you need
}
(compile in Compile) := ((compile in Compile) dependsOn wsdlImport).value
This is very similar to how it was done before 1.x.
Also, there is another way suggested by official SBT docs, which is basically a composition of tasks (instead of dependencies hierarchy). Taking the same example as above:
(compile in Compile) := {
val w = wsdlImport.value
val c = (compile in Compile).value
// you can add more tasks to composition or perform some actions with them
c
}
It feels like giving more flexibility in some cases, though the first example looks a bit neater, as for me.
Tested on SBT 1.2.3 but should work with other 1.x as well.

Related

sbt wrapper plugin and custom logic

I have developed a custom sbt plugin that imports the sbt native packager, and a bunch of other plugins.
This helps bootstrap a scala project easing the process of importing all plugins we use across the project.
I also have custom task for custom purposes, such a:
substituteTemplates
to do custom behaviour.
I'm also changing the behaviour of the plugin I'm importing to cope with my customizations, such as:
packageZipTarball in Universal <<= (packageZipTarball in Universal) dependsOn substituteTemplates.
Now, everything works but the user of my plugin has to do:
val example = (project in file(".")).enablePlugins(JavaAppPackaging).settings(...
if he does not do it the sbt will not compile, because sbt finds definitions of stuff (mappings in Universal and such... ) that do not exist.
Here is how I tried to solve it.
TAKE 1
Now, I'd like to have the possibility to cope with the fact that some plugins need to be enabled.
If I enable the plugins (for example JavaAppPackaging) then I do a bunch of other changes, otherwise I leave the settings untouched.
At first I tried with a custom settings key, such as:
val enableJavaAppPackaging = SettingKey[Boolean]("enableJavaAppPackaging","")
then, I tried to use it:
object MyWrapperPlugin extends AutoPlugin {
override def projectSettings: Seq[Setting[_]] = {
if (enableJavaAppPackaging.value) {
packageZipTarball in Universal <<= (packageZipTarball in Universal) dependsOn substituteTemplates,
mappings in Universal <++= templates.map {
_.keySet.map { k => file(s"target/templates/$k") -> k }.toList
},
// a lot of other stuff....
}
but this cannot work as sbt complains:
`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting
There is a way to do this kind of logic in a way that is OK also for sbt?
TAKE 2
If I cannot do this at least I'd like to turn the JavaApp PAckaging plugin by default, so not to do elsiffing at all.
object MyWrapperPlugin extends AutoPlugin {
override def requires: Plugins = AssemblyPlugin && DependencyGraphPlugin && JavaAppPackaging
but this does not work too.

sbt illegal dynamic reference in runMain

I'm trying to run a code generator, and passing it the filename to write the output:
resourceGenerators in (proj, Compile) += Def.task {
val file = (resourceManaged in (proj, Compile)).value / "swagger.yaml"
(runMain in (proj, Compile)).toTask(s"api.swagger.SwaggerDump $file").value
Seq(file)
}.value
However, this gives me:
build.sbt:172: error: Illegal dynamic reference: file
(runMain in (proj, Compile)).toTask(s"api.swagger.SwaggerDump $file").value
Your code snippet has two problems:
You use { ... }.value instead of { ... }.taskValue. The type of resource generators is Seq[Task[Seq[File]]] and when you do value, you get Seq[File] not Task[Seq[File]]. That causes a legitimate compile error.
The dynamic variable file is used as the argument of toTask, which the current macro implementation prohibits.
Why static?
Sbt forces task implementations to have static dependencies on other tasks. Otherwise, sbt cannot perform task deduplication and cannot provide correct information in the inspect commands. That means that whichever task evaluation you perform inside a task cannot depend on a variable (a value known only at runtime), as your file in toTask does.
To overcome this limitation, there exists dynamic tasks, whose body allows you to return a task. Every "dynamic dependency" has to be defined inside a dynamic task, and then you can depend on the hoisted up dynamic values in the task that you return.
Dynamic solution
The following Scastie is the correct implementation of your task. I copy-paste the code so that folks can have a quick look, but go to that Scastie to check that it successfully compiles and runs.
resourceGenerators in (proj, Compile) += Def.taskDyn {
val file = (resourceManaged in (proj, Compile)).value / "swagger.yaml"
Def.task {
(runMain in (proj, Compile))
.toTask(s"api.swagger.SwaggerDump $file")
.value
Seq(file)
}
}.taskValue
Discussion
If you had fixed the taskValue error, should your task implementation correctly compile?
In my opinion, yes, but I haven't looked at the internal implementation good enough to assert that your task implementation does not hinder task deduplication and dependency extraction. If it does not, the illegal reference check should disappear.
This is a current limitation of sbt that I would like to get rid of, either by improving the whole macro implementation (hoisting up values and making sure that dependency analysis covers more cases) or by just improving the "illegal references checks" to not be over pessimistic. However, this is a hard problem, takes time and it's not likely to happen in the short term.
If this is an issue for you, please file a ticket in sbt/sbt. This is the only way to know the urgency of fixing this issue, if any. For now, the best we can do is to document it.

SBT: how to exclude one sub-project from one aggregated task

I have a bunch of sub-project that I define as follows (actually generated by a project/meta.sbt):
lazy val Top = (project in file("."))
.aggregate(common, p1, p2, tests, scripts)
.dependsOn(common, p1, p2, tests, scripts)
lazy val common = project
lazy val p1 = project.dependsOn(common % "compile->compile;test->test")
lazy val p2 = project.dependsOn(common % "compile->compile;test->test")
lazy val tests = project.dependsOn(common % "compile->compile;test->test")
lazy val scripts = project
AFAICT (so maybe that's wrong), both the aggregate and the dependsOn at the top make sense: the first to run tasks in all sub-projects, and the second to make it convenient to debug stuff.
Now, the thing is that tests contains just a bunch of tests, and scripts is a script that is used as a publishing step -- so I need to somehow make it avoid making jar files only for them. I still want compile/test/etc to be aggregated including them.
My understanding from the docs is that this should be done by setting the aggregate key in Top, and indeed adding something like
.settings(aggregate in clean:= false)
prevents clean from being aggregated -- but I want to do that for package and I don't want to drop it completely, just not package those two sub-projects. Using something like aggregate in (tests, packageBin) is my best guess, but that doesn't seem to make an impression on SBT: looks like packageBin is wrong, and also combining it with tests doesn't work.
Is that possible? Actually, given that SBT is just a tiny bit overengineered, it's always possible, so rephrase: How can I do that?
Update:
Thanks to this answer I found a workaround -- add:
.settings(Keys.`package` := file(""))
to the definitions of subprojects that shouldn't be packaged. That works for avoiding generation of jar files, but it's obviously not an answer for the actual question (which I'm probably going to run into again).

How can I call another task from my SBT task?

I'm trying to call the runTask inside of my task and considered this would work:
name := "hello"
version := "1.0"
scalaVersion := "2.10.2"
lazy val hello = taskKey[Unit]("executes hey")
lazy val helloTask = hello <<= runTask(fullClasspath, "sample.Hey" in run, runner in run)
But, well, it doesn't. Any ideas on how I could do this?
General answer:
To answer your general question, the solution is to make your task depend on the other task. Invoking the task directly would do an end run around the dependency system, the parallel execution system, etc. You depend on, and invoke, the task like this (in 0.13-style syntax):
myTask := {
...
val result = otherTask.value
...
}
Note that otherTask will be invoked before myTask begins, rather than at the point in the body of myTask where the dependency appears; because that's how dependencies work.
If for whatever reason you find doing it the "normal" way inappropriate or unacceptable, consider that good style in sbt is to separate the declaration of a task from its implementation. A typical task implementation simply marshals arguments and then calls a method that actually does the work. If the task you want to call is implemented that way, then an answer to "How do I call task T?" is "Don't; call the same code T calls."
Specific answer:
But from your example, it looks to me like the problem you're actually trying to solve is "How can I create a custom run task, in addition to run?" This question is answered in the sbt FAQ; see http://www.scala-sbt.org/0.13.0/docs/faq.html. The answer is to use the convenience methods fullRunTask and fullRunInputTask.
Incidentally, if you look at the source code for those methods, you'll see that they don't make a task that invokes another task; rather, they take the "call the same code" approach.

Create a new task that runs a program

I need to define a custom tasks that computes the name of a main class and then runs it. I was thinking about something like this
customTask {
mainClass = compute main class name based on env
runMain(mainClass, jvm-args, fork=true)
}
and then in SBT i would be able to run
sbt> custom-task
can this be done in SBT 11.2 ?.
well you can give it a try .. I works fine for me -
lazy val testngRun = inputKey[Unit]("custom run task for testng")
testngRun := {
val one = (runMain in Compile).fullInput(" org.testng.TestNG -testclass com.pg.acceptance.testcase.PfsLoginServiceTest").evaluated
}
Late answer but you can create new SBT tasks as mentioned in documentation http://www.scala-sbt.org/release/docs/Detailed-Topics/Tasks#defining-a-new-task
You can run any scala code as the task code. Tasks can also take input arguments.
Pretty much powerful IMO.