Back to the code page

Using the Sparkle software update framework with a Java application


NOTE: this documentation targets Sparkle 1.5b6 and above.

Sparkle is a really nice framework to add auto-update functionality to your MacOSX software. It is primarily designed to be integrated to Cocoa applications, but you can also use it with a Java application by developing a Java Native Interface (JNI) written in Objective-C and an activator class written in Java.

Basically, Sparkle retrieves information about updates from a file located on your servers (called an appcast). It compares the build number between the current version of the application and the one from the appcast. An update is proposed to the user if the appcast includes a greater version number.

Here are some screenshots of an auto-update handled by Sparkle for the Jitsi (formerly SIP Communicator) software. When an update is available, the following popup appears:

Clicking on the Install Update button will start the download of the update:

Once the new package has been downloaded, it will replace the current one and restart the updated application:

In this document, we will cover how to create a JNI for your Java application and how to integrate Sparkle in your application release. First, download the Sparkle Framework. We will need to modify it slightly to make it work with a packaged Java application. You can find this modified version here (version 1.5b6), but you will find below the procedure to do it by yourself if you want.

Open the Sparkle.xcodeproj file with Xcode (this file is located in the Extras/Source Code directory). Configure Xcode from the menubar with the following:

  • Project -> Set Active Target -> Sparkle
  • Project -> Set Active Build Configuration -> Release

Then, expand the Targets from the Xcode left menu. Select Sparkle, and then go to the File -> Get Info menu. Make sure that you have selected the correct active target and build configuration as explained above before going to the Get Info menu ! Now, in the Build tab, go to the Deployment section, and change the Installation Directory key with the following value:

@executable_path/../Frameworks

Close the menu and compile the framework by clicking on the Build button in Xcode. The new Sparkle.framework will be available in the Extras/Source Code/build/Release/ directory.

Copy the modified version of the Sparkle.framework into your /Library/Frameworks/ system directory. Now, you will need to implement the activator class and the JNI for your application. Let's assume that your activator class is named SparkleActivator.java and is located in the net/java/myappl/sparkle/ directory. It should look like this:


/**
 * Activates the Sparkle Framework
 * net/java/myappl/sparkle/SparkleActivator.java
 */
package net.java.myappl.sparkle;

public class SparkleActivator
{    
    /** 
     * Native method declaration
     */
    public native static void initSparkle(String pathToSparkleFramework, 
                                          boolean updateAtStartup, 
                                          int checkInterval);

    /**
     * Whether updates are checked at startup
     */
    private boolean updateAtStartup = true;

    /**
     * Check interval period, in seconds
     */
    private int checkInterval = 86400;  // 1 day

    /**
     * Dynamically loads the JNI object. Will 
     * fail if it is launched on an non-MacOSX system
     * or when libinit_sparkle.dylib is outside of the 
     * LD_LIBRARY_PATH
     */    
    static {
        System.loadLibrary("sparkle_init");
    }

    /**
     * Initialize and start Sparkle
     *
     * @throws Exception
     */
    public void start() throws Exception
    {
        initSparkle(System.getProperty("user.dir") 
                    + "/../../Frameworks/Sparkle.framework",
                    updateAtStartup, checkInterval);
    }
}

Then, you need to generate the JNI header file that matches the definition of the above native method declaration. For that purpose, compile the Java SparkleActivator class, then go to the root directory where this new class file has been created. For example, if the class has been created in classes/net/java/myappl/sparkle/, go to the classes/ directory, and execute the following command:

$ javah -jni net.java.myappl.sparkle.SparkleActivator

This will create a net_java_myappl_sparkle_SparkleActivator.h header file. Move it into your native/sparkle/ directory. We will now implement the native function which will init the Sparkle framework. Create a file called net_java_myappl_sparkle_SparkleActivator.m in the native/sparkle/ directory, and copy the below code in it:


/**
 * JNI to init the Sparkle subsystem.
 * native/sparkle/net_java_myappl_sparkle_SparkleActivator.m
 */

#include <Cocoa/Cocoa.h>
#include <Sparkle/Sparkle.h>
#include "net_java_myappl_sparkle_SparkleActivator.h"

/*
 * Class:     net_java_myappl_sparkle_SparkleActivator
 * Method:    initSparkle
 * Signature: (Ljava/lang/String;ZI)V
 */

JNIEXPORT void JNICALL
Java_net_java_myappl_sparkle_SparkleActivator_initSparkle
  (JNIEnv *env, jclass obj, 
   jstring pathToSparkleFramework, 
   jboolean updateAtStartup, 
   jint checkInterval)
{
    BOOL hasLaunchedBefore = [[NSUserDefaults standardUserDefaults] 
                              boolForKey:@"SCHasLaunchedBefore"];

    if(!hasLaunchedBefore)
    {
        [[NSUserDefaults standardUserDefaults] setBool:YES 
                                               forKey:@"SCHasLaunchedBefore"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }

    SUUpdater *suUpdater = [SUUpdater updaterForBundle:[NSBundle mainBundle]];

    NSMenu* menu = [[NSApplication sharedApplication] mainMenu];
    NSMenu* applicationMenu = [[menu itemAtIndex:0] submenu];
    NSMenuItem* checkForUpdatesMenuItem = [[NSMenuItem alloc]
                                            initWithTitle:@"Check for Updates..."
                                            action:@selector(checkForUpdates:)
                                            keyEquivalent:@""];

    [checkForUpdatesMenuItem setEnabled:YES];
    [checkForUpdatesMenuItem setTarget:suUpdater];

    // 0 => top, 1 => after "About..."
    [applicationMenu insertItem:checkForUpdatesMenuItem atIndex:1];

    // Update is launched only at the second startup
    if (hasLaunchedBefore && updateAtStartup == JNI_TRUE)
    {
        [suUpdater 
         performSelectorOnMainThread:@selector(checkForUpdatesInBackground)
                                withObject:nil 
                                waitUntilDone:NO];
    }
}
// EOF

You can now compile your JNI. For that purpose, copy this sample Makefile into you native/sparkle/ directory (note that you must set the INSTALL_DIR field to the place where the other libraries for your software are located):


# Makefile for the Sparkle JNI
# native/sparkle/Makefile
#
# Requires the modified Sparkle.framework installed in /Library/Frameworks

CC=gcc -arch x86_64 -arch i386 -arch ppc
TARGET=libsparkle_init.dylib
JNI_INCLUDE_PATH=/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Headers/
CFLAGS=-I$(JNI_INCLUDE_PATH)
LIBS=-framework AppKit -framework Foundation -framework Sparkle
OBJS=net_java_myappl_sparkle_SparkleActivator.o

# Set the below field to the location of your other libraries
# for example lib/native
INSTALL_DIR=../../lib/native/

all:$(TARGET)

clean:
	rm -rf $(TARGET) $(OBJS) *~

install:$(TARGET)
	cp $(TARGET) $(INSTALL_DIR)

libsparkle_init.dylib:$(OBJS)
	$(CC) -dynamiclib -o $(TARGET) $(LIBS) $(CFLAGS) $<
# EOF

Compile the JNI from the native/sparkle/ directory with:

$ cd native/sparkle/
$ make; make install

The modified Sparkle framework and your JNI library must be copied into your software package. First, zip the modified Sparkle.framework directory that we built earlier in this document (name it e.g. Sparkle.framework.zip, or use directly this modified version), and save it in your application resource directory. It will be included in the application package at build time.

Sparkle mandates updates to be signed. You need to create a public/private DSA key pair. This can be easily done with the scripts available in the Sparkle release:

$ cd path/to/Sparkle x.x/
$ ruby "Extras/Signing Tools/generate_keys.rb"

Put the public key (dsa_pub.pem) in your application resource directory, we will include it in the application package. Keep the private key (dsa_priv.pem) safe: no one should get it. If you lose it, you won't be able to issue any new updates.

Before going to the package building step, you have to setup an appcast online for your application. This step is well documented in this documentation on Sparkle website. The appcast XML file must include several information such as the build number, the package name, the package size, and the signature of the package. You may thus create a little script that dynamically creates such appcast file every new update. Note that you will need the previously created DSA private key (dsa_priv.pem) to sign your package, and include the signature in the appcast XML file. How to sign your package is also explained in the Sparkle documentation. The URL where you will store your appcast XML file will be used at the building step below.

We assume you use Ant to build your application, and already have a target in your build.xml that builds your .app package (as explained in this tutorial). We will create a separate target (called macosx-sparkle) that will take care of completing the package with the new libraries. We use a different target in order to provide developers a way to build an .app package with or without sparkle.

First of all, you have to update your macosx target (the one that builds the .app package) in order to add a build number needed by Sparkle. The following example supposes that you invoke Ant with a property called sparkle set to your build number (for example: ant -Dsparkle=1780 macosx). This build number has to be increased every new application build.

Note: the sparkle.jar referenced below is a bundle that activates sparkle at startup. You can check its content here.


<target name="macosx"
        description="Create an .app package for MacOSX">
    [...]
    <!-- Sparkle needs the build number -->
    <jarbundler dir="${macosx.app}"
                [...]
                build="${sparkle}"
                [...]
                workingdirectory="$APP_PACKAGE/Contents/Resources/Java">
        [...]
        <!-- You may want to exclude the Sparkle libraries and 
             jar from the build (it will be added by the macosx-sparkle
             target  -->
        <jarfileset dir=".">
            [...]
            <exclude name="lib/native/libsparkle_init.dylib" />
            <exclude name="${app.resrc}/sparkle.jar" />
            [...]
        </jarfileset>
       [...]
    </jarbundler>
</target>

You can now create a new target (that you will be able to invoke with ant -Dsparkle=<build number> macosx-sparkle) that will take care of completing the package with the new libraries:


<!-- Create a MacOSX application package with Sparkle support,
     invoked with "ant macosx-sparkle" -->
<target name="macosx-sparkle" depends="macosx"
        description="Create an .app package for MacOSX with Sparkle support">

    <!-- Set this property value to the directory where your 
         application package has been created (when you invoked 
         the "ant macosx" target -->
    <property name="macosx.app"
              value="PATH_TO_RELEASE_DIRECTORY"/>

    <!-- Set this property value to your application name.
         Do not include the .app extension -->
    <property name="app.name"
              value="MY_APPLICATION_NAME"/>

    <!-- Set this property value to your application resource 
         directory (where is located your Sparkle jar, the 
         Zip of the Sparkle framework and the public key) -->
    <property name="app.resrc"
              value="PATH_TO_APPLICATION_RESOURCE_DIRECTORY"/>

    <!-- Set this property value to your native library 
         directory (where is located your Sparkle JNI library, 
         e.g. lib/native/) -->
    <property name="app.native"
              value="PATH_TO_NATIVE_RESOURCE_DIRECTORY"/>

    <!-- This property is set to the path where are stored 
         the files inside the .app package. You should not need 
         to change it -->
    <property name="app.internal"
              value="Contents/Resources/Java"/>

    <!-- This property is set to the path where are the 
         Sparkle framework will be stored inside the package. 
         You should not need to change it -->
    <property name="sparkle.path"
              value="Contents/Frameworks/Sparkle.framework"/>

    <!-- Create the native library directory in the package -->
    <mkdir dir="${macosx.app}/${app.name}.app/${app.internal}/${app.native}"/>

    <!-- Add the Sparkle bundle and JNI to the application package -->
    <copy file="${app.resrc}/sparkle.jar"
          todir="${macosx.app}/${app.name}.app/${app.internal}/${app.resrc}"/>
    <copy file="${app.native}/libsparkle_init.dylib"
          todir="${macosx.app}/${app.name}.app/${app.internal}/${app.native}"/>

    <!-- Add the public key file to the application bundle -->
    <copy file="${app.resrc}/dsa_pub.pem"
          todir="${macosx.app}/${app.name}.app/Contents/Resources/"/>

    <!-- The Info.plist file inside the .app package must be updated 
         with several Sparkle properties. Set the SUFeedURL property
         to the URL where your appcast XML file is located -->
    <replace file="${macosx.app}/${app.name}.app/Contents/Info.plist">
        <replacetoken><![CDATA[<key>CFBundleName</key>]]></replacetoken>
        <replacevalue><![CDATA[<key>SUCheckAtStartup</key>
    <string>YES</string>
    <key>SUScheduledCheckInterval</key>
    <string>86400</string>
    <key>SUPublicDSAKeyFile</key>
    <string>dsa_pub.pem</string>
    <key>SUFeedURL</key>
    <string>http://www.example.org/sparkle/updates.xml</string>
    <key>SUShowReleaseNotes</key>
    <string>YES</string>
    <key>CFBundleName</key>]]></replacevalue>
    </replace>

    <!-- The Sparkle Framework must also be copied in the 
         application package -->
    <mkdir dir="${macosx.app}/${app.name}.app/Contents/Frameworks"/>
    <unzip src="${app.resrc}/Sparkle.framework.zip"
           dest="${macosx.app}/${app.name}.app/Contents/Frameworks"/>

    <!-- The Ant unzip task does not preserve symlinks, so we 
         recreate them -->
    <symlink link="${macosx.app}/${app.name}.app/${sparkle.path}/Versions/Current"
             resource="./A"/>
    <symlink 
      link="${macosx.app}/${app.name}.app/${sparkle.path}/Headers"
      resource="./Versions/Current/Headers"/>
    <symlink 
      link="${macosx.app}/${app.name}.app/${sparkle.path}/Resources"
      resource="./Versions/Current/Resources"/>
    <symlink 
      link="${macosx.app}/${app.name}.app/${sparkle.path}/Sparkle"
      resource="./Versions/Current/Sparkle"/>
    <symlink 
      link="${macosx.app}/${app.name}.app/${sparkle.path}/Resources/fr_CA.lproj"
      resource="./fr.lproj"/>

    <!-- Restore the file permissions to the Sparke relaunch tool -->
    <chmod file="${macosx.app}/${app.name}.app/${sparkle.path}/Resources/relaunch" 
           perm="ugo+rx">
    </chmod>
</target>

Every time you build a new version of your application, you should take care of automatically increasing the build number that is given to Ant (through -Dsparkle=<build number>), uploading this new release to your server and updating the appcast XML file with the new build number, the release download URL and the signature of the package.

Sparkle is thus a really powerful and easy-to-use tool to provide auto-update features to your software. You may consult the online Sparkle documentation which includes many other interesting features.