Introduction

The badass-jlink plugin allows you to create custom runtime images for modular applications with minimal effort. It also lets you create an application installer with the jpackage tool introduced in Java 14.

For non-modular applications use the Badass-Runtime plugin.

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. With the jpackage task you can create a platform-specific installer for your application.

The plugin requires Java 11 and Gradle 4.8 or newer. While it might work with some combinations of older Java and Gradle versions, these are not officially supported.

To use the plugin, include the following in your build script:

plugins {
    id 'org.beryx.jlink' version '2.17.0'
}

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 {
    options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
    launcher{
        name = 'hello'
        jvmArgs = ['-Dlog4j.configurationFile=./log4j2.xml']
    }
}

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

suggestMergedModuleInfo

Displays the mergedModule block that will be used if your jlink extension doesn’t include one. You can use the suggested block as a starting point for your custom mergedModule block.
depends on: prepareMergedJarsDir

jpackageImage

Uses the jpackage tool to create a platform-specific application image.
depends on: prepareModulesDir
This task is experimental.

jpackage

Uses the jpackage tool to create a platform-specific application installer.
depends on: jpackageImage
This task is experimental.

A detailed description of these tasks is given in Task details

Properties

imageDir

The directory into which the custom runtime image should be generated.
(If you use the targetPlatform method to generate images for other platforms, the corresponding images will be created in subdirectories of imageDir.)
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")

imageName

Convenience property for setting the values of both imageDir and imageZip as follows:
    imageDir ← buildDir/imageName
    imageZip ← buildDir/imageName.zip
usage example: imageName = 'hello'

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'

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'

mergedModuleJarName

The base name of the jar containing the merged module.
defaultValue: archiveBaseName.merged.module
usage example: mergedModuleJarName = 'my-merged-module'

mergedModuleJarVersion

The version of the jar containing the merged module.
defaultValue: project.version
usage example: mergedModuleJarVersion = '2.1.7'

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, jlink etc.).
defaultValue: the first non-empty value from:
     - the badass.jlink.java.home system property
     - the BADASS_JLINK_JAVA_HOME environment variable
     - the java.home system property (only if it points to a JRE containing the javac, jar, and jlink tools)
     - the JAVA_HOME environment variable
usage example: javaHome = '/usr/lib/jvm/open-jdk'

configuration

The name of the Gradle dependency configuration used to execute your application.
defaultValue: 'runtimeClasspath'
usage example: configuration = 'myAppRuntime'

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 'jakarta.xml.bind-api'

addExtraDependencies(String…​ jarPrefixes)

Instructs the plugin to treat all jars matching the given prefixes as dependencies of the merged module.
A typical situation where this method is needed involves libraries using JavaFX. Some libraries do not specify their JavaFX dependencies, because JavaFX was part of the JDK before being removed in Java 11.
Including addExtraDependencies("javafx") into the jlink block solves this problem.

addExtraModulePath(String modulePath)

Instructs the plugin to include the specified modulePath in the list of paths passed to the --module-path option of jlink.
You can call this method multiple times.
usage example: addExtraModulePath '/usr/lib/jmods'

targetPlatform(String name, String jdkHome, List<String> options = [])

Instructs the plugin to generate an application image for a specific platform.
By default, the plugin generates an image for the platform it runs on. To create images for other platforms, you need to call the targetPlatform method (one call per target platform).
name: an identifier of your choice that will be appended to the imageDir and imageZip properties to determine the location of the image directory and of the image archive.
jdkHome: the path to the target platform JDK.
options: an optional list of platform-specific options. These options will pe passed to jlink in addition to those provided by the options property of the jlink extension.
NOTE: This is only a convenience method. There is a more powerful targetPlatform method (described below), which allows configuring additional parameters of the target platform.

Usage example
jlink {
    ...
    targetPlatform('linux-x64', '/usr/lib/jvm/jdk_x64_linux_hotspot_11_28')
    targetPlatform('linux-s390x', '/usr/lib/jvm/jdk_s390x_linux_hotspot_11_28',
                                                               ['--endian', 'big'])
    ...
}

For a project named hello, executing the jlinkZip task with the above configuration, and assuming default values for the other properties, the plugin will generate the platform-specific images in the directories build/image/hello-linux-x64 and build/image/hello-linux-s390x. The archived images will be available in build/image-linux-x64.zip and build/image-linux-s390x.zip.

targetPlatform(String name, Action<TargetPlatform> action)

