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


"""Options module

This exposes the choices made by the user. Defaults will be applied here, and
some handling of defaults.

"""

# These are for use in option values.
# spell-checker: ignore uiaccess,noannotations,etherium
# spell-checker: ignore nodocstrings,noasserts,nowarnings,norandomization

import fnmatch
import os
import re
import shlex
import sys

from nuitka.containers.OrderedDicts import OrderedDict
from nuitka.containers.OrderedSets import OrderedSet
from nuitka.importing.StandardLibrary import isStandardLibraryPath
from nuitka.Progress import enableProgressBar
from nuitka.PythonFlavors import (
    getPythonFlavorName,
    hasAcceleratedSupportedFlavor,
    isAnacondaPython,
    isApplePython,
    isArchPackagePython,
    isCPythonOfficialPackage,
    isDebianPackagePython,
    isHomebrewPython,
    isManyLinuxPython,
    isMonolithPy,
    isMSYS2MingwPython,
    isPyenvPython,
    isPythonBuildStandalonePython,
    isTermuxPython,
    isUninstalledPython,
)
from nuitka.PythonVersions import (
    getLaunchingSystemPrefixPath,
    getNotYetSupportedPythonVersions,
    getSupportedPythonVersions,
    isDebugPython,
    isPythonWithGil,
    isStaticallyLinkedPython,
    python_release_level,
    python_version,
    python_version_str,
)
from nuitka.States import states
from nuitka.Tracing import (
    doNotBreakSpaces,
    general,
    inclusion_logger,
    onefile_logger,
    optimization_logger,
    options_logger,
    progress_logger,
    setQuiet,
)
from nuitka.utils.Execution import getExecutablePath
from nuitka.utils.FileOperations import (
    getNormalizedPathJoin,
    getReportPath,
    getUserInputNormalizedPath,
    isLegalPath,
    isNonLocalPath,
    isPathExecutable,
    openTextFile,
    resolveShellPatternToFilenames,
)
from nuitka.utils.Images import checkIconUsage
from nuitka.utils.Importing import getInlineCopyFolder
from nuitka.utils.ModuleNames import ModuleName, checkModuleName
from nuitka.utils.StaticLibraries import getSystemStaticLibPythonPath
from nuitka.utils.Utils import (
    getArchitecture,
    getCPUCoreCount,
    getLaunchingNuitkaProcessEnvironmentValue,
    getLinuxDistribution,
    getMacOSRelease,
    getOS,
    getWindowsRelease,
    hasOnefileSupportedOS,
    hasStandaloneSupportedOS,
    isAIX,
    isDebianBasedLinux,
    isFreeBSD,
    isLinux,
    isMacOS,
    isOpenBSD,
    isPosixWindows,
    isRPathUsingPlatform,
    isWin32OrPosixWindows,
    isWin32Windows,
)
from nuitka.Version import getCommercialVersion, getNuitkaVersion

from .OptionParsing import (
    parseOptions,
    run_time_variable_names,
    runSpecialCommandsFromOptions,
)

options = None
positional_args = None
extra_args = []
is_nuitka_run = None


def getUserOptions():
    """Accessor the options, only for use in managing plugin options."""
    return options


def _convertOldStylePathSpecQuotes(value):
    quote = None

    result = ""
    for c in value:
        if c == "%":
            if quote is None:
                quote = "{"
                result += quote
            elif quote == "{":
                result += "}"
                quote = None
        else:
            result += c

    return result


def checkPathSpec(value, arg_name, allow_disable):
    # There are never enough checks here and sysexit is returned,
    # pylint: disable=too-many-branches,too-many-return-statements
    old = value
    value = _convertOldStylePathSpecQuotes(value)
    if old != value:
        options_logger.warning(
            "Adapted '%s' option value from legacy quoting style to '%s' -> '%s'"
            % (arg_name, old, value)
        )

    # This changes the '/' to '\' on Windows at least.
    value = getUserInputNormalizedPath(value)

    if "\n" in value or "\r" in value:
        return options_logger.sysexit(
            "Using a new line in value '%s=%r' value is not allowed."
            % (arg_name, value)
        )

    if "{NONE}" in value:
        if not allow_disable:
            return options_logger.sysexit(
                "Using value '{NONE}' in '%s=%s' value is not allowed."
                % (arg_name, value)
            )

        if value != "{NONE}":
            return options_logger.sysexit(
                "Using value '{NONE}' in '%s=%s' value does not allow anything else used too."
                % (arg_name, value)
            )

    if "{NULL}" in value:
        if not allow_disable:
            return options_logger.sysexit(
                "Using value '{NULL}' in '%s=%s' value is not allowed."
                % (arg_name, value)
            )

        if value != "{NULL}":
            return options_logger.sysexit(
                "Using value '{NULL}' in '%s=%s' value does not allow anything else used too."
                % (arg_name, value)
            )

    if "{COMPANY}" in value and not getCompanyName():
        return options_logger.sysexit(
            "Using value '{COMPANY}' in '%s=%s' value without being specified."
            % (arg_name, value)
        )

    if "{PRODUCT}" in value and not getProductName():
        return options_logger.sysexit(
            "Using value '{PRODUCT}' in '%s=%s' value without being specified."
            % (arg_name, value)
        )

    if "{VERSION}" in value and not (getFileVersionTuple() or getProductVersionTuple()):
        return options_logger.sysexit(
            "Using value '{VERSION}' in '%s=%s' value without being specified."
            % (arg_name, value)
        )

    if value.count("{") != value.count("}"):
        return options_logger.sysexit(
            """Unmatched '{}' is wrong for '%s=%s' and may \
definitely not do what you want it to do."""
            % (arg_name, value)
        )

    # Catch nested or illegal variable names.
    var_name = None
    for c in value:
        if c in "{":
            if var_name is not None:
                return options_logger.sysexit(
                    """Nested '{' is wrong for '%s=%s'.""" % (arg_name, value)
                )
            var_name = ""
        elif c == "}":
            if var_name is None:
                return options_logger.sysexit(
                    """Stray '}' is wrong for '%s=%s'.""" % (arg_name, value)
                )

            if var_name not in run_time_variable_names:
                return onefile_logger.sysexit(
                    "Found unknown variable name '%s' in for '%s=%s'."
                    "" % (var_name, arg_name, value)
                )

            var_name = None
        else:
            if var_name is not None:
                var_name += c

    for candidate in (
        "{PROGRAM}",
        "{PROGRAM_BASE}",
        "{PROGRAM_DIR}",
        "{CACHE_DIR}",
        "{HOME}",
        "{TEMP}",
    ):
        if candidate in value[1:]:
            return options_logger.sysexit(
                """\
Absolute run time paths of '%s' can only be at the start of \
'%s=%s', using it in the middle of it is not allowed."""
                % (candidate, arg_name, value)
            )

        if candidate == value:
            return options_logger.sysexit(
                """Cannot use folder '%s', may only be the \
start of '%s=%s', using that alone is not allowed."""
                % (candidate, arg_name, value)
            )

        if value.startswith(candidate) and candidate != "{PROGRAM_BASE}":
            if value[len(candidate)] != os.path.sep:
                return options_logger.sysexit(
                    """Cannot use general system folder %s, without a path \
separator '%s=%s', just appending to these is not allowed, needs to be \
below them."""
                    % (candidate, arg_name, value)
                )

    is_legal, reason = isLegalPath(value)
    if not is_legal:
        return options_logger.sysexit(
            """Cannot use illegal paths '%s=%s', due to %s."""
            % (arg_name, value, reason)
        )

    return value


def _checkOnefileTargetSpec():
    options.onefile_tempdir_spec = checkPathSpec(
        options.onefile_tempdir_spec,
        arg_name="--onefile-tempdir-spec",
        allow_disable=False,
    )

    if os.path.normpath(options.onefile_tempdir_spec) == ".":
        return options_logger.sysexit(
            """\
Error, using '.' as a value for '--onefile-tempdir-spec' is not supported,
you cannot unpack the onefile payload into the same directory as the binary,
as that would overwrite it and cause locking issues as well."""
        )

    if options.onefile_tempdir_spec.count("{") == 0:
        options_logger.warning(
            """Not using any variables for '--onefile-tempdir-spec' should only be \
done if your program absolutely needs to be in the same path always: '%s'"""
            % options.onefile_tempdir_spec
        )

    if os.path.isabs(options.onefile_tempdir_spec):
        options_logger.warning(
            """\
Using an absolute path should be avoided unless you are targeting a \
very well known environment: anchoring it with e.g. '{TEMP}', \
'{CACHE_DIR}' is recommended: You seemingly gave the value '%s'"""
            % options.onefile_tempdir_spec
        )
    elif not options.onefile_tempdir_spec.startswith(
        ("{TEMP}", "{HOME}", "{CACHE_DIR}", "{PROGRAM_DIR}")
    ):
        options_logger.warning(
            """\
Using a path relative to the onefile executable should be avoided \
unless you are targeting a very well known environment, anchoring \
it with e.g. '{TEMP}', '{CACHE_DIR}' is recommended: '%s'"""
            % options.onefile_tempdir_spec
        )


def _getVersionInformationValues():
    yield getNuitkaVersion()
    yield "Commercial: %s" % getCommercialVersion()
    yield "Python: %s" % sys.version.split("\n", 1)[0]
    yield "Flavor: %s" % getPythonFlavorName()
    if python_version >= 0x3D0:
        yield "GIL: %s" % ("yes" if isPythonWithGil() else "no")
    yield "Executable: %s" % getReportPath(sys.executable)
    yield "OS: %s" % getOS()
    yield "Arch: %s" % getArchitecture()

    if isLinux():
        dist_name, dist_base, dist_version = getLinuxDistribution()

        if dist_base is not None:
            yield "Distribution: %s (based on %s) %s" % (
                dist_name,
                dist_base,
                dist_version,
            )
        else:
            yield "Distribution: %s %s" % (dist_name, dist_version)

    if isWin32OrPosixWindows():
        yield "WindowsRelease: %s" % getWindowsRelease()

    if isMacOS():
        yield "macOSRelease: %s" % getMacOSRelease()


def printVersionInformation():
    print("\n".join(_getVersionInformationValues()))

    from nuitka.build.SconsInterface import (
        asBoolStr,
        getCommonSconsOptions,
        runScons,
    )

    scons_options, env_values = getCommonSconsOptions()
    scons_options["compiler_version_mode"] = asBoolStr("true")

    runScons(
        scons_options=scons_options,
        env_values=env_values,
        scons_filename="CCompilerVersion.scons",
    )


def _warnAppBundleOnlyOption(option_name):
    if isMacOS() and not shallCreateAppBundle():
        if not options.github_workflow_options:
            options_logger.warning(
                """\
Note: Using app bundle specific option '%s' unless building macOS app bundle \
need to use --mode=app if you want that."""
                % option_name
            )


def _warnOnefileOnlyOption(option_name):
    if not options.is_onefile:
        if options.github_workflow_options or (isMacOS() and shallCreateAppBundle()):
            options_logger.info(
                """\
Note: Using onefile mode specific option '%s' has no effect \
with macOS app bundles."""
                % option_name
            )
        else:
            options_logger.warning(
                """\
Using onefile mode specific option '%s' has no effect \
when '--mode=onefile' is not specified."""
                % option_name
            )


def _warningModuleModeOnlyOption(option_name):
    if options.compilation_mode not in ("module", "package"):
        if options.github_workflow_options:
            options_logger.info(
                """\
Using module mode specific option '%s' has no effect \
when neither '--mode=module' or --mode='package' is \
specified."""
                % option_name
            )
        else:
            options_logger.warning(
                """\
Using module mode specific option '%s' has no effect \
when neither '--mode=module' or --mode='package' is \
specified."""
                % option_name
            )


def _warnOSSpecificOption(option_name, *supported_os):
    if getOS() not in supported_os:
        if options.github_workflow_options:
            options_logger.info(
                """\
Note: Using OS specific option '%s' has no effect on %s."""
                % (option_name, getOS())
            )
        else:
            options_logger.warning(
                """\
Using OS specific option '%s' has no effect on %s."""
                % (option_name, getOS())
            )


def _warnMacOSSpecificOption(option_name):
    _warnOSSpecificOption(option_name, "Darwin")


def _warnWindowsSpecificOption(option_name):
    _warnOSSpecificOption(option_name, "Windows")


def _warnMacOSBundleSpecificOption(option_name):
    _warnAppBundleOnlyOption(option_name)
    _warnMacOSSpecificOption(option_name)


def _checkDataDirOptionValue(data_dir, option_name):
    if "=" not in data_dir:
        return options_logger.sysexit(
            "Error, malformed '%s' value '%s' description, must specify a relative target path with '=' separating it."
            % (option_name, data_dir)
        )

    src, dst = data_dir.split("=", 1)

    if os.path.isabs(dst):
        return options_logger.sysexit(
            "Error, malformed '%s' value, must specify relative target path for data dir, not '%s' as in '%s'."
            % (option_name, dst, data_dir)
        )

    if not os.path.isdir(src):
        return options_logger.sysexit(
            "Error, malformed '%s' value, must specify existing source data directory, not '%s' as in '%s'."
            % (option_name, src, data_dir)
        )


def _checkFilenameOnlyArgument(option_name, filename, extra_message):
    if os.path.basename(filename) != filename:
        return options_logger.sysexit(
            """\
Error, '%s' value cannot contain a directory part%s."""
            % (option_name, extra_message)
        )

    is_legal, reason = isLegalPath(filename)
    if not is_legal:
        return options_logger.sysexit(
            """\
Error, '%s' value '%s' is not a legal filename: %s%s."""
            % (option_name, filename, reason, extra_message)
        )


def parseArgs():
    """Parse the command line arguments

    :meta private:
    """
    # Singleton with many cases checking the options right away, and sysexit is returned
    # pylint: disable=global-statement,too-many-branches,too-many-locals,too-many-return-statements,too-many-statements
    global is_nuitka_run, options, positional_args, extra_args

    is_nuitka_run, options, positional_args, extra_args = parseOptions(
        logger=options_logger
    )

    states.is_debug = _isDebug()
    states.is_non_debug = not states.is_debug
    states.is_full_compat = _isFullCompat()

    if hasattr(options, "experimental"):
        _experimental.update(options.experimental)

    # Dedicated option for caches, ccache and bytecode
    if options.disable_ccache:
        options.disabled_caches.append("ccache")
    if options.disable_bytecode_cache:
        options.disabled_caches.append("bytecode")
    if getattr(options, "disable_dll_dependency_cache", False):
        options.disabled_caches.append("dll-dependencies")

    states.report_missing_code_helpers = options.report_missing_code_helpers

    if options.output_folder_name is not None:
        _checkFilenameOnlyArgument(
            "--output-folder-name", options.output_folder_name, ""
        )

    states.report_missing_trust = options.report_missing_trust

    if options.zig:
        if options.clang:
            return options_logger.sysexit(
                "Error, conflicting options '--zig' and '--clang'."
            )

        if isWin32Windows() and getArchitecture() == "x86":
            return options_logger.sysexit(
                "Error, cannot use '--zig' on Windows and x86, only x86_64 is supported."
            )

        if isWin32Windows() and options.mingw64:
            return options_logger.sysexit(
                "Error, conflicting options '--zig' and '--mingw64'."
            )

        if isWin32Windows() and options.msvc_version:
            return options_logger.sysexit(
                "Error, conflicting options '--zig' and '--msvc'."
            )

        if isMacOS():
            # TODO: Keep an eye on macOS, it might become available via ziglang
            # too, and keep this synchronized with the options check.
            if "CC" not in os.environ:
                zig_path = getExecutablePath("zig")

                if zig_path is not None:
                    os.environ["CC"] = getExecutablePath("zig")
                else:
                    return options_logger.sysexit(
                        """\
Error, for macOS there is not automatic download of zig (the 'ziglang' PyPI doesn't yet \
offer it), set the 'CC' environment variable or add it to PATH."""
                    )

    if isWin32Windows() and options.mingw64 and python_version >= 0x3D0:
        return options_logger.sysexit(
            "Error, cannot use '--mingw64' on Python version 3.13 or higher."
        )

    if options.quiet or int(os.getenv("NUITKA_QUIET", "0")):
        setQuiet()

    def _quoteArg(arg):
        if arg.startswith("--"):
            # Handle values too, TODO: Maybe know what arguments use paths at
            # all and not rely on file existence checks.
            if "=" in arg:
                arg_name, value = arg.split("=", 1)

                if os.path.exists(value) and isNonLocalPath(arg):
                    value = getReportPath(value)

                if " " in value:
                    value = '"%s"' % value

                return "%s=%s" % (arg_name, value)
            else:
                return arg
        elif os.path.exists(arg) and isNonLocalPath(arg):
            arg = getReportPath(arg)
            if " " in arg:
                arg = '"%s"' % arg

            return arg
        else:
            return arg

    # This will not return if a non-compiling command is given.
    runSpecialCommandsFromOptions(options)

    if not options.version:
        options_logger.info(
            leader="Used command line options:",
            message=" ".join(doNotBreakSpaces(_quoteArg(arg) for arg in sys.argv[1:])),
        )

    if (
        getLaunchingNuitkaProcessEnvironmentValue("NUITKA_RE_EXECUTION")
        and not isAllowedToReexecute()
    ):
        return general.sysexit(
            "Error, not allowed to re-execute, but that has happened."
        )

    # Force to persist this one early.
    getLaunchingSystemPrefixPath()

    if getProgressBar() != "none":
        enableProgressBar(getProgressBar())

    if options.verbose_output:
        optimization_logger.setFileHandle(
            # Can only have unbuffered binary IO in Python3, therefore not disabling buffering here.
            openTextFile(
                getUserInputNormalizedPath(options.verbose_output),
                "w",
                encoding="utf8",
            )
        )

        options.verbose = True

    states.is_verbose = options.verbose
    states.data_composer_verbose = options.data_composer_verbose

    optimization_logger.is_quiet = not options.verbose

    if options.version:
        printVersionInformation()
        sys.exit(0)

    if options.clean_caches:
        from nuitka.CacheCleanup import cleanCaches

        cleanCaches()

        if not positional_args:
            sys.exit(0)

    if options.show_inclusion_output:
        inclusion_logger.setFileHandle(
            # Can only have unbuffered binary IO in Python3, therefore not disabling buffering here.
            openTextFile(
                getUserInputNormalizedPath(options.show_inclusion_output),
                "w",
                encoding="utf8",
            )
        )

        options.show_inclusion = True

    progress_logger.is_quiet = not options.show_progress

    if options.compilation_mode is not None:
        if (
            options.is_onefile
            or options.is_standalone
            or options.module_mode
            or options.macos_create_bundle
        ):
            return options_logger.sysexit(
                "Cannot use both '--mode=' and deprecated options that specify mode."
            )

        if options.compilation_mode == "onefile":
            options.is_onefile = True
        elif options.compilation_mode == "standalone":
            options.is_standalone = True
        elif options.compilation_mode == "module":
            options.module_mode = True
        elif options.compilation_mode == "package":
            options.module_mode = True
        elif options.compilation_mode == "app":
            if isMacOS():
                options.macos_create_bundle = True
            else:
                options.is_onefile = True
        elif options.compilation_mode == "app-dist":
            if isMacOS():
                options.macos_create_bundle = True
            else:
                options.is_standalone = True
        elif options.compilation_mode == "dll":
            options.is_standalone = True
        elif options.compilation_mode == "accelerated":
            pass
        else:
            assert False, options.compilation_mode

    # Onefile implies standalone build.
    if options.is_onefile:
        options.is_standalone = True

    # macOS bundle implies standalone build.
    if shallCreateAppBundle():
        options.is_standalone = True

    if options.is_project_mode:
        if not isStandaloneMode():
            return options_logger.sysexit(
                "Error, with '--project' you must also select a mode, e.g. '--mode=standalone' or '--mode=onefile'."
            )

    if isMacOS():
        macos_target_arch = getMacOSTargetArch()

        if macos_target_arch == "universal":
            return options_logger.sysexit(
                "Cannot create universal macOS binaries (yet), please pick an arch and create two binaries."
            )

        if (options.macos_target_arch or "native") != "native":
            from nuitka.utils.SharedLibraries import (
                hasUniversalOrMatchingMacOSArchitecture,
            )

            if not hasUniversalOrMatchingMacOSArchitecture(
                os.path.realpath(sys.executable)
            ):
                return options_logger.sysexit(
                    """\
Cannot cross compile to other arch, using non-universal Python binaries \
for macOS. Please install the "universal" Python package as offered on \
the Python download page."""
                )

        if options.macos_target_arch == "arm64" and getArchitecture() == "x86_64":
            return options_logger.sysexit(
                """
Cannot cross compile from 'x86_64' architecture Python to arm64 target. \
Please use an 'arm64' Python to create it."""
            )

    # Standalone implies no_site build unless overridden, therefore put it
    # at start of flags, so "site" can override it.
    if options.is_standalone:
        options.python_flags.insert(0, "no_site")

    # Check onefile tempdir spec.
    if options.onefile_tempdir_spec:
        _warnOnefileOnlyOption("--onefile-tempdir-spec")

        if options.is_onefile:
            _checkOnefileTargetSpec()

    # Check onefile splash image
    if options.splash_screen_image:
        if not os.path.exists(options.splash_screen_image):
            return options_logger.sysexit(
                "Error, splash screen image path '%s' does not exist."
                % options.splash_screen_image
            )

        _warnOnefileOnlyOption("--onefile-windows-splash-screen-image")

    if options.onefile_child_grace_time is not None:
        if (
            not options.onefile_child_grace_time.isdigit()
            and options.onefile_child_grace_time != "infinity"
        ):
            return options_logger.sysexit(
                """\
Error, the value given for '--onefile-child-grace-time' must be integer or 'infinity'."""
            )

        _warnOnefileOnlyOption("--onefile-child-grace-time")

    if getShallIncludeExternallyDataFilePatterns():
        _warnOnefileOnlyOption("--include-onefile-external-data")

    if not shallCreatePyiFile():
        _warningModuleModeOnlyOption("--no-pyi-file")
    if not shallCreatePyiFileContainStubs():
        _warningModuleModeOnlyOption("--no-pyi-stubs")

    if options.force_stdout_spec:
        options.force_stdout_spec = checkPathSpec(
            options.force_stdout_spec, "--force-stdout-spec", allow_disable=True
        )

    if options.force_stderr_spec:
        options.force_stderr_spec = checkPathSpec(
            options.force_stderr_spec, "--force-stderr-spec", allow_disable=True
        )

    # Provide a tempdir spec implies onefile tempdir, even on Linux.
    # Standalone mode implies an executable, not importing "site" module, which is
    # only for this machine, recursing to all modules, and even including the
    # standard library.
    if options.is_standalone:
        if options.module_mode:
            return options_logger.sysexit(
                """\
Error, conflicting options, cannot make standalone module, only executable.

Modules are supposed to be imported to an existing Python installation, therefore it
makes no sense to include a Python runtime."""
            )

    for any_case_module in getShallFollowModules():
        if any_case_module.startswith("."):
            bad = True
        else:
            for char in "/\\:":
                if char in any_case_module:
                    bad = True
                    break
            else:
                bad = False

        if bad:
            return options_logger.sysexit(
                """\
Error, '--follow-import-to' takes only module names or patterns, not directory path '%s'."""
                % any_case_module
            )

    for no_case_module in getShallFollowInNoCase():
        if no_case_module.startswith("."):
            bad = True
        else:
            for char in "/\\:":
                if char in no_case_module:
                    bad = True
                    break
            else:
                bad = False

        if bad:
            return options_logger.sysexit(
                """\
Error, '--nofollow-import-to' takes only module names or patterns, not directory path '%s'."""
                % no_case_module
            )

    scons_python = getPythonPathForScons()

    if scons_python is not None and not os.path.isfile(scons_python):
        return options_logger.sysexit(
            "Error, no such Python binary '%s', should be full path." % scons_python
        )

    output_filename = getOutputFilename()

    if output_filename is not None:
        if shallMakeModule():
            return options_logger.sysexit(
                """\
Error, may not module mode where filenames and modules matching are
mandatory."""
            )
        elif isStandaloneMode() and not isOnefileMode():
            _checkFilenameOnlyArgument(
                "--output-filename", output_filename, " for standalone mode"
            )

        output_filename_dir = os.path.dirname(output_filename) or "."

        output_dir = getOutputPath(output_filename_dir)

        if output_filename_dir != "." and not os.path.isdir(output_dir):
            return options_logger.sysexit(
                """\
Error, specified output directory does not exist, you have to create \
it before using it: '%s' (from --output-filename='%s')."""
                % (
                    output_dir,
                    output_filename,
                )
            )

    if isLinux():
        if len(getLinuxIconPaths()) > 1:
            return options_logger.sysexit("Error, can only use one icon file on Linux.")

    if isMacOS():
        if len(getMacOSIconPaths()) > 1:
            return options_logger.sysexit("Error, can only use one icon file on macOS.")

    for icon_path in getWindowsIconPaths():
        if "#" in icon_path and isWin32Windows():
            icon_path, icon_index = icon_path.rsplit("#", 1)

            if not icon_index.isdigit() or int(icon_index) < 0:
                return options_logger.sysexit(
                    "Error, icon number in '%s#%s' not valid."
                    % (icon_path + "#" + icon_index)
                )

        if getWindowsIconExecutablePath():
            return options_logger.sysexit(
                "Error, can only use icons from template executable or from icon files, but not both."
            )

    icon_exe_path = getWindowsIconExecutablePath()
    if icon_exe_path is not None and not os.path.exists(icon_exe_path):
        return options_logger.sysexit(
            "Error, icon path executable '%s' does not exist." % icon_exe_path
        )

    try:
        file_version = getFileVersionTuple()
    # Catch all the things, don't want any interface, pylint: disable=broad-except
    except Exception:
        return options_logger.sysexit(
            "Error, file version must be a tuple of up to 4 integer values."
        )

    try:
        product_version = getProductVersionTuple()
    # Catch all the things, don't want any interface, pylint: disable=broad-except
    except Exception:
        return options_logger.sysexit(
            "Error, product version must be a tuple of up to 4 integer values."
        )

    if getCompanyName() == "":
        return options_logger.sysexit(
            """Error, empty string is not an acceptable company name."""
        )

    if getProductName() == "":
        return options_logger.sysexit(
            """Error, empty string is not an acceptable product name."""
        )

    splash_screen_filename = getWindowsSplashScreen()

    if splash_screen_filename is not None:
        if not os.path.isfile(splash_screen_filename):
            return options_logger.sysexit(
                "Error, specified splash screen image '%s' does not exist."
                % splash_screen_filename
            )

    if (
        file_version
        or product_version
        or getWindowsVersionInfoStrings()
        and isWin32Windows()
    ):
        if not (file_version or product_version) and getCompanyName():
            return options_logger.sysexit(
                "Error, company name and file or product version need to be given when any version information is given."
            )

    if isAcceleratedMode() and not hasAcceleratedSupportedFlavor():
        return options_logger.sysexit(
            "Error, unsupported OS or Python flavor '%s' for accelerated mode."
            % getPythonFlavorName()
        )

    if isOnefileMode() and not hasOnefileSupportedOS():
        return options_logger.sysexit(
            "Error, unsupported OS for onefile '%s'." % getOS()
        )

    for module_pattern in getShallFollowModules():
        if (
            module_pattern.startswith("-")
            or "/" in module_pattern
            or "\\" in module_pattern
        ):
            return options_logger.sysexit(
                "Error, '--follow-import-to' options needs module name or pattern as an argument, not '%s'."
                % module_pattern
            )
    for module_pattern in getShallFollowInNoCase():
        if (
            module_pattern.startswith("-")
            or "/" in module_pattern
            or "\\" in module_pattern
        ):
            return options_logger.sysexit(
                "Error, '--nofollow-import-to' options needs module name or pattern as an argument, not '%s'."
                % module_pattern
            )

    for data_file_desc in options.data_files:
        if "=" not in data_file_desc:
            return options_logger.sysexit(
                "Error, malformed data file description, must specify relative target path separated with '='."
            )

        if data_file_desc.count("=") == 1:
            src, dst = data_file_desc.split("=", 1)
            src = os.path.expanduser(src)
            src_pattern = src
        else:
            src, dst, pattern = data_file_desc.split("=", 2)
            src = os.path.expanduser(src)
            src_pattern = os.path.join(src, pattern)

        filenames = resolveShellPatternToFilenames(src_pattern)

        if len(filenames) > 1 and not dst.endswith(("/", os.path.sep)):
            return options_logger.sysexit(
                "Error, pattern '%s' matches more than one file, but target has no trailing slash, not a directory."
                % src
            )

        if not filenames:
            return options_logger.sysexit("Error, '%s' does not match any files." % src)

        if os.path.isabs(dst):
            return options_logger.sysexit(
                "Error, must specify relative target path for data file, not absolute path '%s'."
                % data_file_desc
            )

    for data_dir in options.data_dirs:
        _checkDataDirOptionValue(data_dir=data_dir, option_name="--include-data-dir")

    for data_dir in options.raw_dirs:
        _checkDataDirOptionValue(data_dir=data_dir, option_name="--include-raw-dir")

    for pattern in getShallFollowExtraFilePatterns():
        if os.path.isdir(pattern):
            return options_logger.sysexit(
                "Error, pattern '%s' given to '--include-plugin-files' cannot be a directory name."
                % pattern
            )

    for directory_name in getShallFollowExtra():
        if not os.path.isdir(directory_name):
            return options_logger.sysexit(
                "Error, value '%s' given to '--include-plugin-directory' must be a directory name."
                % directory_name
            )

        if isStandardLibraryPath(directory_name):
            return options_logger.sysexit(
                """\
Error, directory '%s' given to '--include-plugin-directory' must not be a \
standard library path. Use '--include-module' or '--include-package' \
options instead."""
                % pattern
            )

    if options.static_libpython == "yes" and getSystemStaticLibPythonPath() is None:
        usable, reason = _couldUseStaticLibPython()

        return options_logger.sysexit(
            """\
Error, a static libpython is either not found or not supported for \
this Python (%s) installation: %s"""
            % (
                getPythonFlavorName(),
                (reason if not usable else "unknown reason"),
            )
        )

    if shallUseStaticLibPython() and getSystemStaticLibPythonPath() is None:
        return options_logger.sysexit(
            """Error, usable static libpython is not found for this Python installation. You \
might be missing required packages. Disable with --static-libpython=no" if you don't \
want to install it."""
        )

    if isApplePython():
        if isStandaloneMode():
            return options_logger.sysexit(
                """\
Error, on macOS, for standalone mode, Apple Python is not supported \
due to being tied to specific OS releases, use e.g. CPython instead \
which is available from https://www.python.org/downloads/macos/ for \
download. With that, your program will work on macOS 10.9 or higher."""
            )

        if str is bytes:
            return options_logger.sysexit(
                "Error, Apple Python 2.7 from macOS is not usable as per Apple decision, use e.g. CPython 2.7 instead."
            )

    if isStandaloneMode() and isRPathUsingPlatform() and not isMacOS():
        # Cyclic dependency
        from nuitka.utils.SharedLibraries import (
            checkPatchElfPresenceAndUsability,
        )

        checkPatchElfPresenceAndUsability(options_logger)

    pgo_executable = getPgoExecutable()
    if pgo_executable and not isPathExecutable(pgo_executable):
        return options_logger.sysexit(
            "Error, path '%s' to binary to use for PGO is not executable."
            % pgo_executable
        )

    if (
        isOnefileMode()
        and isTermuxPython()
        and getExecutablePath("termux-elf-cleaner") is None
    ):
        return options_logger.sysexit(
            """\
Error, onefile mode on Termux requires 'termux-elf-cleaner' to be installed, \
use 'pkg install termux-elf-cleaner' to use it."""
        )

    for user_yaml_filename in getUserProvidedYamlFiles():
        if not os.path.exists(user_yaml_filename):
            return options_logger.sysexit(
                """\
Error, cannot find user provided yaml file '%s'."""
                % user_yaml_filename
            )

    # This triggers checks inside that code
    getCompilationReportUserData()


def commentArgs():
    """Comment on options, where we know something is not having the intended effect.

    :meta private:

    """
    # A ton of cases to consider.
    # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements

    # Check files to exist or be suitable first before giving other warnings.
    for filename in getMainEntryPointFilenames():
        if not os.path.exists(filename):
            return general.sysexit("Error, file '%s' is not found." % filename)

        if isStandardLibraryPath(filename):
            return general.sysexit(
                """\
Error, '%s' is in the standard library, compiling files from there \
as main files is not supported."""
                % filename
            )

        if (shallMakeModule() or isStandaloneMode()) and os.path.normcase(
            os.path.basename(filename)
        ) == "__init__.py":
            return general.sysexit(
                """\
Error, to compile a package, specify its directory but, not the '__init__.py'."""
            )

        if os.path.normcase(os.path.basename(filename)) == "__main__.py":
            general.warning(
                """\
To compile a package with a '__main__' module, specify its containing
directory but, not the '__main__.py' itself, also consider if
'--python-flag=-m' should be used."""
            )

    # Inform the user about potential issues with the running version. e.g. unsupported
    # version.
    if python_version_str not in getSupportedPythonVersions():
        # Do not disturb run of automatic tests with, detected from the presence of
        # that environment variable.
        if "PYTHON" not in os.environ:
            general.warning(
                """\
The Python version '%s' is only experimentally supported by Nuitka '%s', \
but an upcoming release will change that. In the mean time use Python \
version '%s' instead or newer Nuitka."""
                % (
                    python_version_str,
                    getNuitkaVersion(),
                    getSupportedPythonVersions()[-1],
                )
            )

    if python_release_level not in ("final", "candidate"):
        if python_version_str not in getNotYetSupportedPythonVersions():
            return general.sysexit(
                """\
Non-final versions '%s' '%s' are not supported by Nuitka, use the \
final version instead."""
                % (python_version_str, python_release_level)
            )

    if python_version_str in getNotYetSupportedPythonVersions():
        if python_release_level != "final" and not isExperimental(
            "python" + python_version_str
        ):
            general.warning(
                """\
The Python version '%s' level '%s' is only experimentally supported \
by Nuitka and recommended only for use in Nuitka development and \
testing."""
                % (python_version_str, python_release_level)
            )

        elif not isExperimental("python" + python_version_str):
            return general.sysexit(
                """\
The Python version '%s' is not supported by Nuitka '%s', but an upcoming \
release will add it. In the mean time use '%s' instead."""
                % (
                    python_version_str,
                    getNuitkaVersion(),
                    getSupportedPythonVersions()[-1],
                )
            )

    if not isPythonWithGil():
        general.warning(
            """\
The Python without GIL is only experimentally supported by \
and recommended only for use in Nuitka development and testing."""
        )

    if python_version == 0x3D4 and isWin32Windows() and isCPythonOfficialPackage():
        return general.sysexit(
            """\
Due to a CPython bug of 3.13.4 precisely (<=3.13.4 is OK and >=3.13.5
as well) it's not possible to use Nuitka with this Python version, as \
it broke the separation between GIL and no-GIL for the link \
library. Please upgrade/downgrade to a supported micro version."""
        )

    default_reference_mode = (
        "runtime" if shallMakeModule() or isStandaloneMode() else "original"
    )

    if getFileReferenceMode() is None:
        options.file_reference_mode = default_reference_mode
    else:
        if options.file_reference_mode != default_reference_mode:
            options_logger.warning(
                "Using non-default file reference mode '%s' rather than '%s' may cause run time issues."
                % (getFileReferenceMode(), default_reference_mode)
            )
        else:
            options_logger.info(
                "Using default file reference mode '%s' need not be specified."
                % default_reference_mode
            )

    default_mode_name_mode = "runtime" if shallMakeModule() else "original"

    if getModuleNameMode() is None:
        options.module_name_mode = default_mode_name_mode
    elif getModuleNameMode() == default_mode_name_mode:
        options_logger.info(
            "Using module name mode '%s' need not be specified."
            % default_mode_name_mode
        )

    if getWindowsIconExecutablePath():
        _warnWindowsSpecificOption("--windows-icon-from-exe")
    if shallAskForWindowsAdminRights():
        _warnWindowsSpecificOption("--windows-uac-admin")
    if shallAskForWindowsUIAccessRights():
        _warnWindowsSpecificOption("--windows-uac-uiaccess")
    if getWindowsSplashScreen():
        _warnWindowsSpecificOption("--onefile-windows-splash-screen-image")
    if options.mingw64 is not None:
        _warnWindowsSpecificOption("--mingw64")
    if options.msvc_version is not None:
        _warnWindowsSpecificOption("--msvc")
    if options.macos_target_arch is not None:
        _warnMacOSSpecificOption("--macos-target-arch")
    if options.macos_create_bundle is not None:
        _warnMacOSSpecificOption("--macos-create-app-bundle")
    if options.macos_sign_identity is not None:
        _warnMacOSSpecificOption("--macos-sign-identity")
    if options.macos_sign_notarization is not None:
        _warnMacOSSpecificOption("--macos-sign-notarization")
    if getMacOSAppName():
        _warnMacOSBundleSpecificOption("--macos-app-name")
    if getMacOSSignedAppName():
        _warnMacOSBundleSpecificOption("--macos-signed-app-name")
    if getMacOSAppVersion():
        _warnMacOSBundleSpecificOption("--macos-app-version")
    if options.macos_protected_resources:
        _warnMacOSBundleSpecificOption("--macos-app-protected-resource")
    if options.macos_app_mode is not None:
        _warnMacOSBundleSpecificOption("--macos-app-mode")
    if options.macos_create_dmg:
        _warnMacOSBundleSpecificOption("--macos-app-create-dmg")
    if options.macos_prohibit_multiple_instances:
        _warnMacOSBundleSpecificOption("--macos-prohibit-multiple-instances")
    if options.macos_app_console_mode is not None:
        _warnMacOSBundleSpecificOption("--macos-app-console-mode")
    if getMacOSSigningCertificatePassword():
        _warnMacOSBundleSpecificOption("--macos-sign-keyring-password")

    cert_filename = getMacOSSigningCertificateFilename()
    if cert_filename is not None:
        _warnMacOSSpecificOption("--macos-sign-keyring-filename")

        if not os.path.exists(cert_filename):
            return options_logger.sysexit(
                "Error, signing certificate file '%s' does not exist." % cert_filename
            )

    if options.msvc_version:
        if isMSYS2MingwPython() or isPosixWindows():
            return options_logger.sysexit("Requesting MSVC on MSYS2 is not allowed.")

        if isMingw64():
            return options_logger.sysexit(
                "Requesting both Windows specific compilers makes no sense."
            )

    if getMsvcVersion() and getMsvcVersion() not in ("list", "latest"):
        if getMsvcVersion().count(".") != 1 or not all(
            x.isdigit() for x in getMsvcVersion().split(".")
        ):
            return options_logger.sysexit(
                "For '--msvc' only values 'latest', 'info', and 'X.Y' values are allowed, but not '%s'."
                % getMsvcVersion()
            )

    try:
        getJobLimit()
    except ValueError:
        return options_logger.sysexit(
            "For '--jobs' value, use integer values only, not, but not '%s'."
            % options.jobs
        )

    if isOnefileMode():
        standalone_mode = "onefile"
    elif isStandaloneMode():
        standalone_mode = "standalone"
    else:
        standalone_mode = None

    if standalone_mode and not hasStandaloneSupportedOS():
        options_logger.warning(
            "Standalone mode on %s is not known to be supported, might fail to work."
            % getOS()
        )

    if options.follow_all is True and shallMakeModule():
        return optimization_logger.sysexit(
            """\
In module mode you must follow modules more selectively, and e.g. should \
not include standard library or all foreign modules or else it will fail \
to work. You need to instead selectively add them with \
'--follow-import-to=name' though."""
        )

    if options.follow_all is True and standalone_mode:
        options_logger.info(
            "Following all imports is the default for %s mode and need not be specified."
            % standalone_mode
        )

    if options.follow_all is False and standalone_mode:
        options_logger.warning(
            "Following no imports is unlikely to work for %s mode and should not be specified."
            % standalone_mode
        )

    if options.follow_stdlib:
        if standalone_mode:
            options_logger.warning(
                "Following imports to stdlib is the default in standalone mode.."
            )
        else:
            options_logger.warning(
                "Following imports to stdlib not well tested and should not be specified."
            )

    if (
        not shallCreatePythonPgoInput()
        and not isStandaloneMode()
        and not shallMakePackage()
        and options.follow_all is None
        and not options.follow_modules
        and not options.follow_stdlib
        and not options.include_modules
        and not options.include_packages
        and not options.include_extra
        and not options.follow_not_modules
    ):
        options_logger.warning(
            """You did not specify to follow or include anything but main %s. Check options and \
make sure that is intended."""
            % ("module" if shallMakeModule() else "program")
        )

    if options.dependency_tool:
        options_logger.warning(
            "Using removed option '--windows-dependency-tool' is deprecated and has no impact anymore."
        )

    if shallMakeModule() and options.static_libpython == "yes":
        options_logger.warning(
            "In module mode, providing '--static-libpython' has no effect, it's not used."
        )

        options.static_libpython = "no"

    if (
        not isCPgoMode()
        and not isPythonPgoMode()
        and (getPgoArgs() or getPgoExecutable())
    ):
        optimization_logger.warning(
            "Providing PGO arguments without enabling PGO mode has no effect."
        )

    if isCPgoMode():
        if isStandaloneMode():
            optimization_logger.warning(
                """\
Using C level PGO with standalone/onefile mode is not \
currently working. Expect errors."""
            )

        if shallMakeModule():
            optimization_logger.warning(
                """\
Using C level PGO with module mode is not currently \
working. Expect errors."""
            )

    if (
        options.static_libpython == "auto"
        and not shallMakeModule()
        and not shallUseStaticLibPython()
        and getSystemStaticLibPythonPath() is not None
        and not shallUsePythonDebug()
    ):
        options_logger.info(
            """Detected static libpython to exist, consider '--static-libpython=yes' for better performance, \
but errors may happen."""
        )

    if not shallExecuteImmediately():
        if shallRunInDebugger():
            options_logger.warning(
                "The '--debugger' option has no effect outside of '--debug' without '--run' option."
            )

    if shallUsePythonDebug() and not isDebugPython():
        return general.sysexit(
            """\
Error, for using the debug Python version, you need to run it will that version
and not with the non-debug version.
"""
        )

    if shallCreateAppBundle() and not options.macos_icon_path:
        options_logger.warning(
            """\
For application bundles, you ought to specify an icon with '--macos-app-icon=...' \
otherwise a dock icon may not be present. Specify the value as 'none' value \
to disable this warning."""
        )

    if (
        isMacOS()
        and shallUseSigningForNotarization()
        and getMacOSSigningIdentity() == "-"
    ):
        return general.sysexit(
            """\
Error, need to provide signing identity with '--macos-sign-identity' for \
notarization capable signature, the default identify 'ad-hoc' is not going \
to work."""
        )

    if (
        isWin32Windows()
        and 0x340 <= python_version < 0x380
        and getWindowsConsoleMode() != "disable"
    ):
        general.warning(
            """\
On Windows, support for input/output on the console Windows, does \
not work on non-UTF8 systems, unless Python 3.8 or higher is used \
but this is %s, so please consider upgrading, or disabling the \
console window for deployment.
"""
            % python_version_str,
            mnemonic="old-python-windows-console",
        )

    if shallMakeModule() and (getForcedStderrPath() or getForcedStdoutPath()):
        general.warning(
            """\
Extension modules do not control process outputs, therefore the \
options '--force-stdout-spec' and '--force-stderr-spec' have no \
impact and should not be specified."""
        )

    if shallMakeModule() and options.console_mode is not None:
        general.warning(
            """\
Extension modules are not binaries, and therefore the option \
'--windows-console-mode' does not have an impact and should \
not be specified."""
        )

    if options.disable_console in (True, False):
        if isWin32Windows():
            general.warning(
                """\
The old console option '%s' should not be given anymore, use '%s' \
instead. It also has the extra mode 'attach' to consider."""
                % (
                    (
                        "--disable-console"
                        if options.disable_console
                        else "--enable-console"
                    ),
                    "--windows-console-mode=%s"
                    % ("disable" if options.disable_console else "force"),
                )
            )
        else:
            general.warning(
                """The old console option '%s' should not be given anymore, and doesn't
have any effect anymore on non-Windows."""
                % (
                    "--disable-console"
                    if options.disable_console
                    else "--enable-console"
                )
            )

    if (
        isWin32Windows()
        and getWindowsVersionInfoStrings()
        and getProductFileVersion() is None
    ):
        return options_logger.sysexit(
            """\
Error, when providing version information on Windows, you must also
provide either '--product-version' or '--file-version' as these can
not have good defaults, but are forced to be present by the OS."""
        )

    if (
        options.macos_target_arch not in ("native", "universal", None)
        and getArchitecture() != options.macos_target_arch
    ):
        options_logger.warning(
            """\
Do not cross compile using '--macos-target-arch=%s, instead execute with '%s'."""
            % (
                doNotBreakSpaces(
                    options.macos_target_arch,
                    "arch -%s %s" % (options.macos_target_arch, sys.executable),
                )
            )
        )


def isVerbose():
    """:returns: bool derived from ``--verbose``"""
    return states.is_verbose is True


def shallTraceExecution():
    """:returns: bool derived from ``--trace-execution``"""
    return options.trace_execution


def shallExecuteImmediately():
    """:returns: bool derived from ``--run``"""
    return options is not None and options.immediate_execution


def shallRunInDebugger():
    """:returns: bool derived from ``--debugger``"""
    return bool(options.debugger)


def getDebuggerName():
    """:returns: str derived from ``--debugger-choice=...`` or NUITKA_DEBUGGER_CHOICE"""

    # Don't get asked about this, unless it's relevant, all calls to this
    # function should be guarded by that call on the outside.
    assert shallRunInDebugger()

    if options.debugger_choice is None:
        return os.getenv("NUITKA_DEBUGGER_CHOICE")

    return options.debugger_choice


def getXMLDumpOutputFilename():
    """:returns: str derived from ``--xml``"""
    return options.xml_output


def shallOnlyExecCCompilerCall():
    """:returns: bool derived from ``--recompile-c-only``"""
    return options.recompile_c_only


def shallNotDoExecCCompilerCall():
    """:returns: bool derived from ``--generate-c-only``"""
    return options.generate_c_only


def getFileReferenceMode():
    """*str*, one of "runtime", "original", "frozen", coming from ``--file-reference-choice``

    Notes:
        Defaults to runtime for modules and packages, as well as standalone binaries,
        otherwise original is kept.
    """

    return options.file_reference_mode


def getModuleNameMode():
    """*str*, one of "runtime", "original", coming from ``--module-name-choice``

    Notes:
        Defaults to runtime for modules and packages, otherwise original is kept.
    """

    return options.module_name_mode


def shallMakeModule():
    """:returns: bool derived from ``--mode=module|package``."""
    return options is not None and options.module_mode


def shallMakePackage():
    """:returns: bool derived from ``--mode=package``."""
    return options is not None and options.compilation_mode == "package"


def isMakeOnefileDllMode():
    if not isOnefileMode():
        return False

    if isExperimental("onefile-dll"):
        return True
    if isExperimental("onefile-no-dll"):
        return False

    if options is not None and options.onefile_no_dll:
        return False

    if isWin32Windows() and isOnefileTempDirMode():
        return True

    # Static libpython is problematic on Linux still and macOS too seems to need
    # work, but there the DLL mode is not as useful yet anyway.
    return False


def shallMakeDll():
    """:returns: bool derived from ``--mode=dll``."""
    return options is not None and (
        options.compilation_mode == "dll" or isMakeOnefileDllMode()
    )


def shallMakeExe():
    """:returns: bool derived from not using ``--mode=dll|module|package`` ."""
    return not shallMakeModule() and not shallMakeDll()


def shallCreatePyiFile():
    """*bool* = **not** ``--no-pyi-file``"""
    return options.pyi_file


def shallCreatePyiFileContainStubs():
    """*bool* = **not** ``--no-pyi-stubs``"""
    return options.pyi_stubs


def isAllowedToReexecute():
    """*bool* = **not** ``--must-not-re-execute``"""
    return options.allow_reexecute


def shallFollowStandardLibrary():
    """:returns: bool derived from ``--follow-stdlib``"""
    return options.follow_stdlib


def shallFollowNoImports():
    """:returns: bool derived from ``--nofollow-imports``"""
    return options.follow_all is False


def shallFollowAllImports():
    """:returns: bool derived from ``--follow-imports``"""
    if shallCreatePythonPgoInput() and options.is_standalone:
        return True

    return options.is_standalone or options.follow_all is True


def _splitShellPattern(value):
    return value.split(",") if "{" not in value else [value]


def getShallFollowInNoCase():
    """*list*, items of ``--nofollow-import-to=``"""
    return sum([_splitShellPattern(x) for x in options.follow_not_modules], [])


def getShallFollowModules():
    """*list*, items of ``--follow-import-to=`` amended with what ``--include-module`` and ``--include-package`` got"""
    return sum(
        [
            _splitShellPattern(x)
            for x in options.follow_modules
            + options.include_modules
            + options.include_packages
        ],
        [],
    )


def getShallFollowExtra():
    """*list*, items of ``--include-plugin-directory=``"""
    return sum([_splitShellPattern(x) for x in options.include_extra], [])


def getShallFollowExtraFilePatterns():
    """*list*, items of ``--include-plugin-files=``"""
    return sum([_splitShellPattern(x) for x in options.include_extra_files], [])


def getMustIncludeModules():
    """*list*, items of ``--include-module=``"""
    return OrderedSet(sum([_splitShellPattern(x) for x in options.include_modules], []))


def getMustIncludePackages():
    """*list*, items of ``--include-package=``"""
    return OrderedSet(
        sum([_splitShellPattern(x) for x in options.include_packages], [])
    )


def getShallIncludeDistributionMetadata():
    """*list*, items of ``--include-distribution-metadata=``"""
    return sum(
        [_splitShellPattern(x) for x in options.include_distribution_metadata], []
    )


def getShallIncludePackageData():
    """*iterable of (module name, filename pattern)*, derived from ``--include-package-data=``

    The filename pattern can be None if not given. Empty values give None too.
    """
    result = []

    for package_data_pattern in sum(
        [_splitShellPattern(x) for x in options.package_data], []
    ):
        if ":" in package_data_pattern:
            package_name, filename_pattern = package_data_pattern.split(":", 1)
            # Empty equals None.
            filename_pattern = filename_pattern or None
        else:
            package_name = package_data_pattern
            filename_pattern = None

        if not package_name:
            options_logger.sysexit(
                "Error, '--include-package-data' does not accept empty values as in '%s'."
                % package_data_pattern
            )

        if (
            package_name.startswith("-")
            or "/" in package_name
            or "\\" in package_name
            or "=" in package_name
            or not checkModuleName(package_name)
        ):
            return options_logger.sysexit(
                """\
Error, '--include-package-data' needs package name (optionally with a pattern \
separated by ':') as an argument, not '%s'."""
                % package_name
            )

        result.append((ModuleName(package_name), filename_pattern))

    return tuple(result)


def getShallIncludeDataFiles():
    """*list*, items of ``--include-data-files=``"""
    for data_file_desc in options.data_files:
        if data_file_desc.count("=") == 1:
            src, dest = data_file_desc.split("=", 1)

            for pattern in _splitShellPattern(src):
                yield (
                    getUserInputNormalizedPath(pattern),
                    None,
                    dest,
                    data_file_desc,
                )
        else:
            src, dest, pattern = data_file_desc.split("=", 2)

            for pattern in _splitShellPattern(pattern):
                yield (
                    getNormalizedPathJoin(
                        getUserInputNormalizedPath(src),
                        getUserInputNormalizedPath(pattern),
                    ),
                    src,
                    dest,
                    data_file_desc,
                )


def getShallIncludeDataDirs():
    """*list*, items of ``--include-data-dir=``"""
    for data_file in options.data_dirs:
        src, dest = data_file.split("=", 1)

        yield getUserInputNormalizedPath(src), dest


def getShallIncludeRawDirs():
    """*list*, items of ``--include-raw-dir=``"""
    for data_file in options.raw_dirs:
        src, dest = data_file.split("=", 1)

        yield getUserInputNormalizedPath(src), dest


def getShallNotIncludeDataFilePatterns():
    """*list*, items of ``--noinclude-data-files=``"""

    return options.data_files_inhibited


def getShallIncludeExternallyDataFilePatterns():
    """*list*, items of ``--include-data-files-external=``"""

    return options.data_files_external


def getShallNotIncludeDllFilePatterns():
    """*list*, items of ``--noinclude-dlls=``"""

    return options.dll_files_inhibited


def getWindowsRuntimeDllsInclusionOption():
    """*str*, value of ``--include-windows-runtime-dlls=``"""

    return options.include_windows_runtime_dlls


def shallWarnImplicitRaises():
    """:returns: bool derived from ``--warn-implicit-exceptions``"""
    return options.warn_implicit_exceptions


def shallWarnUnusualCode():
    """:returns: bool derived from ``--warn-unusual-code``"""
    return options.warn_unusual_code


def assumeYesForDownloads():
    """:returns: bool derived from ``--assume-yes-for-downloads``"""
    return options is not None and options.assume_yes_for_downloads


def _isDebug():
    """:returns: bool derived from ``--debug`` or ``--debugger``"""
    return options is not None and (options.debug or options.debugger)


def shallUsePythonDebug():
    """:returns: bool derived from ``--python-debug`` or ``sys.flags.debug``

    Passed to Scons as ``python_debug`` so it can consider it when picking
    link libraries to choose the correct variant. Also enables the define
    ``Py_DEBUG`` for C headers. Reference counting checks and other debug
    asserts of Python will happen in this mode.

    """
    return options.python_debug or sys.flags.debug


def isUnstripped():
    """:returns: bool derived from ``--unstripped`` or ``--profile``

    A binary is called stripped when debug information is not present, an
    unstripped when it is present. For profiling and debugging it will be
    necessary, but it doesn't enable debug checks like ``--debug`` does.

    Passed to Scons as ``unstripped_mode`` to it can ask the linker to
    include symbol information.
    """
    return options.unstripped or isRuntimeProfile() or states.is_debug


def isRuntimeProfile():
    """:returns: bool derived from ``--debug-profile-runtime``"""
    return options.debug_profile_runtime


def isCompileTimeProfile():
    """:returns: bool derived from ``--devel-profile-compilation``"""
    return options.devel_profile_compilation


def shallGenerateReadableCode():
    """:returns: bool derived from ``--devel-generate-readable-code``"""
    return options.devel_generate_readable_code or _isDebug()


def shallCreateGraph():
    """:returns: bool derived from ``--internal-graph``"""
    return options.internal_graph


def getOutputFilename():
    """*str*, value of "-o" """
    return (
        getUserInputNormalizedPath(options.output_filename)
        if options.output_filename is not None
        else None
    )


def getOutputPath(path):
    """Return output pathname of a given path (filename)."""
    return getNormalizedPathJoin(getOutputDir(), path)


def getOutputDir():
    """*str*, value of ``--output-dir`` or "." """
    return getUserInputNormalizedPath(options.output_dir) if options.output_dir else "."


def getPositionalArgs():
    """*tuple*, command line positional arguments"""
    return tuple(positional_args)


def getMainArgs():
    """*tuple*, arguments following the optional arguments"""
    return tuple(extra_args)


def getMainEntryPointSpecs():
    """*tuple*, main entry points, none, one or more"""
    result = []

    for main_entry_point in options.main_entry_points:
        if "=" not in main_entry_point:
            options_logger.sysexit(
                "Error, invalid value for main entry point '%s', must be 'binary=module:function'"
                % main_entry_point
            )

        binary_name, rest = main_entry_point.split("=", 1)
        binary_name = binary_name.strip()

        if ":" not in rest:
            options_logger.sysexit(
                "Error, invalid value for main entry point '%s', must be 'binary=module:function'"
                % main_entry_point
            )

        module_name, function_name = rest.split(":", 1)
        module_name = module_name.strip()
        function_name = function_name.strip()

        result.append((binary_name, module_name, function_name))

    return tuple(result)


def getMainEntryPointFilenames():
    """*tuple*, main programs, none, one or more"""
    if options.mains:
        if len(options.mains) == 1:
            assert not positional_args

        result = tuple(options.mains)
    else:
        result = (positional_args[0],)

    return tuple(getUserInputNormalizedPath(r) for r in result)


def addMainEntryPointFilename(filename):
    if options.mains is None:
        options.mains = []
    options.mains.append(filename)


def isMultidistMode():
    return options is not None and options.mains and len(options.mains) > 1


def shallOptimizeStringExec():
    """Inactive yet"""
    return False


_shall_use_static_lib_python = None


def _couldUseStaticLibPython():
    # many cases and return driven,
    # pylint: disable=too-many-branches,too-many-return-statements

    # MonolithPy is good to to static linking.
    if isMonolithPy():
        return True, "MonolithPy is unexpectedly broken."

    if isHomebrewPython():
        return True, "Homebrew Python is unexpectedly broken."

    # Debian packages with are usable if the OS is new enough
    from nuitka.utils.StaticLibraries import isDebianSuitableForStaticLinking

    if (
        isDebianBasedLinux()
        and isDebianPackagePython()
        and isDebianSuitableForStaticLinking()
        and not shallUsePythonDebug()
    ):
        if python_version >= 0x3E0:
            return False, "Not yet supporting 3.14+ static linking"

        if python_version >= 0x3C0 and not os.path.exists(
            getInlineCopyFolder("python_hacl")
        ):
            return (
                False,
                "Nuitka on Debian-Python needs inline copy of hacl not included.",
            )

        return True, "Nuitka on Debian-Python needs package '%s' installed." % (
            "python2-dev" if str is bytes else "python3-dev"
        )

    if isMSYS2MingwPython():
        return True, "Nuitka on MSYS2 needs package 'python-devel' installed."

    # For Anaconda default to trying static lib python library, which
    # normally is just not available or if it is even unusable.
    if isAnacondaPython():
        if isMacOS():
            # TODO: Maybe some linker options can make it happen.
            return (
                False,
                "Anaconda on macOS exports not all symbols when using it.",
            )
        elif not isWin32Windows():
            return (
                True,
                """\
Nuitka on Anaconda needs package for static libpython installed. \
Execute 'conda install libpython-static'.""",
            )

    if isPythonBuildStandalonePython():
        return (
            False,
            """\
Static link library of '%s' is currently using dependent on libraries \
such as tcl that are not included, but would be needed. Please help them \
improve it for best performance of the result."""
            % getPythonFlavorName(),
        )

    if isPyenvPython():
        return True, "Nuitka on pyenv should not use '--enable-shared'."

    if isManyLinuxPython():
        return (
            True,
            """\
Nuitka on 'manylinux' has no shared libraries. Use container with \
the command 'RUN cd /opt/_internal && tar xf static-libs-for-embedding-only.tar.xz' \
added to provide the static link library.""",
        )

    if isMacOS() and isCPythonOfficialPackage():
        if python_version >= 0x3E0:
            return False, "Not yet supporting 3.14+ static linking"
        return True, None

    if isArchPackagePython():
        return True, None

    # If not dynamic link library is available, the static link library will
    # have to do it.
    if isStaticallyLinkedPython():
        return True, None

    return None, None


def _shallUseStaticLibPython():
    if shallMakeModule():
        return False, "not used in module mode"

    if options.static_libpython == "auto":
        result = _couldUseStaticLibPython()

        if result[0] is not None:
            return result

    return options.static_libpython == "yes", None


def shallUseStaticLibPython():
    """:returns: bool derived from ``--static-libpython=yes|auto`` and not module mode

    Notes:
        Currently only Anaconda on non-Windows can do this and MSYS2.
    """

    global _shall_use_static_lib_python  # singleton, pylint: disable=global-statement

    if _shall_use_static_lib_python is None:
        _shall_use_static_lib_python, reason = _shallUseStaticLibPython()

        if _shall_use_static_lib_python and reason:
            static_libpython = getSystemStaticLibPythonPath()

            if not static_libpython:
                options_logger.sysexit(
                    """\
Automatic detection of static libpython failed. %s Disable with '--static-libpython=no' if you don't \
want to install it."""
                    % reason
                )

    return _shall_use_static_lib_python


def shallTreatUninstalledPython():
    """*bool* = derived from Python installation and modes

    Notes:
        Not done for standalone mode obviously. The Python DLL will
        be a dependency of the executable and treated that way.

        Also not done for extension modules, they are loaded with
        a Python runtime available.

        Most often uninstalled Python versions are self compiled or
        from Anaconda.
    """

    if shallMakeModule() or isStandaloneMode():
        return False

    return isUninstalledPython()


def shallCreateScriptFileForExecution():
    """*bool* = derived from Python installation and modes

    Notes: Mostly for accelerated mode with uninstalled python, to make sure
    they find their Python DLL and Python packages. AIX needs help in standalone
    mode too.
    """

    # Accelerated mode
    if shallTreatUninstalledPython():
        return True

    # AIX standalone, but not onefile mode.
    if isAIX() and not isOnefileMode() and isStandaloneMode():
        return True

    return False


def isShowScons():
    """:returns: bool derived from ``--show-scons``"""
    return options.show_scons


def getJobLimit():
    """*int*, value of ``--jobs`` / "-j" or number of CPU kernels"""
    jobs = options.jobs

    # Low memory has a default of 1.
    if jobs is None and isLowMemory():
        return 1

    if jobs is None:
        result = getCPUCoreCount()
    else:
        result = int(jobs)

        if result <= 0:
            result = max(1, getCPUCoreCount() + result)

    return result


def getLtoMode():
    """:returns: bool derived from ``--lto``"""
    return options.lto


def isClang():
    """:returns: bool derived from ``--clang`` or enforced by platform, e.g. macOS or FreeBSD some targets."""

    return (
        options.clang
        or isMacOS()
        or isOpenBSD()
        or (isFreeBSD() and getArchitecture() != "powerpc")
        or isTermuxPython()
    )


def isMingw64():
    """:returns: bool derived from ``--mingw64``, available only on Windows, otherwise false"""
    if isWin32Windows():
        return bool(options.mingw64 or isMSYS2MingwPython())
    else:
        return None


def isZig():
    return options.zig


def getMsvcVersion():
    """:returns: str derived from ``--msvc`` on Windows, otherwise None"""
    if isWin32Windows():
        return options.msvc_version
    else:
        return None


def shallCleanCache(cache_name):
    """:returns: bool derived from ``--clean-cache``"""

    if cache_name in ("clcache", "zig"):
        cache_name = "ccache"

    return "all" in options.clean_caches or cache_name in options.clean_caches


def shallDisableCacheUsage(cache_name):
    """:returns: bool derived from ``--disable-cache``"""
    if options is None:
        return False

    return "all" in options.disabled_caches or cache_name in options.disabled_caches


def shallDisableCCacheUsage():
    """:returns: bool derived from ``--disable-ccache`` or ``--disable--cache=ccache``"""
    return shallDisableCacheUsage("ccache")


def shallDisableBytecodeCacheUsage():
    """:returns: bool derived from ``--disable-bytecode-cache``"""
    return shallDisableCacheUsage("bytecode")


def shallDisableCompressionCacheUsage():
    """:returns: bool derived from ``--disable-cache=compression``"""
    return shallDisableCacheUsage("compression")


def getWindowsConsoleMode():
    """:returns: str from ``--windows-console-mode``"""
    if options.disable_console is True:
        return "disable"
    if options.disable_console is False:
        return "force"
    return options.console_mode or "force"


def getMacOSAppConsoleMode():
    """:returns: str from ``--macos-app-console-mode``"""
    return (isMacOS() and options.macos_app_console_mode) or "disable"


def _isFullCompat():
    """:returns: bool derived from ``--full-compat``

    Notes:
        Code should should use "Options.is_full_compat" instead, this
        is only used to initialize that value.
    """
    return options is not None and not options.improved


def isShowProgress():
    """:returns: bool derived from ``--show-progress``"""
    return options is not None and options.show_progress


def isShowMemory():
    """:returns: bool derived from ``--show-memory``"""
    return options is not None and options.show_memory


def isShowInclusion():
    """:returns: bool derived from ``--show-modules``"""
    return options.show_inclusion


def isRemoveBuildDir():
    """:returns: bool derived from ``--remove-output``"""
    return options.remove_build and not options.generate_c_only


def isDeploymentMode():
    """:returns: bool derived from ``--deployment``"""
    return options.is_deployment


def getNoDeploymentIndications():
    """:returns: list derived from ``--no-deployment-flag``"""
    return options.no_deployment_flags


def hasNonDeploymentIndicator(indicator_name):
    return not isDeploymentMode() and indicator_name not in getNoDeploymentIndications()


_experimental = set()


def isExperimental(indication):
    """Check whether a given experimental feature is enabled.

    Args:
        indication: (str) feature name
    Returns:
        bool
    """
    return indication in _experimental


def enableExperimental(indication):
    _experimental.add(indication)


def getExperimentalIndications():
    """*tuple*, items of ``--experimental=``"""
    if hasattr(options, "experimental"):
        return options.experimental
    else:
        return ()


def getDebugModeIndications():
    result = []

    for debug_option_value_name in (
        "debug_immortal",
        "debug_c_warnings",
        "debug_self_forking",
    ):
        # Makes no sense prior Python3.12
        if debug_option_value_name == "debug_immortal" and python_version < 0x3C0:
            continue

        if _isDebug():
            if getattr(options, debug_option_value_name) is not False:
                result.append(debug_option_value_name)
        else:
            if getattr(options, debug_option_value_name) is True:
                result.append(debug_option_value_name)

    return result


def requireNoDebugImmortalAssumptions(logger, reason):
    if (
        states.is_debug
        and python_version >= 0x3C0
        and options.debug_immortal is not False
    ):
        logger.sysexit(
            "Error, need to disable debug partially with '--no-debug-immortal-assumptions' due to %s."
            % reason,
            reporting=False,
        )


def shallExplainImports():
    """:returns: bool derived from ``--explain-imports``"""
    return options is not None and options.explain_imports


def isStandaloneMode():
    """:returns: bool derived from ``--standalone``"""
    if shallCreatePythonPgoInput():
        return False

    return bool(options.is_standalone or options.list_package_dlls)


def isOnefileMode():
    """:returns: bool derived from ``--onefile``"""
    if shallCreatePythonPgoInput():
        return False

    return bool(options.is_onefile)


def isAcceleratedMode():
    """:returns: bool derived from ``--mode=accelerated``"""
    return not isStandaloneMode() and not shallMakeModule()


def isOnefileTempDirMode():
    """:returns: bool derived from ``--onefile-tempdir-spec`` and ``--onefile-cache-mode``

    Notes:
        Using cached onefile execution when the spec doesn't contain
        volatile things or forced by the user.
    """
    if not isOnefileMode():
        return False

    if shallCreatePythonPgoInput():
        return False

    if options.onefile_cached_mode == "auto":
        spec = getOnefileTempDirSpec()

        for candidate in (
            "{PID}",
            "{TIME}",
            "{PROGRAM}",
            "{PROGRAM_BASE}",
            "{PROGRAM_DIR}",
        ):
            if candidate in spec:
                return True
    elif options.onefile_cached_mode == "temporary":
        return True
    elif options.onefile_cached_mode == "cached":
        return False
    else:
        assert False, options.onefile_cached_mode


def isCPgoMode():
    """:returns: bool derived from ``--pgo-c``"""
    if shallCreatePythonPgoInput():
        return False

    return options.is_c_pgo


def isPythonPgoMode():
    """:returns: bool derived from ``--pgo-python``"""
    return options.is_python_pgo


def getPythonPgoInput():
    """:returns: str derived from ``--pgo-python-input``"""
    return options.python_pgo_input


def shallCreatePythonPgoInput():
    return isPythonPgoMode() and getPythonPgoInput() is None


def getPgoArgs():
    """*list* = ``--pgo-args``"""
    return shlex.split(options.pgo_args)


def getPgoExecutable():
    """*str* = ``--pgo-args``"""

    if options.pgo_executable and os.path.exists(options.pgo_executable):
        if not os.path.isabs(options.pgo_executable):
            options.pgo_executable = getUserInputNormalizedPath(options.pgo_executable)

    return options.pgo_executable


def getPythonPgoUnseenModulePolicy():
    """*str* = ``--python-pgo-unused-module-policy``"""
    return options.python_pgo_policy_unused_module


def getOnefileTempDirSpec():
    """*str* = ``--onefile-tempdir-spec``"""
    result = (
        options.onefile_tempdir_spec or "{TEMP}" + os.path.sep + "onefile_{PID}_{TIME}"
    )

    return result


