# Copyright (C) 2026 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause

# Adds an SBOM build tool target to the current SBOM project.
# Expects the target not to be created yet.
#
# A build tool is something like a compiler, linker, cmake executable, which participates in the
# build process of the current project in some way.
#
# There is a predefined list of known tools which when specified will automatically query
# information like tool version and create an appropriate relationship comment.
#
# If CUSTOM is specified as the build tool type, it's expected that the caller sets any additional
# relevant SBOM options to the SBOM_ARGS option.
#
# If AUTO_ADD_PROJECT_RELATIONSHIP is specified, an automatic relationship entry is created
# that the tool is a BUILD_DEPENDENCY_OF the current project.
function(_qt_internal_add_sbom_build_tool target)
    if(NOT QT_GENERATE_SBOM)
        return()
    endif()

    set(opt_args
        AUTO_ADD_PROJECT_RELATIONSHIP
    )
    set(single_args
        BUILD_TOOL_TYPE
    )
    set(multi_args
        SBOM_ARGS
    )
    cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    set(known_build_tool_types
        CMAKE
        CMAKE_GENERATOR

        COMPILER
        LINKER

        CUSTOM
    )

    if(NOT arg_BUILD_TOOL_TYPE)
        message(FATAL_ERROR "BUILD_TOOL_TYPE must be specified.")
    endif()

    if(NOT arg_BUILD_TOOL_TYPE IN_LIST known_build_tool_types)
        message(FATAL_ERROR "Unknown BUILD_TOOL_TYPE value: ${arg_BUILD_TOOL_TYPE}")
    endif()

    if(arg_BUILD_TOOL_TYPE STREQUAL "CUSTOM" AND arg_AUTO_ADD_PROJECT_RELATIONSHIP)
        message(FATAL_ERROR
            "AUTO_ADD_PROJECT_RELATIONSHIP cannot be used with CUSTOM BUILD_TOOL_TYPE. Consider "
            "creating a relationship manually by passing appropriate SBOM_RELATIONSHIP_ENTRIES "
            "to the SBOM_ARGS option.")
    endif()

    string(TOLOWER "${arg_BUILD_TOOL_TYPE}" build_tool_type_lower_case)

    if(NOT arg_BUILD_TOOL_TYPE STREQUAL "CUSTOM")
        set(build_tool_type_args "")
        if(arg_AUTO_ADD_PROJECT_RELATIONSHIP)
            list(APPEND build_tool_type_args GET_RELATIONSHIP_COMMENT)
        endif()

        _qt_internal_sbom_handle_default_build_tool_type(
            BUILD_TOOL_TYPE "${arg_BUILD_TOOL_TYPE}"
            OUT_VAR_SBOM_ARGS extra_args
            OUT_VAR_RELATIONSHIP_COMMENT relationship_comment
            ${build_tool_type_args}
        )
    endif()

    set(forward_args
        ${arg_SBOM_ARGS}
        SBOM_ENTITY_TYPE BUILD_TOOL
    )
    if(extra_args)
        list(APPEND forward_args ${extra_args})
    endif()

    _qt_internal_add_sbom("${target}" ${forward_args})

    if(arg_AUTO_ADD_PROJECT_RELATIONSHIP)
        _qt_internal_sbom_get_current_project_target(project_target)
        set(relationships
            SBOM_RELATIONSHIP_ENTRY
                SBOM_RELATIONSHIP_FROM
                    "${target}"
                SBOM_RELATIONSHIP_TYPE
                    BUILD_DEPENDENCY_OF
                SBOM_RELATIONSHIP_TO
                    "${project_target}"
                SBOM_RELATIONSHIP_COMMENT
                    "${relationship_comment}"
        )
        _qt_internal_extend_sbom("${project_target}"
            SBOM_RELATIONSHIP_ENTRIES
                ${relationships}
        )
    endif()
endfunction()

# Adds a set of default build tools that CMake uses for the current SBOM project.
# This includes the compiler, cmake, the make tool, and optionally the linker.
function(_qt_internal_sbom_add_project_default_build_tools)
    _qt_internal_sbom_get_project_default_build_tool_types(default_build_tool_types)

    foreach(build_tool_type IN LISTS default_build_tool_types)
        _qt_internal_sbom_get_build_tool_target_for_type(
            BUILD_TOOL_TYPE "${build_tool_type}"
            OUT_VAR_TARGET target
        )
        _qt_internal_add_sbom_build_tool("${target}"
            BUILD_TOOL_TYPE "${build_tool_type}"
            AUTO_ADD_PROJECT_RELATIONSHIP
        )
    endforeach()
endfunction()

# Returns the list of default build tool types that CMake uses for the current SBOM project.
function(_qt_internal_sbom_get_project_default_build_tool_types out_var)
    set(default_build_tool_types
        CMAKE
        CMAKE_GENERATOR
        COMPILER
    )

    if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.29")
        list(APPEND default_build_tool_types LINKER)
    endif()

    set(${out_var} "${default_build_tool_types}" PARENT_SCOPE)
endfunction()

# Returns the target name for one of the project default build tool types.
function(_qt_internal_sbom_get_build_tool_target_for_type)
    if(NOT QT_GENERATE_SBOM)
        if(arg_OUT_VAR_TARGET)
            set(${arg_OUT_VAR_TARGET} "" PARENT_SCOPE)
        endif()
        return()
    endif()

    set(opt_args "")
    set(single_args
        BUILD_TOOL_TYPE
        OUT_VAR_TARGET
    )
    set(multi_args "")
    cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    if(NOT arg_BUILD_TOOL_TYPE)
        message(FATAL_ERROR "BUILD_TOOL_TYPE must be specified.")
    endif()

    if(NOT arg_OUT_VAR_TARGET)
        message(FATAL_ERROR "OUT_VAR_TARGET must be specified.")
    endif()

    set(known_types
        CMAKE
        CMAKE_GENERATOR
        COMPILER
        LINKER
    )

    if(NOT arg_BUILD_TOOL_TYPE IN_LIST known_types)
        message(FATAL_ERROR "Unknown BUILD_TOOL_TYPE value: ${arg_BUILD_TOOL_TYPE}")
    endif()

    _qt_internal_sbom_get_root_project_name_lower_case(project_name_lower_case)
    set(prefix "${project_name_lower_case}SbomBuildTool")

    if(arg_BUILD_TOOL_TYPE STREQUAL "CMAKE")
        set(target "${prefix}CMake")
    elseif(arg_BUILD_TOOL_TYPE STREQUAL "CMAKE_GENERATOR")
        set(target "${prefix}CMakeGenerator")
    elseif(arg_BUILD_TOOL_TYPE STREQUAL "COMPILER")
        set(target "${prefix}Compiler")
    elseif(arg_BUILD_TOOL_TYPE STREQUAL "LINKER")
        set(target "${prefix}Linker")
    endif()

    set(${arg_OUT_VAR_TARGET} "${target}" PARENT_SCOPE)
endfunction()

