Packaging Scala Projects

This page will discuss common topics around packaging your Scala projects for distribution.

Building Executable Assemblies

Mill’s built in .assembly task makes it easy to generate an executable assembly jar from any JVM module. These assembly jars contain the transitive classpath of the module flattened into a single jar file, along with a Bash/Bat script prefix allowing you to run the jar via ./out.jar on Mac/Linux and ./out.bat on Windows. JVM assemblies are convenient mostly-self-contained executables, with the end user only needing to ensure they have a JVM installed on the machine they wish to run them on.

You can also customize the assembly jar as shown below:

build.mill (download, browse)
package build
import mill.*, scalalib.*
import mill.scalalib.Assembly.*

object foo extends ScalaModule {
  def moduleDeps = Seq(bar)
  def scalaVersion = "3.8.0"
  def mvnDeps = Seq(mvn"com.lihaoyi::os-lib:0.11.4")
  def assemblyRules = Seq(
    // all application.conf files will be concatenated into single file
    Rule.Append("application.conf"),
    // all *.conf files will be concatenated into single file
    Rule.AppendPattern(".*\\.conf"),
    // all *.temp files will be excluded from a final jar
    Rule.ExcludePattern(".*\\.temp"),
    // the `shapeless` package will be relocated under the `shade` package
    Rule.Relocate("shapeless.**", "shade.shapeless.@1")
  )

  // def manifest: T[JarManifest]
  // def prependShellScript: T[String]
}

object bar extends ScalaModule {
  def scalaVersion = "3.8.0"
}

The most common way of configuring an assembly is excluding some files from a final jar (like signature files, and manifest files from library jars), and merging duplicated files (for instance reference.conf files from library dependencies). This is done by overriding def assemblyRules as shown above

By default mill excludes all *.sf, *.dsa, *.rsa, and META-INF/MANIFEST.MF files from assembly, and concatenates all reference.conf files. You can also define your own merge/exclude rules.

> ./mill foo.assembly

> unzip -p ./out/foo/assembly.dest/out.jar application.conf || true
Bar Application Conf
Foo Application Conf

> java -jar ./out/foo/assembly.dest/out.jar
Loaded application.conf from resources:...
...Foo Application Conf
...Bar Application Conf

Note that when running the assembly directly via ./out.jar, you can configure JVM flags via the JAVA_OPTS environment variable, and select the JVM to use via JAVA_HOME.

> JAVA_OPTS=-Dtest.property=1337 ./out/foo/assembly.dest/out.jar
Loaded test.property: 1337

Building Native Image Binaries with Graal VM

This example uses NativeImageModule to generate a native executable using Graal VM. We recommend you configure a specific JDK version via a custom JvmWorkerModule overriding def jvmVersion (shown above), as not every JVM can build Graal native images.

build.mill.yaml (download, browse)
extends: ScalaModule
scalaVersion: 3.8.0
foo/package.mill.yaml (download, browse)
extends: [ScalaModule, NativeImageModule]
scalaVersion: 3.8.0
jvmVersion: graalvm-community:17.0.7
nativeImageOptions: ["--no-fallback"]
> ./mill show foo.nativeImage
GraalVM Native Image: Generating...native-executable...
Finished generating...native-executable...

> ./out/foo/nativeImage.dest/native-executable
Hello, World!

For another example building a slightly less trivial project into a Graal native image, see below:

This example shows how to generate native images for projects using third-party libraries, in this case Scalatags and Mainargs. We also demonstrate setting using -Os to optimize for the smallest binary size which is available in the graalvm-community:23 JDK selected above

build.mill.yaml (download, browse)
extends: ScalaModule
scalaVersion: 3.8.0
foo/package.mill.yaml (download, browse)
extends: [ScalaModule, NativeImageModule]
scalaVersion: 3.8.0
mvnDeps:
  - com.lihaoyi::scalatags:0.13.1
  - com.lihaoyi::mainargs:0.7.8
nativeImageOptions: ["--no-fallback", "-Os"]
jvmVersion: graalvm-community:23.0.1
> ./mill show foo.nativeImage

> ./out/foo/nativeImage.dest/native-executable --text hello-world
<h1>hello-world</h1>

