Bundling a Python application on Mac OS X with VirtualEnv

Arjan Molenaar

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

PYVER=2.6
APP=Gaphor.app
INSTALLDIR=$APP/Contents
virtualenv --python=python$PYVER --no-site-packages $INSTALLDIR

did the trick.

I needed to install all PyGTK related libraries in the application folder.

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

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).

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 `resolve_deps $lib`; do
    #log Fixing `basename $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

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:

$INSTALLDIR/bin/easy_install gaphor

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 :).

Comments (3)

  1. » End of an Era: PyGTK Johns Blog - Reply

    April 3, 2011 at 2:49 am

    [...] Mac improvements (all in one installer work) (bundling advice) [...]

  2. Gia Emigholz - Reply

    December 3, 2011 at 4:41 pm

    If you setup virtual hosts on your localhost install, couldn't you avoid having to search and replace [for localhost to yourdomain.com] your db files?

  3. Mike - Reply

    January 21, 2014 at 5:21 am

    Thanks for any other informative blog. Where else may just I am getting that kind of information written in such an ideal means? I've a venture that I am just now running on, and I have been on the glance out for such info.

Add a Comment