This more powerful version of the targetPlatform method allows configuring the target platform parameters using a script block.
name: an identifier of your choice that will be appended to the imageDir and imageZip properties to determine the location of the image directory and of the image archive.
action: a script block for configuring the target platform parameters.
     Parameters:
         jdkHome: the path to the target platform JDK.
         options: an optional list of platform-specific options.
     Methods:
         addOptions(String…​ options): an alternative way of setting the options property.
         addExtraModulePath(String path): pass the specified path to the --module-path option of jlink.
         This method can be used to specify the location of the platform-specific OpenJFX modules.

Usage example
jlink {
    ...
    targetPlatform("linux-s390x") {
        jdkHome = "/usr/lib/jvm/linux-s390x/jdk-11.0.2+9"
        addOptions("--endian", "big")
        addExtraModulePath("/usr/lib/openjfx/linux-s390x/jmods")
    }
    targetPlatform("mac") {
        jdkHome = "/usr/lib/jvm/mac/jdk-11.0.2+9"
        addExtraModulePath("/usr/lib/openjfx/mac/jmods")
    }
    targetPlatform("win") {
        jdkHome = "/usr/lib/jvm/win/jdk-11.0.2+9"
        addExtraModulePath("/usr/lib/openjfx/win/jmods")
    }
    ...
}

Script blocks

The jlink extension can also contain the script blocks detailed below.

mergedModule

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.

If a mergedModule block appears in yout build script, the generated module descriptor will contain the clauses specified in this block. Otherwise, the module descriptor is created using the algorithm implemented by the suggestMergedModuleInfo task.

additive

In many cases the suggested descriptor is just the right one for your merged module, so you don’t need to provide a mergedModule block. In some other cases the suggested descriptor is almost right, in the sense that it only misses one or a few clauses. In these cases you are allowed to configure only the missing clauses in the mergedModule block and instruct the plugin to add them to the suggested descriptor by setting the attribute additive to true.
defaultValue: false

There are also situations where the suggested descriptor contains some unwanted clauses. The plugin provides a few methods that allow excluding these clauses:

excludeRequires(String…​ modules)

Instructs the plugin to not generate requires clauses for the specified modules.
usage example: excludeRequires 'java.rmi', 'java-compiler'

excludeUses(String…​ services)

Instructs the plugin to not generate uses clauses for the specified services.
usage example: excludeUses 'java.nio.file.spi.FileSystemProvider'

excludeProvides(Map constraints)

Instructs the plugin to not generate provides clauses that match the specified constraints.
The following keys are allowed in the constraints map:
service: the qualified name of the service
implementation: the qualified name of the implementation class
servicePattern: the regular expression to be matched by the qualified name of the service
implementationPattern: the regular expression to be matched by the qualified name of the implementation class
usage example: excludeProvides servicePattern: 'org.codehaus.stax2.*'

By calling one of the above methods you automatically enable the additive mode. This means that it’s no longer necessary to explicitly set the additive property to true.

Usage example

Groovy
jlink {
    ...
    mergedModule {
        additive = true  // redundant, because excludeXXX() methods are also present
        requires 'java.desktop'
        requires transitive 'java.sql'
        uses 'java.sql.Driver'
        provides 'java.sql.Driver' with 'org.hsqldb.jdbc.JDBCDriver'
        excludeRequires 'java.compiler', 'java.rmi'
        excludeUses 'org.apache.logging.log4j.message.ThreadDumpMessage.ThreadInfoFactory'
        excludeProvides servicePattern: 'org.apache.logging.*'
    }
    ...
}
Kotlin
jlink {
    ...
    mergedModule {
        additive = true  // redundant, because excludeXXX() methods are also present
        requires("java.desktop")
        requiresTransitive("java.sql")
        uses("java.sql.Driver")
        provides("java.sql.Driver").with("org.hsqldb.jdbc.JDBCDriver")
        excludeRequires("java.compiler", "java.rmi")
        excludeUses("org.apache.logging.log4j.message.ThreadDumpMessage.ThreadInfoFactory")
        excludeProvides(mapOf("servicePattern" to "org.apache.logging.*"))
    }
    ...
}

launcher

The plugin generates script files for launching your application. You can customize these scripts by configuring the following properties in the launcher block.

name

The base name of the script files used to launch your application.
defaultValue: project.name

jvmArgs

list of JVM arguments to be passed to the java executable.
defaultValue: empty list

args

list of arguments to be passed to the application.
defaultValue: empty list

unixScriptTemplate

the template for generating the script file for Unix-like systems.
defaultValue: null (the plugin uses its own template)

windowsScriptTemplate

the template for generating the script file for Windows-based systems.
defaultValue: null (the plugin uses its own template)

The plugin uses Groovy’s SimpleTemplateEngine to parse the templates, with the following variables available:

  • moduleName

  • mainClassName

  • jvmArgs

  • args

Usage example

