beryx/badass-jlink-plugin

Using jlink to create cross-platform JavaFX runtime image fails on Windows during runtime

Closed this issue · 4 comments

What I'm trying to do

I'm working on a JavaFX app with Java 11 and I'm trying to create artifacts that would also run on Windows 10, not just linux. I would like to build artifacts for all platforms on linux.

How I'm trying to do it

My build.gradle file looks like this:

plugins {
    id 'java'
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.10'
    id "org.beryx.jlink" version "2.24.0"
}

java {
    // This enables resource loading with modular apps
    modularity.inferModulePath.set(true)
}

javafx {
    version = "16"
    modules = ['javafx.controls', 'javafx.fxml']
    configuration = 'compileOnly'
}

group 'com.fixturecalendar.tylercrawler'
version '1.0-SNAPSHOT'
sourceCompatibility = 11
mainClassName = "com.fixturecalendar.tylercrawler.Main"

repositories {
    mavenCentral()
}

dependencies {
    implementation group: 'com.google.inject', name: 'guice', version: '5.0.1'
    implementation group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '3.141.59'
    implementation group: 'io.github.bonigarcia', name: 'webdrivermanager', version: '4.4.3'
    implementation group: 'org.slf4j', name: 'slf4j-nop', version: '1.7.30'

    runtimeOnly "org.openjfx:javafx-graphics:$javafx.version:$javafx.platform.classifier"
    runtimeOnly "org.openjfx:javafx-fxml:$javafx.version:$javafx.platform.classifier"
    runtimeOnly "org.openjfx:javafx-controls:$javafx.version:$javafx.platform.classifier"
    runtimeOnly "org.openjfx:javafx-base:$javafx.version:$javafx.platform.classifier"

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

test {
    useJUnitPlatform()
}

jlink {
    launcher {
        name = 'tylercrawler'
    }

    targetPlatform("linux") {
        jdkHome = jdkDownload("https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.11%2B9_openj9-0.26.0/OpenJDK11U-jdk_x64_linux_openj9_11.0.11_9_openj9-0.26.0.tar.gz")
    }

    targetPlatform("win") {
        jdkHome = jdkDownload("https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.11%2B9_openj9-0.26.0/OpenJDK11U-jdk_x64_windows_openj9_11.0.11_9_openj9-0.26.0.zip")
    }
}

What happens

After running ./gradlew jlinkZip, I see 2 zip files generated that I'm interested in:

  • image-linux.zip
  • image-win.zip

On linux everything works, though this is expected as the build happens on linux.

On windows I get the following error after unzipping the artifact and running its launcher:

Microsoft Windows [Version 10.0.19042.804]
(c) 2020 Microsoft Corporation. All rights reserved.

C:\Users\User\Desktop\image-win\tylercrawler-win\bin>java --version
openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment AdoptOpenJDK-11.0.11+9 (build 11.0.11+9)
Eclipse OpenJ9 VM AdoptOpenJDK-11.0.11+9 (build openj9-0.26.0, JRE 11 Windows 10 amd64-64-Bit Compressed References 20210421_976 (JIT enabled, AOT enabled)
OpenJ9   - b4cc246d9
OMR      - 162e6f729
JCL      - 7796c80419 based on jdk-11.0.11+9)

C:\Users\User\Desktop\image-win\tylercrawler-win\bin>tylercrawler
Graphics Device initialization failed for :  d3d, sw
Error initializing QuantumRenderer: no suitable pipeline found
java.lang.RuntimeException: java.lang.RuntimeException: Error initializing QuantumRenderer: no suitable pipeline found
        at javafx.graphics/com.sun.javafx.tk.quantum.QuantumRenderer.getInstance(QuantumRenderer.java:280)
        at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.init(QuantumToolkit.java:244)
        at javafx.graphics/com.sun.javafx.tk.Toolkit.getToolkit(Toolkit.java:261)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:286)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:160)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.startToolkit(LauncherImpl.java:658)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:409)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:363)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:566)
        at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1051)
Caused by: java.lang.RuntimeException: Error initializing QuantumRenderer: no suitable pipeline found
        at javafx.graphics/com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.init(QuantumRenderer.java:94)
        at javafx.graphics/com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:124)
        at java.base/java.lang.Thread.run(Thread.java:836)
Exception in thread "main" java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:566)
        at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1051)
Caused by: java.lang.RuntimeException: No toolkit found
        at javafx.graphics/com.sun.javafx.tk.Toolkit.getToolkit(Toolkit.java:273)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:286)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.startup(PlatformImpl.java:160)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.startToolkit(LauncherImpl.java:658)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:409)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:363)
        ... 5 more

My thoughts and what I've tried

I'm new to modular java and the cross-platform toolkit, all I know is from the docs I've read in the past few days. I think it could be that the JavaFX dependencies packed into the windows zip file are of a wrong platform, but I have tried providing platform-specific definitions and was getting different errors about duplicate packages or modules.

Can you spot any errors in the configuration or in my reasoning?

Since I'm working with a few things that are new to me, I'm not sure which piece of the chain the problem exists in. I appreciate any help or suggestions, thank you :)

Progress

I have found a related issue: beryx-gist/badass-jlink-example-log4j2-javafx#1

