Introduction
The badass-jlink plugin helps you create custom runtime images using jlink.
Many modular applications have one or more non-modular dependencies, which are treated as automatic modules by the Java platform. However, jlink cannot work with automatic modules. The typical way to solve this problem is to convert the non-modular jars to explicit modules, by adding an appropriate module descriptor to each non-modular jar. This is a tedious process if your application has lots of non-modular dependencies.
The badass-jlink plugin takes a more pragmatic approach by combining all non-modular dependencies into a single jar. This way, only the resulting merged module needs a module descriptor.
The plugin provides several tasks. The most frequently used are jlink
, which creates a custom runtime image in a given directory,
and jlinkZip
, which in addition creates a zip archive of the custom runtime image.
To use the badass-jlink plugin, include the following in your build script:
plugins {
id 'org.beryx.jlink' version '1.4.3'
}
Applying the Badass-JLink plugin also implicitly applies the Application plugin.
The plugin uses an extension named jlink
.
The sample below shows a few configuration options.
jlink {
launcherName = 'hello'
mergedModule {
requires 'java.naming';
requires 'java.xml';
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
}
The next sections provide detailed information on how to configure the plugin.
The source code is available on GitHub and is licensed under the Apache-2.0 license. |
User Guide
Creating a custom runtime image can be a challenging task if your application has many non-modular dependencies.
To let you address all possible issues, badass-jlink allows you to configure the jlink
extension using various properties, methods and script blocks.
The operations required to create a custom runtime image are grouped in several tasks.
This gives you the possibility to tweak a particular step by hooking into the corresponding task
(via doFirst
, doLast
, TaskExecutionListener
or TaskActionListener
).
Tasks
- prepareMergedJarsDir
-
Unpacks all non-modular dependencies in a designated directory.
depends on:jar
- createMergedModule
-
Creates the merged module using the content of the directory prepared by the previous task and adding a module descriptor to it.
depends on:prepareMergedJarsDir
- createDelegatingModules
-
For each non-modular dependency, it creates a delegating module, which is an open module consisting only of a module descriptor. The module descriptor specifies that the delegating module
requires transitive
the merged module.
depends on:createMergedModule
- prepareModulesDir
-
Copies all modules needed by jlink to a designated directory.
depends on:createDelegatingModules
- jlink
-
Uses the jlink tool to create the custom runtime image.
depends on:prepareModulesDir
- jlinkZip
-
Creates a zip archive of the custom runtime image.
depends on:jlink
A detailed description of these tasks is given in Task details
Properties
- imageDir
-
The directory into which the custom runtime image should be generated.
defaultValue:buildDir/image
usage example:imageDir = file("$buildDir/myapp-image")
- imageZip
-
The file into which a zip archive of the custom runtime image should be created.
defaultValue:buildDir/image.zip"
usage example:imageZip = file("$buildDir/myapp-image.zip")
- jlinkBasePath
-
The path to the base directory that will be used by the plugin to store intermediate outputs.
defaultValue:buildDir/jlinkbase
usage example:jlinkBasePath = "$buildDir/my-jlinkbase"
- mainClass
-
The main class to be provided as part of the
--launcher
option of jlink.
defaultValue:project.mainClassName
(from the Application plugin)
usage example:mainClass = 'org.example.MyApp'
- launcherName
-
The launcher command name to be provided as part of the
--launcher
option of jlink.
defaultValue:project.name
usage example:launcherName = 'my-app'
- moduleName
-
The module name of this application.
defaultValue: the module name specified in this application’s module-info.java
usage example:moduleName = 'org.example.myapp'
- mergedModuleName
-
The name of the merged module.
defaultValue:moduleName.merged.module"
usage example:mergedModuleName = 'org.example.myapp.merged.module'
- options
-
A list of options to be passed to jlink.
defaultValue: empty list
usage example:options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
- javaHome
-
The path to the JDK providing the tools needed by the plugin (javac, jar, jdeps, jlink etc.).
defaultValue: the value of theJAVA_HOME
environment variable.
usage example:javaHome = '/usr/lib/jvm/open-jdk'
- jdepsEnabled
-
Boolean specifying whether to use jdeps to generate the module-info.java of the merged module.
If this property is false or the jdeps command fails, the module-info.java will be created taking into account the configuration provided in themergedModule
block (see Script blocks).
defaultValue:false
usage example:jdepsEnabled = true
Methods
- addOptions(String… options)
-
Adds options to be passed to jlink. It is an alternative way of setting the
options
property. You can call this method multiple times.
usage example:addOptions '--no-header-files', '--no-man-pages'
- forceMerge(String… jarPrefixes)
-
Instructs the plugin to include all dependencies matching the given prefixes into the merged module. This method is useful when the plugin should handle one or more modular jars as non-modular. You can call this method multiple times.
usage example:forceMerge 'jaxb-api'
Script blocks
The mergedModule
block allows you to configure the module descriptor of the merged module.
It provides a DSL that matches the syntax of the directives in a module declaration file (module-info.java),
but it requires quotes around the names of modules, services, and service implementation classes.
The plugin automatically exports all packages found in the merged module, therefore the DSL does not support exports
directives.
jlink {
...
mergedModule {
requires 'java.desktop'
requires transitive 'java.sql'
uses 'java.sql.Driver'
provides 'java.sql.Driver' with 'org.hsqldb.jdbc.JDBCDriver'
}
...
}
How it works
The plugin combines all non-modular dependencies into a single jar to which it adds a module descriptor
(configured in accordance with the mergedModule
block).
The non-modular dependencies appears as automatic modules in the original module graph.
The plugin replaces them with delegating modules, which are dummy modules containing only a module descriptor that
requires transitive
the merged module.
The figure below illustrates this process.
In some situations, the above approach would lead to cyclic dependencies between modules. For example, in the module graph below the automatic module org.example.mod1 requires the proper module org.example.mod2. Because the content of org.example.mod1 gets merged into the merged module, the merged module must require org.example.mod2. This in turn requires the delegating module org.example.mod3 and hence the merged module.
To prevent such problems, the plugin automatically detects the modular jars that would be involved in a cycle and treats them as if they were non-modular. This means that it also merges these modular jars into the merged module and replaces them with delegating modules. The figure below shows the resulting module graph.
Sometimes, you may want to have a modular jar treated as non-modular, even if it is not affected by a cyclic dependency problem.
You can do this using the forceMerge
method.
Task details
The following properties denote files and directories used by the plugin tasks:
-
imageDir - the directory into which the custom runtime image should be generated.
-
imageZip - the file into which a zip archive of the custom runtime image should be created.
-
jlinkBasePath - the path to the base working directory of the plugin. The table below shows the variable names of the subdirectories created here and their relative path to the base working directory:
Variable name | Path relative to jlinkBasePath |
mergedJarsDir | mergedjars |
tmpMergedModuleDir | tmpmerged |
jlinkJarsDir | jlinkjars |
tmpJarsDir | tmpjars |
tmpModuleInfoDir | tmpmodinfo |
delegatingModulesDir | delegating |
prepareMergedJarsDir
- delete jlinkBasePath - delete jlinkJarsDir - delete nonModularJarsDir - copy modular jars required by non-modular jars to jlinkJarsDir - copy non-modular jars to nonModularJarsDir - unpack all jars from nonModularJarsDir into mergedJarsDir - create MANIFEST.MF in mergedJarsDir
createMergedModule
- archive mergedJarsDir into tmpMergedModuleDir/mergedModuleName.jar - generate module-info.java for the above merged jar into - clean tmpModuleInfoDir and unpack the merged jar in it - compile the generated module-info.java into tmpModuleInfoDir using jlinkJarsDir as module-path - copy the merged jar into jlinkJarsDir (the copied jar will have the path: mergedModuleJar) - insert the module-info.class from tmpModuleInfoDir into mergedModuleJar
createDelegatingModules
- delete nonModularJarsDir: - create delegating module-info.java into /<current-module-name> - clean tmpModuleInfoDir and create MANIFEST.MF in it - compile module-info.java into tmpModuleInfoDir with jlinkJarsDir as module-path - create a jar of tmpModuleInfoDir into delegatingModulesDir- for each file in
prepareModulesDir
- copy delegating modules from delegatingModulesDir to jlinkJarsDir - copy modular jars not required by non-modular jars to jlinkJarsDir
jlink
- delete imageDir - create custom runtime image in imageDir by executing jlink with modules from jlinkJarsDir and project.jar.archivePath
jlinkZip
- zip imageDir to imageZip
Examples
The following projects illustrate how to use this plugin to create custom runtime images:
-
badass-jlink-example - a 'Hello world' application using slf4j and logback.
-
badass-jlink-spring-petclinic - creates a custom runtime image of the Spring PetClinic application.
-
copper-modular-demo - creates a custom runtime image of a COPPER 5 modular application.