1#-----------------------------------------------------------------------------
2# Copyright (c) 2005-2019, 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"""
11Define a modified ModuleGraph that can return its contents as
12a TOC and in other ways act like the old ImpTracker.
13TODO: This class, along with TOC and Tree should be in a separate module.
14
15For reference, the ModuleGraph node types and their contents:
16
17  nodetype       identifier       filename
18
19 Script         full path to .py   full path to .py
20 SourceModule     basename         full path to .py
21 BuiltinModule    basename         None
22 CompiledModule   basename         full path to .pyc
23 Extension        basename         full path to .so
24 MissingModule    basename         None
25 Package          basename         full path to __init__.py
26        packagepath is ['path to package']
27        globalnames is set of global names __init__.py defines
28
29The main extension here over ModuleGraph is a method to extract nodes
30from the flattened graph and return them as a TOC, or added to a TOC.
31Other added methods look up nodes by identifier and return facts
32about them, replacing what the old ImpTracker list could do.
33"""
34
35from __future__ import print_function
36
37import os
38import re
39import sys
40import traceback
41
42from .. import HOMEPATH, configure
43from .. import log as logging
44from ..log import INFO, DEBUG, TRACE
45from ..building.datastruct import TOC
46from ..building.imphook import HooksCache
47from ..building.imphookapi import PreSafeImportModuleAPI, PreFindModulePathAPI
48from ..compat import importlib_load_source, is_py2, PY3_BASE_MODULES,\
49        PURE_PYTHON_MODULE_TYPES, BINARY_MODULE_TYPES, VALID_MODULE_TYPES, \
50        BAD_MODULE_TYPES, MODULE_TYPES_TO_TOC_DICT
51from ..lib.modulegraph.find_modules import get_implies
52from ..lib.modulegraph.modulegraph import ModuleGraph
53from ..utils.hooks import collect_submodules, is_package
54from ..utils.misc import load_py_data_struct
55
56logger = logging.getLogger(__name__)
57
58
59class PyiModuleGraph(ModuleGraph):
60    """
61    Directed graph whose nodes represent modules and edges represent
62    dependencies between these modules.
63
64    This high-level subclass wraps the lower-level `ModuleGraph` class with
65    support for graph and runtime hooks. While each instance of `ModuleGraph`
66    represents a set of disconnected trees, each instance of this class *only*
67    represents a single connected tree whose root node is the Python script
68    originally passed by the user on the command line. For that reason, while
69    there may (and typically do) exist more than one `ModuleGraph` instance,
70    there typically exists only a singleton instance of this class.
71
72    Attributes
73    ----------
74    _hooks_pre_find_module_path : HooksCache
75        Dictionary mapping the fully-qualified names of all modules with
76        pre-find module path hooks to the absolute paths of such hooks. See the
77        the `_find_module_path()` method for details.
78    _hooks_pre_safe_import_module : HooksCache
79        Dictionary mapping the fully-qualified names of all modules with
80        pre-safe import module hooks to the absolute paths of such hooks. See
81        the `_safe_import_module()` method for details.
82    _user_hook_dirs : list
83        List of the absolute paths of all directories containing user-defined
84        hooks for the current application.
85    """
86
87    # Note: these levels are completely arbitrary and may be adjusted if needed.
88    LOG_LEVEL_MAPPING = {0: INFO, 1: DEBUG, 2: TRACE, 3: TRACE, 4: TRACE}
89
90    def __init__(self, pyi_homepath, user_hook_dirs=None, *args, **kwargs):
91        super(PyiModuleGraph, self).__init__(*args, **kwargs)
92        # Homepath to the place where is PyInstaller located.
93        self._homepath = pyi_homepath
94        # modulegraph Node for the main python script that is analyzed
95        # by PyInstaller.
96        self._top_script_node = None
97
98        # Absolute paths of all user-defined hook directories.
99        self._user_hook_dirs = \
100            user_hook_dirs if user_hook_dirs is not None else []
101
102        # Hook-specific lookup tables, defined after defining "_user_hook_dirs".
103        logger.info('Initializing module graph hooks...')
104        self._hooks_pre_safe_import_module = self._cache_hooks('pre_safe_import_module')
105        self._hooks_pre_find_module_path = self._cache_hooks('pre_find_module_path')
106        self._available_rthooks = load_py_data_struct(
107            os.path.join(self._homepath, 'PyInstaller', 'loader', 'rthooks.dat')
108        )
109
110    @staticmethod
111    def _findCaller(*args, **kwargs):
112        # Used to add an additional stack-frame above logger.findCaller.
113        # findCaller expects the caller to be three stack-frames above itself.
114        return logger.findCaller(*args, **kwargs)
115
116    def msg(self, level, s, *args):
117        """
118        Print a debug message with the given level.
119
120        1. Map the msg log level to a logger log level.
121        2. Generate the message format (the same format as ModuleGraph)
122        3. Find the caller, which findCaller expects three stack-frames above
123           itself:
124            [3] caller -> [2] msg (here) -> [1] _findCaller -> [0] logger.findCaller
125        4. Create a logRecord with the caller's information.
126        5. Handle the logRecord.
127        """
128        try:
129            level = self.LOG_LEVEL_MAPPING[level]
130        except KeyError:
131            return
132        if not logger.isEnabledFor(level):
133            return
134
135        msg = "%s %s" % (s, ' '.join(map(repr, args)))
136
137        if is_py2:
138            # Python 2 does not have 'sinfo'
139            try:
140                fn, lno, func = self._findCaller()
141            except ValueError:  # pragma: no cover
142                fn, lno, func = "(unknown file)", 0, "(unknown function)"
143            record = logger.makeRecord(
144                logger.name, level, fn, lno, msg, [], None, func, None)
145        else:
146            try:
147                fn, lno, func, sinfo = self._findCaller()
148            except ValueError:  # pragma: no cover
149                fn, lno, func, sinfo = "(unknown file)", 0, "(unknown function)", None
150            record = logger.makeRecord(
151                logger.name, level, fn, lno, msg, [], None, func, None, sinfo)
152
153        logger.handle(record)
154
155    # Set logging methods so that the stack is correctly detected.
156    msgin = msg
157    msgout = msg
158
159    def _cache_hooks(self, hook_type):
160        """
161        Get a cache of all hooks of the passed type.
162
163        The cache will include all official hooks defined by the PyInstaller
164        codebase _and_ all unofficial hooks defined for the current application.
165
166        Parameters
167        ----------
168        hook_type : str
169            Type of hooks to be cached, equivalent to the basename of the
170            subpackage of the `PyInstaller.hooks` package containing such hooks
171            (e.g., `post_create_package` for post-create package hooks).
172        """
173        # Absolute path of this type hook package's directory.
174        system_hook_dir = configure.get_importhooks_dir(hook_type)
175
176        # Cache of such hooks.
177        # logger.debug("Caching system %s hook dir %r" % (hook_type, system_hook_dir))
178        hooks_cache = HooksCache(system_hook_dir)
179        for user_hook_dir in self._user_hook_dirs:
180            # Absolute path of the user-defined subdirectory of this hook type.
181            user_hook_type_dir = os.path.join(user_hook_dir, hook_type)
182
183            # If this directory exists, cache all hooks in this directory.
184            if os.path.isdir(user_hook_type_dir):
185                # logger.debug("Caching user %s hook dir %r" % (hook_type, hooks_user_dir))
186                hooks_cache.add_custom_paths([user_hook_type_dir])
187
188        return hooks_cache
189
190    def run_script(self, pathname, caller=None):
191        """
192        Wrap the parent's 'run_script' method and create graph from the first
193        script in the analysis, and save its node to use as the "caller" node
194        for all others. This gives a connected graph rather than a collection
195        of unrelated trees,
196        """
197        if self._top_script_node is None:
198            nodes_without_parent = [x for x in self.flatten()]
199            # Remember the node for the first script.
200            try:
201                self._top_script_node = super(PyiModuleGraph, self).run_script(pathname)
202            except SyntaxError as e:
203                print("\nSyntax error in", pathname, file=sys.stderr)
204                formatted_lines = traceback.format_exc().splitlines(True)
205                print(*formatted_lines[-4:], file=sys.stderr)
206                raise SystemExit(1)
207            # Create references from top_script to current modules in graph.
208            # These modules without parents are dependencies that are necessary
209            # for base_library.zip.
210            for node in nodes_without_parent:
211                self.createReference(self._top_script_node, node)
212            # Return top-level script node.
213            return self._top_script_node
214        else:
215            if not caller:
216                # Defaults to as any additional script is called from the top-level
217                # script.
218                caller = self._top_script_node
219            return super(PyiModuleGraph, self).run_script(pathname, caller=caller)
220
221    def _safe_import_module(self, module_basename, module_name, parent_package):
222        """
223        Create a new graph node for the module with the passed name under the
224        parent package signified by the passed graph node.
225
226        This method wraps the superclass method with support for pre-import
227        module hooks. If such a hook exists for this module (e.g., a script
228        `PyInstaller.hooks.hook-{module_name}` containing a function
229        `pre_safe_import_module()`), that hook will be run _before_ the
230        superclass method is called.
231
232        Pre-Safe-Import-Hooks are performed just *prior* to importing
233        the module. When running the hook, the modules parent package
234        has already been imported and ti's `__path__` is set up. But
235        the module is just about to be imported.
236
237        See the superclass method for description of parameters and
238        return value.
239        """
240        # If this module has pre-safe import module hooks, run these first.
241        if module_name in self._hooks_pre_safe_import_module:
242            # For the absolute path of each such hook...
243            for hook_file in self._hooks_pre_safe_import_module[module_name]:
244                # Dynamically import this hook as a fabricated module.
245                logger.info('Processing pre-safe import module hook   %s', module_name)
246                hook_module_name = 'PyInstaller_hooks_pre_safe_import_module_' + module_name.replace('.', '_')
247                hook_module = importlib_load_source(hook_module_name, hook_file)
248
249                # Object communicating changes made by this hook back to us.
250                hook_api = PreSafeImportModuleAPI(
251                    module_graph=self,
252                    module_basename=module_basename,
253                    module_name=module_name,
254                    parent_package=parent_package,
255                )
256
257                # Run this hook, passed this object.
258                if not hasattr(hook_module, 'pre_safe_import_module'):
259                    raise NameError('pre_safe_import_module() function not defined by hook %r.' % hook_file)
260                hook_module.pre_safe_import_module(hook_api)
261
262                # Respect method call changes requested by this hook.
263                module_basename = hook_api.module_basename
264                module_name = hook_api.module_name
265
266            # Prevent subsequent calls from rerunning these hooks.
267            del self._hooks_pre_safe_import_module[module_name]
268
269        # Call the superclass method.
270        return super(PyiModuleGraph, self)._safe_import_module(
271            module_basename, module_name, parent_package)
272
273    def _find_module_path(self, fullname, module_name, search_dirs):
274        """
275        Get a 3-tuple detailing the physical location of the module with the
276        passed name if that module exists _or_ raise `ImportError` otherwise.
277
278        This method wraps the superclass method with support for pre-find module
279        path hooks. If such a hook exists for this module (e.g., a script
280        `PyInstaller.hooks.hook-{module_name}` containing a function
281        `pre_find_module_path()`), that hook will be run _before_ the
282        superclass method is called.
283
284        See superclass method for parameter and return value descriptions.
285        """
286        # If this module has pre-find module path hooks, run these first.
287        if fullname in self._hooks_pre_find_module_path:
288            # For the absolute path of each such hook...
289            for hook_file in self._hooks_pre_find_module_path[fullname]:
290                # Dynamically import this hook as a fabricated module.
291                logger.info('Processing pre-find module path hook   %s', fullname)
292                hook_fullname = 'PyInstaller_hooks_pre_find_module_path_' + fullname.replace('.', '_')
293                hook_module = importlib_load_source(hook_fullname, hook_file)
294
295                # Object communicating changes made by this hook back to us.
296                hook_api = PreFindModulePathAPI(
297                    module_graph=self,
298                    module_name=fullname,
299                    search_dirs=search_dirs,
300                )
301
302                # Run this hook, passed this object.
303                if not hasattr(hook_module, 'pre_find_module_path'):
304                    raise NameError('pre_find_module_path() function not defined by hook %r.' % hook_file)
305                hook_module.pre_find_module_path(hook_api)
306
307                # Respect method call changes requested by this hook.
308                search_dirs = hook_api.search_dirs
309
310            # Prevent subsequent calls from rerunning these hooks.
311            del self._hooks_pre_find_module_path[fullname]
312
313        # Call the superclass method.
314        return super(PyiModuleGraph, self)._find_module_path(
315            fullname, module_name, search_dirs)
316
317    def get_code_objects(self):
318        """
319        Get code objects from ModuleGraph for pure Pyhton modules. This allows
320        to avoid writing .pyc/pyo files to hdd at later stage.
321
322        :return: Dict with module name and code object.
323        """
324        code_dict = {}
325        mod_types = PURE_PYTHON_MODULE_TYPES
326        for node in self.flatten(start=self._top_script_node):
327            # TODO This is terrible. To allow subclassing, types should never be
328            # directly compared. Use isinstance() instead, which is safer,
329            # simpler, and accepts sets. Most other calls to type() in the
330            # codebase should also be refactored to call isinstance() instead.
331
332            # get node type e.g. Script
333            mg_type = type(node).__name__
334            if mg_type in mod_types:
335                if node.code:
336                    code_dict[node.identifier] = node.code
337        return code_dict
338
339    def _make_toc(self, typecode=None, existing_TOC=None):
340        """
341        Return the name, path and type of selected nodes as a TOC, or appended
342        to a TOC. The selection is via a list of PyInstaller TOC typecodes.
343        If that list is empty we return the complete flattened graph as a TOC
344        with the ModuleGraph note types in place of typecodes -- meant for
345        debugging only. Normally we return ModuleGraph nodes whose types map
346        to the requested PyInstaller typecode(s) as indicated in the MODULE_TYPES_TO_TOC_DICT.
347
348        We use the ModuleGraph (really, ObjectGraph) flatten() method to
349        scan all the nodes. This is patterned after ModuleGraph.report().
350        """
351        # Construct regular expression for matching modules that should be
352        # excluded because they are bundled in base_library.zip.
353        #
354        # This expression matches the base module name, optionally followed by
355        # a period and then any number of characters. This matches the module name and
356        # the fully qualified names of any of its submodules.
357        regex_str = '(' + '|'.join(PY3_BASE_MODULES) + r')(\.|$)'
358        module_filter = re.compile(regex_str)
359
360        result = existing_TOC or TOC()
361        for node in self.flatten(start=self._top_script_node):
362            # TODO This is terrible. Everything in Python has a type. It's
363            # nonsensical to even speak of "nodes [that] are not typed." How
364            # would that even occur? After all, even "None" has a type! (It's
365            # "NoneType", for the curious.) Remove this, please.
366
367            # Skip modules that are in base_library.zip.
368            if not is_py2 and module_filter.match(node.identifier):
369                continue
370
371            # get node type e.g. Script
372            mg_type = type(node).__name__
373            assert mg_type is not None
374
375            if typecode and not (mg_type in typecode):
376                # Type is not a to be selected one, skip this one
377                continue
378            # Extract the identifier and a path if any.
379            if mg_type == 'Script':
380                # for Script nodes only, identifier is a whole path
381                (name, ext) = os.path.splitext(node.filename)
382                name = os.path.basename(name)
383            else:
384                name = node.identifier
385            path = node.filename if node.filename is not None else ''
386            # Ensure name is really 'str'. Module graph might return
387            # object type 'modulegraph.Alias' which inherits fromm 'str'.
388            # But 'marshal.dumps()' function is able to marshal only 'str'.
389            # Otherwise on Windows PyInstaller might fail with message like:
390            #
391            #   ValueError: unmarshallable object
392            name = str(name)
393            # Translate to the corresponding TOC typecode.
394            toc_type = MODULE_TYPES_TO_TOC_DICT[mg_type]
395            # TOC.append the data. This checks for a pre-existing name
396            # and skips it if it exists.
397            result.append((name, path, toc_type))
398        return result
399
400    def make_pure_toc(self):
401        """
402        Return all pure Python modules formatted as TOC.
403        """
404        # PyInstaller should handle special module types without code object.
405        return self._make_toc(PURE_PYTHON_MODULE_TYPES)
406
407    def make_binaries_toc(self, existing_toc):
408        """
409        Return all binary Python modules formatted as TOC.
410        """
411        return self._make_toc(BINARY_MODULE_TYPES, existing_toc)
412
413    def make_missing_toc(self):
414        """
415        Return all MISSING Python modules formatted as TOC.
416        """
417        return self._make_toc(BAD_MODULE_TYPES)
418
419    def nodes_to_toc(self, node_list, existing_TOC=None):
420        """
421        Given a list of nodes, create a TOC representing those nodes.
422        This is mainly used to initialize a TOC of scripts with the
423        ones that are runtime hooks. The process is almost the same as
424        _make_toc(), but the caller guarantees the nodes are
425        valid, so minimal checking.
426        """
427        result = existing_TOC or TOC()
428        for node in node_list:
429            mg_type = type(node).__name__
430            toc_type = MODULE_TYPES_TO_TOC_DICT[mg_type]
431            if mg_type == "Script" :
432                (name, ext) = os.path.splitext(node.filename)
433                name = os.path.basename(name)
434            else:
435                name = node.identifier
436            path = node.filename if node.filename is not None else ''
437            result.append( (name, path, toc_type) )
438        return result
439
440    # Return true if the named item is in the graph as a BuiltinModule node.
441    # The passed name is a basename.
442    def is_a_builtin(self, name) :
443        node = self.findNode(name)
444        if node is None:
445            return False
446        return type(node).__name__ == 'BuiltinModule'
447
448    def get_importers(self, name):
449        """List all modules importing the module with the passed name.
450
451        Returns a list of (identifier, DependencyIinfo)-tuples. If the names
452        module has not yet been imported, this method returns an empty list.
453
454        Parameters
455        ----------
456        name : str
457            Fully-qualified name of the module to be examined.
458
459        Returns
460        ----------
461        list
462            List of (fully-qualified names, DependencyIinfo)-tuples of all
463            modules importing the module with the passed fully-qualified name.
464
465        """
466        def get_importer_edge_data(importer):
467            edge = self.graph.edge_by_node(importer, name)
468            # edge might be None in case an AliasModule was added.
469            if edge is not None:
470                return self.graph.edge_data(edge)
471
472        node = self.findNode(name)
473        if node is None : return []
474        _, importers = self.get_edges(node)
475        importers = (importer.identifier
476                     for importer in importers
477                     if importer is not None)
478        return [(importer, get_importer_edge_data(importer))
479                for importer in importers]
480
481    # TODO create class from this function.
482    def analyze_runtime_hooks(self, custom_runhooks):
483        """
484        Analyze custom run-time hooks and run-time hooks implied by found modules.
485
486        :return : list of Graph nodes.
487        """
488        rthooks_nodes = []
489        logger.info('Analyzing run-time hooks ...')
490        # Process custom runtime hooks (from --runtime-hook options).
491        # The runtime hooks are order dependent. First hooks in the list
492        # are executed first. Put their graph nodes at the head of the
493        # priority_scripts list Pyinstaller-defined rthooks and
494        # thus they are executed first.
495        if custom_runhooks:
496            for hook_file in custom_runhooks:
497                logger.info("Including custom run-time hook %r", hook_file)
498                hook_file = os.path.abspath(hook_file)
499                # Not using "try" here because the path is supposed to
500                # exist, if it does not, the raised error will explain.
501                rthooks_nodes.append(self.run_script(hook_file))
502
503        # Find runtime hooks that are implied by packages already imported.
504        # Get a temporary TOC listing all the scripts and packages graphed
505        # so far. Assuming that runtime hooks apply only to modules and packages.
506        temp_toc = self._make_toc(VALID_MODULE_TYPES)
507        for (mod_name, path, typecode) in temp_toc:
508            # Look if there is any run-time hook for given module.
509            if mod_name in self._available_rthooks:
510                # There could be several run-time hooks for a module.
511                for hook in self._available_rthooks[mod_name]:
512                    logger.info("Including run-time hook %r", hook)
513                    path = os.path.join(self._homepath, 'PyInstaller', 'loader', 'rthooks', hook)
514                    rthooks_nodes.append(self.run_script(path))
515
516        return rthooks_nodes
517
518    def add_hiddenimports(self, module_list):
519        """
520        Add hidden imports that are either supplied as CLI option --hidden-import=MODULENAME
521        or as dependencies from some PyInstaller features when enabled (e.g. crypto feature).
522        """
523        # Analyze the script's hidden imports (named on the command line)
524        for modnm in module_list:
525            logger.debug('Hidden import: %s' % modnm)
526            if self.findNode(modnm) is not None:
527                logger.debug('Hidden import %r already found', modnm)
528                continue
529            logger.info("Analyzing hidden import %r", modnm)
530            # ModuleGraph throws ImportError if import not found
531            try :
532                node = self.import_hook(modnm)
533            except ImportError:
534                logger.error("Hidden import %r not found", modnm)
535
536
537    def get_co_using_ctypes(self):
538        """
539        Find modules that imports Python module 'ctypes'.
540
541        Modules that imports 'ctypes' probably load a dll that might be required
542        for bundling with the executable. The usual way to load a DLL is using:
543            ctypes.CDLL('libname')
544            ctypes.cdll.LoadLibrary('libname')
545
546        :return: Code objects that might be scanned for module dependencies.
547        """
548        co_dict = {}
549        pure_python_module_types = PURE_PYTHON_MODULE_TYPES | {'Script',}
550        node = self.findNode('ctypes')
551        if node:
552            referers = self.getReferers(node)
553            for r in referers:
554                r_ident =  r.identifier
555                # Ensure that modulegraph objects has attribute 'code'.
556                if type(r).__name__ in pure_python_module_types:
557                    if r_ident == 'ctypes' or r_ident.startswith('ctypes.'):
558                        # Skip modules of 'ctypes' package.
559                        continue
560                    co_dict[r.identifier] = r.code
561        return co_dict
562
563
564# TODO: A little odd. Couldn't we just push this functionality into the
565# PyiModuleGraph.__init__() constructor and then construct PyiModuleGraph
566# objects directly?
567def initialize_modgraph(excludes=(), user_hook_dirs=None):
568    """
569    Create the module graph and, for Python 3, analyze dependencies for
570    `base_library.zip` (which remain the same for every executable).
571
572    This function might appear weird but is necessary for speeding up
573    test runtime because it allows caching basic ModuleGraph object that
574    gets created for 'base_library.zip'.
575
576    Parameters
577    ----------
578    excludes : list
579        List of the fully-qualified names of all modules to be "excluded" and
580        hence _not_ frozen into the executable.
581    user_hook_dirs : list
582        List of the absolute paths of all directories containing user-defined
583        hooks for the current application or `None` if no such directories were
584        specified.
585
586    Returns
587    ----------
588    PyiModuleGraph
589        Module graph with core dependencies.
590    """
591    logger.info('Initializing module dependency graph...')
592
593    # Construct the initial module graph by analyzing all import statements.
594    graph = PyiModuleGraph(
595        HOMEPATH,
596        excludes=excludes,
597        # get_implies() are hidden imports known by modulgraph.
598        implies=get_implies(),
599        user_hook_dirs=user_hook_dirs,
600    )
601
602    if not is_py2:
603        logger.info('Analyzing base_library.zip ...')
604        required_mods = []
605        # Collect submodules from required modules in base_library.zip.
606        for m in PY3_BASE_MODULES:
607            if is_package(m):
608                required_mods += collect_submodules(m)
609            else:
610                required_mods.append(m)
611        # Initialize ModuleGraph.
612        for m in required_mods:
613            graph.import_hook(m)
614    return graph
615
616
617def get_bootstrap_modules():
618    """
619    Get TOC with the bootstrapping modules and their dependencies.
620    :return: TOC with modules
621    """
622    # Import 'struct' modules to get real paths to module file names.
623    mod_struct = __import__('struct')
624    # Basic modules necessary for the bootstrap process.
625    loader_mods = []
626    loaderpath = os.path.join(HOMEPATH, 'PyInstaller', 'loader')
627    # On some platforms (Windows, Debian/Ubuntu) '_struct' and zlib modules are
628    # built-in modules (linked statically) and thus does not have attribute __file__.
629    # 'struct' module is required for reading Python bytecode from executable.
630    # 'zlib' is required to decompress this bytecode.
631    for mod_name in ['_struct', 'zlib']:
632        mod = __import__(mod_name)  # C extension.
633        if hasattr(mod, '__file__'):
634            loader_mods.append((mod_name, os.path.abspath(mod.__file__), 'EXTENSION'))
635    # NOTE:These modules should be kept simple without any complicated dependencies.
636    loader_mods +=[
637        ('struct', os.path.abspath(mod_struct.__file__), 'PYMODULE'),
638        ('pyimod01_os_path', os.path.join(loaderpath, 'pyimod01_os_path.pyc'), 'PYMODULE'),
639        ('pyimod02_archive',  os.path.join(loaderpath, 'pyimod02_archive.pyc'), 'PYMODULE'),
640        ('pyimod03_importers',  os.path.join(loaderpath, 'pyimod03_importers.pyc'), 'PYMODULE'),
641        ('pyiboot01_bootstrap', os.path.join(loaderpath, 'pyiboot01_bootstrap.py'), 'PYSOURCE'),
642    ]
643    # TODO Why is here the call to TOC()?
644    toc = TOC(loader_mods)
645    return toc
646