How to create a fat JAR with Gradle Kotlin script?
Asked Answered
M

5

78

As titled, I'd like to know how to modify the gradle.build.kts in order to have a task to create a unique jar with all the dependencies (kotlin lib included) inside.

I found this sample in Groovy:

//create a single Jar with all dependencies
task fatJar(type: Jar) {
    manifest {
        attributes 'Implementation-Title': 'Gradle Jar File Example',
            'Implementation-Version': version,
            'Main-Class': 'com.mkyong.DateUtils'
    }
    baseName = project.name + '-all'
    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
    with jar
}

But I have no idea how I could write that in kotlin, other than:

task("fatJar") {

}
Moonier answered 22/1, 2017 at 18:56 Comment(2)
Here is a related question.Chesterfieldian
Does this answer your question? Building a self-executable jar with Gradle and KotlinChesterfieldian
P
63

Here is a version that does not use a plugin, more like the Groovy version.

import org.gradle.jvm.tasks.Jar

val fatJar = task("fatJar", type = Jar::class) {
    baseName = "${project.name}-fat"
    manifest {
        attributes["Implementation-Title"] = "Gradle Jar File Example"
        attributes["Implementation-Version"] = version
        attributes["Main-Class"] = "com.mkyong.DateUtils"
    }
    from(configurations.runtime.map({ if (it.isDirectory) it else zipTree(it) }))
    with(tasks["jar"] as CopySpec)
}

tasks {
    "build" {
        dependsOn(fatJar)
    }
}

Also explained here


Some commenters pointed out that this does not work anymore with newer Gradle versions. Update tested with Gradle 5.4.1:

import org.gradle.jvm.tasks.Jar

val fatJar = task("fatJar", type = Jar::class) {
    baseName = "${project.name}-fat"
    manifest {
        attributes["Implementation-Title"] = "Gradle Jar File Example"
        attributes["Implementation-Version"] = version
        attributes["Main-Class"] = "com.mkyong.DateUtils"
    }
    from(configurations.runtimeClasspath.get().map({ if (it.isDirectory) it else zipTree(it) }))
    with(tasks.jar.get() as CopySpec)
}

tasks {
    "build" {
        dependsOn(fatJar)
    }
}

Note the difference in configurations.runtimeClasspath.get() and with(tasks.jar.get() as CopySpec).

Propagate answered 16/5, 2017 at 9:48 Comment(13)
He's not kidding. This should be added somewhere in github.com/gradle/kotlin-dsl/tree/master/samplesBurrell
Note that in Gradle 5 you will have to replace configurations.runtime.map with configurations.runtime.get().map to avoid unresolved reference: isDirectory. See discussion here.Gileadite
And also note that for Gradle 5.4 you will need to replace with(tasks["jar"] as CopySpec) with with(getByName("jar") as CopySpec)Jeanmariejeanna
@Jeanmariejeanna using Gradle 5.4 your suggested modification actually broke it for me. I got an error: Cause: null cannot be cast to non-null type org.gradle.api.file.CopySpecFlorafloral
In gradle 5.4.1 I had to replace with(tasks["jar"] as CopySpec) with val jar: CopySpec by getting(Jar::class); with(jar)Lyndialyndon
I also had to exclude("META-INF/*.RSA", "META-INF/*.SF", "META-INF/*.DSA", "META-INF/INDEX.LIST") for the main class to be found in the jarYuriyuria
Have to use duplicatesStrategy = DuplicatesStrategy.EXCLUDE inside the fatJar task or exclude some files to avoid failure - LICENSE.txt inside each jar dependency was getting included, which caused some errors.Arianearianie
Make sure that you also exclude **.kotlin_metadata if you want others to consume the fat JAR and receive IDE support. Otherwise the IDE will be unable to resolve references inside the fat JAR: youtrack.jetbrains.com/issue/KT-25709Arianearianie
For who facing deprecated warning on baseName and version , use archiveBaseName.set("") and archiveVersion instead.Sheared
This does not work anymore for gradle 6.8Stellular
@Stellular Try it with attributes["Implementation-Version"] = archiveVersion insteadPropagate
As of Gradle 7.3.3, the from(configurations...) line being present results in this error: Cannot change dependencies of dependency configuration ':jvmApi' after it has been included in dependency resolution.Woodcutter
...Although my problem might be that I'm trying to do this in a multiplatform project, and it looks like I might have the task defined in the wrong place (at the top level at the bottom of the file, instead of within the kotlin { jvm { ... } } block.Woodcutter
C
30

Here are 4 ways to do this. Note that the first 3 methods modify the existing Jar task of Gradle.

Method 1: Placing library files beside the result JAR

This method does not need application or any other plugins.

tasks.jar {
    manifest.attributes["Main-Class"] = "com.example.MyMainClass"
    manifest.attributes["Class-Path"] = configurations
        .runtimeClasspath
        .get()
        .joinToString(separator = " ") { file ->
            "libs/${file.name}"
        }
}

Note that Java requires us to use relative URLs for the Class-Path attribute. So, we cannot use the absolute path of Gradle dependencies (which is also prone to being changed and not available on other systems). If you want to use absolute paths, maybe this workaround will work.

Create the JAR with the following command:

./gradlew jar

The result JAR will be created in build/libs/ directory by default.

After creating your JAR, copy your library JARs in libs/ sub-directory of where you put your result JAR. Make sure your library JAR files do not contain space in their file name (their file name should match the one specified by ${file.name} variable above in the task).

Method 2: Embedding the libraries in the result JAR file (fat or uber JAR)

This method too does not need any Gradle plugin.

tasks.jar {
    manifest.attributes["Main-Class"] = "com.example.MyMainClass"
    val dependencies = configurations
        .runtimeClasspath
        .get()
        .map(::zipTree) // OR .map { zipTree(it) }
    from(dependencies)
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

Creating the JAR is exactly the same as the previous method.

Method 3: Using the Shadow plugin (to create a fat or uber JAR)

plugins {
    id("com.github.johnrengelman.shadow") version "6.0.0"
}
// Shadow task depends on Jar task, so these configs are reflected for Shadow as well
tasks.jar {
    manifest.attributes["Main-Class"] = "com.example.MyMainClass"
}

Create the JAR with this command:

./gradlew shadowJar

See Shadow documentations for more information about configuring the plugin.

Method 4: Creating a new task (instead of modifying the Jar task)

tasks.create("MyFatJar", Jar::class) {
    group = "my tasks" // OR, for example, "build"
    description = "Creates a self-contained fat JAR of the application that can be run."
    manifest.attributes["Main-Class"] = "com.example.MyMainClass"
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    val dependencies = configurations
        .runtimeClasspath
        .get()
        .map(::zipTree)
    from(dependencies)
    with(tasks.jar.get())
}

Running the created JAR

java -jar my-artifact.jar

The above solutions were tested with:

  • Java 17
  • Gradle 7.1 (which uses Kotlin 1.4.31 for .kts build scripts)

See the official Gradle documentation for creating uber (fat) JARs.
For more info about manifests, see Oracle Java Documentation: Working with Manifest files.
For difference between tasks.create() and tasks.register() see this post.

Your resource files will be included in the JAR file automatically (assuming they were placed in classpath: /src/main/resources/ directory or any custom directory set as resources root in the build file). Here is how to access a resource file in your code (note the / at the start of names):

  • Kotlin
    val vegetables = MyClass::class.java.getResource("/vegetables.txt").readText()
    // Alternative ways:
    // val vegetables = object{}.javaClass.getResource("/vegetables.txt").readText()
    // val vegetables = MyClass::class.java.getResourceAsStream("/vegetables.txt").reader().readText()
    // val vegetables = object{}.javaClass.getResourceAsStream("/vegetables.txt").reader().readText()
    
  • Java
    var stream = MyClass.class.getResource("/vegetables.txt").openStream();
    // OR var stream = MyClass.class.getResourceAsStream("/vegetables.txt");
    
    var reader = new BufferedReader(new InputStreamReader(stream));
    var vegetables = reader.lines().collect(Collectors.joining("\n"));
    
Chesterfieldian answered 12/2, 2022 at 12:59 Comment(8)
how would you do this for variants, if one wants a slim jar, and, opitonally a fat(ter) jar? am trying for plantuml, and it is either fat or slim - but no option to control it.Nubble
Sorry, I don't know about this.Chesterfieldian
it works fine like you posted, with the dedicated task, method 4. only thing i am wondering - how to sign the fat jar then, as the standard sign task would not find it.Nubble
Maybe this Gradle guide can help you.Chesterfieldian
got an answer in the gradle forums: discuss.gradle.org/t/build-variant-command-line/42193/8 . which now is in plantuml main gradle script, signing working.Nubble
Upvote for shadow plugin, which was frictionless with KotlinRectus
the question is "how to create a fat jar" and your answer is how to place files alongside the jar? that sounds a little confusing to me ...Nubble
Read the rest of the answer...Chesterfieldian
T
9

Here is how to do it as of Gradle 6.5.1, Kotlin/Kotlin-Multiplatform 1.3.72, utilizing a build.gradle.kts file and without using an extra plugin which does seem unnecessary and problematic with multiplatform;

Note: in reality, few plugins work well with the multiplatform plugin from what I can tell, which is why I suspect its design philosophy is so verbose itself. It's actually fairly elegant IMHO, but not flexible or documented enough so it takes a ton of trial and error to setup even WITHOUT additional plugins.

Hope this helps others.

kotlin {
    jvm {
        compilations {
            val main = getByName("main")
            tasks {
                register<Jar>("fatJar") {
                    group = "application"
                    manifest {
                        attributes["Implementation-Title"] = "Gradle Jar File Example"
                        attributes["Implementation-Version"] = archiveVersion
                        attributes["Main-Class"] = "[[mainClassPath]]"
                    }
                    archiveBaseName.set("${project.name}-fat")
                    from(main.output.classesDirs, main.compileDependencyFiles)
                    with(jar.get() as CopySpec)
                }
            }
        }
    }
}
Tennietenniel answered 3/7, 2020 at 19:37 Comment(1)
If you getting "is a duplicate but no duplicate handling strategy has been set" error than add "duplicatesStrategy = DuplicatesStrategy.WARN" line after "group = .."Necropsy
T
8

You could use the ShadowJar plugin to build a fat jar:

import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

buildscript {
    repositories {
        mavenCentral()
        gradleScriptKotlin()
    }
    dependencies {
        classpath(kotlinModule("gradle-plugin"))
        classpath("com.github.jengelman.gradle.plugins:shadow:1.2.3")
    }
}

apply {
    plugin("kotlin")
    plugin("com.github.johnrengelman.shadow")
}

repositories {
    mavenCentral()
}

val shadowJar: ShadowJar by tasks
shadowJar.apply {
    manifest.attributes.apply {
        put("Implementation-Title", "Gradle Jar File Example")
        put("Implementation-Version" version)
        put("Main-Class", "com.mkyong.DateUtils")
    }

    baseName = project.name + "-all"
}

Simply run the task with 'shadowJar'.

NOTE: This assumes you're using GSK 0.7.0 (latest as of 02/13/2017).

Tippets answered 22/1, 2017 at 19:27 Comment(7)
Not working, in the first line import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar, ShadowJar is unresolved..Moonier
Have you tried my answer since? I have it tested and working. I think the original problem was that the() required an extension class.Tippets
I tried it again right now, on the tasks it says [DELEGATE_SPECIAL_FUNCTION_MISSING] Missing 'getValue(Build_gradle, KProperty<*>)' method on delegate of type 'TaskContainer!'Moonier
Hm, are you on the latest GSK for sure? Compiler and plugin too?Tippets
I am on the last one here. How can I check compiler and plugin? Could you give a try yourself here? I don't know, maybe I am missing some basic step somewhere..Moonier
I took a look and saw that you weren't on the newest dev version of GSK, but rather the most recent stable version. To take advantage of task delegation as outlined above, you'll need to update your gradle distributionUrl. Once I cloned your repo and updated that, it all worked. You can find the latest releases for GSK here: github.com/gradle/gradle-script-kotlin/releasesTippets
Just tried once again, GSK 0.8.0, same resultMoonier
D
3

Wow, the above answers and comments to this now 7-year-old question saved my sanity ... I've upvoted all above who helped me realize the bare-bones build.gradle.kts below - thank you all for contributing!

This is for a build of an AWS Lambda using their Kotlin SDK, running by their Java 21 runtime on the arm64 architecture. The .jar to upload to said AWS Lambda is built from the command line with Gradle 8.5:

>gradle awsJar

plugins {
    kotlin("jvm") version "1.9.20"
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("aws.sdk.kotlin:aws-core-jvm:0.35.0-beta")
    implementation("aws.sdk.kotlin:s3:0.35.0-beta")

    implementation("com.amazonaws:aws-lambda-java-core:1.2.3")
    implementation("com.amazonaws:aws-lambda-java-events:3.11.3")

    runtimeOnly("com.amazonaws:aws-lambda-java-log4j2:1.5.1")
}

kotlin {
    jvmToolchain(21)
}

task("awsJar", type = Jar::class) {
    manifest.attributes["Main-Class"] = "com.example.Handler.handleRequest"
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    from(configurations.runtimeClasspath.get().map { if (it.isDirectory()) it else zipTree(it) })
    with(tasks.jar.get() as CopySpec)
}

Dicot answered 16/12, 2023 at 3:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.