SBT Build Environments Plugin

When first starting with Scala I was introduced to the build system SBT. If you’ve developed in Android or Groovy on Grails before, it will feel familiar as it is somewhat like the Gradle build system. However, I found myself missing some aspects of the Gradle build system, most notably the ability to configure values for the different build environments such as develop, staging and production. Build environments allow you to separate your configurations for the different environments your code runs. So for example the settings in dev can be used when developing locally, but when you run your code on the production server you can deploy the JAR that is built using the prod settings.

Fortunately, you are able to add auto plugins for SBT. These are plugins that extend SBT, and are loaded automagically by it. The goal was to write a simple plugin that provides some pre-configured build environments, and allows the user to easily add more. It was also important that the plugin was able to be integrated with other plugins that manipulate or extend the build system, such as sbt-assembly which is used to build fat JARs. To that end, the sbt-build-environments plugin was created.

The plugin comes with two build environments preconfigured: Development (Dev) and Production (Prod).

Integrating the plugin

The plugin requires SBT version 0.13.5+. Add the following lines to project/plugins.sbt:

resolvers += Resolver.url(
	"bintray-anchormen-sbt-plugins",
	url("http://dl.bintray.com/anchormen/sbt-plugins"))(
	Resolver.ivyStylePatterns)
addSbtPlugin("nl.anchormen.sbt" %% "sbt-build-environments" % "0.1.6")

Two build environments are defined, dev and prod. These define the development and production build environments, respectively. Their resources files must be located in the src directory, at src/dev/resources and src/prod/resources. Any resources should be added to both these directories with the same name, and values should be tailored to the build environment. Any configuration files in these directories cannot exist in the src/main/resources directory. Resources in the src/main/resources directory will be included in all build environments, and files with the same name in the specific build environment resource directories will cause an error when the build process is invoked. Each environment can be used to compile, run and package the project. For example, to run a development build use the command dev:run. To create a package for the productions environment, use the command prod:package

Adding build environments

Adding your own build environment is simple. For example, to add the foo build environment, add the following to build.sbt:

import nl.anchormen.sbt.EnvironmentSettings

lazy val Foo = config("foo") extend Compile
lazy val fooSettings = EnvironmentSettings(Foo.name)

lazy val commonDependencies = Seq(
    ...
)

lazy val commonSettings = Seq(
    ...
)

lazy val aggregatedCommons = commonDependencies ++ commonSettings

lazy val app = project
	.in(file("."))
	.settings(aggregatedCommons)
	.settings(inConfig(Foo)(fooSettings ++ aggregatedCommons))

Integrating with sbt-assembly

If we want to use the plugin with sbt-assembly, we need to configure our environments to inherit its settings. It’s also a good idea to specify a JAR name since we generally want to see for which build environment the JAR was built. In order to do so, add the following to build.sbt:

lazy val devJarName = Seq(assemblyJarName in assembly := s"${name.value}-assembly-${Dev.name}-${version.value}.jar")
lazy val prodJarName = Seq(assemblyJarName in assembly := s"${name.value}-assembly-${Prod.name}-${version.value}.jar")
lazy val stagingJarName = Seq(assemblyJarName in assembly := s"${name.value}-assembly-${Staging.name}-${version.value}.jar")

lazy val assemblySettings = sbtassembly.AssemblyPlugin.assemblySettings
lazy val aggregatedCommons = commonDependencies ++ commonSettings ++ assemblySettings

lazy val app = project
	.in(file("."))
	.settings(aggregatedCommons)
	.settings(inConfig(Staging)(stagingSettings ++ aggregatedCommons ++ stagingJarName))
	.settings(inConfig(Dev)(aggregatedCommons ++ devJarName))
	.settings(inConfig(Prod)(aggregatedCommons ++ prodJarName))

We can then simply run the command prod:assembly to generate a fat JAR for the production environment.

Conclusion

Being able to split application configuration along the build environment axis obviates the need for time consuming and error prone changes during the development and deployment cycles. It introduces a level of robustness to the development process, and is especially useful for teams which follow the DSP model during their development cycles.

The source can be viewed on GitHub.