beryx / badass-jlink-plugin

Create a custom runtime image of your modular application
https://badass-jlink-plugin.beryx.org
Apache License 2.0
386 stars 27 forks source link

Kotlin Coroutines `Cannot derive uses clause from service loader invocation ...` (Ktor) #218

Closed dosier closed 1 year ago

dosier commented 2 years ago

Hello,

I'd like to preface that I am new to the module system introduced in Java 9, so bare with me if this is a silly question.

Context: I am building a GUI using JavaFX that interacts with an API through HTTP requests. I am using Ktor for the HTTP client. Goal: I want to use the JLink plugin to create an image of my app that others can run. Problem: When Ktor launches a coroutine, the application crashes with the following stack trace:

Exception in thread "DefaultDispatcher-worker-1" java.lang.NoClassDefFoundError: Could not initialize class kotlinx.coroutines.CoroutineExceptionHandlerImplKt
        at nl.rug.merged.module@1.0-SNAPSHOT/kotlinx.coroutines.CoroutineExceptionHandlerKt.handleCoroutineException(Unknown Source)
        at nl.rug.merged.module@1.0-SNAPSHOT/kotlinx.coroutines.internal.LimitedDispatcher.run(Unknown Source)
        at nl.rug.merged.module@1.0-SNAPSHOT/kotlinx.coroutines.scheduling.TaskImpl.run(Unknown Source)
        at nl.rug.merged.module@1.0-SNAPSHOT/kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(Unknown Source)
        at nl.rug.merged.module@1.0-SNAPSHOT/kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(Unknown Source)
        at nl.rug.merged.module@1.0-SNAPSHOT/kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(Unknown Source)
        at nl.rug.merged.module@1.0-SNAPSHOT/kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(Unknown Source)

My initial configuration looked as follows:

plugins {
    java
    application
    id("org.openjfx.javafxplugin") version "0.0.13"
    id("org.beryx.jlink") version "2.25.0"
    kotlin("jvm") version "1.6.21"
    kotlin("plugin.serialization") version "1.6.21"
}

group   = "nl.rug"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    val ktor_version = "2.1.1"
    val coroutine_version = "1.6.4"
    implementation("org.kordamp.ikonli:ikonli-javafx:12.3.1")
    implementation("io.ktor:ktor-client-core:${ktor_version}")
    implementation("io.ktor:ktor-client-cio:${ktor_version}")
    implementation("io.ktor:ktor-client-content-negotiation:${ktor_version}")
    implementation("io.ktor:ktor-serialization-kotlinx-json:${ktor_version}")
    implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:$coroutine_version")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-javafx:$coroutine_version")
    implementation("no.tornado:tornadofx:1.7.20")
}

application {
    mainModule.set("nl.rug.gscholarfx")
    mainClass.set("nl.rug.gscholarfx.ScholarApplication")
    applicationName = "Google Scholar GUI"
    applicationDefaultJvmArgs = listOf("--add-reads", "kotlin.stdlib=kotlinx.coroutines.core.jvm")
}

java {
    toolchain.languageVersion.set(JavaLanguageVersion.of(17))
}

javafx {
    version = "17.0.2"
    modules(
        "javafx.controls",
        "javafx.fxml",
    )
}

jlink {
    options.set(listOf("--strip-debug", "--compress", "2", "--no-header-files", "--no-man-pages"))
    addExtraDependencies("javafx", "kotlinx-datetime", "kotlinx-coroutines", "kotlinx-serialization")
    launcher {
        name = "Google Scholar GUI"
        jvmArgs = listOf("--add-reads","kotlin.stdlib=kotlinx.coroutines.core.jvm")
        noConsole = false
    }
}

tasks {
    withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
        kotlinOptions.jvmTarget = "17"
    }
}

Whenever I run the :createMergedModule Gradle task, I get the following warnings

> Task :createMergedModule
Cannot derive uses clause from service loader invocation in: kotlinx/coroutines/internal/MainDispatcherLoader.loadMainDispatcher().
Cannot derive uses clause from service loader invocation in: kotlinx/coroutines/internal/FastServiceLoader.load().
Cannot derive uses clause from service loader invocation in: kotlinx/coroutines/CoroutineExceptionHandlerImplKt.<clinit>().
Cannot derive uses clause from service loader invocation in: io/ktor/client/HttpClientJvmKt.<clinit>().
Cannot derive uses clause from service loader invocation in: kotlin/reflect/jvm/internal/impl/resolve/OverridingUtil.<clinit>().
Cannot derive uses clause from service loader invocation in: kotlin/reflect/jvm/internal/impl/builtins/BuiltInsLoader$Companion$Instance$2.invoke().
Cannot derive uses clause from service loader invocation in: kotlin/reflect/jvm/internal/impl/descriptors/Visibilities.<clinit>().

This lead me to believe I have to manually provide merged module info, so I ran the :suggestMergedModuleInfo task and this gave me the following suggestions:

 mergedModule {
        requires("javafx.graphics")
        requires("java.management")
        requires("javafx.controls")
        requires("java.json")
        requires("java.logging")
        requires("java.prefs")
        requires("java.desktop")
        requires("javafx.base")
        requires("java.instrument")
        requires("javafx.fxml")
        requires("jdk.unsupported")
        uses("tornadofx.Stylesheet")
        uses("tornadofx.ChildInterceptor")
        provides("io.ktor.client.HttpClientEngineContainer").with("io.ktor.client.engine.cio.CIOEngineContainer")
        provides("kotlin.reflect.jvm.internal.impl.resolve.ExternalOverridabilityCondition")
            .with("kotlin.reflect.jvm.internal.impl.load.java.ErasedOverridabilityCondition",
                "kotlin.reflect.jvm.internal.impl.load.java.FieldOverridabilityCondition",
                "kotlin.reflect.jvm.internal.impl.load.java.JavaIncompatibilityRulesOverridabilityCondition")
        provides("kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoader")
            .with("kotlin.reflect.jvm.internal.impl.serialization.deserialization.builtins.BuiltInsLoaderImpl")
        provides("kotlinx.coroutines.internal.MainDispatcherFactory")
            .with("kotlinx.coroutines.javafx.JavaFxDispatcherFactory")
    }

When I include this in the jlink { ... } configuration block, the warning messages shown in the :createMergedModule Gradle task are gone, however when I run the launcher in the generated image, I still get the same exception as before. It does run perfectly fine using the :run task from the application Gradle plugin.

Could anyone shed some light on why this happens, and how I should go about fixing it?

Thanks!

dosier commented 2 years ago

I should note that running coroutines using my own defined CoroutineScope works perfectly fine, e.g.

val searchScope = CoroutineScope(Dispatchers.IO)

authorSearchButton.apply {
    disableWhen(authorSearchField.textProperty().isEmpty)
    setOnAction {
        searchScope.launch {
            println("this is reached")
            val searchProfileResponse = ScholarClient.fetchProfiles(authorSearchField.text)
            println("this is not")
            withContext(Dispatchers.JavaFx) {
                authorListView.items.setAll(searchProfileResponse.profiles)
            }
        }
    }
}

Here is the implementation of ScholarClient.fetchProfiles:

// Crashes when it calls this method
suspend fun fetchProfiles(mauthors: String): SearchProfileResponse =
        client.get(Endpoint.Profiles.makeUrl() + "&mauthors=$mauthors").body()
dosier commented 2 years ago

Similar issue: https://github.com/Kotlin/kotlinx.coroutines/issues/3032

airsquared commented 1 year ago

The latest Kotlin version has Java module support.