When it comes to distributing Python packages, Python has its own mechanism. The tooling (either easy_install or pip) allows you to install a Python package and its dependencies. Typically, those packages are installed as Python Eggs (Java has Jars, Ruby has Gems and Python has Eggs). However, one can not expect Mac users to use these command line tools to download and install Python applications, especially GUI applications.
How can you package an application, including library dependencies nicely on a Mac? That’s the task at hand when I wanted to make Gaphor available on the Mac platform.
The idea has been there for quite some time, and it became a lot more concrete once I started to compile and install my open source software using Homebrew.
In this post I’ll outline the way Gaphor is packaged for this platform. I started Gaphor quite some years ago on Linux. It’s written in Python using the GTK+ gui library. After I switched to OS X as my main OS, I still work on this project. The X11 server on OS X works pretty well, so nothing holds me back. Except that it’s hard to share this app with my fellow Mac users.
Gaphor and most of the dependent modules are available as Python eggs from the Python Package Index, hence installing it is a simple task. GTK+ is not available on Mac OS by default and PyGTK (GTK+’s Python bindings) is not available as egg, so those have to be installed from source. I started off with compiling the dependencies (GTK+ and friends) using Homebrew. Homebrew has a big advantage over MacPorts and Fink in that it does not try to do everything itself (build everything from scratch). Instead it tries to use functionality available on the system already (like X11, Python and some basic libs). With a fork of Stein Magnus Jodal’s Homebrew branch I was able to get GTK+ and PyGTK compiled and installed.
Next step is to get this stuff packages in an application bundle in order to make it available to a broad audience. The de-facto packaging tool, py2app, is not able to handle eggs. Since Gaphor depends on eggs (the services for running the application are defined in the egg meta data), I had to figure out some other way to package the application, while retaining the egg structure.
I started of creating a simple application structure: a Gaphor.app folder, a Info.plist file and some additional directories. Next thing is to make this environment Pythonized. The way to to this is to use VirtualEnv. VirtualEnv is a Python tool that helps you set up isolated environments. This can be very handy for example for developing and testing software. A simple
[bash]
PYVER=2.6
APP=Gaphor.app
INSTALLDIR=$APP/Contents
virtualenv --python=python$PYVER --no-site-packages $INSTALLDIR
[/bash]
did the trick.
I needed to install all PyGTK related libraries in the application folder.
[bash]
LOCALDIR=/usr/local
SITEPACKAGES=$INSTALLDIR/lib/python$PYVER/site-packages
PYTHON_BREWS="pycairo pygobject pygtk pyrsvg"
for brew in $PYTHON_BREWS; do
cp -r $LOCALDIR/Cellar/$brew/*/lib/python$PYVER/site-packages/* $SITEPACKAGES
done
[/bash]
Where $SITEPACKAGES is a directory in the folder that the `virtualenv-ed’ python installation will use to install the site-packages (non-default python modules).
Of course PyGTK won’t do a thing without the backing of GTK+ itself, so those libraries also had to be packages. To solve this a listing of dependencies for the freshly installed libraries will do. otool is able to list all dependencies. Those dependencies can be copied and the paths can be changed to relative paths. Relative paths on OS X, funny enough, relate to the location of the binary that was initially started. In this case Python (which is installed in Gaphor.app/Contents/bin).
[bash] function resolve_deps() { local lib=$1 local dep otool -L $lib | grep -e "^.$LOCALDIR/" |\ while read dep _; do echo $dep done } function fix_paths() { local lib=$1 log Fixing $lib for dep in <code>resolve_deps $lib
; do log Fixingbasename $lib
log "| $dep" install_name_tool -change $dep @executable_path/../lib/basename $dep
$lib done } binlibs=find $INSTALLDIR -type f -name '*.so'
for lib in $binlibs; do log Resolving $lib resolve_deps $lib fix_paths $lib done | sort -u | while read lib; do log Copying $lib cp $lib $LIBDIR chmod u+w $LIBDIR/basename $lib
fix_paths $LIBDIR/basename $lib
done [/bash]
Some extra resource files had to be installed and that was it for GTK+. The launch script takes care of setting the environment so the application will work. Now the application itself:
[bash]
$INSTALLDIR/bin/easy_install gaphor
[/bash]
That was easy! The latest version will be downloaded from the PyPI and easy_install is installing it. One of the benefits of using virtualenv is that easy_install as well as pip are installed by default. This makes it very, very easy to install python applications.
You can find the full script on GitHub.
It took a while to figure out all the details, but creating an installable app, Mac style, is rather trivial. Next is a Windows installer and something tells me it won’t be this easy :).