23 May 2020
Tags: java javafx jetbrains gradle




Let’s setup a javafx app (jdk 14) which doesn’t crash intellijs scene builder and deploys the program to an installer.

Project link

https://gitlab.com/lyze237-java/javafxsampleapp

Why

Setting up a javafx app with jdk 14 turns out to be difficult, especially if you want to use jfx14 components in intellijs scene builder.

Intellij comes bundled with a java 8 runtime and therefore the builder crashes when you include a jdk14 ontrol.

Additionally, building an .msi/.deb installer is quite difficult as well and I wanted to create a nice blog post about it.

First things first…

Fixing intellij

Installing a jdk with openfx libraries

First, we need to make sure intellij runs with a java 14 jre/jdk which contains javafx libraries.

I couldn’t figure out how to include javafx libraries in the official oracle jdk so I searched for one which already contained all the required java fx libraries.

Therefore I chose Liberica jdk. Make sure to download the “full” version and not the “regular” or “lite” version (scroll down).

Liberica jdk

Setting up intellij with the newly installied jdk

Start intellij and search for the choose runtime plugin (File -> Settings -> Plugins), install it and restart intellij.

Choose runtime plugin

Once it restarts, press shift twice and search for “choose runtime”.

Choose runtime shift shift

Click on that, then browse to the installed jdk and select it.

Choose runtime shift shift

Intellij restarts with the selected jdk once you click on ok.

Choose runtime shift shift

Now the scene builder worked most of the time except when I used a custom control, then it crashed with an assertion error.

Fixing scene builder crashes because of assertion errors

Luckily, we can tell intellij to ignore assertion errors, for that we just need to know what the exceptions class name is. Intellij has luckily a log file we can look at. Simply navigate to help -> show log in explorer.

Log file

Then open the log file in an editor and look for an assertion error related to the scene builder. For me it was this one:

2020-05-20 21:12:10,735 [1100341]   INFO - ceneBuilder.SceneBuilderEditor -  
java.lang.AssertionError
	at com.oracle.javafx.scenebuilder.kit.metadata.klass.ComponentClassMetadata.getSubComponentProperty(ComponentClassMetadata.java:129)
	at com.oracle.javafx.scenebuilder.kit.metadata.klass.ComponentClassMetadata.getSubComponentProperty(ComponentClassMetadata.java:75)
	at com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask.getSubComponentPropertyName(DesignHierarchyMask.java:623)
	at com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask.isAcceptingSubComponent(DesignHierarchyMask.java:540)
	at com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.AbstractHierarchyPanelController.updateTreeItem(AbstractHierarchyPanelController.java:942)
	at com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.AbstractHierarchyPanelController.makeTreeItemBorderPane(AbstractHierarchyPanelController.java:699)
	at com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.AbstractHierarchyPanelController.updateTreeItem(AbstractHierarchyPanelController.java:923)
    [...]

Now we need to ignore that error, open intellijs vm options by click on help -> edit custom vm options.

Intellij help custom vm options menu

Then add the following line after the -ea line:

-disableassertions:FULL.CLASS.NAME.FROM.THE.EXCEPTION

For example:

-ea
-disableassertions:com.oracle.javafx.scenebuilder.kit.metadata.klass.ComponentClassMetadata

Restart intellij and the scene builder should work again!

Setting up the gradle project

Example app: https://gitlab.com/lyze237-java/javafxsampleapp

Since the latest gradle version uses quite a bit of ram because of the daemon, let’s limit it to 512mb:

/gradle.properties:

org.gradle.daemon=false
org.gradle.jvmargs=-Xms128m -Xmx512m
org.gradle.configureondemand=false

/build.gradle:

plugins {
    id 'java'
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.8' // java fx setup
    id 'org.beryx.jlink' version '2.19.0' // installer generation
}

def getGitHash = { -> // function to generate version number based on git commit count
    def stdout = new ByteArrayOutputStream()
    exec {
        commandLine 'git', 'rev-list', '--count', 'HEAD'
        standardOutput = stdout
    }
    return stdout.toString().trim()
}

group 'sh.owl'
version = '0.1.'+ getGitHash() // set version based on git commit count
mainClassName = 'sh.owl.javaFxSampleApp.App' // set that to the main class name
applicationName = 'JavaFxSampleApp'

repositories {
    mavenCentral()
}

javafx {
    version = "14" // use java fx version 14
    modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing' ] // the modules javafx should provide
}

jlink { // installer configuration
    addExtraDependencies("javafx") // to include javafx in the build
    jpackage {
        if(org.gradle.internal.os.OperatingSystem.current().windows) { // when we build a windows installer
            installerType = 'msi' // setup msi
            imageOptions += ['--win-console'] // enable console 
            installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut'] // install in %appdata%, allow dir choosing, add a menu entry, add a desktop shortcut
            imageOptions += ['--icon', 'src/main/resources/icons/Icon.ico'] // set the icon of the app
        }
        else { // when we build a linux installer
            installerType = 'deb' // create a deb file
            imageOptions += ['--icon', 'src/main/resources/icons/Icon-128.png'] // set the icon
            installerOptions += ['--linux-menu-group', '--linux-shortcut'] // add a menu entry and desktop shortcut
        }
    }
}

tasks.prepareMergedJarsDir.doLast {
    if(org.gradle.internal.os.OperatingSystem.current().windows) { // when we're on windows the build generates linux javafx libraries and therefore crashes when we want to deploy a msi. remove those jars before we do.
        delete fileTree("build/jlinkbase/jlinkjars") {
            include "**/*linux.jar"
        }
    }
}

sourceSets {
    main {
        resources {
            srcDirs = ["src/main/java", "src/main/resources"]
            includes = ["**/*.fxml", "**/*.properties"]
        }
    }
}

dependencies {
    // logging
    implementation 'org.slf4j:slf4j-api:2.0.0-alpha1'
    implementation 'org.slf4j:slf4j-simple:2.0.0-alpha1'

    // java fx
    implementation 'org.controlsfx:controlsfx:11.0.1'

    // lombock
    compileOnly 'org.projectlombok:lombok:1.18.12'
    annotationProcessor 'org.projectlombok:lombok:1.18.12'

    testCompileOnly 'org.projectlombok:lombok:1.18.12'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.12'

    // testing
    testImplementation group: 'junit', name: 'junit', version: '4.12'
}

Running the project

You need to run the jpackage task on a windows machine when you want to create a windows installer. Same for .deb and a debian machine.

Additionally you need to install https://wixtoolset.org/ on windows and fakeroot on debian.

CI

Since gitlab allows windows cloud runners I’ve created a gitlab-ci.yml file which creates a windows .msi installer and a .deb installer for linux.

desktop-linux-deb:
  image: openjdk:14-jdk-slim
  tags:
    - docker
  only:
    - master
  script:
    - chmod +x ./gradlew
    - export GRADLE_USER_HOME=$(pwd)/.gradle
    - apt update && apt install -y openjfx git fakeroot # install java fx, git and fakeroot
    - ./gradlew jpackage # package it
  artifacts:
    paths:
      - build/jpackage/*.deb
  cache:
    key: ${CI_PROJECT_ID}-lin
    paths:
      - .gradle/wrapper
      - .gradle/caches

desktop-windows:
  tags:
    - shared-windows
  only:
    - master
  script:
    - $global:progressPreference = 'silentlyContinue'
    - Invoke-WebRequest -Uri https://download.bell-sw.com/java/14.0.1+8/bellsoft-jdk14.0.1+8-windows-amd64-full.zip -OutFile jdk.zip # download the jdk
    - Expand-Archive jdk.zip # extract it
    - Remove-Item jdk.zip
    - mv bellsoft* jdk
    - mv jdk/jdk* jdk\jdk
    - $Env:JAVA_HOME = $PWD.path + "\jdk\jdk" # set the JAVA_HOME variable to that sdk

    - choco install -y -i wixtoolset --version=3.10.3.300702 # install wixtoolset

    - ./gradlew.bat jpackage # package it to an msi
  artifacts:
    paths:
      - build/jpackage/*.msi

You can download the test .msi or .deb here: https://gitlab.com/lyze237-java/javafxsampleapp/pipelines by clicking on the arrow on the right side.

Have fun!