Intro
Contrary to the unpopular opinions that it is hard and clumsy, SBT, the de facto build tool for Scala, is one of the best. Ease comes with familiarity. Unfortunately, there aren’t many beginner-friendly guides for specific scenarios. A lot of troubleshooting information is hidden in the forests of issues and public forums. This is despite the fact that SBT has excellent support for inspecting your project definition. The official documentation is like the encyclopedia that is too much to digest.
Given my loyalty to SBT, I decided to write a short series of posts to give people a second chance with it. Target audience may be newcomers1, those facing any difficulties in understandong SBT’s project model or having second thoughts about it. The goal of this series is to prove that it is simple and easy to write an SBT build file. Think of it as an SBT primer to break the ice.
This short series of posts is not the comprehensive SBT guide. For in-depth details and internals, you should refer the official SBT documentation. But I am hoping reading this series of posts will be enough to write a decent SBT build definition without consulting the official documentation.
- How I SBT -
build.sbt
- How I SBT - Settings & Tasks
- How I SBT - Plugins
- How I SBT - Build Code Organization
- How I SBT - Multi-module Builds
- How I SBT - Publishing a Plugin
- How I SBT - Final Edition - a la carte
In this post, I will not be talking about jargon, the infamous axis and all. I have seen it puts off people, especially newcomers. I will start by “declaring”2 a build definition. Yes, directly getting hands dirty - learn by doing.
I will be using Scala 2.12.20 and SBT 1.9.9 for our exercise3.
Some good things in SBT to sway your vote:
- Build definition is declarative
- Build definition is Scala code
- Excellent multi-module build support
- Excellent plugins support. Easy to write your own.
- Cross(-Scala) compilation support is no-brainer
- SBT has a shell where things are pretty fast and at quick reach
- Support for compiling against a specified JDK
Writing a build.sbt
The build definition goes in the canonical build.sbt
, which is placed at the root of the project directory.
I like to write the build definition as a program declaration. I try to avoid littering settings outside the build definition. I prefer to keep my build.sbt
as readable and modular as possible. Let me show you …
lazy val petStore =
project
.in(file("."))
.settings(
name := "pet-store",
version := "0.1.0-SNAPSHOT",
organization := "hkt",
scalaVersion := "2.12.20"
)
.settings(
libraryDependencies ++=
"org.typelevel" %% "cats-core" % "2.12.0" ::
"org.scalatest" %% "scala-test" % "3.12.19" ::
Nil
)
You get the idea. We specify all the settings as part of the project’s fluent API. It gives a clear view of the different parts of your build definition.
Note …
- I have used multiple
.settings
calls to group different settings - For
libraryDependencies
, I have usedList
syntax (::
) instead ofSeq
(and commas) because it is cleaner and easier to comment or move definitions up and down. I do the same for other settings that take in aSeq
. - I prefer not to use
.com
,.net
etc. prefixes for the organization, which is analogous to maven’s group ID. Hence, justhkt
. - There is one situation when I throw some settings outside the project definition fluent API pipeline -
ThisBuild
. I am skipping it now on purpose, but I will touch on that later.
I will present more such patterns as we make progress.
Resolvers and Credentials
Resolvers are repositories where SBT will look for project dependencies. You don’t need to explicitly add resolvers if your dependencies are (open source) libraries available in Sonatype or Maven repositories. SBT sets up those resolvers out of the box. On the other hand, you might have to add resolvers to build settings if your dependency does not come from a predefined provider. Or if it is internal to your organization in a Nexus or JFrog Artifactory. Like so …
lazy val petStore =
project
.in(file("."))
.settings( name, version etc.)
.settings(
resolvers ++=
("MyOrg" at "https://artifactory.myorg.com/maven") :: // For regular JAR artifacts
Resolver.url("MyOrgIvy" at "https://artifactory.myorg.com/sbt")(Resolver.ivyStylePatterns) :: // Ivy style resolution for SBT plugins
Nil
)
.settings(
libraryDependencies ++= ...
)
If your organization requires authentication for its repository, you can set it up so:
credentials += Credentials(
"Artifactory Realm",
"my.artifact.repo.net",
sys.getenv("REPO_USERNAME"),
sys.getenv("REPO_PASSWORD")
)
As you can see, it is pretty straightforward so far.
lazy val petStore =
project
.in(file("."))
.settings(
name := ...,
version := ...,
organization := ...,
scalaVersion := ...
)
.settings(
resolvers ++= ...,
credentials += ...
)
.settings(
libraryDependencies ++= ...
)
Publishing
-
If your project is a library that you would like to publish to an Artifactory, you need to let SBT know about it.
publishTo := Some(Resolver.mavenCentral) // or publishTo := Some("Artifactory Realm" at "https://artifactory.mycompany.com")
The
credentials
setting is used for the credentials for publishing. -
Or if your project is an executable application, and you like to create a distributable, run …
sbt package
This creates the executable JAR for your application. But you still need to reference all the dependent libraries in the class path. Like so …
java -cp \ target/scala-2.12/myapp_2.13-0.1.0-SNAPSHOT.jar::$SCALA_HOME/lib/scala-library.jar \ example.Hello
That is because we are just packaging. And not assembling, which creates a fat JAR (or one of the other ways of including all dependent JARs in the distribution). Hold your horses, we will see about that in a bit.
-
Or if you don’t like to publish for some reason; for instance, root project of multi-module projects …
publish / skip := true
Cliffhanger
Assuming that you are writing a library that you like to publish, here is the consolidated build file:
lazy val petStore =
project
.in(file("."))
.settings(
name := "pet-store",
version := "0.1.0-SNAPSHOT",
organization := "hkt",
scalaVersion := "2.12.20"
)
.settings(
resolvers ++=
("MyOrg" at "https://artifactory.myorg.com/maven") :: // For regular JAR artifacts
Resolver.url("MyOrgIvy" at "https://artifactory.myorg.com/sbt")(Resolver.ivyStylePatterns) :: // Ivy style resolution for SBT plugins
Nil,
credentials += Credentials(
"Artifactory Realm",
"my.artifact.repo.net",
sys.getenv("REPO_USERNAME"),
sys.getenv("REPO_PASSWORD")
),
publishTo := Some("Artifactory Realm" at "https://artifactory.mycompany.com")
)
.settings(
libraryDependencies ++=
"org.typelevel" %% "cats-core" % "2.12.0" ::
"org.scalatest" %% "scala-test" % "3.12.19" ::
Nil
)
- Was that hard to read or understand?
- Think twice before you say yes. And compare with other build tools. Familiarity (spending too much time with Maven or Gradle or other tools) breeds contempt4.
- The initial contact is straight forward. There is nothing cryptic about the build definition. But you will have a lot more things for a bigger project, which might make SBT look horrid. Trust me. That is where SBT shines! It gets modular when it gets bigger. Of course, the responsibility is ours to make it shining! SBT has got all the bells and whistles we need.
- The above build definition is a program which when executed by SBT performs the build of your application. Of course, it is a DSL dictated by SBT. But it is Scala code!
- It is declarative, which means you specify what you need to do in order to build and publish your application.
In the next post, I will introduce:
- Settings (and Tasks, maybe). We used settings in this post without knowing and formally introducing settings.
- Plugins! Swiss army knife in SBT for writing modular and/or advanced build definitions.
-
I expect newcomers to have a litte idea / experience with SBT. For instance, structure of the project, what a build.sbt file is etc. They may not have the intermediate / advanced skills yet to play their wits with SBT. ↩︎
-
Declare here means the conventional functional programming declaration; lazily evaluated. ↩︎
-
Not that it matters too much to what I am trying to do. But I am using recent versions of Scala (2.12) and SBT (1.x) ↩︎
-
Borrowed the quote Familiarity breeds contempt from Earl Nightangle. ↩︎