Foojay Today

Building JavaFX with Gradle

January 05, 2023

Author(s)

  • Avatar photo
    Michael Gasche

    Mike has worked as a software engineer, project manager, consultant and CEO and has over 20 years of experience in the Java programming language. He finally decided to develop his ... Learn more

Usually Maven is my build tool of choice and for Java front-ends I sometimes still build a front-end application using ANT, Swing with Oracle's JDK 8u202 and JDK 8's Java Packager, but don't tell anyone....

Over time, as I wanted to move forward with some more lightweight front-ends, I started building JavaFX applications.

Of course, I don't want to lock customers into JDK maintenance contracts; for backend software and servers, where the customer usually has their own runtime architecture, that's easy, but for GUIs, I generally don't want to ship a full JDK anymore, I want to use modular Java, ship a JDK of my choice, and provide a proper build system for it all. So that's it, I use the following components and software:

Besides all these good reasons, I also want to be faster at creating front-ends and creating nice user interfaces is easier than with Java Swing, I think:


A good reason to use JavaFX: FX and nice GUIs can be created fast!

My front-end applications are based on other Maven projects whose builds I install in my local Maven repository.

Some of them use open source libraries that are not perfectly modularized, and I assume they never will be. I also don't want to modularize every project in my library toolset, even if they are modules used by different projects.

Sooner or later you might run into the modularization jungle anyway and problems when, for example, the same module names are used for different modules. It seems that jakarta.activation is such a prominent case where the same module name was used for the API and the implementation.

There may also be other stumbling blocks encountered when trying to create modularized applications.

I know I could probably solve everything properly by excluding certain JARs from the Maven dependencies and going through each one, but at the end of the day I just don't want to spend time doing that, so I decided to create two Gradle build scripts that apply to modular and non-modular projects.

Modular

First, let's have a look at the modular version of the build-script build.gradle; see comments for explanations:

buildscript {
    ext {
        // Variables if any
        //commonsVersion = '1.0.0'
    }
    repositories {
        mavenLocal()
        mavenCentral()
    }
}

plugins {
    // Stuff we need
    id 'java'
    id 'org.javamodularity.moduleplugin' version "1.8.12"
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.13'
    // We use the Badass JLink Plugin from Serban Iordache
    // to build modular applications!
    id 'org.beryx.jlink' version '2.25.0'
}

apply plugin: 'java'
// Does some integration to eclipse, including linking Java FX SDK
apply plugin: 'eclipse'

jar {
    baseName = 'autumo-europa-client'
    version = '1.0.0'
}

repositories {
    mavenLocal()
    mavenCentral()
}

targetCompatibility = "17"
sourceCompatibility = "11"

project.description = "autumo Europa - Secure Communication."
project.ext.buildDate = new Date()
project.version = "1.0.0"

dependencies {
    implementation "ch.autumo.commons:autumo-commons:1.0.0"
    implementation 'org.apache.logging.log4j:log4j-api:2.17.1'
    implementation 'org.apache.logging.log4j:log4j-core:2.17.1'
    implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.1'
    implementation 'org.apache.commons:commons-lang3:3.12.0'
}

javafx {
    version = "19"
    // Found by eclipse
    modules = [ 'javafx.controls', 'javafx.fxml' ]
}

application {
    mainModule = 'ch.autumo.europa.client'
    mainClass = 'ch.autumo.europa.client.EuropaClient'
}

run {
    jvmArgs = ['-Djdk.gtk.version=2']
}