You can see the Graal documentation to see what flags are available:

Or access the native-image compiler directly via show foo.nativeImageTool if you want to experiment it or view its --help text to see what you need to pass to nativeImageOptions:

> ./mill show foo.nativeImageTool # mac/linux
".../bin/native-image"

> ./mill show foo.nativeImageTool # windows
".../bin/native-image.cmd"

For more details on using Graal, check this blog post:

Building Repackage Assemblies

An alternative way to produce self-executable assemblies is the RepackageModule which used the Spring Boot Tools suite. Instead of copying and merging dependencies classes and resources into a flat jar file, it embeds all dependencies as-is in the final jar. One of the pros of this approach is, that all dependency archives are kept unextracted, which makes later introspection for checksums, authorship and copyright questions easier.

build.mill (download, browse)
package build

import mill.*, api.*, scalalib.*, publish.*
import mill.javalib.repackage.RepackageModule
import mill.javalib.spring.boot.SpringBootToolsModule

trait MyModule extends ScalaModule, PublishModule {
  def scalaVersion = "3.8.0"

  def publishVersion = "0.0.1"

  def pomSettings = PomSettings(
    description = "Hello",
    organization = "com.lihaoyi",
    url = "https://github.com/lihaoyi/example",
    licenses = Seq(License.MIT),
    versionControl = VersionControl.github("lihaoyi", "example"),
    developers = Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi"))
  )

  def mvnDeps = Seq(mvn"org.thymeleaf:thymeleaf:3.1.1.RELEASE")

  object test extends ScalaTests, TestModule.Junit4
}

object SpringBootTools2 extends SpringBootToolsModule {
  // Default version requires Java 17+, so downgrade to latest 2.x version
  override def springBootToolsVersion = "2.7.18"
  lazy val millDiscover = Discover[this.type]
}

object foo extends MyModule, RepackageModule { (1)
  def moduleDeps = Seq(bar, qux)
  override def springBootToolsModule = ModuleRef(SpringBootTools2)
}

object bar extends MyModule {
  def moduleDeps = Seq(qux)
}

object qux extends MyModule
1 Add the mill.javalib.repackage.RepackageModule to the executable module.
> ./mill foo.run
Foo.value: <h1>hello</h1>
Bar.value: <p>world</p>
Qux.value: 31337

> ./mill __.test
...Test run foo.FooTests finished: 0 failed, 0 ignored, 1 total, ...s
...Test run bar.BarTests finished: 0 failed, 0 ignored, 1 total, ...s

> ./mill show foo.repackagedJar
".../out/foo/repackagedJar.dest/out.jar"

> ./out/foo/repackagedJar.dest/out.jar
Foo.value: <h1>hello</h1>
Bar.value: <p>world</p>
Qux.value: 31337

> unzip -l ./out/foo/repackagedJar.dest/out.jar "BOOT-INF/lib*" # mac/linux
...BOOT-INF/lib/scala3-library_3-3.8.0.jar
...BOOT-INF/lib/thymeleaf-3.1.1.RELEASE.jar
...BOOT-INF/lib/ognl-3.3.4.jar
...BOOT-INF/lib/attoparser-2.0.6.RELEASE.jar
...BOOT-INF/lib/unbescape-1.1.6.RELEASE.jar
...BOOT-INF/lib/slf4j-api-2.0.5.jar
...BOOT-INF/lib/javassist-3.29.0-GA.jar
...BOOT-INF/lib/qux_3-0.0.1.jar
...BOOT-INF/lib/bar_3-0.0.1.jar

Further notes:

  • a small wrapper application needs to be added, which is run as entry point and transparently manages loading the embedded jars and running your main method. This works for all Java (also Scala or Kotlin) applications.

  • It’s not necessary to use the Spring Framework in the application.

  • The resulting jar is a self-executable application, but it might not suitable to be used on the classpath of other applications.

  • Since the final jar produced with the RepackageModule.repackagedJar task often contains significantly less ZIP entries then the jar file produced with .assembly, it’s possible to workaround an issue where JavaModule.assembly cannot produce executable assemblies due to some JVM limitations in ZIP file handling of large files.

Mill has builtin support for the JLink and JPackage command line tools. For more details, see: