7

I recently switched from old 1.2 multiplatform into 1.3. Difference is, there's one one build.gradle file per multiplatform module (I got 5 of them) so a lot less configuration. However I can't seem to be able to configure creating runnable fat jar with all dependencies from jvm platform. I used to use standard "application" plugin in my jvm project and jar task, but that does not work anymore. I found there's "jvmJar" task and I modified it (set Main-class), but created jar doesn't contain dependencies and crashes on ClassNotFoundException. How do I do it?

This is what I have now:

    jvm() {
        jvmJar {
            manifest {
                attributes 'Main-Class': 'eu.xx.Runner'
            }
            from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
        }

    }
K.H.
  • 1,383
  • 13
  • 33
  • Perhaps [ShadowJar](https://imperceptiblethoughts.com/shadow/introduction/) would help you? – Greg Kopff Jul 24 '19 at 04:20
  • Yeah possibly. Still not sure how to approach it. – K.H. Jul 24 '19 at 22:33
  • 1
    "there's one one build.gradle file per multiplatform module" Do you have a source for this? The documentation is terrible as I suspect you'll agree and I never heard such a thing was even possible. – gunslingor Jul 07 '20 at 02:59

4 Answers4

3

I did hit that bump and used this work around.

1. Restructure your project

Lets call your project Project.

create another submodule say subA, which will have the gradle notation Project:subA

now, subA has your multiplatform code in it (It is the gradle project with apply :kotlin-multiplafrom) in its build.gradle

2. Add Another submodule

create another submodule which targets only jvm say subB, which will have the gradle notation Project:subB

So, subB will have plugins: 'application' and 'org.jetbrains.kotlin.jvm'

3. Add your module as a gradle dependency (see my build.gradle)

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.3.31'
    id "application"
}

apply plugin: "kotlinx-serialization"

group 'tz.or.self'
version '0.0.0'

mainClassName = "com.example.MainKt"

sourceCompatibility = 1.8

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

dependencies {
    implementation project(':subA')
}

you can proceed and build subB as you would a regular java project or even use the existing plugins, it will work

Community
  • 1
  • 1
andylamax
  • 1,858
  • 1
  • 14
  • 31
  • Why are there 2 projects? Why do they have to be subprojects? I like the general idea though. I tried to create another module on same level as `project` with kotlin/jvm/jar settings + dependency on `project` and it works perfectly. – K.H. Jul 28 '19 at 10:08
  • If it does work perfectly, please take your time to accept the answer – andylamax Jul 29 '19 at 00:57
  • For now, kotlin-multiplatform plugin works well with the application plugin, but incase you start targeting android, you are going to get plugin conflicts (if you use com.android.library or com.android.application in your mpp). I just had to use this as a work around. More over I attempted to configure the application plugin with the kotlin-multiplatform target without success – andylamax Jul 29 '19 at 01:01
  • Don't wanna accept your answer as this isn't what I ended up with. As I mentioned in comment, my way does work perfectly, based on your idea of having separate project, but not quite. – K.H. Jul 30 '19 at 14:33
2

Got it working with the multiplatform plugin in kotlin 1.3.61:

The following works for a main file in src/jvmMain/kotlin/com/example/Hello.kt

Hello.kt must also specify its package as package com.example

I configured my jvm target in this way:

kotlin {
    targets {
        jvm()

        configure([jvm])  {
            withJava()
            jvmJar {
                manifest {
                    attributes 'Main-Class': 'com.example.HelloKt'
                }
                from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
            }
        }
    }
}
Mike Samuel
  • 118,113
  • 30
  • 216
  • 245
luca992
  • 1,548
  • 22
  • 27
  • runtimeClasspath was not working for my multiplatform (Application) project because it was empty. So I found and used the jvmRuntimeClasspath and it worked perfectly. – Andreas Jul 28 '21 at 08:14
2

