SBT - simple build tool for Scala projects

SBT is the favorite build tool for Scala developers, it is used by the Play framework and by the Activator module from Typesafe.

SBT is easy to get started with, offers a wide range of features and is extremely easy to extend. It is the perfect mix between Maven and Ant, but using a Scala DSL instead of XML.

In this post, we will introduce SBT and the key-value model behind it. We will also see how to use it for dependency management.

Setup

To install SBT locally, just follow the instructions on this page.

Directory layout

Even with a fresh setup of the binary, you can already compile without any SBT configuration file.

  • All you have to do is create a class, java or scala, in an empty directory
public class Hello {  
  public static void main(String[] args){
    System.out.println("Hello");
  }
}
  • run: sbt compile
$ sbt compile
[info] Compiling 1 Java source to /Users/jpbunaz/workspace/tutorial/sbt/blog/target/scala-2.10/classes…
  • run: sbt run:
$ sbt run
[info] Running Hello
Hello.  

SBT follows the convention over configuration principle, and compiles any class in the root directory where it is executed. The run command also looks for any executable class to launch.

For the directory layout, SBT follows Maven’s standard directory layout:

src
  main
  javaJava sources
  resourcesApplication resources
  scalaScala sources
test
  javaJava test sources
  resourcesTest resources
  testScala test sources
 

Usage

SBT has two modes. The batch mode runs a command and immediately exits while the interactive modeis the SBT command prompt.

Batch mode

To launch tasks, you just have to launch SBT followed by your list of commands:

