pygst + pyinstaller under OSX
Question: I'm currently trying to bundle this dead simple python script (player.py):

    
    
    #!/usr/bin/env python
    
    import sys, os
    import gobject, glib
    import pygst
    pygst.require("0.10")
    import gst
    
    class Player(object):
        def __init__(self):
            self.player = gst.element_factory_make("playbin2", "player")
            fakesink = gst.element_factory_make("fakesink", "fakesink")
            self.player.set_property("video-sink", fakesink)
            bus = self.player.get_bus()
            bus.add_signal_watch()
            bus.connect("message", self.on_message)
    
        def play(self, url):
            self.player.set_state(gst.STATE_NULL)
            self.player.set_property("uri", url)
            self.player.set_state(gst.STATE_PLAYING)
    
        def on_message(self, bus, message):
            t = message.type
            if t == gst.MESSAGE_EOS:
                global loop
                loop.quit()
            elif t == gst.MESSAGE_ERROR:
                self.player.set_state(gst.STATE_NULL)
                err, debug = message.parse_error()
                print "Error: %s" % err, debug
    
    p = Player()
    p.play("file:///path/to/something.mp3")
    gobject.threads_init()
    loop = glib.MainLoop()
    loop.run()
    

into an OSX (my machine runs Mountain Lion) application using pyinstaller
(2.1.0-dev).

My aim is to create a .app bundle that I can easily distribute. I could also
ask the final user to install GStreamer SDK, even though a self-contained
application would be my primary goal.

The spec file follows (player.spec):

    
    
    # -*- mode: python -*-
    import pygst
    pygst.require('0.10')
    
    a = Analysis(['player.py'],
                 pathex=['/Users/mymy/devel/t/simple'],
                 hiddenimports=[],
                 hookspath=None,
                 runtime_hooks=None)
    pyz = PYZ(a.pure)
    exe = EXE(pyz,
              a.scripts,
              exclude_binaries=True,
              name='player',
              debug=False,
              strip=None,
              upx=True,
              console=True )
    coll = COLLECT(exe,
                   a.binaries,
                   a.zipfiles,
                   a.datas,
                   strip=None,
                   upx=True,
                   name='player')
    

I attempted two strategies, so far:

  1. macports: python (2.7.2), pygst (0.10), gst-plugins-*
  2. system python, GStreamer 0.10 SDK (Framework)

In both cases I can successfully run the script.

When I attempt to run the bundled executable, though, I get the following:

  1. (Macports)

Showing a partial GST debug log:

    
    
    ~/devel/t/simple/dist/player > GST_DEBUG=4 ./player
    [..]
    0:00:00.113260000  8313    0x1001b1a00 DEBUG           GST_REGISTRY gstregistrychunks.c:573:gboolean gst_registry_chunks_load_feature(GstRegistry *, gchar **, gchar *, GstPlugin *): Plugin 'playback' feature 'playbin2' typename : 'GstElementFactory'
    0:00:00.113286000  8313    0x1001b1a00 DEBUG           GST_REGISTRY gstregistrychunks.c:621:gboolean gst_registry_chunks_load_feature(GstRegistry *, gchar **, gchar *, GstPlugin *): Element factory : 'Player Bin 2' with npadtemplates=0
    0:00:00.113300000  8313    0x1001b1a00 DEBUG           GST_REGISTRY gstregistrychunks.c:649:gboolean gst_registry_chunks_load_feature(GstRegistry *, gchar **, gchar *, GstPlugin *): Reading 2 Interfaces at address 0x101971191
    0:00:00.113318000  8313    0x1001b1a00 DEBUG           GST_REGISTRY gstregistry.c:558:gboolean gst_registry_add_feature(GstRegistry *, GstPluginFeature *):<registry0> adding feature 0x10097da20 (playbin2)
    0:00:00.113332000  8313    0x1001b1a00 DEBUG        GST_REFCOUNTING gstobject.c:844:gboolean gst_object_set_parent(GstObject *, GstObject *):<playbin2> set parent (ref and sink)
    0:00:00.113346000  8313    0x1001b1a00 DEBUG           GST_REGISTRY gstregistrychunks.c:709:gboolean gst_registry_chunks_load_feature(GstRegistry *, gchar **, gchar *, GstPlugin *): Added feature playbin2, plugin 0x100975be0 playback
    [..]
    0:00:00.242584000  8297    0x1001b1a00 DEBUG     GST_PLUGIN_LOADING gstpluginfeature.c:106:GstPluginFeature *gst_plugin_feature_load(GstPluginFeature *): loading plugin for feature 0x10097da20; 'playbin2'
    0:00:00.242620000  8297    0x1001b1a00 DEBUG     GST_PLUGIN_LOADING gstpluginfeature.c:110:GstPluginFeature *gst_plugin_feature_load(GstPluginFeature *): loading plugin playback
    0:00:00.242632000  8297    0x1001b1a00 DEBUG     GST_PLUGIN_LOADING gstplugin.c:1293:GstPlugin *gst_plugin_load_by_name(const gchar *): looking up plugin playback in default registry
    0:00:00.242662000  8297    0x1001b1a00 DEBUG     GST_PLUGIN_LOADING gstplugin.c:1296:GstPlugin *gst_plugin_load_by_name(const gchar *): loading plugin playback from file /opt/local/lib/gstreamer-0.10/libgstplaybin.so
    0:00:00.242677000  8297    0x1001b1a00 DEBUG     GST_PLUGIN_LOADING gstplugin.c:737:GstPlugin *gst_plugin_load_file(const gchar *, GError **): attempt to load plugin "/opt/local/lib/gstreamer-0.10/libgstplaybin.so"
    0:00:00.248338000  8297    0x1001b1a00 INFO      GST_PLUGIN_LOADING gstplugin.c:859:GstPlugin *gst_plugin_load_file(const gchar *, GError **): plugin "/opt/local/lib/gstreamer-0.10/libgstplaybin.so" loaded
    0:00:00.248374000  8297    0x1001b1a00 DEBUG     GST_PLUGIN_LOADING gstpluginfeature.c:115:GstPluginFeature *gst_plugin_feature_load(GstPluginFeature *): loaded plugin playback
    0:00:00.248390000  8297    0x1001b1a00 INFO      GST_PLUGIN_LOADING gstpluginfeature.c:145:GstPluginFeature *gst_plugin_feature_load(GstPluginFeature *): Tried to load plugin containing feature 'playbin2', but feature was not found.
    0:00:00.248402000  8297    0x1001b1a00 WARN     GST_ELEMENT_FACTORY gstelementfactory.c:410:GstElement *gst_element_factory_create(GstElementFactory *, const gchar *):<playbin2> loading plugin containing feature player returned NULL!
    0:00:00.248412000  8297    0x1001b1a00 INFO     GST_ELEMENT_FACTORY gstelementfactory.c:472:GstElement *gst_element_factory_make(const gchar *, const gchar *):<playbin2> couldn't create instance!
    Traceback (most recent call last):
      File "<string>", line 33, in <module>
      File "<string>", line 11, in __init__
    gst.ElementNotFoundError: playbin2
    

Getting the same result pre-pending:
GST_PLUGIN_PATH=/opt/local/lib/gstreamer-0.10/

I tried to copy the plugins and their dependencies into the dist/player
folder, scripting a wild install_name_tool mangling in order to correct the
paths of the dylibs, but the result doesn't change either.

  1. (GStreamer SDK)

(PYTHONPATH=/Library/Frameworks/GStreamer.framework/Versions/0.10/lib/python2.7/site-
packages/)

    
    
    ~/devel/t/simple/dist/player > ./player 
    ** Message: pygobject_register_sinkfunc is deprecated (GstObject)
    player.py:11: Warning: cannot register existing type `GstObject'
    player.py:11: Warning: g_once_init_leave: assertion `result != 0' failed
    player.py:11: Warning: gtype.c:2720: You forgot to call g_type_init()
    

and here it hangs. If I sample the process via Activity Monitor, I get this:

    
    
    [..]
    _wrap_gst_element_factory_make  (in gst._gst.so)
    gst_element_factory_make  (in libgstreamer-0.10.0.dylib)
    gst_element_factory_create  (in libgstreamer-0.10.0.dylib)
    gst_plugin_feature_load  (in libgstreamer-0.10.0.dylib)
    gst_plugin_load_by_name  (in libgstreamer-0.10.0.dylib)
    gst_plugin_load_file  (in libgstreamer-0.10.0.dylib)
    gst_plugin_register_func  (in libgstreamer-0.10.0.dylib)
    plugin_init  (in libgstplaybin.so)
    gst_play_bin2_plugin_init  (in libgstplaybin.so)
    gst_pipeline_get_type  (in libgstreamer-0.10.0.dylib)
    gst_bin_get_type  (in libgstreamer-0.10.0.dylib)
    gst_child_proxy_get_type  (in libgstreamer-0.10.0.dylib)
    gst_object_get_type  (in libgstreamer-0.10.0.dylib)
    g_once_init_enter  (in libglib-2.0.0.dylib)
    g_cond_wait  (in libglib-2.0.0.dylib)
    _pthread_cond_wait  (in libsystem_c.dylib)
    __psynch_cvwait  (in libsystem_kernel.dylib)
    

A hint would be immensely appreciated!


Answer: I finally have a working solution for the following setup:

  * macports (python @2.7.3, py27-gst-python @0.10.22, gstreamer010 @0.10.36)
  * pyinstaller 2.1-dev (ccb6f3d3d924a0dc2f9e92aa6278c28a2d743d39)
  * OSX 10.8.3

## pyinstaller spec file (player.spec):

    
    
    # -*- mode: python -*-
    import os
    import pygst
    pygst.require('0.10')
    
    a = Analysis(['rthook.py', 'player.py'],
                 pathex=[os.curdir],
                 hiddenimports=[],
                 hookspath=None,
                 runtime_hooks=None)
    pyz = PYZ(a.pure)
    exe = EXE(pyz,
              a.scripts,
              exclude_binaries=True,
              name='player',
              debug=False,
              strip=None,
              upx=True,
              console=True )
    coll = COLLECT(exe,
                   a.binaries,
                   a.zipfiles,
                   a.datas,
                   strip=None,
                   upx=True,
                   name='player')
    

## _gst.so hook (hook-gst._gst.py):

I created a very primitive hook for gst. I tried to put it inside a local
directory, referencing it via the _hookspath_ parameter of the Analysis
object, but I couldn't figure out why pyinstaller ignored it. Therefore I
moved it to the _/path/to/pyinstaller/PyInstaller/hooks_ folder:

    
    
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import os
    
    GST_PLUGINS = '/opt/local/lib/gstreamer-0.10/'
    
    def hook(mod):
        for f in [so for so in os.listdir(GST_PLUGINS) if so[-3:].lower() == '.so']:
            mod.binaries.append((os.path.join('gst-plugins', f),
                    os.path.join(GST_PLUGINS, f),
                    'BINARY'))
    
        return mod
    

pyinstaller takes care of computing the dependencies trees of the plugins,
copying the .sos into place, along with the dependent dylibs and finally
mangling the mach'o headers of both.

I also created an empty file _/path/to/pyinstaller/PyInstaller/hooks/hook-
gst.py_ to stop pyinstaller to complain about the missing parent hook. And,
anyway, the hook code could go directly to _hook-gst.py_.

## runtime hook file (rthook.py)

Finally I added a runtime hook file, referenced on the Analysis object, that
sets up the environment variables that help gstreamer to locate the plugins.
This code gets executed on the bundled executable before _player.py_
(following the way [Kivy](http://kivy.org/docs/guide/packaging-macosx.html)
sets up pyinstaller and thanks for their precious hint):

    
    
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import os
    import sys
    
    if hasattr(sys, '_MEIPASS'):
        # PyInstaller >= 1.6
        root = sys._MEIPASS
    elif '_MEIPASS2' in environ:
        # PyInstaller < 1.6 (tested on 1.5 only)
        root = os.environ['_MEIPASS2']
    else:
        root = os.path.dirname(sys.argv[0])
    
    os.chdir(root)
    
    os.environ['GST_REGISTRY_FORK'] = 'no'
    os.environ['GST_PLUGIN_PATH'] = os.path.join(root, 'gst-plugins')
    

It seems that disabling
[GST_REGISTRY_FORK](http://gstreamer.freedesktop.org/data/doc/gstreamer/head/gstreamer/html/gst-
running.html) is the only way to have a working outcome. Leaving the default
setting (active) leads to a segmentation fault as soon as the first plugin
gets scanned.

## pyinstaller invocation

pyinstaller can be invoked with:

    
    
    $ /path/to/pyinstaller/pyinstaller.py player.spec
    

