2

I tried to use ShadowJar gradle pluging to pack my ktor app into fat jar. But as result of shadowJar task i get every time almost empty jar. It contains only manifest (main class is properly set).

Gradle configuration (groovy):

import org.gradle.jvm.tasks.Jar

buildscript {
    ext.kotlin_version = '1.3.72'
    ext.ktor_version = '1.3.2'
    ext.serialization_version = '0.20.0'
    ext.sl4j_version = '1.6.1'
    repositories { jcenter() }

    dependencies {
        classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
    }
}

plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.3.61'
}

apply plugin: 'kotlinx-serialization'
apply plugin: 'application'
apply plugin: 'java'

mainClassName = 'com.example.JvmMainKt'

apply plugin: 'com.github.johnrengelman.shadow'

repositories {
    mavenCentral()
    jcenter()
}
group 'com.example'
version '0.0.1'

apply plugin: 'maven-publish'

kotlin {

    jvm {
    }
    js {
        browser {
        }
        nodejs {
        }
    }
    // For ARM, should be changed to iosArm32 or iosArm64
    // For Linux, should be changed to e.g. linuxX64
    // For MacOS, should be changed to e.g. macosX64
    // For Windows, should be changed to e.g. mingwX64
    mingwX64("mingw") {
        binaries {
            executable {
                // Change to specify fully qualified name of your application's entry point:
                entryPoint = 'main'
                // Specify command-line arguments, if necessary:
                runTask?.args('')
            }
        }
    }
    sourceSets {
        commonMain {
            dependencies {
                implementation kotlin('stdlib-common')
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
                implementation "io.ktor:ktor-client-core:$ktor_version"
            }
        }
        commonTest {
            dependencies {
                implementation kotlin('test-common')
                implementation kotlin('test-annotations-common')
            }
        }
        jvmMain {
            dependencies {
                implementation kotlin('stdlib-jdk8')
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version"
                implementation "io.ktor:ktor-serialization:$ktor_version"
                implementation "io.ktor:ktor-server-netty:$ktor_version"
                implementation "org.slf4j:slf4j-simple:$sl4j_version"

            }
        }
        jvmTest {
            dependencies {
                implementation kotlin('test')
                implementation kotlin('test-junit')
            }
        }
        jsMain {
            dependencies {
                implementation kotlin('stdlib-js')
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
            }
        }
        jsTest {
            dependencies {
                implementation kotlin('test-js')
            }
        }
        mingwMain {
            dependencies {
                implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version"
            }
        }
        mingwTest {
        }
    }
}

shadowJar {
    mainClassName = 'com.example.JvmMainKt'

    mergeServiceFiles()
}
marcu
  • 223
  • 4
  • 14

3 Answers3

3

marcu's answer is correct but he doesn't explain why that snipped solved his issue, and he's code snippet cannot be directly converted to build.gradle.kts because a cast is needed to see runtimeDependencyFiles from Kotlin. In groovy, his snippet works because groovy supports duck typing while Kotlin doesnt.

I needed this solution in Gradle Kotlin, so I'm sharing it this way.

The com.github.johnrengelman.shadow gradle plugin is designed to work with regular the java gradle plugin, which builds a single jar by default, so it automatically generates a single fat-jar based on that jar classpath.

The Kotlin-Multiplatform gradle plugin works differently, it creates jar files for each jvm target based on a lot of custom settings that is set up using a methodology that is unique to Kotlin-Multiplatform, that's why the default shadowJar task does not work out of the box on Kotlin-Multiplatform.

