Installmation – An Installer Generator for Java 11+ Apps

GitHub Repo

In a burst of enthusiasm, I spent the last few weeks creating a GUI frontend to the jpackage utility, slated for the upcoming JDK 14 release. I called the tool Installmation, a portmanteau of ‘installation’ and ‘automation’. It’s written in Kotlin and JavaFX, of course, and focuses on creating JavaFX application installers, but can create non-JFX applications.

Generally, you would script the creation of installer creation into your build process, but there are occasions when a simple UI tool is faster. No more remembering the arcane and fiddly parameters, cutting and pasting stuff between projects, and so on. Plus, it can generate batch files for you as well.

I suppose the ultimate test for an installer tool to create its own installer, sort of like how bacteria reproduce – binary fission. So, Installmation passed the binary fission test a few days ago – the installer is available here.

The tool has four capabilities:

  1. Create application images
  2. Create complete installers
  3. Generate standalone shell scripts to create images and installers
  4. List the module dependencies of an application

In this post, I will focus on creating complete installers. I will use it to create an OS X installer for one of my own repos. Generating scripts and module dependencies will be covered in another post, to keep this post mercifully brief.

Setup

Sadly, there is some stuff you will need to download and install first, but hey…cross-platform development has a long and rich history of being a pain in the ass. The suffering continues.

OS X Catalina Headaches

If you are running on OS X Catalina, most of the JDK 14 command line tools will not run due to Apple’s new GateKeeper security ‘feature’. I disabled mine with:

sudo spctl --master-disable

Which makes it behave like older OS X versions and allows all executables to run. This disables Gatekeeper, so use at your own risk. Here is some detail on it.

You will then have to double click on each of the following in Mac’s Finder to remove the warning from each executable (F%^&#$&!!):

  • jpackage
  • jdeps
  • jlink

This issue will go away when JDK 14 is officially released (March 2020) and the installer is properly signed by a verified Apple Developer Certificate. 

Prepare the Target Application

The installer will require a few details about the application to be installed:

  • A main class
  • An application jar file
  • Classpath items

The installer will do the rest, in terms of building a custom runtime by analysing your applications module dependencies.

For the purposes of this post, I have added gradle tasks to the javafx-app-1 repo to makes things easier.

From inside javafx-app-1 repo directory, run the following gradle tasks from the command line:

    gradlew jar
    gradlew libs

This will create the application jar in the build/libs/ directory, and a build/deplibs/ directory with all the dependencies. This is just for the purposes of this post.

Start the Application

You can either start the app after cloning the repo with gradlew runApp or download the binary from  here.

It looks like this.

Create A New Project – app1

Create a new project and save it. All files are saved in the <user.home>/.installmation directory. Fill in the project name,  and set the installer type to pkg. The other fields are optional and will default to sensible values.

Setup Install Directories

These 3 directories can be anywhere, but are populated during installer generation, and are recreated for every generation attempt.

Input Directory – Installmation copies all your project files to a single location, in a format easy for jpackage to process.

Image Build Directory – Location where jpackage will generate the packaged application. On OSX, this is an .app file, on Windows, multiple files and directories. You should be able to run this application out of the directory. This is a good smoke test to ensure everything works.

Installer Directory – Location where installer will be generated – .msi, .exe, .dmg, or .pkg files.

Setup Java Binaries

This dialog is more complicated than you would expect. Until JDK 14 is released and used everywhere, you will need to manage jpackage JDKs separately from your deployment JDK. Similarly,  JavaFX 11+ is not a single install, but split into the libraries and mods downloads.

Jpackage – JDK which contains jpackage. This will be version 14 or better – the EA release (as of December 2019) works fine.

JDK to install – This is the JDK which will be customised into a runtime and added to your build image. This can be be any version (11+ for JavaFX, however).

LibrariesOptional. JavaFX libraries download. This is required only if you have Java 9 modules you need to add.

JmodsOptional. Only used for JavaFX applications.

Setup Application Jar File and Main Class

This panel is straightforward – just set the full path of the main jar file and the main class name.

Setup Classpath

Add all you class path elements. The extra modules is optional and a bit experimental right now.

Generate the Installer

Save the project and hit the installer button and if everything went well, you should see the successful creation of an installer. It is created in the installer directory specified above.

Final Comments

The tool is pretty basic at this stage.  It needs a lot more testing, and has a strong JavaFX bias. A lot of installer tools don’t support JavaFX directly so I made a concerted effort to do so. I made efforts to make the interface as intuitive as possible, but I am sure it could be improved, so any feedback would be great – just create issues in GitHub. 

Some of things I will work on in the short term include:

  • Splash screens and icons
  • Custom configuration file deployment

TableView Cell Factories

GitHub repo (app4)

This post is about TableView factories, and the difference between Cell Factories and Cell Value Factories. There are a few good blog posts about this topic – I am just going to discuss the key concepts and the Kotlin version using Java 11.

CellValueFactories or CellFactories?

A cellValueFactory tells the column which values from the TableView contained type to display in the cells for each row.

A cellFactory tells the column how to display the data as a string.

Simplest Scenario  – Column Represents Object’s String Property

  • Cell Value Factory Required – No
  • Cell Factory Required – No

A lot of the time, you just want to display an object’s string property. This can be done with the PropertyValueFactory:

dateObjectColumn.cellValueFactory = PropertyValueFactory<DateItem, OffsetDateTime>("date")

Simple Object Scenario – Column Represents Object Property And Default toString() is Good Enough 

  • Cell Value Factory Required – No
  • Cell Factory Required – No

Here, the column represents an object, but you are happy that its default string representation is what you want. So, no need for any factories – PropertyValueFactory is good enough.

dateObjectColumn.cellValueFactory = PropertyValueFactory<DateItem, OffsetDateTime>("date")

Realistic Object Scenario – Column Represents a Complex Object

  • Cell Value Factory Required – Maybe
  • Cell Factory Required – Yes

The column maps to an object, but you need to specify which property to use, and you want to specify how the data appears on the screen. This may or may not require a cell value factory.

Dates are a common example – often you want to format dates in a specific way.

The code would look like this in the simpler scenario, without a cell value factory:

dateCustomColumn.cellValueFactory = PropertyValueFactory<DateItem, OffsetDateTime>("date") 
dateCustomColumn.cellFactory = ModifiedISODateCellFactory() 

And with a cell value factory: 

dateCustomColumn.cellValueFactory = DateCellValueFactory() 
dateCustomColumn.cellFactory = ModifiedISODateCellFactory()

Realistic Object Scenario – Lambda Version

The previous version used classes for the factory declarations, but lambdas work as well.

dateLambdaColumn.setCellValueFactory { cell: TableColumn.CellDataFeatures<DateItem, OffsetDateTime> -> ReadOnlyObjectWrapper(cell.value.date) } 

dateLambdaColumn.setCellFactory { 
   object : TableCell<DateItem, OffsetDateTime>() { 
      public override fun updateItem(dt: OffsetDateTime?, empty: Boolean) { 
         super.updateItem(dt, empty) 
         if (!empty) 
            this.text = "Lambda - $dt" 
      } 
   }
}

Last Comments

Table columns are pretty straightforward, once you understand how the factories work. There is a complete demo application demonstrating the scenarios discussed about here – see this on how to setup and run app4.