Sunday, October 26, 2014

Keeping up with Mac Java - Bundling into Executable Apps

Summary: Packaging a Java application into an executable Mac bundle is not difficult, but has changed over time; JavaApplicationStub is replaced by JavaAppLauncher; manually building the package content files and hand editing the Info.plist is straightforward, but the organization and properties have changed. Still irritating that JWS/JNLP does not work properly in Safari.

Long Version.

I have long been a fan of Macs and of Java, and I have a pathological aversion to writing single-platform code, if for no other reason than my favorite platforms tend to vanish without much notice. Since I am a command-line weenie, use XCode only for text editing and never bother much with "integrated development environments" (since they tend to vanish too), I am also a fan of "make", and tend to use it in preference to "ant" for big projects. I am sure "ant" is really cool but editing all those build.xml files just doesn't appeal to me. This probably drives the users of my source code crazy, but c'est la vie.

The relevance of the foregoing is that my Neanderthal approach makes keeping up with Apple's and Oracle's changes to the way in which Java is developed and deployed on the Mac a bit of a challenge. I do need to keep up, because my primary development platform is my Mac laptop, since it has the best of all three "worlds" running on it, the Mac stuff, the Unix stuff and the Windows stuff (under Parallels), and I want my tools to be as useful to as many folks as possible, irrespective of their platform of choice (or that which is inflicted upon them).

Most of the tools in my PixelMed DICOM toolkit, for example, are intended to be run from the command line, but occasionally I try to make something vaguely useful with a user interface (not my forte), like the DoseUtility or DicomCleaner. I deploy these as Java Web Start, which fortunately continues to work fine for Windows, as well for Firefox users on any platform, but since an unfortunate "security fix" from Apple, is not so great in Safari anymore (it downloads the JNLP file, which you have to go find and open manually, rather than automatically starting; blech!). I haven't been able to find a way to restore JNLP files to the "CoreTypes safe list", since the "XProtect.plist XProtect.meta.plist" and "XProtect.plist" files in "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/" don't seem to be responsible for this undesirable change in behavior, and I haven't found an editable file that is yet.

Since not everyone likes JWS, and in some deployment environments it is disabled, I have for a while now also been creating selected downloadable executable bundles, both for Windows and the Mac.

Once upon a time, the way to do this to build Mac applications was with a tool that Apple supplied called "jarbundler". This did the work of populating the tree of files that constitute a Mac application "bundle"; every Mac application is really a folder called "something.app", and it contains various property files and resources, etc., including a binary executable file. In the pre-Oracle days, when Apple supplied its own flavor of Java, the necessary binary file was "JavaApplicationStub", and jarbundler would stuff that into the necessary place when it ran. There is obsolete documentation of this still available from Apple.

Having used jarbundler once, to see what folder structure it made, I stopped using it and just manually cut and past stuff into the right places for each new application, and mirrored what jarbundler did to the Info.plist file when JVM options needed to be added (such as to control the heap size), and populated the resources with the appropriate jar files, updated the classpaths in Info.plist, etc. Automating updates to such predefined structures in the Makefiles was trivial. Since I was using very little if anything that was Apple-JRE specific in my work, when Apple stopped doing the JRE and Oracle took over, it had very little impact on my process. So now I am in the habit of using various bleeding edge OpenJDK versions depending on the phase of the moon, and everything still seems to work just fine (putting aside changes in the appearance and performance of graphics, a story for another day).

Even though I have been compiling to target the 1.5 JVM for a long time, just in case anybody was still on such an old unsupported JRE, I finally decided to bite the bullet and switch to 1.7. This seemed sensible when I noticed that Java 9 (with which I was experimenting) would no longer compile to such an old target. After monkeying around with the relevant javac options (-target, -source, and -bootclasspath) to silence various (important) warnings, everything seemed good to go.

Until I copied one of these 1.7 targeted jar files into a Mac application bundle, and thought hey, why not rev up the JVMVersion property from "1.5+" to "1.7+"? Then it didn't work anymore and gave me a warning about "unsupported versions".

Up to this point, for years I had been smugly ignoring all sorts of anguished messages on the Mac Java mailing list about some new tool called "appbundler" described by Oracle, and the Apple policy that executable apps could no longer depend on the installed JRE, but instead had to be bundled with their own complete copy of the appropriate JRE (see this link). I was content being a fat dumb and happy ostrich, since things were working fine for me, at least as soon as I disabled all that Gatekeeper nonsense by allowing apps from "anywhere" to run (i.e., not just from the App Store, and without signatures), which I do routinely.

