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