hook-gi.repository.GdkPixbuf.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2023, 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 glob
  12. import os
  13. import shutil
  14. import subprocess
  15. from PyInstaller import compat
  16. from PyInstaller.config import CONF # workpath
  17. from PyInstaller.utils.hooks import get_hook_config, logger
  18. from PyInstaller.utils.hooks.gi import GiModuleInfo, collect_glib_translations
  19. LOADERS_PATH = os.path.join('gdk-pixbuf-2.0', '2.10.0', 'loaders')
  20. LOADER_MODULE_DEST_PATH = "lib/gdk-pixbuf/loaders"
  21. LOADER_CACHE_DEST_PATH = "lib/gdk-pixbuf" # NOTE: some search & replace code depends on / being used on all platforms.
  22. def _find_gdk_pixbuf_query_loaders_executable(libdir):
  23. # Distributions either package gdk-pixbuf-query-loaders in the GI libs directory (not on the path), or on the path
  24. # with or without a -x64 suffix, depending on the architecture.
  25. cmds = [
  26. os.path.join(libdir, 'gdk-pixbuf-2.0', 'gdk-pixbuf-query-loaders'),
  27. 'gdk-pixbuf-query-loaders-64',
  28. 'gdk-pixbuf-query-loaders',
  29. ]
  30. for cmd in cmds:
  31. cmd_fullpath = shutil.which(cmd)
  32. if cmd_fullpath is not None:
  33. return cmd_fullpath
  34. return None
  35. def _collect_loaders(libdir):
  36. # Assume loader plugins have .so library suffix on all non-Windows platforms
  37. lib_ext = "*.dll" if compat.is_win else "*.so"
  38. # Find all loaders
  39. loader_libs = []
  40. pattern = os.path.join(libdir, LOADERS_PATH, lib_ext)
  41. for f in glob.glob(pattern):
  42. loader_libs.append(f)
  43. # Sometimes the loaders are stored in a different directory from the library (msys2)
  44. if not loader_libs:
  45. pattern = os.path.abspath(os.path.join(libdir, '..', 'lib', LOADERS_PATH, lib_ext))
  46. for f in glob.glob(pattern):
  47. loader_libs.append(f)
  48. return loader_libs
  49. def _generate_loader_cache(gdk_pixbuf_query_loaders, libdir, loader_libs):
  50. # Run the "gdk-pixbuf-query-loaders" command and capture its standard output providing an updated loader
  51. # cache; then write this output to the loader cache bundled with this frozen application. On all platforms,
  52. # we also move the package structure to point to lib/gdk-pixbuf instead of lib/gdk-pixbuf-2.0/2.10.0 in
  53. # order to make compatible with macOS .app bundle signing.
  54. #
  55. # On macOS we use @executable_path to specify a path relative to the generated bundle. However, on
  56. # non-Windows, we need to rewrite the loader cache because it is not relocatable by default. See
  57. # https://bugzilla.gnome.org/show_bug.cgi?id=737523
  58. #
  59. # To make it easier to rewrite, we just always write @executable_path, since its significantly easier to
  60. # find/replace at runtime. :)
  61. #
  62. # To permit string munging, decode the encoded bytes output by this command (i.e., enable the
  63. # "universal_newlines" option).
  64. #
  65. # On Fedora, the default loaders cache is /usr/lib64, but the libdir is actually /lib64. To get around this,
  66. # we pass the path to the loader command, and it will create a cache with the right path.
  67. #
  68. # On Windows, the loaders lib directory is relative, starts with 'lib', and uses \\ as path separators
  69. # (escaped \).
  70. cachedata = subprocess.run([gdk_pixbuf_query_loaders, *loader_libs],
  71. check=True,
  72. stdout=subprocess.PIPE,
  73. encoding='utf-8').stdout
  74. output_lines = []
  75. prefix = '"' + os.path.join(libdir, 'gdk-pixbuf-2.0', '2.10.0')
  76. plen = len(prefix)
  77. win_prefix = '"' + '\\\\'.join(['lib', 'gdk-pixbuf-2.0', '2.10.0'])
  78. win_plen = len(win_prefix)
  79. msys2_prefix = '"' + os.path.abspath(os.path.join(libdir, '..', 'lib', 'gdk-pixbuf-2.0', '2.10.0'))
  80. msys2_plen = len(msys2_prefix)
  81. # For each line in the updated loader cache...
  82. for line in cachedata.splitlines():
  83. if line.startswith('#'):
  84. continue
  85. if line.startswith(prefix):
  86. line = '"@executable_path/' + LOADER_CACHE_DEST_PATH + line[plen:]
  87. elif line.startswith(win_prefix):
  88. line = '"' + LOADER_CACHE_DEST_PATH.replace('/', '\\\\') + line[win_plen:]
  89. elif line.startswith(msys2_prefix):
  90. line = ('"' + LOADER_CACHE_DEST_PATH + line[msys2_plen:]).replace('/', '\\\\')
  91. output_lines.append(line)
  92. return '\n'.join(output_lines)
  93. def hook(hook_api):
  94. module_info = GiModuleInfo('GdkPixbuf', '2.0')
  95. if not module_info.available:
  96. return
  97. binaries, datas, hiddenimports = module_info.collect_typelib_data()
  98. libdir = module_info.get_libdir()
  99. # Collect GdkPixbuf loaders and generate loader cache file
  100. gdk_pixbuf_query_loaders = _find_gdk_pixbuf_query_loaders_executable(libdir)
  101. logger.debug("gdk-pixbuf-query-loaders executable: %s", gdk_pixbuf_query_loaders)
  102. if not gdk_pixbuf_query_loaders:
  103. logger.warning("gdk-pixbuf-query-loaders executable not found in GI library directory or in PATH!")
  104. else:
  105. # Find all GdkPixbuf loader modules
  106. loader_libs = _collect_loaders(libdir)
  107. # Collect discovered loaders
  108. for lib in loader_libs:
  109. binaries.append((lib, LOADER_MODULE_DEST_PATH))
  110. # Generate loader cache; we need to store it to CONF['workpath'] so we can collect it as a data file.
  111. cachedata = _generate_loader_cache(gdk_pixbuf_query_loaders, libdir, loader_libs)
  112. cachefile = os.path.join(CONF['workpath'], 'loaders.cache')
  113. with open(cachefile, 'w', encoding='utf-8') as fp:
  114. fp.write(cachedata)
  115. datas.append((cachefile, LOADER_CACHE_DEST_PATH))
  116. # Collect translations
  117. lang_list = get_hook_config(hook_api, "gi", "languages")
  118. if gdk_pixbuf_query_loaders:
  119. datas += collect_glib_translations('gdk-pixbuf', lang_list)
  120. hook_api.add_datas(datas)
  121. hook_api.add_binaries(binaries)
  122. hook_api.add_imports(*hiddenimports)