So, when my exposed ostrich butt got bitten by my 1.7 target changes (or whatever other incidental change was responsible), I finally realized that I had to either deal with this properly, or give up on using and sharing Mac executables. Since I have no idea how many, if any, users of my tools are dependent on these executables (I suspect not many), giving up wouldn't have been so bad except that (a) I don't like to give up so easily, and (b) occasionally the bundled applications are useful to me, since they support such things as putting it in the Dock, dragging and dropping to an icon, etc.

How hard can this be I thought? Just run appbundler, right? Well, it turns out the appbundler depends on using ant, which I don't normally use, and its configuration out of the box doesn't seem to handle the JVM options I wanted to specify. One can download it from java.net, and here is its documentation. I noticed it seemed to be a little old (two years) and doesn't seem to be actively maintained by Oracle, which is a bit worrying. It turns out there is a fork of it that is maintained by others (infinitekind) that has more configuration options, but this all seemed to be getting a little more complicated than I wanted to have to deal with. I found a post from Michael Hall on the Mac Java developers mailing list that mentioned a tool he had written, AppConverter, which would supposedly convert the old to the new. Sounded just like what I needed. Unfortunately, it did nothing when I tried it (did not respond to a drag and drop of an app bundle as promised).

I was a bit bummed at this point, since it looked like I was going to have to trawl through the source of one of the appbundler variants or AppConverter, but then I decided I would first try and just cheat, and see if I could find an example of an already bundled Java app, and copy it.

AppConverter turned out to be useful after all, if only to provide a template for me to copy, since when I opened it up to show the Package Contents, sure enough, it was a Java application, contained a copy of the java binary executable JavaAppLauncher, which is what is used now instead of JavaApplicationStub, and had an Info.plist that showed what was necessary. In addition, it was apparent that the folder where the jar files go has moved, from being in "Contents/Resources/Java" to "Contents/Java" (and various posts on the Mac Java developers mailing list mentioned that too).

So, with a bit of manual editing of the file structure and the Info.plist, and copying the JavaAppLauncher out of AppConverter, I got it to work just fine, without the need to figure out how to run and configure appbundler.

By way of example, here is the Package Contents of DicomCleaner the old way:



and here it is the new way:


And here is the old Info.plist:


and here is the new Info.plist:

Note that it is no longer necessary to specify the classpath (not even sure how to); apparently the JavaAppLauncher adds everything in Contents/Java to the classpath automatically.

Rather than have all the Java properties under a single Java key, the JavaAppLauncher seems to use a JVMMainClassName key rather than Java/MainClass, and JVMOptions, rather than Java/VMOptions. Also, I found that in the absence of a specific Java/Properties/apple.laf.useScreenMenuBar key, another item in JVMOptions would work.

Why whoever wrote appbundler thought that they had to introduce these gratuitous inconsistencies, when they could have perpetuated the old Package Content structure and Java/Properties easily enough, I have no idea, but at least the structure is sufficiently "obvious" so as to permit morphing one to the other.

