Thursday, November 25, 2010

Day 3: SBT dependencies and Scala versions

Some people sell Scala saying that it will boost your productivity. Allow me to disagree. It may help a little, but I doubt if it's significant to the productivity boost you get from using other libraries people have written. So, regardless of what you are going to do with Scala, mastering the art of dealing with dependencies is going to make a huge difference. And therefore - before doing anything fancy with SBT - I first want to understand how to do that using SBT.

Dependencies in Scala


Dependencies in Java are simple compared to dependencies in Scala. There, I said it. It's harder to manage dependencies on Scala libraries since different versions of Scala are not compatible. (You might want to read that line again.) The bytecode of a class in Scala 2.8.* just looks different than the bytecode of a class in Scala 2.7.*. It uses different ways to translate syntax constructs in Scala to bytecode.

Things like closures are simply not supported by the Java virtual machine, so there will always be translation to concepts like those to concepts supported by the Java VM. And different versions of Scala do it in different way.

Phrased otherwise: if you are working with Scala 2.8.*, then trying to link to a library that was built using Scala 2.7.* will fail. And there is no way to work around it (yet).

Java doesn't have that problem, which is one of the blessings (and probably the only one) of Sun/Oracle's conservative approach on extending Java.

The Consequences


As a consequence, when you have a dependency on a certain Scala library, you always need to link to a version of that library built for the version of Scala you are running. That makes things slightly more complicated then what you would typically do in Maven.

The good news is: many of the Scala libraries are getting pushed to the central Maven repository. And SBT has a certain scheme for resolving the appropriate versions of your Scala libraries. Let's see how that works.

Customizing an SBT build


Adding dependencies to our SBT project is our first customization of a default project. Before going ahead, it's important that you understand the SBT approach a little bit better. An SBT project is basically nothing but a default implementation of a class coordinating the build. The name of that class is DefaultProject. In order to add your own customization, you will need to create a new class that extends DefaultProject, and override some of its code, or add some more behavior. Your own extension of DefaultProject needs to be placed in project/build.

So, without any further ado: let's add a customization to our SBT project created in the previous post:

import sbt._

class SbtExampleProject(info: ProjectInfo) 
  extends DefaultProject(info)

If you already happened to be running SBT, this is the time to type reload, which will (no surprise) reload the project. So that wasn't all that hard - albeit a bit awkward at first sight. You know have your own customized project, even though nothing has been customized for real yet. Your build should still execute in the same old way.

Adding a dependency on a Java library


Let's first add a Java library, to start the easy way. Let's add guava. Now, this may come as a surprise, but really, then only thing you need to do is add a val with any name you like, with a value that is composed out of three parts. (The groupId, the artifactId and the version.) Like this:

import sbt._

class SbtExampleProject(info: ProjectInfo) 
extends DefaultProject(info) {
  
  val guava = "com.google.guava" % "guava" % "r07"

}

After that, in order to make sure SBT learns about your changes, first enter 'reload' and then 'update' to update its dependencies. (Remember that last step. Without it, nothing will be changed at all.)

> reload
[info] Recompiling project definition...
[info]    Source analysis: 1 new/modified, 0 indirectly invalidated, 0 removed.
[info] Building project test 1.0 against Scala 2.8.1
[info]    using SbtExampleProject with sbt 0.7.5.RC0 and Scala 2.7.7
> update
[info] 
[info] == update ==
[info] downloading http://repo1.maven.org/maven2/com/google/guava/guava/r07/guava-r07.jar ...
[info]  [SUCCESSFUL ] com.google.guava#guava;r07!guava.jar (3176ms)
[info] :: retrieving :: test#test_2.8.1 [sync]
[info]  confs: [compile, runtime, test, provided, system, optional, sources, javadoc]
[info]  1 artifacts copied, 0 already retrieved (1052kB/233ms)
[info] == update ==
[success] Successful.
[info] 
[info] Total time: 9 s, completed Nov 20, 2010 4:07:53 PM

Now, to prove that we actually can start using Guava, let's modify our HelloWorld class a little. Before doing that, I am going to enter '~ compile' and hit enter in SBT. This will make SBT go into a mode in which it will continuously compile source code once it's changed:

> ~compile
[info] 
[info] == compile ==
[info]   Source analysis: 0 new/modified, 0 indirectly invalidated, 0 removed.
[info] Compiling main sources...
[info] Nothing to compile.
[info]   Post-analysis: 2 classes.
[info] == compile ==
[success] Successful.
[info] 
[info] Total time: 0 s, completed Nov 20, 2010 4:18:18 PM
1. Waiting for source changes... (press enter to interrupt)

Just to show it works, I'm changing HelloWorld into this:

import com.google.common.base._
import scala.collection.JavaConversions._

object HelloWorld {
  def main(args: Array[String]) {
    Splitter.on(',').split("foo,bar").foreach(println);
  }
}

… and hit 'run':

