Repository: pyinstaller/pyinstaller

Issue:
Unable to Code Sign macOS App Built with GTK
## Description of the issue
GTK apps that are packaged with PyInstaller have a gdk-pixbuf-2.0 package in the Contents/Resources/MacOS/lib directory. The period in the name makes Apple's codesign utility thinks that it is a bundle and the signing fails.

According to the [Apple docs](https://developer.apple.com/library/archive/technotes/tn2206/_index.html#//apple_ref/doc/uid/DTS40007919-CH1-TNTAG201), these types of files should be the Resources directory.

Issue #3680 came up with a work around to create symlinks between MacOS and Resources. This solution is posted on the wiki as a [OSX Code Signing recipe for Qt apps](https://github.com/pyinstaller/pyinstaller/wiki/Recipe-OSX-Code-Signing-Qt). I wasn't able to get this to work for a GTK app, probably because the package structure is slightly different. 

## Context information (for bug reports)

* Output of `pyinstaller --version`: 4.1
* Version of Python: 3.9.1
* Platform: macOS
* Did you also try this on another platform? Does it work there? N/A

* try the latest development version, using the following command:

```shell
pip install https://github.com/pyinstaller/pyinstaller/archive/develop.zip
```

* follow *all* the instructions in our "If Things Go Wrong" Guide
  (https://github.com/pyinstaller/pyinstaller/wiki/If-Things-Go-Wrong) and

### Make sure [everything is packaged correctly](https://github.com/pyinstaller/pyinstaller/wiki/How-to-Report-Bugs#make-sure-everything-is-packaged-correctly)

  * [X] start with clean installation
  * [X] use the latest development version
  * [X] Run your frozen program **from a command window (shell)** — instead of double-clicking on it
  * [X] Package your program in **--onedir mode**
  * [X] Package **without UPX**, say: use the option `--noupx` or set `upx=False` in your .spec-file
  * [X] Repackage you application in **verbose/debug mode**. For this, pass the option `--debug` to `pyi-makespec` or `pyinstaller` or use `EXE(..., debug=1, ...)` in your .spec file.


### A minimal example program which shows the error

```
(paste text here)
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk


window = Gtk.Window(title="Hello World")
window.show()
window.connect("destroy", Gtk.main_quit)
Gtk.main()
```

### Stacktrace / full error message

```
(.venv) danielyeaw@Daniels-MacBook-Pro gtk-helloworld % ls -R dist/helloworld.app/Contents/MacOS/lib                              
gdk-pixbuf-2.0	python3.9

dist/helloworld.app/Contents/MacOS/lib/gdk-pixbuf-2.0:
2.10.0

dist/helloworld.app/Contents/MacOS/lib/gdk-pixbuf-2.0/2.10.0:
loaders		loaders.cache

dist/helloworld.app/Contents/MacOS/lib/gdk-pixbuf-2.0/2.10.0/loaders:
libpixbufloader-ani.so	libpixbufloader-jpeg.so	libpixbufloader-tga.so
libpixbufloader-bmp.so	libpixbufloader-png.so	libpixbufloader-tiff.so
libpixbufloader-gif.so	libpixbufloader-pnm.so	libpixbufloader-xbm.so
libpixbufloader-icns.so	libpixbufloader-qtif.so	libpixbufloader-xpm.so
libpixbufloader-ico.so	libpixbufloader-svg.so
```

I would like to use this issue to collect feedback on whether we can work to have PyInstaller place these types of files for GTK, QT, and others in the Resources directory. I can help by creating a PR once we agree on a potential solution.



Discussion:
@BoboTiG and @cbenhagen since you helped develop the initial QT workaround, do you have any input on a more proper solution?

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
@@ -14,7 +14,6 @@
 
 import glob
 import os
-import subprocess
 from shutil import which
 
 from PyInstaller.config import CONF
@@ -25,8 +24,8 @@
 
 loaders_path = os.path.join('gdk-pixbuf-2.0', '2.10.0', 'loaders')
 
-destpath = "lib/gdk-pixbuf-2.0/2.10.0/loaders"
-cachedest = "lib/gdk-pixbuf-2.0/2.10.0"
+destpath = "lib/gdk-pixbuf/loaders"
+cachedest = "lib/gdk-pixbuf"
 
 # 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
@@ -102,7 +101,9 @@
             # 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.
+            # application. On all platforms, we also move the package structure
+            # to point to lib/gdk-pixbuf instead of lib/gdk-pixbuf-2.0/2.10.0
+            # in order to make compatible for OSX application signing.
             #
             # On OSX we use @executable_path to specify a path relative to the
             # generated bundle. However, on non-Windows we need to rewrite the
@@ -113,43 +114,42 @@
             # @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:
-                #
-                # 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)
-
-                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))
+            # To permit string munging, decode the encoded bytes output by
+            # this command (i.e., enable the "universal_newlines" option).
+            #
+            # 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.
+            #
+            # On Windows, the loaders lib directory is relative, starts with
+            # 'lib', and uses \\ as path separators (escaped \).
+            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)
+
+            win_prefix = '"' + '\\\\'.join(['lib', 'gdk-pixbuf-2.0', '2.10.0'])
+            win_plen = len(win_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:]
+                elif line.startswith(win_prefix):
+                    line = '"' + cachedest.replace(
+                        '/', '\\\\') + line[win_plen:]
+                cd.append(line)
+
+            cachedata = '\n'.join(cd)
+
+            # Write the updated loader cache to this file.
+            with open_file(cachefile, 'w') as fp:
+                fp.write(cachedata)
 
             # Bundle this loader cache with this frozen application.
             datas.append((cachefile, cachedest))
diff --git a/PyInstaller/hooks/rthooks/pyi_rth_gdkpixbuf.py b/PyInstaller/hooks/rthooks/pyi_rth_gdkpixbuf.py
--- a/PyInstaller/hooks/rthooks/pyi_rth_gdkpixbuf.py
+++ b/PyInstaller/hooks/rthooks/pyi_rth_gdkpixbuf.py
@@ -14,8 +14,7 @@
 import tempfile
 import sys
 
-pixbuf_file = os.path.join(sys._MEIPASS, 'lib', 'gdk-pixbuf-2.0', '2.10.0',
-                           'loaders.cache')
+pixbuf_file = os.path.join(sys._MEIPASS, 'lib', 'gdk-pixbuf', 'loaders.cache')
 
 # If we're not on Windows we need to rewrite the cache
 # -> we rewrite on OSX to support --onefile mode


Test patch: