Netbeans Platform: Mac OSX App Bundle and Embedded JRE with Maven

One problem every Java Desktop developer has to deal with is that Java apps just don't act like native applications. Whether it's the JRE dependency, the swing look and feel, or just bad memories about slow performance in old versions of Java. On Mac OSX there is an additional hurdle where users expect their applications to be a .app bundle inside a DMG image with a shortcut to /Application allowing them to drag/drop the installation. The built in Netbeans Platform installer is nice, but it's a far cry from the native behavior.

With that in mind, I'm going to describe how you can really step up your Netbeans Platform Maven build to include an OSX deployment with a bundled JRE all packaged up as an APP inside a DMG image. What a mouth full.

All of the source code for my implementation in Universal Gcode Sender can be found on GitHub in this commit. The techniques used in this post were heavily inspired (and in many cases copy/pasted from) the Gephi project, which has a very nice packaging and deployment strategy.

Overview

1. Define properties - we'll expose some custom properties in our maven file in order to make it easier to understand and maintain.
2. Create and modify resource files.
3. Resource Filtering - some extra steps need to be taken to apply our properties to configuration files, so we'll have a filtering step utilizing the maven-resources-plugin.
4. Build the artifact - using the maven-antrun-plugin we'll build the Mac OSX bundle.
5. How to use it - What commands need to be run and from where.
6. Next steps - integration with CI, app signing, etc.

Define Properties

We start things off simple by defining a few properties. Here I've prefixed my app's properties with "ugs." to avoid any possible naming collisions and put them in my root POM.xml:
<ugs.app.title>Universal Gcode Platform ${project.version}</ugs.app.title>
  
<!--==== Mac OS X bundle settings ====-->
<ugs.appbundle.name>Universal Gcode Platform</ugs.appbundle.name>
<ugs.bundle.jre.url>http://download.oracle.com/otn-pub/java/jdk/8u131-b11/d54c1d3a095b4ff2b6607d096fa80163</ugs.bundle.jre.url>
<ugs.bundle.jre.version>jre-8u131-macosx-x64</ugs.bundle.jre.version>

<!-- Mac OS X signing identity - must match with a verified Apple developer certificate in the keychain -->
<ugs.codesign.identity>Developer ID Application</ugs.codesign.identity>

<!-- You probably already have this property -->
<brandingToken>ugsplatform</brandingToken>

Create and Modify Resource Files

OSX applications use an information property list file named "Info.plist" to configure the application, we will need to create such a file using the above properties. There are a number of additional configurations which can be made in this file like associating file extensions with your app, but they won't be covered here. You can read more about that in the documentation.

Create this file in your application module at applicaiton/src/main/resource/Info.plist:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<plist version="1.0">
    <dict>

        <key>CFBundleName</key>
        <string>${ugs.appbundle.name}</string>
    
        <key>CFBundleVersion</key>
        <string>${project.version}</string>
    
        <key>CFBundleExecutable</key>
        <string>${brandingToken}</string>
    
        <key>CFBundlePackageType</key>
        <string>APPL</string>
    
        <key>CFBundleShortVersionString</key>
        <string>${project.version}</string>
    
        <key>CFBundleSignature</key>
        <string>????</string>
    
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
    
        <key>CFBundleIdentifier</key>
        <string>${project.groupId}</string>
    
        <key>CFBundleIconFile</key>
        <string>${brandingToken}.icns</string>
    
        <key>NSHighResolutionCapable</key> 
        <true/>
    </dict>
</plist>

For convenience, you should also rename your launcher.conf file to your brandingToken and put it in the same directory as the Info.plist, for me this file is named ugsplatform.conf. The important part in this file is that it contains the commented out jdkhome property, it will be used later for bundling the JRE. Additional options can be configured with the "default_options" parameter:
# \${HOME} will be replaced by user home directory according to platform
default_userdir="\${HOME}/.\${APPNAME}/${project.version}/dev"
default_mac_userdir="\${HOME}/Library/Application Support/\${APPNAME}/${project.version}/dev"

