Running an Apache Felix application from a DMG
17 Oct 2012
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.