def getOnefileChildGraceTime():
    """*int* = ``--onefile-child-grace-time``"""
    if options.onefile_child_grace_time is None:
        return 5000
    elif options.onefile_child_grace_time == "infinity":
        # Using -1 to represent infinity for the C code.
        return -1
    else:
        return int(options.onefile_child_grace_time)


def shallNotCompressOnefile():
    """*bool* = ``--onefile-no-compression``"""
    return options.onefile_no_compression


def shallOnefileAsArchive():
    """*bool* = ``--onefile-as-archive``"""
    return options.onefile_as_archive


def _checkedIconPaths(icon_paths):
    icon_paths = tuple(getUserInputNormalizedPath(x) for x in icon_paths)

    for icon_path in icon_paths:
        if not os.path.exists(icon_path):
            options_logger.sysexit("Error, icon path '%s' does not exist." % icon_path)

        checkIconUsage(logger=options_logger, icon_path=icon_path)

    return icon_paths


def getWindowsIconPaths():
    """*list of str*, values of ``--windows-icon-from-ico``"""
    if not isWin32Windows():
        return ()

    return _checkedIconPaths(options.windows_icon_path)


def getLinuxIconPaths():
    """*list of str*, values of ``--linux-icon``"""
    result = options.linux_icon_path

    # Check if Linux icon requirement is met.
    if isLinux() and not result and isOnefileMode():
        # spell-checker: ignore pixmaps
        default_icons = (
            "/usr/share/pixmaps/python%s.xpm" % python_version_str,
            "/usr/share/pixmaps/python%s.xpm" % sys.version_info[0],
            "/usr/share/pixmaps/python.xpm",
        )

        for icon in default_icons:
            if os.path.exists(icon):
                result.append(icon)
                break

    return _checkedIconPaths(result)


def getMacOSIconPaths():
    """*list of str*, values of ``--macos-app-icon``"""
    return _checkedIconPaths(
        icon_path for icon_path in options.macos_icon_path if icon_path != "none"
    )


def getWindowsIconExecutablePath():
    """*str* or *None* if not given, value of ``--windows-icon-from-exe``"""
    if not isWin32Windows():
        return None

    return options.icon_exe_path


def shallAskForWindowsAdminRights():
    """*bool*, value of ``--windows-uac-admin`` or ``--windows-uac-uiaccess``"""
    return options.windows_uac_admin


def shallAskForWindowsUIAccessRights():
    """*bool*, value of ``--windows-uac-uiaccess``"""
    return options.windows_uac_uiaccess


def getLegalCopyright():
    """*str* name of the product to use derived from ``--copyright``"""
    return options.legal_copyright


def getLegalTrademarks():
    """*str* name of the product to use derived from ``--trademarks``"""
    return options.legal_trademarks


def getLegalInformation():
    result = options.legal_copyright

    if options.legal_trademarks:
        if result is not None:
            result += "\nTrademark information:" + options.legal_trademarks
        else:
            result = options.legal_trademarks

    return result


def getWindowsVersionInfoStrings():
    """*dict of str*, values of ."""

    result = {}

    company_name = getCompanyName()
    if company_name:
        result["CompanyName"] = company_name

    product_name = getProductName()
    if product_name:
        result["ProductName"] = product_name

    if options.file_description:
        result["FileDescription"] = options.file_description

    if options.legal_copyright:
        result["LegalCopyright"] = options.legal_copyright

    if options.legal_trademarks:
        result["LegalTrademarks"] = options.legal_trademarks

    return result


def _parseVersionNumber(value):
    if value:
        parts = value.split(".")

        assert len(parts) <= 4

        while len(parts) < 4:
            parts.append("0")

        r = tuple(int(d) for d in parts)
        assert min(r) >= 0
        assert max(r) < 2**16
        return r
    else:
        return None


def getProductVersion():
    """:returns: str, derived from ``--product-version``"""
    return options.product_version


def getProductVersionTuple():
    """:returns: tuple of 4 ints or None, derived from ``--product-version``"""
    return _parseVersionNumber(options.product_version)


def getFileVersion():
    """:returns str, derived from ``--file-version``"""
    return options.file_version


def getFileVersionTuple():
    """:returns tuple of 4 ints or None, derived from ``--file-version``"""
    return _parseVersionNumber(options.file_version)


def getProductFileVersion():
    if options.product_version:
        if options.file_version:
            return "%s-%s" % (options.product_version, options.file_version)
        else:
            return options.product_version
    else:
        return options.file_version


def getWindowsSplashScreen():
    """:returns: bool derived from ``--onefile-windows-splash-screen-image``"""
    return (
        getUserInputNormalizedPath(options.splash_screen_image)
        if options.splash_screen_image
        else None
    )


def getCompanyName():
    """*str* name of the company to use derived from ``--company-name``"""
    return options.company_name


def getProductName():
    """*str* name of the product to use derived from ``--product-name``"""
    return options.product_name


def getMacOSTargetArch():
    """:returns: str enum ("universal", "arm64", "x86_64") derived from ``--macos-target-arch`` value"""
    if options is None:
        macos_target_arch = "native"
    else:
        macos_target_arch = options.macos_target_arch or "native"

    if macos_target_arch == "native":
        macos_target_arch = getArchitecture()

    return macos_target_arch


def shallCreateAppBundle():
    """*bool* shall create an application bundle, derived from ``--macos-create-app-bundle`` value"""
    if shallCreatePythonPgoInput():
        return False

    return options.macos_create_bundle and isMacOS()


def shallCreateDmgFile():
    """*bool* shall create a DMG file, derived from ``--macos-app-create-dmg`` value"""
    return options.macos_create_dmg and isMacOS()


def getMacOSSigningIdentity():
    """*str* value to use as identity for codesign, derived from ``--macos-sign-identity`` value"""
    result = options.macos_sign_identity

    if result is None:
        result = "ad-hoc"

    if result == "ad-hoc":
        result = "-"

    return result


def shallUseSigningForNotarization():
    """*bool* flag to use for codesign, derived from ``--macos-sign-notarization`` value"""
    return bool(options.macos_sign_notarization)


def getMacOSAppName():
    """*str* name of the app to use bundle"""
    return options.macos_app_name


def getMacOSSignedAppName():
    """*str* name of the app to use during signing"""
    return options.macos_signed_app_name


def getMacOSAppVersion():
    """*str* version of the app to use for bundle"""
    return options.macos_app_version


# Mapping of Info.plist keys to the corresponding entitlement keys for hardened
# runtime, spell-checker: ignore addressbook
_macos_protected_resource_entitlements = {
    "NSCameraUsageDescription": "com.apple.security.device.camera",
    "NSMicrophoneUsageDescription": "com.apple.security.device.microphone",
    "NSLocationWhenInUseUsageDescription": "com.apple.security.personal-information.location",
    "NSContactsUsageDescription": "com.apple.security.personal-information.addressbook",
    "NSCalendarsUsageDescription": "com.apple.security.personal-information.calendars",
    "NSRemindersUsageDescription": "com.apple.security.personal-information.reminders",
    # Siri, spell-checker: ignore Siri
    "NSSiriUsageDescription": "com.apple.developer.siri",
    # Bluetooth peripheral access
    "NSBluetoothAlwaysUsageDescription": "com.apple.security.device.bluetooth",
    "NSBluetoothPeripheralUsageDescription": "com.apple.security.device.bluetooth",
    # Apple Events
    "NSAppleEventsUsageDescription": "com.apple.security.automation.apple-events",
    # HomeKit, spell-checker: ignore HomeKit
    "NSHomeKitUsageDescription": "com.apple.developer.homekit",
    # Downloads Folder Usage
    "NSDownloadsFolderUsageDescription": (
        "com.apple.security.files.downloads.read-only",
        "com.apple.security.files.downloads.read-write",
    ),
    # Desktop Folder Usage
    "NSDesktopFolderUsageDescription": (
        "com.apple.security.files.desktop.read-only",
        "com.apple.security.files.desktop.read-write",
    ),
    # User selected files
    "NSUserSelectedFilesUsageDescription": (
        "com.apple.security.files.user-selected.read-only",
        "com.apple.security.files.user-selected.read-write",
    ),
    # Network volumes
    "NSNetworkVolumesUsageDescription": (
        "com.apple.security.files.network-volumes.read-only",
        "com.apple.security.files.network-volumes.read-write",
    ),
    # Removable volumes
    "NSRemovableVolumesUsageDescription": (
        "com.apple.security.files.removable-disks.read-only",
        "com.apple.security.files.removable-disks.read-write",
    ),
    # Local network access
    "NSLocalNetworkUsageDescription": (
        "com.apple.security.network.client",
        "com.apple.security.network.server",
    ),
    # Access to music library
    "NSAppleMusicUsageDescription": (
        "com.apple.security.assets.music.read-only",
        "com.apple.security.assets.music.read-write",
    ),
    # Both full access and add-only map to the same top-level entitlement.
    "NSPhotoLibraryUsageDescription": (
        "com.apple.security.assets.photos.read-only",
        "com.apple.security.assets.photos.read-write",
    ),
    "NSPhotoLibraryAddUsageDescription": (
        "com.apple.security.assets.photos.read-only",
        "com.apple.security.assets.photos.read-write",
    ),
    # Speech recognition
    # TODO: "NSSpeechRecognitionUsageDescription":
}


def getMacOSAppProtectedResourcesAccesses():
    """*list* key, value for protected resources of the app to use for bundle"""
    if not shallCreateAppBundle():
        return ()

    result = []

    for macos_protected_resource in options.macos_protected_resources:
        if ":" not in macos_protected_resource:
            options_logger.sysexit(
                """\
Wrong format for '--macos-app-protected-resource' value '%s', it \
needs to contain separator ':' with a description."""
                % macos_protected_resource
            )

        resource_description_name, description = macos_protected_resource.split(":", 1)

        if resource_description_name not in _macos_protected_resource_entitlements:
            options_logger.sysexit(
                """\
Wrong key for '--macos-app-protected-resource' value '%s', it \
needs to be one of the following: %s."""
                % (
                    macos_protected_resource,
                    ", ".join(_macos_protected_resource_entitlements.keys()),
                )
            )

        entitlement = _macos_protected_resource_entitlements[resource_description_name]

        if type(entitlement) is tuple:
            # If we have a tuple of size 2, checking for "read-only" and "read-write"
            # in description allows to select one.
            if len(entitlement) == 2:
                for entitlement_name in entitlement:
                    if entitlement_name.rsplit(".")[-1] in description.lower():
                        entitlement = entitlement_name
                        break

            # We still can put all values, that's fine, we support that now.

        result.append((resource_description_name, description, entitlement))

    return tuple(result)


def isMacOSBackgroundApp():
    """*bool*, derived from ``--macos-app-mode``"""
    return options.macos_app_mode == "background"


def isMacOSUiElementApp():
    """*bool*, derived from ``--macos-app-mode``"""
    return options.macos_app_mode == "ui-element"


def getMacOSSigningCertificateFilename():
    """*str* or *None* if not given, value of ``--macos-sign-keyring-filename``"""
    if not isMacOS():
        return None

    return options.macos_sign_keyring_filename


def getMacOSSigningCertificatePassword():
    """*str* or *None* if not given, value of ``--macos-sign-keyring-password``"""
    if not isMacOS():
        return None

    return options.macos_sign_keyring_password


def shallMacOSProhibitMultipleInstances():
    """*bool*, derived from ``--macos-prohibit-multiple-instances``"""
    if not isMacOS():
        return False

    return options.macos_prohibit_multiple_instances


_python_flags = None


