Building a self-executable JAR with Gradle and Kotlin
Asked Answered
A

8

60

I've written a simple Kotlin source file in order to get started, and a Gradle script file. But I can't figure out how to add the main function to the manifest, so that the JAR could be self-executable.

Here is my build.gradle script :

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:0.9.66'
    }
}
apply plugin: "kotlin"
repositories {
    mavenCentral()
}
dependencies {
    compile 'org.jetbrains.kotlin:kotlin-stdlib:0.9.66'
}

jar {
    manifest {
        attributes 'Main-Class': 'com.loloof64.kotlin.exps.ExpsPackage'
    }
}

Here is my com.loloof64.kotlin.exps.Multideclarations.kt:

package com.loloof64.kotlin.exps

class Person(val firstName: String, val lastName: String) {
    fun component1(): String {
        return firstName
    }
    fun component2(): String {
        return lastName
    }
}

fun main(args: Array < String > ) {
    val(first, last) = Person("Laurent", "Bernabé")
    println("First name : $first - Last name : $last")
}

When I launch the JAR from terminal (java -jar MYJar.jar) I get the following stack trace, saying that the Kotlin reflection library classes are missing, and indeed they have not been added to the JAR. It seems that I am missing the kotlin-compiler artifact classes from the final JAR, and also the kotlin-stdlib sources, but I don't know how to adapt the Gradle build.

$> java -jar build/libs/kotlin_exps.jar 

Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/reflect/jvm/internal/InternalPackage
    at com.loloof64.kotlin.exps.ExpsPackage.<clinit>(MultiDeclarations.kt)
Caused by: java.lang.ClassNotFoundException: kotlin.reflect.jvm.internal.InternalPackage
    at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 1 more

I am using Kotlin 0.9.66 and Gradle 2.1.

Andvari answered 20/10, 2014 at 15:29 Comment(3)
Note that the classname generated by Kotlin for a top-level function such as main is the package defined in the same file, plus the filename with KT appended. In your case this would be com.loloof64.kotlin.exps.MultideclarationsKT. You can change this behavior by adding to the top of the file @file:JvmName("OtherName") to define a new classname for all top-level functions in a file.Worrell
You append the two characters Kt not KT... that is upper case K and lower case t. It is an easy error to make, but will cause the jar execution to fail. I had this problem myself on occasion due to imperfect typing.Quad
See this rather comprehensive answer.Chiclayo
A
29

I've found the workaround (thanks to MkYong website)

  1. The gradle script needed the kotlin-compiler artifact as a dependency
  2. The gradle script needed a way to collect all kotlin files and put them into the jar.

So i get with the following gradle script :

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
       classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.1-2'
    }
}

apply plugin: "kotlin"

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.jetbrains.kotlin:kotlin-stdlib:1.0.1-2'
}

jar {
    manifest {
        attributes 'Main-Class': 'com.loloof64.kotlin.exps.MultideclarationsKT'
    }

    // NEW LINE HERE !!!
    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}
Andvari answered 20/10, 2014 at 16:16 Comment(5)
for me it worked without the compiler dependency, collecting the files for the jar was enoughTerrific
Can confirm that the compiler dependency is not needed, collecting the files is enough.Currency
Where is the jar after executing the task?Importance
Placement of the jar file depends on the method to build the jar. If you execute the gradle jar task it is put into the directory build/libs. The build directory is at the same level as the build.gradle file. If you construct the jar mapping inside the IntelliJ IDE as an artifact and build the artifact with IntelliJ, then it is placed inside the out/artifacts directory at the level of the IntelliJ projectQuad
Specifically, I found that after adding this jar_block to the build.gradle file, then I could create a successful jar either using the IntelliJ artifact mechanism or by building the jar file with gradle task mechanism.Quad
J
38

Add the plugin application, then set the mainClassName as

mainClassName = '[your_namespace].[your_arctifact]Kt'

For instance, suppose you have placed the following code in a file named main.kt:

package net.mydomain.kotlinlearn

import kotlin
import java.util.ArrayList

fun main(args: Array<String>) {

    println("Hello!")

}

your build.gradle should be:

apply plugin: 'kotlin'
apply plugin: 'application'

mainClassName = "net.mydomain.kotlinlearn.MainKt"

In fact Kotlin is building a class to encapsulate your main function named with the same name of your file - with Title Case.

Jobey answered 15/5, 2015 at 15:14 Comment(6)
Can you update your answer to the new naming convention for the how Kotlin creates classes for toplevel functions. In his example, that would now be: com.loloof64.kotlin.exps.MultideclarationsKT as the classname. This is true for Kotlin 1.0 beta and beyond.Worrell
Updated for Kotlin 1.0 Beta and newer. Thanks Jayson.Jobey
This shouldn't be the accepted answer; it's correct but not the full picture: It's required to 'collect' the Kotlin runtime files to make a complete executable jar, as below in @Andvari 's example.Mitchellmitchem
This does not work anymore. I'll remove my downvote if you fix it.Smoodge
@AdamArold I tested with 1.1.51 and it works. What version are you referring to? ThxJobey
I'm using 1.1.4-3.Smoodge
A
29

I've found the workaround (thanks to MkYong website)

  1. The gradle script needed the kotlin-compiler artifact as a dependency
  2. The gradle script needed a way to collect all kotlin files and put them into the jar.

So i get with the following gradle script :

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
       classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.1-2'
    }
}

apply plugin: "kotlin"

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.jetbrains.kotlin:kotlin-stdlib:1.0.1-2'
}

jar {
    manifest {
        attributes 'Main-Class': 'com.loloof64.kotlin.exps.MultideclarationsKT'
    }

    // NEW LINE HERE !!!
    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}
Andvari answered 20/10, 2014 at 16:16 Comment(5)
for me it worked without the compiler dependency, collecting the files for the jar was enoughTerrific
Can confirm that the compiler dependency is not needed, collecting the files is enough.Currency
Where is the jar after executing the task?Importance
Placement of the jar file depends on the method to build the jar. If you execute the gradle jar task it is put into the directory build/libs. The build directory is at the same level as the build.gradle file. If you construct the jar mapping inside the IntelliJ IDE as an artifact and build the artifact with IntelliJ, then it is placed inside the out/artifacts directory at the level of the IntelliJ projectQuad
Specifically, I found that after adding this jar_block to the build.gradle file, then I could create a successful jar either using the IntelliJ artifact mechanism or by building the jar file with gradle task mechanism.Quad
F
22

None of the above solution worked for me while building Artifact.

IDE version IntelliJ IDEA 2019.1.1.

To fix the issue, do the following

Steps

Step 1 - Create Artifact

  1. Go to File -> Project Structure -> Artifacts

  2. Click the + -> JAR -> From modules with dependencies

enter image description here

  1. Select your program's Main Class

enter image description here

Step 2 - Change MANIFEST Path

  1. Change value of Directory for META-INF/MANIFEST.MF to your project root.

    For example , from /your/project/directory/src/main/kotlin to /your/project/directory

enter image description here

  1. Press OK,then Press Apply and OK.

Step 3 - Build Artifact

  1. Finally, Go to Build -> Build Artifacts -> [your-artifact-name] -> Build.

The generated JAR file can be found in the out/artifact/[your-artifact-name] directory. (y)

Fann answered 22/4, 2019 at 7:32 Comment(2)
Thank you very much!!! After several attempts, that was the only thing that worked for me!Conservator
Kotlin is such a cool language, but it has a horrendous build system (magic recepies all around). Thanks for the help!Arnold
C
4

This is for Kotlin DSL (build.gradle.kts).
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 Doc: 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"));
    
Chiclayo answered 18/5, 2022 at 7:42 Comment(0)
C
2

In case you happen to be dumb, like me:

Don't create your IntelliJ run configuration as an Application. IntelliJ will assume it's Java & it will never work. Instead, use the "Kotlin" entry.

Curran answered 16/7, 2017 at 2:50 Comment(0)
S
2

Thanks, for me it worked adding the jar section

jar {
    manifest {
        attributes 'Main-Class': 'com.photofiles.Application'
    }

    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}

No need for the application plugin.

Suboxide answered 18/8, 2018 at 6:39 Comment(1)
The main-class name needs to end with Kt. So above it should be ApplicationKt.Quad
R
2

If using Gradle with the Kotlin DSL, then my duplicate question has an answer of:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
    kotlin("jvm") version "1.2.51"
    id("com.github.johnrengelman.shadow") version "2.0.4"
}

group = "xxx.yyy"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation(kotlin("stdlib-jdk8"))
}

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "1.8"
}

tasks.withType<ShadowJar> {

    manifest.attributes.apply {
        put("Implementation-Title", "Gradle Jar File Example")
        //put("Implementation-Version" version)
        put("Main-Class", "HelloKotlinWorld.App")
    }

Which is, I think, the simplest solution. Oh, perhaps you're using just Kotlin itself and not the DSL.

Rainstorm answered 17/12, 2018 at 12:25 Comment(0)
M
1

For anyone using the Kotlin MP plugin here is your code

jvm{
    jvmJar {
        manifest{
            attributes 'Main-Class':'Class path here'
        }
    }

}
Maharashtra answered 26/1, 2020 at 21:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.