// JLink for modular projects
jlink {
    // Some default options
    options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
    launcher {
        name = 'Europa Client'
        jvmArgs = ['-Djdk.gtk.version=2']
    }
    // Does some magic to log4j JARs that don't want to merge
    forceMerge('log4j-api') 

    // Pack it!
    jpackage {
        // Could be taken from command line, here it is defined statically
        // project.findProperty('installerOs')
        //    (example: -PinstallerOs=mac)
        targetPlatformName = 'mac' 
        // Resource directory for native package overrides,
        // you can do lots of magic here too...
        resourceDir = file('package/')

        if (targetPlatformName == 'mac') { // we are on mac
            targetPlatform("mac") {
                // Use downloaded Eclipse Temurin JDK
                jdkHome = '/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home'
                // Use another local JDK
                //jdkHome = '/Library/Java/JavaVirtualMachines/jdk-17.0.2.jdk/Contents/Home'
            }
            installerType = 'pkg' // we want to have macOS PKG
        }
        if (targetPlatformName == 'win') { // we are on Windows
            targetPlatform("win") {
                jdkHome = 'C:/Applications/JDKs/Adoptium/temurin-17.jdk'
            }
            installerType = 'exe'
        }
        if (targetPlatformName == 'linux') { // we are on linux
            targetPlatform("linux") {
            }
            installerType = 'deb'
        }  

        // Add jpackage-specific options
        installerOptions = [
            '--name', 'Europa-Client', // installer name
            '--description', project.description,
            '--copyright', 'Copyrigth 2022 autumo GmbH',
            '--vendor', 'autumo GmbH'
        ]

        // We also could take the installer type from comand line
        // installerType = project.findProperty('installerType')
        // We would pass this from the command line
        //    (example: -PinstallerType=msi)

        // Add platform-specific options for the target image and for jpackage
        if (installerType == 'pkg') {
            imageOptions += ['--icon', 'src/main/resources/icon.icns']
            installerOptions += [
                '--license-file', 'package/LICENSE-OS-Installer.txt'
            ]
        }
        if (installerType == 'exe') {
            imageOptions += ['--icon', 'src/main/resources/icon.ico']
            installerOptions += [
                // '--win-per-user-install', // Install only for current user
                // '--win-console', // Shows what Java outputs to the console
                '--win-dir-chooser',  
                '--win-menu', '--win-shortcut'
            ]
        }
        if (installerType in ['deb', 'rpm']) {
            imageOptions += ['--icon', 'src/main/resources/icon_256x256.png']
            installerOptions += [
                '--linux-menu-group', 'Network',
                '--linux-shortcut'
            ]
        }
        if (installerType == 'deb') {
            installerOptions += [
                '--linux-deb-maintainer', '[email protected]'
            ]
        }
        if (installerType == 'rpm') {
            installerOptions += [
                '--linux-rpm-license-type', 'GPLv3'
            ]
        }
    }
}

jpackage {
    // Could be used for pre-checks;
    // e.g., are certain command line arguments defined?
    doFirst {
        // project.findProperty('installerOs')
        //    (example: -PinstallerOs=mac)
        // project.getProperty('installerType') // throws exception if its missing
    }
}


Non-Modular

When it comes to building non-modular JavaFX applications, we use the "The Badass Runtime Plugin" instead the "The Badass JLink Plugin" from Serban Iordache.

The build-script build.gradle for the non-modular version looks like this; see comments for explanations:

buildscript {
    ext {
    }
    repositories {
        mavenLocal()
        mavenCentral()
    }
}

plugins {
    // Stuff we need
    id 'java'
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.13'
    // We use the Badass Runtime Plugin from Serban Iordache
    // to build modular applications!
    id 'org.beryx.runtime' version '1.13.0'
}

apply plugin: 'java'
// Does some integration to eclipse, including linking Java FX SDK
apply plugin: 'eclipse'

jar {
    baseName = 'autumo-documents-ui'
    version = '1.0.0'
}

repositories {
    mavenLocal()
    mavenCentral()
}

targetCompatibility = "17"
sourceCompatibility = "8"

project.description = "The documetns viewer shows the ability of the autumo documents component to extract any file for indexing."
project.ext.buildDate = new Date()
project.version = "1.0.0"

javafx {
    version = "19"
    modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.web' ]
    // I want to specifially define where the JavaFX SDK is
    sdk = '/Users/Mike/Library/Java/javafx-sdk-19' // on mac
    //sdk = 'C:/Applications/JavaFX/javafx-sdk-19' // on windows
}

configurations {
    // In case of conflicts I could exclude modules
    //all*.exclude group: 'xml-apis'
}

dependencies {
    implementation "ch.autumo.commons:autumo-commons:1.0.0"
    implementation "ch.autumo.beetroot:autumo-beetroot:1.3.3"
    implementation "ch.autumo.search:autumo-search:1.0.0"   
    implementation "ch.autumo.documents:autumo-documents:1.0.0"
    implementation "org.apache.poi:poi:5.2.3"
    implementation "org.apache.poi:poi-ooxml:5.2.3"
    implementation "org.apache.poi:poi-scratchpad:5.2.3"
    implementation "org.apache.tika:tika-core:2.6.0"
    implementation "org.apache.tika:tika-parsers-standard-package:2.6.0"
    implementation "org.apache.pdfbox:pdfbox:2.0.27"
    implementation "org.apache.pdfbox:preflight:2.0.27"
    implementation "commons-dbutils:commons-dbutils:2.0-SNAPSHOT"
    implementation "org.jsoup:jsoup:1.15.3"
    implementation "com.google.zxing:core:3.5.0"
    implementation "com.google.zxing:javase:3.5.0"
    implementation "com.zaxxer:HikariCP:5.0.1"
    implementation "org.apache.lucene:lucene-core:9.4.0"
    implementation "org.apache.lucene:lucene-queryparser:9.4.0"
    implementation "org.apache.lucene:lucene-highlighter:9.4.0"
    implementation "org.apache.lucene:lucene-analyzers-common:8.11.2"
    implementation "org.apache.lucene:lucene-analyzers-phonetic:8.11.2"
    //implementation(files('lib/afile.jar')) // no local files needed
}

application {
    mainClass = 'ch.autumo.documents.ui.Main'
}

run {
    jvmArgs = ['-Djdk.gtk.version=2']
}

// Runtime for non-modular projects
runtime {
    // Some default options
    options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
    // The modules !
    modules = ['java.naming', 'jdk.charsets', 'java.xml', 'javafx.controls', 'javafx.fxml', 'javafx.web', 'java.sql' ]

    // Pack it!
    jpackage {

        // Could be taken from command line, here it is defined statically
        // project.findProperty('installerOs')
        //    (example: -PinstallerOs=mac)
        targetPlatformName = 'mac'
        // The app name
        imageName = 'autumo Documents Viewer'
        jvmArgs = ['-Djdk.gtk.version=2']
        // Resource directory for native package overrides,
        // you can do lots of magic here too...     
        resourceDir = file('package/')

        if (targetPlatformName == 'mac') {
            targetPlatform("mac") {
                // Use downloaded Bellsoft Liberica JDK with JavaFX bundled!
                jdkHome = '/Volumes/Fastdrive/Development/JDKs/Bellsoft/mac/jdk-17.0.5-full.jdk'
                // We also could directly download a JDK
                //jdkHome = jdkDownload("https://download.java.net/java/GA/jdk17.0.1/2a2082e5a09d4267845be086888add4f/12/GPL/openjdk-17.0.1_macos-x64_bin.tar.gz")
            }
            installerType = 'pkg'
        }
        if (targetPlatformName == 'win') {
            targetPlatform("win") {
                jdkHome = 'C:/Applications/JDKs/Bellsoft/jdk-17.0.5-full'
            }
            installerType = 'exe'
        }
        if (targetPlatformName == 'linux') {
            targetPlatform("linux") {
            }
            installerType = 'deb'
        }    

        // Add jpackage-specific options
        installerOptions = [
            '--name', 'autumo-Documents-Viewer', // installer name
            '--description', project.description,
            '--copyright', 'Copyright 2022 autumo GmbH',
            '--vendor', 'autumo GmbH'
        ]

        // We also could take the installer type from comand line
        // installerType = project.findProperty('installerType')
        // We would pass this from the command line
        //    (example: -PinstallerType=msi)

        if (installerType == 'pkg') {
            imageOptions += ['--icon', 'src/main/resources/icon.icns']
            installerOptions += [
                '--license-file', 'package/LICENSE-OS-Installer.txt'
            ]
        }
        if (installerType == 'exe') {
            imageOptions += ['--icon', 'src/main/resources/icon.ico'] 
            installerOptions += [
                // '--win-per-user-install', // Install only for current user
                // '--win-console', // Shows what Java outputs to the console
                '--win-dir-chooser',
                '--win-menu', '--win-shortcut'
            ]
        }
        if (installerType in ['deb', 'rpm']) {
            imageOptions += ['--icon', 'src/main/resources/icon_256x256.png']
            installerOptions += [
                '--linux-menu-group', 'Utility',
                '--linux-shortcut'
            ]
        }
        if (installerType == 'deb') {
            installerOptions += [
                '--linux-deb-maintainer', '[email protected]'
            ]
        }
        if (installerType == 'rpm') {
            installerOptions += [
                '--linux-rpm-license-type', 'GPLv3'
            ]
        }
    }
}

jpackage {
    // Could be used for pre-checks;
    // e.g., are certain command line arguments defined?
    doFirst {
        // project.findProperty('installerOs')
        //    (example: -PinstallerOs=mac)
        // project.getProperty('installerType') // throws exception if its missing
    }
}


Does it work?

Yes it does, and it works on all platforms!

I'm pretty sure there are still things "under the lid" in the build scripts that I don't quite understand yet, but the above scripts work and I can build modular and non-modular JavaFX applications with a JDK of my choice.

The day is saved, you might want to use these scripts as templates! Enjoy 💫

References

Some more references to this article:

Topics:

Related Articles

View All

Author(s)

  • Avatar photo
    Michael Gasche

    Mike has worked as a software engineer, project manager, consultant and CEO and has over 20 years of experience in the Java programming language. He finally decided to develop his ... Learn more

Comments (4)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Roman

Awesome!
Still I don’t quite understand why to use javafx plugin along with Liberica full jdk

Avatar photo
Michael Gasche

When I am not mistaken, the JavaFX plugin is used during development in the Gradle-project, but Liberica’s Java FX is bundled within the jpackage.

Mike

The badass jlink plugin (https://badass-jlink-plugin.beryx.org/releases/latest/) makes this super easy. An entire build.gradle for a JavaFX app is ~25 lines long (not including dependencies) when using this plugin, that includes packaging a slimmed down runtime with jlink/jpackage.

Avatar photo
Michael Gasche

That’s exactly why is used it 👀

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard