1# ----------------------------------------------------------------------------
2# Copyright (c) 2005-2019, PyInstaller Development Team.
3#
4# Distributed under the terms of the GNU General Public License with exception
5# for distributing bootloader.
6#
7# The full license is in the file COPYING.txt, distributed with this software.
8# ----------------------------------------------------------------------------
9import os
10import re
11
12from ..hooks import collect_submodules, collect_system_data_files, eval_statement, exec_statement
13from ... import log as logging
14from ...compat import base_prefix, is_darwin, is_win, open_file, \
15    text_read_mode, text_type
16from ...depend.bindepend import findSystemLibrary
17
18logger = logging.getLogger(__name__)
19
20
21def get_typelibs(module, version):
22    """deprecated; only here for backwards compat.
23    """
24    logger.warning("get_typelibs is deprecated, use get_gi_typelibs instead")
25    return get_gi_typelibs(module, version)[1]
26
27
28def get_gi_libdir(module, version):
29    statement = """
30        import gi
31        gi.require_version("GIRepository", "2.0")
32        from gi.repository import GIRepository
33        repo = GIRepository.Repository.get_default()
34        module, version = (%r, %r)
35        repo.require(module, version,
36                     GIRepository.RepositoryLoadFlags.IREPOSITORY_LOAD_FLAG_LAZY)
37        print(repo.get_shared_library(module))
38    """
39    statement %= (module, version)
40    libs = exec_statement(statement).split(',')
41    for lib in libs:
42        path = findSystemLibrary(lib.strip())
43        return os.path.normpath(os.path.dirname(path))
44
45    raise ValueError("Could not find libdir for %s-%s" % (module, version))
46
47
48def get_gi_typelibs(module, version):
49    """
50    Return a tuple of (binaries, datas, hiddenimports) to be used by PyGObject
51    related hooks. Searches for and adds dependencies recursively.
52
53    :param module: GI module name, as passed to 'gi.require_version()'
54    :param version: GI module version, as passed to 'gi.require_version()'
55    """
56    datas = []
57    binaries = []
58    hiddenimports = []
59
60    statement = """
61        import gi
62        gi.require_version("GIRepository", "2.0")
63        from gi.repository import GIRepository
64        repo = GIRepository.Repository.get_default()
65        module, version = (%r, %r)
66        repo.require(module, version,
67                     GIRepository.RepositoryLoadFlags.IREPOSITORY_LOAD_FLAG_LAZY)
68        get_deps = getattr(repo, 'get_immediate_dependencies', None)
69        if not get_deps:
70            get_deps = repo.get_dependencies
71        print({'sharedlib': repo.get_shared_library(module),
72               'typelib': repo.get_typelib_path(module),
73               'deps': get_deps(module) or []})
74    """
75    statement %= (module, version)
76    typelibs_data = eval_statement(statement)
77    if not typelibs_data:
78        logger.error("gi repository 'GIRepository 2.0' not found. "
79                     "Please make sure libgirepository-gir2.0 resp. "
80                     "lib64girepository-gir2.0 is installed.")
81        # :todo: should we raise a SystemError here?
82    else:
83        logger.debug("Adding files for %s %s", module, version)
84
85        if typelibs_data['sharedlib']:
86            for lib in typelibs_data['sharedlib'].split(','):
87                path = findSystemLibrary(lib.strip())
88                if path:
89                    logger.debug('Found shared library %s at %s', lib, path)
90                    binaries.append((path, '.'))
91
92        d = gir_library_path_fix(typelibs_data['typelib'])
93        if d:
94            logger.debug('Found gir typelib at %s', d)
95            datas.append(d)
96
97        hiddenimports += collect_submodules('gi.overrides',
98                           lambda name: name.endswith('.' + module))
99
100        # Load dependencies recursively
101        for dep in typelibs_data['deps']:
102            m, _ = dep.rsplit('-', 1)
103            hiddenimports += ['gi.repository.%s' % m]
104
105    return binaries, datas, hiddenimports
106
107
108def gir_library_path_fix(path):
109    import subprocess
110    # 'PyInstaller.config' cannot be imported as other top-level modules.
111    from ...config import CONF
112
113    path = os.path.abspath(path)
114
115    # On OSX we need to recompile the GIR files to reference the loader path,
116    # but this is not necessary on other platforms
117    if is_darwin:
118
119        # If using a virtualenv, the base prefix and the path of the typelib
120        # have really nothing to do with each other, so try to detect that
121        common_path = os.path.commonprefix([base_prefix, path])
122        if common_path == '/':
123            logger.debug("virtualenv detected? fixing the gir path...")
124            common_path = os.path.abspath(os.path.join(path, '..', '..', '..'))
125
126        gir_path = os.path.join(common_path, 'share', 'gir-1.0')
127
128        typelib_name = os.path.basename(path)
129        gir_name = os.path.splitext(typelib_name)[0] + '.gir'
130
131        gir_file = os.path.join(gir_path, gir_name)
132
133        if not os.path.exists(gir_path):
134            logger.error('Unable to find gir directory: %s.\n'
135                         'Try installing your platforms gobject-introspection '
136                         'package.', gir_path)
137            return None
138        if not os.path.exists(gir_file):
139            logger.error('Unable to find gir file: %s.\n'
140                         'Try installing your platforms gobject-introspection '
141                         'package.', gir_file)
142            return None
143
144        with open_file(gir_file, text_read_mode, encoding='utf-8') as f:
145            lines = f.readlines()
146        # GIR files are `XML encoded <https://developer.gnome.org/gi/stable/gi-gir-reference.html>`_,
147        # which means they are by definition encoded using UTF-8.
148        with open_file(os.path.join(CONF['workpath'], gir_name), 'w',
149                       encoding='utf-8') as f:
150            for line in lines:
151                if 'shared-library' in line:
152                    split = re.split('(=)', line)
153                    files = re.split('(["|,])', split[2])
154                    for count, item in enumerate(files):
155                        if 'lib' in item:
156                            files[count] = '@loader_path/' + os.path.basename(item)
157                    line = ''.join(split[0:2]) + ''.join(files)
158                f.write(text_type(line))
159
160        # g-ir-compiler expects a file so we cannot just pipe the fixed file to it.
161        command = subprocess.Popen(('g-ir-compiler', os.path.join(CONF['workpath'], gir_name),
162                                    '-o', os.path.join(CONF['workpath'], typelib_name)))
163        command.wait()
164
165        return os.path.join(CONF['workpath'], typelib_name), 'gi_typelibs'
166    else:
167        return path, 'gi_typelibs'
168
169
170def get_glib_system_data_dirs():
171    statement = """
172        import gi
173        gi.require_version('GLib', '2.0')
174        from gi.repository import GLib
175        print(GLib.get_system_data_dirs())
176    """
177    data_dirs = eval_statement(statement)
178    if not data_dirs:
179        logger.error("gi repository 'GIRepository 2.0' not found. "
180                     "Please make sure libgirepository-gir2.0 resp. "
181                     "lib64girepository-gir2.0 is installed.")
182        # :todo: should we raise a SystemError here?
183    return data_dirs
184
185
186def get_glib_sysconf_dirs():
187    """Try to return the sysconf directories, eg /etc."""
188    if is_win:
189        # On windows, if you look at gtkwin32.c, sysconfdir is actually
190        # relative to the location of the GTK DLL. Since that's what
191        # we're actually interested in (not the user path), we have to
192        # do that the hard way'''
193        return [os.path.join(get_gi_libdir('GLib', '2.0'), 'etc')]
194
195    statement = """
196        import gi
197        gi.require_version('GLib', '2.0')
198        from gi.repository import GLib
199        print(GLib.get_system_config_dirs())
200    """
201    data_dirs = eval_statement(statement)
202    if not data_dirs:
203        logger.error("gi repository 'GIRepository 2.0' not found. "
204                     "Please make sure libgirepository-gir2.0 resp. "
205                     "lib64girepository-gir2.0 is installed.")
206        # :todo: should we raise a SystemError here?
207    return data_dirs
208
209
210def collect_glib_share_files(*path):
211    """path is relative to the system data directory (eg, /usr/share)"""
212    glib_data_dirs = get_glib_system_data_dirs()
213    if glib_data_dirs is None:
214        return []
215
216    destdir = os.path.join('share', *path[:-1])
217
218    # TODO: will this return too much?
219    collected = []
220    for data_dir in glib_data_dirs:
221        p = os.path.join(data_dir, *path)
222        collected += collect_system_data_files(p, destdir=destdir, include_py_files=False)
223
224    return collected
225
226
227def collect_glib_etc_files(*path):
228    """path is relative to the system config directory (eg, /etc)"""
229    glib_config_dirs = get_glib_sysconf_dirs()
230    if glib_config_dirs is None:
231        return []
232
233    destdir = os.path.join('etc', *path[:-1])
234
235    # TODO: will this return too much?
236    collected = []
237    for config_dir in glib_config_dirs:
238        p = os.path.join(config_dir, *path)
239        collected += collect_system_data_files(p, destdir=destdir, include_py_files=False)
240
241    return collected
242
243_glib_translations = None
244
245
246def collect_glib_translations(prog):
247    """
248    Return a list of translations in the system locale directory whose names equal prog.mo.
249    """
250    global _glib_translations
251    if _glib_translations is None:
252        _glib_translations = collect_glib_share_files('locale')
253
254    names = [os.sep + prog + '.mo',
255             os.sep + prog + '.po']
256    namelen = len(names[0])
257
258    return [(src, dst) for src, dst in _glib_translations if src[-namelen:] in names]
259
260__all__ = ('get_typelibs', 'get_gi_libdir', 'get_gi_typelibs', 'gir_library_path_fix', 'get_glib_system_data_dirs',
261           'get_glib_sysconf_dirs', 'collect_glib_share_files', 'collect_glib_etc_files', 'collect_glib_translations')
262