How to create a fat JAR with Gradle Kotlin script?

29,764

Solution 1

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).

Solution 2

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)
                }
            }
        }
    }
}

Solution 3

Note that the first 3 methods here 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"] = "org.example.MainKt"
}

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 information about manifests, see Oracle Java Documentation: Working with Manifest files.
For difference between tasks.create() and tasks.register() see this post.

Note that your resource files will be included in the JAR file automatically (assuming they were placed in /src/main/resources/ directory or any custom directory set as resources root in the build file). To access a resource file in your application, use this 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"));
    

Solution 4

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).

Share:
29,764
elect
Author by

elect

https://github.com/elect86/ https://github.com/kotlin-graphics

Updated on July 08, 2022

Comments

  • elect
    elect almost 2 years

    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") {
    
    }
    
  • elect
    elect over 7 years
    Not working, in the first line import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar, ShadowJar is unresolved..
  • mbStavola
    mbStavola about 7 years
    Have you tried my answer since? I have it tested and working. I think the original problem was that the() required an extension class.
  • elect
    elect about 7 years
    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!'
  • mbStavola
    mbStavola about 7 years
    Hm, are you on the latest GSK for sure? Compiler and plugin too?
  • elect
    elect about 7 years
    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..
  • mbStavola
    mbStavola about 7 years
    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/releases
  • elect
    elect about 7 years
    Just tried once again, GSK 0.8.0, same result
  • Mitchell Tracy
    Mitchell Tracy over 6 years
    He's not kidding. This should be added somewhere in github.com/gradle/kotlin-dsl/tree/master/samples
  • PHPirate
    PHPirate over 5 years
    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.
  • Laurence
    Laurence about 5 years
    And also note that for Gradle 5.4 you will need to replace with(tasks["jar"] as CopySpec) with with(getByName("jar") as CopySpec)
  • t-h-
    t-h- about 5 years
    @Laurence 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.CopySpec
  • jtonic
    jtonic almost 5 years
    In gradle 5.4.1 I had to replace with(tasks["jar"] as CopySpec) with val jar: CopySpec by getting(Jar::class); with(jar)
  • mbonnin
    mbonnin almost 5 years
    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 jar
  • breandan
    breandan over 4 years
    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.
  • breandan
    breandan over 4 years
    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-25709
  • mustofa.id
    mustofa.id about 4 years
    For who facing deprecated warning on baseName and version , use archiveBaseName.set("") and archiveVersion instead.
  • Chris
    Chris about 3 years
    This does not work anymore for gradle 6.8
  • FrontierPsychiatrist
    FrontierPsychiatrist about 3 years
    @Chris Try it with attributes["Implementation-Version"] = archiveVersion instead
  • Dave Yarwood
    Dave Yarwood over 2 years
    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.
  • Dave Yarwood
    Dave Yarwood over 2 years
    ...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.
  • soloturn
    soloturn about 2 years
    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.
  • Mahozad
    Mahozad about 2 years
    Sorry, I don't know about this.
  • soloturn
    soloturn about 2 years
    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.
  • Mahozad
    Mahozad about 2 years
    Maybe this Gradle guide can help you.