— 6 min read

Bundling JavaFX 12 on MacOS for a custom file extension using AppleScript

tldr: Link to Boilerplate on Github

I recently found myself in the position of wanting to bundle a small desktop application for interacting with a custom file type. It should recognise the file extension and be as minimal as possible, ideally acting as a thin layer on top of an existing code base. Said code base, to give some context, consists of a native c++ sdk and wrappers for mobile platforms. In this post I’d like to address the issues I faced using JavaFX to build a desktop app bundle for MacOS, that acts as a viewer for a custom file type. We will be using OpenJDK 12, OpenJFX 12 and Gradle. The goal:

In order to handle opening a file, the application somehow needs to receive a file path upon launch. Usually, this information is provided through command line arguments, however MacOS behaves differently. The operating system sends Apple system events containing the filename to the application and it is left to the developer to be ready for capturing mentioned events once they are fired. This stems from expecting multiple app instances by default, however results in somes issues for Java bundles and represents the main reason for this post.

Boilerplate recognising .pew files.

File handler#

An implementation to catch these system events could look as follows:

import java.awt.*;

/**
 * Attempt listening for APP_OPEN_FILE events as soon as possible.
 */
static {
  if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.APP_OPEN_FILE)) {
    Desktop.getDesktop().setOpenFileHandler(event -> {
      for (File file : event.getFiles()) {
        // Handle file path..
      }

      final String searchTerm = event.getSearchTerm();
      // Handle search term..
    });
  }
}

Through the documentation of setOpenFileHandler we can learn that this will only work, if the Java app is a bundled MacOS application and contains a valid Info.plist file with a CFBundleDocumentTypes array present. The missing piece of information (or hint) is, that this will not work with a bash launcher script, because bash is not capable of receiving the events. This Api has gone through many iterations, from com.apple.mrj, to com.apple.eio, to com.apple.eawt and to com.sun.glass.ui.Application. These have all been deprecated in favour of the latest installment of the api: java.awt.Desktop. However, all versions have one thing in common: The information around them is extremely scarce or only valid for older iterations.

As of Java 9, the JDK has been modularised, which allows for far lighter builds, with the JRE only containing exactly what is required for the application. Fat JARs are no more. Following the OpenJFX 12 documentation, we end up with a packaged and lightweight JRE inside platform-specific images. However, the Java application will be launched using a shell launcher script. MacOS will send the system events to the launcher script specified in the plist file, not the actual java instance. Hence the events will never reach the code snippet above. The only remedy is creating a launcher that is capable of receiving these events and is responsible for spawning the application instances. This brings us to the actual problem space that this posts attempts to address:

Create a lightweight launcher that is capable of forwarding the apple system events for opening files and package it within a MacOS .app bundle. The custom file extension can be declared in the resulting Info.plist file, such that Finder assigns the application to the file type. Double-click on a file should spawn a new application instance.

AppleScript droplets to the rescue#

Certain parts of Java were removed for the sake of modularisation, without providing an alternative, yet. The javapackager tool is one of them. The absence of this tool has left developers coming up with their own tools, using backported javapacker versions from mailing lists, or - in a lot of cases - simply sticking to Java 8. The jpackage tool will fill this space in Java 14, however we have to find an alternative in the meantime.

Since this application never was intended to be spent a lot of time on, I was looking to leverage some sort of boilerplate and not writing my own launcher. In the end, I found a somewhat unconventional solution with AppleScript, which provides templates for Droplet applications in the Mac “Script Editor” application. These Droplets are intended to be used for batch file processing and in this case just spawn one Java application instance per file “dropped”, forwarding the file path as a command-line argument. The apple script can be compiled to a .app bundle with the appropriate structure from within the editor. All left to do, is to populate the Info.plist file with the appropriate information and moving the java application to the bundle.

Launcher in the Script Editor app.

Steps required to create such a applescript launcher:

  • Open “Script Editor”
  • Choose File > New from template... > Droplets > Recursive File Processing Droplet
  • Strip out the recursive parts of the script
  • Fill in the custom file extension values applicable
  • Parse the absolute path to each file that is “dropped” and execute the shell launcher for javafx

When saving the script, choose file format “Application” and option “Stay open after run handler”. This will make sure, that the launcher script keeps receiving drops during the life-cycle of your java application. The application file format will generate the proper bundle structure, with a boilerplate Info.plist file.

I have compiled this approach in a Github repository and the apple script can be found here. The Makefile in the repository conveniently compiles the applescript to the bundle and repaces the Info.plist file with the one from ./blueprint.

Caveats#

Naturally, there are some caveats to this solution, since it is more a workaround than a proper solution. Ideally, the java application should directly be capable of handling these events. Unfortunately, I have not been able to find the proper solution yet. The caveats are as follows:

  • The java application itself will not carry the application name specified in the MacOS top menu bar, but will be named java. This should be solveable by providing the name as an option (-Xdock:name="Example App") to the shell launcher, however that option has always been ignored for me.
  • Since the applescript launcher needs to “stay open”, in order to receive further file opening events after launch, the launcher will remain in the Mac dock.

Conclusion#

The community efforts around the Open JDK really have been amazing, however the fragmentation around the different Java versions render searching for solutions quite troublesome. Older links tend to end up redirecting to the java.net maintenance page. Ideally, I’d wish for a pure gradle solution, that takes care of all the steps necessary to achieve the .app bundle artefact, also taking into account Apple’s rules around the file structure required to publish on the App Store. However, this would require efforts from Apple, which is not very likely with their direction. I’m anticipating the release of the jpackager tool with Java 14 and hope this process gets somewhat facilitated with its release.

The boilerplate can be found on Github.