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


"""
The Nuitka scons file. If you have Scons or platform knowledge, please be
especially invited and contribute improvements.

This file is used to build an executable or shared library. Nuitka needs no
build process for itself, although it can be compiled using the same method.
"""

# Make nuitka package importable from calling installation

import sys
import os
import types

sys.modules["nuitka"] = types.ModuleType("nuitka")
sys.modules["nuitka"].__path__ = [os.environ["NUITKA_PACKAGE_DIR"]]

# We are in the build.build package really.
import nuitka.build  # pylint: disable=unused-import

__package__ = "nuitka.build"  # pylint: disable=redefined-builtin

# isort:start

from nuitka.Tracing import scons_details_logger, scons_logger
from nuitka.utils.FileOperations import (
    changeFilenameExtension,
    getFilenameExtension,
    getNormalizedPath,
)
from nuitka.utils.Json import loadJsonFromFilename
from nuitka.utils.Utils import (
    isAIX,
    isDebianBasedLinux,
    isFedoraBasedLinux,
    isFreeBSD,
    isLinux,
    isMacOS,
)

from .SconsCompilerSettings import (
    createNuitkaSconsEnvironment,
    enableWindowsStackSize,
    reportCCompiler,
    setupCCompiler,
)
from .SconsHacks import makeGccUseLinkerFile
from .SconsProgress import setSconsProgressBarTotal
from .SconsPythonBuild import (
    addPythonHaclLib,
    addWin32PythonLib,
    applyPythonBuildSettings,
)
from .SconsSpawn import enableSpawnMonitoring
from .SconsUtils import (
    changeKeyboardInterruptToErrorExit,
    createDefinitionsFile,
    getArgumentBool,
    getArgumentDefaulted,
    getArgumentInt,
    getArgumentList,
    getArgumentRequired,
    makeResultPathFileSystemEncodable,
    scanSourceDir,
    writeSconsReport,
)

# spell-checker: ignore cflags,cppdefines,cpppath,
# spell-checker: ignore linkflags,libpath,libflags

# Set up the basic stuff.
env = createNuitkaSconsEnvironment()


# The name of executable that we are supposed to produce.
result_exe = getArgumentRequired("result_exe")

# Name of the main program (in executable mode, typically __main__ but can be in a package too.)
main_module_name = getArgumentDefaulted("main_module_name", "__main__")


# Full compatibility, even where it's stupid, i.e. do not provide information,
# even if available, in order to assert maximum compatibility. Intended to
# control level of compatibility to absurd.
full_compat_mode = getArgumentBool("full_compat", False)

# Profiling mode: Outputs vmprof based information from program run.
profile_mode = getArgumentBool("profile_mode", False)

# sys.flags values to pass along
no_python_warnings = getArgumentBool("no_python_warnings", False)

# python_sysflag_py3k_warning
python_sysflag_py3k_warning = getArgumentBool("python_sysflag_py3k_warning", False)
# python_sysflag_division_warning
python_sysflag_division_warning = getArgumentBool(
    "python_sysflag_division_warning", False
)
# python_sysflag_division_warning
python_sysflag_bytes_warning = getArgumentBool("python_sysflag_bytes_warning", False)
# python_sysflag_no_site
python_sysflag_no_site = getArgumentBool("python_sysflag_no_site", False)
# python_sysflag_verbose
python_sysflag_verbose = getArgumentBool("python_sysflag_verbose", False)
# python_sysflag_unicode
python_sysflag_unicode = getArgumentBool("python_sysflag_unicode", False)
# python_sysflag_utf8
python_sysflag_utf8 = getArgumentBool("python_sysflag_utf8", False)
# python_sysflag_optimize
python_sysflag_optimize = getArgumentInt("python_sysflag_optimize", 0)
# python_flag_no_asserts
python_flag_no_asserts = getArgumentBool("python_flag_no_asserts", False)
# python_flag_no_docstrings
python_flag_no_docstrings = getArgumentBool("python_flag_no_docstrings", False)
# python_flag_no_annotations
python_flag_no_annotations = getArgumentBool("python_flag_no_annotations", False)
# python_sysflag_no_randomization
python_sysflag_no_randomization = getArgumentBool(
    "python_sysflag_no_randomization", False
)

