SBT - Scalaās ((mistaken as) NOT SO) Simple Build Tool
The first thing that one notices when jumping into Scala development is its utterly complicated build tool. Complications arise due to several reasons (and reasons different for different people, Iām sure) but, a consistent gripe that people seems to have is the syntax used in the build definition file (typically build.sbt although, any *.sbt would work at the root of the project) for people not very familiar with some operators/constructors that the Scala community heavily relies on and, the monstrous amount of time it takes to get the first build started. The syntax seems strange at times even for Scala developers not familiar with sbt, that is because sbt is a DSL (Domain Specific Language). Letās dive in to understand the essential basics!
[When I first ran āsbt runā, I wasnāt sure if things were even working at all, stared into the terminal for few 10s of minutes for several hours and nothing showed up. Google searches showed that it was a common problem! Although, in my case it was a conflict between the versions of Scala and sbt that were individually installed on my machine!]
With that welcoming note, letās dive into the sbt world - it is bound to make your life easier if you know what you are doing. And it is an extremely powerful build tool.
I highly recommend that you read the sbt getting started guide completely before starting your project (this post is not a replacement as there are several details that are important and wonāt be discussed here). Iām assuming that youāve installed a recent version of sbt already. Iām writing this to provide a quick start (which might be a lot quicker than the āQuick Start Guideā that sbt documentation provides.)
Getting Started With āsbt newā
The recent versions of sbt (later than 0.13) provide a quick way to kick start your Scala projects Ā -
$ sbt new sbt/scala-seed.g8
sbt uses g8 templates to create the basic structure of your project. There are tons of useful ones available and you can create your own as well. If you come from the Java world, think of these as Maven archetypes of sorts. The command above would ask you for some project details like the project name, my projectās name is: Whipp. (For reasons unknown to humanity!)
The previous command should set up a simple Scala project for you with a directory structure that resembles that used by Maven:
src/[main, test]/[scala, java, resources]
$ tree -L 2
.
āāā build.sbt
āāā project
ā Ā āāā Dependencies.scala
ā Ā āāā build.properties
ā Ā āāā plugins.sbt
ā Ā āāā project
ā Ā āāā target
āāā src
ā Ā āāā main
ā Ā āāā test
āāā target
Ā Ā āāā scala-2.12
Ā Ā āāā streams9 directories, 4 files
The file that would the focus of our discussion in this post is the build.sbt file - the build definition.
(Donāt forget to add target/ to your .gitignore before you make your initial commit.)
Building and running the project is simple:
$ sbt #takes you to the sbt shell from where you can issue commands like run [info] sbt server started at 127.0.0.1:5522 sbt:Whipp> run #The task we wish to execute.
This would compile the project files and start the project. This could take a while if you are running your first project. Patience trumps hate!
Some cool things about the sbt shell are its tab-completion feature and history. It essentially behaves like your terminalās shell. Pretty cool huh?
You could also provide tasks in a ābatch modeā like this:
#takes sbt:Whipp> clean compile run
sbt provides a continuous build option. Append a task with ~ and sbt would watch for changes in your files, auto compile them and, execute the task - for instance, ~run would add efficiency to your development setup!
Hereās a list of all the commands available for you to use: https://www.scala-sbt.org/1.x/docs/Command-Line-Reference.html
The build definition - build.sbt
This is the most important and interesting section of this post so grab your coffee and read on! The key element in understanding the build.sbt file is that it is not just a properties file but, valid Scala code (well, valid sbt DSL code to be precise but, you get the drift!)
Read the generated *.sbt file with that perspective first and see what you can gather from it:
import Dependencies._
lazy val root = (project in file(".")).
Ā settings(
Ā Ā inThisBuild(List(
Ā Ā Ā organization := ācom.snortingcodeā,
Ā Ā Ā scalaVersion := "2.12.3",
Ā Ā Ā version Ā Ā Ā := "0.1.0-SNAPSHOT"
Ā Ā )),
Ā Ā name := āWhippā,
Ā Ā libraryDependencies ++= Seq(
Ā Ā Ā scalaTest % Test)
Ā )
(Depending on which seed project you start with, this might look a little different. Hold on for a while and it would make a lot of sense to you!)
Letās first look at what the below line is all about:
ālazy val root = (project in file(ā.ā))ā
We are declaring a lazy value ārootā which happens to be everything followed by ālazy val rootā in this case. Your build file is composed of several āvalā and/or ālazy valā, these would typically fall under these 3 categories:
In fact, if you take a look at the config-classes under the target/ directory, you would find a cache file that has all the lazy val and val that you defined in your build.sbt file. You can also use the sbt shell to see what these keys are.
With that in mind, if you look at the build.sbt again, youād see that we have a lazy val called root that contains the settings for each āprojectā in the current directory [project in file(ā.ā)]
Some of these are generic properties like the name of the project, organisation, etc. These are unlikely to change (it is a common practice to extract them out to a val and add it to the project.settings.)
In that sense, most of the build definition is just a bunch of key-value pairs => some help define things, some do interesting stuff. The value ānameā is an example of a key that defines something.
A quick digression: If you try to assign a non-String value to ānameā, you would get a typeMismatch error. The key ānameā (which we did not define ourselves) comes along with the implied imports => sbt._, Keys._
Letās try to add some interesting key that does something. Naturally, we would turn to a TaskKey[T]. Add these lines to your build definition:
lazy val doSomething = taskKey[Unit](āI do something cool!ā)
Now, add this key to your settings. The updated build.sbt might look something like this:
import Dependencies._
lazy val root = (project in file(".")).
Ā settings(
Ā Ā inThisBuild(List(
Ā Ā Ā organization := ācom.snortingcodeā,
Ā Ā Ā scalaVersion := "2.12.3",
Ā Ā Ā version Ā Ā Ā := "0.1.0-SNAPSHOT"
Ā Ā )),
Ā Ā name := āWhippā,
Ā Ā libraryDependencies ++= Seq(
Ā Ā Ā scalaTest % Test),
Ā doSomething := {println(āLook what I did!ā)}
Ā )
lazy val doSomething = taskKey[Unit](āI do something cool!ā)
We defined a TaskKey[Unit] named doSomething using the method in the sbt DSL - taskKey[T]. Once you enter the sbt console (by restarting the sbt shell), you would notice that you can call this new task from the shell, simply by typing the taskās name - doSomething.
sbt:Whipp> doSomething
Look what I did!
[success] Total time: 1 s, completed 8 Feb, 2018 1:59:01 PM
You can thus, create a DAG (Directed Acyclic Graph) using these tasks and create a workflow for your project! Sweet!
A neat way to see what a task is doing is to āinspectā it:
sbt:Whipp>inspect compile
[info] Task: xsbti.compile.CompileAnalysis
[info] Description:
[info] Compiles sources.
[info] Provided by:
[info] {file:/Users/prasoonjoshi/code/whipp/whipp/}root/compile:compile
(The actual output is rather long, so Iāve truncated it. Each [info] tells you important information about that task. (Try this out with the doSomething task you created just now and see if everything makes sense!)
We talked about the SettingKey[T] and TaskKey[T] a little bit, what is this InputKey[T] all about? Well, for now, you can just think of the InputKey as a method to take input from the user to create tasks on the fly (simple and powerful - weāll talk about this in a future post soon!)
Letās jump to the libraryDependencies
Yes, you know what they are! The essential information about this section of your build definitionās settings are:
+= is a method to append the provided dependency to the old libraryDependencies and return the new one!
libraryDependencies ++= Seq[<your list of dependencies>] is another way youāll see the dependencies often defined in the build definition. (This is what our example build definition has.)
sbt uses Ivy for dependency resolution. The % that you see in the syntax below are sbt DSLās methods that construct the Ivy module Id.
To define a new libraryDependency, you need to define the dependency first, then add it to the list of dependencies.
Hereās an example that adds mongo awesomeness to your project:
val mongo = "org.reactivemongo" %% "reactivemongo" % "0.12.6"
libraryDependencies += mongo#
# This line is part of my plugins.sbt file under the project directory.
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.7")
libraryDependencies += ws
Of course, you can add the val directly to the settings without declaring it first. See what works best for you!
I promised earlier that everything in your build definition is just a key (SettingKey, TaskKey, InputKey). See what the libraryDependencies key shows you on the sbt shell and see if that makes sense :)
A final note on the evaluation order in your build definition file:
Since the build definition is just a DAG which is interspersed with ālazy valā, you should expect a sequential processing, with lazy val(s) evaluated only when they are required.
A point to keep in mind is that the SettingKeys are evaluated once, TaskKeys are evaluated each time the task is executed (these would usually have side-effects as you can see).
There are several beautiful aspects of sbt that we canāt explore here in this quick guide. I would encourage you to check out the documentation and Josh Suerethās great talk on YouTube.
[Please do reach out to suggest improvements or corrections to this post :) ]