The only way to get gradle/multiplatform working appears to be endless trial and error; It's a nightmare, it's not being built as a "build" system so much as a "build system"; to put it another way, these two tools (together or in isolation) are a means of implementing only a single software development life cycle that the plugin maker intended, however, if you've engineered a desired software lifecycle and CI/CD system and now your trying to implement that engineering, it will be MUCH harder to do it with these tools than it would be to do it with scripts, code or maven. There are a number of reasons for this:

  • Massive changing in coding convention due to the plugin makers only exposing bar minimum configurability, probably only giving access to the things they need for their own personal project.
  • Very poor documentation updates; Kotlin, gradle and plugins are changing so rapidly I have begun to seriously question the usefulness of these tools.

Thus, at the time of writing this seems to be the correct syntax to use when using kotlin 1.3.72, multiplatform 1.3.72, ktor 1.3.2 and gradle 6.2.2 (using the kts format).

Note the fatJar seems to assemble correctly but won't run, it can't find the class, so I included the second runLocally task I've been using in the mean time.

This isn't a complete solution so I hate posting it on here, but from what I can tell... it is the most complete and up to date solution I can find documented anywhere.

//Import variables from gradle.properties
val environment: String by project
val kotlinVersion: String by project
val ktorVersion: String by project
val kotlinExposedVersion: String by project
val mySqlConnectorVersion: String by project
val logbackVersion: String by project
val romeToolsVersion: String by project
val klaxonVersion: String by project
val kotlinLoggingVersion: String by project
val skrapeItVersion: String by project
val jsoupVersion: String by project
val devWebApiServer: String by project
val devWebApiServerVersion: 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 {
        compilations {
            val main = getByName("main")
            tasks {
                register<Jar>("buildFatJar") {
                    group = "application"
                    manifest {
                        attributes["Implementation-Title"] = "Gradle Jar File Example"
                        attributes["Implementation-Version"] = archiveVersion
                        attributes["Main-Class"] = "com.app.BackendAppKt"
                    }
                    archiveBaseName.set("${project.name}-fat")
                    from(main.output.classesDirs, main.compileDependencyFiles)
                    with(jar.get() as CopySpec)
                }
                register<JavaExec>("runLocally") {
                    group = "application"
                    setMain("com.app.BackendAppKt")
                    classpath = main.output.classesDirs
                    classpath += main.compileDependencyFiles
                }
            }
        }
    }
    js {
        browser { EXCLUDED FOR LENGTH }
    }
    sourceSets { EXCLUDED FOR LENGTH }
}
gunslingor
  • 1,358
  • 12
  • 34
  • Wow, can't believe my luck that you have answered this just yesterday. `The only way to get gradle/multiplatform working appears to be endless trial and error` Couldn't agree more! `Note the fatJar seems to assemble correctly but won't run, it can't find the class` I checked the contents of the jar using `jar tvf -fat-.jar` I noticed that the archive had a bunch of `.jar` files. I'm not an expert, it's been a while since I worked with fat jars but I guess fat jars should have only `.class` files I'll dig deeper, but perhaps something you'd already know? – Yogesh Nachnani Jul 08 '20 at 09:13
  • So [docs](https://docs.oracle.com/javase/tutorial/deployment/jar/downman.html) suggest that jars within jars are allowed. I tried to add a Class-Path by adding `attributes["Class-Path"] = main.compileDependencyFiles.map { it.name }.joinToString(separator = " ")` but that doesn't work either. Upon Checking MANIFEST.MF, the only oddity I found was that newline characters were added at the 72nd column (weird!). Basically I'm trying to get `java -jar ` to work – Yogesh Nachnani Jul 08 '20 at 13:09
  • The workaround I'm presently using, FWIW, is to take the jar created by your code, untar it using `jar xvf ` and then run it using `java -cp "./*:." com.app.BackendAppKt`. So it's probably down to how the manifest.mf is being generated. Needless to say, I'm a step away from pulling my hair out – Yogesh Nachnani Jul 08 '20 at 13:14
  • Actually solved it completely on another similar question. I think I was missing withJava here which is why it wasn't running build, almost, building correctly. Here is more info: https://stackoverflow.com/a/62770101/2550940 – gunslingor Jul 10 '20 at 00:16
1

Got it to work with a slightly modified version of what luca992 did:

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