Repository: pyinstaller/pyinstaller

Issue:
Hook for gi.repository.GdkPixbuf Looks for gdk-pixbuf-query-loaders on PATH
I am trying to troubleshoot issues with packaging up [Gaphor](https://github.com/gaphor/gaphor) for Windows using MSYS2 by first trying to get it to execute in Linux.

When executing a GTK app that uses a png image, I am getting an error:
`gi.repository.GLib.Error: gdk-pixbuf-error-quark: Couldn’t recognize the image file format for file “temp-icon.png” (3)`

This seems like a regression from the fix in #1833.

I am using the master version of PyInstaller (4.0.dev0+ga9b29999)
Python: 3.7.5
Platform: Ubuntu 19.10 Linux-5.3.0-18-generic-x86_64-with-debian-buster-sid
GTK 3.24.12

I have created a [simple example](https://gist.github.com/danyeaw/faa74e21e188a33c3817fcb8400ab3df) posted to gist. 

```
[9159] LOADER: Running pyiboot01_bootstrap.py
[9159] LOADER: Running pyi_rth_gtk.py
[9159] LOADER: Running pyi_rth_glib.py
[9159] LOADER: Running pyi_rth_gdkpixbuf.py
[9159] LOADER: Running pyi_rth_gio.py
[9159] LOADER: Running pyi_rth_gi.py
[9159] LOADER: Running simple-example.py
Gtk-Message: 09:49:45.114: Failed to load module "canberra-gtk-module"
Traceback (most recent call last):
  File "win-installer/simple-example.py", line 8, in <module>
gi.repository.GLib.Error: gdk-pixbuf-error-quark: Couldn’t recognize the image file format for file “temp-icon.png” (3)
[9159] Failed to execute script simple-example
[9159] LOADER: OK.
[9159] LOADER: Cleaning up Python interpreter.
[9158] LOADER: Restoring signal handlers
[9158] LOADER: freeing args
[9158] LOADER: returning child exit status 255
[9158] LOADER: Back to parent (RC: 255)
[9158] LOADER: Doing cleanup
[9158] LOADER: Freeing archive status for /home/dan/Projects/gaphor/win-installer/dist/simple-example/simple-example
```
Running with strace gives:
```
[9216] LOADER: Running pyi_rth_gi.py
[9216] LOADER: Running simple-example.py
strace: Process 9217 attached
strace: Process 9218 attached
Gtk-Message: 09:51:29.339: Failed to load module "canberra-gtk-module"
Traceback (most recent call last):
  File "win-installer/simple-example.py", line 8, in <module>
gi.repository.GLib.Error: gdk-pixbuf-error-quark: Couldn’t recognize the image file format for file “temp-icon.png” (3)
[9216] Failed to execute script simple-example
[9216] LOADER: OK.
[9216] LOADER: Cleaning up Python interpreter.
[pid  9218] +++ exited with 255 +++
[pid  9217] +++ exited with 255 +++
[pid  9216] +++ exited with 255 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=9216, si_uid=1000, si_status=255, si_utime=21, si_stime=4} ---
```
I would be glad to submit a PR if someone could help point me in the right direction. @virtuald you helped with the last issue, is this something you could assist with as well?


Discussion:
I was able to resolve this myself. I noticed that there was a warning that I missed:
`7416 WARNING: "hook-gi.repository.GdkPixbuf" ignored, since GDK not found (i.e., "gdk-pixbuf-query-loaders" not in $PATH).`

In Ubuntu and Debian, gdk-pixbuf-query-loaders is not on the PATH. For example on my system it is located `/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders`.

A workaround is to run pyinstaller with:
`$ env PATH="$PATH:/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0" pyinstaller -y simple-example.spec`

I can submit a PR to fix this, any thoughts on how we should find the location of gdk-pixbuf-query-loaders from the hook?

Resolving patch:
diff --git a/PyInstaller/hooks/hook-gi.repository.GdkPixbuf.py b/PyInstaller/hooks/hook-gi.repository.GdkPixbuf.py
--- a/PyInstaller/hooks/hook-gi.repository.GdkPixbuf.py
+++ b/PyInstaller/hooks/hook-gi.repository.GdkPixbuf.py
@@ -15,7 +15,6 @@
 import subprocess
 
 from PyInstaller.config import CONF
-from PyInstaller import compat
 from PyInstaller.compat import (
     exec_command_stdout, is_darwin, is_win, is_linux, open_file, which)
 from PyInstaller.utils.hooks import (
@@ -26,116 +25,139 @@
 destpath = "lib/gdk-pixbuf-2.0/2.10.0/loaders"
 cachedest = "lib/gdk-pixbuf-2.0/2.10.0"
 
-# If the "gdk-pixbuf-query-loaders" command is not in the current ${PATH}, GDK
-# and thus GdkPixbuf is unavailable. Return with a non-fatal warning.
+# If the "gdk-pixbuf-query-loaders" command is not in the current ${PATH}, or
+# is not in the GI lib path, GDK and thus GdkPixbuf is unavailable. Return with
+# a non-fatal warning.
 gdk_pixbuf_query_loaders = None
 
-if compat.architecture == '64bit':
-    # CentOS/Fedora package as -64
-    cmds = ['gdk-pixbuf-query-loaders-64', 'gdk-pixbuf-query-loaders']
-else:
-    cmds = ['gdk-pixbuf-query-loaders']
-
-for cmd in cmds:
-    gdk_pixbuf_query_loaders = which(cmd)
-    if gdk_pixbuf_query_loaders is not None:
-        break
-
-if gdk_pixbuf_query_loaders is None:
+try:
+    libdir = get_gi_libdir('GdkPixbuf', '2.0')
+except ValueError:
     logger.warning(
-        '"hook-gi.repository.GdkPixbuf" ignored, since GDK not found '
-        '(i.e., "gdk-pixbuf-query-loaders" not in $PATH).'
+        '"hook-gi.repository.GdkPixbuf" ignored, '
+        'since GdkPixbuf library not found'
     )
-# Else, GDK is available. Let's do this.
-else:
-    binaries, datas, hiddenimports = get_gi_typelibs('GdkPixbuf', '2.0')
-    datas += collect_glib_translations('gdk-pixbuf')
-
-    libdir = get_gi_libdir('GdkPixbuf', '2.0')
-
-    # To add support for a new platform, add a new "elif" branch below with the
-    # proper is_<platform>() test and glob for finding loaders on that platform.
-    if is_win:
-        ext = "*.dll"
-    elif is_darwin or is_linux:
-        ext = "*.so"
-
-    # If loader detection is supported on this platform, bundle all detected
-    # loaders and an updated loader cache.
-    if ext:
-        loader_libs = []
-
-        # Bundle all found loaders with this user application.
-        pattern = os.path.join(libdir, loaders_path, ext)
-        for f in glob.glob(pattern):
-            binaries.append((f, destpath))
-            loader_libs.append(f)
-
-        # Sometimes the loaders are stored in a different directory from
-        # the library (msys2)
-        if not loader_libs:
-            pattern = os.path.join(libdir, '..', 'lib', loaders_path, ext)
+    libdir = None
+
+if libdir:
+
+    # Distributions either package gdk-pixbuf-query-loaders in the GI libs
+    # directory (not on the path), or on the path with or without a -x64 suffix
+    # depending on the architecture
+    cmds = [
+        os.path.join(libdir, 'gdk-pixbuf-2.0/gdk-pixbuf-query-loaders'),
+        'gdk-pixbuf-query-loaders-64',
+        'gdk-pixbuf-query-loaders',
+    ]
+
+    for cmd in cmds:
+        gdk_pixbuf_query_loaders = which(cmd)
+        if gdk_pixbuf_query_loaders is not None:
+            break
+
+    if gdk_pixbuf_query_loaders is None:
+        logger.warning(
+            '"hook-gi.repository.GdkPixbuf" ignored, since '
+            '"gdk-pixbuf-query-loaders" is not in $PATH or gi lib dir.'
+        )
+
+    # Else, GDK is available. Let's do this.
+    else:
+        binaries, datas, hiddenimports = get_gi_typelibs('GdkPixbuf', '2.0')
+        datas += collect_glib_translations('gdk-pixbuf')
+
+        # To add support for a new platform, add a new "elif" branch below with
+        # the proper is_<platform>() test and glob for finding loaders on that
+        # platform.
+        if is_win:
+            ext = "*.dll"
+        elif is_darwin or is_linux:
+            ext = "*.so"
+
+        # If loader detection is supported on this platform, bundle all
+        # detected loaders and an updated loader cache.
+        if ext:
+            loader_libs = []
+
+            # Bundle all found loaders with this user application.
+            pattern = os.path.join(libdir, loaders_path, ext)
             for f in glob.glob(pattern):
                 binaries.append((f, destpath))
                 loader_libs.append(f)
 
-        # Filename of the loader cache to be written below.
-        cachefile = os.path.join(CONF['workpath'], 'loaders.cache')
-
-        # Run the "gdk-pixbuf-query-loaders" command and capture its standard
-        # output providing an updated loader cache; then write this output to
-        # the loader cache bundled with this frozen application.
-        #
-        # On OSX we use @executable_path to specify a path relative to the
-        # generated bundle. However, on non-Windows we need to rewrite the
-        # loader cache because it isn't relocatable by default. See
-        # https://bugzilla.gnome.org/show_bug.cgi?id=737523
-        #
-        # To make it easier to rewrite, we just always write @executable_path,
-        # since its significantly easier to find/replace at runtime. :)
-        #
-        # If we need to rewrite it...
-        if not is_win:
-            # To permit string munging, decode the encoded bytes output by this
-            # command (i.e., enable the "universal_newlines" option). Note that:
+            # Sometimes the loaders are stored in a different directory from
+            # the library (msys2)
+            if not loader_libs:
+                pattern = os.path.join(libdir, '..', 'lib', loaders_path, ext)
+                for f in glob.glob(pattern):
+                    binaries.append((f, destpath))
+                    loader_libs.append(f)
+
+            # Filename of the loader cache to be written below.
+            cachefile = os.path.join(CONF['workpath'], 'loaders.cache')
+
+            # Run the "gdk-pixbuf-query-loaders" command and capture its
+            # standard output providing an updated loader cache; then write
+            # this output to the loader cache bundled with this frozen
+            # application.
+            #
+            # On OSX we use @executable_path to specify a path relative to the
+            # generated bundle. However, on non-Windows we need to rewrite the
+            # loader cache because it isn't relocatable by default. See
+            # https://bugzilla.gnome.org/show_bug.cgi?id=737523
             #
-            # * Under Python 2.7, "cachedata" will be a decoded "unicode" object.
-            # * Under Python 3.x, "cachedata" will be a decoded "str" object.
+            # To make it easier to rewrite, we just always write
+            # @executable_path, since its significantly easier to find/replace
+            # at runtime. :)
             #
-            # On Fedora, the default loaders cache is /usr/lib64, but the libdir
-            # is actually /lib64. To get around this, we pass the path to the
-            # loader command, and it will create a cache with the right path.
-            cachedata = exec_command_stdout(gdk_pixbuf_query_loaders,
-                                            *loader_libs)
-
-            cd = []
-            prefix = '"' + os.path.join(libdir, 'gdk-pixbuf-2.0', '2.10.0')
-            plen = len(prefix)
-
-            # For each line in the updated loader cache...
-            for line in cachedata.splitlines():
-                if line.startswith('#'):
-                    continue
-                if line.startswith(prefix):
-                    line = '"@executable_path/' + cachedest + line[plen:]
-                cd.append(line)
-
-            # Rejoin these lines in a manner preserving this object's "unicode"
-            # type under Python 2.
-            cachedata = u'\n'.join(cd)
-
-            # Write the updated loader cache to this file.
-            with open_file(cachefile, 'w') as fp:
-                fp.write(cachedata)
-        # Else, GdkPixbuf will do the right thing on Windows, so no changes to
-        # the loader cache are required. For efficiency and reliability, this
-        # command's encoded byte output is written as is without being decoded.
+            # If we need to rewrite it...
+            if not is_win:
+                # To permit string munging, decode the encoded bytes output by
+                # this command (i.e., enable the "universal_newlines" option).
+                # Note that:
+                #
+                # * Under Python 2.7, "cachedata" will be a decoded "unicode"
+                # object. * Under Python 3.x, "cachedata" will be a decoded
+                # "str" object.
+                #
+                # On Fedora, the default loaders cache is /usr/lib64, but the
+                # libdir is actually /lib64. To get around this, we pass the
+                # path to the loader command, and it will create a cache with
+                # the right path.
+                cachedata = exec_command_stdout(gdk_pixbuf_query_loaders,
+                                                *loader_libs)
+
+                cd = []
+                prefix = '"' + os.path.join(libdir, 'gdk-pixbuf-2.0', '2.10.0')
+                plen = len(prefix)
+
+                # For each line in the updated loader cache...
+                for line in cachedata.splitlines():
+                    if line.startswith('#'):
+                        continue
+                    if line.startswith(prefix):
+                        line = '"@executable_path/' + cachedest + line[plen:]
+                    cd.append(line)
+
+                # Rejoin these lines in a manner preserving this object's
+                # "unicode" type under Python 2.
+                cachedata = u'\n'.join(cd)
+
+                # Write the updated loader cache to this file.
+                with open_file(cachefile, 'w') as fp:
+                    fp.write(cachedata)
+            # Else, GdkPixbuf will do the right thing on Windows, so no changes
+            # to the loader cache are required. For efficiency and reliability,
+            # this command's encoded byte output is written as is without being
+            # decoded.
+            else:
+                with open_file(cachefile, 'wb') as fp:
+                    fp.write(subprocess.check_output(gdk_pixbuf_query_loaders))
+
+            # Bundle this loader cache with this frozen application.
+            datas.append((cachefile, cachedest))
+        # Else, loader detection is unsupported on this platform.
         else:
-            with open_file(cachefile, 'wb') as fp:
-                fp.write(subprocess.check_output(gdk_pixbuf_query_loaders))
-
-        # Bundle this loader cache with this frozen application.
-        datas.append((cachefile, cachedest))
-    # Else, loader detection is unsupported on this platform.
-    else:
-        logger.warning('GdkPixbuf loader bundling unsupported on your platform.')
+            logger.warning(
+                'GdkPixbuf loader bundling unsupported on your platform.'
+            )


Test patch: