Repository: kivy/kivy

Issue:
PyInstaller package does not include GStreamer (solved)
### Versions

* Python: 2.7.15
* OS: macOS 10.14.1 Mojave
* Kivy: 1.10.1
* Kivy installation method: `pip install kivy --no-binary kivy` in virtualenv
* GStreamer 1.14.4 (homebrew; gst-plugins-* also installed)

### Description

When packaging an app that uses `kivy.core.audio.SoundLoader`, PyInstaller does not include the GStreamer plugins or libraries they require in the package. As a result, trying to play a sound results in `GstPlayerException: Unable to create a playbin`. (The unpackaged app works fine).

I think there is code missing from the kivy PyInstaller hook that would find and include the plugin files. The runtime hook includes a directory 'gst-plugins' in the GST_PLUGIN_PATH, but that path isn't mentioned anywhere else.

I solved the issue by adding the following code to my spec file:

```py
def find_gst_plugin_path():
    p = subprocess.Popen(
        ['gst-inspect-1.0', 'coreelements'],
        stdout=subprocess.PIPE)
    (stdoutdata, stderrdata) = p.communicate()

    match = re.search(r'\s+(\S+libgstcoreelements\.\S+)', stdoutdata)

    if not match:
        raise Exception('could not find GStreamer plugins')

    return os.path.dirname(match.group(1))


def find_gst_binaries():
    plugin_path = find_gst_plugin_path()

    plugin_filepaths = glob.glob(os.path.join(plugin_path, 'libgst*'))

   # PyInstaller does not automatically include the libraries the plugins link to
    lib_filepaths = set()
    for plugin_filepath in plugin_filepaths:
        plugin_deps = bindepend.selectImports(plugin_filepath)
        lib_filepaths.update([path for _, path in plugin_deps])

    plugin_binaries = [(f, 'gst-plugins') for f in plugin_filepaths]
    lib_binaries = [(f, '.') for f in lib_filepaths]

    return plugin_binaries + lib_binaries

# SNIP
a = Analysis(['../testapp/main.py'],
             pathex=['.'],
             binaries=find_gst_binaries(),
# SNIP
```

### Code and Logs

Here's my test app:
https://github.com/brsaylor/soundloader-test

(It requires a command line argument with the sound file to play).

Deleting the `find_gst_binaries()` code demonstrates the issue.


Discussion:
The real question is, do we want to do that?
Aka GStreamer is not mandatory, you may uses ffpyplayer too, so adding by default gst libraries will not work either. And we cannot analyze your app to know what you wanted to use.

Maybe it would be better to document and provide snippet for users?
Good point - it wouldn't make sense to always include GStreamer whenever SoundLoader is imported. However, it looks like there's way to specify what providers you want to include in the package with `get_deps_minimal`:

https://github.com/kivy/kivy/blob/master/kivy/tools/packaging/pyinstaller_hooks/__init__.py#L153

Wouldn't it make sense to pull in the GStreamer files when 'gstplayer' is passed to `get_deps_minimal`?
Yep you're right. Maybe that could be your first contribution?

Resolving patch:
diff --git a/kivy/tools/packaging/pyinstaller_hooks/__init__.py b/kivy/tools/packaging/pyinstaller_hooks/__init__.py
--- a/kivy/tools/packaging/pyinstaller_hooks/__init__.py
+++ b/kivy/tools/packaging/pyinstaller_hooks/__init__.py
@@ -64,10 +64,14 @@
 import logging
 from os.path import dirname, join
 import importlib
+import subprocess
+import re
+import glob
 
 import kivy
 import kivy.deps
 from kivy.factory import Factory
+from PyInstaller.depend import bindepend
 
 from os import environ
 if 'KIVY_DOC' not in environ:
@@ -163,9 +167,10 @@ def get_deps_minimal(exclude_ignored=True, **kwargs):
 
     :returns:
 
