gradlegradle-kotlin-dslgraalvmgraalvm-native-imageclikt

GraalVM native-image is unable to find lib from FatJar


GraalVM version on macOS Sonoma 14.5:

java -version
java version "21" 2023-09-19
Java(TM) SE Runtime Environment Oracle GraalVM 21+35.1 (build 21+35-jvmci-23.1-b15)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 21+35.1 (build 21+35-jvmci-23.1-b15, mixed mode, sharing)

gradle version:

gradle --v

------------------------------------------------------------
Gradle 8.7
------------------------------------------------------------

Build time:   2024-03-22 15:52:46 UTC
Revision:     650af14d7653aa949fce5e886e685efc9cf97c10

Kotlin:       1.9.22
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          21 (Oracle Corporation 21+35-jvmci-23.1-b15)
OS:           Mac OS X 14.5 aarch64

build.gradle.kts:

plugins {
    kotlin("jvm") version "1.9.23"
}

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

tasks.withType<Jar> {
    manifest {
        attributes["Main-Class"] = "com.example.SampleCli"
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.github.ajalt.clikt:clikt:4.3.0")
    testImplementation(kotlin("test"))
}

tasks.test {
    useJUnitPlatform()
}
kotlin {
    jvmToolchain(21)
}

fun getClasspath() = "libs/" + file("$buildDir/libs").list()?.joinToString(":libs/")

task<Exec>("buildNativeImage") {
    description = "Build native image using GraalVM"
    dependsOn(tasks.getByName("build"))
    workingDir(buildDir)
    commandLine(
        "native-image",
        "-cp", getClasspath(),
        "-H:+ReportExceptionStackTraces",
        "-H:+AddAllCharsets",
        // "--report-unsupported-elements-at-runtime",
        // "--allow-incomplete-classpath",
        "--no-server",
        "--no-fallback",
        "--enable-http",
        "--enable-https",
        "com.example.SampleCli",
        "com.example"
    )
}

Main-Class:

package com.example

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.help
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.prompt
import com.github.ajalt.clikt.parameters.types.int

class SampleCli() : CliktCommand(name="cli") {

    val count: Int by option().int().default(1).help("Number of greetings")
    val name: String by option().prompt("Your name").help("The person to greet")

    override fun run() {
        repeat(count) {
            echo("Hello $name!")
        }
    }
}

fun main(args: Array<String>) = SampleCli().main(args)

Whereas I am to build this from the command line and also via IntellIJ using ./gradlew build:

BUILD SUCCESSFUL in 647ms
3 actionable tasks: 3 executed

It fails when I try to generate a GraalVM native-image (same error when I try ./gradlew buildNativeImage):

native-image -jar build/libs/SampleCli-1.0-SNAPSHOT.jar cli

Error:

========================================================================================================================
GraalVM Native Image: Generating 'cli' (executable)...
========================================================================================================================
[1/8] Initializing...                                                                                    (0.0s @ 0.15GB)

The build process encountered an unexpected error:

> java.lang.NoClassDefFoundError: com/github/ajalt/clikt/core/CliktCommand

Please inspect the generated error report at:
/Users/user1/SampleProject/svm_err_b_20240603T04124.720_pid14052.md

If you are unable to resolve this problem, please file an issue with the error report at:
https://graalvm.org/support


Solution

  • Despite your question title, you are not building a bad-practice fat jar, but your jar only contains your own code.

    One way to fix this is to use the -cp argument to native-image to properly give your dependencies to the tool. With your manual call you do not give any -cp argument at all, with your buildNativeImage task you give a -cp argument, but most probably it does not have the correct value. You can easily check that by printing the value you give or by using --info or --debug to see the arguments given to the JavaExec task.

    Another possibility would most probably be to use the GraalVM Gradle plugin which you can find at https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html, which most probably handles the dependencies properly.