Groovy
jlink {
    ...
    launcher {
        name = 'my-app'
        jvmArgs = ['-Dlog4j.debug=true', '-Dlog4j.configurationFile=./log4j2.xml']
        args = ['--user', 'alice']
        unixScriptTemplate = file('unixStartScript.txt')
        windowsScriptTemplate = file('windowsStartScript.txt')
    }
    ...
}
Kotlin
jlink {
    ...
    launcher {
        name = "my-app"
        jvmArgs = listOf("-Dlog4j.debug=true", "-Dlog4j.configurationFile=./log4j2.xml")
        args = listOf("--user", "alice")
        unixScriptTemplate = file("unixStartScript.txt")
        windowsScriptTemplate = file("windowsStartScript.txt")
    }
    ...
}

secondaryLauncher

The plugin can generate script files for additional applications besides the main one. For each additional application you configure a secondaryLauncher block. This block supports all properties of the launcher block and also the following ones:

mainClass

the main class of this additional application.

moduleName

the module containing the main class of this additional application.
defaultValue: the value of the moduleName property in the enclosing jlink extension

Usage example

Groovy
jlink {
    ...
    secondaryLauncher {
        name = 'my-additional-app'
        mainClass = 'org.example.MyAdditionalApp'
        args = ['--user', 'emma']
    }
    ...
}
Kotlin
jlink {
    ...
    launcher {
        name = "my-additional-app"
        mainClass = "org.example.MyAdditionalApp"
        args = listOf("--user", "emma")
    }
    ...
}

customImage

By default, all application modules are included in the custom runtime image. This block allows you to create a custom runtime image containing only some of the application modules.

If the customImage block is empty, the plugin will create a JRE containing only the JDK modules required by your application. The plugin figures out by itself which JDK modules are needed, but you can use the below property to request a different set of modules.

jdkModules

list of JDK modules to be included in the generated image.
defaultValue: null (the plugin figures out by itself which JDK modules are needed)

jdkAdditive

if true, the custom image will contain both the modules in the jdkModules list and the JDK modules identified as required by the plugin itself. defaultValue: false

appModules

list of application modules to be included in the generated image.
Modules required by those in this list will be automatically included.
defaultValue: null (all application modules are included)

Usage example

Groovy
jlink {
    ...
    mergedModuleName = 'my.merged.module'
    customImage {
        jdkModules = ['java.desktop', 'java.xml', 'jdk.unsupported']
        appModules = ['my.merged.module']
    }
    ...
}
Kotlin
jlink {
    ...
    mergedModuleName = "my.merged.module"
    customImage {
        jdkModules = listOf("java.desktop", "java.xml", "jdk.unsupported")
        appModules = listOf("my.merged.module")
    }
    ...
}

jpackage

This experimental script block allows you to customize the jpackage-based generation of platform-specific application images and installers.

jpackageHome

The path to the JDK providing the jpackage tool.
defaultValue: the first non-empty value from:
     - the badass.jlink.jpackage.home system property
     - the BADASS_JLINK_JPACKAGE_HOME environment variable
     - the java.home system property (only if it points to a JRE containing the jpackage tool)
     - the JAVA_HOME environment variable
usage example: jpackageHome = "/usr/lib/jvm/jdk14"

outputDir

Convenience property for setting both imageOutputDir and installerOutputDir with the value buildDir/outputDir.
defaultValue: "jpackage"
usage example: outputDir = "my-packaging"

imageOutputDir

the directory passed as argument to the --output option of jpackage when executing the jpackageImage task . defaultValue: buildDir/outputDir
usage example: imageOutputDir = file("$buildDir/my-packaging-image")

imageName

the argument passed to the --name option when executing the jpackageImage task.
defaultValue: the name value configured in the launcher block or project.name
usage example: imageName = "MyApp"

imageOptions

list of additional options to be passed to the jpackage executable when executing the jpackageImage task.
defaultValue: empty list
usage example: imageOptions = ["--win-console"]

resourceDir

the directory passed as argument to the --resource-dir option when running jpackage to create an application installer. It is also applicable when creating an application image when you want your own application image instead of the default java image.
usage example: resourceDir = file("$buildDir/my-packaging-resources")

skipInstaller

boolean value that lets you generate only the platform-specific application image and skip the generation of the platform-specific application installer.
defaultValue: false
usage example: skipInstaller = true

installerType

the type of installer to be generated.
defaultValue: null (all supported types for the current platform will be generated)
usage example: installerType = "rpm"

installerOutputDir

the directory passed as argument to the --output option when running jpackage when executing the jpackage task. defaultValue: buildDir/outputDir
usage example: installerOutputDir = file("$buildDir/my-packaging-installer")

installerName