# options used by the launcher by default, can be overridden by explicit
# command line switches
default_options="--branding ${brandingToken} -J-Xms64m -J-Xverify:none -J-Dsun.java2d.noddraw=true -J-Dsun.awt.noerasebackground=true -J-Dnetbeans.indexing.noFileRefresh=true"
# for development purposes you may wish to append: -J-Dnetbeans.logger.console=true -J-ea

# default location of JDK/JRE, can be overridden by using --jdkhome <dir> switch
#jdkhome="/path/to/jdk"

# clusters' paths separated by path.separator (semicolon on Windows, colon on Unices)
#extra_clusters=

Finally, we also need to create a .icns icon file, it should have the same name as your brandingToken and be located in the application module at application/src/main/app-resources/

Resource Filtering

Now that we have our resource files, we need to substitute the properties. This is done using the maven-resources-plugin. There is nothing especially fancy here, we are setting filtering to true and copying the filtered resources into the target directory:

<!-- Copy ressources ${brandingToken}.conf and Info.plist with filtering --><plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <executions>
        <execution>
            <id>generate-app-conf-file</id>
            <phase>generate-resources</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>${basedir}/target/</outputDirectory>
                <resources>
                    <resource>
                        <directory>src/main/resources</directory>
                        <includes>
                            <include>${brandingToken}.conf</include>
                            <include>Info.plist</include>
                        </includes>
                        <filtering>true</filtering>
                    </resource>
                </resources>
                <escapeString>\</escapeString>
            </configuration>
        </execution>
    </executions>
</plugin>

Build the Artifact

This is the big one, we download the JRE, create the package structure, sign the application and package it all up into a DMG image. By utilizing the maven-antrun-plugin we can do this in a procedural way.

Because we don't want to create the DMG image for every build, this is created as a profile which can be enabled independently of other features. I'll briefly describe the sections commented below to explain what is happening, but for further detail you'll need to carefully read the code.

Clean

In case the build fails, we need to delete any files which may have been left behind from a previous build. This will prevent accidentally releasing stale files.

Create folders

A special file structure needs to be created in order for the .app to be used, we start setting up that file structure by creating the folders.

Copy application

One important piece of the .app file structure is that the executable is put in a specific location. First we're copying the executable into the build directory so that we can do further modifications to it in a moment.

Copy logo and configuration files

Two more important files, copy the ${brandingToken}.icns and Info.plist files to their specified location.

Modify application script and copy to final location

We now move the executable into its final location in the .app file, which happens to be Contents/MacOS/. For OSX, the netbeans platform uses a shell script. We need to make a slight modification to this script because the relative location of the resources is different with this new file structure. To that end the replace command to update the working directory back to the bin directory so that everything works as it ought to.

Bundling the JRE

To bundle the JRE we first download it and untar the jre directory into .app/Contents/PlugIns/, some additional manipulation is needed to make sure everything works smoothly. Like resetting a quarantine bit and updating libjli.dylib

We also need to set the jdkhome variable in our .conf file, this is done with another substitution looking for #jdkhome="/path/to/jdk" and replacing it with the relative path to our bundled JRE.

Codesign JRE and App

This piece is something I haven't actually gotten to work yet. Presumably if you have your codesign identity configured properly on the build machine it should work as expected.

Create the DMG

Finally we need our DMG file complete with a symling to the /Application directory. You can take a look at the hdiutil and genisoimage commands which build the image on a OSX and Linux machines respectively.


