Packaging ZeroBrane Studio as OSX app

(This post is by Daniil Kulchenko who helped with many aspects of the ZeroBrane Studio project.)

Up until recently, ZeroBrane Studio (ZBS in the text that follows) was only available on Windows. But being a Lua and wxWidgets-based app has its benefits -- the code is inherently cross-platform, so distributing on other platforms ought to be only be a matter of figuring out the app packaging systems of each. This turned out to be quite the challenge, so I'll go over the roadbumps we hit along the way with each platform: first OS X, then Ubuntu/Debian. This is the first of a two-part post; this post will cover how we went from a set of Lua files to a complete, distributable .dmg for OS X users.

What constitutes an OS X ".app"

The main hurdle was figuring out how to package the libraries that ZBS depends along with the app, namely wxLua, wxWidgets, and Lua itself. OS X generally uses a system of "frameworks" to do so; for example, if you have Adium installed, navigate to /Applications/Adium.app/Contents/Frameworks to see a list of ~21 frameworks, which are collections of libraries and other resources needed to run Adium itself. But in the interest of keeping the code tree looking as similar as possible among platforms, we decided to instead keep the wx and wxLua .dylib (library) files in the 'bin' directory of ZBS, much like how the .dlls are kept on the Windows version of ZBS.

To do so, we needed to ensure OS X would know where to look for the .dylib files when starting up ZBS. From my days of developing on Linux, I knew about LD_LIBRARY_PATH that would do that exact job with Linux's equivalent: .so files. OS X has an equivalent DYLD_LIBRARY_PATH that does the same job, so we wrote a wrapper script that would start ZBS with the correct environment set up such that Lua would be able to 'require' in the wxLua .dylib, which would in turn be able to find wxWidgets itself (being present in the .dylib search path).