[info] == run ==
[info] Running HelloWorld 
foo
bar
[info] == run ==
[success] Successful.

Good, so that worked out fine. Now that was adding a dependency on a Java library. But as I said, adding Scala libraries is a little bit more complicated. So, let's try that as well.

Adding a Scala library dependency


As I said, if you have dependencies on Scala libraries, then you need to carefully consider how these dependencies should be resolved, in order to make sure you get the libraries that match the version of Scala used in your build.

If you're wondering which version of Scala is used during your build, then SBT provides a simple command that you can run, which will give you all the information you need. You just type current and hit ENTER.

> current
Current project is test 1.0
Current Scala version is 2.8.0
Current log level is info
Stack traces are enabled

So apparently, we are compiling our sources using Scala 2.8.0. Now, say that we would like to give the specs library a try. For that, we would have to find a version 2.8.0 specific library. I am not aware of a place where you can search for Scala libraries specifically, so I will stick to mvnrepository.com.

If you search for org.scala-tools.testing on mvnrepository.com, you will see many different versions of the specs library. These are the versions that have been built using specific versions of Scala, named specs_2.8.0, specs_2.8.1.RC0, etc.

There are two ways we can refer to these libraries. The first approach is to link to these versions that directly, by adding this to the SbtExampleProject class:

val specs = "org.scala-tools.testing" % "specs_2.8.0" % "1.6.5"

If I run reload and update after that, everything is fine: the new dependency got included.

However, if I switch the target version (++2.7.7), then I'm still stuck with a version of specs that has been specifically built for Scala 2.8.0. Not so nice. (Adding some real test code to the project doesn't reveal a real problem yet, so you will have to wait for another post on the actual problems you might run into.)

It turns out, SBT offers a way out. You can make your build a little bit more tolerant towards version differences. In this particular case, it just requires a small modification. Instead of the dependency as illustrated above, you include a dependency as given below. By replacing the '%' with a '%%' (wonder what the right name for that operator would be), you essentially tell SBT to go and look for a version of specs 1.6.5 that was built for your current version of Scala.

val specs = "org.scala-tools.testing" %% "specs" % "1.6.5"

Now, in theory, this should solve everything. However, reality is a little harder on us. It turns out that if I do switch to version 2.8.1 of Scala, that particular version does not exist:

[error] sbt.ResolveException: unresolved dependency: org.scala-tools.testing#specs_2.8.1;1.6.5: not found

Looks like I'm back on square one. Although... not entirely. It would be awesome if the entire Scala world would maintain builds of their libraries for every version of Scala that would ever come about. In absence of that, you might be able to use various versions of that library, assuming that these different versions have been built with different versions of Scala. (Unfortunately, there is not always an easy way to tell which version of Scala was used to build these libraries.)

If that's what you want to do - pick different version of a library based on the version of Scala you are running - then SBT allows you to state that as well. And this is where it comes in handy that SBT is really just Scala at work. You can just code your dependency policy in Scala:

  val specs =
    buildScalaVersion match {
      case "2.8.0" => "org.scala-tools.testing" %% "specs" % "1.6.5"
      case "2.8.1" =>"org.scala-tools.testing" %% "specs" % "1.6.6"
      case x => error("Unsupported Scala version " + x)
    }

Switching to Scala 2.8.1 and running update now gives:

> update
[info] 
[info] == update ==
[info] downloading http://repo1.maven.org/maven2/org/scala-tools/testing/specs_2.8.1/1.6.6/specs_2.8.1-1.6.6.jar ...
[info]  [SUCCESSFUL ] org.scala-tools.testing#specs_2.8.1;1.6.6!specs_2.8.1.jar (7135ms)
[info] :: retrieving :: test#test_2.8.1 [sync]
[info]  confs: [compile, runtime, test, provided, system, optional, sources, javadoc]
[info]  1 artifacts copied, 1 already retrieved (2793kB/110ms)
[info] == update ==
[success] Successful.
[info] 
[info] Total time: 11 s, completed Nov 25, 2010 8:43:16 PM

Huray! It managed to get a version of specs that a) is available on the Internet, and b) compatible with the latest version of Scala. Nice!

Summary


This turned out to be a long (!) post. In fact, it took me way more time to put this one together than I had anticipated. I just comfort myself that this is probably one of the harder areas of working with Scala. Once you master dealing with different versions of Scala, the rest will be relatively easy. (Right?)

In working with different versions of Scala, SBT seems to be a blessing, especially with regard to the not-so-much-standardized namespace used for managing Scala dependencies and Scala versions. At least you are able to encode your own policy inside the SBT build file - something that is probably a lot harder to do in Maven.

All in all, I have to say that this second time I am looking at SBT, it actually feels quite ok. That is, I will definitely look further at other solutions in the future (stay tuned), but for now, it's time to move on to something different. Let's see how well all of this works in combination with some of the Scala-supporting IDEs, in particular Ensime, which will be the topic of the next post.

No comments:

Post a Comment