About
The gradle application plugin permits to create a desktop/console application but does not give any possibility to embed a jvm (ie a jre or jdk). This page shows how to do with a minimal knowledge of Gradle code.
This example was done with the kotlin DSL
The steps by steps will embed a whole jre/jdk and will result in a archive that as a significant size. A better way in Java would be to use Jlink to create a smaller image (With gradle, see the badass-runtime-plugin)
Steps
Create a configuration
We create a configuration that will get the JVM as dependencies because they will then not be added to the runtime or compile class path.
Therefore, there is no chance that they interfere with other tasks plugin (such as getting in the libs)
val jdk: Configuration by configurations.creating
Define a web repository
To download dependencies, a repository is mandatory.
Because we will just download the JRE/JDK from the internet (ie in our case Amazon correto), we are creating an ivy repository.
repositories {
// To download the JVM
var corretto = ivy {
url = uri("https://corretto.aws/downloads/latest")
patternLayout {
artifact("/[organisation]-[module]-[revision]-[classifier].[ext]")
}
// This is required in Gradle 6.0+ as metadata file (ivy.xml) is mandatory.
// https://docs.gradle.org/6.2/userguide/declaring_repositories.html#sec:supported_metadata_sources
metadataSources { artifact() }
}
// Use correto only for amazon dependencies
// https://docs.gradle.org/current/userguide/declaring_repositories.html#declaring_content_exclusively_found_in_one_repository
exclusiveContent {
forRepositories(corretto)
filter { includeGroup("amazon") }
}
}
Defining the JVM that we want as dependencies
We are taking the jdk but you could as well defining the JRE.
dependencies {
// For the corretto ivy repo, the dependency maps to the pattern:
// [organisation]:[module]:[revision]:[classifier]@[ext]
// In maven term: [group]:[artifact]:[version]:[classifier]@[ext]
jdk("amazon:corretto:8:x64-windows-jdk@zip")
jdk("amazon:corretto:8:x86-windows-jdk@zip")
jdk("amazon:corretto:8:[email protected]")
jdk("amazon:corretto:8:[email protected]")
}
Defining the Targeted OS
The Target Os for the needed distributions as variable
val x64Windows = "x64-windows"
val x86Windows = "x86-windows"
val x64Linux = "x64-linux"
val x64Macos = "x64-macos"
Creating the task that will extract the content of JRE/JDK
For each targeted os, we will just extract the content of the JDK/JRE and copy them at the distribution location. To not repeat ourzelf, we create a custom task with arguments
abstract class JvmTask @Inject constructor(
private val jdk: Configuration,
private val osTarget: String
) : DefaultTask() {
@TaskAction
fun copyJvm() {
project.copy {
// Listing
// project.configurations.compileClasspath.get().files.map { key -> println(key.name) };
val corretto = Regex("corretto.*-${osTarget}.*");
val jvmZipFile = project.configurations[jdk.name].resolve().find { file -> file.name.matches(corretto) };
println("$osTarget Jvm file to unzip: $jvmZipFile")
// Unarchive doc https://docs.gradle.org/current/userguide/working_with_files.html#sec:unpacking_archives_example
if (jvmZipFile == null) {
throw RuntimeException("Unable to find a Jvm file dependency for the OS (${osTarget})")
}
val fileTree: FileTree = when (jvmZipFile.extension) {
"zip" -> project.zipTree(jvmZipFile)
"gz" -> project.tarTree(jvmZipFile)
else -> null
} ?: throw RuntimeException("The extension (${jvmZipFile.extension}) is unknown for the Jvm file")
from(fileTree) {
eachFile {
// delete the first directory as explained in the example 12
// https://docs.gradle.org/current/userguide/working_with_files.html#sec:unpacking_archives_example
var segments = relativePath.segments.drop(1);
// macos has more than a root directory:
// * the home is at amazon-correto-8/Contents/Home
// * and there is third directory such as Contents/MacOS
if (osTarget == "macos") {
if (segments.size >= 2 && segments[0] == "Contents" && segments[1] == "Home") {
segments = segments.drop(2);
} else {
this.exclude();
}
}
relativePath = RelativePath(true, *segments.toTypedArray())
}
}
into(project.layout.buildDirectory.dir("jdk/${osTarget}"))
println("${osTarget} Jvm file Unzipped")
}
}
}
Create the tasks that will extract the JRE/JDK for each targeted OS
tasks.register<JvmTask>("${x86Windows}Jvm", jdk, x86Windows);
tasks.register<JvmTask>("${x64Windows}Jvm", jdk, x64Windows);
tasks.register<JvmTask>("${x64Linux}Jvm", jdk, x64Linux);
tasks.register<JvmTask>("${x64Macos}Jvm", jdk, x64Macos);
Distributions
In the distribution plugin, the plugin that creates Zip and Tar files, add a configuration by targeted os
distributions {
main {
contents {
into("/bin") {
from("${buildDir}/scripts") {
fileMode = Integer.parseInt("755", 8) // or 493 which is 755 base 8 in base 10
}
}
// Copy the jar of the cli (ie the actual project artifact generated)
into("lib") {
from(project.layout.buildDirectory.dir("libs").get().file(jar.archiveFileName))
}
// Copy the runtime dependencies
into("lib") { //
from(configurations.runtimeClasspath)
}
}
create(x64Windows) {
contents {
into("/jdk") {
from("${buildDir}/jdk/$x64Windows")
}
with(distributions.main.get().contents)
}
}
create(x86Windows) {
contents {
into("/jdk") {
from("${buildDir}/jdk/$x86Windows")
}
with(distributions.main.get().contents)
}
}
create(x64Linux) {
contents {
into("/jdk") {
from("${buildDir}/jdk/$x64Linux")
}
with(distributions.main.get().contents)
}
}
create(x64Macos) {
contents {
into("/jdk") {
from("${buildDir}/jdk/$x64Macos")
}
with(distributions.main.get().contents)
}
}
}
The fileMode of the script is expected to be converted from base 8 to base 10
fileMode = Integer.parseInt("755", 8)
Add the tasks dependency to unzip the JRE/JDK before the distribution task
tasks.getByName("${x86Windows}DistZip")
.dependsOn("${x86Windows}Jvm")
tasks.getByName("${x64Windows}DistZip")
.dependsOn("${x64Windows}Jvm")
tasks.getByName("${x64Linux}DistZip")
.dependsOn("${x64Linux}Jvm")
tasks.getByName("${x64Macos}DistZip")
.dependsOn("${x64Macos}Jvm")
Create a task to create all distribution at once
val allDistZip = tasks.register("allDistZip")
.get()
.dependsOn(tasks.getByName("distZip"))
.dependsOn("${x86Windows}DistZip")
.dependsOn("${x64Windows}DistZip")
.dependsOn("${x64Macos}DistZip")
.dependsOn("${x64Linux}DistZip")
Support
The constructor for type xxx.xxx should be annotated with @Inject
If you get this error:
The constructor for type xxx.xxx should be annotated with @Inject.
be sure that when creating a custom task, you have:
- not use any global variable
- or written an non-qualified qualified function. For instance don't use zipTree but project.zipTree
Unable to determine constructor argument #1 - value configuration is not assignable to type …, or no service of type Configuration
Same as the problem above be sure to qualify all function in your custom task.
Documentation / Reference
- With a big thanks to Björn Kautler with its helps on this topic