Search code examples
kotlingradlekotlin-multiplatform

How to execute Kotlin Multiplatform Jar


I have a full stack Kotlin Multiplatform web app with Kotlin/JVM backend and Kotlin/JS frontend.

The problem I'm having is that, when I go to execute the JAR, there is no Main-Class manifest entry and I get this error:

$ java -jar shoppinglist-jvm-1.0-SNAPSHOT.jar
no main manifest attribute, in shoppinglist-jvm-1.0-SNAPSHOT.jar

I assume that this is, for some reason, by design and that I'm missing something essential.

Further Details:

  • I am using the Ktor example project provided by Jetbrains, but this is also occurring my own project in the same way.
  • I am executing the jvm JAR in the libs folder, but I also attempted it on both the js and metadata JARs with the same result.
  • I am using gradle build within IntelliJ Run Configurations to compile.

Extracting the JARs with 7-Zip reveals that js and metadata don't contain Java bytecode; so, I'm, mostly, ignoring those for now.

However, jvm has interesting contents:

  • The manifest file contains the version without the Main-Class.
  • There is a .kotlin_module file that does contain some kind of class list, but I'm not sure how that can be used.
  • The bytecode .class and .js files are all in the expected locations.
  • There is a .tar and .zip file that contains all the lib JARs along with a script that seems as though it is supposed to start the application, but it's all in an archive.
    • In my project, these are included in the JAR itself.
    • In other projects, these are included in the distributions directory.

I'm thinking this is something simple that I'm missing, but I can't seem to find documentation or questions about this specifically anywhere.


Solution

  • You have 3 options to make it work.

    1. Out of box option

    # Build from the root dir
    ./gradlew clean build
    
    cd build/distributions/
    
    # unzip
    tar -xvf shoppinglist-1.0-SNAPSHOT.tar
    
    cd shoppinglist-1.0-SNAPSHOT/bin
    
    # Execute launch script (generated by application plugin)
    ./shoppinglist
    
    # Result
    Hello, JVM!
    

    2. Distribution option (My Fork with changes)

    We need to add our custom distribution and edit manifest if you really want to execute it like java -jar shoppinglist-jvm-1.0-SNAPSHOT.jar

    1. Replace application plugin with distribution plugin
    2. Remove application configuration block
    3. Remove this
    tasks.getByName<JavaExec>("run") {
        classpath(tasks.getByName<Jar>("jvmJar")) // so that the JS artifacts generated by `jvmJar` can be found and served
    }
    
    1. Replace distribution configuration block with
    distributions {
        main {
            distributionBaseName.set("shoppinglist")
            contents {
                into("") {
                    val jvmJar by tasks.getting
                    from(jvmJar)
                }
                into("lib/") {
                    val main by kotlin.jvm().compilations.getting
                    from(main.runtimeDependencyFiles)
                }
            }
        }
    }
    
    tasks.withType<Jar> {
        doFirst {
            manifest {
                val main by kotlin.jvm().compilations.getting
                attributes(
                    "Main-Class" to "ServerKt",
                    "Class-Path" to main.runtimeDependencyFiles.files.joinToString(" ") { "lib/" + it.name }
                )
            }
        }
    }
    

    Now you can launch it like so

    # Build from the root dir
    ./gradlew clean build
    
    cd build/distributions/
    
    # unzip
    tar -xvf shoppinglist-1.0-SNAPSHOT.tar
    
    # Run
    java -jar shoppinglist-jvm-1.0-SNAPSHOT.jar
    
    # Result
    Hello, JVM!
    

    3. uberJar (fatJar) option (My Fork with changes)

    1. Remove `applicaion` plugin
    2. Remove `distributions` and `application` configuration blocks
    3. Remove `stage` and `run` tasts
    4. Add uberJar task
    
    tasks.withType<Jar> {
        doFirst {
            duplicatesStrategy = DuplicatesStrategy.EXCLUDE
            val main by kotlin.jvm().compilations.getting
            manifest {
                attributes(
                    "Main-Class" to "ServerKt",
                )
            }
            from({
                main.runtimeDependencyFiles.files.filter { it.name.endsWith("jar") }.map { zipTree(it) }
            })
        }
    }