I have updated my build.gradle and checked that it works on both windows and linux, but I'm unable to specify the platform dynamically eg. in the example below the "win" platform in dependencies is hardcoded.

How can I define a dependency's platform dynamically from jlink?

plugins {
    id 'java'
    id 'application'
    id "org.javamodularity.moduleplugin" version "1.8.7"
    id "org.beryx.jlink" version "2.24.0"
}

java {
    // This enables resource loading with modular apps
    modularity.inferModulePath.set(true)
}

group 'com.fixturecalendar.tylercrawler'
version '1.0-SNAPSHOT'
sourceCompatibility = 11
mainClassName = "com.fixturecalendar.tylercrawler.Main"

repositories {
    mavenCentral()
}

dependencies {
    implementation group: 'com.google.inject', name: 'guice', version: '5.0.1'
    implementation group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '3.141.59'
    implementation group: 'io.github.bonigarcia', name: 'webdrivermanager', version: '4.4.3'
    implementation group: 'org.slf4j', name: 'slf4j-nop', version: '1.7.30'

    implementation "org.openjfx:javafx-base:16:win"
    implementation "org.openjfx:javafx-controls:16:win"
    implementation "org.openjfx:javafx-fxml:16:win"
    implementation "org.openjfx:javafx-graphics:16:win"

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

test {
    useJUnitPlatform()
}

jlink {
    launcher {
        name = 'tylercrawler'
    }

//    targetPlatform("linux") {
//        jdkHome = jdkDownload("https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.11%2B9_openj9-0.26.0/OpenJDK11U-jdk_x64_linux_openj9_11.0.11_9_openj9-0.26.0.tar.gz")
//    }

    targetPlatform("win") {
        jdkHome = jdkDownload("https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.11%2B9_openj9-0.26.0/OpenJDK11U-jdk_x64_windows_openj9_11.0.11_9_openj9-0.26.0.zip")
    }
}

Try something like this:

def currentOS = org.gradle.internal.os.OperatingSystem.current()
def platform = currentOS.isMacOsX() ? 'mac' : currentOS.isLinux() ? 'linux' : 'win'

dependencies {
  ...
  implementation compile("org.openjfx:javafx-base:16:${platform}")
  implementation compile("org.openjfx:javafx-controls:16:${platform}")
  implementation compile("org.openjfx:javafx-fxml:16:${platform}")
  implementation compile("org.openjfx:javafx-graphics:16:${platform}")
  ...
}

I appreciate your suggestion. However, it is not doing what I'm trying to do.

The code above sets the $platform variable to the platform which is running the build script, not the platform I'm building the app for. In my case I'm always building on linux even when I'm building for windows and therefore the dependencies get resolved to their linux versions which later fail during runtime on windows.

I worked around it by defining a gradle argument:

def platformOverride = "linux";
if (project.hasProperty("platform")) {
    platformOverride = project.getProperty("platform")
    println "Target platform set to $platformOverride"
}

dependencies {
    //...

    implementation "org.openjfx:javafx-base:16:$platformOverride"
    implementation "org.openjfx:javafx-controls:16:$platformOverride"
    implementation "org.openjfx:javafx-fxml:16:$platformOverride"
    implementation "org.openjfx:javafx-graphics:16:$platformOverride"

    //...
}

jlink {
    launcher {
        name = 'tylercrawler'
        noConsole = true
    }

    if (platformOverride == "linux") {
        targetPlatform("linux") {
            jdkHome = jdkDownload("https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.11%2B9_openj9-0.26.0/OpenJDK11U-jdk_x64_linux_openj9_11.0.11_9_openj9-0.26.0.tar.gz")
        }

    } else if (platformOverride == "win") {
        targetPlatform("win") {
            jdkHome = jdkDownload("https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.11%2B9_openj9-0.26.0/OpenJDK11U-jdk_x64_windows_openj9_11.0.11_9_openj9-0.26.0.zip")
        }

    } else if (platformOverride == "mac") {
        targetPlatform("mac") {
            jdkHome = jdkDownload("https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.11%2B9_openj9-0.26.0/OpenJDK11U-jdk_x64_mac_openj9_11.0.11_9_openj9-0.26.0.tar.gz")
        }
    }
}

Then I have to run the build three times, each time preceded with ./gradlew clean in order to flush the intermediary steps that contain artifacts for the wrong platforms:

./gradlew clean
./gradlew jlinkZip -Pplatform="linux"
// publish artifacts for linux

./gradlew clean
./gradlew jlinkZip -Pplatform="win"
// publish artifacts for win

./gradlew clean
./gradlew jlinkZip -Pplatform="mac"
// publish artifacts for mac

This is clumsy as jlink has the ability to build for all supported platforms in one step, I just am unable to configure it so that it uses the correct dependencies for the correct platform. I would need to define the dependencies (in my code $platformOverride) using a variable from the jlink step, so that each targeted platform takes dependencies for a matching platform.

Alternatively, I could just include all versions (for all platforms) of the said dependencies and then somehow configure gradle, java modules or something not to complain about multiple implementations of the same package, which I have tried but without success.

Yes, it's clumsy, but it's probably your only option. I don't see a better solution.