<profile>
  <id>create-dmg</id>
  <properties>
      <skipCreateDmg>false</skipCreateDmg>
  </properties>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-antrun-plugin</artifactId>
        <executions>
          <!-- Create the Mac OS X application bundle and dmg file -->
          <execution>
              <id>create-app-bundle</id>
              <phase>package</phase>
              <goals>
                  <goal>run</goal>
              </goals>
              <configuration>
                  <skip>${skipCreateDmg}</skip>
                  <target>
                      <!-- Clean -->
                      <delete includeEmptyDirs="true" failonerror="false" removeNotFollowedSymlinks="true">
                          <fileset dir="${project.build.directory}/${ugs.appbundle.name}.app" followsymlinks="false"/>
                          <fileset dir="${project.build.directory}/${ugs.app.title}" followsymlinks="false"/>
                          <fileset dir="${project.build.directory}/${project.artifactId}-${project.version}.dmg" followsymlinks="false"/>
                      </delete>
                      <delete file="${project.build.directory}/${project.artifactId}-${project.version}.dmg" failonerror="false"/>

                      <!-- Create folders -->
                      <mkdir dir="${project.build.directory}/${ugs.appbundle.name}.app"/>
                      <mkdir dir="${project.build.directory}/${ugs.appbundle.name}.app/Contents/MacOS"/>

                      <!-- Copy application -->
                      <copy todir="${project.build.directory}/${ugs.appbundle.name}.app/Contents/Resources/${brandingToken}">
                          <fileset dir="${project.build.directory}/${brandingToken}"/>
                      </copy>

                      <!-- Copy logo and configuration files -->
                      <copy tofile="${project.build.directory}/${ugs.appbundle.name}.app/Contents/Resources/${brandingToken}.icns" file="src/main/app-resources/${brandingToken}.ic
ns" />
                      <copy tofile="${project.build.directory}/${ugs.appbundle.name}.app/Contents/Info.plist" file="${project.build.directory}/Info.plist"/>

                      <!-- Modify application script and copy to final location -->
                      <move file="${project.build.directory}/${ugs.appbundle.name}.app/Contents/Resources/${brandingToken}/bin/${brandingToken}" todir="${project.build.directory}/
${ugs.appbundle.name}.app/Contents/MacOS"/>
                      <replace file="${project.build.directory}/${ugs.appbundle.name}.app/Contents/MacOS/${brandingToken}" token="`dirname &quot;$PRG&quot;`" value="`dirname &quot