# python_sysflag_unbuffered (--python-flag=-U)
python_sysflag_unbuffered = getArgumentBool("python_sysflag_unbuffered", False)

# python_sysflag_dontwritebytecode (--python-flag=-B)
python_sysflag_dontwritebytecode = getArgumentBool(
    "python_sysflag_dontwritebytecode", False
)

# python_sysflag_safe_path (--python-flag=-P)
python_sysflag_safe_path = getArgumentBool("python_sysflag_safe_path", False)

# python_sysflag_isolated
python_sysflag_isolated = getArgumentBool("python_sysflag_isolated", False)

# file reference mode
file_reference_mode = getArgumentRequired("file_reference_mode")

# Preprocessor defines from plugins
cpp_defines = getArgumentList("cpp_defines", "")
cpp_include_dirs = getArgumentList("cpp_include_dirs", "")
link_dirs = getArgumentList("link_dirs", "")
link_libraries = getArgumentList("link_libraries", "")

# From statically compiled modules of the Python
link_module_libs = getArgumentList("link_module_libs", "")


# Report the C compiler used.
reportCCompiler(env, "Backend", scons_logger.info)

# Set up C compiler settings.
setupCCompiler(
    env=env,
    pgo_mode=env.pgo_mode,
    exe_target=not env.module_mode and not env.dll_mode,
    onefile_compile=False,
)


if env.exe_mode:
    enableWindowsStackSize(env=env, target_arch=env.target_arch)

if full_compat_mode:
    env.Append(CPPDEFINES=["_NUITKA_FULL_COMPAT"])

if profile_mode:
    env.Append(CPPDEFINES=["_NUITKA_PROFILE"])

if env.trace_mode:
    env.Append(CPPDEFINES=["_NUITKA_TRACE"])

applyPythonBuildSettings(env)

adapted_python_header_files_dir = env.get("adapted_python_header_files_dir")
if adapted_python_header_files_dir:
    env.Prepend(CPPPATH=[adapted_python_header_files_dir])

# TODO: This ought to be decided outside of scons and per flavor maybe.
if env.static_libpython and ((not os.name == "nt" and not isMacOS()) or env.monolithpy):
    env.Append(CPPDEFINES=["_NUITKA_USE_UNEXPOSED_API"])

link_module_libs = addPythonHaclLib(env, link_module_libs=link_module_libs)

if os.name == "nt":
    if env.monolithpy:
        link_data = loadJsonFromFilename(
            os.path.join(env.python_prefix_external, "link.json")
        )
        env.Append(LIBS=link_data["libraries"])
        env.Append(LIBPATH=link_data["library_dirs"])
        for define, value in link_data["macros"]:
            if value:
                env.Append(CPPDEFINES=[define + "=" + value])
            else:
                env.Append(CPPDEFINES=[define])

        if "link_flags" in link_data:
            env.Append(LINKFLAGS=link_data["link_flags"])
        if "compile_flags" in link_data:
            env.Append(CFLAGS=link_data["compile_flags"])
    elif env.gcc_mode and env.static_libpython:
        env.Append(LIBS=[env.File(env.static_libpython)])
    else:
        # All Python flavors on Windows have this.
        addWin32PythonLib(env)
elif env.exe_mode or env.dll_mode:
    # Add the python library path to the library path
    if env.self_compiled_python_uninstalled:
        python_lib_path = env.python_prefix_external
    else:
        python_lib_path = os.path.join(env.python_prefix_external, "lib")

    env.Append(LIBPATH=[python_lib_path])

    # Any module libs that are for self-compiled Python to be static.
    env.Append(_LIBFLAGS=["-l" + lib_desc for lib_desc in link_module_libs])

    if env.monolithpy:
        link_data = loadJsonFromFilename(
            os.path.join(env.python_prefix_external, "link.json")
        )
        for lib in link_data["libraries"]:
            # Need to prevent Scons from stripping .a from the passed in libs.
            if lib.startswith(":") and lib.endswith(".a"):
                env.Append(_LIBFLAGS=["-l" + lib])
            elif os.path.isfile(lib):
                env.Append(_LIBFLAGS=[lib])
            else:
                env.Append(LIBS=[lib])
        env.Append(LIBPATH=link_data["library_dirs"])
        for define, value in link_data["macros"]:
            if value:
                env.Append(CPPDEFINES=[define + "=" + value])
            else:
                env.Append(CPPDEFINES=[define])

        if "link_flags" in link_data:
            env.Append(LINKFLAGS=link_data["link_flags"])
        if "compile_flags" in link_data:
            env.Append(CFLAGS=link_data["compile_flags"])
    elif env.static_libpython:
        env.Append(LIBS=[env.File(env.static_libpython)])

        # The linker won't succeed in searching for those for system Python of Debian
        # compiled Pythons. Help that. spell-checker: ignore rdynamic,Xlinker,pthread
        if env.python_prefix_external == "/usr" and isDebianBasedLinux():
            env.Append(LIBS=["z", "m", "util", "pthread"])

            if env.python_version >= (3,):
                env.Append(LIBS=["expat"])

            env.Append(LINKFLAGS=["-Xlinker", "-export-dynamic", "-rdynamic"])

        if env.arch_python:
            env.Append(LINKFLAGS=["-rdynamic"])
    else:
        # Fedora, Debian and Ubuntu distinguish the system libraries like this.
        if (
            env.python_debug
            and env.python_prefix_external == "/usr"
            and env.python_version < (3,)
            and (isDebianBasedLinux() or isFedoraBasedLinux())
        ):
            env.Append(LIBS=["python" + env.python_abi_version + "_d"])
        else:
            env.Append(LIBS=["python" + env.python_abi_version])

    if env.python_prefix_external != "/usr" and isLinux():
        env.Append(LIBS=["dl", "pthread", "util", "rt", "m"])

        if env.gcc_mode:
            if env.clang_mode:
                env.Append(LINKFLAGS=["-Wl,--export-dynamic"])
            else:
                env.Append(LINKFLAGS=["-export-dynamic"])

    # For NetBSD the rpath is required, on FreeBSD it's warned as unused, macOS
    # and AIX don't have it, but some Python flavors like Standalone Python
    # won't find it unless provided like this, so lets try to add this by
    # default and remove OSes that give errors one by one.
    if not isFreeBSD() and not isMacOS() and not isAIX():
        env.Append(LINKFLAGS=["-Wl,-rpath=" + python_lib_path])

if isMacOS():
    if env.module_mode or env.dll_mode:
        # Dynamic lookup needed for extension modules.
        env.Append(LINKFLAGS=["-undefined", "dynamic_lookup"])
    if not env.module_mode:
        # For macOS we need to make sure install_name_tool can do its work
        # spell-checker: ignore headerpad
        env.Append(LINKFLAGS=["-headerpad_max_install_names"])

if env.android_termux_python or isAIX():
    # For Android Termux Python and AIX we need modules to link against the
    # shared library of Python, different from all other ones.
    # spell-checker: ignore lpython

    if env.module_mode:
        env.Append(_LIBFLAGS=["-lpython%s" % env.python_version_str])

# The static include files reside in Nuitka installation, which may be where
# the "nuitka.build" package lives.
nuitka_include = os.path.join(env.nuitka_src, "include")

if not os.path.exists(os.path.join(nuitka_include, "nuitka", "prelude.h")):
    scons_logger.sysexit(
        "Error, cannot locate Nuitka includes at '%s', this is a broken Nuitka installation."
        % nuitka_include
    )

# We have include files in the build directory and the static include directory
# that is located inside Nuitka installation.
env.Append(
    CPPPATH=[
        env.source_dir,
        nuitka_include,
        os.path.join(env.nuitka_src, "static_src"),
        os.path.join(env.nuitka_src, "inline_copy", "libbacktrace"),
    ]
)

# Set load libpython from binary directory default
if (
    env.gcc_mode
    and not isMacOS()
    and not isAIX()
    and not os.name == "nt"
    and (env.exe_mode or env.dll_mode)
):
    # TODO: It would appear that this should be merged with above code that does
    # env.Append(LINKFLAGS=["-Wl,-rpath=" + python_lib_path]) relatively
    # unconditionally already.
    if env.standalone_mode:
        rpath = "$$ORIGIN"
    else:
        rpath = python_lib_path

    env.Append(LINKFLAGS=["-Wl,-R,'%s'" % rpath])

    # The rpath is no longer used unless we do this on modern Linux. The
    # option name is not very revealing, but basically without this, the
    # rpath in the binary will be ignored by the loader.
    # spell-checker: ignore dtags
    if isLinux():
        env.Append(LINKFLAGS=["-Wl,--disable-new-dtags"])


def discoverSourceFiles():
    result = []

    # Scan for Nuitka created source files, and add them too.
    result.extend(scanSourceDir(env=env, dirname=env.source_dir, plugins=False))
    result.extend(
        scanSourceDir(
            env=env,
            dirname=os.path.join(env.source_dir, "plugins"),
            plugins=True,
        )
    )

    result.extend(
        scanSourceDir(
            env=env,
            dirname=os.path.join(env.source_dir, "static_src"),
            plugins=False,
        )
    )

    return result


source_files = discoverSourceFiles()

# Remove the target file to avoid cases where it falsely doesn't get rebuild and
# then lingers from previous builds, and also workaround for MinGW64 not
# supporting unicode result paths for "-o" basename.
result_exe = makeResultPathFileSystemEncodable(env=env, result_exe=result_exe)

if env.module_mode:
    # For Python modules, the standard shared library extension is not what gets
    # used. spell-checker: ignore SHLIBSUFFIX
    module_suffix = getFilenameExtension(result_exe)
    result_base_path = changeFilenameExtension(getNormalizedPath(result_exe), "")

    extra_suffix = getFilenameExtension(result_base_path)
    if extra_suffix != "":
        module_suffix = extra_suffix + module_suffix
        result_base_path = changeFilenameExtension(
            getNormalizedPath(result_base_path), ""
        )

    assert getFilenameExtension(result_base_path) == "", result_base_path

    env["SHLIBSUFFIX"] = module_suffix

    # For scons internal use, we use Python native paths as MSYS2 does wrong things otherwise.
    target = env.SharedLibrary(
        os.path.normpath(result_base_path),
        source_files,
        no_import_lib=env.no_import_lib,
    )
elif env.dll_mode:
    module_suffix = getFilenameExtension(result_exe)
    result_base_path = changeFilenameExtension(getNormalizedPath(result_exe), "")
    env["SHLIBSUFFIX"] = module_suffix

    # For scons internal use, we use Python native paths as MSYS2 does wrong things otherwise.
    target = env.SharedLibrary(
        os.path.normpath(result_base_path),
        source_files,
        no_import_lib=env.no_import_lib,
    )
else:
    # For scons internal use, we use Python native paths as MSYS2 does wrong things otherwise.
    target = env.Program(os.path.normpath(result_exe), source_files)


def createBuildDefinitionsFile():
    if env.uninstalled_python and not env.static_libpython:
        # Use the non-external one, so it's not a short path.
        env.build_definitions["PYTHON_HOME_PATH"] = env.python_prefix

    env.build_definitions["NO_PYTHON_WARNINGS"] = 1 if no_python_warnings else 0

    if env.python_version < (3,):
        env.build_definitions["SYSFLAG_PY3K_WARNING"] = (
            1 if python_sysflag_py3k_warning else 0
        )

        env.build_definitions["SYSFLAG_DIVISION_WARNING"] = (
            1 if python_sysflag_division_warning else 0
        )

        env.build_definitions["SYSFLAG_UNICODE"] = 1 if python_sysflag_unicode else 0

    env.build_definitions["SYSFLAG_BYTES_WARNING"] = (
        1 if python_sysflag_bytes_warning else 0
    )

    env.build_definitions["SYSFLAG_NO_SITE"] = 1 if python_sysflag_no_site else 0

    env.build_definitions["SYSFLAG_VERBOSE"] = 1 if python_sysflag_verbose else 0

    env.build_definitions["SYSFLAG_UTF8"] = 1 if python_sysflag_utf8 else 0

    env.build_definitions["SYSFLAG_OPTIMIZE"] = python_sysflag_optimize

    env.build_definitions["SYSFLAG_DONTWRITEBYTECODE"] = (
        1 if python_sysflag_dontwritebytecode else 0
    )

    env.build_definitions["_NUITKA_NO_ASSERTS"] = 1 if python_flag_no_asserts else 0

    env.build_definitions["_NUITKA_NO_DOCSTRINGS"] = (
        1 if python_flag_no_docstrings else 0
    )

    env.build_definitions["_NUITKA_NO_ANNOTATIONS"] = (
        1 if python_flag_no_annotations else 0
    )

    env.build_definitions["SYSFLAG_NO_RANDOMIZATION"] = (
        1 if python_sysflag_no_randomization else 0
    )

    env.build_definitions["SYSFLAG_UNBUFFERED"] = 1 if python_sysflag_unbuffered else 0

    env.build_definitions["SYSFLAG_DONTWRITEBYTECODE"] = (
        1 if python_sysflag_dontwritebytecode else 0
    )

    env.build_definitions["SYSFLAG_SAFE_PATH"] = 1 if python_sysflag_safe_path else 0

    env.build_definitions["SYSFLAG_ISOLATED"] = 1 if python_sysflag_isolated else 0

    env.build_definitions["NUITKA_MAIN_MODULE_NAME"] = main_module_name
    env.build_definitions["NUITKA_MAIN_IS_PACKAGE_BOOL"] = (
        main_module_name != "__main__"
    )

    if env.forced_stdout_path is not None:
        if env.forced_stdout_path == "{NONE}":
            env.build_definitions["NUITKA_FORCED_STDOUT_NONE_BOOL"] = 1
        elif env.forced_stdout_path == "{NULL}":
            env.build_definitions["NUITKA_FORCED_STDOUT_NULL_BOOL"] = 1
        else:
            env.build_definitions["NUITKA_FORCED_STDOUT_PATH"] = env.forced_stdout_path

    if env.forced_stderr_path is not None:
        if env.forced_stderr_path == "{NONE}":
            env.build_definitions["NUITKA_FORCED_STDERR_NONE_BOOL"] = 1
        elif env.forced_stderr_path == "{NULL}":
            env.build_definitions["NUITKA_FORCED_STDERR_NULL_BOOL"] = 1
        else:
            env.build_definitions["NUITKA_FORCED_STDERR_PATH"] = env.forced_stderr_path

    createDefinitionsFile(env.source_dir, "build_definitions.h", env.build_definitions)


if file_reference_mode == "original":
    env.Append(CPPDEFINES=["_NUITKA_FILE_REFERENCE_ORIGINAL_MODE"])


createBuildDefinitionsFile()

# The meta path based loader might want to respect that, so it does verbose traces in module
# mode, mostly for debugging purposes only.
if env.module_mode and python_sysflag_verbose:
    env.Append(CPPDEFINES=["_NUITKA_SYSFLAG_VERBOSE=1"])

# Hack to make Scons use tempfile for gcc linking, to avoid line length limits,
# which can make linking fail with many modules otherwise. Most needed on Windows,
# but useful on other platforms too.
if env.gcc_mode and (not env.clang_mode or env.zig_mode):
    makeGccUseLinkerFile(
        env=env,
        source_files=source_files,
    )

# Plugin contributed C defines should be used too.
env.Append(CPPDEFINES=cpp_defines)
# Plugin contributed C include directories should be used too.
env.Prepend(CPPPATH=cpp_include_dirs)
# Plugin contributed link dirs should be used too.
env.Append(LIBPATH=link_dirs)
# Plugin contributed link libraries should be used too.
env.Append(LIBS=link_libraries)

# Work around windows bugs and use watchdogs to track progress of compilation.
enableSpawnMonitoring(
    env=env,
    source_files=source_files,
)

# Before we go, also lets turn KeyboardInterrupt into a mere error exit as the
# scons traceback is not going to be very interesting to us.
changeKeyboardInterruptToErrorExit()

writeSconsReport(env=env, target=target)

setSconsProgressBarTotal(name=env.progressbar_name, total=len(source_files))

scons_details_logger.info("Launching Scons target: %s" % target)
env.Default(target)

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