To solve this problem, we have to create a new ShadowJar task for each jvm target that we need a fat-jar manually, we can do it after we declare the targets (as marcu's example is showing), or during the creation of them. I'm going to show both ways, but I recommend doing during the creation to avoid having to cast objects.

Another thing is that I created function to apply the needed configuration because I have a 8 JVM targets, so this way I don't need to copy-paste the snipped 8 times, I only call the function instead.

During the creation of the target

The explanations are commented on the code:

// Add this imports on top of your build.gradle.kts file
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget

// Since you need a main class, I added this default constant
// which you can change as you need:
val defaultMainClassName: String? = null

// Here I declare the function that will register the new ShadowJar task for the target
// If you need another mainClassName, you can pass it as parameter
fun KotlinJvmTarget.registerShadowJar(mainClassName: String? = defaultMainClassName) {
    // We get the name from the target here to avoid conflicting 
    // with the name of the compilation unit
    val targetName = name
    // Access the main compilation
    // We only want to create ShadowJar 
    // for the main compilation of the target, not the test
    compilations.named("main") {
        // Access the tasks
        tasks {
            // Here we register our custom ShadowJar task, 
            // it's being prefixed by the target name
            val shadowJar = register<ShadowJar>("${targetName}ShadowJar") {
                // Allows our task to be grouped alongside with other build task
                // this is only for organization
                group = "build"
                // This is important, it adds all output of the build to the fat-jar
                from(output)
                // This tells ShadowJar to merge all jars in runtime environment
                // into the fat-jar for this target
                configurations = listOf(runtimeDependencyFiles)
                // Here we configure the name of the resulting fat-jar file
                // appendix makes sure we append the target name before the version
                archiveAppendix.set(targetName)
                // classifier is appended after the version, 
                // it's a common practice to set this classifier to fat-jars
                archiveClassifier.set("all")

                // Apply the main class name attribute
                if (mainClassName != null) {
                    manifest {
                        attributes("Main-Class" to mainClassName)
                    }
                }

                // This instruction tells the ShadowJar plugin to combine
                // ServiceLoader files together, this is needed because
                // a lot of Kotlin libs uses service files and
                // they would break without this instruction 
                mergeServiceFiles()
            }

            // Finally, we get the normal jar task for this target
            // and tells kotlin to execute our recently created ShadowJar task
            // after the normal jar task completes
            getByName("${targetName}Jar") {
                finalizedBy(shadowJar)
            }
        }
    }
}
kotlin {
    // Here we create a JVM target
    jvm("jvm8") {
        // We can configure anything we want
        compilations.all {
            kotlinOptions.jvmTarget = "1.8"
        }
        testRuns["test"].executionTask.configure {
            useJUnitPlatform()
        }
        // If we need a ShadowJar for it, 
        // all we have to do now is call
        // our custom function
        // Here's an example of what I'm saying:
        // https://stackoverflow.com/a/57951962/804976
        registerShadowJar()
    }
    // Another one just to illustrate the example
    jvm("jvm16") {
        compilations.all {
            kotlinOptions.jvmTarget = "16"
        }
        testRuns["test"].executionTask.configure {
            useJUnitPlatform()
        }
        registerShadowJar()
    }
}
With comments removed
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget

val defaultMainClassName: String? = null

fun KotlinJvmTarget.registerShadowJar(mainClassName: String? = defaultMainClassName) {
    val targetName = name
    compilations.named("main") {
        tasks {
            val shadowJar = register<ShadowJar>("${targetName}ShadowJar") {
                group = "build"
                from(output)
                configurations = listOf(runtimeDependencyFiles)
                archiveAppendix.set(targetName)
                archiveClassifier.set("all")
                if (mainClassName != null) {
                    manifest {
                        attributes("Main-Class" to mainClassName)
                    }
                }
                mergeServiceFiles()
            }
            getByName("${targetName}Jar") {
                finalizedBy(shadowJar)
            }
        }
    }
}
kotlin {
    jvm("jvm8") {
        compilations.all {
            kotlinOptions.jvmTarget = "1.8"
        }
        testRuns["test"].executionTask.configure {
            useJUnitPlatform()
        }
        registerShadowJar()
    }
    jvm("jvm16") {
        compilations.all {
            kotlinOptions.jvmTarget = "16"
        }
        testRuns["test"].executionTask.configure {
            useJUnitPlatform()
        }
        registerShadowJar()
    }
}

After the creation of the target

Now, I'm going to comment only the changes:

import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget

kotlin {
    jvm("jvm8") {
        compilations.all {
            kotlinOptions.jvmTarget = "1.8"
        }
        testRuns["test"].executionTask.configure {
            useJUnitPlatform()
        }
        // Not registering on creation this time,
        // we are going to register the task later
    }
    jvm("jvm16") {
        compilations.all {
            kotlinOptions.jvmTarget = "16"
        }
        testRuns["test"].executionTask.configure {
            useJUnitPlatform()
        }
    }
}


val defaultMainClassName: String? = null

// Instead of having KotlinJvmTarget as receiver,
// we are going to cast the target to it.
// We are also getting the target name from
// the function parameter
fun registerShadowJar(targetName: String, mainClassName: String? = defaultMainClassName) {
    // Get the target casting to KotlinJvmTarget in the process
    kotlin.targets.named<KotlinJvmTarget>(targetName) {
        // Access the main compilation
        compilations.named("main") {
            tasks {
                val shadowJar = register<ShadowJar>("${targetName}ShadowJar") {
                    group = "build"
                    from(output)
                    configurations = listOf(runtimeDependencyFiles)
                    archiveAppendix.set(targetName)
                    archiveClassifier.set("all")
                    if (mainClassName != null) {
                        manifest {
                        attributes("Main-Class" to mainClassName)
                        }
                    }
                    mergeServiceFiles()
                }
                getByName("${targetName}Jar") {
                    finalizedBy(shadowJar)
                }
            }
        }
    }
}

// Now we call the method for each JVM target that we need to create a ShadowJar
registerShadowJar("jvm8")
registerShadowJar("jvm16")
With comments removed
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget

kotlin {
    jvm("jvm8") {
        compilations.all {
            kotlinOptions.jvmTarget = "1.8"
        }
        testRuns["test"].executionTask.configure {
            useJUnitPlatform()
        }
    }
    jvm("jvm16") {
        compilations.all {
            kotlinOptions.jvmTarget = "16"
        }
        testRuns["test"].executionTask.configure {
            useJUnitPlatform()
        }
    }
}

val defaultMainClassName: String? = null

fun registerShadowJar(targetName: String, mainClassName: String? = defaultMainClassName) {
    kotlin.targets.named<KotlinJvmTarget>(targetName) {
        compilations.named("main") {
            tasks {
                val shadowJar = register<ShadowJar>("${targetName}ShadowJar") {
                    group = "build"
                    from(output)
                    configurations = listOf(runtimeDependencyFiles)
                    archiveAppendix.set(targetName)
                    archiveClassifier.set("all")
                    if (mainClassName != null) {
                        manifest {
                        attributes("Main-Class" to mainClassName)
                        }
                    }
                    mergeServiceFiles()
                }
                getByName("${targetName}Jar") {
                    finalizedBy(shadowJar)
                }
            }
        }
    }
}
registerShadowJar("jvm8")
registerShadowJar("jvm16")
Polyana Fontes
  • 3,156
  • 1
  • 27
  • 41
0

I've faced the same issue in the past, the syntax that was working (for groovy gradle) for me was:

shadowJar {
    mergeServiceFiles()
    manifest {
        attributes 'Main-Class': 'com.example.JvmMainKt'
    }
}
Animesh Sahu
  • 7,445
  • 2
  • 21
  • 49
0

I finally found solution to my problem.

import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

//...

task shadowJar2(type: ShadowJar) {
    def target = kotlin.targets.jvm
    from target.compilations.main.output
    def runtimeClasspath = target.compilations.main.runtimeDependencyFiles
    manifest {
        attributes 'Main-Class': mainClassName
    }
    configurations = [runtimeClasspath]
}
marcu
  • 223
  • 4
  • 14