Building a Kotlin fat JAR with IntelliJ IDEA

and with Kotlin scripting, not Groovy

Here are my initial assumptions:

  • You want to build a command-line tool or server task.
  • You want to use Kotlin for as much of the project as possible.
    • You might use some Java-based libraries
    • Your main main() binary will be in Kotlin.
  • You want building the binary to be as quick and easy as possible.
  • You want to end up with a single fat jar file you can deploy to any JVM.
  • You’re using IntelliJ IDEA, or possibly the community edition aimed at Android developers.

First of all, don’t set up a new Kotlin project using IntelliJ IDEA. If you do that, the project will be set up to have IDEA build the code, which will make your life harder. In fact, if you want to (say) use kotlinx.serialization it’ll make your life impossible. Yes, there’s a plugin to build Kotlin code with IDEA, but think of that as a set of training wheels to get newbies writing Kotlin without needing to know about build tasks.

I say this because I tried to use Kotlin without Gradle being involved. I’d had a few bad experiences involving Android development, and really didn’t want to repeat them. However, eventually I just had to give in and start using Gradle, and when I did… it actually wasn’t that bad. Gradle’s pretty speedy these days.

But to get to that point, you need to tell IDEA to set up a Gradle project, but targeting Kotlin/JVM. At that point I was faced with a small but significant checkbox: “Kotlin DSL build script”. I checked the box.

Gradle is traditionally scripted with Groovy, a scripting language modeled on Ruby. Groovy has a bit of a troubled history – it started off as a project of G2One, who were bought by SpringSource, who were bought by VMware. VMware were bought by EMC, Pivotal was spun off and given Groovy to look after, EMC were bought by Dell, and Pivotal gave Groovy to the Apache Software Foundation, which tends to be a bit of a software hospice ward. The JSR document for standardizing Groovy was set to dormant in 2012 after 8 years of inactivity. Not auspicious, and I don’t feel like investing time in learning Groovy.

The downside of using Kotlin for scripting Gradle is that almost all the how-to information out on the Internet assumes you’re using Groovy – even for building Kotlin applications, because Kotlin scripting of Gradle only hit 1.0 in 2018.

Directory structure

So, I told IDEA to set me up a Gradle project targeting Kotlin/JVM with Kotlin DSL build script. I gave it a group ID and project name, it churned away for a while and set me up a new project:

.gradle/
.idea/
gradle/
src/
  main/
    java/
    kotlin/
    resources/
  test/
    java/
    kotlin/
    resources/
build.gradle.kts
gradle.properties
gradlew
gradlew.bat
settings.gradle

It also enabled the Gradle integration in IDEA, which added an editor pane listing the available Gradle tasks so I could just double-click to (say) build a jar.

I dropped a couple of Kotlin files into src/main/kotlin and was able to build a jar. It didn’t run, though – no main class in manifest.

Adding a manifest

Gradle with the Kotlin DSL has an application plugin. I thought that might do the trick, so I added it to the plugins stanza in the build.gradle.kts file:

plugins {
    kotlin("jvm") version "1.3.40"
    application
}

…and added a stanza specifying options, including the main class name:

application {
    mainClassName = "myapp.MainKt"
}

No luck. It turns out the application plugin is to make it easy to run the application from inside your IDE. However, it’s not too hard to change how the jar file is generated to add the manifest info, and take it from the application options so you don’t have to specify the class name twice:

tasks.withType<Jar> {
    manifest {
        attributes["Main-Class"] = application.mainClassName
    }
}

I now had a Hello World application that would build, run, build into a jar, and run as a jar.

Adding dependencies

I’d already decided I wanted to use tinylog with its Kotlin API. That turns out to be simple enough. I opened up the build.gradle.kts file again and found the dependencies section and added to it:

dependencies {
    // Improved Kotlin stdlib using JDK 8 features
    implementation(kotlin("stdlib-jdk8"))
    implementation("org.tinylog:tinylog-api-kotlin:2.0.0-M4.3")
    implementation("org.tinylog:tinylog-impl:2.0.0-M4.3")
}

I added some logging calls and ran a new build. As expected, I got a jar with my code in, but it would no longer run because the library classes weren’t found.

Fat Jar

I found a bunch of contradictory suggestions for how to build a fat JAR file. The method that works, which is the one JetBrains use themselves, is to use the Gradle Shadow Jar plugin.

The documentation for Gradle Shadow tells you how to use it if you’re scripting in Groovy, but not if you’re scripting in Kotlin. Turns out the incantation is as shown here:

plugins {
    kotlin("jvm") version "1.3.40"
    application
    id("com.github.johnrengelman.shadow") version "5.0.0"
}

There’s a catch, though. The Shadow plugin only works with Gradle 5 and up, whereas IDEA bundles an older version of Gradle. I downloaded and unpacked a binary distribution of Gradle 5 point something, and navigated through IDEA preferences, Build Execution Deployment, Build Tools, Gradle. There I chose “use local Gradle distribution” and pointed it at the directory I’d just unpacked.

The machine did mysterious things for a worrying amount of time, then rebuilt the project using Gradle 5.

One last thing

I had one more problem, which was that my dependencies soon grew to request two different versions of the same package — kotlin-reflect. The solution was to add an explicit dependency to force a particular version:

dependencies {
    implementation(kotlin("stdlib-jdk8"))
    implementation("org.jetbrains.kotlin:kotlin-reflect:1.3.40")
    implementation("org.tinylog:tinylog-api-kotlin:2.0.0-M4.3")
    implementation("org.tinylog:tinylog-impl:2.0.0-M4.3")
}

With that, I had a new set of tasks under shadow. I double-clicked shadowJar and IDEA started Gradle, which built me a fat jar that worked.