Cross-version Scala Logging with SBT

In this article, I want to give a beginner example of how to use SBT's cross version feature for building for both e.g. Scala 2.10 and Scala 2.11 by also solving a real-world dependency problem with Typesafe's Scala Logging.

In this article, I want to give a beginner example of how to use SBT's cross version feature for building for both e.g. Scala 2.10 and Scala 2.11 by also solving a real-world dependency problem. I encountered a cross version problem when using Typesafes Scala Logging library. The dependency for 2.10 has a different dependency name, as well as, a different package name, see issue 1 and this gist.

So we have to deal with a different dependency name and different import code for 2.10 and 2.11. The dependency name for 2.10 is com.typesafe.scala-logging.scala-logging-slf4j and the dependency name for 2.11 is com.typesafe.scala-logging.scala-logging. For 2.10 the location of the Logger is com.typesafe.scalalogging.slf4j.Logger and for 2.11 the location of the Logger is com.typesafe.scalalogging.Logger.

First, I will show the modifications for the Build file. Afterwards, I will show how we can diverge the package import dependend on the Scala version. For the library dependency we can use CrossVersion.partialVersion to pattern match on the Scala major version and include the dependency accordingly:

crossScalaVersions := Seq("2.10.6","2.11.7")

lazy val scalaLogging_210 = "com.typesafe.scala-logging" % "scala-logging-slf4j_2.10" % "2.1.2"
lazy val scalaLogging_211 = "com.typesafe.scala-logging" %% "scala-logging" % "3.1.0"

libraryDependencies ++= CrossVersion.partialVersion(scalaVersion.value) match {
    case Some((2, major)) if major >= 11 => scalaLogging_211
    case _ => scalaLogging_210
}

Note that we also added crossScalaVersions to instruct SBT to build two versions on cross-version compilation. In order to handle the different package names you can create a wrapper object for the Logger for each version. Just place the 2.10 version of the wrapper inside /src/main/scala-2.10 and the 2.11 version inside the folder /src/main/scala-2.11. In the version independent code (inside /src/main/scala, for example) instead of calling the Logger directly, you call the wrapper object. The implementation of the wrapper object depends on the Scala version set in the compilation process. For Scala 2.10, the following code defines a possible wrapper object:

import com.typesafe.scalalogging.Logger
import org.slf4j.{Logger => Underlying}

object CrossVersionLogger {
    def apply(underlying: Underlying): Logger = Logger(underlying)
}

The second line aliases Slf4j's underlying logger to Underlying to avoid name conflicts. In Scala 2.10, a possible wrapper object is:

import com.typesafe.scalalogging.slf4j.Logger
import org.slf4j.{Logger => Underlying}

object CrossVersionLogger {
    def apply(underlying: Underlying): Logger = Logger(underlying)
}

As I mentioned earlier, instead of calling the logger directly, you have to use the version-specific wrapper object. A common practice is to mix in a Logging trait. A possible version-independet trait using the version-dependend wrapper object is:

trait Logging { 
    lazy val log = {
        CrossVersionLogger(loggerFactory.getLogger(getClass.getName))
    }
}

To trigger the cross version compilation instead of e.g. sbt compile, you have to use sbt +compile. The same schema applies to other SBT verbs. I hope, that I clarified cross-version compilation with SBT. If you have any suggestions or questions, don't hesitate to get in touch with me.


One or two mails a month about the latest technology I'm hacking on.