-        A dict with two keys, ``hiddenimports`` and ``excludes``. Their values
-        are a list of the corresponding modules to include/exclude. This can
-        be passed directly to `Analysis`` with e.g. ::
+        A dict with three keys, ``hiddenimports``, ``excludes``, and
+        ``binaries``. Their values are a list of the corresponding modules to
+        include/exclude. This can be passed directly to `Analysis`` with
+        e.g. ::
 
             a = Analysis(['..\\kivy\\examples\\demo\\touchtracer\\main.py'],
                         ...
@@ -229,9 +234,17 @@ def get_deps_minimal(exclude_ignored=True, **kwargs):
 
     mods = sorted(set(mods))
 
-    if exclude_ignored and not any('gstplayer' in m for m in mods):
+    binaries = []
+    if any('gstplayer' in m for m in mods):
+        binaries = _find_gst_binaries()
+    elif exclude_ignored:
         excludes.append('kivy.lib.gstplayer')
-    return {'hiddenimports': mods, 'excludes': excludes}
+
+    return {
+        'hiddenimports': mods,
+        'excludes': excludes,
+        'binaries': binaries,
+    }
 
 
 def get_deps_all():
@@ -245,15 +258,17 @@ def get_deps_all():
 
     :returns:
 
-        A dict with two keys, ``hiddenimports`` and ``excludes``. Their values
-        are a list of the corresponding modules to include/exclude. This can
-        be passed directly to `Analysis`` with e.g. ::
+        A dict with three keys, ``hiddenimports``, ``excludes``, and
+        ``binaries``. Their values are a list of the corresponding modules to
+        include/exclude. This can be passed directly to `Analysis`` with
+        e.g. ::
 
             a = Analysis(['..\\kivy\\examples\\demo\\touchtracer\\main.py'],
                         ...
                          **get_deps_all())
     '''
     return {
+        'binaries': _find_gst_binaries(),
         'hiddenimports': sorted(set(kivy_modules +
                                     collect_submodules('kivy.core'))),
         'excludes': []}
@@ -284,3 +299,54 @@ def add_dep_paths():
         if hasattr(mod, 'dep_bins'):
             paths.extend(mod.dep_bins)
     sys.path.extend(paths)
+
+
+def _find_gst_plugin_path():
+    '''Returns a list of directories to search for GStreamer plugins.
+    '''
+    if 'GST_PLUGIN_PATH' in environ:
+        return [
+            os.path.abspath(os.path.expanduser(path))
+            for path in environ['GST_PLUGIN_PATH'].split(os.pathsep)
+        ]
+
+    try:
+        p = subprocess.Popen(
+            ['gst-inspect-1.0', 'coreelements'],
+            stdout=subprocess.PIPE)
+    except:
+        return []
+    (stdoutdata, stderrdata) = p.communicate()
+
+    match = re.search(r'\s+(\S+libgstcoreelements\.\S+)', stdoutdata)
+
+    if not match:
+        return []
+
+    return [os.path.dirname(match.group(1))]
+
+
+def _find_gst_binaries():
+    '''Returns a list of GStreamer plugins and libraries to pass as the
+    ``binaries`` argument of ``Analysis``.
+    '''
+    gst_plugin_path = _find_gst_plugin_path()
+
+    plugin_filepaths = []
+    for plugin_dir in gst_plugin_path:
+        plugin_filepaths.extend(
+            glob.glob(os.path.join(plugin_dir, 'libgst*')))
+    if len(plugin_filepaths) == 0:
+        logging.warn('Could not find GStreamer plugins. ' +
+                     'Possible solution: set GST_PLUGIN_PATH')
+        return []
+
+    lib_filepaths = set()
+    for plugin_filepath in plugin_filepaths:
+        plugin_deps = bindepend.selectImports(plugin_filepath)
+        lib_filepaths.update([path for _, path in plugin_deps])
+
+    plugin_binaries = [(f, 'gst-plugins') for f in plugin_filepaths]
+    lib_binaries = [(f, '.') for f in lib_filepaths]
+
+    return plugin_binaries + lib_binaries
diff --git a/kivy/tools/packaging/pyinstaller_hooks/pyi_rth_kivy.py b/kivy/tools/packaging/pyinstaller_hooks/pyi_rth_kivy.py
--- a/kivy/tools/packaging/pyinstaller_hooks/pyi_rth_kivy.py
+++ b/kivy/tools/packaging/pyinstaller_hooks/pyi_rth_kivy.py
@@ -5,8 +5,7 @@
 
 os.environ['KIVY_DATA_DIR'] = os.path.join(root, 'data')
 os.environ['KIVY_MODULES_DIR'] = os.path.join(root, 'modules')
-os.environ['GST_PLUGIN_PATH'] = '{};{}'.format(
-    sys._MEIPASS, os.path.join(sys._MEIPASS, 'gst-plugins'))
+os.environ['GST_PLUGIN_PATH'] = os.path.join(sys._MEIPASS, 'gst-plugins')
 os.environ['GST_REGISTRY'] = os.path.join(sys._MEIPASS, 'registry.bin')
 
 sys.path += [os.path.join(root, '_libs')]


Test patch: