5

Trying to create a runnable jar for a kotlin multiplatform project which includes a ktor server component, building with Kotlin Gradle DSL.

I have seen several questions including Create fat jar from kotlin multiplatform project which asks and answers how to create the gradle build file in Groovy, but how do you do it in kotlin dsl?

The groovy code that is reported to work is:

kotlin {
jvm() {
    withJava()
    jvmJar {
        manifest {
            attributes 'Main-Class': 'sample.MainKt'
        }
        from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
    }
}
...
}

How would this translate to Kotlin DSL? I have tried many variations, some of which compile and run, but don't create the desired output... a runnable jar.

cfnz
  • 196
  • 3
  • 14
  • Worth noting IMHO, multi-platform breaks many plugins, including google app-engine. After 4 months developing with it, unfortunately mostly "on" it not "with" it, I am trying something else. gralde sub-projects... I think I can accomplish the same thing in a more traditionally engineered fashion (only 8 hours in but impressed so far with what I can flexibly do with it). – gunslingor Jul 10 '20 at 00:24

4 Answers4

3

I spent 3 days trying to get the fat jar to work, below is the solution and what follows before the solution is clarification:

ERRORS I MADE EARLY

  • Shouldn't have rushed into docker, should've made fat jar work local first.
  • withJava() was left out, this was the main waste of 36 man hours... WTF is the point of this function?
  • dependsOn(build): why the jar task type doesn't know this already I do not understand.
  • main.compileDependencyFiles: I was using this for a time in place of the map from argument below.
  • main.output.classesDirs: was missing from other solutions and is what seems to include your actual code.

NOTE: No shadow plug in required, which is fantastic (gradle plugins tend not to play well together IMHO, ever).

NOTE: Versioning is important because this stack seems to change roughly 50 times faster than the documentation does in some cases, the following was used for this solution:

  • Kotlin 1.3.72
  • Gradle 6.5
  • Ktor 1.3.2

CODE:

//Import variables from gradle.properties
val environment: String by project
val kotlinVersion: String by project
val ktorVersion: String by project

//Build File Configuration
plugins {
    java
    kotlin("multiplatform") version "1.3.72"
}

group = "com.app"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
    jcenter()
    jcenter {
        url = uri("https://kotlin.bintray.com/kotlin-js-wrappers")
    }
    maven {
        url = uri("https://jitpack.io")
    }
}

//Multiplatform Configuration
kotlin {
    jvm {
        withJava()
        compilations {
            val main = getByName("main")
            tasks {
                register<Jar>("buildFatJar2") {
                    group = "application"
                    dependsOn(build)
                    manifest {
                        attributes["Main-Class"] = "com.app.BackendAppKt"
                    }
                    from(configurations.getByName("runtimeClasspath").map { if (it.isDirectory) it else zipTree(it) }, main.output.classesDirs)
                    archiveBaseName.set("${project.name}-fat2")
                }
            }
        }
    }
    js {
        browser {

        }
    }
    sourceSets { SKIPPED FOR LENGTH }
}

I hope this saves someone 3 days, let me know if you find improvements (I'm still learning too). Kotlin, gradle, multiplatform, docker... all are very tough to deal with, they need to update the docs in parallel IMHO or jetbrains is doomed.

POTENTIAL IMPROVEMENTS:

  • The produced jar looks much bigger than it should be with tons of unnecessary stuff, suspect changing to the compile path instead of runtime path will fix that (CONFIRMED 30% size reduction).
  • More manifest attributes perhaps.
  • Also worth noting, I read an article aptly suggesting fatJars shouldn't be deployed to docker, that the java dependencies should be built as part of the image.
gunslingor
  • 1,358
  • 12
  • 34
  • This worked very well for me with mpp/kotlin/1.4 ktor and js (I added the js output to your task). You made my day! Actually, three days :D Thanks man! – Jako Sep 06 '20 at 05:40
1

@andylamax answer is pretty close but leads to the error that @cfnz was seeing

To fix that you need to add doFirst as in this example:

val jvm = jvm() {
    withJava()
    val jvmJar by tasks.getting(org.gradle.jvm.tasks.Jar::class) {
        doFirst {
            manifest {
                attributes["Main-Class"] = project.ext["mainClass"]
            }
            from(configurations.getByName("runtimeClasspath").map { if (it.isDirectory) it else zipTree(it) })
        }
    }
}

It is working as expected in here with gradle jvmJar && java -jar build/libs/laguna-jvm.jar

Javier Diaz
  • 1,791
  • 1
  • 17
  • 25
  • This didn't actually work for my project so I started a new multiplatform project and it I can confirm that the built jar does work :-). So this solution at least works and I can now start to build up my app and see what causes my failure. Thanks. – cfnz Apr 29 '20 at 07:48
1

This one worked for me with using gradle multiplatform (mpp) project

EDIT:

tasks {
    named<JavaExec>("run") {
        standardInput = System.`in`
        classpath += objects.fileCollection().from(
            named("compileKotlinJvm"),
            configurations.named("jvmRuntimeClasspath")
        )
    }
    shadowJar {
        manifest { attributes["Main-Class"] = theMainClass }
        val jvmJar = named<org.gradle.jvm.tasks.Jar>("jvmJar").get()
        from(jvmJar.archiveFile)
        configurations.add(project.configurations.named("jvmRuntimeClasspath").get())
    }
}
--- old version:
plugins {
    kotlin("multiplatform") version Deps.JetBrains.Kotlin.VERSION
    application
    id("com.github.johnrengelman.shadow") version Deps.Plugins.Shadow.VERSION
}
application {
    mainClass.set(theMainClass)
}

tasks {
    val shadowCreate by creating(com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar::class) {
        manifest { attributes["Main-Class"] = theMainClass }
        archiveClassifier.set("fat")
        mergeServiceFiles()
        from(kotlin.jvm().compilations.getByName("main").output)
        configurations = mutableListOf(kotlin.jvm().compilations.getByName("main").compileDependencyFiles as Configuration)
    }
    val build by existing {
        dependsOn(shadowCreate)
    }
}
tasks.named<JavaExec>("run") {
    classpath += objects.fileCollection().from(
        tasks.named("compileKotlinJvm"),
        configurations.named("jvmRuntimeClasspath")
    )
}
Dirk Hoffmann
  • 1,444
  • 17
  • 35
0

Your groovy dsl can be written in kotlin as follows

kotlin {
  jvm {
    withJava()
    val jvmJar by tasks.getting(org.gradle.jvm.tasks.Jar::class) {
        manifest {
            attributes["Main-Class"] = "sample.MainKt"
        }
        from(configurations.getByName("runtimeClasspath").map { if (it.isDirectory) it else zipTree(it) })
    }
  }
}
andylamax
  • 1,858
  • 1
  • 14
  • 31
  • To compile I had to put the code in a `jvm {}` block, i.e. `kotlin { jvm { ... }}`. So it compiles and you can build it but it still does not make a fat jar. Any idea how to go about that? – cfnz Apr 17 '20 at 02:49
  • @cfnz I usually create another gradle project. With plugin kotlin("jvm") and use the multiplatform lib as a dependency – andylamax Apr 18 '20 at 04:42
  • Still trying to get this to work. Getting error `A problem occurred configuring project, Failed to notify project evaluation listener. > Cannot change dependencies of dependency configuration ':my-multiplatform-project:jvmApi' after it has been included in dependency resolution.` Same error for `:jvmRuntime`. If I take out the `withJava()` I don't get the error, but the jar still does not have all the dependencies. – cfnz Apr 21 '20 at 23:19
  • Also noted that without `withJava()` I needed the `application` plugin. Have tried the solution with and without the `application` plugin no no avail. – cfnz Apr 21 '20 at 23:33
  • I suggest you use my workaround. Create another kotlin-jvm project, use your mpp project as a dependency to it. apply the `application` plugin, and that should work. It does work for me – andylamax Apr 23 '20 at 00:03
  • I did try your workaround, but still no go. However, see accepted answer... that also did not work for my project so I started a new multiplatform project and it worked. So at least I can start building that out and find the problem. Thanks for your help. – cfnz Apr 29 '20 at 07:46