Running an Apache Felix application from a DMG

17 Oct 2012 Sunny Perspective

Over the summer I moved bliss onto the OSGi platform. The main reason for this was to benefit from the ability to update bliss's code, even while bliss runs. Thus, in-app update could be implemented.

There are a number of useful side-benefits. OSGi allows for greater code modularity and more dynamism. The downside was it was a lot of work, both understanding OSGi and then learning about different OSGi containers' subtle differences. In the end I decided to adopt Apache Felix as bliss's OSGi container, as it seemed to have good community support and was relatively lightweight.

An important part of the work was to change bliss's build process such that the bliss application would run on Windows, Linux, Vortexbox and OS X. For the first three platforms, bliss uses the IzPack installer. However, for OS X, bliss is packaged as a DMG. Here's what I found about creating DMGs for Felix based applications.

Booting Felix from a DMG

Early on I decided it would be best if I could simply boot Felix directly from bliss's startup scripts/executables. This way I wouldn't need to write any 'glue' code to embed Felix. At the time, much of the example code used APIs specific to Felix, and I wanted to avoid that. In fact I wanted to avoid writing any code at all.

The standard way of booting Felix is to execute the felix.jar file using the -jar switch in the java executable:

java -jar felix.jar

So far, so standard. In fact, prior to the port to OSGi, bliss also used that same mechanism. Given that booting Felix from the command line is pretty identical to booting any other Java application, the DMG file's structure did not need to change an awful amount to what it was previously. The bare bones are:

bliss.app/
	Contents/
		MacOS/
			JavaApplicationStub
		Resources/
			Java/
		Info.plist
		PkgInfo

The Info.plist is an OS X properties file that includes the declaration:

<key>CFBundleExecutable</key>
<string>JavaApplicationStub</string>

Thus, when the app bundle is run, OS X executes the JavaApplicationStub. To add Felix to the mix, we drop the felix.jar into the Resources/Java folder...

bliss.app/
	Contents/
		MacOS/
			JavaApplicationStub
		Resources/
			Java/
				felix.jar
		Info.plist
		PkgInfo

And then we include a reference to the JAR in the ClassPath property in the Info.plist:

<key>ClassPath</key>
<array>
	<string>$JAVAROOT/felix.jar</string>
	<string>$JAVAROOT</string>
</array>

So now, when the bliss.app application bundle is run, JavaApplicationStub is executed and felix.jar is added to the classpath.

That's not quite enough yet though, because we haven't actually started Felix. Back to the Info.plist file, we specify the Felix Main class containing a main method:

<key>MainClass</key>
<string>org.apache.felix.main.Main</string>

That's enough to start Felix from a DMG file.

Running bliss within Felix

But an OSGi container with nothing inside it is pretty uninteresting! So the next step is to make sure the bliss application is run inside the container.

One way of 'bootstrapping' a Felix container is to configure an auto deploy folder. The location of the directory can be specified in a Java system property, which are easy to set in the Info.plist. Here, we set Felix's auto deploy folder:

<key>felix.auto.deploy.dir</key>
<string>$APP_PACKAGE/Contents/Resources/bundle</string>

This means that, when Felix starts, Felix looks inside the Resources/bundle folder for OSGi bundles that are then started automatically. At this point, the DMG's structure begins to resemble the vanilla bliss install:

bliss.app/
	Contents/
		MacOS/
			JavaApplicationStub
		Resources/
			bundle/
				com.elsten.bliss.bootstrapbundle.jar
				org.apache.felix.bundlerepository-1.6.7-20120701.125126-3.jar
			Java/
				felix.jar
		Info.plist
		PkgInfo

bliss's bootstrap mechanism is a discussion for another day, but suffice to say bliss uses a Felix capability called the 'bundle repository' to load the rest of the bliss code and its dependencies. At this point, normal OSGi rules apply, so the bliss platform is booted through that bundle's activator, the bliss UI is started and so on.

Later update

When Felix runs and bundles are resolved and activated within, the bundles are stored inside a bundle cache on the file system. Later, when in-app update wants to update bliss's code, the same cache is overwritten with the new bundle code.

I wanted to control where this cache was stored, because the nature of a distributed DMG, normally, is that it is read only. Fortunately that's easy to do with yet another system property:

<key>org.osgi.framework.storage</key>
<string>$USER_HOME/.bliss/felix-cache</string>

This stores the cache inside a .bliss folder in the user's home folder.

Wrap-up

For the record, here's the entire Info.plist file used by bliss:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
<dict>
	<key>CFBundleIdentifier</key>
	<string>com.elstensoftware.bliss</string>
	<key>CFBundleName</key>
	<string>bliss</string>
	<key>CFBundleVersion</key>
	<string>%{bliss.version}</string>
	<key>CFBundleAllowMixedLocalizations</key>
	<string>true</string>
	<key>CFBundleExecutable</key>
	<string>JavaApplicationStub</string>
	<key>CFBundleDevelopmentRegion</key>
	<string>English</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleIconFile</key>
	<string>bliss.icns</string>
	<key>Java</key>
	<dict>
		<key>VMOptions</key>
		<string>-XX:+HeapDumpOnOutOfMemoryError -Xmx256M</string>
		<key>MainClass</key>
		<string>org.apache.felix.main.Main</string>
		<key>JVMVersion</key>
		<string>1.6+</string>
		<key>WorkingDirectory</key>
		<string>$APP_PACKAGE/Contents/Resources</string>
		<key>Properties</key>
			<dict>
				<key>java.util.logging.config.file</key>
				<string>$JAVAROOT/logging.properties</string>
				<key>java.io.tmpdir</key>
				<string>/tmp</string>
				<key>java.library.path</key>
				<string>$JAVAROOT</string>
				<key>jetty.home.bundle</key>
				<string>com.elsten.bliss.jetty</string>
				<key>felix.auto.deploy.dir</key>
				<string>$APP_PACKAGE/Contents/Resources/bundle</string>
				<key>org.osgi.framework.storage</key>
				<string>$USER_HOME/.bliss/felix-cache</string>
				<key>bliss.bootstrapbundle.initialbundledir</key>
				<string>$APP_PACKAGE/Contents/Resources/bliss-bundle</string>
			</dict>
		<key>ClassPath</key>
		<array>
			<string>$JAVAROOT/felix.jar</string>
			<string>$JAVAROOT</string>
		</array>
	</dict>
</dict>
</plist>

Gatekeeper

A final special word for a new feature enabled by default in OS X Mountain Lion. Gatekeeper disallows any downloaded application bundles from executing if the bundle is not signed by the author with a recognised Apple developer identification.

This is largely out of scope for this article, but the .app file must be signed with this certificate before the DMG is packaged.


Hope this helps someone!

Thanks to Pierre Bédat for the image above.
blog comments powered by Disqus