Creating an application w/ bundled JVM for distribution

Hello!
I solved an annoying problem related to creating a nice packaged .exe file for my game today, and I wanted to share that.
First up, I’m using Kotlin with Gradle and LibGDX to develop my game. A couple days ago, I tried to share the game with a friend, but because I was using Java 11 to compile it and they only had Java 8, they weren’t able to run it. Sure, they could update the JVM, but that’s annoying and I want the game to just work.
After some research, it seemed that the most modern solution to this was the jpackage tool introduced in Java 14. I downloaded the most recent JDK, 15.0.1, and put a copy of it in my project folder so I wouldn’t have to worry about environment variables or whatever. With a little bit of Gradle Groovy scripting, I was able to create a task that packages the game with a Java runtime and gives an executable and some folders for distribution.

task jpackage(type: Exec) { //exec tasks=command line tasks
project(":client") { // go inside of the client project
    doFirst() {
        project.delete("${buildDir}/distribution/powerworks") // delete results of last bundling
    }
    workingDir project.projectDir // set cmd line working directory to the project dir
    def commands = [ //new list of cmd arguments, will be separated by spaces
            "${project.projectDir}/jdk-15.0.1/bin/jpackage", // first, the command itself. jpackage.exe is located in jdk/bin
            '--type', 'app-image', // select app-image here to just create the runnable file, otherwise it defaults to runnable installer
            '--dest', "${buildDir}/distribution", // destination for files
            '--input', "${buildDir}/libs", // this should be the folder containing just the game's jar file
            '--name', 'powerworks', // name of app
            '--main-class', project.mainClassName, // references the project's main class, if you don't set this above somewhere just add it manually
            '--main-jar', jar.archiveFile.get().asFile.getName(), //references the jar task, this can also be set manually
    ]

    if (osName.contains('windows')) { //just platform specific icon stuff and the mac xstartonfirstthread
        commands << '--icon'
        commands << "${project.projectDir}/logo.ico"
    } else if (osName.contains('linux')) {
        commands << '--icon'
        commands << "${project.projectDir}/logo.png"
    } else if (osName.contains('mac')) {
        commands << '--icon'
        commands << "${project.projectDir}/logo.icns"
        commands << '--java-options'
        commands << "-XstartOnFirstThread"
    }

    commandLine = commands // this is what actually executes the command
}
}

Every other time I’ve tried to do this I failed miserably, but this time I succeeded. I hope this can be of some small help to someone. One final note, you can select the JVM runtime that it uses by using the --runtime-image option, but if you don’t want to it will automatically create one for you with jlink. I tried to select it but it was causing problems so I just removed the argument entirely.
I partially wanted to make this because every other guide seemed to be out of date with the latest version of jpackage, so I promise this one works with JDK 15.0.1.

5 Likes

Nice… I’ve used Packr for my last project and it worked very well with Java 8. But I also tried it with Java 11 and above and failed. So next time I will try with jpackage tool.

There is also a jlink tool since Java 9 that may work out. Has anyone tried this?

I believe @philfrei has had a go with it.

I used jlink to package ReferenceNoteKeyboard. I think it was OpenJDK 9 and OpenJFX. If I remember correctly, it took me from 300+ MB down to around 80+ MB, and zipping brought the file down to the high teens. I did it on the command line from source code. There were only a couple of steps, the first being a javac command, then copy the assets/resources into the compiled file structure, then run the jlink command. After you do it once you can copy and save each cli command.

But it’s should also be possible now to first compile at the IDE, then use jlink on the resulting jar. It’s a lot easier these days to use jlink, since the IDE support for ensuring that the module.info file is correct is much better now.

I wrote a tutorial but I think it is pretty out of date. Also it was very much geared to someone who barely knew how to get around with DOS or BASH.

I haven’t used both of them myself, but there exists a Gradle plugin which allows to use both jlink and jpackage tools by the name Badass Jlink Plugin.

The instructions seem pretty clear to me, although I can’t test it as I’m currently stuck on Java 8 for Android development.

1 Like

I’ve tried this on linux now for my setup. Works perfectly, thanks for sharing!

Maybe there is still some possibility to improve the size of the image by removing more unused stuff and libs manually? will try this when I find some time.