1. Introduction

When tracking down a bug in a piece of software, knowing the version affected by the issue is vital to narrow the scope of the investigation and speed up the process.

The build.sbt file of our project contains all the information we need to uniquely identify our code version. However, they are not easily accessible in the application.

The sbt-buildinfo plugin allows us to easily get hold of these pieces of information. Its motto clearly states it: I know this because build.sbt knows this.

2. Setup

To set up the sbt-buildinfo plugin, we need to first add it to the plugins in project/plugins.sbt:

addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0")

Make sure to refer to the latest stable versions of the plugin for your sbt version.

We can now enable it in the build.sbt:

lazy val myProject = (project in file(".")).
  enablePlugins(BuildInfoPlugin).
  settings(
    buildInfoKeys := Seq[BuildInfoKey](name, version, 
    scalaVersion, sbtVersion),
    buildInfoPackage := "buindInfoArticle"
  )addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0")

Let’s run sbt compile and see where the build information ends up. By looking in the target directory, we can see – under the ./target/scala-<your-scala-version>/src_managed/main/sbt-buildinfo directory – a BuildInfo object containing all the information about our build:

package buindInfoArticle

import scala.Predef._

/** This object was generated by sbt-buildinfo. */
case object BuildInfo {
  /** The value is "myProject". */
  val name: String = "myProject"
  /** The value is "0.1.0-SNAPSHOT". */
  val version: String = "0.1.0-SNAPSHOT"
  /** The value is "2.12.10". */
  val scalaVersion: String = "2.12.10"
  /** The value is "1.3.9". */
  val sbtVersion: String = "1.3.9"
  override val toString: String = {
    "name: %s, version: %s, scalaVersion: %s, sbtVersion: %s".format(
      name, version, scalaVersion, sbtVersion
    )
  }
}

Bingo! We now have a representation of our current build nicely modeled in a case object.

We can start using it by, for example, printing out which version of the application we’re running at startup:

object Main {
  def main(args: Array[String]) {
    println("Application Build info: " + BuildInfo.toString)
  }
}

Let’s run it and see what we get:

Application Build info: name: myProject, version: 0.1.0-SNAPSHOT, scalaVersion: 2.12.10, sbtVersion: 1.3.9

We easily got hold of the build information in our code without writing a single additional line of code!

3. How to Customize It

So far, we’ve seen the out-of-the-box behavior of the plugin. However, it’s possible to customize the BuildInfo object generated.

3.1. Custom buildInfoKeys

Anything accessible in the build.sbt can enrich it. Let’s add a resolver and some dependencies and expose them in BuildInfo:

resolvers += "my-internal-repo" at "https://my-artifact-repo/"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.10" % "test"

Now, we can customize our buildInfoKeys. This is how the new build.sbt will look:

import sys.process._

lazy val myProject = (project in file(".")).
  enablePlugins(BuildInfoPlugin).
  settings(
    buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion),
    buildInfoPackage := "buindInfoArticle"
  )

resolvers += "my-internal-repo" at "https://my-artifact-repo/"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.10" % "test"

buildInfoKeys ++= Seq[BuildInfoKey](
  // Add a build number. Automatically incremented every time the code is compiled.
  buildInfoBuildNumber,
  // Add resolvers
  resolvers,
  // Add library dependencies
  libraryDependencies,
  // Add custom key version and append timestamp to the version number
  BuildInfoKey.map(version) { case (k, v) => "mycustom" + k.capitalize -> s"$v-${System.currentTimeMillis.toString}" },
  // Add a custom field with the team owning the package
  "ownerTeam" -> "BestTeam",
  // Add a custom field with the build size
  BuildInfoKey.action("buildSize") {
    "du -h -d 0 ." !!
  }
)

Note that both SettingKey and TaskKey can be used to define the buildInfoKeys. After recompiling the code, let’s looks at the new build information object:

// $COVERAGE-OFF$
package buindInfoArticle

import scala.Predef._

/** This object was generated by sbt-buildinfo. */
case object BuildInfo {
  /** The value is "myProject". */
  val name: String = "myProject"
  /** The value is "0.1.0-SNAPSHOT". */
  val version: String = "0.1.0-SNAPSHOT"
  /** The value is "2.12.10". */
  val scalaVersion: String = "2.12.10"
  /** The value is "1.3.9". */
  val sbtVersion: String = "1.3.9"
  /** The value is 2. */
  val buildInfoBuildNumber: scala.Int = 2
  /** The value is scala.collection.immutable.Seq("my-internal-repo: https://my-artifact-repo/"). */
  val resolvers: scala.collection.immutable.Seq[String] = scala.collection.immutable.Seq("my-internal-repo: https://my-artifact-repo/")
  /** The value is scala.collection.immutable.Seq("org.scala-lang:scala-library:2.12.10", "org.scalatest:scalatest:3.2.10:test"). */
  val libraryDependencies: scala.collection.immutable.Seq[String] = scala.collection.immutable.Seq("org.scala-lang:scala-library:2.12.10", "org.scalatest:scalatest:3.2.10:test")
  /** The value is "0.1.0-SNAPSHOT-1642114563724". */
  val mycustomVersion = "0.1.0-SNAPSHOT-1642114563724"
  /** The value is "BestTeam". */
  val ownerTeam: String = "BestTeam"
  /** The value is "696K\t.\n". */
  val buildSize: String = "696K\t.\n"
  override val toString: String = {
    "name: %s, version: %s, scalaVersion: %s, sbtVersion: %s, buildInfoBuildNumber: %s, resolvers: %s, libraryDependencies: %s, mycustomVersion: %s, ownerTeam: %s, buildSize: %s".format(
      name, version, scalaVersion, sbtVersion, buildInfoBuildNumber, resolvers, libraryDependencies, mycustomVersion, ownerTeam, buildSize
    )
  }
}
// $COVERAGE-ON$

3.2. Custom buildInfoObject and buildInfoPackage

Further customizations that the plugin allows are the change of the build info object and the package. The latter can be particularly useful to avoid name clashes with other artifacts.

Let’s override the default values in our build.sbt:

buildInfoObject := "MyCustomBuildInfo"
buildInfoPackage := "myCustomPackge"

This will produce the following object:

// $COVERAGE-OFF$
package myCustomPackge

import scala.Predef._

/** This object was generated by sbt-buildinfo. */
case object MyCustomBuildInfo {
  /** The value is "myProject". */
  val name: String = "myProject"
  /** The value is "0.1.0-SNAPSHOT". */
  val version: String = "0.1.0-SNAPSHOT"
  /** The value is "2.12.10". */
  val scalaVersion: String = "2.12.10"
  /** The value is "1.3.9". */
  val sbtVersion: String = "1.3.9"
  override val toString: String = {
    "name: %s, version: %s, scalaVersion: %s, sbtVersion: %s".format(
      name, version, scalaVersion, sbtVersion
    )
  }
}
// $COVERAGE-ON$

We can see the overridden build info object and, more importantly, the package name.

3.3. Custom buildInfoOptions

Finally, a really handy parameter allows us to add some extra methods to the build info object. As usual, we’re going to add to the initial build.sbt we created and see what is the effect:

buildInfoOptions := Seq(
  BuildInfoOption.ToJson, // Add a toJson method to BuildInfo
  BuildInfoOption.ToMap, // Add a toMap method to BuildInfo
  BuildInfoOption.BuildTime // Add timestamp values
)

Each of the options adds some extra methods in the BuildInfo object. BuildInfoOption.ToJson adds some methods to easily serialize our information to JSON format:

private def quote(x: scala.Any): String = "\"" + x + "\""
private def toJsonValue(value: scala.Any): String = {
  value match {
    case elem: scala.collection.Seq[_] => elem.map(toJsonValue).mkString("[", ",", "]")
    case elem: scala.Option[_] => elem.map(toJsonValue).getOrElse("null")
    case elem: scala.collection.Map[_, scala.Any] => elem.map {
      case (k, v) => toJsonValue(k.toString) + ":" + toJsonValue(v)
    }.mkString("{", ", ", "}")
    case d: scala.Double => d.toString
    case f: scala.Float => f.toString
    case l: scala.Long => l.toString
    case i: scala.Int => i.toString
    case s: scala.Short => s.toString
    case bool: scala.Boolean => bool.toString
    case str: String => quote(str)
    case other => quote(other.toString)
  }
}

BuildInfoOption.ToMap instead adds a toMap method to return the same information in a Map:

val toMap: Map[String, scala.Any] = Map[String, scala.Any](
  "name" -> name,
  "version" -> version,
  "scalaVersion" -> scalaVersion,
  "sbtVersion" -> sbtVersion,
  "buildInfoBuildNumber" -> buildInfoBuildNumber,
  "resolvers" -> resolvers,
  "libraryDependencies" -> libraryDependencies,
  "mycustomVersion" -> mycustomVersion,
  "ownerTeam" -> ownerTeam,
  "buildSize" -> buildSize,
  "builtAtString" -> builtAtString,
  "builtAtMillis" -> builtAtMillis)

Another option, BuildInfoOption.BuildTime, adds information about the build time:

/** The value is "2022-01-14 22:54:23.385+0100". */
val builtAtString: String = "2022-01-14 22:54:23.385+0100"
/** The value is 1642197263385L. */
val builtAtMillis: scala.Long = 1642197263385L

The last option we’ll look at allows mixin capability in the BuildInfo object via traits. Let’s suppose we need an XML to represent our information. We can write a trait with a method producing the XML we need, for example:

package com.baeldung

trait XMLTranformer {
  def toXML(input: Map[String, Any]): String = {
    s"""
      <buildInfo>
        <name>${input.get("name")}</name>
        <version>${input.get("version")}</version>
        <scalaVersion>${input.get("scalaVersion")}</scalaVersion>
      </buildInfo>
    """
  }
}

How do we tell BuildInfo to extend it? We simply add the following to the buildInfoOption:

BuildInfoOption.Traits("com.baeldung.XMLTranformer")

Once we recompile the code, we’ll notice BuildInfo extending the trait we just created:

case object BuildInfo extends com.baeldung.XMLTranformer {
...
}

We can now use the toXML method to produce the desired XML in the application:

object Main {
  def main(args: Array[String]) {
    println("XML Application Build info: " + BuildInfo.toXML(BuildInfo.toMap))
  }
}

This will give us the output:

XML Application Build info:
      <buildInfo>
        <name>Some(myProject)</name>
        <version>Some(0.1.0-SNAPSHOT)</version>
        <scalaVersion>Some(2.12.10)</scalaVersion>
      </buildInfo>

As we’ve seen, this feature is really powerful since it allows maximum extensibility to the BuildInfo object initially created by the plugin.

4. Conclusion

Knowing our artifacts’ build information are key in a software project of any dimension.

Producing and consuming them in an automated and frictionless way is important to avoid being sidetracked during the software development process. sbt-buildinfo is a good example of a library that seamlessly enriches our artifacts with useful and highly customizable build information.

Comments are closed on this article!