toc_conversion.py 6.9 KB

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