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