This page gets you started creating your own settings and tasks.
To understand this page, be sure you’ve read earlier pages in the Getting Started Guide, especially build.sbt and task graph.
Keys is packed with examples illustrating how to define keys. Most of the keys are implemented in Defaults.
Keys have one of three types. SettingKey
and TaskKey
are described in
.sbt build definition. Read about InputKey
on the
Input Tasks page.
Some examples from Keys:
val scalaVersion = settingKey[String]("The version of Scala used for building.")
val clean = taskKey[Unit]("Deletes files produced by the build, such as generated sources, compiled classes, and task caches.")
The key constructors have two string parameters: the name of the key
("scalaVersion"
) and a documentation string
("The version of scala used for building."
).
Remember from .sbt build definition that the type
parameter T
in SettingKey[T]
indicates the type of value a setting has.
T
in TaskKey[T]
indicates the type of the task’s result. Also remember
from .sbt build definition that a setting has a fixed
value until project reload, while a task is re-computed for every “task
execution” (every time someone types a command at the sbt interactive
prompt or in batch mode).
Keys may be defined in an .sbt file,
a .scala file, or in an auto plugin.
Any val
s found under autoImport
object of an enabled auto plugin
will be imported automatically into your .sbt
files.
Once you’ve defined a key for your task, you’ll need to complete it with
a task definition. You could be defining your own task, or you could be
planning to redefine an existing task. Either way looks the same; use :=
to associate some code with the task key:
val sampleStringTask = taskKey[String]("A sample string task.")
val sampleIntTask = taskKey[Int]("A sample int task.")
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.18"
lazy val library = (project in file("library"))
.settings(
sampleStringTask := System.getProperty("user.home"),
sampleIntTask := {
val sum = 1 + 2
println("sum: " + sum)
sum
}
)
If the task has dependencies, you’d reference their value using value
,
as discussed in task graph.
The hardest part about implementing tasks is often not sbt-specific; tasks are just Scala code. The hard part could be writing the “body” of your task that does whatever you’re trying to do. For example, maybe you’re trying to format HTML in which case you might want to use an HTML library (you would add a library dependency to your build definition and write code based on the HTML library, perhaps).
sbt has some utility libraries and convenience functions, in particular you can often use the convenient APIs in IO to manipulate files and directories.
When depending on other tasks from a custom task using value
,
an important detail to note is the execution semantics of the tasks.
By execution semantics, we mean exactly when these tasks are evaluated.
If we take sampleIntTask
for instance, each line in the body of the task
should be strictly evaluated one after the other. That is sequential semantics:
sampleIntTask := {
val sum = 1 + 2 // first
println("sum: " + sum) // second
sum // third
}
In reality JVM may inline the sum
to 3
, but the observable effect of the
task will remain identical as if each line were executed one after the other.
Now suppose we define two more custom tasks startServer
and stopServer
,
and modify sampleIntTask
as follows:
val startServer = taskKey[Unit]("start server")
val stopServer = taskKey[Unit]("stop server")
val sampleIntTask = taskKey[Int]("A sample int task.")
val sampleStringTask = taskKey[String]("A sample string task.")
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.18"
lazy val library = (project in file("library"))
.settings(
startServer := {
println("starting...")
Thread.sleep(500)
},
stopServer := {
println("stopping...")
Thread.sleep(500)
},
sampleIntTask := {
startServer.value
val sum = 1 + 2
println("sum: " + sum)
stopServer.value // THIS WON'T WORK
sum
},
sampleStringTask := {
startServer.value
val s = sampleIntTask.value.toString
println("s: " + s)
s
}
)
Running sampleIntTask
from sbt interactive prompt results to the following:
> sampleIntTask
stopping...
starting...
sum: 3
[success] Total time: 1 s, completed Dec 22, 2014 5:00:00 PM
To review what happened, let’s look at a graphical notation of sampleIntTask
:
Unlike plain Scala method calls, invoking value
method on tasks will not
be evaluated strictly. Instead, they simply act as placeholders to denote
that sampleIntTask
depends on startServer
and stopServer
tasks.
When sampleIntTask
is invoked by you, sbt’s tasks engine will:
sampleIntTask
(partial ordering)
To demonstrate the last point, we can run sampleStringTask
from sbt
interactive prompt.
> sampleStringTask
stopping...
starting...
sum: 3
s: 3
[success] Total time: 1 s, completed Dec 22, 2014 5:30:00 PM
Because sampleStringTask
depends on both startServer
and sampleIntTask
task,
and sampleIntTask
also depends on startServer
task, it appears twice as task dependency.
If this was a plain Scala method call it would be evaluated twice,
but since value
is just denoting a task dependency, it will be evaluated once.
The following is a graphical notation of sampleStringTask
’s evaluation:
If we did not deduplicate the task dependencies, we will end up
compiling test source code many times when test
task is invoked
since Test / compile
appears many times as a task dependency of Test / test
.
How should one implement stopServer
task?
The notion of cleanup task does not fit into the execution model of tasks because
tasks are about tracking dependencies.
The last operation should become the task that depends
on other intermediate tasks. For instance stopServer
should depend on sampleStringTask
,
at which point stopServer
should be the sampleStringTask
.
lazy val library = (project in file("library"))
.settings(
startServer := {
println("starting...")
Thread.sleep(500)
},
sampleIntTask := {
startServer.value
val sum = 1 + 2
println("sum: " + sum)
sum
},
sampleStringTask := {
startServer.value
val s = sampleIntTask.value.toString
println("s: " + s)
s
},
sampleStringTask := {
val old = sampleStringTask.value
println("stopping...")
Thread.sleep(500)
old
}
)
To demonstrate that it works, run sampleStringTask
from the interactive prompt:
> sampleStringTask
starting...
sum: 3
s: 3
stopping...
[success] Total time: 1 s, completed Dec 22, 2014 6:00:00 PM
Another way of making sure that something happens after some other thing is to use Scala.
Implement a simple function in project/ServerUtil.scala
for example, and you can write:
sampleIntTask := {
ServerUtil.startServer
try {
val sum = 1 + 2
println("sum: " + sum)
} finally {
ServerUtil.stopServer
}
sum
}
Since plain method calls follow sequential semantics, everything happens in order. There’s no deduplication, so you have to be careful about that.
If you find you have a lot of custom code, consider moving it to a plugin for re-use across multiple builds.
It’s very easy to create a plugin, as teased earlier and discussed at more length here.
This page has been a quick taste; there’s much much more about custom tasks on the Tasks page.