A quick tour of build tools in Scala

Published on 19 April 2018 , last updated on 31 May 2018

Update 20.04.2018: added Polyglot Maven

Scala has a rich ecosystem and active community producing a lot of useful libraries. So much so that, sometimes it is not easy as a newcomer to decide which library to pick for a given task (this is the case for example when it comes to database access and JSON handling). In this article we are going to cover another domain in which there is an increasing number of alternatives: build tools. At the time of writing this article in April 2018 we have at our disposal:

sbt

cbt

mill

fury

maven

polyglot maven

gradle

ant (to be written)

bazel (to be written)

pants (to be written)

make (to be written)

Let’s take a bit of time and walk through each of these options and see how they work

sbt

sbt, shorthand for sbt, is Scala’s first (or so I think) and most well-known build tool. It is, by now, solid, reliable and has a large set of plugins and integrations.

Installation

sbt is available via Homebrew on on Mac, MSI on Windows and packages on linux (Debian or RedHat). On Mac, you’d install it using

1

2

brew install sbt@1

Creating a new project

New projects can be created using Giter8 templates. For a simple project you can use the scala-seed template:

Defining the build

sbt requires a build.sbt file located at the root of the project to work. Additional build definitions can be defined in Scala files in the project directory. This is what we get from the template:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

~/workspace/sbt-test>cat build.sbt

import Dependencies._

lazy val root=(project infile(".")).

settings(

inThisBuild(List(

organization:="com.example",

scalaVersion:="2.12.4",

version:="0.1.0-SNAPSHOT"

)),

name:="sbt-test",

libraryDependencies+=scalaTest%Test

)

Running the project

There is two ways to working with sbt. You can run a task directly on the command-line shell, or enter the sbt shell. Typically you’ll want to run the tasks from within the sbt shell, because that’s the fastest (starting the shell takes a while). You can start the shell simply by running sbt:

This is a bit like an interface – it tells us the name of the task, its type (this task will return a String) and also lets us define a description.

We then need to implement this task:

1

2

3

4

5

6

lazy val root=(project infile(".")).

settings(

// ...

sampleStringTask:="Hello, world"

)

This is a short example and only gives you a short glimpse at how things work. For more background I invite you to check out the documentation.

Documentation, plugins, community

sbt has a fairly complete documentation, a strong community and a myriad of plugins (the previous link only shows the plugins that are hosted in the general sbt Github organization, but there are many more out there).

cbt

cbt, shorthand for Chris’ Build Tool, is the result of Christopher Vogt having had enough of using sbt, but not quite enough so for choosing an entirely new name. It is a build orchestration tool aiming at using the Scala language and only a few concepts in order to create builds. Unlike sbt, it maps task execution to JVM invocations instead of adding its own layer in between.

Installation

To install cbt you have to clone the cbt repository and can then add the directory to your path:

1

2

git clonehttps://github.com/cvogt/cbt.git

At the first execution, it compiles itself and also nags about being faster when installing nailgun. To install nailgun, run:

1

2

brew install nailgun

Creating a new project

Now that we’re all set, we can let cbt create a new project for us:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

~/workspace>mkdir cbt-test

~/workspace>cd cbt-test

~/cbt-test>cbt tools createMain

Created Main.scala

()

cbt-test>cat Main.scala

packagecbt_test

objectMain{

def main(args:Array[String]):Unit={

println(Console.GREEN++"Hello World"++Console.RESET)

}

}

Defining the build

Without a build file, cbt will use default build settings. It is possible to let it create a new build file:

Creating a new project

Defining the build

The build is defined in a file at the root of the
This gives you the following build definition:

1

2

3

4

5

6

7

8

// build.sc

import mill._

import mill.scalalib._

objectfooextendsScalaModule{

def scalaVersion="2.12.4"

}

The project structure is:

1

2

3

4

5

6

7

8

9

.

|--build.sc

|--foo

|--src

|--foo

|--Example.scala

3directories,2files

What you might notice right away is that this structure is not following the now almost-standard maven project structure. Mill doesn’t impose a particular structure on the projects, instead it allows for quite some flexibility in regards to how modules are layed out. There’s a few common project layouts documented, amongst which an sbt-compatible layout.