the argument passed to the --name option when running jpackage when executing the jpackage task.
defaultValue: the name value configured in the launcher block or project.name
usage example: installerName = "MyApp"

appVersion

the argument passed to the --app-version option when running jpackage when executing the jpackage and jpackageImage tasks.
defaultValue: the project version
usage example: appVersion = "1.0.0"

jvmArgs

list of JVM arguments to be passed to the virtual machine.
defaultValue: the jvmArgs value configured in the launcher block or an empty list

installerOptions

list of additional options to be passed to the jpackage executable when executing the jpackage task.
defaultValue: empty list
usage example: installerOptions = ["--win-console"]

targetPlatformName

This property is required only when using the targetPlatform method. It specifies which of the images produced by jlink should be used as runtime image by jpackage. Its value must match the name provided in one of the calls to the targetPlatform method.
defaultValue: null
usage example: targetPlatform = "linux"

Usage example

Groovy
jlink {
    ...
    jpackage {
        jpackageHome = '/usr/lib/jvm/jdk14'
        outputDir = 'my-packaging'
        // imageOutputDir = file("$buildDir/my-packaging-image")
        // installerOutputDir = file("$buildDir/my-packaging-installer")
        imageName = 'MyApp'
        imageOptions = ['--win-console']
        skipInstaller = false
        installerName = 'MyApp'
        installerType = 'msi'
        installerOptions = ['--win-console', '--win-menu', '--win-shortcut']
    }
    ...
}
Kotlin
jlink {
    ...
    jpackage {
        jpackageHome = "/usr/lib/jvm/jdk14"
        outputDir = "my-packaging"
        // imageOutputDir = file("$buildDir/my-packaging-image")
        // installerOutputDir = file("$buildDir/my-packaging-installer")
        imageName = "MyApp"
        imageOptions = listOf("--win-console")
        skipInstaller = false
        installerName = "MyApp"
        installerType = "msi"
        installerOptions = listOf("--win-console", "--win-menu", "--win-shortcut")
    }
    ...
}

How it works

The plugin combines all non-modular dependencies into a single jar to which it adds a module descriptor. If the jlink extension contains a mergedModule block, its directives will be used to generate the module descriptor. Otherwise, a module descriptor is created using the algorithm implemented by the suggestMergedModuleInfo task. If the attribute additive is set to true in the mergedModule block, the generated module descriptor adds the clauses specified in this block to the "suggested" descriptor.

The non-modular dependencies appear 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.

merging

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.

merging.cycle

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.

merging.no cycle

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 namePath relative to jlinkBasePath
mergedJarsDirmergedjars
tmpMergedModuleDirtmpmerged
jlinkJarsDirjlinkjars
tmpjars
tmpModuleInfoDirtmpmodinfo
delegatingModulesDirdelegating

prepareMergedJarsDir

- clean jlinkBasePath
- 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 tmpJarsDir
- 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
- insert the module-info.class from tmpModuleInfoDir into the merged jar

createDelegatingModules

- delete tmpJarsDir
- for each file in nonModularJarsDir:
    - create delegating module-info.java into tmpJarsDir/<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

prepareModulesDir

- copy delegating modules from delegatingModulesDir to jlinkJarsDir
- copy modular jars not required by non-modular jars to jlinkJarsDir
- copy the main module jar from project.jar.archivePath to jlinkJarsDir
- adjust all module descriptors containing qualified exports or opens clauses
        referring to modules integrated in the merged module. These clauses
        will be changed to also refer to the merged module.
- delete imageDir
- create custom runtime image in imageDir by executing jlink
        with modules from jlinkJarsDir

jlinkZip

- zip imageDir to imageZip

suggestMergedModuleInfo

- determine the modules required by the merged module
- determine the services used by the merged module
- determine the services provided by the merged module
- print the suggested `mergedModule` block
Options
language

the DSL for which the mergedModule block should be displayed.
default value: groovy
accepted values: groovy, kotlin, java
usage example: ./gradlew suggestMergedModuleInfo --language=kotlin

jpackageImage

- create a platform-specific application image in imageOutputDir by executing:
        jpackage --runtime-image imageDir --module-path jlinkJarsDir ...

The properties imageOutputDir and imageDir can be configured in the jpackage script block.

jpackage

- if skipInstaller is false:
        create a platform-specific application installer in installerOutputDir by executing:
        jpackage --type installerType --app-image=imageOutputDir/imageName ...

The properties installerOutputDir, installerType, imageOutputDir, and imageName can be configured in the jpackage script block.

If no installerType has been configured, the plugin will run jpackage several times, one for each type supported by the current platform.

Examples

The following projects illustrate how to use this plugin to create custom runtime images: