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


"""Collection of information for reports and their writing.

These reports are in XML form, and with Jinja2 templates in any form you like.

"""

import atexit
import os
import sys
import traceback

from nuitka.__past__ import unicode
from nuitka.build.DataComposerInterface import getDataComposerReportValues
from nuitka.build.SconsCaching import getCcacheModuleStats
from nuitka.build.SconsUtils import (
    getSconsCompilerUsed,
    getSconsObjectSizes,
    getSconsReportValue,
    getSconsReportValueBool,
    readSconsErrorReport,
    readSconsResourceUsageReports,
)
from nuitka.code_generation.ConstantCodes import getDistributionMetadataValues
from nuitka.containers.OrderedSets import OrderedSet
from nuitka.freezer.IncludedDataFiles import getIncludedDataFiles
from nuitka.freezer.IncludedEntryPoints import getStandaloneEntryPoints
from nuitka.freezer.Standalone import getRemovedUsedDllsInfo
from nuitka.importing.Importing import (
    getPackageSearchPath,
    getRecompileDecisionReason,
)
from nuitka.importing.Recursion import getRecursionDecisions
from nuitka.ModuleRegistry import (
    getDoneModules,
    getModuleCodeGenerationTimingInfos,
    getModuleInclusionInfoByName,
    getModuleInfluences,
    getModuleOptimizationTimingInfos,
)
from nuitka.options.Options import (
    getCompilationMode,
    getCompilationReportFilename,
    getCompilationReportTemplates,
    getCompilationReportUserData,
    getOnefileTempDirSpec,
    isOnefileMode,
    isOnefileTempDirMode,
    isStandaloneMode,
    shallCreateDiffableCompilationReport,
)
from nuitka.OutputDirectories import (
    getResultFullpath,
    getResultRunFilename,
    getSourceDirectoryPath,
    hasMainModule,
)
from nuitka.plugins.Plugins import getActivePlugins
from nuitka.PythonFlavors import getPythonFlavorName
from nuitka.PythonVersions import (
    getLaunchingSystemPrefixPath,
    getSystemPrefixPath,
    isPythonWithGil,
    python_version,
    python_version_full_str,
)
from nuitka.Tracing import ReportingSystemExit, reports_logger
from nuitka.TreeXML import Element, appendTreeElement, convertXmlToString
from nuitka.utils.Distributions import (
    getDistributionInstallerName,
    getDistributionLicense,
    getDistributionName,
    getDistributionsFromModuleName,
    getDistributionVersion,
    isDistributionVendored,
)
from nuitka.utils.FileOperations import (
    getFileSize,
    getNormalizedPath,
    getReportPath,
    putBinaryFileContents,
    putTextFileContents,
)
from nuitka.utils.Jinja2 import getTemplate
from nuitka.utils.MemoryUsage import getMemoryInfos
from nuitka.utils.Utils import (
    getArchitecture,
    getLinuxDistribution,
    getMacOSRelease,
    getOS,
    getWindowsRelease,
    isLinux,
    isMacOS,
    isWin32OrPosixWindows,
)
from nuitka.Version import getCommercialVersion, getNuitkaVersion


def _getReportInputData(aborted):
    """Collect all information for reporting into a dictionary."""

    # used with locals for laziness and these are to populate a dictionary with
    # many entries,
    # pylint: disable=possibly-unused-variable,too-many-branches,too-many-locals,too-many-statements

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

    module_kinds = dict(
        (module.getFullName(), module.__class__.__name__) for module in getDoneModules()
    )

    module_sources = dict(
        (module.getFullName(), module.source_ref) for module in getDoneModules()
    )

    module_inclusion_infos = dict(
        (module.getFullName(), getModuleInclusionInfoByName(module.getFullName()))
        for module in getDoneModules()
    )

    module_plugin_influences = dict(
        (module.getFullName(), getModuleInfluences(module.getFullName()))
        for module in getDoneModules()
    )

    module_timing_infos = dict(
        (
            module.getFullName(),
            getModuleOptimizationTimingInfos(module.getFullName()),
        )
        for module in getDoneModules()
    )

    module_generation_timing_infos = dict(
        (
            module.getFullName(),
            getModuleCodeGenerationTimingInfos(module.getFullName()),
        )
        for module in getDoneModules()
    )

    module_usages = dict(
        (module.getFullName(), tuple(module.getUsedModules()))
        for module in getDoneModules()
    )

    module_distributions = {}
    distribution_modules = {}

    for module in getDoneModules():
        module_distributions[module.getFullName()] = getDistributionsFromModuleName(
            module.getFullName()
        )
        for _distribution in module_distributions[module.getFullName()]:
            if _distribution not in distribution_modules:
                distribution_modules[_distribution] = OrderedSet()

            distribution_modules[_distribution].add(module.getFullName())

    module_distribution_usages = {}
    for module in getDoneModules():
        module_distribution_usages[module.getFullName()] = OrderedSet()

        for _module_usage in module_usages[module.getFullName()]:
            if _module_usage.module_name not in module_usages:
                continue

            module_distribution_usages[module.getFullName()].update(
                dist
                for dist in module_distributions[_module_usage.module_name]
                if dist not in module_distributions[module.getFullName()]
            )

    module_distribution_names = dict(
        (module.getFullName(), module.getUsedDistributions())
        for module in getDoneModules()
    )

    all_distributions = tuple(
        sorted(
            set(sum(module_distributions.values(), ())),
            key=getDistributionName,
        )
    )

    module_distribution_installers = dict(
        (
            getDistributionName(dist),
            getDistributionInstallerName(getDistributionName(dist)),
        )
        for dist in all_distributions
    )

    module_distribution_vendored = dict(
        (
            getDistributionName(dist),
            isDistributionVendored(getDistributionName(dist)),
        )
        for dist in all_distributions
    )

    module_exclusions = dict((module.getFullName(), {}) for module in getDoneModules())

    # TODO: The module filename, and other things can be None. Once we change to
    # namedtuples, we need to adapt the type check.
    def _replaceNoneWithString(value):
        if type(value) is tuple:
            return tuple(_replaceNoneWithString(element) for element in value)

        return value if value is not None else ""

    for (
        _using_module_name,
        _module_filename,
        _module_name,
        _module_kind,
        _extra_recursion,
    ), (_decision, _reason) in sorted(
        getRecursionDecisions().items(), key=_replaceNoneWithString
    ):
        if _decision is not False:
            continue

        if _using_module_name is None:
            continue

        # We might be interrupted, and have this information, but never actually
        # finished the module.
        if _using_module_name not in module_exclusions:
            continue

        module_exclusions[_using_module_name][_module_name] = _reason

    included_metadata = dict(
        (distribution_name, meta_data_value.reasons)
        for distribution_name, meta_data_value in getDistributionMetadataValues()
    )

    memory_infos = getMemoryInfos()

    python_exe = sys.executable

    python_flavor = getPythonFlavorName()

    # We want that name though, we started using it before
    # pylint: disable=redefined-outer-name
    python_version = python_version_full_str
    os_name = getOS()
    arch_name = getArchitecture()

    gil = isPythonWithGil()

    # Record system encoding, spell-checker: ignore getfilesystemencoding
    filesystem_encoding = sys.getfilesystemencoding()

    if isWin32OrPosixWindows():
        os_release = str(getWindowsRelease())
    elif isLinux():
        os_release = "-".join(x for x in getLinuxDistribution() if x)
    elif isMacOS():
        os_release = getMacOSRelease()
    else:
        os_release = "unknown"

    nuitka_version = getNuitkaVersion()
    nuitka_commercial_version = getCommercialVersion() or "not installed"

    nuitka_aborted = aborted

    nuitka_exception = sys.exc_info()

    user_data = getCompilationReportUserData()

    data_composer = getDataComposerReportValues()

    if hasMainModule():
        output_run_filename = os.path.abspath(
            getResultRunFilename(onefile=isOnefileMode())
        )
        backend_executable = os.path.abspath(
            getResultFullpath(onefile=False, real=True)
        )
        if os.path.exists(backend_executable):
            backend_executable_size = getFileSize(backend_executable)
        else:
            backend_executable_size = None

        if isOnefileMode():
            onefile_executable = os.path.abspath(
                getResultFullpath(onefile=True, real=True)
            )
            if os.path.exists(onefile_executable):
                onefile_executable_size = getFileSize(onefile_executable)
            else:
                onefile_executable_size = None

            onefile_source_dir = getSourceDirectoryPath(onefile=True, create=False)
            if onefile_source_dir is not None:
                onefile_resource_mode = getSconsReportValue(
                    onefile_source_dir, "resource_mode", default=None
                )
                onefile_reproducible = getSconsReportValueBool(
                    onefile_source_dir, "reproducible", default=None
                )
            else:
                onefile_resource_mode = None
                onefile_reproducible = None
        else:
            onefile_executable = None
            onefile_executable_size = None
            onefile_resource_mode = None
            onefile_reproducible = None

        scons_error_report_data = readSconsErrorReport(
            source_dir=getSourceDirectoryPath(onefile=False, create=False)
        )
        scons_resource_usage_data = readSconsResourceUsageReports(
            source_dir=getSourceDirectoryPath(onefile=False, create=False)
        )
        scons_ccache_stats = getCcacheModuleStats()
    else:
        scons_error_report_data = {}
        scons_resource_usage_data = {}
        scons_ccache_stats = {}
        output_run_filename = "failed too early"
        backend_executable = "failed too early"
        backend_executable_size = None
        onefile_executable = None
        onefile_executable_size = None
        onefile_resource_mode = None

    source_dir = (
        getSourceDirectoryPath(onefile=False, create=False) if hasMainModule() else None
    )

    if source_dir is not None:
        c_compiler = getSconsCompilerUsed(source_dir)
        cpp_flags = getSconsReportValue(source_dir, "cpp_flags", default=None)
        c_flags = getSconsReportValue(source_dir, "c_flags", default=None)
        cc_flags = getSconsReportValue(source_dir, "cc_flags", default=None)
        cxx_flags = getSconsReportValue(source_dir, "cxx_flags", default=None)
        ld_flags = getSconsReportValue(source_dir, "ld_flags", default=None)
        the_cc_name = getSconsReportValue(source_dir, "the_cc_name", default=None)
        the_compiler = getSconsReportValue(source_dir, "the_compiler", default=None)

        backend_resource_mode = getSconsReportValue(
            source_dir, "resource_mode", default=None
        )
        backend_reproducible = getSconsReportValueBool(
            source_dir, "reproducible", default=None
        )
    else:
        c_compiler = None
        cpp_flags = None
        c_flags = None
        cc_flags = None
        cxx_flags = None
        ld_flags = None
        the_cc_name = None
        the_compiler = None
        backend_resource_mode = None
        backend_reproducible = None

    module_object_sizes = getSconsObjectSizes(source_dir) if source_dir else {}

    del source_dir

    compilation_mode = getCompilationMode()

    performance_totals = {
        "optimization_passes": {},
        "code_generation": {
            "time": 0.0,
            "count": 0,
            "cpu_instr": None,
            "cpu_cycles": None,
        },
        "all_passes": {"time": 0.0, "count": 0, "cpu_instr": None, "cpu_cycles": None},
    }

    _total_all_passes_module_set = set()

    for _module_name, _timing_infos in module_timing_infos.items():
        if _timing_infos:
            _total_all_passes_module_set.add(_module_name)
        for _timing_info in _timing_infos:
            _pass_num = _timing_info.pass_number
            if _pass_num not in performance_totals["optimization_passes"]:
                performance_totals["optimization_passes"][_pass_num] = {
                    "time": 0.0,
                    "count": 0,
                    "cpu_instr": None,
                    "cpu_cycles": None,
                }
            performance_totals["optimization_passes"][_pass_num][
                "time"
            ] += _timing_info.time_used
            performance_totals["optimization_passes"][_pass_num]["count"] += 1
            performance_totals["all_passes"]["time"] += _timing_info.time_used

            if _timing_info.cpu_instr_count is not None:
                if (
                    performance_totals["optimization_passes"][_pass_num]["cpu_instr"]
                    is None
                ):
                    performance_totals["optimization_passes"][_pass_num][
                        "cpu_instr"
                    ] = 0
                    performance_totals["optimization_passes"][_pass_num][
                        "cpu_cycles"
                    ] = 0
                performance_totals["optimization_passes"][_pass_num][
                    "cpu_instr"
                ] += _timing_info.cpu_instr_count
                performance_totals["optimization_passes"][_pass_num][
                    "cpu_cycles"
                ] += _timing_info.cpu_cycles_count

                if performance_totals["all_passes"]["cpu_instr"] is None:
                    performance_totals["all_passes"]["cpu_instr"] = 0
                    performance_totals["all_passes"]["cpu_cycles"] = 0
                performance_totals["all_passes"][
                    "cpu_instr"
                ] += _timing_info.cpu_instr_count
                performance_totals["all_passes"][
                    "cpu_cycles"
                ] += _timing_info.cpu_cycles_count

    performance_totals["all_passes"]["count"] = len(_total_all_passes_module_set)

    for _module_name, _timing_info in module_generation_timing_infos.items():
        if _timing_info is not None:
            performance_totals["code_generation"]["time"] += _timing_info.time_used
            performance_totals["code_generation"]["count"] += 1

            if getattr(_timing_info, "cpu_instr_count", None) is not None:
                if performance_totals["code_generation"]["cpu_instr"] is None:
                    performance_totals["code_generation"]["cpu_instr"] = 0
                    performance_totals["code_generation"]["cpu_cycles"] = 0
                performance_totals["code_generation"][
                    "cpu_instr"
                ] += _timing_info.cpu_instr_count
                performance_totals["code_generation"][
                    "cpu_cycles"
                ] += _timing_info.cpu_cycles_count

    return dict(
        (var_name, var_value)
        for var_name, var_value in locals().items()
        if not var_name.startswith("_")
    )


_report_prefixes = None


def _getReportPathPrefixes():
    # Using global here, as this is really a singleton, in the form of a module,
    # pylint: disable=global-statement
    global _report_prefixes

    if _report_prefixes is None:
        _report_prefixes = []

        sys_prefix = getLaunchingSystemPrefixPath() or sys.prefix
        real_sys_prefix = getSystemPrefixPath()

        if real_sys_prefix != sys_prefix:
            _report_prefixes.append(("${sys.real_prefix}", real_sys_prefix))

        _report_prefixes.append(("${sys.prefix}", sys_prefix))
        _report_prefixes.append(("${cwd}", os.getcwd()))

    return _report_prefixes


def _getCompilationReportPath(path):
    return getReportPath(path, prefixes=_getReportPathPrefixes())


def _addModulesToReport(root, report_input_data, diffable):
    # Many details to work with,
    # pylint: disable=too-many-branches,too-many-locals,too-many-statements

    for module_name in report_input_data["module_names"]:
        active_module_info = report_input_data["module_inclusion_infos"][module_name]

        module_xml_node = appendTreeElement(
            root,
            "module",
            name=module_name,
            kind=report_input_data["module_kinds"][module_name],
            usage=active_module_info.usage_tag,
            reason=active_module_info.reason,
            source_path=_getCompilationReportPath(
                report_input_data["module_sources"][module_name].getFilename()
            ),
        )

        recompiled, recompiled_reason = getRecompileDecisionReason(module_name)
        if recompiled_reason is not None:
            module_xml_node.attrib["recompiled_extension"] = (
                "yes" if recompiled else "no"
            )
            module_xml_node.attrib["recompiled_extension_reason"] = recompiled_reason

        distributions = report_input_data["module_distributions"][module_name]

        if distributions:
            module_xml_node.attrib["distribution"] = ",".join(
                getDistributionName(dist) for dist in distributions
            )

        for plugin_name, influence, detail in report_input_data[
            "module_plugin_influences"
        ][module_name]:
            influence_xml_node = Element(
                "plugin-influence", name=plugin_name, influence=influence
            )

            if influence == "condition-used":
                condition, condition_tags_used, condition_result = detail

                influence_xml_node.attrib["condition"] = condition
                if condition_tags_used:
                    influence_xml_node.attrib["tags_used"] = ",".join(
                        condition_tags_used
                    )
                influence_xml_node.attrib["result"] = str(condition_result).lower()
            elif influence == "variable-used":
                (
                    variable_name,
                    condition_tags_used,
                    variable_value,
                    config_module_name,
                ) = detail

                influence_xml_node.attrib["variable"] = variable_name
                if condition_tags_used:
                    influence_xml_node.attrib["tags_used"] = ",".join(
                        condition_tags_used
                    )
                influence_xml_node.attrib["value"] = variable_value

                if module_name != config_module_name:
                    influence_xml_node.attrib["config_module"] = config_module_name

            elif influence == "parameter-used":
                parameter_name, condition_tags_used, parameter_value = detail

                influence_xml_node.attrib["parameter"] = parameter_name
                if condition_tags_used:
                    influence_xml_node.attrib["tags_used"] = ",".join(
                        condition_tags_used
                    )
                influence_xml_node.attrib["value"] = repr(parameter_value)
            elif influence == "detection":
                detection_name, detection_value = detail

                influence_xml_node.attrib["detection"] = detection_name
                influence_xml_node.attrib["value"] = repr(detection_value)
            else:
                assert False, influence

            module_xml_node.append(influence_xml_node)

        for timing_info in report_input_data["module_timing_infos"][module_name]:
            timing_xml_node = Element(
                "optimization-time",
            )

            # Going via attrib, because pass is a keyword in Python.
            timing_xml_node.attrib["pass"] = str(timing_info.pass_number)
            timing_xml_node.attrib["time"] = (
                "volatile" if diffable else "%.4f" % timing_info.time_used
            )

            if timing_info.micro_passes:
                timing_xml_node.attrib["micro_passes"] = str(timing_info.micro_passes)

            if timing_info.merge_counts:
                merged_total = 0

                for branch_count, merge_count in timing_info.merge_counts.items():
                    merged_total += branch_count * merge_count

                max_merge_size = max(timing_info.merge_counts)

                timing_xml_node.attrib["max_branch_merge"] = str(max_merge_size)
                timing_xml_node.attrib["merged_total"] = str(merged_total)

            if timing_info.cpu_instr_count is not None:
                timing_xml_node.attrib["cpu_instr"] = str(timing_info.cpu_instr_count)
            if timing_info.cpu_cycles_count is not None:
                timing_xml_node.attrib["cpu_cycles"] = str(timing_info.cpu_cycles_count)

            module_xml_node.append(timing_xml_node)

        timing_info = report_input_data["module_generation_timing_infos"].get(
            module_name
        )
        if timing_info is not None:
            timing_xml_node = Element(
                "code-generation-time",
            )

            timing_xml_node.attrib["time"] = (
                "volatile" if diffable else "%.4f" % timing_info.time_used
            )

            if timing_info.cpu_instr_count is not None:
                timing_xml_node.attrib["cpu_instr"] = str(timing_info.cpu_instr_count)
            if timing_info.cpu_cycles_count is not None:
                timing_xml_node.attrib["cpu_cycles"] = str(timing_info.cpu_cycles_count)

            module_xml_node.append(timing_xml_node)

        if report_input_data["scons_resource_usage_data"] is not None:
            compile_rusage = report_input_data["scons_resource_usage_data"].get(
                module_name.asString()
            )
        else:
            compile_rusage = None

        if report_input_data["scons_ccache_stats"]:
            ccache_info = report_input_data["scons_ccache_stats"].get(
                module_name.asString()
            )
        else:
            ccache_info = None

        module_object_size = report_input_data["module_object_sizes"].get(module_name)

        if not diffable and (
            compile_rusage or ccache_info is not None or module_object_size is not None
        ):
            compile_xml_node = appendTreeElement(
                module_xml_node,
                "c-compilation-resources",
            )

            if ccache_info is not None:
                appendTreeElement(
                    compile_xml_node,
                    "c-cache",
                    tech=ccache_info[0],
                    result=ccache_info[1],
                )
            else:
                appendTreeElement(
                    compile_xml_node,
                    "c-cache",
                    tech="none",
                )

            if module_object_size is not None:
                appendTreeElement(
                    compile_xml_node,
                    "object-file",
                    size=str(module_object_size),
                )

            if compile_rusage:
                for group_name, group_data in compile_rusage.items():
                    group_xml_node = appendTreeElement(
                        compile_xml_node,
                        group_name,
                    )
                    for key, value in group_data.items():
                        if type(value) is float:
                            value = "%.4f" % value
                        else:
                            value = str(value)
                        group_xml_node.attrib[key] = value

        distributions = report_input_data["module_distribution_usages"][module_name]

        if distributions:
            distributions_xml_node = appendTreeElement(
                module_xml_node,
                "distribution-usages",
            )

            for distribution in distributions:
                appendTreeElement(
                    distributions_xml_node,
                    "distribution-usage",
                    name=getDistributionName(distribution),
                )

        module_distribution_names = report_input_data["module_distribution_names"][
            module_name
        ]

        if module_distribution_names:
            module_distribution_names_xml_node = appendTreeElement(
                module_xml_node,
                "distribution-lookups",
            )

            for distribution_name, found in module_distribution_names.items():
                appendTreeElement(
                    module_distribution_names_xml_node,
                    "distribution-lookup",
                    name=distribution_name,
                    found="yes" if found else "no",
                )

        used_modules_xml_node = appendTreeElement(
            module_xml_node,
            "module_usages",
        )

        for count, used_module in enumerate(
            report_input_data["module_usages"][module_name]
        ):
            # We don't want to see those parent imports, unless they have
            # an effect.
            if used_module.reason == "import path parent":
                while True:
                    count += 1
                    next_used_module = report_input_data["module_usages"][module_name][
                        count
                    ]

                    if next_used_module.reason != "import path parent":
                        break

                exclusion_reason = report_input_data["module_exclusions"][
                    module_name
                ].get(next_used_module.module_name)

                if exclusion_reason is None or next_used_module.finding != "not-found":
                    continue

            module_usage_node = appendTreeElement(
                used_modules_xml_node,
                "module_usage",
                name=used_module.module_name.asString(),
                finding=used_module.finding,
                line=str(used_module.source_ref.getLineNumber()),
                # TODO: Add reason in a hotfix.
                # reason=used_module.reason,
            )

            exclusion_reason = report_input_data["module_exclusions"][module_name].get(
                used_module.module_name
            )

            # Include reason why a module was excluded unless it is obvious like
            # with built-in modules.
            if exclusion_reason is not None and used_module.module_kind != "built-in":
                module_usage_node.attrib["finding"] = "excluded"
                module_usage_node.attrib["exclusion_reason"] = exclusion_reason


def _addMemoryInfosToReport(performance_xml_node, memory_infos, diffable):
    for key, value in memory_infos.items():
        # Only top level values for now.
        if type(value) is not int:
            continue

        appendTreeElement(
            performance_xml_node,
            "memory_usage",
            name=key,
            value="volatile" if diffable else str(value),
        )


def _addUserDataToReport(root, user_data):
    if user_data:
        user_data_xml_node = appendTreeElement(
            root,
            "user-data",
        )

        for key, value in user_data.items():
            user_data_value_xml_node = appendTreeElement(
                user_data_xml_node,
                key,
            )

            user_data_value_xml_node.text = value


def writeCompilationReport(report_filename, report_input_data, diffable):
    """Write the compilation report in XML format."""
    # Many details, pylint: disable=too-many-branches,too-many-locals,too-many-statements

    # When failing to write the crash report, we need to indicate that it was
    # not done to the atexit handler, pylint: disable=global-statement
    global _crash_report_filename

    exit_message = None
    if not report_input_data["nuitka_aborted"]:
        completion = "yes"
    elif report_input_data["nuitka_exception"][0] is KeyboardInterrupt:
        completion = "interrupted"
    elif report_input_data["nuitka_exception"][0] is SystemExit:
        completion = "error exit (%s)" % report_input_data["nuitka_exception"][1].code
    elif report_input_data["nuitka_exception"][0] is ReportingSystemExit:
        completion = (
            "error exit message (%s)" % report_input_data["nuitka_exception"][1].code
        )
        exit_message = report_input_data["nuitka_exception"][1].exit_message
    else:
        completion = "exception"

    root = Element(
        "nuitka-compilation-report",
        nuitka_version=report_input_data["nuitka_version"],
        nuitka_commercial_version=report_input_data["nuitka_commercial_version"],
        mode=report_input_data["compilation_mode"],
        completion=completion,
    )

    if exit_message is not None:
        root.attrib["exit_message"] = exit_message

    if completion == "exception":
        exception_xml_node = appendTreeElement(
            root,
            "exception",
            exception_type=str(sys.exc_info()[0].__name__),
            exception_value=str(sys.exc_info()[1]),
        )

        exception_xml_node.text = "\n" + traceback.format_exc()

    if report_input_data["cpp_flags"] is not None:
        scons_environment_xml_node = appendTreeElement(
            root,
            "scons_environment",
        )

        if report_input_data["c_compiler"] is not None:
            scons_environment_xml_node.attrib["c_compiler"] = report_input_data[
                "c_compiler"
            ]
        if report_input_data["the_cc_name"] is not None:
            scons_environment_xml_node.attrib["the_cc_name"] = report_input_data[
                "the_cc_name"
            ]
        if report_input_data["the_compiler"] is not None:
            scons_environment_xml_node.attrib["the_compiler"] = (
                _getCompilationReportPath(report_input_data["the_compiler"])
            )
        if report_input_data["cpp_flags"] != "":
            scons_environment_xml_node.attrib["cpp_flags"] = report_input_data[
                "cpp_flags"
            ]
        if report_input_data["c_flags"] != "":
            scons_environment_xml_node.attrib["c_flags"] = report_input_data["c_flags"]
        if report_input_data["cc_flags"] != "":
            scons_environment_xml_node.attrib["cc_flags"] = report_input_data[
                "cc_flags"
            ]
        if report_input_data["cxx_flags"] != "":
            scons_environment_xml_node.attrib["cxx_flags"] = report_input_data[
                "cxx_flags"
            ]
        if report_input_data["ld_flags"] != "":
            scons_environment_xml_node.attrib["ld_flags"] = report_input_data[
                "ld_flags"
            ]

    if report_input_data["scons_error_report_data"]:
        scons_error_reports_node = appendTreeElement(root, "scons_error_reports")

        for cmd, (stdout, stderr) in report_input_data[
            "scons_error_report_data"
        ].items():
            scons_error_report_node = appendTreeElement(
                scons_error_reports_node, "scons_error_report"
            )

            appendTreeElement(
                scons_error_report_node,
                "command",
            ).text = cmd

            if stdout:
                if not stdout.startswith("\n"):
                    stdout = "\n" + stdout

                stdout = stdout.rstrip("\n") + "\n"

                appendTreeElement(
                    scons_error_report_node,
                    "stdout",
                ).text = stdout

            if stderr:
                if not stderr.startswith("\n"):
                    stderr = "\n" + stderr

                stderr = stderr.rstrip("\n") + "\n"

                appendTreeElement(
                    scons_error_report_node,
                    "stderr",
                ).text = stderr

    _addModulesToReport(
        root=root, report_input_data=report_input_data, diffable=diffable
    )

    if report_input_data["scons_resource_usage_data"]:
        linker_rusage = report_input_data["scons_resource_usage_data"].get("@linker")

        if not diffable and linker_rusage:
            linker_xml_node = appendTreeElement(root, "linker-resources")
            for group_name, group_data in linker_rusage.items():
                group_xml_node = appendTreeElement(
                    linker_xml_node,
                    group_name,
                )
                for key, value in group_data.items():
                    if type(value) is float:
                        value = "%.4f" % value
                    else:
                        value = str(value)
                    group_xml_node.attrib[key] = value

    if report_input_data["memory_infos"]:
        performance_xml_node = appendTreeElement(
            root,
            "performance",
        )

        _addMemoryInfosToReport(
            performance_xml_node=performance_xml_node,
            memory_infos=report_input_data["memory_infos"],
            diffable=diffable,
        )

    for included_datafile in getIncludedDataFiles():
        if included_datafile.kind == "data_file":
            appendTreeElement(
                root,
                "data_file",
                name=included_datafile.dest_path,
                source=_getCompilationReportPath(included_datafile.source_path),
                size=str(included_datafile.getFileSize()),
                reason=included_datafile.reason,
                tags=",".join(included_datafile.tags),
            )
        elif included_datafile.kind == "data_blob":
            appendTreeElement(
                root,
                "data_blob",
                name=included_datafile.dest_path,
                size=str(included_datafile.getFileSize()),
                reason=included_datafile.reason,
                tags=",".join(included_datafile.tags),
            )

    if report_input_data["included_metadata"]:
        metadata_node = appendTreeElement(
            root,
            "metadata",
        )

        for distribution_name, reasons in sorted(
            report_input_data["included_metadata"].items()
        ):
            appendTreeElement(
                metadata_node,
                "included_metadata",
                name=distribution_name,
                reason=". ".join(reasons),
            )

    for standalone_entry_point in sorted(
        getStandaloneEntryPoints(), key=lambda entry: entry.dest_path
    ):
        if standalone_entry_point.kind == "executable":
            continue

        kind = standalone_entry_point.kind

        if kind.endswith("_ignored"):
            ignored = True
            kind = kind.replace("_ignored", "")
        else:
            ignored = False

        appendTreeElement(
            root,
            "included_" + kind,
            name=os.path.basename(standalone_entry_point.dest_path),
            dest_path=standalone_entry_point.dest_path,
            source_path=(
                "{generated}"
                if standalone_entry_point.reason == "main binary"
                else _getCompilationReportPath(standalone_entry_point.source_path)
            ),
            package=standalone_entry_point.package_name or "",
            ignored="yes" if ignored else "no",
            reason=standalone_entry_point.reason,
            tags=",".join(standalone_entry_point.tags),
        )

    for standalone_entry_point, (reason, removed_dll_paths) in getRemovedUsedDllsInfo():
        for removed_dll_path in removed_dll_paths:
            appendTreeElement(
                root,
                "excluded_dll",
                name=_getCompilationReportPath(removed_dll_path),
                used_by=standalone_entry_point.dest_path,
                reason=reason,
            )

    if not diffable:
        data_composer_values = getDataComposerReportValues()

        data_composer_xml_node = appendTreeElement(
            root,
            "data_composer",
            blob_size=str(data_composer_values["blob_size"]),
        )

        data_composer_stats = data_composer_values["stats"]
        if data_composer_stats:
            for item, item_value in data_composer_stats.items():
                assert type(item) in (str, unicode)
                if type(item_value) is int:
                    data_composer_xml_node.attrib[item] = str(item_value)
                else:
                    for key in item_value:
                        item_value[key] = str(item_value[key])

                    appendTreeElement(
                        data_composer_xml_node,
                        "module_data",
                        filename=item,
                        **item_value
                    )

    performance_totals = report_input_data["performance_totals"]

    performance_totals_node = appendTreeElement(
        root,
        "performance-totals",
    )

    for pass_num in sorted(performance_totals["optimization_passes"]):
        timing_xml_node = appendTreeElement(
            performance_totals_node,
            "optimization-time",
        )
        timing_xml_node.attrib["pass"] = str(pass_num)
        timing_xml_node.attrib["time"] = (
            "volatile"
            if diffable
            else "%.4f" % performance_totals["optimization_passes"][pass_num]["time"]
        )
        timing_xml_node.attrib["module_count"] = str(
            performance_totals["optimization_passes"][pass_num]["count"]
        )
        if performance_totals["optimization_passes"][pass_num]["cpu_instr"] is not None:
            if not diffable:
                timing_xml_node.attrib["cpu_instr"] = str(
                    performance_totals["optimization_passes"][pass_num]["cpu_instr"]
                )
                timing_xml_node.attrib["cpu_cycles"] = str(
                    performance_totals["optimization_passes"][pass_num]["cpu_cycles"]
                )

    timing_xml_node = appendTreeElement(
        performance_totals_node,
        "optimization-time",
    )
    timing_xml_node.attrib["pass"] = "all"
    timing_xml_node.attrib["time"] = (
        "volatile" if diffable else "%.4f" % performance_totals["all_passes"]["time"]
    )
    timing_xml_node.attrib["module_count"] = str(
        performance_totals["all_passes"]["count"]
    )
    if performance_totals["all_passes"]["cpu_instr"] is not None:
        if not diffable:
            timing_xml_node.attrib["cpu_instr"] = str(
                performance_totals["all_passes"]["cpu_instr"]
            )
            timing_xml_node.attrib["cpu_cycles"] = str(
                performance_totals["all_passes"]["cpu_cycles"]
            )

    timing_xml_node = appendTreeElement(
        performance_totals_node,
        "code-generation-time",
    )
    timing_xml_node.attrib["time"] = (
        "volatile"
        if diffable
        else "%.4f" % performance_totals["code_generation"]["time"]
    )
    timing_xml_node.attrib["module_count"] = str(
        performance_totals["code_generation"]["count"]
    )
    if performance_totals["code_generation"]["cpu_instr"] is not None:
        if not diffable:
            timing_xml_node.attrib["cpu_instr"] = str(
                performance_totals["code_generation"]["cpu_instr"]
            )
            timing_xml_node.attrib["cpu_cycles"] = str(
                performance_totals["code_generation"]["cpu_cycles"]
            )

    if report_input_data["backend_executable"] != "failed too early":
        python_binary_xml_node = appendTreeElement(
            root,
            "python_binary",
            filename=(
                os.path.basename(report_input_data["backend_executable"])
                if isStandaloneMode()
                else _getCompilationReportPath(report_input_data["backend_executable"])
            ),
        )

        if report_input_data["backend_resource_mode"] is not None:
            python_binary_xml_node.attrib["backend_resource_mode"] = report_input_data[
                "backend_resource_mode"
            ]

        if report_input_data["backend_reproducible"] is not None:
            python_binary_xml_node.attrib["reproducible"] = (
                "yes" if report_input_data["backend_reproducible"] else "no"
            )

        if diffable:
            python_binary_xml_node.attrib["size"] = "volatile"
        elif report_input_data["backend_executable_size"] is not None:
            python_binary_xml_node.attrib["size"] = str(
                report_input_data["backend_executable_size"]
            )

    if report_input_data.get("onefile_executable") is not None:
        onefile_binary_xml_node = appendTreeElement(
            root,
            "onefile_binary",
            filename=os.path.basename(report_input_data["onefile_executable"]),
        )

        if report_input_data.get("onefile_resource_mode") is not None:
            onefile_binary_xml_node.attrib["onefile_resource_mode"] = report_input_data[
                "onefile_resource_mode"
            ]

        if report_input_data.get("onefile_reproducible") is not None:
            onefile_binary_xml_node.attrib["reproducible"] = (
                "yes" if report_input_data["onefile_reproducible"] else "no"
            )

        if diffable:
            onefile_binary_xml_node.attrib["size"] = "volatile"
        elif report_input_data.get("onefile_executable_size") is not None:
            onefile_binary_xml_node.attrib["size"] = str(
                report_input_data["onefile_executable_size"]
            )

    options_xml_node = appendTreeElement(
        root,
        "command_line",
    )

    for arg in sys.argv[1:]:
        appendTreeElement(options_xml_node, "option", value=arg)

    active_plugins_xml_node = appendTreeElement(
        root,
        "plugins",
    )

    for plugin in getActivePlugins():
        if plugin.isDetector():
            continue

        plugin_element = appendTreeElement(
            active_plugins_xml_node,
            "plugin",
            name=plugin.plugin_name,
            user_enabled="no" if plugin.isAlwaysEnabled() else "yes",
        )

        try:
            # TODO: Actually expose these to other reports as well.
            for key, value in plugin.getReportData():
                if type(value) is bool:
                    value = "yes" if value else "no"

                plugin_element.attrib[key] = value

        except Exception:  # pylint: disable=broad-exception-caught
            # Don't fail report generation for plugin report issues, they might be badly coded,
            # for now we don't even warn about them.
            pass

    if isOnefileMode():
        _onefile_xml_node = appendTreeElement(
            root,
            "onefile",
            cache_mode="temporary" if isOnefileTempDirMode() else "cached",
            unpack_dir=getOnefileTempDirSpec(),
        )

    distributions_xml_node = appendTreeElement(
        root,
        "distributions",
    )

    for distribution in report_input_data["all_distributions"]:
        distribution_node = appendTreeElement(
            distributions_xml_node,
            "distribution",
            name=getDistributionName(distribution),
            version=getDistributionVersion(distribution),
            installer=report_input_data["module_distribution_installers"][
                getDistributionName(distribution)
            ],
        )

        if report_input_data["module_distribution_vendored"][
            getDistributionName(distribution)
        ]:
            distribution_node.attrib["vendored"] = "yes"

    python_xml_node = appendTreeElement(
        root,
        "python",
        python_exe=_getCompilationReportPath(report_input_data["python_exe"]),
        python_flavor=report_input_data["python_flavor"],
        python_version=report_input_data["python_version"],
        os_name=report_input_data["os_name"],
        os_release=report_input_data["os_release"],
        arch_name=report_input_data["arch_name"],
        filesystem_encoding=report_input_data["filesystem_encoding"],
    )

    if python_version >= 0x3D0:
        python_xml_node.attrib["gil"] = "yes" if report_input_data["gil"] else "no"

    search_path = getPackageSearchPath(None)

    if search_path is not None:
        search_path_xml_node = appendTreeElement(
            python_xml_node,
            "search_path",
        )

        for search_path in getPackageSearchPath(None):
            appendTreeElement(
                search_path_xml_node,
                "path",
                value=_getCompilationReportPath(search_path),
            )

    _addUserDataToReport(root=root, user_data=report_input_data["user_data"])

    python_xml_node = appendTreeElement(
        root,
        "output",
    )

    if report_input_data["output_run_filename"] != "failed too early":
        python_xml_node.attrib["run_filename"] = _getCompilationReportPath(
            report_input_data["output_run_filename"]
        )

    contents = convertXmlToString(root)

    if type(contents) is not bytes:
        contents = contents.encode("utf8")

    try:
        putBinaryFileContents(
            filename=getNormalizedPath(report_filename), contents=contents
        )
    except OSError as e:
        reports_logger.warning(
            "Compilation report write to file '%s' failed due to: %s."
            % (report_filename, e)
        )

        if _crash_report_filename == report_filename:
            _crash_report_filename = None
    else:
        if _crash_report_filename != report_filename:
            reports_logger.info(
                "Compilation report written to file '%s'." % report_filename
            )


def writeCompilationReportFromTemplate(
    template_filename, report_filename, report_input_data
):
    template = getTemplate(
        package_name=None,
        template_subdir=os.path.dirname(template_filename) or ".",
        template_name=os.path.basename(template_filename),
        extensions=("jinja2.ext.do",),
    )

    def quoted(value):
        if isinstance(value, str):
            return "'%s'" % value
        else:
            return [quoted(element) for element in value]

    report_text = template.render(
        # Get the license text.
        get_distribution_license=getDistributionLicense,
        # get the distribution_name
        get_distribution_name=getDistributionName,
        # get the distribution version
        get_distribution_version=getDistributionVersion,
        # Quote a list of strings.
        quoted=quoted,
        # For checking length of lists.
        len=len,
        **report_input_data
    )

    try:
        putTextFileContents(filename=report_filename, contents=report_text)
    except OSError as e:
        reports_logger.warning(
            "Compilation report from template failed write file '%s' due to: %s."
            % (report_filename, e)
        )
    else:
        reports_logger.info(
            "Compilation report from template written to file '%s'." % report_filename
        )


_crash_report_filename = "nuitka-crash-report.xml"
_crash_report_bug_message = True


def _informAboutCrashReport():
    if _crash_report_filename is not None:
        message = (
            "Compilation crash report written to file '%s'." % _crash_report_filename
        )

        if _crash_report_bug_message:
            message += " Please include it in your bug report."

        reports_logger.info(
            message,
            style="red",
        )


def writeCompilationReports(aborted):
    report_filename = getCompilationReportFilename()
    template_specs = getCompilationReportTemplates()
    diffable = shallCreateDiffableCompilationReport()

    if (
        report_filename is None
        and aborted
        and sys.exc_info()[0] not in (KeyboardInterrupt, SystemExit)
    ):
        report_filename = _crash_report_filename

        # Inform user about bug reporting of a bug only, if this is not some sort
        # of reporting exit, these do not constitute definitive bugs of Nuitka but
        # are often usage errors only.

        # Using global here, as this is really a singleton
        # pylint: disable=global-statement
        global _crash_report_bug_message
        _crash_report_bug_message = sys.exc_info()[0] is not ReportingSystemExit

        atexit.register(_informAboutCrashReport)

    if report_filename or template_specs:
        report_input_data = _getReportInputData(aborted)

        if report_filename:
            writeCompilationReport(
                report_filename=report_filename,
                report_input_data=report_input_data,
                diffable=diffable,
            )

        for template_filename, report_filename in template_specs:
            if (
                not os.path.exists(template_filename)
                and os.path.sep not in template_filename
            ):
                candidate = os.path.join(os.path.dirname(__file__), template_filename)

                if not candidate.endswith(".rst.j2"):
                    candidate += ".rst.j2"

                if os.path.exists(candidate):
                    template_filename = candidate

            if not os.path.exists(template_filename):
                reports_logger.warning(
                    "Cannot find report template '%s' ignoring report request."
                    % template_filename
                )
                continue

            writeCompilationReportFromTemplate(
                template_filename=getNormalizedPath(template_filename),
                report_filename=getNormalizedPath(report_filename),
                report_input_data=report_input_data,
            )


#     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.