Running the project

The common syntax for running project tasks folllows the pattern module.task:

1

2

3

4

5

6

~/workspace/mill-test>mill foo.run

Compiling(synthetic)/ammonite/predef/interpBridge.sc

Compiling/Users/manu/workspace/mill-test/build.sc

[36/36]foo.run

Hello World

Adding dependencies

Mill uses a DSL for adding dependencies to a project:

1

2

3

4

5

6

7

8

9

10

11

12

import mill._

import mill.scalalib._

objectfooextendsScalaModule{

def scalaVersion="2.12.4"

def ivyDeps=Agg(

ivy"com.lihaoyi::upickle:0.5.1",

ivy"com.lihaoyi::pprint:0.5.2",

ivy"com.lihaoyi::fansi:0.2.4",

ivy"org.scala-lang:scala-reflect:${scalaVersion()}"

)

}

Similar to sbt’s %% notation, Scala dependencies are expressed using the :: notation. It’d be nice, that is, if like cbt, Mill did also have a compatible way of accepting the sbt format for the sake of copy-paste.

One thing I’d like to point out here is that mill uses coursier to fetch dependencies. Coursier is quite a bit faster than sbt’s dependency resolution which has personally been somwhat painful to use over the years (it has gotten much better, but used to drive me insane). For example, it supports downloading multiple artifacts in parallel and has no global lock (the infamous Waiting for ~/.ivy2/.sbt.ivy.lock to be available which you are guaranteed to get if you are working with both sbt and IntelliJ at the same time.

Creating custom tasks

Mill defines its own task graph abstraction to handle task definition, ordering and cache. Calling one task from another establishes a dependency, which makes it quite natural to understand. There are 3 types of tasks:

Gradle

Just to mention this possibility as well, there’s a Gradle Scala plugin if you want to use Gradle for your Scala project build.

Great, now which one to pick?

I find this one more difficult to answer than for the database and JSON topics.

Clearly, sbt is well-established and has a large amount of plugins to integrate with many, many things out there. But if you are a newcomer and need to do something for which there isn’t a well-documented plugin or just a documented way of doing things, things can become quite frustrating. And this isn’t only true for newcomers – you’ll find quite a few experienced Scala developers that are frustrated with sbt. See, to some extent it is possible to just get by using sbt without really understanding it and relying on copy-pasting things from Stack Overflow or from other build definitions. It’s when you need to do this one thing that’s a bit different and not so well documented that it gets frustrating. Or maybe things are well-documented, but you don’t have the conceptual model of sbt in your mind (and don’t find a good way to acquire it), so you don’t know how to proceed. Now, I’m not saying “don’t use sbt”. What I’m trying to say is that sbt is hard and that, as a newcomer, you should approach it as something that is hard, which will help a lot with regards to your personal frustration and willingness to learn (because, if it is simple, you shouldn’t need to have to invest time in learning it and it should just work, right?).

And therefore I completely understand that there are a few new alternatives coming up, which is something I feel happens in the Scala community a bit too easily and often – rather than trying to improve existing tools, people go ahead and create new ones. Except that, well, in this case and with its current design, I don’t see any easy way of making sbt radically easier to grasp.

Now, all that rambling doesn’t help. What to pick? Well, here’s what I do:

for projects at clients, unless there’s a strong drive from the client, I’d stick to sbt, because that’s a safe choice

for my own projects, should I one day have time for them, I think I’ll opt for Mill at the moment. It uses Coursier (remember, I wear the scars of waiting about 4 minutes simply for dependencies to resolve), is well-documented, under active development and even if at the moment there isn’t a clear plugin repository I do get the impression that this is just a matter of time

Good to know that there is a plugin, thanks! Yes I’m not arguing that SBT hasn’t made progress – it is quite amazing how it has evolved recently. And once you invest time in learning SBT it becomes manageable – I just think there’s quite a few people that don’t want to spend that time or don’t expect it to be necessary. And at the end of the day I think it is quite normal to have several build tool alternatives in Scala. I mean just looking at other languages (Java, Clojure, etc.) it seems normal that multiple approaches co-exist.