Packaging Java Projects
This page will discuss common topics around packaging your Java 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.*, javalib.*
import mill.javalib.Assembly.*
object foo extends JavaModule {
def moduleDeps = Seq(bar)
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")
)
}
object bar extends JavaModule {}
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: [JavaModule, NativeImageModule]
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: [JavaModule, NativeImageModule]
mvnDeps:
- net.sourceforge.argparse4j:argparse4j:0.9.0
- org.thymeleaf:thymeleaf:3.1.1.RELEASE
- org.slf4j:slf4j-nop:2.0.7
nativeImageOptions:
- "--no-fallback"
- "-H:IncludeResourceBundles=net.sourceforge.argparse4j.internal.ArgumentParserImpl"
- "-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.*, javalib.*, publish.*
import mill.javalib.repackage.RepackageModule
trait MyModule extends JavaModule, PublishModule {
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 JavaTests, TestModule.Junit4
}
object foo extends MyModule, RepackageModule { (1)
def moduleDeps = Seq(bar, qux)
}
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*"
...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-0.0.1.jar
...BOOT-INF/lib/bar-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.
Java App and Bundles using jlink
This example illustrates how to use Mill to generate a runtime image using the jlink tool.
Starting with JDK 9, jlink bundles Java app code with a stripped-down version of the JVM.
package build
import mill.*, javalib.*
import mill.javalib.Assembly.*
import mill.scalalib.JlinkModule
object foo extends JavaModule, JlinkModule {
def jlinkModuleName: T[String] = Task { "foo" }
def jlinkModuleVersion: T[Option[String]] = Task { Option("1.0") }
def jlinkCompressLevel: T[String] = Task { "2" }
}
Most of the work is done by the trait JlinkModule in two steps:
-
It uses the
jmodtool to create ajlink.jmodfile for the main Java module. The main Java module is typically the module containing themainClass.
If your build file doesn’t explicitly specify a mainClass, JlinkModule will infer it from JavaModule, which is its parent trait.
You can explicitly specify a mainClass like so in your build file:
def mainClass: T[Option[String]] = { Some("com.foo.app.Main") }
-
It then uses the
jlinktool, to link the previously createdjlink.jmodwith a runtime image.
With respect to the jlinkCompressLevel option, on recent builds of OpenJDK and its descendants,
jlink will accept [0, 1, 2] but it will issue a deprecation warning.
Valid values on OpenJDK range between: ["zip-0" - "zip-9"].
The version of jlink that ships with the Oracle JDK will only accept [0, 1, 2]
as valid values for compression, with 0 being "no compression"
and 2 being "ZIP compression".
|
To use a specific JDK, first set your JAVA_HOME environment variable prior to running the build.
export JAVA_HOME=/Users/mac/.sdkman/candidates/java/17.0.9-oracle/
> ./mill foo.jlinkAppImage
> ./mill show foo.jlinkAppImage
".../out/foo/jlinkAppImage.dest/jlink-runtime"
> ./out/foo/jlinkAppImage.dest/jlink-runtime/bin/jlink
... foo.Bar main
INFO: Hello World!
Java Installers using jpackage
This example illustrates how to use Mill to generate a native package/installer
using the jpackage tool.
package build
import mill.*, javalib.*
import mill.javalib.Assembly.*
import mill.scalalib.JpackageModule
object foo extends JavaModule, JpackageModule {
def jpackageType = "app-image"
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")
)
}
JPMS (Java Platform Module System) is a modern distribution format that was designed to avoid several of the shortcomings of the ubiquitous JAR format, especially "JAR Hell".
A defining characteristic of module-based Java applications based on the JPMS format
is that a module-info.java must be defined at the root of the module’s source file hierarchy.
The module-info.java must explicitly list modules that it depends on, and also list
packages that it exports, to make the integrity of these relationships easy to verify,
both at compile-time and run-time.
Starting with version 14, the JDK ships with the jpackage tool which can
assemble any module-based Java application into a native package/installer.
The above build file expects the following project layout:
build.mill
foo/
src/
Foo.java
Bar.java
module-info.java
> ./mill foo.jpackageAppImage
> ./mill show foo.jpackageAppImage
".../out/foo/jpackageAppImage.dest/image"
The term Module is also used in Mill to refer to Mill Modules.
This is not to be confused with Java app code structured as modules according to the JPMS format.
|
The JpackageModule trait will infer most of the options needed to assemble a native
package/installer, but you can still customize its output. In our example, we specified:
def jpackageType = "pkg"
This tells jpackage to generate a .pkg, which is the native installer format on macOS.
Valid values on macOS are: dmg, pkg and app-image.
jpackage doesn’t support cross-build to a different OS/CPU combination than
the one you the build is running on. For example, the jpackage binary shipped with a macOS JDK
cannot be used to produce a native installer for another OS like Windows or Linux.
|
On macOS, jpackageType accepts 3 values: "dmg" or "pkg" or "app-image" (default).
-
Setting
def jpackageType = "dmg"will produce:
> ls -l ./out/foo/jpackageAppImage.dest/image ... foo-1.0.dmg
-
Setting
def jpackageType = "pkg"will produce:
> ls -l ./out/foo/jpackageAppImage.dest/image ... foo-1.0.pkg
-
Setting
def jpackageType = "app-image"will produce:
> ls -l ./out/foo/jpackageAppImage.dest/image ... foo.app/ ./out/foo/jpackageAppImage.dest/image/foo.app/Contents/MacOS/foo ... foo.Foo readConf INFO: Loaded application.conf from resources: Foo Application Conf ... foo.Bar ... INFO: Hello World application started successfully