A while back I started programming Scala for the first time. It was also the first introduction to the scala build tool. The defacto standard build tool for compiling and packaging scala applications.
The first steps were easy, I had projects up and running in no time. But now I've come across a scenario where I need to create a piece of software that actually is composed of multiple
projects. This presented me with an interesting problem. How can you build a project with SBT that depends on other projects that also use SBT.
It turns out that it's not too bad. But you need to know a few small tricks to get it working correctly.
In this post I will show you how you can get multi-project builds running in Scala in less time than you think.
Defining the project structure
SBT uses a specific folder structure for its projects. It looks like this:
- build.sbt
- project
- build.properties
- plugins.sbt
At the root of every project there's a build.sbt file which contains all the properties
needed for your build. It doesn't normally contain any executable code. If you want
to define executable code as part of your build you need to create a new .scala file
inside the project folder. The SBT build will automatically include and compile the .scala
file in the project folder when you start the SBT tool.
To create a folder structure for a piece of software that is split into separate projects
you define the folder structure described above in the root folder. To add a child project
you need to create a new folder for the child project under the root folder and place the same
files in that folder.
Hooking up the projects
The next step is to create a new scala file to define the relationships between the child projects.
Why use a scala file? We're actually going to define a piece of the build model, which should be
executable so you need a new .scala file in the project file to do this. The name doesn't matter,
as long as it is something that is recognizable for people later on.
Inside the file you define a new class which looks like this
import sbt._
import Keys._
object MyBuild extends Build {
lazy val root = Project(id = "root", base = file("."))
}
Now to add a reference to the child projects. For now imagine that we have a
core project which contains all the basic functionality of the project and a
console project that contains a console interface for the project.
To define the described project structure you need to add more lazy values of type
project to the scala file you just created.
import sbt._
import Keys._
object MyBuild extends Build {
lazy val root = Project(id = "root", base = file("."))
lazy val core = Project(id = "core", base = file("core"))
lazy val console = Project(id = "console", base = file("console"))
}
Now if you start sbt right now and you enter the command projects it will
display the root, core and console project. You can even switch between projects
by entering project [name]
. Run your regular commands after that just like you normally would.
Defining dependencies between projects
It so happens that the console project is dependening upon the core project, because
the console interface obviously needs to talk to the core to get access to your
application's functionality. To make this work, you need to define that console
depends on core in the build scala file you just created.
import sbt._
import Keys._
object MyBuild extends Build {
lazy val root = Project(id = "root", base = file("."))
lazy val core = Project(id = "core", base = file("core"))
lazy val console = Project(id = "console", base = file("console")) dependsOn "core"
}
Now that we have declared that console depends on core, the classes from core
will be available to the project console. Also, when you run commands like compile
on the console project, SBT will make sure that the core project is also compiled.
You can depend on multiple projects, too. Just add more projects, separated by a comma
to the list of projects in the dependsOn call.
Compiling all projects at once
With the dependencies set up you may want to compile all projects in one go. To do
this you specify an aggregation.
import sbt._
import Keys._
object MyBuild extends Build {
lazy val core = Project(id = "core", base = file("core"))
lazy val console = Project(id = "console", base = file("console")) dependsOn "core"
lazy val root = Project(id = "root", base = file(".")) aggregate "core", "console"
}
Now when you start SBT you can invoke compile and it will compile the root, core and console projects
all at once. All tasks you run in a project that is an aggregate of other projects will be run
against all projects defined in the aggregate statement.
Here's another cool trick: You can define an empty project folder and use that as an aggregate
for a subset of projects. This works, because SBT only requires a build.sbt file to be present
in a directory to define a project. The build.sbt file in that directory can be empty, but you
can of course define properties specific for that subset if you want.
Final thoughts
As much as I think SBT is a little strange, once you know how to set up multi-project builds it works quite nice..