Previously, I showed you how to write a SBT build definition without knowing much at all. Neither did I talk about simple things like directory structure nor about advanced things like scope or axis, yet you could pretty easily write a SBT build definition for a library and setup publishing. All by writing Scala code (DSL for SBT).
- How I SBT -
build.sbt
- How I SBT - Settings & Tasks
- How I SBT - Plugins
- How I SBT - Build Code Organization
- How I SBT - Multi-module Builds
- How I SBT - Publishing a Plugin
Today, we are going to talk about Settings and Tasks.
Settings
While I was repeatedly using the term settings in a general way, SBT has the concept of Settings (defined as SettingKey[T]
); along with Tasks. Settings are like variables; typed! They are evaluated once and cached when SBT loads your build definition. Any assignments using :=
you saw in the previous post is a Setting.
name := ...,
scalaVersion := ...,
When I say evaluated once, think of val x = <value>
. However, the value may be mutable itself but x
is not. Why do I say that? Because the value of Settings like libraryDependencies
, dependencyOverrides
may be updated.
libraryDependencies ++= <dependency-1> :: <dependency-2> :: Nil
Assigning libraryDependencies := <dependencies>
will reset the value to <dependencies>
(unlike ++=
or +=
)
In any case, the Settings are evaluted once and cached for the build no matter how many times you have assigned or updated in your build definition.
Settings
recap:
- Produce a value. Value assigned using :=.
- Evaluated once when sbt starts up, and cached.
- Can only depend on other settings
- For values that donβt change during a build
Let us take a look at how one of the Settings, say version
, is defined in SBT:
val version: : SettingKey[String] =
settingKey[String]("The version/revision of the current module.")
Would you like to define your own (custom) setting? You will have to wait until we talk about plugins. Because defining a custom setting directly in build.sbt
has little value. Providing a setting via plugins is a standard mechanism for custom plugins.
Hold your horses. On to next Tasks (pun intended).
Tasks
If Settings are like variables, Tasks are like functions. In fact, they are. They are executed and produce values as many times as they are referenced for their value. A lot of the commands that you issue to SBT on the command line are actually backed by tasks - compile
, clean
, publishLocal
, test:compile
etc.
Here is how to declare one of your own:
lazy val greet = taskKey[Unit]("Greets the user before compilation starts")
greet := {
val whoami = System.getProperty("'user.name'")
println(s"May the force be with you, $whoami")
}
// .... then in your build definition
.settings(
Compile / compile := (Compile / compile).dependsOn(greet).value
)
Alright, so …
- Tasks can be reassigned. Here, the task
Compile / compile
is reassigned by evaluating based on its current value, which is queried using.value
. Note that the value of a Task can be queried only within another Task. For instance, you cannotsomeSetting := someTask.value
, which a lot of developers havea trouble with in their intial days with SBT. Compile / compile
denotes compilation task of the main sources.Test / compile
denotes compilation task of the test sources.compile
is the Task.Compile
andTest
are Scopes. I won’t say anything more about Scopes; not now.
.dependsOn
says that the given task (Compile / compile
) depends on another task - our task,greet
. That means, every time you issue a compile command, you will be see the Star Wars message first then the code will compile. After all, it is fair to greet and bless before you undertake a big endeavor.
Tasks and Return Values
The greet
task does not return any value. But Tasks may return value:
lazy val arbString: task[String]("Generates a random string")
arbString := {
scala.util.Random.nextString(10) // Implementation not important here
}
Why do Tasks have to return value? So that they can be compose with other Tasks . It means, one Task can reference another for its value:
lazy val rndFileName: task[String]("Generates a random file Name")
rndFileName := {
val token = arbString.value
s"file-$token.tmp"
}
Tasks and Inputs
Tasks can also take inputs:
lazy val genRndStr = inputKey[String]("Generates a random string of specified length")
genRndStr := {
import complete.DefaultParsers.spaceDelimited
import scala.util.Random
val args: Seq[String] = spaceDelimited("<arg>").parsed
val length: Int = args.headOption.map(_.toInt).getOrElse(10)
Random.alphanumeric.take(length).mkString
}
You can to invoke genRndStr
:
- From command line
sbt 'show genRndStr' # Default length = 10 sbt 'show genRndStr 24'
- Within the build file
val token = genRndStr.toTask(24).value // use token in other tasks / logic
The SBT Shell
Meet one of SBT’s super powers - the SBT shell. The epitome of its advantages is that it loads the JVM once so the successive commands you issue are way faster than invoking them one at a time from your OS shell. But its real power is in giving you an interactive space to inspect your build definition.
$ sbt
sbt:petStore>
sbt:petStore> projects
api
core-lib
* petStore
integration-tests
$ sbt
is launching the sbt shell from the OS prompt, which drops you into the sbt shellsbt:petStore>
petStore
is the name of an imaginary Scala project, which has sub-projects listed by theprojects
command*
in the project list denotes the current project
There are other powerful commands like inspect
that allow you inspect the build definition like tasks, settings etc.
sbt:snorriRoot> inspect genRndStr
[info] Input task: java.lang.String
[info] Description:
[info] Generates a random alphanumeric string of specified length
[info] Provided by:
[info] ProjectRef(uri("file:...../pet-store"), "petStore") / genRndStr
[info] Defined at:
[info] ......../pet-store/build.sbt:13
[info] Reverse dependencies:
[info] testGenRndStr
[info] Delegates:
[info] genRndStr
[info] ThisBuild / genRndStr
[info] Global / genRndStr
The help
Β command in the sbt shell is the man page for all things that you can do in the sbt shell.
Other Tidbits:
sbt -client
launches the client (shell) that connects to the SBT build server, which is started new if one is already running.sbtn
launches the native version of the same.
Cliffhanger
I hope this post gave a quick and clear picture of Settings and Tasks. They are orthogonal and at the same time complementary. For instance, you can think of the name := <app-name>
as an independent Setting while the scalacOptions ++= ...
is a Setting consumed by the compile
task.
I know I mentioned that I will cover plugins in this post. But I like to push it to the next post since this post went longer than expected. But we now know the most we need to move to the next stage - Plugins. In the next post, I will talk about Plugins, which should make you feel confident and content about SBT. Then I hope you should be able to appreciate the modularity and flexibility of SBT.