# Returns common build tool information depending on the build tool type.
function(_qt_internal_sbom_handle_default_build_tool_type)
    set(opt_args
        GET_RELATIONSHIP_COMMENT
    )
    set(single_args
        BUILD_TOOL_TYPE
        OUT_VAR_SBOM_ARGS
        OUT_VAR_RELATIONSHIP_COMMENT
    )
    set(multi_args "")
    cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    if(NOT arg_BUILD_TOOL_TYPE)
        message(FATAL_ERROR "BUILD_TOOL_TYPE must be specified.")
    endif()

    if(NOT arg_OUT_VAR_SBOM_ARGS)
        message(FATAL_ERROR "OUT_VAR_SBOM_ARGS must be specified.")
    endif()

    if(arg_GET_RELATIONSHIP_COMMENT AND NOT arg_OUT_VAR_RELATIONSHIP_COMMENT)
        message(FATAL_ERROR "OUT_VAR_RELATIONSHIP_COMMENT must be specified.")
    endif()

    set(sbom_args "")
    set(build_tool_args "")

    if(arg_GET_RELATIONSHIP_COMMENT)
        _qt_internal_sbom_get_current_project_target(project_target)
        _qt_internal_sbom_get_current_project_spdx_id(project_spdx_id)
    endif()

    if(arg_BUILD_TOOL_TYPE STREQUAL "CMAKE")
        set(sbom_args
            FRIENDLY_PACKAGE_NAME "CMake"
            PACKAGE_VERSION "${CMAKE_VERSION}"
        )
        if(arg_GET_RELATIONSHIP_COMMENT)
            string(CONCAT relationship_comment
                "${project_spdx_id} is built using CMake version ${CMAKE_VERSION}"
            )
        endif()
    elseif(arg_BUILD_TOOL_TYPE STREQUAL "CMAKE_GENERATOR")
        set(version "")

        set(sbom_args
            FRIENDLY_PACKAGE_NAME "CMake generator ${CMAKE_GENERATOR}"
        )

        if((CMAKE_GENERATOR MATCHES "Ninja"
                OR CMAKE_GENERATOR STREQUAL "Unix Makefiles"
                OR CMAKE_GENERATOR STREQUAL "MinGW Makefiles"
                OR CMAKE_GENERATOR STREQUAL "NMake Makefiles JOM"
                OR CMAKE_GENERATOR STREQUAL "Xcode")
                AND CMAKE_MAKE_PROGRAM
                AND NOT QT_SBOM_NO_QUERY_MAKE_PROGRAM_VERSION
            )
            if(CMAKE_GENERATOR STREQUAL "Xcode")
                set(query_version_option "-version")
            elseif(CMAKE_GENERATOR STREQUAL "NMake Makefiles JOM")
                # Regular NMake Makefiles doesn't have a /version option, so we don't extract
                # that atm.
                set(query_version_option "/version")
            else()
                # Ninja, Unix Makefiles, MinGW Makefiles
                set(query_version_option "--version")
            endif()

            execute_process(
                COMMAND "${CMAKE_MAKE_PROGRAM}" ${query_version_option}
                OUTPUT_VARIABLE make_tool_output
                ERROR_VARIABLE make_tool_output
                RESULT_VARIABLE make_tool_version_result
                OUTPUT_STRIP_TRAILING_WHITESPACE
            )
            if(make_tool_version_result EQUAL 0)
                # Get the first line of output.
                string(REGEX MATCH "^[^\n\r]+" version_first_line "${make_tool_output}")

                # Strip characters that might break CMake argument parsing or SBOM generation.
                # Leave only alpha numeric characters, dots, dashes and spaces.
                string(REGEX REPLACE "[^a-zA-Z0-9. -]" "" version_first_line
                    "${version_first_line}")

                set(version "${version_first_line}")
            endif()
        endif()

        if(version)
            list(APPEND sbom_args PACKAGE_VERSION "${version}")
        endif()

        if(arg_GET_RELATIONSHIP_COMMENT)
            set(version_comment "")
            if(version)
                string(APPEND version_comment "version '${version}'")
            endif()

            if(CMAKE_GENERATOR_PLATFORM)
                string(APPEND version_comment
                    ", CMAKE_GENERATOR_PLATFORM '${CMAKE_GENERATOR_PLATFORM}'")
            endif()

            if(CMAKE_GENERATOR_TOOLSET)
                string(APPEND version_comment
                    ", CMAKE_GENERATOR_TOOLSET '${CMAKE_GENERATOR_TOOLSET}'")
            endif()

            if(CMAKE_GENERATOR_INSTANCE)
                string(APPEND version_comment
                    ", CMAKE_GENERATOR_INSTANCE '${CMAKE_GENERATOR_INSTANCE}'")
            endif()

            # This isn't set for the Visual Studio generator.
            if(CMAKE_MAKE_PROGRAM)
                get_filename_component(make_program_name "${CMAKE_MAKE_PROGRAM}" NAME)
                set(make_program_comment
                    "and CMAKE_MAKE_PROGRAM: '${make_program_name}' ")
            endif()

            string(CONCAT relationship_comment
                "${project_spdx_id} is built using generator CMAKE_GENERATOR: '${CMAKE_GENERATOR}' "
                "${make_program_comment}"
                "${version_comment}"
            )
        endif()
    elseif(arg_BUILD_TOOL_TYPE STREQUAL "COMPILER")
        string(CONCAT package_summary
            "The compiler as identified by CMake, running on '${CMAKE_HOST_SYSTEM_NAME}' "
            "(${CMAKE_HOST_SYSTEM_PROCESSOR})")

        set(sbom_args
            FRIENDLY_PACKAGE_NAME "Compiler ${CMAKE_CXX_COMPILER_ID}"
            PACKAGE_SUMMARY "${package_summary}"
        )

        # CMake might not be able to extract the compiler version for all compilers.
        if(CMAKE_CXX_COMPILER_VERSION)
            list(APPEND sbom_args PACKAGE_VERSION "${CMAKE_CXX_COMPILER_VERSION}")
        endif()

        if(arg_GET_RELATIONSHIP_COMMENT)
            string(CONCAT relationship_comment
                "${project_spdx_id} is built by compiler: '${CMAKE_CXX_COMPILER_ID}' "
                "version: '${CMAKE_CXX_COMPILER_VERSION}' "
                "frontend variant: '${CMAKE_CXX_COMPILER_FRONTEND_VARIANT}'"
            )
        endif()
    elseif(arg_BUILD_TOOL_TYPE STREQUAL "LINKER")
        # Note this info is only available with CMake 3.29+, caller needs to ensure they don't call
        # this when the info is not available.
        set(sbom_args
            FRIENDLY_PACKAGE_NAME "Linker ${CMAKE_CXX_COMPILER_LINKER_ID}"
        )

        # CMake might not be able to extract the linker version for all platforms, e.g. for
        # MinGW + GCC.
        if(CMAKE_CXX_COMPILER_LINKER_VERSION)
            list(APPEND sbom_args PACKAGE_VERSION "${CMAKE_CXX_COMPILER_LINKER_VERSION}")
        endif()

        if(arg_GET_RELATIONSHIP_COMMENT)
            string(CONCAT relationship_comment
                "${project_spdx_id} is built by linker '${CMAKE_CXX_COMPILER_LINKER_ID}' "
                "version '${CMAKE_CXX_COMPILER_LINKER_VERSION}' "
                "frontend variant: '${CMAKE_CXX_COMPILER_LINKER_FRONTEND_VARIANT}'"
            )
        endif()
    endif()

    set(${arg_OUT_VAR_SBOM_ARGS} "${sbom_args}" PARENT_SCOPE)
    if(arg_GET_RELATIONSHIP_COMMENT)
        set(${arg_OUT_VAR_RELATIONSHIP_COMMENT} "${relationship_comment}" PARENT_SCOPE)
    endif()
endfunction()
