toc_conversion.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. # -----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2022, 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 zipfile
  13. import pkg_resources
  14. from PyInstaller import log as logging
  15. from PyInstaller.building.datastruct import TOC, Tree
  16. from PyInstaller.compat import ALL_SUFFIXES
  17. from PyInstaller.depend.utils import get_path_to_egg
  18. logger = logging.getLogger(__name__)
  19. # create a list of excludes suitable for Tree.
  20. PY_IGNORE_EXTENSIONS = {
  21. *('*' + x for x in ALL_SUFFIXES),
  22. # Exclude EGG-INFO, too, as long as we do not have a way to hold several in one archive.
  23. 'EGG-INFO',
  24. }
  25. class DependencyProcessor:
  26. """
  27. Class to convert final module dependency graph into TOC data structures.
  28. TOC data structures are suitable for creating the final executable.
  29. """
  30. def __init__(self, graph, additional_files):
  31. self._binaries = set()
  32. self._datas = set()
  33. self._distributions = set()
  34. self.__seen_distribution_paths = set()
  35. # Include files that were found by hooks. graph.iter_graph() should include only those modules that are
  36. # reachable from the top-level script.
  37. for node in graph.iter_graph(start=graph._top_script_node):
  38. # Update 'binaries', 'datas'
  39. name = node.identifier
  40. if name in additional_files:
  41. self._binaries.update(additional_files.binaries(name))
  42. self._datas.update(additional_files.datas(name))
  43. # Any module can belong to a single distribution
  44. self._distributions.update(self._get_distribution_for_node(node))
  45. def _get_distribution_for_node(self, node):
  46. """
  47. Get the distribution a module belongs to.
  48. Bug: This currently only handles packages in eggs.
  49. """
  50. # TODO: Modulegraph could flag a module as residing in a zip file
  51. # TODO add support for single modules in eggs (e.g. mock-1.0.1)
  52. # TODO add support for egg-info:
  53. # TODO add support for wheels (dist-info)
  54. #
  55. # TODO add support for unpacked eggs and for new .whl packages.
  56. # Wheels:
  57. # .../site-packages/pip/ # It seams this has to be a package
  58. # .../site-packages/pip-6.1.1.dist-info
  59. # Unzipped Eggs:
  60. # .../site-packages/mock.py # this may be a single module, too!
  61. # .../site-packages/mock-1.0.1-py2.7.egg-info
  62. # Unzipped Eggs (I assume: multiple-versions externally managed):
  63. # .../site-packages/pyPdf-1.13-py2.6.egg/pyPdf/
  64. # .../site-packages/pyPdf-1.13-py2.6.egg/EGG_INFO
  65. # Zipped Egg:
  66. # .../site-packages/zipped.egg/zipped_egg/
  67. # .../site-packages/zipped.egg/EGG_INFO
  68. modpath = node.filename
  69. if not modpath:
  70. # e.g. namespace-package
  71. return []
  72. # TODO: add other ways to get a distribution path
  73. distpath = get_path_to_egg(modpath)
  74. if not distpath or distpath in self.__seen_distribution_paths:
  75. # no egg or already handled
  76. return []
  77. self.__seen_distribution_paths.add(distpath)
  78. dists = list(pkg_resources.find_distributions(distpath))
  79. assert len(dists) == 1
  80. dist = dists[0]
  81. dist._pyinstaller_info = {
  82. 'zipped': zipfile.is_zipfile(dist.location),
  83. 'egg': True, # TODO when supporting other types
  84. 'zip-safe': dist.has_metadata('zip-safe'),
  85. }
  86. return dists
  87. # Public methods.
  88. def make_binaries_toc(self):
  89. # TODO create a real TOC when handling of more files is added.
  90. return [(x, y, 'BINARY') for x, y in self._binaries]
  91. def make_datas_toc(self):
  92. toc = TOC((x, y, 'DATA') for x, y in self._datas)
  93. for dist in self._distributions:
  94. if (
  95. dist._pyinstaller_info['egg'] and not dist._pyinstaller_info['zipped']
  96. and not dist._pyinstaller_info['zip-safe']
  97. ):
  98. # this is a un-zipped, not-zip-safe egg
  99. toplevel = dist.get_metadata('top_level.txt').strip()
  100. basedir = dist.location
  101. if toplevel:
  102. os.path.join(basedir, toplevel)
  103. tree = Tree(dist.location, excludes=PY_IGNORE_EXTENSIONS)
  104. toc.extend(tree)
  105. return toc
  106. def make_zipfiles_toc(self):
  107. # TODO create a real TOC when handling of more files is added.
  108. toc = []
  109. for dist in self._distributions:
  110. if dist._pyinstaller_info['zipped'] and not dist._pyinstaller_info['egg']:
  111. # Hmm, this should never happen as normal zip-files are not associated with a distribution, are they?
  112. toc.append(("eggs/" + os.path.basename(dist.location), dist.location, 'ZIPFILE'))
  113. return toc
  114. @staticmethod
  115. def __collect_data_files_from_zip(zipfilename):
  116. # 'PyInstaller.config' cannot be imported as other top-level modules.
  117. from PyInstaller.config import CONF
  118. workpath = os.path.join(CONF['workpath'], os.path.basename(zipfilename))
  119. try:
  120. os.makedirs(workpath)
  121. except OSError as e:
  122. import errno
  123. if e.errno != errno.EEXIST:
  124. raise
  125. # TODO: extract only those file which would then be included
  126. with zipfile.ZipFile(zipfilename) as zfh:
  127. zfh.extractall(workpath)
  128. return Tree(workpath, excludes=PY_IGNORE_EXTENSIONS)
  129. def make_zipped_data_toc(self):
  130. toc = TOC()
  131. logger.debug('Looking for egg data files...')
  132. for dist in self._distributions:
  133. if dist._pyinstaller_info['egg']:
  134. # TODO: check in docs if top_level.txt always exists
  135. toplevel = dist.get_metadata('top_level.txt').strip()
  136. if dist._pyinstaller_info['zipped']:
  137. # this is a zipped egg
  138. tree = self.__collect_data_files_from_zip(dist.location)
  139. toc.extend(tree)
  140. elif dist._pyinstaller_info['zip-safe']:
  141. # this is an un-zipped, zip-safe egg
  142. basedir = dist.location
  143. if toplevel:
  144. os.path.join(basedir, toplevel)
  145. tree = Tree(dist.location, excludes=PY_IGNORE_EXTENSIONS)
  146. toc.extend(tree)
  147. else:
  148. # this is an un-zipped, not-zip-safe egg, handled in make_datas_toc()
  149. pass
  150. return toc