1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
 
import glob
import os
import shutil
 
from PyInstaller import compat
from PyInstaller.config import CONF  # workpath
from PyInstaller.utils.hooks import get_hook_config, logger
from PyInstaller.utils.hooks.gi import GiModuleInfo, collect_glib_translations
 
LOADERS_PATH = os.path.join('gdk-pixbuf-2.0', '2.10.0', 'loaders')
LOADER_MODULE_DEST_PATH = "lib/gdk-pixbuf/loaders"
LOADER_CACHE_DEST_PATH = "lib/gdk-pixbuf"  # NOTE: some search & replace code depends on / being used on all platforms.
 
 
def _find_gdk_pixbuf_query_loaders_executable(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:
        cmd_fullpath = shutil.which(cmd)
        if cmd_fullpath is not None:
            return cmd_fullpath
 
    return None
 
 
def _collect_loaders(libdir):
    # Assume loader plugins have .so library suffix on all non-Windows platforms
    lib_ext = "*.dll" if compat.is_win else "*.so"
 
    # Find all loaders
    loader_libs = []
    pattern = os.path.join(libdir, LOADERS_PATH, lib_ext)
    for f in glob.glob(pattern):
        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, lib_ext)
        for f in glob.glob(pattern):
            loader_libs.append(f)
 
    return loader_libs
 
 
def _generate_loader_cache(gdk_pixbuf_query_loaders, libdir, loader_libs):
    # 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 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 Mac OS 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 is not 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. :)
    #
    # 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 = compat.exec_command_stdout(gdk_pixbuf_query_loaders, *loader_libs)
 
    output_lines = []
    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/' + LOADER_CACHE_DEST_PATH + line[plen:]
        elif line.startswith(win_prefix):
            line = '"' + LOADER_CACHE_DEST_PATH.replace('/', '\\\\') + line[win_plen:]
        output_lines.append(line)
 
    return '\n'.join(output_lines)
 
 
def hook(hook_api):
    module_info = GiModuleInfo('GdkPixbuf', '2.0')
    if not module_info.available:
        return
 
    binaries, datas, hiddenimports = module_info.collect_typelib_data()
 
    libdir = module_info.get_libdir()
 
    # Collect GdkPixbuf loaders and generate loader cache file
    gdk_pixbuf_query_loaders = _find_gdk_pixbuf_query_loaders_executable(libdir)
    logger.debug("gdk-pixbuf-query-loaders executable: %s", gdk_pixbuf_query_loaders)
    if not gdk_pixbuf_query_loaders:
        logger.warning("gdk-pixbuf-query-loaders executable not found in GI library directory or in PATH!")
    else:
        # Find all GdkPixbuf loader modules
        loader_libs = _collect_loaders(libdir)
 
        # Collect discovered loaders
        for lib in loader_libs:
            binaries.append((lib, LOADER_MODULE_DEST_PATH))
 
        # Generate loader cache; we need to store it to CONF['workpath'] so we can collect it as a data file.
        cachedata = _generate_loader_cache(gdk_pixbuf_query_loaders, libdir, loader_libs)
        cachefile = os.path.join(CONF['workpath'], 'loaders.cache')
        with open(cachefile, 'w') as fp:
            fp.write(cachedata)
        datas.append((cachefile, LOADER_CACHE_DEST_PATH))
 
    # Collect translations
    lang_list = get_hook_config(hook_api, "gi", "languages")
    if gdk_pixbuf_query_loaders:
        datas += collect_glib_translations('gdk-pixbuf', lang_list)
 
    hook_api.add_datas(datas)
    hook_api.add_binaries(binaries)
    hook_api.add_imports(*hiddenimports)