#     Copyright 2026, Kay Hayen, mailto:kay.hayen@gmail.com find license text at end of file


"""

Plugins: Welcome to Nuitka! This is your shortest way to become part of it.

This is to provide the base class for all plugins. Some of which are part of
proper Nuitka, and some of which are waiting to be created and submitted for
inclusion by you.

The base class in PluginBase will serve as documentation of available.

"""

import inspect
import os
import sys
import traceback
from contextlib import contextmanager
from optparse import OptionConflictError

import nuitka.plugins.Hooks
from nuitka.__past__ import basestring, iter_modules
from nuitka.build.DataComposerInterface import deriveModuleConstantsBlobName
from nuitka.containers.Namedtuples import makeNamedtupleClass
from nuitka.containers.OrderedDicts import OrderedDict
from nuitka.containers.OrderedSets import OrderedSet
from nuitka.Errors import NuitkaForbiddenImportEncounter, NuitkaSyntaxError
from nuitka.freezer.IncludedDataFiles import IncludedDataFile
from nuitka.freezer.IncludedEntryPoints import IncludedEntryPoint
from nuitka.importing.FakeModules import FakeModuleDescription, addFakeModule
from nuitka.importing.Importing import locateModule
from nuitka.importing.Recursion import decideRecursion, recurseTo
from nuitka.ModuleRegistry import (
    addUsedModule,
    getDoneModules,
    getModuleByName,
    hasDoneModule,
)
from nuitka.options.CommandLineOptionsTools import OurOptionGroup
from nuitka.options.Options import (
    assumeYesForDownloads,
    getForcedRuntimeEnvironmentVariableValues,
    getPluginNameConsideringRenames,
    getPluginsDisabled,
    getPluginsEnabled,
    getUserOptions,
    hasPythonFlagNoAnnotations,
    hasPythonFlagNoAsserts,
    hasPythonFlagNoDocStrings,
    isShowInclusion,
    shallDetectMissingPlugins,
    shallRecompileExtensionModules,
)
from nuitka.OutputDirectories import getSourceDirectoryPath
from nuitka.PythonVersions import python_version
from nuitka.States import states
from nuitka.Tracing import plugins_logger, printLine
from nuitka.tree.SourceHandling import writeSourceCode
from nuitka.utils.FileOperations import (
    getDllBasename,
    getNormalizedPathJoin,
    makePath,
    putBinaryFileContents,
    putTextFileContents,
)
from nuitka.utils.Importing import importFileAsModule
from nuitka.utils.ModuleNames import (
    ModuleName,
    checkModuleName,
    makeTriggerModuleName,
    post_module_load_trigger_name,
    pre_module_load_trigger_name,
)
from nuitka.Version import getCommercialVersion

from .PluginBase import NuitkaPluginBase, control_tags
from .PluginsUsage import counted_plugin_method

# Maps plugin name to plugin instances.
active_plugins = OrderedDict()
active_plugins_with_implicit_imports = []
active_plugins_with_decide_compilation = []
active_plugins_with_decide_annotations = []
active_plugins_with_decide_doc_strings = []
active_plugins_with_decide_assertions = []
active_plugins_with_function_body_parsing = []
active_plugins_with_class_body_parsing = []
active_plugins_with_variable_constant_value = []
plugin_name2plugin_classes = {}
plugin_options = OrderedDict()
plugin_values = {}
user_plugins = OrderedSet()

# Trigger modules
pre_modules = {}
pre_modules_reasons = {}
post_modules = {}
post_modules_reasons = {}
fake_modules = {}
has_active_gui_toolkit_plugin = False

FakeModuleInfo = makeNamedtupleClass("FakeModuleInfo", ("module", "plugin", "reason"))


@contextmanager
def withPluginProblemReporting(plugin, template, args):
    try:
        yield
    except Exception:  # Catch all the things, pylint: disable=broad-except
        message = """\
Plugin issue while working on '%s'. Please report the bug with the above \
traceback included.""" % (template % args)

        if states.is_debug:
            plugin.warning(message)
            raise

        traceback.print_exception(*sys.exc_info())

        plugin.sysexit(message)


def withPluginModuleNameProblemReporting(plugin, module_name):
    return withPluginProblemReporting(plugin, "module '%s'", module_name.asString())


def withPluginModuleProblemReporting(plugin, module):
    return withPluginModuleNameProblemReporting(plugin, module.getFullName())


def _addActivePlugin(plugin_class, args, force=False):
    plugin_name = plugin_class.plugin_name

    # No duplicates please.
    if not force:
        assert plugin_name not in active_plugins.keys(), (
            plugin_name,
            active_plugins[plugin_name],
        )

    if args:
        plugin_args = getPluginOptions(plugin_name)
    else:
        plugin_args = {}

    with withPluginProblemReporting(plugin_class, "plugin initialization", ()):
        plugin_instance = plugin_class(**plugin_args)

    assert isinstance(plugin_instance, NuitkaPluginBase), plugin_instance

    active_plugins[plugin_name] = plugin_instance

    plugin_type = type(plugin_instance)

    for callback_name, plugin_collection in (
        ("getImplicitImports", active_plugins_with_implicit_imports),
        ("decideCompilation", active_plugins_with_decide_compilation),
        ("decideAnnotations", active_plugins_with_decide_annotations),
        ("decideDocStrings", active_plugins_with_decide_doc_strings),
        ("decideAssertions", active_plugins_with_decide_assertions),
        ("onFunctionBodyParsing", active_plugins_with_function_body_parsing),
        ("onClassBodyParsing", active_plugins_with_class_body_parsing),
        ("getVariableConstantValue", active_plugins_with_variable_constant_value),
    ):
        if getattr(plugin_type, callback_name) is not getattr(
            NuitkaPluginBase, callback_name
        ):
            plugin_collection.append(plugin_instance)

    is_gui_toolkit_plugin = getattr(plugin_class, "plugin_gui_toolkit", False)

    # Singleton, pylint: disable=global-statement
    global has_active_gui_toolkit_plugin
    has_active_gui_toolkit_plugin = (
        has_active_gui_toolkit_plugin or is_gui_toolkit_plugin
    )

    # Do GUI toolkit exclusion control tags generically. You may have two of
    # them and we don't want them to override each other.
    if is_gui_toolkit_plugin:
        for binding_name in getGUIBindingNames():
            is_matching = binding_name.lower() == plugin_class.binding_name.lower()

            tag_name = "use_%s" % binding_name.lower()

            # Set if matching, set to False only if not matching and not already set.
            if is_matching:
                control_tags[tag_name] = True
            elif is_matching not in control_tags:
                control_tags[tag_name] = False

    control_tags.update(plugin_instance.getEvaluationConditionControlTags())


def getActivePlugins():
    """Return list of active plugins.

    Returns:
        list of plugins

    """

    return active_plugins.values()


def getActiveQtPlugin():
    """Get active Qt plugin name."""
    for plugin_name in getQtPluginNames():
        if hasActivePlugin(plugin_name):
            if hasActivePlugin(plugin_name):
                return plugin_name

    return None


def getActiveQtPluginBindingName():
    """Get active Qt plugin binding name."""
    plugin_name = getActiveQtPlugin()

    if plugin_name is None:
        return None
    else:
        return getPluginClass(plugin_name).binding_name


def getQtBindingNames():
    return ("PySide", "PySide2", "PySide6", "PyQt4", "PyQt5", "PyQt6")


def getOtherGUIBindingNames():
    return ("wx", "tkinter", "Tkinter")


def getGUIBindingNames():
    return getQtBindingNames() + getOtherGUIBindingNames()


def getQtPluginNames():
    return tuple(qt_binding_name.lower() for qt_binding_name in getQtBindingNames())


def getOtherGuiPluginNames():
    return ("tk-inter",)


def getGuiPluginNames():
    return getQtPluginNames() + getOtherGuiPluginNames()


def hasActiveGuiPluginForBinding(binding_name):
    if binding_name in ("tkinter", "Tkinter"):
        return hasActivePlugin("tk-inter")
    elif binding_name in getQtBindingNames():
        return hasActivePlugin(binding_name.lower())
    else:
        # For wx, we do not have a plugin right now, it just works, but
        # also means it cannot be picked.
        return False


def hasActivePlugin(plugin_name):
    """Decide if a plugin is active.

    Args:
        plugin_name - name of the plugin

    Notes:
        Detectors do not count as an active plugin and ignored.

    Returns:
        bool - plugin is loaded

    """
    if plugin_name not in active_plugins:
        return False

    # Detectors do not count.
    plugin_instance = active_plugins.get(plugin_name)
    return not hasattr(plugin_instance, "detector_for")


def getUserActivatedPluginNames():
    for plugin_name, plugin_instance in active_plugins.items():
        # Detectors do not count.
        if hasattr(plugin_instance, "detector_for"):
            continue

        # Always activated does not count either
        if plugin_instance.isAlwaysEnabled():
            continue

        yield plugin_name


def getPluginClass(plugin_name):
    # First, load plugin classes, to know what we are talking about.
    loadPlugins()

    # Backward compatibility.
    plugin_name = getPluginNameConsideringRenames(plugin_name)

    if plugin_name not in plugin_name2plugin_classes:
        for plugin_name2 in plugin_name2plugin_classes:
            if plugin_name.lower() == plugin_name2.lower():
                return plugins_logger.sysexit(
                    """\
Error, unknown plug-in '%s' in wrong case referenced, use '%s' instead."""
                    % (plugin_name, plugin_name2)
                )

        return plugins_logger.sysexit(
            "Error, unknown plug-in '%s' referenced." % plugin_name
        )

    return plugin_name2plugin_classes[plugin_name][0]


def hasPluginName(plugin_name):
    # First, load plugin classes, to know what we are talking about.
    loadPlugins()

    # Backward compatibility.
    plugin_name = getPluginNameConsideringRenames(plugin_name)

    return plugin_name in plugin_name2plugin_classes


def _addPluginClass(plugin_class, detector):
    plugin_name = plugin_class.plugin_name

    if plugin_name in plugin_name2plugin_classes:
        return plugins_logger.sysexit(
            "Error, plugins collide by name %s: %s <-> %s"
            % (plugin_name, plugin_class, plugin_name2plugin_classes[plugin_name])
        )

    plugin_name2plugin_classes[plugin_name] = (
        plugin_class,
        detector,
    )


def _loadPluginClassesFromPackage(scan_package_name):
    # We check many things here, pylint: disable=too-many-branches
    try:
        scan_package = __import__(scan_package_name, fromlist=["*"])
    except ImportError:
        if "commercial" not in scan_package_name:
            return
        raise

    scan_path = scan_package.__path__

    for item in iter_modules(scan_path):
        if item.ispkg:  # spell-checker: ignore ispkg
            continue

        if item.name.endswith("Base"):
            continue

        if python_version < 0x3C0:
            module_loader = item.module_finder.find_module(item.name)
        else:
            module_loader = item.module_finder.find_spec(item.name).loader

        # Ignore bytecode only left overs.
        try:
            if module_loader.get_filename().endswith(".pyc"):
                continue
        except AttributeError:
            # Not a bytecode loader, but e.g. extension module, which is OK in case
            # it was compiled with Nuitka.
            pass

        full_name = scan_package_name + "." + item.name

        try:
            plugin_module = __import__(full_name, fromlist=["*"])
        except Exception:
            if states.is_non_debug:
                plugins_logger.warning(
                    "Problem loading plugin %r ('%s'), ignored. Use '--debug' to make it visible."
                    % (full_name, module_loader.get_filename())
                )
                continue

            raise

        plugin_classes = set(
            obj
            for obj in plugin_module.__dict__.values()
            if isObjectAUserPluginBaseClass(obj)
        )

        detectors = [
            plugin_class
            for plugin_class in plugin_classes
            if hasattr(plugin_class, "detector_for")
        ]

        # First the ones with detectors.
        for detector in detectors:
            if detector.detector_for is None:
                return plugins_logger.sysexit(
                    """Error, detector plugin %s without detector_for pointing to a plugin."""
                    % detector
                )

            plugin_class = detector.detector_for

            if detector.__name__.replace(
                "NuitkaPluginDetector", ""
            ) != plugin_class.__name__.replace("NuitkaPlugin", ""):
                plugins_logger.warning(
                    "Class names %r and %r do not match NuitkaPlugin* and NuitkaPluginDetector* naming convention."
                    % (plugin_class.__name__, detector.__name__)
                )

            assert detector.plugin_name is None, detector
            detector.plugin_name = plugin_class.plugin_name

            if plugin_class not in plugin_classes:
                return plugins_logger.sysexit(
                    "Plugin detector %r references unknown plugin %r"
                    % (detector, plugin_class)
                )

            plugin_classes.remove(detector)
            plugin_classes.remove(plugin_class)

            _addPluginClass(
                plugin_class=plugin_class,
                detector=detector,
            )

        # Remaining ones have no detector.
        for plugin_class in plugin_classes:
            _addPluginClass(plugin_class=plugin_class, detector=None)


def loadStandardPluginClasses():
    """Load plugin files located in 'standard' folder.

    Notes:
        Scan through the 'standard' and 'commercial' plugins. Import each valid
        Python module (but not packages) and process it as a plugin.
    Returns:
        None
    """

    _loadPluginClassesFromPackage("nuitka.plugins.standard")
    if getCommercialVersion() is not None:
        _loadPluginClassesFromPackage("nuitka.plugins.commercial")


class Plugins(object):
    implicit_imports_cache = {}
    extra_scan_paths_cache = {}

    @staticmethod
    @counted_plugin_method
    def _considerImplicitImports(plugin, module):
        result = []

        def iterateModuleNames(value):
            def sysexit(message):
                return plugin.sysexit(message)

            for v in value:
                if type(v) in (tuple, list):
                    sysexit(
                        "Plugin '%s' needs to be change to only return modules names, not %r (for module '%s')"
                        % (plugin.plugin_name, v, module.getFullName())
                    )

                if inspect.isgenerator(v):
                    for w in iterateModuleNames(v):
                        yield w

                    return

                if not checkModuleName(v):
                    sysexit(
                        "Plugin '%s' returned an invalid module name, not %r (for module '%s')"
                        % (plugin.plugin_name, v, module.getFullName())
                    )

                v = ModuleName(v)

                if v.getTopLevelPackageName() == "":
                    sysexit(
                        "Plugin '%s' returned an invalid relative module name, not %r (for module '%s')"
                        % (plugin.plugin_name, v, module.getFullName())
                    )

                yield v

        seen = set()

        for full_name in iterateModuleNames(plugin.getImplicitImports(module)):
            if full_name in seen:
                continue
            seen.add(full_name)

            # Ignore dependencies on self. TODO: Make this an error for the
            # plugin.
            if full_name == module.getFullName():
                continue

            try:
                _module_name, module_filename, module_kind, _finding = locateModule(
                    module_name=full_name, parent_package=None, level=0
                )
            except Exception:
                plugin.warning(
                    "Problem locating '%s' for implicit imports of '%s'."
                    % (full_name, module.getFullName())
                )
                raise

            if module_filename is None:
                if isShowInclusion():
                    plugin.info(
                        "Implicit module '%s' suggested for '%s' not found."
                        % (full_name, module.getFullName())
                    )

                continue

            result.append((full_name, module_filename, module_kind))

        if result and isShowInclusion():
            plugin.info(
                "Implicit dependencies of module '%s' added '%s'."
                % (module.getFullName(), ",".join(r[0] for r in result))
            )

        return result

    @staticmethod
    @counted_plugin_method
    def _reportImplicitImports(plugin, module, implicit_imports):
        for full_name, module_filename, module_kind in implicit_imports:

            # This will get back to all other plugins allowing them to inhibit it though.
            decision, decision_reason = decideRecursion(
                using_module_name=module.getFullName(),
                module_filename=module_filename,
                module_name=full_name,
                module_kind=module_kind,
            )

            if decision:
                try:
                    imported_module = recurseTo(
                        module_name=full_name,
                        module_filename=module_filename,
                        module_kind=module_kind,
                        source_ref=module.getSourceReference(),
                        reason="implicit import",
                        using_module_name=module.module_name,
                    )
                except NuitkaForbiddenImportEncounter as e:
                    return plugin.sysexit(
                        """\
Error, forbidden import of '%s' (intending to avoid '%s') in module '%s' \
through implicit import by '%s' plugin encountered."""
                        % (
                            e.args[0],
                            e.args[1],
                            module.module_name,
                            plugin.plugin_name,
                        )
                    )

                addUsedModule(
                    module=imported_module,
                    using_module=module,
                    usage_tag="plugin:" + plugin.plugin_name,
                    reason=decision_reason,
                    source_ref=module.source_ref,
                )

    @classmethod
    def _getPackageExtraScanPaths(cls, plugin, package_name, package_dir):
        with withPluginModuleNameProblemReporting(plugin, package_name):
            for path in plugin.getPackageExtraScanPaths(package_name, package_dir):
                if os.path.isdir(path):
                    yield path

    @classmethod
    @counted_plugin_method
    def getPackageExtraScanPaths(cls, package_name, package_dir):
        key = package_name, package_dir

        if key not in cls.extra_scan_paths_cache:
            cls.extra_scan_paths_cache[key] = ()

            for plugin in getActivePlugins():
                cls.extra_scan_paths_cache[key] += tuple(
                    cls._getPackageExtraScanPaths(
                        plugin=plugin,
                        package_name=package_name,
                        package_dir=package_dir,
                    )
                )

        return cls.extra_scan_paths_cache[key]

    @classmethod
    @counted_plugin_method
    def considerImplicitImports(cls, module):
        """Let plugins add implicit imports for a module.

        Args:
            module: module object
        Returns:
            iterable of module names
        """

        for plugin in active_plugins_with_implicit_imports:
            key = (module.getFullName(), plugin)

            if key not in cls.implicit_imports_cache:
                with withPluginModuleProblemReporting(plugin, module):
                    cls.implicit_imports_cache[key] = tuple(
                        cls._considerImplicitImports(plugin=plugin, module=module)
                    )

            if cls.implicit_imports_cache[key]:
                cls._reportImplicitImports(
                    plugin=plugin,
                    module=module,
                    implicit_imports=cls.implicit_imports_cache[key],
                )

        # Pre and post load code may have been created, if so indicate it's used.
        full_name = module.getFullName()

        if full_name in pre_modules:
            addUsedModule(
                pre_modules[full_name],
                using_module=module,
                usage_tag="plugins",
                reason=" ".join(pre_modules_reasons[full_name]),
                source_ref=module.source_ref,
            )

        if full_name in post_modules:
            addUsedModule(
                module=post_modules[full_name],
                using_module=module,
                usage_tag="plugins",
                reason=" ".join(post_modules_reasons[full_name]),
                source_ref=module.source_ref,
            )

        if full_name in fake_modules:
            for fake_module_info in fake_modules[full_name]:
                addUsedModule(
                    module=fake_module_info.module,
                    using_module=module,
                    usage_tag="plugins",
                    reason=fake_module_info.reason,
                    source_ref=module.source_ref,
                )

    @staticmethod
    @counted_plugin_method
    def onCopiedDLLs(dist_dir, standalone_entry_points):
        """Lets the plugins modify entry points on disk."""
        for entry_point in standalone_entry_points:
            if entry_point.kind.endswith("_ignored"):
                continue

            for plugin in getActivePlugins():
                dll_path = os.path.join(dist_dir, entry_point.dest_path)

                with withPluginProblemReporting(plugin, "DLL '%s'", dll_path):
                    plugin.onCopiedDLL(dll_path)

    @staticmethod
    @counted_plugin_method
    def onBeforeCodeParsing():
        """Let plugins prepare for code parsing"""
        for plugin in getActivePlugins():
            plugin.onBeforeCodeParsing()

    @staticmethod
    @counted_plugin_method
    def onStandaloneDistributionFinished(dist_dir, standalone_binary):
        """Let plugins post-process the distribution folder in standalone mode"""
        for plugin in getActivePlugins():
            plugin.onStandaloneDistributionFinished(dist_dir)

        for plugin in getActivePlugins():
            plugin.onStandaloneBinary(standalone_binary)

    @staticmethod
    @counted_plugin_method
    def onGeneratedSourceCode(source_dir, onefile):
        """Let plugins modify the generated source code"""
        for plugin in getActivePlugins():
            plugin.onGeneratedSourceCode(source_dir, onefile)

    @staticmethod
    @counted_plugin_method
    def onMetaPathLoaderEntryTemplate(module, template_args):
        """Let plugins modify meta path loader entry template arguments."""
        for plugin in getActivePlugins():
            plugin.onMetaPathLoaderEntryTemplate(
                module=module, template_args=template_args
            )

    @staticmethod
    @counted_plugin_method
    def onOnefileFinished(filename):
        """Let plugins post-process the onefile executable in onefile mode"""
        for plugin in getActivePlugins():
            plugin.onOnefileFinished(filename)

    @staticmethod
    @counted_plugin_method
    def onBootstrapBinary(filename):
        """Let plugins add to bootstrap binary in some way"""
        for plugin in getActivePlugins():
            plugin.onBootstrapBinary(filename)

    @staticmethod
    @counted_plugin_method
    def onPostProcessingResources(result_filename, onefile):
        """Let plugins act on resource addition for the executable."""
        for plugin in getActivePlugins():
            plugin.onPostProcessingResources(result_filename, onefile)

    @staticmethod
    @counted_plugin_method
    def onFinalResult(filename):
        """Let plugins add to final binary in some way"""
        for plugin in getActivePlugins():
            plugin.onFinalResult(filename)

    @staticmethod
    @counted_plugin_method
    def onInstallerOutput(filename):
        """Let plugins post-process the created installer"""
        for plugin in getActivePlugins():
            plugin.onInstallerOutput(filename)

    @staticmethod
    def considerExtraDlls(module):
        """Ask plugins to provide extra DLLs.

        Notes:
            These will be of type IncludedEntryPoint or generators providing them, so
            we get "yield from" effective with simple yield.

        """

        result = []

        # TODO: This is probably something generic that only different
        def _iterateExtraBinaries(plugin, value):
            if value is None:
                pass
            elif isinstance(value, IncludedEntryPoint):
                yield value
            elif isinstance(value, (tuple, list)) or inspect.isgenerator(value):
                for val in value:
                    for v in _iterateExtraBinaries(plugin, val):
                        yield v
            else:
                plugin.sysexit(
                    "Returned object '%s' for module '%s' but should do 'IncludedEntryPoint' or generator."
                    % (repr(value), module.asString())
                )

        for plugin in getActivePlugins():
            with withPluginModuleProblemReporting(plugin, module):
                for entry_point in _iterateExtraBinaries(
                    plugin, plugin.getExtraDlls(module)
                ):
                    result.append(entry_point)

        return result

    @staticmethod
    @counted_plugin_method
    def getModuleSpecificDllPaths(module_name):
        """Provide a list of directories, where DLLs should be searched for this package (or module).

        Args:
            module_name: name of a package or module, for which the DLL path addition applies.

        """
        result = OrderedSet()
        for plugin in getActivePlugins():
            for dll_path in plugin.getModuleSpecificDllPaths(module_name):
                result.add(dll_path)

        return result

    _uncompiled_decorator_names = None

    @classmethod
    @counted_plugin_method
    def getUncompiledDecoratorNames(cls):
        """Provide a list of decorators that should cause a function to be uncompiled.

        Returns:
            set of strings
        """

        if cls._uncompiled_decorator_names is None:
            cls._uncompiled_decorator_names = set()

            for plugin in getActivePlugins():
                for decorator_name in plugin.getUncompiledDecoratorNames():
                    assert type(decorator_name) is str, decorator_name
                    cls._uncompiled_decorator_names.add(decorator_name)

        return cls._uncompiled_decorator_names

    sys_path_additions_cache = {}

    @classmethod
    @counted_plugin_method
    def getModuleSysPathAdditions(cls, module_name):
        """Provide a list of directories, that should be considered in 'PYTHONPATH' when this module is used.

        Args:
            module_name: name of a package or module
        Returns:
            iterable of paths
        """

        if module_name not in cls.sys_path_additions_cache:
            cls.sys_path_additions_cache[module_name] = OrderedSet()

            for plugin in getActivePlugins():
                for dll_path in plugin.getModuleSysPathAdditions(module_name):
                    cls.sys_path_additions_cache[module_name].add(dll_path)

        return cls.sys_path_additions_cache[module_name]

    @staticmethod
    @counted_plugin_method
    def removeDllDependencies(dll_filename, dll_filenames):
        """Create list of removable shared libraries by scanning through the plugins.

        Args:
            dll_filename: shared library filename
            dll_filenames: list of shared library filenames
        Returns:
            list of removable files
        """
        dll_filenames = tuple(sorted(dll_filenames))

        to_remove = OrderedSet()

        for plugin in getActivePlugins():
            removed_dlls = tuple(
                plugin.removeDllDependencies(dll_filename, dll_filenames)
            )

            if removed_dlls and isShowInclusion():
                plugin.info(
                    "Removing DLLs %s of %s by plugin decision."
                    % (dll_filename, removed_dlls)
                )

            for removed_dll in removed_dlls:
                to_remove.add(removed_dll)

        return to_remove

    @staticmethod
    def considerDataFiles(module):
        """For a given module, ask plugins for any needed data files it may require.

        Args:
            module: module object
        Yields:
            Data file description pairs, either (source, dest) or (func, dest)
            where the func will be called to create the content dynamically.
        """

        def _iterateIncludedDataFiles(plugin, value):
            if value is None:
                pass
            elif isinstance(value, IncludedDataFile):
                yield value
            elif inspect.isgenerator(value):
                for val in value:
                    for v in _iterateIncludedDataFiles(plugin, val):
                        yield v
            else:
                plugin.sysexit("Plugin return non-datafile '%s'" % repr(value))

        for plugin in getActivePlugins():
            for value in plugin.considerDataFiles(module):
                for included_datafile in _iterateIncludedDataFiles(plugin, value):
                    yield included_datafile

    _data_file_tags_cache = set()

    @classmethod
    @counted_plugin_method
    def onDataFileTags(cls, included_datafile):
        if included_datafile.dest_path in cls._data_file_tags_cache:
            return

        cls._data_file_tags_cache.add(included_datafile.dest_path)

        for plugin in getActivePlugins():
            plugin.onDataFileTags(included_datafile)

    @staticmethod
    @counted_plugin_method
    def onDllTags(included_entry_point):
        for plugin in getActivePlugins():
            plugin.onDllTags(included_entry_point)

    @classmethod
    def _createTriggerLoadedModule(cls, module, trigger_name, code, flags):
        """Create a "trigger" for a module to be imported.

        Notes:
            The trigger will incorporate the code to be prepended / appended.
            Called by @onModuleDiscovered.

        Args:
            module: the module object (serves as dict key)
            trigger_name: string ("preLoad"/"postLoad")
            code: the code string

        Returns
            trigger_module
        """

        from nuitka.tree.Building import buildModule

        module_name = makeTriggerModuleName(module.getFullName(), trigger_name)

        # In debug mode, put the files in the build folder, so they can be looked up easily.
        if states.is_debug and "HIDE_SOURCE" not in flags:
            source_path = os.path.join(
                getSourceDirectoryPath(onefile=False, create=False),
                module_name + ".py",
            )

            putTextFileContents(filename=source_path, contents=code)

        try:
            trigger_module = buildModule(
                module_filename=getNormalizedPathJoin(
                    os.path.dirname(module.getCompileTimeFilename()),
                    module_name.asPath() + ".py",
                ),
                module_name=module_name,
                reason="trigger",
                source_code=code,
                is_top=False,
                is_main=False,
                module_kind="py",
                is_fake=module_name,
                hide_syntax_error=False,
            )
        except SyntaxError as e:
            plugins_logger.sysexit(
                "SyntaxError in plugin provided source code for '%s': %s."
                % (module_name, e)
            )

        if trigger_module.getCompilationMode() == "bytecode":
            trigger_module.setSourceCode(code)

        return trigger_module

    @classmethod
    def onModuleDiscovered(cls, module):
        # We offer plugins many ways to provide extra stuff
        # pylint: disable=too-many-locals,too-many-statements
        full_name = module.getFullName()

        def _untangleLoadDescription(description):
            if description and inspect.isgenerator(description):
                description = tuple(description)

            if description:
                if type(description[0]) not in (tuple, list):
                    description = [description]

                for desc in description:
                    if desc is None:
                        continue

                    if len(desc) == 2:
                        code, reason = desc
                        flags = ()
                    else:
                        code, reason, flags = desc

                        if flags is None:
                            flags = ()
                        elif type(flags) is str:
                            flags = (flags,)

                    yield plugin, code, reason, flags

        def _untangleFakeDesc(description):
            if description and inspect.isgenerator(description):
                description = tuple(description)

            if description:
                if isinstance(description, FakeModuleDescription):
                    description = (description,)
                elif type(description[0]) in (tuple, list) or isinstance(
                    description[0], FakeModuleDescription
                ):
                    pass
                else:
                    description = (description,)

                for desc in description:
                    if not isinstance(desc, FakeModuleDescription):
                        desc = FakeModuleDescription(*desc)

                    yield plugin, desc

        pre_module_load_descriptions = []
        post_module_load_descriptions = []
        fake_module_descriptions = []

        if module.isMainModule():
            plugin = None
            pre_module_load_descriptions.extend(
                _untangleLoadDescription(_getMainModulePreloadCodes())
            )

        for plugin in getActivePlugins():
            plugin.onModuleDiscovered(module)

            pre_module_load_descriptions.extend(
                _untangleLoadDescription(
                    description=plugin.createPreModuleLoadCode(module)
                )
            )
            post_module_load_descriptions.extend(
                _untangleLoadDescription(
                    description=plugin.createPostModuleLoadCode(module)
                )
            )
            fake_module_descriptions.extend(
                _untangleFakeDesc(description=plugin.createFakeModuleDependency(module))
            )

        for _plugin, fake_module_description in fake_module_descriptions:
            addFakeModule(fake_module_description.module_name)

        def combineLoadCodes(module_load_descriptions):
            future_imports_code = []
            normal_code_code = []
            total_flags = OrderedSet()
            reasons = []

            for _plugin, code, reason, flags in module_load_descriptions:
                if code:
                    for line in code.splitlines():
                        line = line + "\n"

                        if line.startswith("from __future__"):
                            future_imports_code.append(line)
                        else:
                            normal_code_code.append(line)

                    total_flags.update(flags)
                    reasons.append(reason)

            total_code = future_imports_code + normal_code_code

            return total_code, reasons, total_flags

        if pre_module_load_descriptions:
            total_code, reasons, total_flags = combineLoadCodes(
                module_load_descriptions=pre_module_load_descriptions
            )

            if total_code:
                assert full_name not in pre_modules

                pre_modules[full_name] = cls._createTriggerLoadedModule(
                    module=module,
                    trigger_name=pre_module_load_trigger_name,
                    code="".join(total_code),
                    flags=total_flags,
                )
                pre_modules_reasons[full_name] = tuple(reasons)

        if post_module_load_descriptions:
            total_code, reasons, total_flags = combineLoadCodes(
                module_load_descriptions=post_module_load_descriptions
            )

            if total_code:
                assert full_name not in post_modules

                post_modules[full_name] = cls._createTriggerLoadedModule(
                    module=module,
                    trigger_name=post_module_load_trigger_name,
                    code="".join(total_code),
                    flags=total_flags,
                )
                post_modules_reasons[full_name] = reasons

        if fake_module_descriptions:
            fake_modules[full_name] = []

            from nuitka.tree.Building import buildModule

            for plugin, fake_module_description in fake_module_descriptions:
                fake_module = buildModule(
                    module_filename=fake_module_description.source_filename,
                    module_name=fake_module_description.module_name,
                    reason="fake",
                    source_code=fake_module_description.source_code,
                    is_top=False,
                    is_main=False,
                    module_kind="py",
                    is_fake=fake_module_description.module_name,
                    hide_syntax_error=False,
                )

                if fake_module.getCompilationMode() == "bytecode":
                    fake_module.setSourceCode(fake_module_description.source_code)

                fake_modules[full_name].append(
                    FakeModuleInfo(
                        module=fake_module,
                        plugin=plugin,
                        reason=fake_module_description.reason,
                    )
                )

                # Main modules do not get added to the import cache, but plugins get to see it.
                cls.onModuleDiscovered(fake_module)

    @staticmethod
    @counted_plugin_method
    def onModuleSourceCode(module_name, source_filename, source_code):
        assert type(module_name) is ModuleName
        assert type(source_code) is str

        contributing_plugins = OrderedSet()

        for plugin in getActivePlugins():
            with withPluginModuleNameProblemReporting(plugin, module_name):
                new_source_code = plugin.onModuleSourceCode(
                    module_name=module_name,
                    source_filename=source_filename,
                    source_code=source_code,
                )
                if new_source_code is not None and new_source_code != source_code:
                    source_code = new_source_code
                    contributing_plugins.add(plugin)

                assert type(source_code) is str

        return source_code, contributing_plugins

    @staticmethod
    @counted_plugin_method
    def onFrozenModuleBytecode(module_name, is_package, bytecode):
        assert type(module_name) is ModuleName
        assert bytecode.__class__.__name__ == "code"

        for plugin in getActivePlugins():
            bytecode = plugin.onFrozenModuleBytecode(module_name, is_package, bytecode)
            assert bytecode.__class__.__name__ == "code"

        return bytecode

    @staticmethod
    @counted_plugin_method
    def onModuleEncounter(using_module_name, module_name, module_filename, module_kind):
        result = None
        deciding_plugins = []

        for plugin in getActivePlugins():
            must_recurse = plugin.onModuleEncounter(
                using_module_name=using_module_name,
                module_name=module_name,
                module_filename=module_filename,
                module_kind=module_kind,
            )

            if must_recurse is None:
                continue

            if type(must_recurse) is not tuple and must_recurse not in (True, False):
                plugin.sysexit(
                    "Error, onModuleEncounter code failed to return a None or tuple(bool, reason) result."
                )

            if result is not None:
                if result[0] != must_recurse[0]:
                    plugin.sysexit(
                        """\
Error, follow decision '%s' for module '%s' of plugin '%s' does not match other plugin '%s' decision '%s'."""
                        % (
                            must_recurse[0],
                            module_name,
                            plugin.plugin_name,
                            ".".join(
                                deciding_plugin.plugin_name
                                for deciding_plugin in deciding_plugins
                            ),
                            result[0],
                        )
                    )

            deciding_plugins.append(plugin)
            result = must_recurse

        return result, deciding_plugins

    module_usage_looked_ahead_cache = set()

    @classmethod
    def onModuleUsageLookAhead(cls, module_name, module_filename, module_kind):
        if module_name.getTopLevelPackageName() == "":
            return

        if module_name in cls.module_usage_looked_ahead_cache:
            return

        cls.module_usage_looked_ahead_cache.add(module_name)

        # Do parent package look ahead first.
        parent_package_name = module_name.getPackageName()
        if parent_package_name is not None:
            (
                _parent_package_name,
                parent_module_filename,
                parent_module_kind,
                _parent_module_finding,
            ) = locateModule(
                module_name=parent_package_name,
                parent_package=None,
                level=0,
            )

            cls.onModuleUsageLookAhead(
                module_name=parent_package_name,
                module_filename=parent_module_filename,
                module_kind=parent_module_kind,
            )

        # Lazy load the source code if a plugin wants it, the pre_load caches
        # the result for later usage.
        def getModuleSourceCode():
            if module_kind != "py" or module_filename is None:
                return None

            from nuitka.tree.SourceHandling import readSourceCodeFromFilename

            if os.path.isdir(module_filename):
                source_filename = os.path.join(module_filename, "__init__.py")
            else:
                source_filename = module_filename

            if not os.path.isfile(source_filename):
                return None

            try:
                return readSourceCodeFromFilename(
                    module_name=module_name,
                    source_filename=source_filename,
                    pre_load=True,
                )
            except SyntaxError as e:
                # Look ahead just not possible.
                raise NuitkaSyntaxError(e)

        try:
            for plugin in getActivePlugins():
                plugin.onModuleUsageLookAhead(
                    module_name=module_name,
                    module_filename=module_filename,
                    module_kind=module_kind,
                    get_module_source=getModuleSourceCode,
                )
        except NuitkaSyntaxError:
            pass

    @staticmethod
    @counted_plugin_method
    def onModuleRecursion(
        module_name, module_filename, module_kind, using_module_name, source_ref, reason
    ):
        for plugin in getActivePlugins():
            plugin.onModuleRecursion(
                module_name=module_name,
                module_filename=module_filename,
                module_kind=module_kind,
                using_module_name=using_module_name,
                source_ref=source_ref,
                reason=reason,
            )

    @staticmethod
    @counted_plugin_method
    def onCompilationStartChecks():
        """The compilation is setup, locating modules if expected to work."""

        for plugin in getActivePlugins():
            with withPluginProblemReporting(plugin, "plugin startup checks", ()):
                plugin.onCompilationStartChecks()

    @staticmethod
    @counted_plugin_method
    def onModuleInitialSet():
        """The initial set of root modules is complete, plugins may add more."""

        from nuitka.ModuleRegistry import addRootModule

        for plugin in getActivePlugins():
            for module in plugin.onModuleInitialSet():
                addRootModule(module)

    @staticmethod
    @counted_plugin_method
    def considerIncompleteModuleSet():
        """The module set is incomplete, giving plugins a chance to add more."""

        module_names = tuple(module.getFullName() for module in getDoneModules())

        modules_to_add = OrderedDict()

        # First ask all plugins for the names to follow now.
        for plugin in getActivePlugins():
            for (
                config_module_name,
                module_namespace_to_add,
            ) in plugin.onIncompleteModuleSet(module_names):
                module_namespace_to_add = ModuleName(module_namespace_to_add)
                modules_to_add[module_namespace_to_add] = (plugin, config_module_name)

        # Now remove sub-namespaces.
        for module_namespace_to_add in tuple(modules_to_add):
            package_name = module_namespace_to_add.getPackageName()

            if package_name is not None and package_name.hasOneOfNamespaces(
                *modules_to_add
            ):
                del modules_to_add[module_namespace_to_add]

        if modules_to_add:
            Plugins._addIncompleteModules(modules_to_add)

    @staticmethod
    def _addIncompleteModules(modules_to_add):
        for module in getDoneModules():
            for module_usage_attempt in module.getUsedModules():
                if module_usage_attempt.filename is not None:
                    used_module_name = module_usage_attempt.module_name

                    if not hasDoneModule(used_module_name):
                        for module_namespace_to_add, (
                            plugin,
                            config_module_name,
                        ) in modules_to_add.items():
                            if used_module_name.hasNamespace(module_namespace_to_add):
                                config_module = getModuleByName(config_module_name)
                                assert config_module is not None, config_module_name

                                try:
                                    new_module = recurseTo(
                                        module_name=used_module_name,
                                        module_filename=module_usage_attempt.filename,
                                        module_kind=module_usage_attempt.module_kind,
                                        source_ref=module_usage_attempt.source_ref,
                                        reason=module_usage_attempt.reason,
                                        using_module_name=config_module_name,
                                    )
                                except NuitkaForbiddenImportEncounter as e:
                                    plugin.sysexit(
                                        """\
Error, forbidden import of '%s' (intending to avoid '%s') in module '%s' \
through incomplete set import by '%s' plugin encountered."""
                                        % (
                                            e.args[0],
                                            e.args[1],
                                            config_module_name,
                                            plugin.plugin_name,
                                        )
                                    )

                                if new_module:
                                    addUsedModule(
                                        module=new_module,
                                        using_module=config_module,
                                        usage_tag="conditional-auto-follow",
                                        reason=module_usage_attempt.reason,
                                        source_ref=module_usage_attempt.source_ref,
                                    )

                                break

    @staticmethod
    @counted_plugin_method
    def onModuleCompleteSet():
        """The final set of modules is determined, this is only for inspection, cannot change."""

        # Make sure it's immutable.
        module_set = tuple(getDoneModules())

        for plugin in getActivePlugins():
            plugin.onModuleCompleteSet(module_set)

    @staticmethod
    @counted_plugin_method
    def suppressUnknownImportWarning(importing, source_ref, module_name):
        """Let plugins decide whether to suppress import warnings for an unknown module.

        Notes:
            If all plugins return False or None, the return will be False, else True.
        Args:
            importing: the module which is importing "module_name"
            source_ref: pointer to file source code or bytecode
            module_name: the module to be imported
        returns:
            True or False (default)
        """
        source_ref = importing.getSourceReference()

        for plugin in getActivePlugins():
            if plugin.suppressUnknownImportWarning(importing, module_name, source_ref):
                return True

        return False

    registered_compilation_decisions = {}

    @classmethod
    def registerDecisionCompilation(cls, plugin_name, module_name, decision):
        if type(module_name) is str:
            module_name = ModuleName(module_name)

        if module_name not in cls.registered_compilation_decisions:
            cls.registered_compilation_decisions[module_name] = OrderedDict()

        cls.registered_compilation_decisions[module_name][plugin_name] = decision

    @classmethod
    @counted_plugin_method
    def decideCompilation(cls, module_name):
        """Let plugins decide whether to C compile a module or include as bytecode.

        Notes:
            The decision is made by plugins answering, with collision checks
            if multiple plugins provide conflicting decisions.

        Returns:
            "compiled" (default) or "bytecode".
        """
        return cls._decideWithoutDisagreement(
            method_name="decideCompilation",
            call_per_plugin=lambda plugin: plugin.decideCompilation(module_name),
            legal_values=("compiled", "bytecode", None),
            abstain_values=(None,),
            default_value=None,
            plugins_list=active_plugins_with_decide_compilation,
            registered_values=cls.registered_compilation_decisions.get(module_name),
        )

    @staticmethod
    @counted_plugin_method
    def decideRecompileExtensionModules(module_name):
        """Let plugins decide whether to re-compile an extension module from source code.

        Notes:
            The decision is made by the first plugin "never", otherwise a matching
            "yes" config wins, "no" is allowed to be overridden by command line options.
        """
        result = None

        for plugin in getActivePlugins():
            plugin_result = plugin.decideRecompileExtensionModules(module_name)
            if plugin_result is not None:
                value, plugin_reason = plugin_result

                if value is not None:
                    assert value in ("yes", "no", "never"), value

                    if value == "never":
                        return False, plugin_reason
                    elif value == "yes":
                        result = True, plugin_reason
                    elif value == "no" and result is None:
                        result = False, plugin_reason

        options_value = shallRecompileExtensionModules(module_name)
        if options_value[0] in (True, False):
            return options_value

        if result is None:
            return None, "default behavior"
        else:
            return result

    preprocessor_symbols = None

    @classmethod
    @counted_plugin_method
    def getPreprocessorSymbols(cls, onefile):
        """Let plugins provide C defines to be used in compilation.

        Notes:
            The plugins can each contribute, but are hopefully using
            a namespace for their defines.

        Returns:
            OrderedDict(), where None value indicates no define value,
            i.e. "-Dkey=value" vs. "-Dkey"
        """

        # spell-checker: ignore -Dkey

        if cls.preprocessor_symbols is None:
            cls.preprocessor_symbols = OrderedDict()

            for plugin in getActivePlugins():
                value = plugin.getPreprocessorSymbols(onefile=onefile)

                if value is not None:
                    assert type(value) is dict, value

                    # We order per plugin, but from the plugins, lets just take a dict
                    # and achieve determinism by ordering the defines by name.
                    for key, value in sorted(value.items()):
                        # False alarm, pylint: disable=I0021,unsupported-assignment-operation
                        cls.preprocessor_symbols[key] = value

        return cls.preprocessor_symbols

    build_definitions = None

    @classmethod
    @counted_plugin_method
    def getBuildDefinitions(cls):
        """Let plugins provide C source defines to be used in compilation.

        Notes:
            The plugins can each contribute, but are hopefully using
            a namespace for their defines. Only specific code sees these
            if it chooses to include "build_definitions.h" file.

        Returns:
            OrderedDict() with keys and values."
        """

        if cls.build_definitions is None:
            cls.build_definitions = OrderedDict()

            for plugin in getActivePlugins():
                value = plugin.getBuildDefinitions()

                if value is not None:
                    assert type(value) is dict, value

                    # We order per plugin, but from the plugins themselves, lets just assume
                    # unordered dict and achieve determinism by ordering the defines by name.
                    for key, value in sorted(value.items()):
                        # False alarm, pylint: disable=I0021,unsupported-assignment-operation
                        cls.build_definitions[key] = value

        return cls.build_definitions

    extra_include_directories = None

    @classmethod
    @counted_plugin_method
    def getExtraIncludeDirectories(cls):
        """Let plugins extra directories to use for C includes in compilation.

        Notes:
            The plugins can each contribute, but are hopefully not colliding,
            order will be plugin order.

        Returns:
            tuple of paths to include as well.
        """
        if cls.extra_include_directories is None:
            cls.extra_include_directories = OrderedSet()

            for plugin in getActivePlugins():
                value = plugin.getExtraIncludeDirectories()

                if value:
                    cls.extra_include_directories.update(value)

        return tuple(cls.extra_include_directories)

    @staticmethod
    @counted_plugin_method
    def _getExtraCodeFiles(for_onefile):
        result = OrderedDict()

        for plugin in getActivePlugins():
            value = plugin.getExtraCodeFiles()

            if value is not None:
                assert type(value) is dict

                # We order per plugin, but from the plugins, lets just take a dict
                # and achieve determinism by ordering the files by name.
                for key, value in sorted(value.items()):
                    if (for_onefile and "onefile_" not in key) or (
                        not for_onefile and "onefile_" in key
                    ):
                        continue

                    if not key.startswith("nuitka_"):
                        key = "plugin." + plugin.plugin_name + "." + key

                    assert key not in result, key
                    result[key] = value

        return result

    @classmethod
    def writeExtraCodeFiles(cls, onefile):
        source_dir = getSourceDirectoryPath(onefile=onefile, create=True)

        for filename, source_code in cls._getExtraCodeFiles(
            for_onefile=onefile
        ).items():
            target_dir = os.path.join(source_dir, "plugins")

            if not os.path.isdir(target_dir):
                makePath(target_dir)

            target_filename = os.path.join(target_dir, filename)

            if type(source_code) is bytes:
                putBinaryFileContents(filename=target_filename, contents=source_code)
            else:
                writeSourceCode(
                    filename=target_filename,
                    source_code=source_code,
                    logger=plugins_logger,
                    assume_yes_for_downloads=assumeYesForDownloads(),
                )

    extra_link_libraries = None

    @classmethod
    @counted_plugin_method
    def getExtraLinkLibraries(cls):
        if cls.extra_link_libraries is None:
            cls.extra_link_libraries = OrderedSet()

            for plugin in getActivePlugins():
                value = plugin.getExtraLinkLibraries()

                if value is not None:
                    if isinstance(value, basestring):
                        cls.extra_link_libraries.add(os.path.normcase(value))
                    else:
                        for library_name in value:
                            cls.extra_link_libraries.add(os.path.normcase(library_name))

        return tuple(cls.extra_link_libraries)

    extra_link_directories = None

    @classmethod
    @counted_plugin_method
    def getExtraLinkDirectories(cls):
        if cls.extra_link_directories is None:
            cls.extra_link_directories = OrderedSet()

            for plugin in getActivePlugins():
                value = plugin.getExtraLinkDirectories()

                if value is not None:
                    if isinstance(value, basestring):
                        cls.extra_link_directories.add(value)
                    else:
                        for dir_name in value:
                            cls.extra_link_directories.add(dir_name)

        return tuple(cls.extra_link_directories)

    @classmethod
    @counted_plugin_method
    def onDataComposerRun(cls):
        for plugin in getActivePlugins():
            plugin.onDataComposerRun()

    @classmethod
    @counted_plugin_method
    def onDataComposerResult(cls, blob_filenames):
        for plugin in getActivePlugins():
            plugin.onDataComposerResult(blob_filenames)

    @classmethod
    def deriveModuleConstantsBlobName(cls, data_filename):
        result = deriveModuleConstantsBlobName(data_filename)

        return cls.encodeDataComposerName(result)

    @classmethod
    @counted_plugin_method
    def encodeDataComposerName(cls, name):
        # Encoding needs to match generated source code output.
        if str is not bytes:
            name = name.encode("utf8")

        for plugin in getActivePlugins():
            r = plugin.encodeDataComposerName(name)

            if r is not None:
                name = r
                break

        return name

    @classmethod
    @counted_plugin_method
    def onFunctionBodyParsing(cls, provider, function_name, body):
        module_name = provider.getParentModule().getFullName()

        function_qualname = provider.getChildQualname(function_name)

        for plugin in active_plugins_with_function_body_parsing:
            plugin.onFunctionBodyParsing(
                module_name=module_name,
                function_qualname=function_qualname,
                function_name=function_name,
                body=body,
            )

    @classmethod
    @counted_plugin_method
    def onClassBodyParsing(cls, provider, class_name, node):
        module_name = provider.getParentModule().getFullName()

        for plugin in active_plugins_with_class_body_parsing:
            plugin.onClassBodyParsing(
                module_name=module_name,
                class_name=class_name,
                node=node,
            )

    _variable_constant_cache = {}

    @classmethod
    def getVariableConstantValue(cls, module_name, variable_name):
        """Let plugins provide compile-time constant values for module-level variables.

        Returns:
            The constant value from the first plugin that returns non-None,
            or None if no plugin provides a value.
        """
        cache_key = (module_name, variable_name)

        if cache_key in cls._variable_constant_cache:
            return cls._variable_constant_cache[cache_key]

        for plugin in active_plugins_with_variable_constant_value:
            result = plugin.getVariableConstantValue(
                module_name=module_name, variable_name=variable_name
            )

            if result is not None:
                cls._variable_constant_cache[cache_key] = result
                return result

        cls._variable_constant_cache[cache_key] = None
        return None

    cache_contribution_values_cache = {}

    @classmethod
    @counted_plugin_method
    def getPluginsCacheContributionValues(cls, module_name):
        """Let plugins provide values that need to be taken into account for caching."""

        if module_name not in cls.cache_contribution_values_cache:
            result = []

            for plugin in getActivePlugins():
                for value in plugin.getCacheContributionValues(module_name):
                    result.append(value)

            cls.cache_contribution_values_cache[module_name] = tuple(result)

        return cls.cache_contribution_values_cache[module_name]

    @classmethod
    @counted_plugin_method
    def getExtraConstantDefaultPopulation(cls):
        for plugin in getActivePlugins():
            for value in plugin.getExtraConstantDefaultPopulation():
                yield value

    @classmethod
    def _decideWithoutDisagreement(
        cls,
        method_name,
        call_per_plugin,
        legal_values,
        abstain_values,
        default_value,
        plugins_list,
        registered_values,
    ):
        per_plugin_decisions = []

        if registered_values is not None:
            for deciding_plugin_name, value in registered_values.items():
                if value not in legal_values:
                    return plugins_logger.sysexit(
                        "Error, can only register '%s' for '%s' not %r"
                        % (legal_values, method_name, value)
                    )

                if value not in abstain_values:
                    per_plugin_decisions.append(
                        (deciding_plugin_name, value, plugins_logger)
                    )

        for plugin in plugins_list:
            value = call_per_plugin(plugin)

            if value not in legal_values:
                return plugin.sysexit(
                    "Error, can only return '%s' from '%s' not %r"
                    % (legal_values, method_name, value)
                )

            if value not in abstain_values:
                per_plugin_decisions.append((plugin.plugin_name, value, plugin))

        result = abstain_values[0]
        plugin_name = None

        for deciding_plugin_name, value, deciding_plugin_logger in per_plugin_decisions:
            if value != result:
                if result in abstain_values:
                    result = value
                    plugin_name = deciding_plugin_name
                else:
                    return deciding_plugin_logger.sysexit(
                        "Error, conflicting value '%s' with plug-in '%s' value '%s'."
                        % (value, plugin_name, result)
                    )

        if result in abstain_values:
            result = default_value

        return result

    decide_annotations_cache = {}

    @classmethod
    def decideAnnotations(cls, module_name):
        # For Python2 it's not a thing.
        if module_name not in cls.decide_annotations_cache:
            if str is bytes:
                cls.decide_annotations_cache[module_name] = False
            else:
                cls.decide_annotations_cache[module_name] = (
                    cls._decideWithoutDisagreement(
                        call_per_plugin=lambda plugin: plugin.decideAnnotations(
                            module_name
                        ),
                        legal_values=(None, True, False),
                        abstain_values=(None,),
                        method_name="decideAnnotations",
                        default_value=not hasPythonFlagNoAnnotations(),
                        plugins_list=active_plugins_with_decide_annotations,
                        registered_values=None,
                    )
                )

        return cls.decide_annotations_cache[module_name]

    decide_doc_strings_cache = {}

    @classmethod
    def decideDocStrings(cls, module_name):
        if module_name not in cls.decide_doc_strings_cache:
            cls.decide_doc_strings_cache[module_name] = cls._decideWithoutDisagreement(
                call_per_plugin=lambda plugin: plugin.decideDocStrings(module_name),
                legal_values=(None, True, False),
                abstain_values=(None,),
                method_name="decideDocStrings",
                default_value=not hasPythonFlagNoDocStrings(),
                plugins_list=active_plugins_with_decide_doc_strings,
                registered_values=None,
            )

        return cls.decide_doc_strings_cache[module_name]

    decide_assertions_cache = {}

    @classmethod
    def decideAssertions(cls, module_name):
        if module_name not in cls.decide_assertions_cache:
            cls.decide_assertions_cache[module_name] = cls._decideWithoutDisagreement(
                call_per_plugin=lambda plugin: plugin.decideAssertions(module_name),
                legal_values=(None, True, False),
                abstain_values=(None,),
                method_name="decideAssertions",
                default_value=not hasPythonFlagNoAsserts(),
                plugins_list=active_plugins_with_decide_assertions,
                registered_values=None,
            )

        return cls.decide_assertions_cache[module_name]

    @classmethod
    @counted_plugin_method
    def decideAllowOutsideDependencies(cls, module_name):
        result = None
        plugin_name = None

        assert module_name is not None

        for plugin in getActivePlugins():
            value = plugin.decideAllowOutsideDependencies(module_name)

            if value is True:
                if result is False:
                    return plugin.sysexit(
                        "Error, conflicting allow/disallow outside dependencies of plug-in '%s'."
                        % plugin_name
                    )

                result = True
                plugin_name = plugin.plugin_name

            elif value is False:
                if result is False:
                    return plugin.sysexit(
                        "Error, conflicting allow/disallow outside dependencies of plug-in '%s'."
                        % plugin_name
                    )

                result = False
                plugin_name = plugin.plugin_name
            elif value is not None:
                return plugin.sysexit(
                    "Error, can only return True, False, None from 'decideAllowOutsideDependencies' not %r"
                    % value
                )

        return result

    @classmethod
    @counted_plugin_method
    def isAcceptableMissingDLL(cls, package_name, filename):
        dll_basename = getDllBasename(os.path.basename(filename))

        # Not a DLL filename, then it cannot be true, but it's kind of strange
        # to get asked.
        if dll_basename is None:
            return None, None

        result = None
        plugin_name = None

        for plugin in getActivePlugins():
            value = plugin.isAcceptableMissingDLL(
                package_name=package_name, dll_basename=dll_basename
            )

            if value is True:
                if result is False:
                    return plugin.sysexit(
                        "Error, conflicting accept/reject missing DLLs of plug-in '%s'."
                        % plugin_name
                    )

                result = True
                plugin_name = plugin.plugin_name

            elif value is False:
                if result is False:
                    return plugin.sysexit(
                        "Error, conflicting accept/reject missing DLLs of plug-in '%s'."
                        % plugin_name
                    )

                result = False
                plugin_name = plugin.plugin_name
            elif value is not None:
                return plugin.sysexit(
                    "Error, can only return True, False, None from 'isAcceptableMissingDLL' not %r"
                    % value
                )

        return result, plugin_name


def listPlugins():
    """Print available standard plugins."""

    loadPlugins()

    printLine("The following plugins are available in Nuitka".center(80))
    printLine("-" * 80)

    plist = []
    max_name_length = 0
    for plugin_name, (plugin_class, plugin_detector) in sorted(
        plugin_name2plugin_classes.items(), key=_keyPluginForSort2
    ):
        if plugin_class.isDeprecated():
            continue

        plugin_desc = getattr(plugin_class, "plugin_desc", None)

        categories = ", ".join(plugin_class.getCategories())
        if categories:
            categories = "(%s)" % categories
        else:
            categories = ""

        status = []
        if plugin_class.isAlwaysEnabled() and plugin_class.isRelevant():
            status.append("auto-enabled")
        if plugin_detector is not None:
            status.append("has detector")

        if status:
            status_str = "[%s]" % ", ".join(status)
        else:
            status_str = ""

        plist.append((plugin_name, plugin_desc, categories, status_str))

        max_name_length = max(len(plugin_name), max_name_length)

    for line in plist:
        printLine(
            " " + line[0].ljust(max_name_length + 1),
            line[1],
            line[2],
            line[3],
        )


def isObjectAUserPluginBaseClass(obj):
    """Verify that a user plugin inherits from UserPluginBase."""
    try:
        return (
            obj is not NuitkaPluginBase
            and issubclass(obj, NuitkaPluginBase)
            and not inspect.isabstract(obj)
            and not obj.__name__.endswith("PluginBase")
        )
    except TypeError:
        return False


def loadUserPlugin(plugin_filename):
    """Load of a user plugins and store them in list of active plugins.

    Notes:
        A plugin is accepted only if it has a non-empty variable plugin_name, which
        does not equal that of a disabled (standard) plugin.
        Supports plugin option specifications.
    Returns:
        None
    """
    if not os.path.exists(plugin_filename):
        return plugins_logger.sysexit("Error, cannot find '%s'." % plugin_filename)

    user_plugin_module = importFileAsModule(plugin_filename)

    valid_file = False
    plugin_class = None
    for key in dir(user_plugin_module):
        obj = getattr(user_plugin_module, key)
        if not isObjectAUserPluginBaseClass(obj):
            continue

        plugin_name = getattr(obj, "plugin_name", None)
        if plugin_name and plugin_name not in getPluginsDisabled():
            plugin_class = obj

            valid_file = True
            break  # do not look for more in that module

    if not valid_file:  # this is not a plugin file ...
        return plugins_logger.sysexit(
            "Error, '%s' is not a plugin file." % plugin_filename
        )

    return plugin_class


_loaded_plugins = False


def loadPlugins():
    """Initialize plugin class

    Notes:
        Load user plugins provided as Python script file names, and standard
        plugins via their class attribute 'plugin_name'.

        Several checks are made, see the loader functions.

        User plugins are enabled as a first step, because they themselves may
        enable standard plugins.

    Returns:
        None
    """

    # Singleton, called potentially multiple times, pylint: disable=global-statement
    global _loaded_plugins
    if not _loaded_plugins:
        _loaded_plugins = True

        # now enable standard plugins
        loadStandardPluginClasses()


# TODO: Use in more places.
def _keyPluginForSort(plugin_class):
    return tuple(plugin_class.getCategories()), plugin_class.plugin_name


def _keyPluginForSort2(item):
    _plugin_name, (plugin_class, _plugin_detector) = item

    return _keyPluginForSort(plugin_class)


def addStandardPluginCommandLineOptions(parser, plugin_help_mode):
    loadPlugins()

    for _plugin_name, (plugin_class, _plugin_detector) in sorted(
        plugin_name2plugin_classes.items(), key=_keyPluginForSort2
    ):
        if plugin_class.isAlwaysEnabled():
            if not plugin_help_mode or not plugin_class.hasCategory("package-support"):
                _addPluginCommandLineOptions(
                    parser=parser,
                    plugin_class=plugin_class,
                    plugin_help_mode=plugin_help_mode,
                )
        else:
            if plugin_help_mode:
                _addPluginCommandLineOptions(
                    parser=parser,
                    plugin_class=plugin_class,
                    plugin_help_mode=plugin_help_mode,
                )


def activatePlugins():
    """Activate selected plugin classes

    Args:
        None

    Notes:
        This creates actual plugin instances, before only class objects were
        used.

        User plugins are activated as a first step, because they themselves may
        enable standard plugins.

    Returns:
        None
    """
    # Many cases, often with UI related decisions, pylint: disable=too-many-branches

    loadPlugins()

    # ensure plugin is known and not both, enabled and disabled
    for plugin_name in getPluginsEnabled() + getPluginsDisabled():
        if plugin_name not in plugin_name2plugin_classes:
            return plugins_logger.sysexit(
                "Error, unknown plug-in '%s' referenced." % plugin_name
            )

        if plugin_name in getPluginsEnabled() and plugin_name in getPluginsDisabled():
            return plugins_logger.sysexit(
                "Error, conflicting enable/disable of plug-in '%s'." % plugin_name
            )

    plugin_detectors = OrderedSet()

    for plugin_name, (plugin_class, plugin_detector) in sorted(
        plugin_name2plugin_classes.items()
    ):
        if plugin_name in getPluginsEnabled():
            if plugin_class.isAlwaysEnabled():
                plugin_class.warning(
                    "Plugin is defined as always enabled, no need to enable it."
                )

            if plugin_class.isRelevant():
                _addActivePlugin(plugin_class, args=True)
            elif plugin_class.isDeprecated():
                plugin_class.warning(
                    "This plugin has been deprecated, do not enable it anymore."
                )
            else:
                plugin_class.warning(
                    "Not relevant with this OS, or Nuitka arguments given, not activated."
                )
        elif plugin_name in getPluginsDisabled():
            pass
        elif plugin_class.isAlwaysEnabled() and plugin_class.isRelevant():
            _addActivePlugin(plugin_class, args=True)
        elif (
            plugin_detector is not None
            and shallDetectMissingPlugins()
            and plugin_detector.isRelevant()
        ):
            plugin_detectors.add(plugin_detector)

    for plugin_class in user_plugins:
        _addActivePlugin(plugin_class, args=True)

    # Suppress GUI toolkit detectors automatically.
    for plugin_detector in plugin_detectors:
        if (
            not has_active_gui_toolkit_plugin
            or plugin_detector.plugin_name not in getGuiPluginNames()
        ):
            _addActivePlugin(plugin_detector, args=False)


def _addPluginCommandLineOptions(parser, plugin_class, plugin_help_mode):
    plugin_name = plugin_class.plugin_name

    if plugin_name not in plugin_options:
        plugin_display_name = repr(plugin_name)

        if plugin_help_mode:
            # Ignore duplicate per design givers.
            if plugin_name in ("pyside2", "pyqt6", "pyqt5"):
                return

            if plugin_name == "pyside6":
                plugin_display_name = (
                    "'pyside6' (same for 'pyside2', 'pyqt6', 'pyqt5' plugins)"
                )

        option_group = OurOptionGroup(
            parser,
            "Plugin options of %s (categories: %s)"
            % (plugin_display_name, ", ".join(plugin_class.getCategories())),
        )
        try:
            plugin_class.addPluginCommandLineOptions(option_group)
        except OptionConflictError as e:
            for other_plugin_name, other_plugin_option_list in plugin_options.items():
                for other_plugin_option in other_plugin_option_list:
                    # no public interface for that, pylint: disable=protected-access
                    if (
                        e.option_id in other_plugin_option._long_opts
                        or other_plugin_option._short_opts
                    ):
                        return plugins_logger.sysexit(
                            "Plugin '%s' failed to add options due to conflict with '%s' from plugin '%s."
                            % (plugin_name, e.option_id, other_plugin_name)
                        )

        if option_group.option_list:
            parser.add_option_group(option_group)
            plugin_options[plugin_name] = option_group.option_list
        else:
            plugin_options[plugin_name] = ()


def addPluginCommandLineOptions(parser, plugin_names, plugin_help_mode):
    """Add option group for the plugin to the parser.

    Notes:
        This is exclusively for use in the command line parsing. Not all
        plugins have to have options. But this will add them to the
        parser in a first pass, so they can be recognized in a second
        pass with them included.

    Returns:
        None
    """
    for plugin_name in plugin_names:
        plugin_class = getPluginClass(plugin_name)
        _addPluginCommandLineOptions(
            parser=parser, plugin_class=plugin_class, plugin_help_mode=plugin_help_mode
        )


def addUserPluginCommandLineOptions(parser, filename):
    plugin_class = loadUserPlugin(filename)
    _addPluginCommandLineOptions(
        parser=parser,
        plugin_class=plugin_class,
        # List those user plugins always.
        plugin_help_mode=True,
    )

    user_plugins.add(plugin_class)


def setPluginOptions(plugin_name, values):
    """Set the option values for the specified plugin.

    Args:
        plugin_name: plugin identifier
        values: dictionary to be used for the plugin constructor
    Notes:
        Use this function, if you want to set the plugin values, without using
        the actual command line parsing.

        Normally the command line arguments are populating the dictionary for
        the plugin, but this will be used if given, and command line parsing
        is not done.
    """
    assert isinstance(values, dict), values
    plugin_values[plugin_name] = values


def getPluginOptions(plugin_name):
    """Return the options values for the specified plugin.

    Args:
        plugin_name: plugin identifier
    Returns:
        dict with key, value of options given, potentially from default values.
    """
    result = plugin_values.get(plugin_name, {})

    for option in plugin_options.get(plugin_name, {}):
        option_name = option._long_opts[0]  # pylint: disable=protected-access

        arg_value = getattr(getUserOptions(), option.dest)

        if "[REQUIRED]" in option.help:
            if not arg_value:
                return plugins_logger.sysexit(
                    "Error, required plugin argument '%s' of Nuitka plugin '%s' not given."
                    % (option_name, plugin_name)
                )

        result[option.dest] = arg_value

    return result


def replaceTriggerModule(old, new):
    """Replace a trigger module with another form if it. For use in bytecode demotion."""

    found = None
    for key, value in pre_modules.items():
        if value is old:
            found = key
            break

    if found is not None:
        pre_modules[found] = new

    found = None
    for key, value in post_modules.items():
        if value is old:
            found = key
            break

    if found is not None:
        post_modules[found] = new


def isTriggerModule(module):
    """Decide of a module is a trigger module."""
    return module in pre_modules.values() or module in post_modules.values()


# TODO: Make this a dedicated thing generally.
def _getMainModulePreloadCodes():
    forced_runtime_env_variable_values = tuple(
        getForcedRuntimeEnvironmentVariableValues()
    )
    if not forced_runtime_env_variable_values:
        return

    result = ["import os"]

    for (
        forced_runtime_env_variable_name,
        forced_runtime_env_variable_value,
    ) in forced_runtime_env_variable_values:
        result.append(
            "os.environ['%s'] = '%s'"
            % (forced_runtime_env_variable_name, forced_runtime_env_variable_value)
        )

    yield ("\n".join(result), "forcing environment variable(s)")


def setupHooks():
    """Provide the implementation bases to Hooks interface.

    Notes:
        This is to avoid circular dependencies, doing it just once here.
    """

    nuitka.plugins.Hooks.Plugins = Plugins


#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the GNU Affero General Public License, Version 3 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        https://www.gnu.org/licenses/agpl-3.0.txt
#
#     See also: "Nuitka Runtime Library Exception, Version 1.0" in file
#     "LICENSE-RUNTIME.txt" for additional permissions granted under Section 7.
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
