PageRenderTime 53ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/PyInstaller/building/imphook.py

https://gitlab.com/132nd-etcher/pyinstaller
Python | 311 lines | 148 code | 30 blank | 133 comment | 19 complexity | 8b84fe09af41927bd722e7bb0cc8d3c9 MD5 | raw file
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2015, 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. #-----------------------------------------------------------------------------
  9. """
  10. Code related to processing of import hooks.
  11. """
  12. import glob
  13. import os.path
  14. import re
  15. import sys
  16. import warnings
  17. from .. import log as logging
  18. from .utils import format_binaries_and_datas
  19. from ..compat import expand_path
  20. from ..compat import importlib_load_source, is_py2
  21. from ..utils.misc import get_code_object
  22. from .imphookapi import PostGraphAPI
  23. from ..lib.modulegraph.modulegraph import GraphError
  24. logger = logging.getLogger(__name__)
  25. class HooksCache(dict):
  26. """
  27. Dictionary mapping from the fully-qualified names of each module hooked by
  28. at least one hook script to lists of the absolute paths of these scripts.
  29. This `dict` subclass caches the list of all hooks applicable to each module,
  30. permitting Pythonic mapping, iteration, addition, and removal of such hooks.
  31. Each dictionary key is a fully-qualified module name. Each dictionary value
  32. is a list of the absolute paths of all hook scripts specific to that module,
  33. including both official PyInstaller hooks and unofficial user-defined hooks.
  34. See Also
  35. ----------
  36. `_load_file_list()`
  37. For details on hook priority.
  38. """
  39. def __init__(self, hooks_dir):
  40. """
  41. Initialize this dictionary.
  42. Parameters
  43. ----------
  44. hook_dir : str
  45. Absolute or relative path of the directory containing hooks with
  46. which to populate this cache. By default, this is the absolute path
  47. of the `PyInstaller/hooks` directory containing official hooks.
  48. """
  49. super(dict, self).__init__()
  50. self._load_file_list(hooks_dir)
  51. def _load_file_list(self, hooks_dir):
  52. """
  53. Cache all hooks in the passed directory.
  54. **Order of caching is significant** with respect to hooks for the same
  55. module, as the values of this dictionary are ordered lists. Hooks for
  56. the same module will be run in the order in which they are cached.
  57. Previously cached hooks are always preserved (rather than overidden).
  58. Specifically, any hook in the passed directory having the same module
  59. name as that of a previously cached hook will be appended to the list of
  60. hooks for that module name. By default, official hooks are cached
  61. _before_ user-defined hooks. For modules with both official and
  62. user-defined hooks, this implies that the former take priority over and
  63. will be run _before_ the latter.
  64. Parameters
  65. ----------
  66. hooks_dir : str
  67. Absolute or relative path of the directory containing additional
  68. hooks to be cached. For convenience, tilde and variable expansion
  69. will be applied to this path (e.g., a leading `~` will be replaced
  70. by the absolute path of the corresponding home directory).
  71. """
  72. # Perform tilde and variable expansion and validate the result.
  73. hooks_dir = expand_path(hooks_dir)
  74. if not os.path.isdir(hooks_dir):
  75. logger.error('Hook directory %r not found',
  76. os.path.abspath(hooks_dir))
  77. return
  78. # For each hook in the passed directory...
  79. hook_files = glob.glob(os.path.join(hooks_dir, 'hook-*.py'))
  80. for hook_file in hook_files:
  81. # Absolute path of this hook's script.
  82. hook_file = os.path.abspath(hook_file)
  83. # Fully-qualified name of this hook's corresponding module,
  84. # constructed by removing the "hook-" prefix and ".py" suffix.
  85. module_name = os.path.basename(hook_file)[5:-3]
  86. # If this module already has cached hooks, append this hook's path
  87. # to the existing list of such paths.
  88. if module_name in self:
  89. self[module_name].append(hook_file)
  90. # Else, default to a new list containing only this hook's path.
  91. else:
  92. self[module_name] = [hook_file]
  93. def add_custom_paths(self, hooks_dirs):
  94. """
  95. Cache all hooks in the list of passed directories.
  96. Parameters
  97. ----------
  98. hooks_dirs : list
  99. List of the absolute or relative paths of all directories containing
  100. additional hooks to be cached.
  101. """
  102. for hooks_dir in hooks_dirs:
  103. self._load_file_list(hooks_dir)
  104. def remove(self, module_names):
  105. """
  106. Remove all key-value pairs whose key is a fully-qualified module name in
  107. the passed list from this dictionary.
  108. Parameters
  109. ----------
  110. module_names : list
  111. List of all fully-qualified module names to be removed.
  112. """
  113. for module_name in set(module_names): # Eliminate duplicate entries.
  114. if module_name in self:
  115. del self[module_name]
  116. class AdditionalFilesCache(object):
  117. """
  118. Cache for storing what binaries and datas were pushed by what modules
  119. when import hooks were processed.
  120. """
  121. def __init__(self):
  122. self._binaries = {}
  123. self._datas = {}
  124. def add(self, modname, binaries, datas):
  125. self._binaries[modname] = binaries or []
  126. self._datas[modname] = datas or []
  127. def __contains__(self, name):
  128. return name in self._binaries or name in self._datas
  129. def binaries(self, modname):
  130. """
  131. Return list of binaries for given module name.
  132. """
  133. return self._binaries[modname]
  134. def datas(self, modname):
  135. """
  136. Return list of datas for given module name.
  137. """
  138. return self._datas[modname]
  139. class ImportHook(object):
  140. """
  141. Class encapsulating processing of hook attributes like hiddenimports, etc.
  142. """
  143. def __init__(self, modname, hook_filename):
  144. """
  145. :param hook_filename: File name where to load hook from.
  146. """
  147. logger.info('Processing hook %s' % os.path.basename(hook_filename))
  148. self._name = modname
  149. self._filename = hook_filename
  150. # _module represents the code of 'hook-modname.py'
  151. # Load hook from file and parse and interpret it's content.
  152. hook_modname = 'PyInstaller_hooks_' + modname.replace('.', '_')
  153. self._module = importlib_load_source(hook_modname, self._filename)
  154. # Public import hook attributes for further processing.
  155. self.binaries = set()
  156. self.datas = set()
  157. # Internal methods for processing.
  158. def _process_hook_function(self, mod_graph):
  159. """
  160. Call the hook function hook(mod).
  161. Function hook(mod) has to be called first because this function
  162. could update other attributes - datas, hiddenimports, etc.
  163. """
  164. # Process a `hook(hook_api)` function.
  165. hook_api = PostGraphAPI(self._name, mod_graph)
  166. self._module.hook(hook_api)
  167. self.datas.update(set(hook_api._added_datas))
  168. self.binaries.update(set(hook_api._added_binaries))
  169. for item in hook_api._added_imports:
  170. self._process_one_hiddenimport(item, mod_graph)
  171. for item in hook_api._deleted_imports:
  172. # Remove the graph link between the hooked module and item.
  173. # This removes the 'item' node from the graph if no other
  174. # links go to it (no other modules import it)
  175. mod_graph.removeReference(hook_api.node, item)
  176. def _process_hiddenimports(self, mod_graph):
  177. """
  178. 'hiddenimports' is a list of Python module names that PyInstaller
  179. is not able detect.
  180. """
  181. # push hidden imports into the graph, as if imported from self._name
  182. for item in self._module.hiddenimports:
  183. self._process_one_hiddenimport(item, mod_graph)
  184. def _process_one_hiddenimport(self, item, mod_graph):
  185. try:
  186. # Do not try to first find out if a module by that name already exist.
  187. # Rely on modulegraph to handle that properly.
  188. # Do not automatically create namespace packages if they do not exist.
  189. caller = mod_graph.findNode(self._name, create_nspkg=False)
  190. mod_graph.import_hook(item, caller=caller)
  191. except ImportError:
  192. # Print warning if a module from hiddenimport could not be found.
  193. # modulegraph raises ImporError when a module is not found.
  194. # Import hook with non-existing hiddenimport is probably a stale hook
  195. # that was not updated for a long time.
  196. logger.warn("Hidden import '%s' not found (probably old hook)" % item)
  197. def _process_excludedimports(self, mod_graph):
  198. """
  199. 'excludedimports' is a list of Python module names that PyInstaller
  200. should not detect as dependency of this module name.
  201. So remove all import-edges from the current module (and it's
  202. submodules) to the given `excludedimports` (end their submodules).
  203. """
  204. def find_all_package_nodes(name):
  205. mods = [name]
  206. name += '.'
  207. for subnode in mod_graph.nodes():
  208. if subnode.identifier.startswith(name):
  209. mods.append(subnode.identifier)
  210. return mods
  211. # Collect all submodules of this module.
  212. hooked_mods = find_all_package_nodes(self._name)
  213. # Collect all dependencies and their submodules
  214. # TODO: Optimize this by using a pattern and walking the graph
  215. # only once.
  216. for item in set(self._module.excludedimports):
  217. excluded_node = mod_graph.findNode(item, create_nspkg=False)
  218. if excluded_node is None:
  219. logger.info("Import to be excluded not found: %r", item)
  220. continue
  221. logger.info("Excluding import %r", item)
  222. imports_to_remove = set(find_all_package_nodes(item))
  223. # Remove references between module nodes, as though they would
  224. # not be imported from 'name'.
  225. # Note: Doing this in a nested loop is less efficient than
  226. # collecting all import to remove first, but log messages
  227. # are easier to understand since related to the "Excluding ..."
  228. # message above.
  229. for src in hooked_mods:
  230. # modules, this `src` does import
  231. references = set(n.identifier for n in mod_graph.getReferences(src))
  232. # Remove all of these imports which are also in `imports_to_remove`
  233. for dest in imports_to_remove & references:
  234. mod_graph.removeReference(src, dest)
  235. logger.warn(" From %s removing import %s", src, dest)
  236. def _process_datas(self, mod_graph):
  237. """
  238. 'datas' is a list of globs of files or
  239. directories to bundle as datafiles. For each
  240. glob, a destination directory is specified.
  241. """
  242. # Find all files and interpret glob statements.
  243. self.datas.update(set(format_binaries_and_datas(self._module.datas)))
  244. def _process_binaries(self, mod_graph):
  245. """
  246. 'binaries' is a list of files to bundle as binaries.
  247. Binaries are special that PyInstaller will check if they
  248. might depend on other dlls (dynamic libraries).
  249. """
  250. self.binaries.update(set(format_binaries_and_datas(self._module.binaries)))
  251. # Public methods
  252. def update_dependencies(self, mod_graph):
  253. """
  254. Update module dependency graph with import hook attributes (hiddenimports, etc.)
  255. :param mod_graph: PyiModuleGraph object to be updated.
  256. """
  257. if hasattr(self._module, 'hook'):
  258. self._process_hook_function(mod_graph)
  259. if hasattr(self._module, 'hiddenimports'):
  260. self._process_hiddenimports(mod_graph)
  261. if hasattr(self._module, 'excludedimports'):
  262. self._process_excludedimports(mod_graph)
  263. if hasattr(self._module, 'datas'):
  264. self._process_datas(mod_graph)
  265. if hasattr(self._module, 'binaries'):
  266. self._process_binaries(mod_graph)