Recipe Gtk Application

Dustin Spicuzza edited this page Feb 22, 2016 · 3 revisions

Bundling a GTK Application (in OS X)

Note: The current version of pyinstaller should have the appropriate runtime hooks builtin, so that these steps are unnecessary when building a GTK3 app that uses PyGObject.

(Published by Charles S. Sharman on the Mailinglist.)

This recipe documents the steps necessary to get a working bundled OS X gtk application. I used python 2.7.7 and pyinstaller 2.1 under homebrew. You must have a working application from source before bundling.

Editor's note: The basics of this recipe should be the same for other platforms.

Begin by changing to your application directory and letting pyinstaller create its own spec file:

pyinstaller -w MyApp.py

where MyApp.py is the name of my application. The -w option makes pyinstaller create a .app bundle.

For a light gtk application, you may be able to change to the dist/MyApp folder and run MyApp. However, if you port it to another computer, you'll be in trouble. gtk requires external helper files that aren't copied by the current pyinstaller hook-gtk.py.

Correcting Pixbuf

gtk uses a loaders.cache file to associate pixbuf files with certain libraries. You'll need a local copy in your directory:

cp /usr/local/lib/gdk-pixbuf-2.0/210.0/loaders.cache .

or

gdk-pixbuf-query-loaders > loaders.cache

If you use the first command, find the correct path to loaders.cache. If you use the second command, it should create a local loaders.cache file, provided gdk-pixbuf-query-loaders is present on your machine. Now, edit the file replacing all the long paths with nothing. For example, I changed

/usr/local/Cellar/gdk-pixbuf/2.30.8/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-ani.so

to

libpixbufloader-ani.so

and so on.

You must add a run-time hook to alert gtk of your loaders.cache location, and you must add loaders.cache and the loaders themselves to your .spec file, but I'll show that later.

Correcting Pango

Like Pixbuf, Pango expects two special file called pango.modules and pangox.aliases. You'll need a copy in your directory:

cp /usr/local/etc/pango/pango.modules .

or

pango-querymodules > pango.modules

If you use the first command, find the correct path to pango.modules. If you use the second command it should create a local pango.modules file, provided pango-querymodules is present on your machine. Now, edit the file replacing all the long paths with nothing. For example, I changed:

/usr/local/Cellar/pango/1.36.3/lib/pango/1.8.0/modules/pango-arabic-lang.so ArabicScriptEngineLang PangoEngineLang PangoRenderNone arabic:*

to:

pango-arabic-lang.so ArabicScriptEngineLang PangoEngineLang PangoRenderNone arabic:*

pangox.aliases does not need to be edited. It simply can be copied:

cp /usr/local/etc/pango/pangox.aliases .

Make sure the path is correct on your system.

Now, pango cannot be alerted of all changes with environment variables like pixbuf could. Instead, pango can be alerted with a pangorc file. In the same directory, create a pangorc file with these contents:

  [Pango]
  ModuleFiles = ./pango.modules

  [PangoX]
  AliasFiles = ./pangox.aliases

You must adjust your run-time hook to alert gtk of your pangorc file, and you must add the files and the libraries to your .spec file.

Run Time Hook

You can redirect gtk to your local loaders.cache, pango.modules, and pangorc through environment variables set in a real-time hook. Create a pyinstaller real-time hook called osx_rthook.py with the following contents:

import os, sys

os.environ['GDK_PIXBUF_MODULE_FILE'] = sys._MEIPASS + '/loaders.cache'
os.environ['PANGO_LIBDIR'] = sys._MEIPASS
os.environ['PANGO_RC_FILE'] = sys._MEIPASS + '/pangorc'

sys._MEIPASS is a pyinstaller-created string that points to your bundled directory.

Spec File

Now it's time to update your spec file to include the real-time hook, the data files, and the binary files. Here's how it ought to look. Lines with '# Changed' or '# Added' appended are changed or added lines:

  # -*- mode: python -*-
  a = Analysis(['MyApp.py'],
               pathex=['/Users/apple/Downloads/MyApp'],
               hiddenimports=None,
               hookspath=None,
               runtime_hooks=['osx_rthook.py']) # Changed
  pyz = PYZ(a.pure)
  exe = EXE(pyz,
            a.scripts,
            exclude_binaries=True,
            name='MyApp',
            debug=False,
            strip=None,
            upx=True,
            console=False )

  base_dir = '.' # Added
  gtks = ['loaders.cache', 'pangorc', 'pango.modules', 'pangox.aliases'] # Added
  data_files = [(x, os.path.join(base_dir, x), 'DATA') for x in gtks] # Added

  more_binaries = [] # Added
  pixbuf_dir = '/usr/local/lib/gdk-pixbuf-2.0/2.10.0/loaders' # Added
  for pixbuf_type in os.listdir(pixbuf_dir): # Added
      if pixbuf_type.endswith('.so'): # Added
          more_binaries.append((pixbuf_type, os.path.join(pixbuf_dir, pixbuf_type), 'BINARY')) # Added

  pango_dir = '/usr/local/lib/pango/1.8.0/modules' # Added
  for pango_type in os.listdir(pango_dir): # Added
      if pango_type.endswith('.so'): # Added
          more_binaries.append((os.path.join('pango/1.8.0/modules', pango_type), os.path.join(pango_dir, pango_type), 'BINARY')) # Added

  coll = COLLECT(exe, data_files, # Changed
                 a.binaries + more_binaries, # Changed
                 a.zipfiles,
                 a.datas,
                 strip=None,
                 upx=True,
                 name='MyApp')

  app = BUNDLE(coll,
               name='MyApp.app',
               icon=None)

To add an icon to your app, change the last line icon=None to icon=MyApp.icns, where MyApp.icns is the name of your icon file.

Conclusion

These steps allowed me to get a working PyInstaller bundle for an OS X gtk application. I suspect there may be more gotchas if your application extends gtk's abilities beyond MyApp. Additionally, MacPorts or pkgsrc may require alternate adjustments.

If it proves to be a universal OS X recipe, it ought to be coded into pyinstaller itself.