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:
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.
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
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.
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
mainmethod. 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.repackagedJartask often contains significantly less ZIP entries then the jar file produced with.assembly, it’s possible to workaround an issue whereJavaModule.assemblycannot produce executable assemblies due to some JVM limitations in ZIP file handling of large files.