70

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") {

}
Mahozad
  • 18,032
  • 13
  • 118
  • 133
elect
  • 6,765
  • 10
  • 53
  • 119
  • Here is a [related question](https://stackoverflow.com/q/21721119/8583692). – Mahozad Feb 12 '22 at 13:01
  • Does this answer your question? [Building a self-executable jar with Gradle and Kotlin](https://stackoverflow.com/questions/26469365/building-a-self-executable-jar-with-gradle-and-kotlin) – Mahozad Feb 12 '22 at 13:06

4 Answers4

59

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

FrontierPsychiatrist
  • 1,343
  • 10
  • 20
  • 6
    He's not kidding. This should be added somewhere in https://github.com/gradle/kotlin-dsl/tree/master/samples – Mitchell Tracy Dec 11 '17 at 21:33
  • 24
    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](https://github.com/gradle/kotlin-dsl/issues/1082#issuecomment-433037363). – PHPirate Dec 19 '18 at 17:54
  • And also note that for Gradle 5.4 you will need to replace `with(tasks["jar"] as CopySpec)` with `with(getByName("jar") as CopySpec)` – Laurence Apr 16 '19 at 20:50
  • @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` – t-h- Apr 21 '19 at 16:18
  • In gradle 5.4.1 I had to replace `with(tasks["jar"] as CopySpec)` with `val jar: CopySpec by getting(Jar::class); with(jar)` – jtonic May 10 '19 at 16:25
  • 2
    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 – mbonnin Jul 27 '19 at 17:22
  • 5
    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 Aug 25 '19 at 02:11
  • 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: https://youtrack.jetbrains.com/issue/KT-25709 – breandan Oct 02 '19 at 01:50
  • For who facing deprecated warning on `baseName` and `version` , use `archiveBaseName.set("")` and `archiveVersion` instead. – mustofa.id Mar 02 '20 at 06:42
  • 1
    This does not work anymore for gradle 6.8 – Chris Mar 14 '21 at 19:48
  • @Chris Try it with `attributes["Implementation-Version"] = archiveVersion` instead – FrontierPsychiatrist Mar 22 '21 at 07:32
  • 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 Jan 17 '22 at 20:39
  • ...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. – Dave Yarwood Jan 17 '22 at 20:44
26

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 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"));
    
Mahozad
  • 18,032
  • 13
  • 118
  • 133
  • 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. – soloturn Feb 12 '22 at 18:38
  • Sorry, I don't know about this. – Mahozad Feb 12 '22 at 18:42
  • 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. – soloturn Feb 13 '22 at 13:36
  • Maybe [this Gradle guide](https://docs.gradle.org/current/userguide/signing_plugin.html#sec:signing_tasks) can help you. – Mahozad Feb 13 '22 at 14:31
  • got an answer in the gradle forums: https://discuss.gradle.org/t/build-variant-command-line/42193/8 . which now is in plantuml main gradle script, signing working. – soloturn May 12 '23 at 07:08
  • Upvote for shadow plugin, which was frictionless with Kotlin – kris larson May 26 '23 at 13:57
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)
                }
            }
        }
    }
}
gunslingor
  • 1,358
  • 12
  • 34
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).

mbStavola
  • 358
  • 3
  • 13
  • Not working, in the first line `import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar`, `ShadowJar` is unresolved.. – elect Jan 23 '17 at 20:20
  • Have you tried my answer since? I have it tested and working. I think the original problem was that the() required an extension class. – mbStavola Feb 09 '17 at 20:09
  • I tried it again [right now](http://imgur.com/qUozPd2), on the `tasks` it says `[DELEGATE_SPECIAL_FUNCTION_MISSING] Missing 'getValue(Build_gradle, KProperty<*>)' method on delegate of type 'TaskContainer!'` – elect Feb 09 '17 at 20:26
  • Hm, are you on the latest GSK for sure? Compiler and plugin too? – mbStavola Feb 10 '17 at 00:40
  • I am on the last one [here](https://repo.gradle.org/gradle/dist-snapshots/). How can I check compiler and plugin? Could you give a try yourself [here](https://github.com/java-graphics/assimp)? I don't know, maybe I am missing some basic step somewhere.. – elect Feb 10 '17 at 08:27
  • 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: https://github.com/gradle/gradle-script-kotlin/releases – mbStavola Feb 13 '17 at 23:29
  • Just tried once again, GSK 0.8.0, [same result](http://imgur.com/VvfCPWq) – elect Mar 09 '17 at 09:03