def _getPythonFlags():
    """*list*, values of ``--python-flag``"""
    # There is many flags, pylint: disable=too-many-branches

    # singleton, pylint: disable=global-statement
    global _python_flags

    if _python_flags is None:
        _python_flags = set()

        for parts in options.python_flags:
            for part in parts.split(","):
                if part in ("-S", "nosite", "no_site"):
                    _python_flags.add("no_site")
                elif part in ("site",):
                    if "no_site" in _python_flags:
                        _python_flags.remove("no_site")
                elif part in (
                    "-R",
                    "static_hashes",
                    "norandomization",
                    "no_randomization",
                ):
                    _python_flags.add("no_randomization")
                elif part in ("-v", "trace_imports", "trace_import"):
                    _python_flags.add("trace_imports")
                elif part in ("no_warnings", "nowarnings"):
                    _python_flags.add("no_warnings")
                elif part in ("-O", "no_asserts", "noasserts"):
                    _python_flags.add("no_asserts")
                elif part in ("no_docstrings", "nodocstrings"):
                    _python_flags.add("no_docstrings")
                elif part in ("-OO",):
                    _python_flags.add("no_docstrings")
                    _python_flags.add("no_asserts")
                elif part in ("no_annotations", "noannotations"):
                    _python_flags.add("no_annotations")
                elif part in ("unbuffered", "-u"):
                    _python_flags.add("unbuffered")
                elif part in ("-m", "package_mode"):
                    _python_flags.add("package_mode")
                elif part in ("-I", "isolated"):
                    _python_flags.add("isolated")
                elif part in ("-B", "dont_write_bytecode", "dontwritebytecode"):
                    _python_flags.add("dontwritebytecode")
                elif part in ("-P", "safe_path"):
                    _python_flags.add("safe_path")
                else:
                    options_logger.sysexit("Unsupported python flag '%s'." % part)

    return _python_flags


def hasPythonFlagNoSite():
    """*bool* = "no_site" in python flags given"""

    return "no_site" in _getPythonFlags()


def hasPythonFlagNoAnnotations():
    """*bool* = "no_annotations" in python flags given"""

    return "no_annotations" in _getPythonFlags()


def hasPythonFlagNoAsserts():
    """*bool* = "no_asserts" in python flags given"""

    return "no_asserts" in _getPythonFlags()


def hasPythonFlagNoDocStrings():
    """*bool* = "no_docstrings" in python flags given"""

    return "no_docstrings" in _getPythonFlags()


def hasPythonFlagNoWarnings():
    """*bool* = "no_warnings" in python flags given"""

    return "no_warnings" in _getPythonFlags()


def hasPythonFlagIsolated():
    """*bool* = "isolated" in python flags given"""

    return "isolated" in _getPythonFlags()


def hasPythonFlagTraceImports():
    """*bool* = "trace_imports", "-v" in python flags given"""

    return "trace_imports" in _getPythonFlags()


def hasPythonFlagNoRandomization():
    """*bool* = "no_randomization", "-R", "static_hashes" in python flags given"""

    return "no_randomization" in _getPythonFlags()


def hasPythonFlagNoBytecodeRuntimeCache():
    """*bool* = "dontwritebytecode", "-B" in python flags given"""

    return "dontwritebytecode" in _getPythonFlags()


def hasPythonFlagNoCurrentDirectoryInPath():
    """*bool* = "safe_path", "-P" in python flags given"""

    return "safe_path" in _getPythonFlags()


def hasPythonFlagUnbuffered():
    """*bool* = "unbuffered", "-u" in python flags given"""

    return "unbuffered" in _getPythonFlags()


def hasPythonFlagPackageMode():
    """*bool* = "package_mode", "-m" in python flags given"""

    return "package_mode" in _getPythonFlags()


def shallNotUseDependsExeCachedResults():
    """:returns: bool derived from ``--disable-dll-dependency-cache`` or ``--force-dll-dependency-cache-update``"""
    return shallNotStoreDependsExeCachedResults() or getattr(
        options, "update_dependency_cache", False
    )


def shallNotStoreDependsExeCachedResults():
    """:returns: bool derived from ``--disable-dll-dependency-cache``"""
    return shallDisableCacheUsage("dll-dependencies")


def getPluginNameConsideringRenames(plugin_name):
    """Name of the plugin with renames considered."""

    # spell-checker: ignore delvewheel,pyzmq

    if plugin_name == "etherium":
        return "ethereum"
    if plugin_name == "pyzmq":
        return "delvewheel"
    if plugin_name == "signing":
        return "signtool"

    return plugin_name


def getPluginsEnabled():
    """*tuple*, user enabled (standard) plugins (not including user plugins)

    Note:
        Do not use this outside of main binary, as plugins are allowed
        to activate plugins themselves and that will not be visible here.
    """
    result = OrderedSet()

    if options:
        for plugin_enabled in options.plugins_enabled:
            result.update(
                getPluginNameConsideringRenames(plugin_name)
                for plugin_name in plugin_enabled.split(",")
            )

    return tuple(result)


def getPluginsDisabled():
    """*tuple*, user disabled (standard) plugins.

    Note:
        Do not use this outside of main binary, as other plugins, e.g.
        hinted compilation will activate plugins themselves and this
        will not be visible here.
    """
    result = OrderedSet()

    if options:
        for plugin_disabled in options.plugins_disabled:
            result.update(
                getPluginNameConsideringRenames(plugin_name)
                for plugin_name in plugin_disabled.split(",")
            )

    return tuple(result)


def getUserPlugins():
    """*tuple*, items user provided of ``--user-plugin=``"""
    if not options:
        return ()

    return tuple(set(options.user_plugins))


def shallDetectMissingPlugins():
    """*bool* = **not** ``--plugin-no-detection``"""
    return options is not None and options.detect_missing_plugins


def getPythonPathForScons():
    """*str*, value of ``--python-for-scons``"""
    return options.python_scons


def shallCompileWithoutBuildDirectory():
    """*bool* currently hard coded, not when using debugger.

    When this is used, compilation is executed in a fashion that it runs
    inside the build folder, hiding it, attempting to make results more
    reproducible across builds of different programs.

    TODO: Make this not hardcoded, but possible to disable via an
    options.
    """
    return not shallRunInDebugger()


def shallRecompileExtensionModules(module_name):
    """*bool* prefer source code over extension modules if both are there"""
    if options is None:
        return None, "no user options available"

    if options.prefer_source_code in (True, False):
        return (
            options.prefer_source_code,
            "global user option to %s"
            % ("enable" if options.prefer_source_code else "disable"),
        )

    result = module_name.matchesToShellPatterns(options.recompile_extension_modules)

    if result is None:
        return None, "no user options given"

    return result


def getProgressBar():
    """*str* what progress bar to use if any, derived from ``--progress-bar`` and legacy option ``--no-progressbar``"""
    if options.no_progress_bar:
        return "none"
    else:
        return options.progress_bar


def getForcedStdoutPath():
    """*str* force program stdout output into that filename"""
    if shallCreatePythonPgoInput():
        return False

    return options.force_stdout_spec


def getForcedStderrPath():
    """*str* force program stderr output into that filename"""
    if shallCreatePythonPgoInput():
        return False

    return options.force_stderr_spec


def shallShowSourceModifications(module_name):
    """*bool* display plugin source changes derived from --show-source-changes"""
    if options is None:
        return False

    result, _reason = module_name.matchesToShellPatterns(options.show_source_changes)

    return result


def isLowMemory():
    """*bool* low memory usage requested"""
    return options.low_memory


def getCompilationReportFilename():
    """*str* filename to write XML report of compilation to"""
    return options.compilation_report_filename


def getCompilationReportTemplates():
    """*tuple of str,str* template and output filenames to write reports to"""
    result = []
    for value in options.compilation_report_templates:
        result.append(value.split(":", 1))

    return tuple(result)


def getCompilationReportUserData():
    result = OrderedDict()

    for desc in options.compilation_report_user_data:
        if "=" not in desc:
            options_logger.sysexit(
                "Error, user report data must be of key=value form not '%s'." % desc
            )

        key, value = desc.split("=", 1)

        if key in result and value != result[key]:
            options_logger.sysexit(
                "Error, user report data key '%s' has been given conflicting values '%s' and '%s'."
                % (
                    key,
                    result[key],
                    value,
                )
            )

        if not re.match(
            r"^([_a-z][\w]?|[a-w_yz][\w]{2,}|[_a-z][a-l_n-z\d][\w]+|[_a-z][\w][a-k_m-z\d][\w]*)$",
            key,
        ):
            options_logger.sysexit(
                "Error, user report data key '%s' is not valid as an XML tag, and therefore cannot be used."
                % key
            )

        result[key] = value

    return result


def shallCreateDiffableCompilationReport():
    """*bool*" derived from --report-diffable"""
    return options.compilation_report_diffable


def getOutputFolderName():
    """*str* or *None*, value of ``--output-folder-name``"""
    return (
        getUserInputNormalizedPath(options.output_folder_name)
        if options.output_folder_name
        else None
    )


def getUserProvidedYamlFiles():
    """*list* files with user provided Yaml files"""
    return options.user_yaml_files


def _getWarningMnemonicsDisabled():
    return sum([_splitShellPattern(x) for x in options.nowarn_mnemonics], [])


def shallDisplayWarningMnemonic(mnemonic):
    """*bool*" derived from --nowarn-mnemonic"""
    for pattern in _getWarningMnemonicsDisabled():
        if fnmatch.fnmatch(mnemonic, pattern):
            return False

    return True


def shallShowExecutedCommands():
    return isExperimental("show-commands")


def getTargetPythonDescription():
    """:returns: tuple(python_version,OS/arch) string derived from ``--target``"""
    if options.target_spec is not None:
        # TODO: Only one we are working on right now.
        assert options.target_spec == "wasi"

        return python_version, "wasi"

    return None


def getFcfProtectionMode():
    """:returns: string derived from ``--fcf-protection``"""
    return options.cf_protection


def getModuleParameter(module_name, parameter_name):
    """:returns: string derived from ``--module-parameter``"""

    module_name_prefix = module_name.getTopLevelPackageName().asString()

    if parameter_name.startswith(module_name_prefix + "-"):
        option_name = parameter_name
    else:
        option_name = module_name_prefix + "-" + parameter_name

    for module_option in options.module_parameters:
        try:
            module_option_name, module_option_value = module_option.split("=", 1)
        except ValueError:
            optimization_logger.sysexit(
                """\
Error, must specify module parameter name and value with a separating \
'=' and not '%s"."""
                % module_option
            )

        if option_name == module_option_name:
            return module_option_value

    return None


def getForcedRuntimeEnvironmentVariableValues():
    """:returns: iterable (string, string) derived from ``--force-runtime-environment-variable``"""

    for forced_runtime_env_variables_spec in options.forced_runtime_env_variables:
        name, value = forced_runtime_env_variables_spec.split("=", 1)

        yield (name, value)


def getCompilationMode():
    """For reporting only, use shorter specific tests."""
    # return driven, pylint: disable=too-many-return-statements

    if isAcceleratedMode():
        return "accelerated"
    elif shallMakeModule():
        return "module"
    elif shallMakePackage():
        return "package"
    elif shallCreateAppBundle():
        return "app"
    elif isOnefileMode():
        return "onefile"
    elif isStandaloneMode():
        return "standalone"
    elif shallMakeDll():
        return "dll"


def getPyProjectRequiredPackages():
    """Get the requirements that are not Nuitka from project configuration."""
    # TODO: Move this to using code to avoid cyclic dependency.
    from nuitka.utils.Distributions import filterInstallRequires

    return filterInstallRequires(options.pyproject_requires)


#     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
#
#        http://www.gnu.org/licenses/agpl.txt
#
#     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.
