Tuesday, April 21, 2020

JavaFX desktop application on macOS - Part III

Copyright © 2020, Steven E. Houchin. All rights reserved.

This is the third and final post in this series, which deals with installing a JavaFX desktop application on macOS, when it was developed on a Windows system.  In my first post on this subject, I left two issues unresolved, the first of which I discussed in the second post.

Here, I discuss the second lingering issue left over from that first post, namely that the application's title in the macOS system menu bar shows as simply "java," instead of my application's title, "MapApp." This happens when I merely double click on the app's JAR file to run it.

To solve the problem, I had to create an installation package on the Mac, then run that package so the app is properly placed in the Mac's Applications folder, thus appearing like any other Mac application.


Build Steps on Windows


The key to making all that happen starts back on my Windows machine, and my build steps in NetBeans.  Since my app is made up of an application JAR, a library JAR, and some support files (like image files), I needed to integrate them together in a single JAR for the Mac.

  1.  I included the support files in my NetBeans Java package, along with the java source files for the application. That bundles those support files as Java package resources, instead of separate files on the disk.
  2.  I modified the application package's 'build.xml' file to add a new Run Target named "package-for-store."  It is intended to run after all the sources are compiled into their JARs. I show that new target script below.
  3.  Once the sources were built into JARs, I used NetBeans to execute the new Run Target. This is done by …
    1. Open a Files window on NetBeans
    2. Right-click the application's 'build.xml' file
    3. Select Run Target
    4. Select Other Targets
    5. Select the target "package-for-store," which runs the packaging script
  4. Once the packaging script runs, the resulting application JAR file is found in the folder 'store', which is a peer to the project's 'dist' folder. This is the JAR file that contains everything, including libraries, that can then be copied over to the Mac. The script must be run manually anytime the application is changed.
So, what is in this packaging script that I added to the 'build.xml' that performs this magic?  It is a custom "target" I added, and it looks like this:

<target name="package-for-store" depends="jar">
 <property name="store.jar.name" value="MapApp"/>
 <property name="store.dir" value="store"/>
 <property name="store.jar" value="${store.dir}/${store.jar.name}.jar"/>
 <echo message="Packaging ${application.title}
 into a single JAR at ${store.jar}"/>
 <delete dir="${store.dir}"/>
 <mkdir dir="${store.dir}"/>
 <jar destfile="${store.dir}/temp_final.jar" filesetmanifest="skip">
  <zipgroupfileset dir="dist" includes="*.jar"/>
  <zipgroupfileset dir="dist/lib" includes="*.jar"/>
  <manifest>
   <attribute name="Main-Class" value="mapapp.MapApp"/>
  </manifest>
 </jar>

 <zip destfile="${store.jar}">
  <zipfileset src="${store.dir}/temp_final.jar"
  excludes="META-INF/*.SF, META-INF/*.DSA, META-INF/*.RSA"/>
 </zip>
 <delete file="${store.dir}/temp_final.jar"/>
</target>


Here are key items to note in the target script above.  The properties at the top specify the application name, the 'store' folder, and the jar file to create in the 'store' folder.  The "message" displays in NetBeans when the script is run. The 'store' folder is deleted and remade anew. A temporary JAR is created, which includes all JARs in the 'dist' folder and in the 'dist/lib' folder. The application's main startup class is specified by package name (mapapp) and class name (MapApp) in the "Main-Class" attribute. The finalized MapApp.jar is created from the temp JAR, and the temp is deleted.

When the script is done, "store/MapApp.jar" should exist, ready to be copied to the Mac.

Packaging Steps on Mac


So, I must copy the final JAR from above over to the Mac so it can be made into a macOS .pkg file.

On the Mac, I created a working folder 'MapApp/packaging' under my Documents. In it, I placed the JAR, and a .css file I need in the package. Under the 'packaging' folder, I also created a 'package/macosx' folder. As part of the process, the macOS java packaging utility looks for certain files there (all optional), which it uses to customize the package. The files are:

  • Info.plist - this is a standard macOS bundle configuration file for setting application properties, such as version number and copyright string.
  • MapApp-background.png - this is an image that displays at the lower left of the installation window when the package executed.
  • MapApp.icns - the application's custom icon file.
  • MapApp-post-image.sh - a script to run after the application image is populated.
Here is a shell script file I created to automate all of this to conveniently create the package (.pkg) file, which I run from a Bash terminal window:



#/bin/bash
#
# Create a .pkg file for the java MapApp app
#
jdk="/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home"

# Run the packager utlity
$jdk/bin/javapackager -version
$jdk/bin/javapackager -deploy -native pkg -name MapApp
-Bappversion=1.0.0 -srcdir . -srcfiles MapApp.jar:mapapp.css
-appclass mapapp.MapApp -outdir out -outfile MapApp
-description "MapApp description"
-title "MapApp Title"
-vendor "Company Name" -v

# Copy the new package
rm -f ./mapapp-installer.pkg
cp out/bundles/MapApp-*.pkg ./mapapp-installer.pkg

# List the results
ls -l


The key here is the "javapackager" utility, which is part of the installed JDK on my system.  The input sources to this are the MapApp.jar and mapapp.css files, as seen in the -srcfiles option.

The result is the file 'mapapp-installer.pkg' in my 'packaging' folder.  When I execute this, it installs my MapApp application in the usual macOS Applications folder, like any other application.

So, after all this, I've accomplished two things: a standard application installation and, when it executes, the title "MapApp" shows properly in the system menu bar.