Programming for fun and profit

A blog about software engineering, programming languages and technical tinkering

Wed 17 October 2018

Creating a standalone (runnable) Kotlin .jar file with IntelliJ and Gradle

Posted by Simon Larsén in Programming   

I've recently started dabbling in some Kotlin, and have found it a very pleasant experience. One of the first things I wanted to do was to create a standalone .jar file, including the Kotlin runtime and any other dependencies. This, as it turns out, was a bit tricky. In this short article, I will walk you through creating a small command line application using the awesome clikt library, and then packaging it into a standalone .jar.

Setting up

Start out with creating a new project by going to File -> New -> Project, select Gradle in the leftmost menu bar (i.e. not Kotlin), and then tick the Kotlin box in the Additional Libraries and Frameworks menu. Then just fill in any GroupId, ArtifactId and Version (I will use slarse, app and 0.1 for these fields, respectively). Then just click Next with the defaults until the project is created.

Initial Gradle configuration

In the project root, you should now have a file called build.gradle, which looks something like this:

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.2.51'
}

group 'slarse'
version '0.1'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

Before we can compile a project with clikt, we need to add it as a dependency. We can do that by adding compile "com.github.ajalt:clikt:1.5.0" in the dependencies section. It should now look like this:

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    compile "com.github.ajalt:clikt:1.5.0"
}

Then hit the little refresh symbol in the bottom left corner (should say Refresh Gradle Project when you hover your mouse over it) to download the new dependency. And that's it for now! We'll get back to the gradle.build file once we want to configure our jar task, but let's create the app first!

Creating the application

Let's make this easy: we'll just use the sample application available from the clikt documentation. It looks like this:

class Hello : CliktCommand() {
    val count: Int by option(help="Number of greetings").int().default(1)
    val name: String by option(help="The person to greet").prompt("Your name")

    override fun run() {
        for (i in 1..count) {
            echo("Hello $name!")
        }
    }
}

fun main(args: Array<String>) = Hello().main(args)

Create a Kotlin file called main.kt at src/main/kotlin/main.kt and paste the above code into it. Note that we are using the default package here (i.e. not defining a package) for the sake of simplicity.

For this to compile, we will need to add the following imports at the top:

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.prompt
import com.github.ajalt.clikt.parameters.types.int

And that's it for the application, you should now be able to run it as usual. When running it, there should appear a prompt in the terminal saying Your name:. With that out of the way, the only thing left to do is to package our fantastic application into a standalone .jar file.

Packaging the application into a standalone .jar file

This is actually not very difficult, but you need to know what to do. We need to create a so-called "fat" jar, which includes both the Kotlin runtime and the clikt library. We also need to specify the name of our main class.

jar {
    manifest {
        attributes 'Main-Class': 'MainKt'
    }
    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

Note that the class file generated by Kotlin for a file called something.kt will be SomethingKt.class, which is why our main class is called MainKt. With that in mind, the manifest section is self-explanatory: we specify the main class. The from section collects all compile dependencies (that we specified in the dependencies section) and package them with the .jar file. The little piece of logic in the lambda is to properly add directories and .jar files, respectively (directories are just added, .jar files are unzipped and added).

Important: The main class file must be specified with its fully qualified name. For example, if I were to define main.kt in the package se.slarse, then I would need to put se.slarse.MainKt instead of just MainKt in the manifest.

Anyway, that's really all we need to do. It should now be possible to run the jar Gradle task to produce a .jar file in build/libs/<ArtifactId>-<Version> (so in my case it is at build/libs/app-0.1.jar). And that's it, hope it helped someone!

Full source code and build.gradle

Here are both of the files we wrote in this tutorial, in their entirety.

// main.kt
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.prompt
import com.github.ajalt.clikt.parameters.types.int

class Hello : CliktCommand() {
    val count: Int by option(help="Number of greetings").int().default(1)
    val name: String by option(help="The person to greet").prompt("Your name")

    override fun run() {
        for (i in 1..count) {
            echo("Hello $name!")
        }
    }
}

fun main(args: Array<String>) = Hello().main(args)

// build.gradle
plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.2.51'
}

group 'se.slarse'
version '0.1'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    compile "com.github.ajalt:clikt:1.5.0"
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

jar {
    manifest {
        attributes 'Main-Class': 'MainKt'
    }
    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}