$ sbt clean compile`

If a command takes arguments, enclose them in quotes:

$ sbt clean compile "myTaksWithArguments arg1 arg2 arg3"

Unlike Maven, to run SBT tasks on a sub-project, you stay in the root directory and give the relative path of the sub-project:

$ sbt mySubProject/clean

Interactive mode tour

To launch the interactive mode, type sbt without any argument:

$ sbt
[info] Set current project to blog (in build file:/Users/jpbunaz/workspace/tutorial/sbt/blog/)

You can then run any SBT command:

> compile
[info] Updating {file:/Users/jpbunaz/workspace/tutorial/sbt/blog/}blog...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Compiling 1 Java source to /Users/jpbunaz/workspace/tutorial/sbt/blog/target/scala-2.10/classes...
[success] Total time: 2 s, completed 28 oct. 2015 16:52:41

You can now interact with SBT and take advantage of the auto-completion:

 comp   // press tab
compile                   compile:                  compileAnalysisFilename   compileIncremental        compilerCache  
compilers                 completions  

You can get a history of the commands with !:

> !
History commands:  
   !!    Execute the last command again
   !:    Show all previous commands
   !:n    Show the last n commands
   !n    Execute the command with index n, as shown by the !: command
   !-n    Execute the nth command before this one
   !string    Execute the most recent command starting with 'string'
   !?string    Execute the most recent command containing 'string'
>

Get a list of tasks available from you SBT configuration file:

> tasks
This is a list of tasks defined for the current project.  
It does not list the scopes the tasks are defined in; use the 'inspect' command for that.  
Tasks produce values.  Use the 'show' command to run the task and print the resulting value.

  clean    Deletes files produced by the build, such as generated sources, ...
  compile  Compiles sources.
...
  update   Resolves and optionally retrieves dependencies, producing a report.

More tasks may be viewed by increasing verbosity.  See 'help tasks'.  

One of the most interesting features in SBT is the continuous mode, to automatically recompile or run the tests whenever you save a source file. This is especially useful to run tests in the background when coding. All you have to do is to precede your task with “~”:

 ~ test
[info] Updating {file:/Users/jpbunaz/workspace/tutorial/sbt/hello/}util...
[info] Updating {file:/Users/jpbunaz/workspace/tutorial/sbt/hello/}root...
[info] Resolving jline#jline;2.12 ...
[info] Done updating.
[info] Resolving org.apache.derby#derby;10.4.1.3 ...
[info] Compiling 1 Scala source to /Users/jpbunaz/workspace/tutorial/sbt/hello/util/target/scala-2.11/classes...
[info] Resolving jline#jline;2.12 ...
[info] Done updating.
[info] Compiling 1 Scala source to /Users/jpbunaz/workspace/tutorial/sbt/hello/target/scala-2.11/classes...
[success] Total time: 5 s, completed 28 oct. 2015 17:16:48
1. Waiting for source changes... (press enter to interrupt)  

How does a SBT build work?

You can see SBT projects as a Map describing them, and the execution of the build will transform the Map by applying a list of Setting[T] (T being the type of value in the Map).

When you write the build definition, all you have to do is construct a list of Setting[T] to apply to the Map.

For example, let’s assume we start with an empty Map and apply the following settings:

  1. Add the value “hello” to the entry “name” (“name” being the project name by convention)
  2. Generate a Jar file from the project name, associated to the entry “package”

In this example, step 2 depends on step 1. SBT is smart enough to order the list of settings to apply and execute step 1 before step 2.

Map() -> Setting 1 -> Map("name" -> "Hello") -> Setting 2 -> Map("name" -> "Hello", "package" -> new File("target/hello.jar"))  

A Key could have 3 types:

  • SettingKey[T]: the value is executed once, when the build is started
  • TaskKey[T]: the value is executed every time the task is run. The task could have side effects, such as writing files on disk
  • InputKey[T]:A TaskKey[T] with command line arguments

Beware, a TaskKey is always run only once per execution. SBT will look for all the dependencies and will deduplicate identical tasks. Therefore, you can not describe a task with the following algorithm:

  • call clean task
  • generate classes from sources
  • call clean task again

The classes generated in the second step will not be cleaned because the clean task has already been run in step 1.

The last important thing to understand how SBT works is the Scope. In the Map generated by the SBT execution, the key is not formed by the name but by the name + the scope. This allows you to have multiple values for the same key.

There are 3 scopes:

  • Project: The value name has a different value depending on the sub-project
  • Configuration: The value is different depending on if you are using a test or compilation configuration.
  • Task: The value is different depending on the task where it is declared

How to describe a build?

There are 3 ways to describe a SBT build:

  • A bare .sbt build definition
  • A multi-project .sbt build definition
  • A .scala build definition

The .scala build definitions are placed in a project sub-directory. This directory is itself a new SBT project with the Scala source code necessary for the build. The build and application sources are then well separated. If you want to turn your Scala build sources into a SBT plugin, you already have a separated SBT project!

In this post, we will not use the .scala build definition but we will use the project sub-directory to write tasks code and a build.sbt file in the project’s root to describe the build.

The build.sbt can have two versions, depending on if you are using it for a simple project or multiple projects.

SimpleProject.sbt

name := "My first project"  
version := "1.0-SNAPSHOT"  
organization := "usa.ippon.blog"  

MultipleProjects.sbt

lazy val root = (project in file(".")).  
  settings(
    name := "My first project",
    version := "1.0-SNAPSHOT", 
    organization := "usa.ippon.blog"
  )

A SBT build definition is just a list of Setting definitions processed to transform the Map. When you work with a simple project, without extending any feature, Keys are SettingKey[T] by default.

Dependency management

There are two ways to deal with dependencies in SBT.

Unmanaged mode

The unmanaged – or manual – mode just requires dropping JAR files in the lib directory and they will be automatically added to the classpath by convention. Using scopes, you can have a separate directory for tests. We will not elaborate on this, but who still uses manual dependencies?

Managed mode

In this mode you add values to the libraryDependencies key which is of type SettingKey[Seq[ModuleID]]. Therefore, your dependency declaration needs to be of type ModuleID, which is greatly simplified thanks to the DSL in build.sbt:

val derby: ModuleID = "org.apache.derby" % "derby" % "10.4.1.3" % "test"  
                              |              |           |          |
                        organization       name       version     scope

lazy val root = (project in file(".")).  
  settings(
    name := "My first project",
    version := "1.0-SNAPSHOT", 
    organization := "usa.ippon.blog",
    libraryDependencies += derby,   // Add only one dependency
    libraryDependencies ++= Seq(scalaTest, hibernate)  // Add multiple dependencies
  )

If you use “%%” instead of “%” between the organization and the name declaration, SBT will pick a dependency specific to your Scala version:

"org.scalatest" %% "scalatest" % "2.2.4" % "test"

With a 2.11 Scala version declared, it will automatically translate like the following Maven declaration:

<dependency>  
  <groupId> org.scalatest</groupId>
   <artifactId>scalatest_2.11</artifactId>
   <version>2.2.4</version>
   <scope>test</scope>
</dependency>  

To manage libraries, SBT uses Ivy under the hood. Therefore, it is possible to use dynamic version declaration. For example, you can declare a 2.2+ version to automatically upgrade to fixes on the 2.2.x version of your dependency.

See this page to know more about dynamic versions.

You can add new dependency repositories by adding a value to the resolvers key:

resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"  

Scala / SBT options

Compiling for multiple scala versions:

scalaVersion := "2.11.4"  // Scala version for the project  
crossScalaVersions := Seq( "2.10.4", "2.11.0")  
// The project will be built for each declared version if you precede a task with “+”
> + compile
[…]
> + package

Another very useful option to deal with different SBT versions installed in each developer local environment is the possibility to specify the SBT version to use. For this, you need to create a build.properties file in the project directory:

// No matter the SBT version installed, 0.13.9 will be downloaded and used for the build
sbt.version=0.13.9  

Conclusion

SBT is a powerful tool to describe your build and manage your dependencies.

Understanding the key-value model behind the scenes and its immutable nature can help debugging and also writing plugins.

SBT is the de facto standard tool for Scala projects and we encourage you to use it.


Found this post useful? Kindly tap
Author image
Software engineer, continuous delivery and data intensive applications.
Washington, DC LinkedIn
OUR COMPANY
Ippon Technologies is an international consulting firm that specializes in Agile Development, Big Data and DevOps / Cloud. Our 300+ highly skilled consultants are located in the US, France and Australia. Ippon technologies has a $32 million revenue and a 20% annual growth rate.