gi.py 9.8 KB

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