1. Introduction

In this article, we’ll introduce some of the key features of SBT with examples.

SBT has been the most popular and the most used build tool in the Scala ecosystem for quite some time now. This wide acceptance and its variety of features make it the obvious choice for every Scala project. Another key point on why SBT is so popular is the fact that the build definition is written in Scala.

2. Installation and First Project

Similar to Maven, SBT can be downloaded as a compressed folder; the only installation needed is the uncompression. In addition to the unzip method, SBT can be installed via other tools, based on the platform you’re on. For example, we can install SBT on a Mac with brew, with SDKMAN on a Debian-based Linux, and with the MSI installer on Windows.

Now that we have SBT installed, it’s time to create a simple project with giter8:

sbt new scala/scala-seed.g8

A new project directory is created after we follow the giter8 instructions. The directory structure of an SBT project is:

project/
  build.properties
src/
  main/
    resources/
    scala/
  test/
    resources/
    scala/
build.sbt

3. Workflow

In order to execute SBT commands, we need either to enter the SBT shell via the SBT command or to prepend every command with sbt. In our examples, we’ll use the second approach and prepend every command with sbt. The following sections will describe the most frequent SBT tasks.

3.1. Compile

To compile and check for any compilation errors, we use the compile command:

sbt compile
[info] welcome to sbt 1.6.2 (Oracle Corporation Java 11.0.12)
// more logs
[success] Total time: 2 s, completed Jun 28, 2022, 2:45:40 PM

3.2. Test Compile

The test:compile command compiles test classes. It’s important to note that before the test classes, SBT first compiles the non-test classes. Let’s execute the test:compile command:

sbt test:compile
[info] welcome to sbt 1.6.2 (Oracle Corporation Java 11.0.12)
[info] compiling 1 Scala source to /scala-tutorials/scala-sbt/intro-to-sbt/target/scala-2.13/classes ...
[info] compiling 1 Scala source to /scala-tutorials/scala-sbt/intro-to-sbt/target/scala-2.13/test-classes ...
[success] Total time: 3 s, completed Jun 28, 2022, 2:48:53 PM

3.3. Test

As shown in the directory structure above, /src/test/scala/ is the standard location for test sources. Luckily for us, the giter8 template has already implemented a HelloSpec. Let’s see the test command’s output:

sbt test
[info] welcome to sbt 1.6.2 (Oracle Corporation Java 11.0.12)
// more logs
[info] HelloSpec:
[info] The Hello object
[info] - should say hello
[info] Run completed in 212 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 1 s, completed Jun 28, 2022, 2:52:43 PM

3.4. Run

The run command executes the main function of the project. The main function can be configured via the mainClass property or we can extend the scala.App trait. As a convenience, the giter8 template comes with a class that extends the scala.App trait. Let’s execute the run command:

sbt run
[info] welcome to sbt 1.6.2 (Oracle Corporation Java 11.0.12)
// more logs
[info] running com.baeldung.Hello 
hello
[success] Total time: 0 s, completed Jun 28, 2022, 2:57:46 PM

But as software engineers, we often like to put tools under various tests. The next execution demonstrates how the run command handles multiple main classes:

sbt run
[info] welcome to sbt 1.6.2 (Oracle Corporation Java 11.0.12)
// more logs
Multiple main classes detected. Select one to run:
 [1] com.baeldung.Hello
 [2] com.baeldung.Hello2

Enter number: 2
[info] running com.baeldung.Hello2 
hello 2
[success] Total time: 278 s (04:38), completed Jun 28, 2022, 3:06:51 PM

3.5. Clean

The clean command removes the previously generated target directories:

sbt clean
[info] welcome to sbt 1.6.2 (Oracle Corporation Java 11.0.12)
// more logs
[success] Total time: 0 s, completed Jun 28, 2022, 3:10:32 PM

4. The build.sbt File

The build.sbt file, similarly to the pom.xml for Maven projects, describes everything that happens during the build process. In this section, we’ll describe its most used concepts.

4.1. Library Dependencies

We can add additional libraries with the libraryDependencies key:

// one library
//libraryDependencies += groupID % artifactID % revision
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test
//multiple libraries
// libraryDependencies ++= Seq(
//   groupID1 % artifactID1 % revision1,
//   groupID2 % artifactID2 % revision2
// )
libraryDependencies ++= Seq(
  "org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test,
  "org.mockito" % "mockito-core" % "3.5.13" % Test
)

4.2. Tasks

In general, tasks produce values and are referenced by a key. Tasks can produce any Scala type. Let’s see a simple task that prints “hello” to the console:

lazy val printHello = taskKey[Unit]("prints hello")
printHello := println("hello")

Let’s run our printHello task:

sbt printHello
[info] welcome to sbt 1.6.2 (Oracle Corporation Java 11.0.12)
// more logs
hello
[success] Total time: 0 s, completed Jun 29, 2022, 2:31:38 PM

Also, tasks can have other tasks or attributes as input. To see it in action, let’s write a task that takes the value of another task and multiplies it:

lazy val one = taskKey[Int]("one")
one := 1

lazy val oneTimesTwo = taskKey[Unit]("one times two")
oneTimesTwo := println(s"2 * 1 = ${2 * one.value}")

Let’s run the oneTimesTwo task:

sbt oneTimesTwo
[info] welcome to sbt 1.6.2 (Oracle Corporation Java 11.0.12)
// more logs
2 * 1 = 2
[success] Total time: 0 s, completed Jun 29, 2022, 2:40:49 PM

4.3. Commands

Commands are named operations that take as an argument the current state of the build and compute the new state. While command definition looks very similar to task definition, commands are more powerful since they can access and potentially change the state. Let’s define a command that changes the state and adds another command to the state:

lazy val helloCommand = Command.command("helloCommand")(state => {
  println("hello from command")
  state
})

lazy val helloCommand2 = Command.command("helloCommand2")(state => {
  println("hello from command 2")
  state.copy(definedCommands = state.definedCommands :+ helloCommand)
})

lazy val root = (project in file("."))
  .settings(
    // other settings ..
    commands ++= Seq(helloCommand2)
  )

It’s important to note that the helloCommand is not added to the project commands. As a result, the helloCommand is not available before calling helloCommand2. Also, for this example, we need to use the SBT shell so the state is preserved:

sbt
[info] welcome to sbt 1.6.2 (Oracle Corporation Java 11.0.12)
// more logs
sbt:intro-to-sbt> helloCommand
[error] Not a valid command: helloCommand (similar: helloCommand2, reload)
[error] Not a valid project ID: helloCommand
[error] Expected ':'
[error] Not a valid key: helloCommand (similar: watchBeforeCommand, commands, hello)
[error] helloCommand
[error]             ^
sbt:intro-to-sbt> helloCommand2
hello from command 2
sbt:intro-to-sbt> helloCommand
hello from command

4.4. Command Aliases

In case we want to execute a group of tasks or commands, we can utilize command aliases. Let’s write an alias that calls compile and test:

addCommandAlias("compileAndTest", "compile;test;")

And let’s check the output of our sbt compileAndTest alias:

sbt compileAndTest
[info] welcome to sbt 1.6.2 (Oracle Corporation Java 11.0.12)
// compile logs
[success] Total time: 0 s, completed Jun 29, 2022, 3:26:46 PM
// test logs
[info] All tests passed.
[success] Total time: 3 s, completed Jun 29, 2022, 3:26:49 PM

4.5. Resolvers

The resolvers setting is how we add repositories for the SBT to look up dependencies. Here, we add Typesafe and Maven2 repositories:

resolvers ++= Seq(
  "Typesafe" at "https://repo.typesafe.com/typesafe/releases/",
  "Java.net Maven2 Repository" at "https://download.java.net/maven/2/"
)

5. Plugins

SBT plugins mainly contain build definitions. In other words, plugins enable us to reuse code for our builds. To add a new plugin to a project, we need to define it in the project/plugins.sbt file.

Let’s add the scalafmt plugin to our project:

addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")

6. Conclusion

In this article, we’ve presented SBT’s key concepts and features using a few handy examples.

As always, the code of the above examples is available over on GitHub.

Comments are closed on this article!