gi.py 9.2 KB

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