#!/bin/bash
ZBS_PATH=${0%/*/*}
if [ ! -d $ZBS_PATH ]; then ZBS_PATH=${PWD%/*}; fi
export DYLD_LIBRARY_PATH="$ZBS_PATH/ZeroBraneStudio/bin"
(cd "$ZBS_PATH/ZeroBraneStudio"; bin/lua src/main.lua zbstudio)

By this point, on most platforms, your app would be ready to run, but on OS X, it's a different story: running the script above will open the zbs window, but in a curiously vegetative state: you can't focus the window, you can't Cmd-Tab to it, and it doesn't show up in the dock. For all intents and purposes, you have no way of interacting with the window you've just opened. On OS X, that privilege is reserved for those programs packaged in a .app directory in a specially predefined format:

ZeroBraneStudio.app/
  Contents/
    MacOS/
    Resources/
    Info.plist

Let's go through these one at a time. The app's main executable/shell script goes into the MacOS directory. In our case, that's the shell script from above. Info.plist is a beast of its own, and the easiest way to create this one is to take one from another app and modify it to your needs. The important keys are CFBundleExecutable (which is the name of the file placed in MacOS), CFBundleDisplayName (the way the app's name will be presented in the OS), and CFBundleIconFile (the path of the app's icon, using Resources/ as a base).

Creating the icon file was surprisingly easy to do: all one needs is Xcode's "Graphical Tools for Xcode" addon. To create the icon file, you'd open Icon Composer by first opening Xcode and going to Xcode | Open Developer Tool | Icon Composer. From here, you can drag and drop .pngs of various sizes (256×256, 128×128, etc), then save your .icns file and drop it in the Resources/ folder.

The rest of the organization of the app bundle is flexible; we placed the ZBS code itself within ZeroBraneStudio.app/Contents/ZeroBraneStudio (referenced by the shell script above).

At this point, running ZBS via open ZeroBraneStudio.app will open up the window, add it to the dock, and allow us to focus and interact with the window. Awesome!

Code signing

In OS X 10.8, Mountain Lion, Apple introduced "Gatekeeper", which institutes a default security setting of allowing only apps either distributed through the Mac App Store or apps signed with a "Developer ID" to run on a Mac. To do so, you need to sign your .app directory with the "codesign" utility shipped with OS X. Those using Xcode will have all of this taken care of them automatically, but as we're a Lua app and DIY-ing the whole process, the luxury wasn't available to us. To sign the app for distribution, you need a certificate imported into your "keychain". But just signing the app isn't enough; signing OS X apps has been around for years, and many various vendors sell certificates which you can use apps. We learned the hard way that those are insufficient to satisfy Gatekeeper; you specifically need a "Developer ID" certificate directly from Apple, which requires enrollment in the Mac Developer Program and costs $99/year. Once we obtained one, we imported it into the keychain and ran:

$ codesign -s "Company Name" ZeroBraneStudio.app

To test to ensure Gatekeeper is satisfied:

$ sudo spctl -a -v ZeroBraneStudio.app

You should see: "./x.app: accepted".

Here's Apple's overview of the process.

Distribution

At this point, the app is ready for distribution. There's two primary ways of doing so on OS X: a .zip or a .dmg. For all practical purposes, there's no difference between the two aside from the fact that you can run an app directly from a .dmg without having to "install" the app. We chose to go with a .dmg since it allows for a nice splash screen while installing the app (like the one you see at the top of this post). Here's how the process goes.

Start by creating an empty 100MB writeable .dmg using Disk Utility. If you're familiar with loopback filesystems on Linux, a .dmg is more or less the same thing.

Generally the .dmg template includes the app itself along with a shortcut to /Applications, allowing a drag and drop from one to the other (this is the "installation" process on OS X). We placed the ZeroBraneStudio.app directory in the dmg file and ran cd /Volumes/ZeroBraneStudio; ln -s /Applications to create the basic structure.

To make the .dmg look good when mounted, the general practice is to use a background image for the .dmg and position the app + /Applications icons to match the artwork as needed. I used the GIMP for this. Once you've created a .png to use as the background, create a .background directory in the .dmg and save the .png there.

Open the .dmg's volume in Finder and go to View -> Show View Options (or hit Cmd-J). At the bottom of the window, choose Background: Picture. Open another Finder window and browse to the .background directory on your .dmg (it's more than likely hidden, so use Cmd-Shift-G to navigate to .background). Drag the .png from the .background directory into the blank square to the right of "Picture" in the View Options window. Your background will appear behind the .dmg files.

About halfway down the View Options window you'll notice an "Icon Size" option. This, along with "Grid Spacing" is up to you to configure to match your background artwork. For ZBS, we used 128×128, and this is the usual configuration for most .dmgs.

Finally, drag your icons into place to match your artwork. Go to View and ensure the "path bar", the status bar, the toolbar, and the sidebar are all hidden. Now, resize your window to match the background dimensions.

At this point, your .dmg should look exactly how you'll want it to look to users. To finalize the .dmg for distribution, open up Disk Utility, select the mounted disk image, unmount it, then hit "Convert". Set "Image Format" to "compressed" and Encryption to "none" and save the new .dmg. Make sure you don't overwrite the original .dmg, keep it aorund in case you need to make changes and re-finalize.

At this point you should have a new .dmg that's a fraction of the size of the original (it'll be readonly and compressed). If you double-click the .dmg to mount it, a window should pop up that looks exactly like the window you configured above. If so, you're done and ready to distribute the .dmg!

Automation

Obviously, this is a rather tedious process and not something you want to have to do every time you need to regenerate a .dmg for distribution. For ZBS, we automated this entire process with a bash script. As input, the script uses a skeleton dmg, which is the 100MB writeable .dmg which we created above, with all the app's files removed (everything within ZeroBraneStudio.app, but without deleting .app itself). This is then run through bzip2, converting the 100MB dmg into a 60KB bz2 file.

It looks something like this:

#!/bin/bash

set -x

TEMPLATE_DMG=/tmp/ZeroBraneStudio-template.dmg
BUILT_DMG=ZeroBraneStudio.dmg
WKDIR=/tmp/zbs-build

rm ../zbstudio/ZeroBraneStudio.app/Contents/ZeroBraneStudio
bunzip2 -kf ZeroBraneStudio.dmg.bz2
mv ZeroBraneStudio.dmg $TEMPLATE_DMG
hdiutil attach "${TEMPLATE_DMG}" -noautoopen -quiet -mountpoint "${WKDIR}"

rm -rf "${WKDIR}/ZeroBraneStudio.app"
cp -pr "../zbstudio/ZeroBraneStudio.app" "${WKDIR}/ZeroBraneStudio.app"
mkdir "${WKDIR}/ZeroBraneStudio.app/Contents/ZeroBraneStudio"

(cd ".."; tar cf - $(< zbstudio/MANIFEST) $(< zbstudio/MANIFEST-bin-macos) | (cd "${WKDIR}/ZeroBraneStudio.app/Contents/ZeroBraneStudio/"; tar xf -))

codesign -s "ZeroBrane LLC" ${WKDIR}/ZeroBraneStudio.app
codesign --signature-size 6400 -s "ZeroBrane LLC" ${WKDIR}/ZeroBraneStudio.app/Contents/ZeroBraneStudio/bin/lua.app

sudo rm -rf "${WKDIR}/.Trashes"
sudo rm -rf "${WKDIR}/.fseventsd"

hdiutil detach "${WKDIR}" -quiet -force
hdiutil convert "${TEMPLATE_DMG}" -quiet -format UDZO -imagekey zlib-level=9 -o "${BUILT_DMG}"

rm -f "${TEMPLATE_DMG}"

cd ../zbstudio/ZeroBraneStudio.app/Contents
ln -s ../../.. ZeroBraneStudio

echo Built ${BUILT_DMG}.

This script builds a ready-to-use distributable package for OSX. Get your copy of ZeroBrane Studio and enjoy!

You should get a copy of my slick ZeroBrane Studio IDE and follow me on twitter here.

Leave a comment

what will you say?
(required)
(required)

About

I am Paul Kulchenko.
I live in Kirkland, WA with my wife and three kids.
I do consulting as a software developer.
I study robotics and artificial intelligence.
I write books and open-source software.
I teach introductory computer science.
I develop a slick Lua IDE and debugger.

Recommended

Close