1. Introduction

Packaging of the application for deployment is the final step for most software development. There are multiple ways in which we can package and share our application. Depending on the requirements, we can create different types of packaging like JAR, DEB, EXE, and so on. In this tutorial, we’ll discuss different ways in which we can package a Scala application.

2. SBT Assembly

The easiest and simplest way of packaging an application is by using JAR files. A JAR file can run on any platform where the JRE is installed. Another advantage of executable JAR packaging is that there’s no need for installation, as we can directly run the JAR file. SBT Assembly is a popular SBT plugin for creating fat JARs.

2.1. Adding Dependencies

To use sbt-assembly, we need to add the plugin dependency in plugins.sbt:

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

Next, let’s say we’re writing an application that uses the os-lib library, so we’ll add that to the dependencies as well:

libraryDependencies ++= Seq(
  "com.lihaoyi" %% "os-lib" % "0.7.8"
)

Finally, let’s create the main class for our application:

package com.baeldung.packaging
import os._
@main def mainMethod() =
  val osName = System.getProperty("os.name")
  val path = os.pwd.toString
  println(s"""
    | Hello from the packaged app! 
    | Current Path: ${path}
    """.stripMargin)

We can now provide the main class file for the executable JAR in the build.sbt:

assembly / mainClass := Some("com.baeldung.packaging.mainMethod")

2.2. Generating the Assembly JAR

Now, let’s run the sbt command to generate the assembly JAR:

sbt assembly

This will generate the assembly JAR under the path <project_root>/target/scala-3.1.0/ We’ll be able to run the JAR as:

java -jar <assemblyjarfile.jar>

Assembly flattens all the class files from different JARs into a single path. When the number of dependencies increases, assembly JAR creation might become a bit more complex. Therefore, we might need to add configurations to handle possible conflicts of file names from multiple JAR files.

3. SBT Native Packager

SBT Native Packager is another plugin that can be used to build a variety of platform-specific packages, including ZIP, TAR, EXE, MSI, DEB, RPM, and so on.

3.1. Adding the Dependency

When using sbt-native-packager, we need to add the dependency in plugins.sbt:

addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.7")

Now, let’s enable the plugin in build.sbt:

enablePlugins(JavaAppPackaging)

We can then provide the main class name in build.sbt:

maintainer := "Baeldung"
Compile / mainClass := Some("com.baeldung.packaging.mainMethod")

3.2. Packaging the App

To create the packaged app, we’ll run the SBT command:

sbt stage

This will create a packaging under the path <project_root>/target/universal/stage/. Within the stage directory, the lib directory holds all the dependency JARs, and the bin directory holds the executable files, both Unix and Windows-style script files. We can move the entire stage directory to any desired location and run the application using the script file. To create a ZIP package:

sbt universal:packageBin

This will convert the stage directory into a ZIP file format that can then be easily transferred and executed.

3.3. Platform-Specific Packaging Options

We can create many different types of native packages using this plugin. However, these platform-specific packages need to be created on the same platform. However, there may be pre-requisites to generate these platform-specific packages. For example, to generate msi package for windows, the system should have the WIX toolkit already installed. Similarly, for debian packaging, there should be the relevant dpkg tools already installed. To create a windows packaging, we need to use the command:

sbt windows:packageBin

For creating a debian package:

sbt debian:packageBin

Similarly, we can create other native packaging formats like rpm, docker, pkg, and so on.

3.4. Creating Jlink Package

We can also create jlink based packaging using this plugin. jlink wraps the necessary JRE modules along with the application, therefore, the app can run even without the installation of a JRE on the target machine. Let’s enable the plugin in the build.sbt and add the necessary config:

enablePlugins(JlinkPlugin)
jlinkIgnoreMissingDependency := JlinkIgnore.only(
  "scala.quoted" -> "scala",
  "scala.quoted.runtime" -> "scala"
)

Sometimes, jlink might not be able to automatically handle some of the transitive dependencies, causing the build to fail. The JlinkIgnore config informs the packager to ignore the provided dependencies while packaging. To create the package, we need to run the command:

sbt universal:packageBin

This will package the application as a ZIP file along with the minimal JRE modules needed to run it. Note that jlink packaging will work only from JDK 11 or later. On JDK 10 and below, running this will fail with the error:

java.io.FileNotFoundException: $HOME/jvm/[email protected]/Contents/Home/jre/release (No such file or directory)

The jlink packaging needs to be run on the same platform as the target system since the JRE is platform-dependent.

4. SBT ProGuard

ProGuard is a Java-based tool that is used to obfuscate and optimize the application while packaging. This is widely used in Android and other platforms where the app size is important. SBT ProGuard is a plugin that packages the Scala application using the ProGuard tool. First, let’s add the plugin to plugins.sbt:

addSbtPlugin("com.github.sbt" % "sbt-proguard" % "0.5.0")

Next, we’ll enable the plugin in build.sbt and provide the basic configurations:

enablePlugins(SbtProguard)
Proguard / proguardOptions ++= Seq("-dontoptimize","-dontnote", "-dontwarn", "-ignorewarnings")
Proguard / proguardOptions += ProguardOptions.keepMain("com.baeldung.packaging.mainMethod")
Proguard / proguardInputs := (Compile / dependencyClasspath).value.files
Proguard / proguardFilteredInputs ++= ProguardOptions.noFilter((Compile / packageBin).value)

The proguardOptions flag helps to optimize and configure the packaging process. Lastly, let’s generate the package:

sbt proguard

This will generate the executable JAR file under path <project_root>/target/scala-3.1.0/proguard. We can now run the app like any other JAR file, using the command java -jar <jarname.jar>. If we compare the JAR files generated by both assembly and ProGuard, the generated assembly JAR file size is 7 MB, whereas the ProGuard JAR file is just 1 MB.

5. Using Scala-CLI

Scala-CLI is a new command-line tool that can be used to write and run Scala programs. It is similar to Ammonite scripting in some areas. We can write small Scala scripts using scala-cli. Additionally, we can package and distribute the script files as executables. We need to first install the tool. After installation, we can verify the installation using the command:

scala-cli

If installed successfully, this will print scala-cli help information. Next, let’s write our simple application as a scala-cli script:

using scala "3.1.0"
package com.baeldung.scalacli
import $dep.`com.lihaoyi::os-lib:0.7.8`
import os._
object ScalaCliApp {
  @main def app() = {
    val osName = System.getProperty("os.name")
    val path = os.pwd.toString
    println(s"""
            | Hello from the scala-cli packaged app!
            | Current Path: ${path}
            """.stripMargin)
  }
}

We can specify the Scala version using the directive using. Scala-CLI also follows the Ivy style syntax to add external dependencies. Now, let’s compile the program:

scala-cli compile ScalaCliApp.scala

At the first run, scala-cli will download all the necessary dependencies using Coursier. We can then run the application using the command:

scala-cli run ScalaCliApp.scala

The final step is to package the application with the dependency libraries:

scala-cli package ScalaCliApp.scala -o app --assembly

The –assembly flag packages the application along with the dependency JARs. This will generate an executable package with the name app. We can now distribute this generated application and execute it like any other executable file. Scala-CLI is a good option to distribute a small Scala file. However, it is not really ideal for building a big project. It is better to use an SBT project for building a bigger application as it might need more complex requirements. Scala-CLI is more suitable for prototyping, experimenting, and for sharing single-file Scala applications.

6. Conclusion

In this article, we looked at different ways in which we can package a Scala application. As always, the sample code used in this article is available over on GitHub.

Comments are closed on this article!