Though I had propagated various properties that jarbundler had originally included, and added one that AppConverter had used (Bundle display name), I was interested to know just what the minimal set was, so I started removing stuff to see if it would keep working, and sure enough it would. Here is the bare minimum that "works" (assuming you don't need any JVM options, don't care what name is displayed in the top line and despite the Apple documentation's list of "required" properties):


To reiterate, I used the JavaAppLauncher copied out of AppConverter, because it worked, and it wasn't obvious where to get it "officially".

I did try copying the JavaAppLauncher binary that is present in the "com/oracle/appbundler/JavaAppLauncher" in appbundler-1.0.jar, but for some reason that didn't work. I also poked around inside javapackager (vide infra), and extracted "com/oracle/tools/packager/mac/JavaAppLauncher" from the JDKs "lib/ant-javafx.jar", but that didn't work either (reported "com.apple.launchd.peruser ... Job failed to exec(3) for weird reason: 13"), so I will give up for now and stick with what works.

It would be nice to have an "official" source for JavaAppLauncher though.

In case it has any impact, I was using OS 10.8.5 and JDK 1.8.0_40-ea whilst doing these experiments.

David

PS. What I have not done is figure out how to include a bundled JRE, since I haven't had a need to do this myself yet (and am not motivated to bother with the AppStore), but I dare say it should be easy enough to find another example and copy it. I did find what looks like a fairly thorough description in this blog entry by Danno Ferrin about getting stuff ready for the AppStore.

PPS. I will refrain from (much) editorial comment about the pros and cons of requiring an embedded JRE in every tiny app, sufficeth to say I haven't found many reasons to do it, except for turn key applications (such as on a CD) where I do this on Windows a bit, just because one can. I am happy Apple/Oracle have enabled it, but surprised that Apple mandated it (for the AppStore).

PPPS. There is apparently also something from Oracle called "javafxpackager", which is pretty well documented, and which is supposed to be able to package non-FX apps as well, but I haven't tried it. Learning it looked more complicated than just doing it by hand. Digging deeper, it seems that this has been renamed to just "javapackager" and is distributed with current JDKs.

PPPPS. There is apparently an effort to develop a binary app that works with either the Apple or Oracle Package Contents and Info.plist properties, called "universalJavaApplicationStub", but I haven't tried that either.


6 comments:

David Clunie said...

I got feedback from Ben Staveley-Taylor and Michael Hall about where to find an "official" JavaAppLauncher version.

Based on their suggestions, I tried:

https://java.net/projects/appbundler/

compiled it after changing the build.xml to point to give the "-isysroot" arg to gcc as point to MacOSX10.8.sdk (rather than MacOSX10.7.sdk, which got removed in some XCode update), and the compiled JavaAppLauncher works after copying it into the Contents/MacOS folder of the Package Contents.

I also tried:

https://bitbucket.org/infinitekind/appbundler

but its compiled JavaAppLauncher presented a "JRELoadError" when I tried it; I guess its findDylib() logic did not find a JRE.

Looking at its source code, there is a check there that restricts it to Java 7 or Java 8, and I also have a Java 9 installed, so it found nothing:

{
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath:@"/usr/libexec/java_home"];

NSArray *args = [NSArray arrayWithObjects: @"-v", @"1.7+", nil];
[task setArguments:args];
...
if ( [outRead rangeOfString:@"jdk1.7"].location != NSNotFound
|| [outRead rangeOfString:@"jdk1.8"].location != NSNotFound)
{
... do good stuff
}

Sticking in a a check for "jdk1.9" makes it work, for now.

This is an unfortunate design, since any apps bundled with infinitekind's appbundler will fail when folks get Java 9. It presumably should check for >= 1.7, not specific versions.

David

On 10/27/14 3:44 AM, Michael Hall wrote:
> On Oct 26, 2014, at 2:59 PM, Ben Staveley-Taylor wrote:
>
>> Try:
>>
>> https://java.net/projects/appbundler
>
> https://bitbucket.org/infinitekind/appbundler
>
> might be more actively supported.
>
> Michael Hall

Dylan Myers said...

The project for which I'm one of the lead developers has found the universaleJavaApplicationStub [when renamed to JavaApplicationStub] to be the best solution for full support all around. We have our ant build script creating our .app files manually instead of using either Apple or Oracle bundlers. We are however using the Apple plist format, and I've opened a couple bug/feature enhancements with the uJAS script author with changes that we made to make it work perfect for us.

Igor Nasibyants said...

It's very usefull information. Thank you guys, but I got one problem:
if I launch my application from command line as "open .app/Contents/MacOS/JavaAppLauncher" it works well, but if I just duble clicked on .app icon, I saw error message - "The application can't be opened -10810". Can you give piece of advice how to fix this problem?

Igor Nasibyants said...
This comment has been removed by the author.
James Stanhope said...

+1 for universaleJavaApplicationStub

I spent many hours trying to get an application launcher to set my application's working directory at Java 7/8 run-time startup. I was [reluctantly] about to start writing a script when I found your blog. Once I replaced JavaAppLauncher with uJAS in Oracle's AppBundler package I haven't had any problems.

Dem Pilafian said...

Apple removed the utilities Jar Bundler, icon Composer, and PacakgeMaker, and it's been a royal pain trying to figure out the correct path forward. The awesome research in the post answered a ton of questions for me.

It looks like javapackager is the way forward, and it's actually not that complicated to use. I put an example at centerkey.com/mac/java. For better or worse, javapackager bundles the JRE (for bandwidth reasons, I now host the resulting .dmg installer files on GitHub).