Build definition basics
This page discusses the build.sbt build definition.
What is a build definition?
A build definition is defined in build.sbt,
and it consists of a set of projects (of type Project).
Because the term project can be ambiguous,
we often call it a subproject in this guide.
For instance, in build.sbt you define
the subproject located in the current directory like this:
scalaVersion := "3.3.3"
name := "Hello"
or more explicitly:
lazy val root = (project in file("."))
.settings(
scalaVersion := "3.3.3",
name := "Hello",
)
Each subproject is configured by key-value pairs.
For example, one key is name and it maps to a string value, the name of
your subproject.
The key-value pairs are listed under the .settings(...) method.
build.sbt DSL
build.sbt defines subprojects using a DSL called build.sbt DSL, which is based on Scala.
Initially you can use build.sbt DSL, like a YAML file, declaring just scalaVersion and libraryDependencies,
but it can supports more features to keep the build definition organized as the build grows larger.
Typed setting expression
Let's take a closer look at the build.sbt DSL:
organization := "com.example"
^^^^^^^^^^^^ ^^^^^^^^ ^^^^^^^^^^^^^
key operator (setting/task) body
Each entry is called a setting expression. Some among them are also called task expressions. We will see more on the difference later in this page.
A setting expression consists of three parts:
- Left-hand side is a key.
- Operator, which in this case is
:= - Right-hand side is called the body, or the setting/task body.
On the left-hand side, name, version, and scalaVersion are keys.
A key is an instance of
SettingKey[A],
TaskKey[A], or
InputKey[A] where A is the
expected value type.
Because key name is typed to SettingKey[String],
the := operator on name is also typed specifically to String.
If you use the wrong value type, the build definition will not compile:
name := 42 // will not compile
vals and lazy vals
To avoid repeating the same information, like the version number for a library,
build.sbt may be interspersed with vals, lazy vals, and defs.
val toolkitV = "0.2.0"
val toolkit = "org.scala-lang" %% "toolkit" % toolkitV
val toolkitTest = "org.scala-lang" %% "toolkit-test" % toolkitV
scalaVersion := "3.7.3"
libraryDependencies += toolkit
libraryDependencies += (toolkitTest % Test)
In the above, val defines a variable, which are initialized from the top to bottom.
This means that toolkitV must be defined before it is referenced.
Here's a bad example:
// bad example
val toolkit = "org.scala-lang" %% "toolkit" % toolkitV // uninitialized reference!
val toolkitTest = "org.scala-lang" %% "toolkit-test" % toolkitV // uninitialized reference!
val toolkitV = "0.2.0"
sbt will fail to load with java.lang.ExceptionInInitializerError cased by a NullPointerException if your build.sbt contains an uninitialized forward reference.
One way to let the compiler fix this is to define the variables as lazy:
lazy val toolkit = "org.scala-lang" %% "toolkit" % toolkitV
lazy val toolkitTest = "org.scala-lang" %% "toolkit-test" % toolkitV
lazy val toolkitV = "0.2.0"
Some frown upon gratuitous lazy vals, but Scala 3 lazy vals are efficient,
and we think it makes the build definition more robust for copy-pasting.