Using Sparkle to keep Java applications updated

The Sparkle framework is used in Cocoa applications to get instant self-update functionality. If you're a Mac user, you probably use Adium, so you've probably seen Sparkle in action, telling you there's a new version, how it differs from the version you've got, and letting you choose between "Skip This Version", "Remind Me Later", and "Install Update".

If you've got a Cocoa application, using Sparkle could hardly be easier. If you've want to use Sparkle in your Java application, though, you've a little more work to do. I'll ignore the setting up of "appcast" feeds and the like, because that's exactly the same as with a Cocoa application. This post is just meant to run down the things you'll have to do that are different and otherwise undocumented.

Things your Java application may not already do

Set CFBundleVersion. The first thing you need to take care of is ensuring that you set the CFBundleVersion property in your Info.plist. You may not have bothered with that before, because it's not particularly useful. I'm sure there are some advanced Mac OS users out there who regularly check their application versions from the Finder or System Profiler, but I bet there are far more who have no clue about this, and use the cross-platform "have a look in the about box" method instead. Sparkle, though, uses the CFBundleVersion to see if the appcast has a newer version available, so you'll need to make sure you're setting it correctly. (Don't ask why Apple's java(1) doesn't make use of the Info.plist information when showing its default about box, and why I had to write a bunch of code to duplicate that dialog in Swing.)

Launch a JVM directly. The second thing is a little more subtle. For Sparkle to work, [NSBundle mainBundle] needs to point to your application's .app bundle. What that means to you as a Java developer is that the executable that starts the JVM must be in the Contents/MacOS/ directory. Specifically, you can't have a shell script or whatever in there that calls java(1). You need a binary launcher that directly starts a JVM. There may be a way to fix up the mainBundle after the fact, but I wasn't able to find one. So unless you were already using a custom launcher (or, I imagine, Apple's one from JarBundler), there's some extra work for you there.

Things your Java application definitely doesn't yet do

Install Sparkle.framework. The first new thing is trivial: you need to copy Sparkle.framework into your Contents/Frameworks/ directory. Note that Sparkle will currently add at least 1.4 MiB to your application's size (more if you want non-English localizations). Not a problem if your application is a monster, but most of my Java applications are currently smaller than that, thanks to the small size of Java bytecode.

Initialize Sparkle. You'll need to add something like this to one of your Java classes involved in startup:

static {
public native static void initSparkle();

As usual, I recommend salma-hayek's JavaHpp for all your JNI needs, but however you do things you'll need something like this in your JNI:

#include <Cocoa/Cocoa.h>

void org_jessies_app_AppClass::initSparkle() {
bool haveBundle = ([[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"] != nil);

NSString* path = [NSString initWithUTF8String:path_to_sparkle_dot_framework];
NSBundle* bundle = [NSBundle bundleWithPath:path];
Class suUpdaterClass = [bundle classNamed:@"SUUpdater"];
id suUpdater = [[suUpdaterClass alloc] init];

NSMenu* menu = [[NSApplication sharedApplication] mainMenu];
NSMenu* applicationMenu = [[menu itemAtIndex:0] submenu];
NSMenuItem* checkForUpdatesMenuItem = [[NSMenuItem alloc]
initWithTitle:@"Check for Updates..."
if (haveBundle) {
[checkForUpdatesMenuItem setEnabled:YES];
[checkForUpdatesMenuItem setTarget:suUpdater];
// 0 => top, 1 => after "About...", 2 => after separator, 3 => after "Preferences...".
// FIXME: should search for "Preferences...", because it might not be there!
[applicationMenu insertItem:checkForUpdatesMenuItem atIndex:3];

Notice the weird little dance I do to instantiate the class. This is so that you don't have to worry about sorting out your build system to link your JNI code against the Sparkle framework, though you will have to provide the path to the framework at runtime.

And that's it. (Assuming, as I said, that you've already set up your server as described in the regular Sparkle documentation.)

Why aren't jessies.org projects using Sparkle?

If you use any of the jessies.org projects, you might be asking yourself at this point why this functionality hasn't been added to any of the jessies.org projects yet. It's not that I've only just worked this out. I've been sitting on this (for no reason other than the usual lack of time) for months now.

It's too big/duplication is evil. I mentioned earlier that Sparkle's quite big by the standards of Java applications (as opposed to the JDK/JRE, either of which easily dwarf Sparkle). That can be a consideration, because you're potentially doubling the size of your download at the same time as making your application significantly easier to update. Someone's paying for that bandwidth, and if it's you, you might care.

Relatedly, the fact that every Sparkle-using application has to ship its own copy is a pain. There are good reasons why some developers prefer to ship their own, tested combinations of pre-requisites rather than be at the mercy of random updates, and there are equally good reasons why some system administrators prefer to use their own, centralized combinations of pre-requisites rather than be at the mercy of slow-to-update developers, but given that Sparkle is meant to improve the end-user experience, it's a shame that there's no central Sparkle equivalent of Apple's Software Update, at least for those people with more than one Sparkle-using application. This is on the to-do list for Sparkle 2, but if this bothers you (because you have an Adobe-like suite of applications, say), you might care.

Plus you'll need a known copy of Sparkle.framework for your build system to copy. A minor thing, but build systems have a deserved reputation for making it difficult to do even the simplest things, and you'll need to consider the best way to commit a large binary file (or a tree of someone else's files) into your revision control system. And there are usual license issues to consider (Sparkle uses an MIT license).

It's not cross-platform. You might argue that this is an unfair complaint because Sparkle's mission statement only talks about Cocoa, and it does a damn fine job there. I wouldn't argue that Sparkle should support Linux and Windows so much as I'd argue that "if you only care about the Mac, you shouldn't be writing a Java application". Seriously, any other choice than Cocoa is madness unless you care about being cross-platform. (Some people argue that the "right way" to do cross-platform is to write multiple independent front-ends. But I'll assume that's too "expensive", and point to Microsoft Office as justification for thinking so, regardless of who you are. You may be a beautiful and unique snowflake, but you're not bigger than Microsoft.)

One approach would be to write your own Sparkle in Java. There's nothing fundamentally difficult about what Sparkle does, though obviously it's already been tested in the field for some time, where your reimplementation hasn't. (Sparkle's relaunch code was distressingly primitive in the version I looked at, though I don't know that it's possible to do much better on Mac OS.)

You could duplicate Sparkle in Java, and you could even make it compatible enough to use the same appcasts. In some ways, though, Sparkle only really makes sense on Mac OS. Linux has its .deb and .rpm packages, so on Ubuntu, for example, you can take part in system-wide software updates by making your application available in your own .deb repository (or by getting a package into the official Debian or Ubuntu repositories). This won't automatically restart your application, but that's not the done thing on Linux anyway. On Windows you have .msi installers which address reinstallation but do nothing about automatic update.

One problem with the .deb repository is that although an Ubuntu user can install an application by clicking on a link to a .deb package in Firefox, there's currently no sensible mechanism for "subscribing" to the application's .deb repository. They're going to have to go back to the web site with Firefox, see if anything's changed, and click the link again to install the new version. There's no equivalent one-click-install that adds a .deb repository to your system-wide list of package sources.

So Debian-derived Linux users have two related but segregated methods, one more convenient in the short term, the other more convenient in the long term. It seems likely to me that the best solution at the present would be to add functionality to an one-click installed application so that it checks for an updated .deb and downloads and installs it if necessary. It would probably make more sense, though, to use the .deb repository's existing metadata rather than add the Sparkle appcast metadata.

I don't know enough about RedHat-derived Linux distributions to comment intelligently on the situation there.

For Windows, your choice would be between the web server's metadata for the .msi package and adding Sparkle appcast metadata.

It requires a custom launcher. This is perhaps the most annoying part. Not only is this more code to maintain, it's very different code on Mac OS than the usual Linux/Solaris/Windows code. Also, if you were using your start-up script to provide a command-line interface to your application, you'll find that unless the GUI is launched via the .app itself, your dock icon will have the leafname of your launcher binary, which isn't necessary the application name you'd like to see. (This applies to the text in the command-tab window too, though Expose's okay because it uses the window titles as labels instead.)

In the end, it's up to you to consider your concerns and priorities, and decide what makes most sense. Personally, I plan on revisiting the question when Sparkle 2 arrives, or if 10.5's new package format changes anything.

Still, if you think it makes sense for your application, have at it!

Update: Dustin Sacks mentions Three Rings' "Getdown", which doesn't appeal to me because of its apparent cart-before-the-horse JNLP style of making the application seem like a plug-in to the updater, and because of the "Metal LAF" problem of trying to do its own platform-neutral thing, rather than trying to integrate with the individual platforms' existing systems. If you're looking for a solution right now, though, and don't want to have to do the work yourself, this (or JNLP itself) might be worth looking into.