Scala/SBT: How to deploy a single, executable Jar file (sbt-assembly)

This is an excerpt from the 1st Edition of the Scala Cookbook (partially modified for the internet). This is Recipe 18.14, “How to Deploy a Single, Executable JAR File.”

Problem

You’re building a Scala application, such as a Swing application, and want to deploy a single, executable JAR file to your users.

Solution

The solution is to use the sbt-assembly plugin. Read the background for more background information, or skip ahead to the “Using sbt-assembly” section for details on how to use sbt-assembly.

Background

The sbt package command creates a JAR file that includes the class files it compiles from your source code, along with the resources in your project (from src/main/resources), but there are two things it doesn’t include in the JAR file:

  • Your project dependencies (JAR files in your project’s lib folder, or managed dependencies declared in build.sbt).
  • Libraries from the Scala distribution that are needed to execute the JAR file with the java command.

This makes it difficult to distribute a single, executable JAR file for your application. There are three things you can do to solve this problem:

  • Distribute all the JAR files necessary with a script that builds the classpath and executes the JAR file with the scala command. This requires that Scala be installed on client systems.
  • Distribute all the JAR files necessary (including Scala libraries) with a script that builds the classpath and executes the JAR file with the java command. This requires that Java is installed on client systems.
  • Use an SBT plug-in such as sbt-assembly to build a single, complete JAR file that can be executed with a simple java command. This requires that Java is installed on client systems.

This solution focuses on the third approach. The first two approaches are examined in the Discussion.

Using sbt-assembly

The installation instructions for sbt-assembly will probably change, but at the time of this writing (May, 2018), just create a file named assembly.sbt in your SBT project directory with this line:

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")

After that, just reload your SBT project. That’s all you have to do. (It used to be harder than this.)

See the sbt-assembly project documentation for the latest version and configuration information.

Now run sbt assembly to create your single, executable JAR file:

$ sbt assembly

When the assembly task finishes running it will tell you where the executable JAR file is located. For instance, when packaging my Blue Parrot application, SBT prints the following lines of output that show the dependencies sbt-assembly is including, and the location of the final JAR file:

[info] Including akka-actor-2.0.1.jar
[info] Including scala-library.jar
[info] Including applescriptutils_2.9.1-1.0.jar
[info] Including forms-1.0.7.jar
[info] Including sounds_2.9.1-1.0.jar
[info] Packaging target/BlueParrot-assembly-1.0.jar ...
[info] Done packaging.

The sbt-assembly plug-in works by copying the class files from your source code, the class files from your dependencies, and the class files from the Scala library into one single JAR file that can be executed with the java interpreter. This can be important if there are license restrictions on a JAR file, for instance.

As noted, there are other plug-ins to help solve this problem, including One-JAR, but sbt-assembly worked best with several applications I’ve deployed as single, executable JAR files.

Discussion

A JAR file created by SBT can be run by the Scala interpreter, but not the Java interpreter. This is because class files in the JAR file created by sbt package have dependencies on Scala class files (Scala libraries), which aren’t included in the JAR file SBT generates. This is easily demonstrated.

First, create an empty SBT project directory. (See Recipe 18.1 for easy ways to do this.) Then place the following code in a file named Main.scala in the root directory of the project:

package foo.bar.baz

object Main extends App {
    println("Hello, world")
}

Next, run sbt package to create the JAR file:

$ sbt package
[info] Loading global plugins from /Users/Al/.sbt/plugins
[info] Done updating.
[info] Compiling 1 Scala source to target/scala-2.10/classes...
[info] Packaging target/scala-2.10/basic_2.10-1.0.jar ...
[info] Done packaging.
[success] Total time: 6 s

Now attempt to run the JAR file with the java -jar command. This will fail:

$ java -jar target/scala-2.10/basic_2.10-1.0.jar
Exception in thread "main" java.lang.NoClassDefFoundError: scala/App
    at java.lang.ClassLoader.defineClass1(Native Method)
    ... 32 more

This fails because the Java interpreter doesn’t know where the scala/App trait is. Next, demonstrate that you can run the same JAR file with the Scala interpreter:

$ scala target/scala-2.10/basic_2.10-1.0.jar
Hello, world

This works fine.

For the Java interpreter to run your JAR file, it needs the scala-library.jar file from your Scala installation to be on its classpath. You can get this example to work with Java by including that JAR file on its classpath with this command:

$ java -cp "${CLASSPATH}:${SCALA_HOME}/lib/scala-library.jar:target/scala-2.10/basic_2.10-1.0.jar" foo.bar.baz.Main
Hello, world

As shown, adding the scala-library.jar file lets the Java interpreter find the scala/App trait (which is a normal Java class file), which lets it run the application successfully for you.

This is part of the work that sbt-assembly performs for you. It repackages the class files from ${SCALA_HOME}/lib/scala-library.jar into your single, executable JAR file, and does the same thing with your other project dependencies. Note that if your application is more complicated, it may need additional JAR files from the ${SCALA_HOME}/lib directory.

See Also