;$PRG&quot;`&quot;/../Resources/${brandingToken}/bin&quot;"/>
                      <chmod file="${project.build.directory}/${ugs.appbundle.name}.app/Contents/MacOS/${brandingToken}" perm="ugo+rx"/>

                      <!-- Download and untar JRE -->
                      <exec dir="${project.build.directory}" executable="curl">
                          <arg line="-L"/>
                          <arg line="-C"/>
                          <arg line="-"/>
                          <arg line="-b"/>
                          <arg line="&quot;oraclelicense=accept-securebackup-cookie&quot;"/>
                          <arg line="-O"/>
                          <arg line="${ugs.bundle.jre.url}/${ugs.bundle.jre.version}.tar.gz"/>
                      </exec>

                      <!-- Unzip archive -->
                      <mkdir dir="${project.build.directory}/${ugs.appbundle.name}.app/Contents/PlugIns"/>
                      <exec dir="${project.build.directory}" executable="tar">
                          <arg line="-zxf"/>
                          <arg line="${ugs.bundle.jre.version}.tar.gz"/>
                          <arg line="-C &quot;${ugs.appbundle.name}.app/Contents/PlugIns&quot;"/>
                      </exec>

                      <!-- Remove quarantine bit set recursively on JRE -->
                      <exec dir="${project.build.directory}" os="Mac OS X" executable="xattr">
                          <arg line="-rd"/>
                          <arg line="com.apple.quarantine"/>
                          <arg line="&quot;${ugs.appbundle.name}.app/Contents/PlugIns&quot;"/>
                      </exec>

                      <!-- Get the JRE folder name -->
                      <path id="jre_name">
                          <dirset dir="${project.build.directory}/${ugs.appbundle.name}.app/Contents/PlugIns" includes="jre*" />
                      </path>
                      <property name="bundle.jre.path" refid="jre_name" />
                      <basename property="bundle.jre.name" file="${bundle.jre.path}"/>

                      <!-- Configure relative JRE path into ugsplatform.conf -->
                      <replace file="${project.build.directory}/${ugs.appbundle.name}.app/Contents/Resources/${brandingToken}/etc/${brandingToken}.conf" token="#jdkhome=&quot;/pat
h/to/jdk&quot;" value="jdkhome=&quot;../../PlugIns/${bundle.jre.name}/Contents/Home&quot;"/>
                      <chmod file="${project.build.directory}/${ugs.appbundle.name}.app/Contents/PlugIns/**" perm="+x" type="both"/>

                      <!-- Fix JRE by replacing libjli.dylib symlink with real file -->
                      <delete file="${project.build.directory}/${ugs.appbundle.name}.app/Contents/PlugIns/${bundle.jre.name}/Contents/MacOS/libjli.dylib"/>
                      <copy file="${project.build.directory}/${ugs.appbundle.name}.app/Contents/PlugIns/${bundle.jre.name}/Contents/Home/lib/jli/libjli.dylib" todir="${project.bui
ld.directory}/${ugs.appbundle.name}.app/Contents/PlugIns/${bundle.jre.name}/Contents/MacOS"/>

                      <!-- Codesign JRE -->
                      <exec dir="${project.build.directory}" os="Mac OS X" executable="codesign">
                          <arg value="-fs"/>
                          <arg value="${ugs.codesign.identity}"/>
                          <arg value="-v"/>
                          <arg value="${ugs.appbundle.name}.app/Contents/PlugIns/${bundle.jre.name}/"/>
                      </exec>

                      <!-- Codesign app -->
                      <exec dir="${project.build.directory}" os="Mac OS X" executable="codesign">
                          <arg value="-fs"/>
                          <arg value="${ugs.codesign.identity}"/>
                          <arg value="-v"/>
                          <arg value="${ugs.appbundle.name}.app"/>
                      </exec>

                      <!-- Create application folder and add Applications dynamic link -->
                      <mkdir dir="${project.build.directory}/${ugs.app.title}"/>
                      <move file="${project.build.directory}/${ugs.appbundle.name}.app" todir="${project.build.directory}/${ugs.app.title}/" />
                      <symlink link="${project.build.directory}/${ugs.app.title}/Applications" resource="/Applications" failonerror="false" />

                      <!-- Create DMG (Mac OS X) -->
                      <exec dir="${project.build.directory}" os="Mac OS X" executable="hdiutil">
                          <arg value="create"/>
                          <arg value="-noanyowners"/>
                          <arg value="-imagekey"/>
                          <arg value="zlib-level=9"/>
                          <arg value="-srcfolder"/>
                          <arg value="${project.build.directory}/${ugs.app.title}"/>
                          <arg value="${project.artifactId}-${project.version}.dmg"/>
                      </exec>

                      <!-- Create DMG (Linux), only for testing -->
                      <exec dir="${project.build.directory}" os="Linux" executable="genisoimage">
                          <arg value="-V"/>
                          <arg value="${ugs.appbundle.name}"/>
                          <arg value="-U"/>
                          <arg value="-D"/>
                          <arg value="-l"/>
                          <arg value="-allow-multidot"/>
                          <arg value="-max-iso9660-filenames"/>
                          <arg value="-relaxed-filenames"/>
                          <arg value="-no-iso-translate"/>
                          <arg value="-r"/>
                          <arg value="-o"/>
                          <arg value="${project.artifactId}-${project.version}.dmg"/>
                          <arg value="-root"/>
                          <arg value="${project.build.directory}/${ugs.app.title}"/>
                          <arg value="${project.build.directory}/${ugs.app.title}"/>
                      </exec>
                  </target>
              </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</profile>

How to use it

After all that, using it is a simple matter of running "mvn package -P create-dmg". The resulting file will look something like ugs-platform-app-2.0-SNAPSHOT.dmg and be located in your application/target directory.

Next Steps 

The next steps for me are to create a windows installer with a bundled JRE, and setup TravisCI (which has OSX machines available) to build my artifacts.

Netbeans Platform: Good, Bad and Ugly


The Good


The Netbeans Platform is incredibly simple to get started with. There are extensive tutorial based guides to get you started, like this tutorial about creating a CRUD application with zero code, or making a paint app, and videos too, not to mention Geertjan's Blog which goes into many advanced examples. If you're using the Netbeans IDE you also get a huge assortment of wizards to help you along with various annotation-based integrations. The wizards have excellent ANT support and pretty good Maven support as well. There are many advanced features in the box from window management, plugins, configurable key-bindings and an auto-update framework.

The Bad

When you want to do something advanced which isn't covered by a tutorial, you quickly need to roll up your sleeves and dig into the source code. For example, if you choose to migrate from the default ANT build to Maven, this guide is lacking if you aren't very familiar with Maven. Or even if you used the built in Maven wizard, there is no documentation that you need to run with the deployment profile to build the installers (if you're stuck on this one, it's "mvn package -P deployment"). Another big pain point for me was trying to create actions at runtime with localization which also showed up in menu's, this took lots of research before eventually creating a custom service, and they still didn't work for all cases. Even integrating my build with Jenkins was a headache.

The Ugly

When you're finished with your application, you still have a Java application. No matter how convenient and productive the language is there is still a strong bias against Java on the Desktop, and that bias doesn't seem to be going away (maybe JavaFX with native bundling will help sway people). Until it's easier to bundle your Netbeans Platform application with a native look and feel that bias isn't going anywhere.

UGS 2.0 to Netbeans Platform first impressions

For the past year Universal G-code Sender has suffered pretty severe feature creep. Lots of people had lots of good ideas, but it made the front end code confusing and hard to extend. In large part this is just because it's hard to compartmentalize a complex GUI. GRBL has had a lot of interesting features added in the past year which are begging to be leveraged, but UGS wasn't in a position to use them. With all this in mind I started trying to come up with a solution.

The first step was to decouple the "model" object from the GUI. Inspiration for this started nearly a year ago when Bob Jones implemented a web interface for UGS. Bob made a thin API layer which exposed a few simple functions and allowed him to build a completely separate web-based user interface. This pattern is known as Model-View-Presenter, and allows for multiple front ends which use the same back end code. It enhances test-ability of the business logic and helps facilitate a flexible front end. It didn't take more than a few hours to flesh out the rest of the functions this API needed so that the thin API exposed all of the back end features needed for the classic UGS GUI. After that it was a matter of re-implementing the Swing GUI to utilize the new API.

With the foundation laid it was time to consider what to build on top of it. Rugbymauri on github clued me into an idea that more experienced GUI developers have probably known for a long time: plugin based applications. Then he took it a step further and told me about the Eclipse Rich Client Platform (Eclipse RCP) and the Netbeans Platform. After some initial investigation I decided to pursue Netbeans Platform.

It took about a week for me to completely settle on the idea of a plugin based architecture. In the end, it was a perfect fit for UGS. The back end application already had features for sending status updates to any number of front end listeners. This has been used by the Visualizer from day one, it is a completely separate GUI component and it listens to the same back end events as the main GUI. Something like the Netbeans Platform would take this a step further and facilitate the creation of these modules - whereas in the classic UGS interface all these windows are managed in a monolithic "MainWindow" class.

Moving forward the concept of modules allows me to think about other features in a way that wont cause the code to feel bloated. For instance - how would a Gcode editor that can highlight some number of lines and have it displayed in the 3D visualizer? In the classic interface this would be a huge undertaking that would massively complicate the "MainWindow". On the other hand with a modular design there just needs to be a new "Editor" module, the Netbeans Platform handles sharing of the gcode file and provides APIs for binding selection events between the Editor and Visualizer. What would have been a week of effort could now be done in an afternoon or two. By the way, the Netbeans Platform also comes with all the modules created for the Netbeans IDE, so it already has GUI components for a code editor with breakpoints (Breakpoints? Maybe another afternoon).

There are a few areas that I'm not sold on yet, for instance UGS already has several observer API's and I'm not sure if I'll convert them to use corresponding Netbeans Platform features. Also the library wrapper modules are a little kludgy, especially if your project is built using a separate library jar like UGS is. Every time I rebuild my library jar I need to update the library wrapper module. The workaround I settled on was to modify my library's build script to dump the jar inside the library wrapper module, a side effect to which is that if I do a clean on that project it breaks the library wrapper.

The UGS Platform, UGS's 2.0 release, is currently in a proof of concept state. All the major integration has been done, now its a matter of creating all the different modules that exist in the Classic GUI. After that other features features of the Netbeans Platform can be leveraged for things like integrated Preferences and Configuration menu's, and default Window layout schemes.