diff --git a/cmake/Qt3rdPartyLibraryHelpers.cmake b/cmake/Qt3rdPartyLibraryHelpers.cmake index c5719a504b2..17a6afd5551 100644 --- a/cmake/Qt3rdPartyLibraryHelpers.cmake +++ b/cmake/Qt3rdPartyLibraryHelpers.cmake @@ -130,23 +130,49 @@ function(qt_internal_add_cmake_library target) ) endfunction() +macro(qt_internal_get_3rdparty_library_sbom_options option_args single_args multi_args) + set(${option_args} "") + set(${single_args} + PACKAGE_VERSION + CPE_VENDOR + CPE_PRODUCT + LICENSE_EXPRESSION + DOWNLOAD_LOCATION + ${__qt_internal_sbom_single_args} + ) + set(${multi_args} + COPYRIGHTS + CPE # Common Platform Enumeration, free-form + ${__qt_internal_sbom_multi_args} + ) +endmacro() + # This function replaces qmake's qt_helper_lib feature. It is intended to # compile 3rdparty libraries as part of the build. # function(qt_internal_add_3rdparty_library target) qt_internal_get_add_library_option_args(library_option_args) + qt_internal_get_3rdparty_library_sbom_options( + sbom_option_args + sbom_single_args + sbom_multi_args + ) + set(option_args EXCEPTIONS INSTALL SKIP_AUTOMOC + ${sbom_option_args} ) set(single_args OUTPUT_DIRECTORY QMAKE_LIB_NAME + ${sbom_single_args} ) set(multi_args ${__default_private_args} ${__default_public_args} + ${sbom_multi_args} ) cmake_parse_arguments(PARSE_ARGV 1 arg @@ -253,6 +279,12 @@ function(qt_internal_add_3rdparty_library target) ) if(NOT BUILD_SHARED_LIBS OR arg_INSTALL) + set(will_install TRUE) + else() + set(will_install FALSE) + endif() + + if(will_install) qt_generate_3rdparty_lib_pri_file("${target}" "${arg_QMAKE_LIB_NAME}" pri_file) if(pri_file) qt_install(FILES "${pri_file}" DESTINATION "${INSTALL_MKSPECSDIR}/modules") @@ -327,6 +359,55 @@ function(qt_internal_add_3rdparty_library target) INTERPROCEDURAL_OPTIMIZATION OFF ) endif() + + if(QT_GENERATE_SBOM) + set(sbom_args "") + list(APPEND sbom_args TYPE QT_THIRD_PARTY_MODULE) + + if(NOT will_install) + list(APPEND sbom_args NO_INSTALL) + endif() + + qt_get_cmake_configurations(configs) + foreach(config IN LISTS configs) + _qt_internal_sbom_append_multi_config_aware_single_arg_option( + RUNTIME_PATH + "${INSTALL_BINDIR}" + "${config}" + sbom_args + ) + _qt_internal_sbom_append_multi_config_aware_single_arg_option( + LIBRARY_PATH + "${INSTALL_LIBDIR}" + "${config}" + sbom_args + ) + _qt_internal_sbom_append_multi_config_aware_single_arg_option( + ARCHIVE_PATH + "${INSTALL_LIBDIR}" + "${config}" + sbom_args + ) + endforeach() + + _qt_internal_forward_function_args( + FORWARD_APPEND + FORWARD_PREFIX arg + FORWARD_OUT_VAR sbom_args + FORWARD_SINGLE + ${sbom_single_args} + FORWARD_MULTI + ${sbom_multi_args} + ) + + _qt_internal_extend_sbom(${target} ${sbom_args}) + endif() + + qt_add_list_file_finalizer(qt_internal_finalize_3rdparty_library ${target}) +endfunction() + +function(qt_internal_finalize_3rdparty_library target) + _qt_internal_finalize_sbom(${target}) endfunction() function(qt_install_3rdparty_library_wrap_config_extra_file target) diff --git a/cmake/QtAppHelpers.cmake b/cmake/QtAppHelpers.cmake index f0dbd110ab1..1220680688e 100644 --- a/cmake/QtAppHelpers.cmake +++ b/cmake/QtAppHelpers.cmake @@ -4,10 +4,27 @@ # This function creates a CMake target for a Qt internal app. # Such projects had a load(qt_app) command. function(qt_internal_add_app target) + set(option_args + NO_INSTALL + INSTALL_VERSIONED_LINK + EXCEPTIONS + NO_UNITY_BUILD + ) + set(single_args + ${__default_target_info_args} + ${__qt_internal_sbom_single_args} + INSTALL_DIR + ) + set(multi_args + ${__default_private_args} + ${__qt_internal_sbom_multi_args} + PUBLIC_LIBRARIES + ) + cmake_parse_arguments(PARSE_ARGV 1 arg - "NO_INSTALL;INSTALL_VERSIONED_LINK;EXCEPTIONS;NO_UNITY_BUILD" - "${__default_target_info_args};INSTALL_DIR" - "${__default_private_args};PUBLIC_LIBRARIES" + "${option_args}" + "${single_args}" + "${multi_args}" ) _qt_internal_validate_all_args_are_parsed(arg) @@ -67,6 +84,10 @@ function(qt_internal_add_app target) MOC_OPTIONS ${arg_MOC_OPTIONS} ENABLE_AUTOGEN_TOOLS ${arg_ENABLE_AUTOGEN_TOOLS} DISABLE_AUTOGEN_TOOLS ${arg_DISABLE_AUTOGEN_TOOLS} + ATTRIBUTION_ENTRY_INDEX "${arg_ATTRIBUTION_ENTRY_INDEX}" + ATTRIBUTION_FILE_PATHS ${arg_ATTRIBUTION_FILE_PATHS} + ATTRIBUTION_FILE_DIR_PATHS ${arg_ATTRIBUTION_FILE_DIR_PATHS} + SBOM_DEPENDENCIES ${arg_SBOM_DEPENDENCIES} TARGET_VERSION ${arg_TARGET_VERSION} TARGET_PRODUCT ${arg_TARGET_PRODUCT} TARGET_DESCRIPTION ${arg_TARGET_DESCRIPTION} @@ -96,6 +117,38 @@ function(qt_internal_add_app target) TARGETS ${target}) endif() + if(QT_GENERATE_SBOM) + set(sbom_args "") + list(APPEND sbom_args TYPE QT_APP) + + qt_get_cmake_configurations(cmake_configs) + foreach(cmake_config IN LISTS cmake_configs) + qt_get_install_target_default_args( + OUT_VAR unused_install_targets_default_args + OUT_VAR_RUNTIME runtime_install_destination + RUNTIME "${arg_INSTALL_DIR}" + CMAKE_CONFIG "${cmake_config}" + ALL_CMAKE_CONFIGS ${cmake_configs}) + + _qt_internal_sbom_append_multi_config_aware_single_arg_option( + RUNTIME_PATH + "${runtime_install_destination}" + "${cmake_config}" + sbom_args + ) + endforeach() + + _qt_internal_forward_function_args( + FORWARD_APPEND + FORWARD_PREFIX arg + FORWARD_OUT_VAR sbom_args + FORWARD_OPTIONS + NO_INSTALL + ) + + _qt_internal_extend_sbom(${target} ${sbom_args}) + endif() + qt_add_list_file_finalizer(qt_internal_finalize_app ${target}) endfunction() @@ -143,4 +196,5 @@ function(qt_internal_finalize_app target) # set after a qt_internal_add_app call. qt_apply_rpaths(TARGET "${target}" INSTALL_PATH "${INSTALL_BINDIR}" RELATIVE_RPATH) qt_internal_apply_staging_prefix_build_rpath_workaround() + _qt_internal_finalize_sbom(${target}) endfunction() diff --git a/cmake/QtBaseGlobalTargets.cmake b/cmake/QtBaseGlobalTargets.cmake index 1e604559ed7..bf8be498a2a 100644 --- a/cmake/QtBaseGlobalTargets.cmake +++ b/cmake/QtBaseGlobalTargets.cmake @@ -133,6 +133,10 @@ target_include_directories(GlobalConfigPrivate INTERFACE ) add_library(Qt::GlobalConfigPrivate ALIAS GlobalConfigPrivate) add_library(${QT_CMAKE_EXPORT_NAMESPACE}::GlobalConfigPrivate ALIAS GlobalConfigPrivate) +qt_internal_add_sbom(GlobalConfigPrivate + TYPE QT_MODULE + IMMEDIATE_FINALIZATION +) qt_internal_setup_public_platform_target() diff --git a/cmake/QtBuildHelpers.cmake b/cmake/QtBuildHelpers.cmake index cce57cd5f6e..e101950cc04 100644 --- a/cmake/QtBuildHelpers.cmake +++ b/cmake/QtBuildHelpers.cmake @@ -196,6 +196,7 @@ function(qt_internal_get_qt_build_private_helpers out_var) QtResourceHelpers QtRpathHelpers QtSanitizerHelpers + QtSbomHelpers QtScopeFinalizerHelpers QtSeparateDebugInfo QtSimdHelpers @@ -278,7 +279,10 @@ function(qt_internal_get_qt_build_public_helpers out_var) QtPublicExternalProjectHelpers QtPublicFinalizerHelpers QtPublicFindPackageHelpers + QtPublicGitHelpers QtPublicPluginHelpers + QtPublicSbomGenerationHelpers + QtPublicSbomHelpers QtPublicTargetHelpers QtPublicTestHelpers QtPublicToolHelpers diff --git a/cmake/QtBuildPathsHelpers.cmake b/cmake/QtBuildPathsHelpers.cmake index 6431fa19375..21193db89a7 100644 --- a/cmake/QtBuildPathsHelpers.cmake +++ b/cmake/QtBuildPathsHelpers.cmake @@ -193,6 +193,8 @@ macro(qt_internal_setup_configure_install_paths) qt_configure_process_path(INSTALL_DESCRIPTIONSDIR "${INSTALL_ARCHDATADIR}/modules" "Module description files directory") + qt_configure_process_path(INSTALL_SBOMDIR "${INSTALL_ARCHDATADIR}/sbom" + "SBOM [PREFIX/sbom]") endmacro() macro(qt_internal_set_cmake_install_libdir) diff --git a/cmake/QtBuildRepoHelpers.cmake b/cmake/QtBuildRepoHelpers.cmake index 8bd0615090b..f35a2adb168 100644 --- a/cmake/QtBuildRepoHelpers.cmake +++ b/cmake/QtBuildRepoHelpers.cmake @@ -308,6 +308,12 @@ macro(qt_build_repo_begin) if(QT_INTERNAL_SYNCED_MODULES) set_property(GLOBAL PROPERTY _qt_synced_modules ${QT_INTERNAL_SYNCED_MODULES}) endif() + + _qt_internal_sbom_begin_project( + INSTALL_PREFIX "${QT_STAGING_PREFIX}" + INSTALL_SBOM_DIR "${INSTALL_SBOMDIR}" + QT_CPE + ) endmacro() # Runs delayed actions on some of the Qt targets. @@ -371,6 +377,8 @@ macro(qt_build_repo_end) set(QT_INTERNAL_FRESH_REQUESTED "FALSE" CACHE INTERNAL "") endif() + _qt_internal_sbom_end_project() + if(NOT QT_SUPERBUILD) qt_internal_qt_configure_end() endif() diff --git a/cmake/QtExecutableHelpers.cmake b/cmake/QtExecutableHelpers.cmake index 08da17b7e00..2e447a601d5 100644 --- a/cmake/QtExecutableHelpers.cmake +++ b/cmake/QtExecutableHelpers.cmake @@ -138,6 +138,10 @@ function(qt_internal_add_executable name) MOC_OPTIONS ${arg_MOC_OPTIONS} ENABLE_AUTOGEN_TOOLS ${arg_ENABLE_AUTOGEN_TOOLS} DISABLE_AUTOGEN_TOOLS ${arg_DISABLE_AUTOGEN_TOOLS} + ATTRIBUTION_ENTRY_INDEX "${arg_ATTRIBUTION_ENTRY_INDEX}" + ATTRIBUTION_FILE_PATHS ${arg_ATTRIBUTION_FILE_PATHS} + ATTRIBUTION_FILE_DIR_PATHS ${arg_ATTRIBUTION_FILE_DIR_PATHS} + SBOM_DEPENDENCIES ${arg_SBOM_DEPENDENCIES} ) set_target_properties("${name}" PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${arg_OUTPUT_DIRECTORY}" diff --git a/cmake/QtFindPackageHelpers.cmake b/cmake/QtFindPackageHelpers.cmake index af6057bf51f..b71226f322b 100644 --- a/cmake/QtFindPackageHelpers.cmake +++ b/cmake/QtFindPackageHelpers.cmake @@ -233,8 +233,25 @@ macro(qt_find_package) qt_find_package_promote_targets_to_global_scope( "${qt_find_package_target_name}") endif() - endif() + set(_qt_find_package_sbom_args "") + + if(_qt_find_package_found_version) + list(APPEND _qt_find_package_sbom_args + PACKAGE_VERSION "${_qt_find_package_found_version}" + ) + endif() + + # Work around: QTBUG-125371 + if(NOT "${ARGV0}" STREQUAL "Qt6") + _qt_internal_sbom_record_system_library_usage( + "${qt_find_package_target_name}" + TYPE SYSTEM_LIBRARY + FRIENDLY_PACKAGE_NAME "${ARGV0}" + ${_qt_find_package_sbom_args} + ) + endif() + endif() endforeach() if(arg_MODULE_NAME AND arg_QMAKE_LIB diff --git a/cmake/QtInternalTargets.cmake b/cmake/QtInternalTargets.cmake index d7eadc1a73a..21670973246 100644 --- a/cmake/QtInternalTargets.cmake +++ b/cmake/QtInternalTargets.cmake @@ -138,22 +138,42 @@ endfunction() add_library(PlatformCommonInternal INTERFACE) qt_internal_add_target_aliases(PlatformCommonInternal) target_link_libraries(PlatformCommonInternal INTERFACE Platform) +qt_internal_add_sbom(PlatformCommonInternal + TYPE QT_MODULE + IMMEDIATE_FINALIZATION +) add_library(PlatformModuleInternal INTERFACE) qt_internal_add_target_aliases(PlatformModuleInternal) target_link_libraries(PlatformModuleInternal INTERFACE PlatformCommonInternal) +qt_internal_add_sbom(PlatformModuleInternal + TYPE QT_MODULE + IMMEDIATE_FINALIZATION +) add_library(PlatformPluginInternal INTERFACE) qt_internal_add_target_aliases(PlatformPluginInternal) target_link_libraries(PlatformPluginInternal INTERFACE PlatformCommonInternal) +qt_internal_add_sbom(PlatformPluginInternal + TYPE QT_MODULE + IMMEDIATE_FINALIZATION +) add_library(PlatformAppInternal INTERFACE) qt_internal_add_target_aliases(PlatformAppInternal) target_link_libraries(PlatformAppInternal INTERFACE PlatformCommonInternal) +qt_internal_add_sbom(PlatformAppInternal + TYPE QT_MODULE + IMMEDIATE_FINALIZATION +) add_library(PlatformToolInternal INTERFACE) qt_internal_add_target_aliases(PlatformToolInternal) target_link_libraries(PlatformToolInternal INTERFACE PlatformAppInternal) +qt_internal_add_sbom(PlatformToolInternal + TYPE QT_MODULE + IMMEDIATE_FINALIZATION +) qt_internal_add_global_definition(QT_NO_JAVA_STYLE_ITERATORS) qt_internal_add_global_definition(QT_NO_QASCONST) diff --git a/cmake/QtModuleHelpers.cmake b/cmake/QtModuleHelpers.cmake index ba60cdef444..0a6084a157e 100644 --- a/cmake/QtModuleHelpers.cmake +++ b/cmake/QtModuleHelpers.cmake @@ -33,6 +33,7 @@ macro(qt_internal_get_internal_add_module_keywords option_args single_args multi SSG_HEADER_FILTERS HEADER_SYNC_SOURCE_DIRECTORY ${__default_target_info_args} + ${__qt_internal_sbom_single_args} ) set(${multi_args} QMAKE_MODULE_CONFIG @@ -43,6 +44,7 @@ macro(qt_internal_get_internal_add_module_keywords option_args single_args multi ${__default_private_args} ${__default_public_args} ${__default_private_module_args} + ${__qt_internal_sbom_multi_args} ) endmacro() @@ -235,6 +237,10 @@ function(qt_internal_add_module target) set_target_properties(${target} PROPERTIES _qt_is_internal_module TRUE) set_property(TARGET ${target} APPEND PROPERTY EXPORT_PROPERTIES _qt_is_internal_module) endif() + if(arg_HEADER_MODULE) + set_target_properties(${target} PROPERTIES _qt_is_header_module TRUE) + set_property(TARGET ${target} APPEND PROPERTY EXPORT_PROPERTIES _qt_is_header_module) + endif() if(NOT arg_CONFIG_MODULE_NAME) set(arg_CONFIG_MODULE_NAME "${module_lower}") @@ -635,6 +641,10 @@ function(qt_internal_add_module target) DISABLE_AUTOGEN_TOOLS ${arg_DISABLE_AUTOGEN_TOOLS} PRECOMPILED_HEADER ${arg_PRECOMPILED_HEADER} NO_PCH_SOURCES ${arg_NO_PCH_SOURCES} + ATTRIBUTION_ENTRY_INDEX "${arg_ATTRIBUTION_ENTRY_INDEX}" + ATTRIBUTION_FILE_PATHS ${arg_ATTRIBUTION_FILE_PATHS} + ATTRIBUTION_FILE_DIR_PATHS ${arg_ATTRIBUTION_FILE_DIR_PATHS} + SBOM_DEPENDENCIES ${arg_SBOM_DEPENDENCIES} ) # The public module define is not meant to be used when building the module itself, @@ -909,6 +919,43 @@ set(QT_ALLOW_MISSING_TOOLS_PACKAGES TRUE)") endif() qt_describe_module(${target}) + + set(sbom_args "") + + if(QT_GENERATE_SBOM) + list(APPEND sbom_args TYPE QT_MODULE) + + qt_get_cmake_configurations(configs) + foreach(config IN LISTS configs) + _qt_internal_sbom_append_multi_config_aware_single_arg_option( + RUNTIME_PATH + "${INSTALL_BINDIR}" + "${config}" + sbom_args + ) + _qt_internal_sbom_append_multi_config_aware_single_arg_option( + LIBRARY_PATH + "${INSTALL_LIBDIR}" + "${config}" + sbom_args + ) + _qt_internal_sbom_append_multi_config_aware_single_arg_option( + ARCHIVE_PATH + "${INSTALL_LIBDIR}" + "${config}" + sbom_args + ) + _qt_internal_sbom_append_multi_config_aware_single_arg_option( + FRAMEWORK_PATH + "${INSTALL_LIBDIR}/${fw_versioned_binary_dir}" + "${config}" + sbom_args + ) + endforeach() + + _qt_internal_extend_sbom(${target} ${sbom_args}) + endif() + qt_add_list_file_finalizer(qt_finalize_module ${target} ${arg_INTERNAL_MODULE} ${arg_NO_PRIVATE_MODULE}) endfunction() @@ -958,6 +1005,7 @@ function(qt_finalize_module target) qt_generate_module_pri_file("${target}" ${ARGN}) qt_internal_generate_pkg_config_file(${target}) qt_internal_apply_apple_privacy_manifest(${target}) + _qt_internal_finalize_sbom(${target}) endfunction() # Get a set of Qt module related values based on the target. diff --git a/cmake/QtPlatformTargetHelpers.cmake b/cmake/QtPlatformTargetHelpers.cmake index f1976b9975a..a89239b92ac 100644 --- a/cmake/QtPlatformTargetHelpers.cmake +++ b/cmake/QtPlatformTargetHelpers.cmake @@ -72,6 +72,11 @@ function(qt_internal_setup_public_platform_target) # Generate a pkgconfig for Qt::Platform. qt_internal_generate_pkg_config_file(Platform) + + qt_internal_add_sbom(Platform + TYPE QT_MODULE + IMMEDIATE_FINALIZATION + ) endfunction() function(qt_internal_get_platform_definition_include_dir install_interface build_interface) diff --git a/cmake/QtPluginHelpers.cmake b/cmake/QtPluginHelpers.cmake index a4188b72897..46953d89cb7 100644 --- a/cmake/QtPluginHelpers.cmake +++ b/cmake/QtPluginHelpers.cmake @@ -15,10 +15,12 @@ macro(qt_internal_get_internal_add_plugin_keywords option_args single_args multi INSTALL_DIRECTORY ARCHIVE_INSTALL_DIRECTORY ${__default_target_info_args} + ${__qt_internal_sbom_single_args} ) set(${multi_args} ${__default_private_args} ${__default_public_args} + ${__qt_internal_sbom_multi_args} DEFAULT_IF ) endmacro() @@ -312,6 +314,10 @@ function(qt_internal_add_plugin target) MOC_OPTIONS ${arg_MOC_OPTIONS} ENABLE_AUTOGEN_TOOLS ${arg_ENABLE_AUTOGEN_TOOLS} DISABLE_AUTOGEN_TOOLS ${arg_DISABLE_AUTOGEN_TOOLS} + ATTRIBUTION_ENTRY_INDEX "${arg_ATTRIBUTION_ENTRY_INDEX}" + ATTRIBUTION_FILE_PATHS ${arg_ATTRIBUTION_FILE_PATHS} + ATTRIBUTION_FILE_DIR_PATHS ${arg_ATTRIBUTION_FILE_DIR_PATHS} + SBOM_DEPENDENCIES ${arg_SBOM_DEPENDENCIES} ) qt_internal_add_repo_local_defines("${target}") @@ -414,6 +420,24 @@ function(qt_internal_add_plugin target) if(NOT arg_SKIP_INSTALL) list(APPEND finalizer_extra_args INSTALL_PATH "${install_directory}") endif() + + if(QT_GENERATE_SBOM) + set(sbom_args "") + list(APPEND sbom_args TYPE QT_PLUGIN) + + qt_get_cmake_configurations(configs) + foreach(config IN LISTS configs) + _qt_internal_sbom_append_multi_config_aware_single_arg_option( + INSTALL_PATH + "${install_directory}" + "${config}" + sbom_args + ) + endforeach() + + _qt_internal_extend_sbom(${target} ${sbom_args}) + endif() + qt_add_list_file_finalizer(qt_finalize_plugin ${target} ${finalizer_extra_args}) if(NOT arg_SKIP_INSTALL) @@ -442,6 +466,8 @@ function(qt_finalize_plugin target) qt_generate_plugin_pri_file("${target}") endif() endif() + + _qt_internal_finalize_sbom(${target}) endfunction() function(qt_get_sanitized_plugin_type plugin_type out_var) diff --git a/cmake/QtPostProcessHelpers.cmake b/cmake/QtPostProcessHelpers.cmake index 9654b186645..5cbee283b66 100644 --- a/cmake/QtPostProcessHelpers.cmake +++ b/cmake/QtPostProcessHelpers.cmake @@ -574,7 +574,7 @@ function(qt_generate_install_prefixes out_var) set(vars INSTALL_BINDIR INSTALL_INCLUDEDIR INSTALL_LIBDIR INSTALL_MKSPECSDIR INSTALL_ARCHDATADIR INSTALL_PLUGINSDIR INSTALL_LIBEXECDIR INSTALL_QMLDIR INSTALL_DATADIR INSTALL_DOCDIR INSTALL_TRANSLATIONSDIR INSTALL_SYSCONFDIR INSTALL_EXAMPLESDIR INSTALL_TESTSDIR - INSTALL_DESCRIPTIONSDIR) + INSTALL_DESCRIPTIONSDIR INSTALL_SBOMDIR) foreach(var ${vars}) get_property(docstring CACHE "${var}" PROPERTY HELPSTRING) diff --git a/cmake/QtProcessConfigureArgs.cmake b/cmake/QtProcessConfigureArgs.cmake index df0dfe48de5..94bf5f4bff1 100644 --- a/cmake/QtProcessConfigureArgs.cmake +++ b/cmake/QtProcessConfigureArgs.cmake @@ -150,6 +150,8 @@ while(NOT "${configure_args}" STREQUAL "") elseif(arg STREQUAL "-no-prefix") set(no_prefix_option TRUE) push("-DFEATURE_no_prefix=ON") + elseif(arg STREQUAL "-sbom") + push("-DQT_GENERATE_SBOM=ON") elseif(arg STREQUAL "-cmake-file-api") set(cmake_file_api TRUE) elseif(arg STREQUAL "-no-cmake-file-api") @@ -261,6 +263,11 @@ defstub(set_package_properties) defstub(qt_qml_find_python) defstub(qt_set01) defstub(qt_internal_check_if_linker_is_available) +defstub(qt_internal_add_sbom) +defstub(qt_internal_extend_sbom) +defstub(qt_internal_sbom_add_license) +defstub(qt_internal_extend_sbom_dependencies) +defstub(qt_find_package_extend_sbom) #################################################################################################### # Define functions/macros that are called in qt_cmdline.cmake files @@ -934,6 +941,7 @@ endforeach() translate_path_input(headerdir INSTALL_INCLUDEDIR) translate_path_input(plugindir INSTALL_PLUGINSDIR) translate_path_input(translationdir INSTALL_TRANSLATIONSDIR) +translate_path_input(sbomdir INSTALL_SBOMDIR) if(NOT "${INPUT_device}" STREQUAL "") push("-DQT_QMAKE_TARGET_MKSPEC=devices/${INPUT_device}") diff --git a/cmake/QtPublicCMakeHelpers.cmake b/cmake/QtPublicCMakeHelpers.cmake index 31e7591b990..bb4601d81db 100644 --- a/cmake/QtPublicCMakeHelpers.cmake +++ b/cmake/QtPublicCMakeHelpers.cmake @@ -434,6 +434,10 @@ function(_qt_internal_create_versionless_targets targets install_namespace) _qt_package_name _qt_package_version _qt_private_module_target_name + _qt_sbom_spdx_id + _qt_sbom_spdx_repo_document_namespace + _qt_sbom_spdx_relative_installed_repo_document_path + _qt_sbom_spdx_repo_project_name_lowercase ) set(supported_target_types STATIC_LIBRARY MODULE_LIBRARY SHARED_LIBRARY OBJECT_LIBRARY diff --git a/cmake/QtPublicDependencyHelpers.cmake b/cmake/QtPublicDependencyHelpers.cmake index bd8b4a55c42..f426cbc0b48 100644 --- a/cmake/QtPublicDependencyHelpers.cmake +++ b/cmake/QtPublicDependencyHelpers.cmake @@ -35,6 +35,37 @@ macro(_qt_internal_find_third_party_dependencies target target_dep_list) else() find_dependency(${__qt_${target}_find_package_args}) endif() + + _qt_internal_get_package_components_id( + PACKAGE_NAME "${__qt_${target}_pkg}" + COMPONENTS ${__qt_${target}_components} + OPTIONAL_COMPONENTS ${__qt_${target}_optional_components} + OUT_VAR_KEY __qt_${target}_package_components_id + ) + if(${__qt_${target}_pkg}_FOUND + AND __qt_${target}_third_party_package_${__qt_${target}_package_components_id}_provided_targets) + set(__qt_${target}_sbom_args "") + + if(${__qt_${target}_pkg}_VERSION) + list(APPEND __qt_${target}_sbom_args + PACKAGE_VERSION "${${__qt_${target}_pkg}_VERSION}" + ) + endif() + + # Work around: QTBUG-125371 + if(NOT "${ARGV0}" STREQUAL "Qt6") + foreach(__qt_${target}_provided_target + IN LISTS + __qt_${target}_third_party_package_${__qt_${target}_package_components_id}_provided_targets) + _qt_internal_sbom_record_system_library_usage( + "${__qt_${target}_provided_target}" + TYPE SYSTEM_LIBRARY + FRIENDLY_PACKAGE_NAME "${__qt_${target}_pkg}" + ${__qt_${target}_sbom_args} + ) + endforeach() + endif() + endif() endforeach() endmacro() diff --git a/cmake/QtPublicGitHelpers.cmake b/cmake/QtPublicGitHelpers.cmake new file mode 100644 index 00000000000..2326454a15f --- /dev/null +++ b/cmake/QtPublicGitHelpers.cmake @@ -0,0 +1,153 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# Copyright (C) 2023-2024 Jochem Rutgers +# SPDX-License-Identifier: BSD-3-Clause AND MIT + +macro(_qt_internal_find_git_package) + find_package(Git) +endmacro() + +# Helper to set the various git version variables in the parent scope across multiple return points. +macro(_qt_internal_set_git_query_variables) + set("${arg_OUT_VAR_PREFIX}git_hash" "${version_git_hash}" PARENT_SCOPE) + set("${arg_OUT_VAR_PREFIX}git_hash_short" "${version_git_head}" PARENT_SCOPE) + set("${arg_OUT_VAR_PREFIX}git_version" "${git_version}" PARENT_SCOPE) + + # git version sanitized for file paths. + string(REGEX REPLACE "[^-a-zA-Z0-9_.]+" "+" git_version_path "${git_version}") + set("${arg_OUT_VAR_PREFIX}git_version_path" "${git_version_path}" PARENT_SCOPE) +endmacro() + +# Caches the results per working-directory in global cmake properties. +# Sets the following variables in the outer scope: +# - git_hash: Full git hash. +# - git_hash_short: Short git hash. +# - git_version: Git version string. +# - git_version_path: Git version string sanitized for file paths. +function(_qt_internal_query_git_version) + set(opt_args + EMPTY_VALUE_WHEN_NOT_GIT_REPO + ) + set(single_args + WORKING_DIRECTORY + OUT_VAR_PREFIX + ) + 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(arg_EMPTY_VALUE_WHEN_NOT_GIT_REPO) + set(version_git_head "") + set(version_git_hash "") + set(version_git_branch "") + set(version_git_tag "") + set(git_version "") + else() + set(version_git_head "unknown") + set(version_git_hash "") + set(version_git_branch "dev") + set(version_git_tag "") + set(git_version "${version_git_head}+${version_git_branch}") + endif() + + if(NOT Git_FOUND) + message(STATUS "Git not found, skipping querying git version.") + _qt_internal_set_git_query_variables() + return() + endif() + + if(arg_WORKING_DIRECTORY) + set(working_directory "${arg_WORKING_DIRECTORY}") + else() + set(working_directory "${PROJECT_SOURCE_DIR}") + endif() + + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --is-inside-work-tree + WORKING_DIRECTORY "${working_directory}" + OUTPUT_VARIABLE is_inside_work_tree_output + RESULT_VARIABLE is_inside_work_tree_result + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + if((NOT is_inside_work_tree_result EQUAL 0) OR (NOT is_inside_work_tree_output STREQUAL "true")) + message(STATUS "Git repo not found, skipping querying git version.") + _qt_internal_set_git_query_variables() + return() + endif() + + get_cmake_property(git_hash_cache _qt_git_hash_cache_${working_directory}) + get_cmake_property(git_hash_short_cache _qt_git_hash_short_cache_${working_directory}) + get_cmake_property(git_version_cache _qt_git_version_cache_${working_directory}) + get_cmake_property(git_version_path_cache _qt_git_version_path_cache_${working_directory}) + if(git_hash_cache) + set(git_hash "${git_hash_cache}") + set(git_hash_short "${git_hash_short_cache}") + set(git_version "${git_version_cache}") + set(git_version_path "${git_version_path_cache}") + _qt_internal_set_git_query_variables() + return() + endif() + + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + WORKING_DIRECTORY "${working_directory}" + OUTPUT_VARIABLE version_git_head + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse HEAD + WORKING_DIRECTORY "${working_directory}" + OUTPUT_VARIABLE version_git_hash + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD + WORKING_DIRECTORY "${working_directory}" + OUTPUT_VARIABLE version_git_branch + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + execute_process( + COMMAND ${GIT_EXECUTABLE} tag --points-at HEAD + WORKING_DIRECTORY "${working_directory}" + OUTPUT_VARIABLE version_git_tag + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + string(REGEX REPLACE "[ \t\r\n].*$" "" version_git_tag "${version_git_tag}") + + execute_process( + COMMAND ${GIT_EXECUTABLE} status -s + WORKING_DIRECTORY "${working_directory}" + OUTPUT_VARIABLE version_git_dirty + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + if(NOT "${version_git_dirty}" STREQUAL "") + set(version_git_dirty "+dirty") + endif() + + if(NOT "${version_git_tag}" STREQUAL "") + set(git_version "${version_git_tag}") + + if("${git_version}" MATCHES "^v[0-9]+\.") + string(REGEX REPLACE "^v" "" git_version "${git_version}") + endif() + + set(git_version "${git_version}${version_git_dirty}") + else() + set(git_version + "${version_git_head}+${version_git_branch}${version_git_dirty}" + ) + endif() + + set_property(GLOBAL PROPERTY _qt_git_hash_cache_${working_directory} "${git_hash}") + set_property(GLOBAL PROPERTY _qt_git_hash_short_cache_${working_directory} "${git_hash_short}") + set_property(GLOBAL PROPERTY _qt_git_version_cache_${working_directory} "${git_version}") + set_property(GLOBAL PROPERTY _qt_git_version_path_cache_${working_directory} + "${git_version_path}") + + _qt_internal_set_git_query_variables() +endfunction() diff --git a/cmake/QtPublicSbomGenerationHelpers.cmake b/cmake/QtPublicSbomGenerationHelpers.cmake new file mode 100644 index 00000000000..0ada3ce938c --- /dev/null +++ b/cmake/QtPublicSbomGenerationHelpers.cmake @@ -0,0 +1,1139 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# Copyright (C) 2023-2024 Jochem Rutgers +# SPDX-License-Identifier: MIT AND BSD-3-Clause + +# Helper to set a single arg option to a default value if not set. +function(qt_internal_sbom_set_default_option_value option_name default) + if(NOT arg_${option_name}) + set(arg_${option_name} "${default}" PARENT_SCOPE) + endif() +endfunction() + +# Helper to set a single arg option to a default value if not set. +# Errors out if the end value is empty. Including if the default value was empty. +function(qt_internal_sbom_set_default_option_value_and_error_if_empty option_name default) + qt_internal_sbom_set_default_option_value("${option_name}" "${default}") + if(NOT arg_${option_name}) + message(FATAL_ERROR "Specifying a non-empty ${option_name} is required") + endif() +endfunction() + +# Computes the current platform CPE. +# Mostly matches the OS and architecture. +function(_qt_internal_sbom_get_platform_cpe out_var) + set(cpe "") + + if(CMAKE_SYSTEM_PROCESSOR) + set(system_processor "${CMAKE_SYSTEM_PROCESSOR}") + else() + set(system_processor "*") + endif() + + if(WIN32) + if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "AMD64") + set(arch "x64") + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "IA64") + set(arch "x64") + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ARM64") + set(arch "arm64") + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "X86") + set(arch "x86") + elseif(CMAKE_CXX_COMPILER MATCHES "64") + set(arch "x64") + elseif(CMAKE_CXX_COMPILER MATCHES "86") + set(arch "x86") + else() + set(arch "*") + endif() + + if("${CMAKE_SYSTEM_VERSION}" STREQUAL "6.1") + set(cpe "cpe:2.3:o:microsoft:windows_7:-:*:*:*:*:*:${arch}:*") + elseif("${CMAKE_SYSTEM_VERSION}" STREQUAL "6.2") + set(cpe "cpe:2.3:o:microsoft:windows_8:-:*:*:*:*:*:${arch}:*") + elseif("${CMAKE_SYSTEM_VERSION}" STREQUAL "6.3") + set(cpe "cpe:2.3:o:microsoft:windows_8.1:-:*:*:*:*:*:${arch}:*") + elseif("${CMAKE_SYSTEM_VERSION}" GREATER_EQUAL 10) + set(cpe "cpe:2.3:o:microsoft:windows_10:-:*:*:*:*:*:${arch}:*") + else() + set(cpe "cpe:2.3:o:microsoft:windows:-:*:*:*:*:*:${arch}:*") + endif() + elseif(APPLE) + set(cpe "cpe:2.3:o:apple:mac_os:*:*:*:*:*:*:${system_processor}:*") + elseif(UNIX) + set(cpe "cpe:2.3:o:*:*:-:*:*:*:*:*:${system_processor}:*") + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm") + set(cpe "cpe:2.3:o:arm:arm:-:*:*:*:*:*:*:*") + else() + message(DEBUG "Can't compute CPE for unsupported platform") + set(cpe "cpe:2.3:o:*:*:-:*:*:*:*:*:*:*") + endif() + + set(${out_var} "${cpe}" PARENT_SCOPE) +endfunction() + +# Helper that returns the directory where the intermediate sbom files will be generated. +function(_qt_internal_get_current_project_sbom_dir out_var) + set(sbom_dir "${PROJECT_BINARY_DIR}/qt_sbom") + set(${out_var} "${sbom_dir}" PARENT_SCOPE) +endfunction() + +# Helper to return the path to staging spdx file, where content will be incrementally appended to. +function(_qt_internal_get_staging_area_spdx_file_path out_var) + _qt_internal_get_current_project_sbom_dir(sbom_dir) + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + set(staging_area_spdx_file "${sbom_dir}/staging-${repo_project_name_lowercase}.spdx.in") + set(${out_var} "${staging_area_spdx_file}" PARENT_SCOPE) +endfunction() + +# Starts recording information for the generation of an sbom for a project. +# The intermediate files that generate the sbom are generated at cmake generation time, but are only +# actually run at build time or install time. +# The files are tracked in cmake global properties. +function(_qt_internal_sbom_begin_project_generate) + set(opt_args "") + set(single_args + OUTPUT + LICENSE + COPYRIGHT + DOWNLOAD_LOCATION + PROJECT + PROJECT_FOR_SPDX_ID + SUPPLIER + SUPPLIER_URL + NAMESPACE + CPE + OUT_VAR_PROJECT_SPDX_ID + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + string(TIMESTAMP current_utc UTC) + string(TIMESTAMP current_year "%Y" UTC) + + qt_internal_sbom_set_default_option_value(PROJECT "${PROJECT_NAME}") + + set(default_sbom_file_name + "${arg_PROJECT}/${arg_PROJECT}-sbom-${QT_SBOM_GIT_VERSION_PATH}.spdx") + set(default_install_sbom_path + "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/${default_sbom_file_name}") + + qt_internal_sbom_set_default_option_value(OUTPUT "${default_install_sbom_path}") + + qt_internal_sbom_set_default_option_value(LICENSE "NOASSERTION") + qt_internal_sbom_set_default_option_value(PROJECT_FOR_SPDX "${PROJECT_NAME}") + qt_internal_sbom_set_default_option_value_and_error_if_empty(SUPPLIER "") + qt_internal_sbom_set_default_option_value(COPYRIGHT "${current_year} ${arg_SUPPLIER}") + qt_internal_sbom_set_default_option_value_and_error_if_empty(SUPPLIER_URL + "${PROJECT_HOMEPAGE_URL}") + qt_internal_sbom_set_default_option_value(NAMESPACE + "${arg_SUPPLIER}/spdxdocs/${arg_PROJECT}-${QT_SBOM_GIT_VERSION}") + + if(arg_CPE) + set(QT_SBOM_CPE "${arg_CPE}") + else() + _qt_internal_sbom_get_platform_cpe(platform_cpe) + set(QT_SBOM_CPE "${platform_cpe}") + endif() + + string(REGEX REPLACE "[^A-Za-z0-9.]+" "-" arg_PROJECT_FOR_SPDX_ID "${arg_PROJECT_FOR_SPDX_ID}") + string(REGEX REPLACE "-+$" "" arg_PROJECT_FOR_SPDX_ID "${arg_PROJECT_FOR_SPDX_ID}") + # Prevent collision with other generated SPDXID with -[0-9]+ suffix. + string(REGEX REPLACE "-([0-9]+)$" "\\1" arg_PROJECT_FOR_SPDX_ID "${arg_PROJECT_FOR_SPDX_ID}") + + set(project_spdx_id "SPDXRef-${arg_PROJECT_FOR_SPDX_ID}") + if(arg_OUT_VAR_PROJECT_SPDX_ID) + set(${arg_OUT_VAR_PROJECT_SPDX_ID} "${project_spdx_id}" PARENT_SCOPE) + endif() + + get_filename_component(doc_name "${arg_OUTPUT}" NAME_WLE) + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(cmake_configs "${CMAKE_CONFIGURATION_TYPES}") + else() + set(cmake_configs "${CMAKE_BUILD_TYPE}") + endif() + + qt_internal_sbom_set_default_option_value(DOWNLOAD_LOCATION "NOASSERTION") + + set(content + "SPDXVersion: SPDX-2.3 +DataLicense: CC0-1.0 +SPDXID: SPDXRef-DOCUMENT +DocumentName: ${doc_name} +DocumentNamespace: ${arg_NAMESPACE} +Creator: Organization: ${arg_SUPPLIER} +Creator: Tool: Qt Build System +CreatorComment: This SPDX document was created from CMake ${CMAKE_VERSION}, using the qt +build system from https://code.qt.io/cgit/qt/qtbase.git/tree/cmake/QtPublicSbomHelpers.cmake +Created: ${current_utc}\${QT_SBOM_EXTERNAL_DOC_REFS} + +PackageName: ${CMAKE_CXX_COMPILER_ID} +SPDXID: SPDXRef-compiler +PackageVersion: ${CMAKE_CXX_COMPILER_VERSION} +PackageDownloadLocation: NOASSERTION +PackageLicenseConcluded: NOASSERTION +PackageLicenseDeclared: NOASSERTION +PackageCopyrightText: NOASSERTION +PackageSupplier: Organization: Anonymous +FilesAnalyzed: false +PackageSummary: The compiler as identified by CMake, running on ${CMAKE_HOST_SYSTEM_NAME} (${CMAKE_HOST_SYSTEM_PROCESSOR}) +PrimaryPackagePurpose: APPLICATION +Relationship: SPDXRef-compiler BUILD_DEPENDENCY_OF ${project_spdx_id} +RelationshipComment: ${project_spdx_id} is built by compiler ${CMAKE_CXX_COMPILER_ID} (${CMAKE_CXX_COMPILER}) version ${CMAKE_CXX_COMPILER_VERSION} + +PackageName: ${arg_PROJECT} +SPDXID: ${project_spdx_id} +ExternalRef: SECURITY cpe23Type ${QT_SBOM_CPE} +ExternalRef: PACKAGE-MANAGER purl pkg:generic/${arg_SUPPLIER}/${arg_PROJECT}@${QT_SBOM_GIT_VERSION} +PackageVersion: ${QT_SBOM_GIT_VERSION} +PackageSupplier: Organization: ${arg_SUPPLIER} +PackageDownloadLocation: ${arg_DOWNLOAD_LOCATION} +PackageLicenseConcluded: ${arg_LICENSE} +PackageLicenseDeclared: ${arg_LICENSE} +PackageCopyrightText: ${arg_COPYRIGHT} +PackageHomePage: ${arg_SUPPLIER_URL} +PackageComment: Built by CMake ${CMAKE_VERSION} with ${cmake_configs} configuration for ${CMAKE_SYSTEM_NAME} (${CMAKE_SYSTEM_PROCESSOR}) +PackageVerificationCode: \${QT_SBOM_VERIFICATION_CODE} +BuiltDate: ${current_utc} +Relationship: SPDXRef-DOCUMENT DESCRIBES ${project_spdx_id} +") + + # Create the directory that will contain all sbom related files. + _qt_internal_get_current_project_sbom_dir(sbom_dir) + file(MAKE_DIRECTORY "${sbom_dir}") + + # Generate project document intro spdx file. + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + set(document_intro_file_name + "${sbom_dir}/SPDXRef-DOCUMENT-${repo_project_name_lowercase}.spdx.in") + file(GENERATE OUTPUT "${document_intro_file_name}" CONTENT "${content}") + + # This is the file that will be incrementally assembled by having content appended to it. + _qt_internal_get_staging_area_spdx_file_path(staging_area_spdx_file) + + get_filename_component(computed_sbom_file_name "${arg_OUTPUT}" NAME_WLE) + get_filename_component(computed_sbom_file_name_ext "${arg_OUTPUT}" LAST_EXT) + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(multi_config_suffix "-$") + else() + set(multi_config_suffix "") + endif() + + set(computed_sbom_file_name + "${computed_sbom_file_name}${multi_config_suffix}${computed_sbom_file_name_ext}") + + set(build_sbom_path "${sbom_dir}/${computed_sbom_file_name}") + + # Create cmake file to append the document intro spdx to the staging file. + set(create_staging_file "${sbom_dir}/append_document_to_staging${multi_config_suffix}.cmake") + set(content " + cmake_minimum_required(VERSION 3.16) + message(STATUS \"Starting SBOM generation in build dir: ${staging_area_spdx_file}\") + set(QT_SBOM_EXTERNAL_DOC_REFS \"\") + file(READ \"${document_intro_file_name}\" content) + # Override any previous file because we're starting from scratch. + file(WRITE \"${staging_area_spdx_file}\" \"\${content}\") +") + file(GENERATE OUTPUT "${create_staging_file}" CONTENT "${content}") + + set_property(GLOBAL PROPERTY _qt_sbom_project_name "${arg_PROJECT}") + set_property(GLOBAL PROPERTY _qt_sbom_build_output_path "${build_sbom_path}") + set_property(GLOBAL PROPERTY _qt_sbom_install_output_path "${arg_OUTPUT}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_include_files "${create_staging_file}") + + set_property(GLOBAL PROPERTY _qt_sbom_spdx_id_count 0) +endfunction() + +# Signals the end of recording sbom information for a project. +# Creates an 'sbom' custom target to generate an incomplete sbom at build time (no checksums). +# Creates install rules to install a complete (with checksums) sbom. +# Also allows running various post-installation steps like NTIA validation, auditing, json +# generation, etc +function(_qt_internal_sbom_end_project_generate) + set(opt_args + GENERATE_JSON + VERIFY + SHOW_TABLE + AUDIT + AUDIT_NO_ERROR + ) + set(single_args "") + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + get_property(sbom_build_output_path GLOBAL PROPERTY _qt_sbom_build_output_path) + get_property(sbom_install_output_path GLOBAL PROPERTY _qt_sbom_install_output_path) + + if(NOT sbom_build_output_path) + message(FATAL_ERROR "Call _qt_internal_sbom_begin_project() first") + endif() + + _qt_internal_get_staging_area_spdx_file_path(staging_area_spdx_file) + + if((arg_GENERATE_JSON OR arg_VERIFY) AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) + _qt_internal_sbom_find_python() + _qt_internal_sbom_find_python_dependencies() + endif() + + if(arg_GENERATE_JSON AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) + _qt_internal_sbom_generate_json() + endif() + + if(arg_VERIFY AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) + _qt_internal_sbom_verify_valid_and_ntia_compliant() + endif() + + if(arg_SHOW_TABLE AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) + _qt_internal_sbom_find_python_dependency_program(NAME sbom2doc REQUIRED) + _qt_internal_sbom_show_table() + endif() + + if(arg_AUDIT AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) + set(audit_no_error_option "") + if(arg_AUDIT_NO_ERROR) + set(audit_no_error_option NO_ERROR) + endif() + _qt_internal_sbom_find_python_dependency_program(NAME sbomaudit REQUIRED) + _qt_internal_sbom_audit(${audit_no_error_option}) + endif() + + get_cmake_property(cmake_include_files _qt_sbom_cmake_include_files) + get_cmake_property(cmake_end_include_files _qt_sbom_cmake_end_include_files) + get_cmake_property(cmake_verify_include_files _qt_sbom_cmake_verify_include_files) + + set(includes "") + if(cmake_include_files) + foreach(cmake_include_file IN LISTS cmake_include_files) + list(APPEND includes "include(\"${cmake_include_file}\")") + endforeach() + endif() + + if(cmake_end_include_files) + foreach(cmake_include_file IN LISTS cmake_end_include_files) + list(APPEND includes "include(\"${cmake_include_file}\")") + endforeach() + endif() + + list(JOIN includes "\n" includes) + + # Verification only makes sense on installation, where the checksums are present. + set(verify_includes "") + if(cmake_verify_include_files) + foreach(cmake_include_file IN LISTS cmake_verify_include_files) + list(APPEND verify_includes "include(\"${cmake_include_file}\")") + endforeach() + endif() + list(JOIN verify_includes "\n" verify_includes) + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(multi_config_suffix "-$") + else() + set(multi_config_suffix "") + endif() + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(content " + # QT_SBOM_BUILD_TIME be set to FALSE at install time, so don't override if it's set. + # This allows reusing the same cmake file for both build and install. + if(NOT DEFINED QT_SBOM_BUILD_TIME) + set(QT_SBOM_BUILD_TIME TRUE) + endif() + if(NOT QT_SBOM_OUTPUT_PATH) + set(QT_SBOM_OUTPUT_PATH \"${sbom_build_output_path}\") + endif() + set(QT_SBOM_VERIFICATION_CODES \"\") + ${includes} + if(QT_SBOM_BUILD_TIME) + message(STATUS \"Finalizing SBOM generation in build dir: \${QT_SBOM_OUTPUT_PATH}\") + configure_file(\"${staging_area_spdx_file}\" \"\${QT_SBOM_OUTPUT_PATH}\") + endif() +") + set(assemble_sbom "${sbom_dir}/assemble_sbom${multi_config_suffix}.cmake") + file(GENERATE OUTPUT "${assemble_sbom}" CONTENT "${content}") + + if(NOT TARGET sbom) + add_custom_target(sbom) + endif() + + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + + # Create a build target to create a build-time sbom (no verification codes or sha1s). + set(repo_sbom_target "sbom_${repo_project_name_lowercase}") + set(comment "") + string(APPEND comment "Assembling build time SPDX document without checksums for " + "${repo_project_name_lowercase}. Just for testing.") + add_custom_target(${repo_sbom_target} + COMMAND "${CMAKE_COMMAND}" -P "${assemble_sbom}" + COMMENT "${comment}" + VERBATIM + USES_TERMINAL # To avoid running two configs of the command in parallel + ) + add_dependencies(sbom ${repo_sbom_target}) + + set(extra_code_begin "") + set(extra_code_inner_end "") + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(configs ${CMAKE_CONFIGURATION_TYPES}) + + set(install_markers_dir "${sbom_dir}") + set(install_marker_path "${install_markers_dir}/finished_install-$.cmake") + + set(install_marker_code " + message(STATUS \"Writing install marker for config $: ${install_marker_path} \") + file(WRITE \"${install_marker_path}\" \"\") +") + + install(CODE "${install_marker_code}" COMPONENT sbom) + if(QT_SUPERBUILD) + install(CODE "${install_marker_code}" COMPONENT "sbom_${repo_project_name_lowercase}" + EXCLUDE_FROM_ALL) + endif() + + set(install_markers "") + foreach(config IN LISTS configs) + set(marker_path "${install_markers_dir}/finished_install-${config}.cmake") + list(APPEND install_markers "${marker_path}") + # Remove the markers on reconfiguration, just in case there are stale ones. + if(EXISTS "${marker_path}") + file(REMOVE "${marker_path}") + endif() + endforeach() + + set(extra_code_begin " + set(QT_SBOM_INSTALL_MARKERS \"${install_markers}\") + foreach(QT_SBOM_INSTALL_MARKER IN LISTS QT_SBOM_INSTALL_MARKERS) + if(NOT EXISTS \"\${QT_SBOM_INSTALL_MARKER}\") + set(QT_SBOM_INSTALLED_ALL_CONFIGS FALSE) + endif() + endforeach() +") + set(extra_code_inner_end " + foreach(QT_SBOM_INSTALL_MARKER IN LISTS QT_SBOM_INSTALL_MARKERS) + message(STATUS + \"Removing install marker: \${QT_SBOM_INSTALL_MARKER} \") + file(REMOVE \"\${QT_SBOM_INSTALL_MARKER}\") + endforeach() +") + endif() + + # Allow skipping checksum computation for testing purposes, while installing just the sbom + # documents, without requiring to build and install all the actual files. + if(QT_INTERNAL_SBOM_FAKE_CHECKSUM) + string(APPEND extra_code_begin " + set(QT_SBOM_FAKE_CHECKSUM TRUE)") + endif() + + set(assemble_sbom_install " + set(QT_SBOM_INSTALLED_ALL_CONFIGS TRUE) + ${extra_code_begin} + if(QT_SBOM_INSTALLED_ALL_CONFIGS) + set(QT_SBOM_BUILD_TIME FALSE) + set(QT_SBOM_OUTPUT_PATH \"${sbom_install_output_path}\") + include(\"${assemble_sbom}\") + list(SORT QT_SBOM_VERIFICATION_CODES) + string(REPLACE \";\" \"\" QT_SBOM_VERIFICATION_CODES \"\${QT_SBOM_VERIFICATION_CODES}\") + file(WRITE \"${sbom_dir}/verification.txt\" \"\${QT_SBOM_VERIFICATION_CODES}\") + file(SHA1 \"${sbom_dir}/verification.txt\" QT_SBOM_VERIFICATION_CODE) + message(STATUS \"Finalizing SBOM generation in install dir: \${QT_SBOM_OUTPUT_PATH}\") + configure_file(\"${staging_area_spdx_file}\" \"\${QT_SBOM_OUTPUT_PATH}\") + ${verify_includes} + ${extra_code_inner_end} + else() + message(STATUS \"Skipping SBOM finalization because not all configs were installed.\") + endif() +") + + install(CODE "${assemble_sbom_install}" COMPONENT sbom) + if(QT_SUPERBUILD) + install(CODE "${assemble_sbom_install}" COMPONENT "sbom_${repo_project_name_lowercase}" + EXCLUDE_FROM_ALL) + endif() + + # Clean up properties, so that they are empty for possible next repo in a top-level build. + set_property(GLOBAL PROPERTY _qt_sbom_cmake_include_files "") + set_property(GLOBAL PROPERTY _qt_sbom_cmake_end_include_files "") + set_property(GLOBAL PROPERTY _qt_sbom_cmake_verify_include_files "") +endfunction() + +# Helper to add info about a file to the sbom. +# Targets are backed by multiple files in multi-config builds. To support multi-config, +# we generate a -$ file for each config, but we only include / install the one that is +# specified via the CONFIG option. +# For build time sboms, we skip checking file existence and sha1 computation, because the files +# are not installed yet. +function(_qt_internal_sbom_generate_add_file) + set(opt_args + OPTIONAL + ) + set(single_args + FILENAME + FILETYPE + RELATIONSHIP + SPDXID + CONFIG + LICENSE + COPYRIGHT + INSTALL_PREFIX + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + qt_internal_sbom_set_default_option_value_and_error_if_empty(FILENAME "") + qt_internal_sbom_set_default_option_value_and_error_if_empty(FILETYPE "") + + _qt_internal_sbom_get_and_check_spdx_id( + VARIABLE arg_SPDXID + CHECK "${arg_SPDXID}" + HINTS "SPDXRef-${arg_FILENAME}" + ) + + qt_internal_sbom_set_default_option_value(LICENSE "NOASSERTION") + qt_internal_sbom_set_default_option_value(COPYRIGHT "NOASSERTION") + + get_property(sbom_project_name GLOBAL PROPERTY _qt_sbom_project_name) + if(NOT sbom_project_name) + message(FATAL_ERROR "Call _qt_internal_sbom_begin_project() first") + endif() + if(NOT arg_RELATIONSHIP) + set(arg_RELATIONSHIP "SPDXRef-${sbom_project_name} CONTAINS ${arg_SPDXID}") + else() + string(REPLACE + "@QT_SBOM_LAST_SPDXID@" "${arg_SPDXID}" arg_RELATIONSHIP "${arg_RELATIONSHIP}") + endif() + + set(fields "") + + if(arg_LICENSE) + set(fields "${fields} +LicenseConcluded: ${arg_LICENSE}" + ) + else() + set(fields "${fields} +LicenseConcluded: NOASSERTION" + ) + endif() + + if(arg_COPYRIGHT) + set(fields "${fields} +FileCopyrightText: ${arg_COPYRIGHT}" + ) + else() + set(fields "${fields} +FileCopyrightText: NOASSERTION" + ) + endif() + + set(file_suffix_to_generate "") + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(file_suffix_to_generate "-$") + endif() + + if(arg_CONFIG) + set(file_suffix_to_install "-${arg_CONFIG}") + else() + set(file_suffix_to_install "") + endif() + + _qt_internal_get_staging_area_spdx_file_path(staging_area_spdx_file) + + if(arg_INSTALL_PREFIX) + set(install_prefix "${arg_INSTALL_PREFIX}") + else() + set(install_prefix "${CMAKE_INSTALL_PREFIX}") + endif() + + set(content " + if(NOT EXISTS $ENV{DESTDIR}${install_prefix}/${arg_FILENAME} + AND NOT QT_SBOM_BUILD_TIME AND NOT QT_SBOM_FAKE_CHECKSUM) + if(NOT ${arg_OPTIONAL}) + message(FATAL_ERROR \"Cannot find ${arg_FILENAME}\") + endif() + else() + if(NOT QT_SBOM_BUILD_TIME) + if(QT_SBOM_FAKE_CHECKSUM) + set(sha1 \"158942a783ee1095eafacaffd93de73edeadbeef\") + else() + file(SHA1 $ENV{DESTDIR}${install_prefix}/${arg_FILENAME} sha1) + endif() + list(APPEND QT_SBOM_VERIFICATION_CODES \${sha1}) + endif() + file(APPEND \"${staging_area_spdx_file}\" +\" +FileName: ./${arg_FILENAME} +SPDXID: ${arg_SPDXID} +FileType: ${arg_FILETYPE} +FileChecksum: SHA1: \${sha1}${fields} +LicenseInfoInFile: NOASSERTION +Relationship: ${arg_RELATIONSHIP} +\" + ) + endif() +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(file_sbom "${sbom_dir}/${arg_SPDXID}${file_suffix_to_generate}.cmake") + file(GENERATE OUTPUT "${file_sbom}" CONTENT "${content}") + + set(file_sbom_to_install "${sbom_dir}/${arg_SPDXID}${file_suffix_to_install}.cmake") + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_include_files "${file_sbom_to_install}") +endfunction() + +# Helper to add info about an external reference to a different project spdx sbom file. +function(_qt_internal_sbom_generate_add_external_reference) + set(opt_args + NO_AUTO_RELATIONSHIP + ) + set(single_args + EXTERNAL + FILENAME + RENAME + SPDXID + RELATIONSHIP + + ) + set(multi_args + INSTALL_PREFIXES + ) + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + qt_internal_sbom_set_default_option_value_and_error_if_empty(EXTERNAL "") + qt_internal_sbom_set_default_option_value_and_error_if_empty(FILENAME "") + + if(NOT arg_SPDXID) + get_property(spdx_id_count GLOBAL PROPERTY _qt_sbom_spdx_id_count) + set(arg_SPDXID "DocumentRef-${spdx_id_count}") + math(EXPR spdx_id_count "${spdx_id_count} + 1") + set_property(GLOBAL PROPERTY _qt_sbom_spdx_id_count "${spdx_id_count}") + endif() + + if(NOT "${arg_SPDXID}" MATCHES "^DocumentRef-[-a-zA-Z0-9]+$") + message(FATAL_ERROR "Invalid DocumentRef \"${arg_SPDXID}\"") + endif() + + get_property(sbom_project_name GLOBAL PROPERTY _qt_sbom_project_name) + if(NOT sbom_project_name) + message(FATAL_ERROR "Call _qt_internal_sbom_begin_project() first") + endif() + if(NOT arg_RELATIONSHIP) + if(NOT arg_NO_AUTO_RELATIONSHIP) + set(arg_RELATIONSHIP + "SPDXRef-${sbom_project_name} DEPENDS_ON ${arg_SPDXID}:${arg_EXTERNAL}") + else() + set(arg_RELATIONSHIP "") + endif() + else() + string(REPLACE + "@QT_SBOM_LAST_SPDXID@" "${arg_SPDXID}" arg_RELATIONSHIP "${arg_RELATIONSHIP}") + endif() + + _qt_internal_get_staging_area_spdx_file_path(staging_area_spdx_file) + + set(install_prefixes "") + if(arg_INSTALL_PREFIXES) + list(APPEND install_prefixes ${arg_INSTALL_PREFIXES}) + endif() + if(QT6_INSTALL_PREFIX) + list(APPEND install_prefixes ${QT6_INSTALL_PREFIX}) + endif() + if(QT_ADDITIONAL_PACKAGES_PREFIX_PATH) + list(APPEND install_prefixes ${QT_ADDITIONAL_PACKAGES_PREFIX_PATH}) + endif() + if(QT_ADDITIONAL_SBOM_DOCUMENT_PATHS) + list(APPEND install_prefixes ${QT_ADDITIONAL_SBOM_DOCUMENT_PATHS}) + endif() + list(REMOVE_DUPLICATES install_prefixes) + + set(relationship_content "") + if(arg_RELATIONSHIP) + set(relationship_content " + file(APPEND \"${staging_area_spdx_file}\" + \" + Relationship: ${arg_RELATIONSHIP}\") +") + endif() + + # Filename may not exist yet, and it could be a generator expression. + set(content " + set(relative_file_name \"${arg_FILENAME}\") + set(document_dir_paths ${install_prefixes}) + foreach(document_dir_path IN LISTS document_dir_paths) + set(document_file_path \"\${document_dir_path}/\${relative_file_name}\") + if(EXISTS \"\${document_file_path}\") + break() + endif() + endforeach() + if(NOT EXISTS \"\${document_file_path}\") + message(FATAL_ERROR \"Could not find external SBOM document \${relative_file_name}\" + \" in any of the document dir paths: \${document_dir_paths} \" + ) + endif() + file(SHA1 \"\${document_file_path}\" ext_sha1) + file(READ \"\${document_file_path}\" ext_content) + + if(NOT \"\${ext_content}\" MATCHES \"[\\r\\n]DocumentNamespace:\") + message(FATAL_ERROR \"Missing DocumentNamespace in \${document_file_path}\") + endif() + + string(REGEX REPLACE \"^.*[\\r\\n]DocumentNamespace:[ \\t]*([^#\\r\\n]*).*$\" + \"\\\\1\" ext_ns \"\${ext_content}\") + + list(APPEND QT_SBOM_EXTERNAL_DOC_REFS \" +ExternalDocumentRef: ${arg_SPDXID} \${ext_ns} SHA1: \${ext_sha1}\") + + ${relationship_content} +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(ext_ref_sbom "${sbom_dir}/${arg_SPDXID}.cmake") + file(GENERATE OUTPUT "${ext_ref_sbom}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_end_include_files "${ext_ref_sbom}") +endfunction() + +# Helper to add info about a package to the sbom. Usually a package is a mapping to a cmake target. +function(_qt_internal_sbom_generate_add_package) + set(opt_args + CONTAINS_FILES + ) + set(single_args + PACKAGE + VERSION + LICENSE_DECLARED + LICENSE_CONCLUDED + COPYRIGHT + DOWNLOAD_LOCATION + RELATIONSHIP + SPDXID + SUPPLIER + PURPOSE + COMMENT + ) + set(multi_args + EXTREF + CPE + ) + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + qt_internal_sbom_set_default_option_value_and_error_if_empty(PACKAGE "") + + _qt_internal_sbom_get_and_check_spdx_id( + VARIABLE arg_SPDXID + CHECK "${arg_SPDXID}" + HINTS "SPDXRef-${arg_PACKAGE}" + ) + + qt_internal_sbom_set_default_option_value(DOWNLOAD_LOCATION "NOASSERTION") + qt_internal_sbom_set_default_option_value(VERSION "unknown") + qt_internal_sbom_set_default_option_value(SUPPLIER "Person: Anonymous") + qt_internal_sbom_set_default_option_value(LICENSE_DECLARED "NOASSERTION") + qt_internal_sbom_set_default_option_value(LICENSE_CONCLUDED "NOASSERTION") + qt_internal_sbom_set_default_option_value(COPYRIGHT "NOASSERTION") + qt_internal_sbom_set_default_option_value(PURPOSE "OTHER") + + set(fields "") + + if(arg_LICENSE_CONCLUDED) + set(fields "${fields} +PackageLicenseConcluded: ${arg_LICENSE_CONCLUDED}" + ) + else() + set(fields "${fields} +PackageLicenseConcluded: NOASSERTION" + ) + endif() + + if(arg_LICENSE_DECLARED) + set(fields "${fields} +PackageLicenseDeclared: ${arg_LICENSE_DECLARED}" + ) + else() + set(fields "${fields} +PackageLicenseDeclared: NOASSERTION" + ) + endif() + + foreach(ext_ref IN LISTS arg_EXTREF) + set(fields "${fields} +ExternalRef: ${ext_ref}" + ) + endforeach() + + if(arg_CONTAINS_FILES) + set(fields "${fields} +FilesAnalyzed: true" + ) + else() + set(fields "${fields} +FilesAnalyzed: false" + ) + endif() + + if(arg_COPYRIGHT) + set(fields "${fields} +PackageCopyrightText: ${arg_COPYRIGHT}" + ) + else() + set(fields "${fields} +PackageCopyrightText: NOASSERTION" + ) + endif() + + if(arg_PURPOSE) + set(fields "${fields} +PrimaryPackagePurpose: ${arg_PURPOSE}" + ) + else() + set(fields "${fields} +PrimaryPackagePurpose: OTHER" + ) + endif() + + if(arg_COMMENT) + set(fields "${fields} +PackageComment: ${arg_COMMENT}" + ) + endif() + + _qt_internal_sbom_get_platform_cpe(platform_cpe) + if(NOT arg_CPE) + set(fields "${fields} +ExternalRef: SECURITY cpe23Type ${platform_cpe}" + ) + endif() + + foreach(cpe IN LISTS arg_CPE) + set(fields "${fields} +ExternalRef: SECURITY cpe23Type ${cpe}" + ) + endforeach() + + get_property(sbom_project_name GLOBAL PROPERTY _qt_sbom_project_name) + if(NOT sbom_project_name) + message(FATAL_ERROR "Call _qt_internal_sbom_begin_project() first") + endif() + if(NOT arg_RELATIONSHIP) + set(arg_RELATIONSHIP "SPDXRef-${sbom_project_name} CONTAINS ${arg_SPDXID}") + else() + string(REPLACE "@QT_SBOM_LAST_SPDXID@" "${arg_SPDXID}" arg_RELATIONSHIP "${arg_RELATIONSHIP}") + endif() + + _qt_internal_get_staging_area_spdx_file_path(staging_area_spdx_file) + + set(content " + file(APPEND \"${staging_area_spdx_file}\" +\" +PackageName: ${arg_PACKAGE} +SPDXID: ${arg_SPDXID} +PackageDownloadLocation: ${arg_DOWNLOAD_LOCATION} +PackageVersion: ${arg_VERSION} +PackageSupplier: ${arg_SUPPLIER}${fields} +Relationship: ${arg_RELATIONSHIP} +\" + ) +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(package_sbom "${sbom_dir}/${arg_SPDXID}.cmake") + file(GENERATE OUTPUT "${package_sbom}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_include_files "${package_sbom}") +endfunction() + +# Helper to add a license text from a file or text into the sbom document. +function(_qt_internal_sbom_generate_add_license) + set(opt_args "") + set(single_args + LICENSE_ID + EXTRACTED_TEXT + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + qt_internal_sbom_set_default_option_value_and_error_if_empty(LICENSE_ID "") + + _qt_internal_sbom_get_and_check_spdx_id( + VARIABLE arg_SPDXID + CHECK "${arg_SPDXID}" + HINTS "SPDXRef-${arg_LICENSE_ID}" + ) + + if(NOT arg_EXTRACTED_TEXT) + set(licenses_dir "${PROJECT_SOURCE_DIR}/LICENSES") + file(READ "${licenses_dir}/${arg_LICENSE_ID}.txt" arg_EXTRACTED_TEXT) + string(PREPEND arg_EXTRACTED_TEXT "") + string(APPEND arg_EXTRACTED_TEXT "") + endif() + + _qt_internal_get_staging_area_spdx_file_path(staging_area_spdx_file) + + set(content " + file(APPEND \"${staging_area_spdx_file}\" +\" +LicenseID: ${arg_LICENSE_ID} +ExtractedText: ${arg_EXTRACTED_TEXT} +\" + ) +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(license_sbom "${sbom_dir}/${arg_SPDXID}.cmake") + file(GENERATE OUTPUT "${license_sbom}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_end_include_files "${license_sbom}") +endfunction() + +# Helper to retrieve a valid spdx id, given some hints. +# HINTS can be a list of values, one of which will be sanitized and used as the spdx id. +# CHECK is expected to be a valid spdx id. +function(_qt_internal_sbom_get_and_check_spdx_id) + set(opt_args "") + set(single_args + VARIABLE + CHECK + ) + set(multi_args + HINTS + ) + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + qt_internal_sbom_set_default_option_value_and_error_if_empty(VARIABLE "") + + if(NOT arg_CHECK) + get_property(spdx_id_count GLOBAL PROPERTY _qt_sbom_spdx_id_count) + set(suffix "-${spdx_id_count}") + math(EXPR spdx_id_count "${spdx_id_count} + 1") + set_property(GLOBAL PROPERTY _qt_sbom_spdx_id_count "${spdx_id_count}") + + foreach(hint IN LISTS arg_HINTS) + _qt_internal_sbom_get_sanitized_spdx_id(id "${hint}") + if(id) + set(id "${id}${suffix}") + break() + endif() + endforeach() + + if(NOT id) + set(id "SPDXRef${suffix}") + endif() + else() + set(id "${arg_CHECK}") + endif() + + if("${id}" MATCHES "^SPDXRef-[-]+$" + OR (NOT "${id}" MATCHES "^SPDXRef-[-a-zA-Z0-9]+$")) + message(FATAL_ERROR "Invalid SPDXID \"${id}\"") + endif() + + set(${arg_VARIABLE} "${id}" PARENT_SCOPE) +endfunction() + +# Helper to find the python interpreter, to be able to run post-installation steps like NTIA +# verification. +macro(_qt_internal_sbom_find_python) + if(QT_INTERNAL_NO_SBOM_FIND_PYTHON_FRAMEWORK) + set(__qt_sbom_python_find_framework "${Python_FIND_FRAMEWORK}") + set(__qt_sbom_python3_find_framework "${Python3_FIND_FRAMEWORK}") + set(Python_FIND_FRAMEWORK NEVER) + set(Python3_FIND_FRAMEWORK NEVER) + endif() + + if(NOT Python3_EXECUTABLE) + # NTIA-compliance checker requires Python 3.9 or later. + find_package(Python3 3.9 REQUIRED COMPONENTS Interpreter) + endif() + + if(QT_INTERNAL_NO_SBOM_FIND_PYTHON_FRAMEWORK) + set(Python_FIND_FRAMEWORK ${__qt_sbom_python_find_framework}) + set(Python3_FIND_FRAMEWORK ${__qt_sbom_python3_find_framework}) + endif() +endmacro() + +# Helper to find the various python package dependencies needed to run the post-installation NTIA +# verification and the spdx format validation step. +function(_qt_internal_sbom_find_python_dependencies) + if(NOT Python3_EXECUTABLE) + message(FATAL_ERROR "Python interpreter not found for sbom dependencies.") + endif() + + if(QT_SBOM_HAVE_PYTHON_DEPS) + return() + endif() + execute_process( + COMMAND + ${Python3_EXECUTABLE} -c " +import spdx_tools.spdx.clitools.pyspdxtools +import ntia_conformance_checker.main +" + RESULT_VARIABLE res + OUTPUT_VARIABLE output + ERROR_VARIABLE output + ) + + if("${res}" STREQUAL "0") + set(QT_SBOM_HAVE_PYTHON_DEPS TRUE CACHE INTERNAL "") + else() + message(FATAL_ERROR "SBOM Python dependencies not found. Error:\n${output}") + endif() +endfunction() + +# Helper to find a python installed CLI utility. +# Expected to be in PATH. +function(_qt_internal_sbom_find_python_dependency_program) + set(opt_args + REQUIRED + ) + set(single_args + NAME + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set(program_name "${arg_NAME}") + string(TOUPPER "${program_name}" upper_name) + set(cache_var "QT_SBOM_PROGRAM_${upper_name}") + + find_program(${cache_var} + NAMES ${program_name} + ) + + if(NOT ${cache_var}) + if(arg_REQUIRED) + set(message_type "FATAL_ERROR") + set(prefix "Required ") + else() + set(message_type "STATUS") + set(prefix "Optional ") + endif() + message(${message_type} "${prefix}SBOM python program '${program_name}' not found.") + endif() +endfunction() + +# Helper to generate a json file. This also implies some additional validity checks, useful +# to ensure a proper sbom file. +function(_qt_internal_sbom_generate_json) + if(NOT Python3_EXECUTABLE) + message(FATAL_ERROR "Python interpreter not found for generating SBOM json file.") + endif() + + set(content " + message(STATUS \"Generating JSON: \${QT_SBOM_OUTPUT_PATH}.json\") + execute_process( + COMMAND ${Python3_EXECUTABLE} -m spdx_tools.spdx.clitools.pyspdxtools + -i \"\${QT_SBOM_OUTPUT_PATH}\" -o \"\${QT_SBOM_OUTPUT_PATH}.json\" + RESULT_VARIABLE res + ) + if(NOT res EQUAL 0) + message(FATAL_ERROR \"SBOM conversion to JSON failed: \${res}\") + endif() +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(verify_sbom "${sbom_dir}/convert_to_json.cmake") + file(GENERATE OUTPUT "${verify_sbom}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_verify_include_files "${verify_sbom}") +endfunction() + +# Helper to verify the generated sbom is valid and NTIA compliant. +function(_qt_internal_sbom_verify_valid_and_ntia_compliant) + if(NOT Python3_EXECUTABLE) + message(FATAL_ERROR "Python interpreter not found for verifying SBOM file.") + endif() + + set(content " + message(STATUS \"Verifying: \${QT_SBOM_OUTPUT_PATH}\") + execute_process( + COMMAND ${Python3_EXECUTABLE} -m spdx_tools.spdx.clitools.pyspdxtools + -i \"\${QT_SBOM_OUTPUT_PATH}\" + RESULT_VARIABLE res + ) + if(NOT res EQUAL 0) + message(FATAL_ERROR \"SBOM verification failed: \${res}\") + endif() + + execute_process( + COMMAND ${Python3_EXECUTABLE} -m ntia_conformance_checker.main + --file \"\${QT_SBOM_OUTPUT_PATH}\" + RESULT_VARIABLE res + ) + if(NOT res EQUAL 0) + message(FATAL_ERROR \"SBOM NTIA verification failed: \{res}\") + endif() +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(verify_sbom "${sbom_dir}/verify_valid_and_ntia.cmake") + file(GENERATE OUTPUT "${verify_sbom}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_verify_include_files "${verify_sbom}") +endfunction() + +# Helper to show the main sbom document info in the form of a CLI table. +function(_qt_internal_sbom_show_table) + set(content " + message(STATUS \"Showing main SBOM document info: \${QT_SBOM_OUTPUT_PATH}\") + execute_process( + COMMAND sbom2doc -i \"\${QT_SBOM_OUTPUT_PATH}\" + RESULT_VARIABLE res + ) + if(NOT res EQUAL 0) + message(FATAL_ERROR \"Showing SBOM document failed: \${res}\") + endif() +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(verify_sbom "${sbom_dir}/show_table.cmake") + file(GENERATE OUTPUT "${verify_sbom}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_verify_include_files "${verify_sbom}") +endfunction() + +# Helper to audit the generated sbom. +function(_qt_internal_sbom_audit) + set(opt_args NO_ERROR) + set(single_args "") + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set(handle_error "") + if(NOT arg_NO_ERROR) + set(handle_error " + if(NOT res EQUAL 0) + message(FATAL_ERROR \"SBOM Audit failed: \${res}\") + endif() +") + endif() + + set(content " + message(STATUS \"Auditing SBOM: \${QT_SBOM_OUTPUT_PATH}\") + execute_process( + COMMAND sbomaudit -i \"\${QT_SBOM_OUTPUT_PATH}\" + --disable-license-check --cpecheck --offline + RESULT_VARIABLE res + ) + ${handle_error} +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(verify_sbom "${sbom_dir}/audit.cmake") + file(GENERATE OUTPUT "${verify_sbom}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_verify_include_files "${verify_sbom}") +endfunction() diff --git a/cmake/QtPublicSbomHelpers.cmake b/cmake/QtPublicSbomHelpers.cmake new file mode 100644 index 00000000000..e93f7762985 --- /dev/null +++ b/cmake/QtPublicSbomHelpers.cmake @@ -0,0 +1,2644 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Starts repo sbom generation. +# Should be called before any targets are added to the sbom. +# +# INSTALL_PREFIX should be passed a value like CMAKE_INSTALL_PREFIX or QT_STAGING_PREFIX +# INSTALL_SBOM_DIR should be passed a value like CMAKE_INSTALL_DATAROOTDIR or +# Qt's INSTALL_SBOMDIR +# SUPPLIER, SUPPLIER_URL, DOCUMENT_NAMESPACE, COPYRIGHTS are self-explanatory. +function(_qt_internal_sbom_begin_project) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + set(opt_args + QT_CPE + ) + set(single_args + INSTALL_PREFIX + INSTALL_SBOM_DIR + LICENSE_EXPRESSION + SUPPLIER + SUPPLIER_URL + DOWNLOAD_LOCATION + DOCUMENT_NAMESPACE + VERSION + SBOM_PROJECT_NAME + CPE + ) + set(multi_args + COPYRIGHTS + ) + + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(CMAKE_VERSION LESS_EQUAL "3.19") + if(QT_IGNORE_MIN_CMAKE_VERSION_FOR_SBOM) + message(STATUS + "Using CMake version older than 3.19, and QT_IGNORE_MIN_CMAKE_VERSION_FOR_SBOM was " + "set to ON. qt_attribution.json files will not be processed.") + else() + message(FATAL_ERROR + "Generating an SBOM requires CMake version 3.19 or newer. You can pass " + "-DQT_IGNORE_MIN_CMAKE_VERSION_FOR_SBOM=ON to try to generate the SBOM anyway, " + "but it is not officially supported, and the SBOM might be incomplete.") + endif() + endif() + + # The ntia-conformance-checker insists that a SPDX document contain at least one + # relationship that DESCRIBES a package, and that the package contains the string + # "Package-" in the spdx id. boot2qt spdx seems to contain the same. + + if(arg_SBOM_PROJECT_NAME) + _qt_internal_sbom_set_root_project_name("${arg_SBOM_PROJECT_NAME}") + else() + _qt_internal_sbom_set_root_project_name("${PROJECT_NAME}") + endif() + _qt_internal_sbom_get_root_project_name_for_spdx_id(repo_project_name_for_spdx_id) + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + + if(arg_SUPPLIER_URL) + set(repo_supplier_url "${arg_SUPPLIER_URL}") + else() + _qt_internal_sbom_get_default_supplier_url(repo_supplier_url) + endif() + + # Manual override. + if(arg_VERSION) + set(QT_SBOM_GIT_VERSION "${arg_VERSION}") + set(QT_SBOM_GIT_VERSION_PATH "${arg_VERSION}") + set(QT_SBOM_GIT_HASH "") # empty on purpose, no source of info + set(non_git_version "${arg_VERSION}") + else() + # Query git version info. + _qt_internal_find_git_package() + _qt_internal_query_git_version( + EMPTY_VALUE_WHEN_NOT_GIT_REPO + OUT_VAR_PREFIX __sbom_ + ) + set(QT_SBOM_GIT_VERSION "${__sbom_git_version}") + set(QT_SBOM_GIT_VERSION_PATH "${__sbom_git_version_path}") + set(QT_SBOM_GIT_HASH "${__sbom_git_hash}") + + # Git version might not be available. + set(non_git_version "${QT_REPO_MODULE_VERSION}") + if(NOT QT_SBOM_GIT_VERSION) + set(QT_SBOM_GIT_VERSION "${non_git_version}") + endif() + if(NOT QT_SBOM_GIT_VERSION_PATH) + set(QT_SBOM_GIT_VERSION_PATH "${non_git_version}") + endif() + endif() + + # Set the variables in the outer scope, so they can be accessed by the generation functions + # in QtPublicSbomGenerationHelpers.cmake + set(QT_SBOM_GIT_VERSION "${QT_SBOM_GIT_VERSION}" PARENT_SCOPE) + set(QT_SBOM_GIT_VERSION_PATH "${QT_SBOM_GIT_VERSION_PATH}" PARENT_SCOPE) + set(QT_SBOM_GIT_HASH "${QT_SBOM_GIT_HASH}" PARENT_SCOPE) + + if(arg_DOCUMENT_NAMESPACE) + set(repo_spdx_namespace "${arg_DOCUMENT_NAMESPACE}") + else() + # Used in external refs, either URI + UUID or URI + checksum. We use git version for now + # which is probably not conformat to spec. + set(repo_name_and_version "${repo_project_name_lowercase}-${QT_SBOM_GIT_VERSION}") + set(repo_spdx_namespace + "${repo_supplier_url}/spdxdocs/${repo_name_and_version}") + endif() + + if(non_git_version) + set(version_suffix "-${non_git_version}") + else() + set(version_suffix "") + endif() + + set(repo_spdx_relative_install_path + "${arg_INSTALL_SBOM_DIR}/${repo_project_name_lowercase}${version_suffix}.spdx") + + # Prepend DESTDIR, to allow relocating installed sbom. Needed for CI. + set(repo_spdx_install_path + "\$ENV{DESTDIR}${arg_INSTALL_PREFIX}/${repo_spdx_relative_install_path}") + + if(arg_LICENSE_EXPRESSION) + set(repo_license "${arg_LICENSE_EXPRESSION}") + else() + # Default to NOASSERTION for root repo SPDX packages, because we have some repos + # with multiple licenses and AND-ing them together will create a giant unreadable list. + # It's better to rely on the more granular package licenses. + set(repo_license "") + endif() + + if(arg_COPYRIGHTS) + list(JOIN arg_COPYRIGHTS "\n" arg_COPYRIGHTS) + set(repo_copyright "${arg_COPYRIGHTS}") + else() + _qt_internal_sbom_get_default_qt_copyright_header(repo_copyright) + endif() + + if(arg_SUPPLIER) + set(repo_supplier "${arg_SUPPLIER}") + else() + _qt_internal_sbom_get_default_supplier(repo_supplier) + endif() + + if(arg_CPE) + set(qt_cpe "${arg_CPE}") + elseif(arg_QT_CPE) + _qt_internal_sbom_get_cpe_qt_repo(qt_cpe) + else() + set(qt_cpe "") + endif() + + if(arg_DOWNLOAD_LOCATION) + set(download_location "${arg_DOWNLOAD_LOCATION}") + else() + _qt_internal_sbom_get_qt_repo_source_download_location(download_location) + endif() + + _qt_internal_sbom_begin_project_generate( + OUTPUT "${repo_spdx_install_path}" + LICENSE "${repo_license}" + COPYRIGHT "${repo_copyright}" + SUPPLIER "${repo_supplier}" # This must not contain spaces! + SUPPLIER_URL "${repo_supplier_url}" + DOWNLOAD_LOCATION "${download_location}" + PROJECT "${repo_project_name_lowercase}" + PROJECT_FOR_SPDX_ID "${repo_project_name_for_spdx_id}" + NAMESPACE "${repo_spdx_namespace}" + CPE "${qt_cpe}" + OUT_VAR_PROJECT_SPDX_ID repo_project_spdx_id + ) + + set_property(GLOBAL PROPERTY _qt_internal_sbom_repo_document_namespace + "${repo_spdx_namespace}") + + set_property(GLOBAL PROPERTY _qt_internal_sbom_relative_installed_repo_document_path + "${repo_spdx_relative_install_path}") + + set_property(GLOBAL PROPERTY _qt_internal_sbom_repo_project_name_lowercase + "${repo_project_name_lowercase}") + + set_property(GLOBAL PROPERTY _qt_internal_sbom_install_prefix + "${arg_INSTALL_PREFIX}") + + set_property(GLOBAL PROPERTY _qt_internal_sbom_project_spdx_id + "${repo_project_spdx_id}") + + file(GLOB license_files "${PROJECT_SOURCE_DIR}/LICENSES/LicenseRef-*.txt") + foreach(license_file IN LISTS license_files) + get_filename_component(license_id "${license_file}" NAME_WLE) + _qt_internal_sbom_add_license( + LICENSE_ID "${license_id}" + LICENSE_PATH "${license_file}" + NO_LICENSE_REF_PREFIX + ) + endforeach() + + # Make sure that any system library dependencies that have been found via qt_find_package or + # _qt_internal_find_third_party_dependencies have their spdx id registered now. + _qt_internal_sbom_record_system_library_spdx_ids() + + set_property(GLOBAL PROPERTY _qt_internal_sbom_repo_begin_called TRUE) +endfunction() + +# Ends repo sbom project generation. +# Should be called after all relevant targets are added to the sbom. +# Handles registering sbom info for recorded system libraries and then creates the sbom build +# and install rules. +function(_qt_internal_sbom_end_project) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + # Now that we know which system libraries are linked against because we added all + # subdirectories, we can add the recorded system libs to the sbom. + _qt_internal_sbom_add_recorded_system_libraries() + + # Run sbom finalization for targets that had it scheduled, but haven't run yet. + # This can happen when _qt_internal_sbom_end_project is called within the same + # subdirectory scope as where the targets are meant to be finalized, but that would be too late + # and the targets wouldn't be added to the sbom. + # This would mostly happen in user projects, and not Qt repos, because in Qt repos we afaik + # never create targets in the root cmakelists (aside from the qtbase Platform targets). + get_cmake_property(targets _qt_internal_sbom_targets_waiting_for_finalization) + if(targets) + foreach(target IN LISTS targets) + _qt_internal_finalize_sbom("${target}") + endforeach() + endif() + + set(end_project_options "") + if(QT_INTERNAL_SBOM_VERIFY OR QT_INTERNAL_SBOM_DEFAULT_CHECKS) + list(APPEND end_project_options VERIFY) + endif() + if(QT_INTERNAL_SBOM_SHOW_TABLE OR QT_INTERNAL_SBOM_DEFAULT_CHECKS) + list(APPEND end_project_options SHOW_TABLE) + endif() + if(QT_INTERNAL_SBOM_AUDIT OR QT_INTERNAL_SBOM_AUDIT_NO_ERROR) + list(APPEND end_project_options AUDIT) + endif() + if(QT_INTERNAL_SBOM_AUDIT_NO_ERROR) + list(APPEND end_project_options AUDIT_NO_ERROR) + endif() + if(QT_INTERNAL_SBOM_GENERATE_JSON OR QT_INTERNAL_SBOM_DEFAULT_CHECKS) + list(APPEND end_project_options GENERATE_JSON) + endif() + + _qt_internal_sbom_end_project_generate( + ${end_project_options} + ) + + # Clean up external document ref properties, because each repo needs to start from scratch + # in a top-level build. + get_cmake_property(known_external_documents _qt_known_external_documents) + set_property(GLOBAL PROPERTY _qt_known_external_documents "") + foreach(external_document IN LISTS known_external_documents) + set_property(GLOBAL PROPERTY _qt_known_external_documents_${external_document} "") + endforeach() + + set_property(GLOBAL PROPERTY _qt_internal_sbom_repo_begin_called FALSE) +endfunction() + +# Helper to get the options that _qt_internal_sbom_add_target understands. +# Also used in qt_find_package_extend_sbom. +macro(_qt_internal_get_sbom_add_target_options opt_args single_args multi_args) + set(${opt_args} + NO_INSTALL + NO_CURRENT_DIR_ATTRIBUTION + NO_DEFAULT_QT_LICENSE + NO_DEFAULT_DIRECTORY_QT_LICENSE + NO_DEFAULT_QT_COPYRIGHTS + NO_DEFAULT_QT_PACKAGE_VERSION + NO_DEFAULT_QT_SUPPLIER + ) + set(${single_args} + TYPE + PACKAGE_VERSION + FRIENDLY_PACKAGE_NAME + CPE_VENDOR + CPE_PRODUCT + LICENSE_EXPRESSION + DOWNLOAD_LOCATION + ATTRIBUTION_ENTRY_INDEX + ) + set(${multi_args} + COPYRIGHTS + LIBRARIES + PUBLIC_LIBRARIES + CPE + SBOM_DEPENDENCIES + ATTRIBUTION_FILE_PATHS + ATTRIBUTION_FILE_DIR_PATHS + ) + + _qt_internal_sbom_get_multi_config_single_args(multi_config_single_args) + list(APPEND single_args ${multi_config_single_args}) +endmacro() + +# Generate sbom information for a given target. +# Creates: +# - a SPDX package for the target +# - zero or more SPDX file entries for each installed binary file +# - each binary file entry gets a list of 'generated from source files' section +# - dependency relationships to other target packages +# - other relevant information like licenses, copyright, etc. +# For licenses, copyrights, these can either be passed as options, or read from qt_attribution.json +# files. +# For dependencies, these are either specified via options, or read from properties set on the +# target by qt_internal_extend_target. +function(_qt_internal_sbom_add_target target) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + _qt_internal_get_sbom_add_target_options(opt_args single_args multi_args) + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + get_target_property(target_type ${target} TYPE) + + # Mark the target as a Qt module for sbom processing purposes. + # Needed for non-standard targets like Bootstrap and QtLibraryInfo, that don't have a Qt:: + # namespace prefix. + if(arg_TYPE STREQUAL QT_MODULE) + set_target_properties(${target} PROPERTIES _qt_sbom_is_qt_module TRUE) + endif() + + set(project_package_options "") + + _qt_internal_sbom_is_qt_entity_type("${arg_TYPE}" is_qt_entity_type) + + if(arg_FRIENDLY_PACKAGE_NAME) + set(package_name_for_spdx_id "${arg_FRIENDLY_PACKAGE_NAME}") + else() + set(package_name_for_spdx_id "${target}") + endif() + + set(package_comment "") + + # Record the target spdx id right now, so we can refer to it in later attribution targets + # if needed. + _qt_internal_sbom_record_target_spdx_id(${target} + TYPE "${arg_TYPE}" + PACKAGE_NAME "${package_name_for_spdx_id}" + OUT_VAR package_spdx_id + ) + + set(attribution_args + PARENT_TARGET "${target}" + ) + + if(is_qt_entity_type) + list(APPEND attribution_args CREATE_SBOM_FOR_EACH_ATTRIBUTION) + endif() + + _qt_internal_forward_function_args( + FORWARD_APPEND + FORWARD_PREFIX arg + FORWARD_OUT_VAR attribution_args + FORWARD_SINGLE + ATTRIBUTION_ENTRY_INDEX + FORWARD_MULTI + ATTRIBUTION_FILE_PATHS + ATTRIBUTION_FILE_DIR_PATHS + ) + + if(NOT arg_NO_CURRENT_DIR_ATTRIBUTION + AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/qt_attribution.json") + list(APPEND attribution_args + ATTRIBUTION_FILE_PATHS "${CMAKE_CURRENT_SOURCE_DIR}/qt_attribution.json" + ) + endif() + + _qt_internal_sbom_handle_qt_attribution_files(qa ${attribution_args}) + + # Collect license expressions, but each expression needs to be abided, so we AND them. + set(license_expression "") + + if(arg_LICENSE_EXPRESSION) + set(license_expression "${arg_LICENSE_EXPRESSION}") + endif() + + if(is_qt_entity_type AND NOT arg_NO_DEFAULT_QT_LICENSE) + _qt_internal_sbom_get_default_qt_license_id(qt_license_expression) + _qt_internal_sbom_join_two_license_ids_with_op( + "${license_expression}" "AND" "${qt_license_expression}" + license_expression) + endif() + + # Allow setting a license expression per directory scope via a variable. + if(is_qt_entity_type AND QT_SBOM_LICENSE_EXPRESSION AND NOT arg_NO_DEFAULT_DIRECTORY_QT_LICENSE) + set(qt_license_expression "${_qt_internal_sbom_get_default_qt_license_id}") + _qt_internal_sbom_join_two_license_ids_with_op( + "${license_expression}" "AND" "${qt_license_expression}" + license_expression) + endif() + + if(qa_license_id) + if(NOT qa_license_id MATCHES "urn:dje:license") + _qt_internal_sbom_join_two_license_ids_with_op( + "${license_expression}" "AND" "${qa_license_id}" + license_expression) + else() + message(DEBUG + "Attribution license id contains invalid spdx license reference: ${qa_license_id}") + set(invalid_license_comment + " Attribution license ID with invalid spdx license reference: ") + string(APPEND invalid_license_comment "${qa_license_id}\n") + string(APPEND package_comment "${invalid_license_comment}") + endif() + endif() + + if(license_expression) + list(APPEND project_package_options LICENSE_CONCLUDED "${license_expression}") + if(is_qt_entity_type) + list(APPEND project_package_options LICENSE_DECLARED "${license_expression}") + endif() + endif() + + # Copyrights are additive, so we collect them from all sources that were found. + set(copyrights "") + if(arg_COPYRIGHTS) + list(APPEND copyrights "${arg_COPYRIGHTS}") + endif() + if(is_qt_entity_type AND NOT arg_NO_DEFAULT_QT_COPYRIGHTS) + _qt_internal_sbom_get_default_qt_copyright_header(qt_default_copyright) + if(qt_default_copyright) + list(APPEND copyrights "${qt_default_copyright}") + endif() + endif() + if(qa_copyrights) + list(APPEND copyrights "${qa_copyrights}") + endif() + if(copyrights) + list(JOIN copyrights "\n" copyrights) + list(APPEND project_package_options COPYRIGHT "${copyrights}") + endif() + + set(package_version "") + if(arg_PACKAGE_VERSION) + set(package_version "${arg_PACKAGE_VERSION}") + elseif(is_qt_entity_type AND NOT arg_NO_DEFAULT_QT_PACKAGE_VERSION) + _qt_internal_sbom_get_default_qt_package_version(package_version) + elseif(qa_version) + set(package_version "${qa_version}") + endif() + if(package_version) + list(APPEND project_package_options VERSION "${package_version}") + endif() + + set(supplier "") + if((is_qt_entity_type + OR arg_TYPE STREQUAL "QT_THIRD_PARTY_MODULE" + OR arg_TYPE STREQUAL "QT_THIRD_PARTY_SOURCES") + AND NOT arg_NO_DEFAULT_QT_SUPPLIER) + _qt_internal_sbom_get_default_supplier(supplier) + endif() + if(supplier) + list(APPEND project_package_options SUPPLIER "Organization: ${supplier}") + endif() + + set(download_location "") + if(arg_DOWNLOAD_LOCATION) + set(download_location "${arg_DOWNLOAD_LOCATION}") + elseif(is_qt_entity_type) + _qt_internal_sbom_get_qt_repo_source_download_location(download_location) + elseif(arg_TYPE STREQUAL "QT_THIRD_PARTY_MODULE" OR arg_TYPE STREQUAL "QT_THIRD_PARTY_SOURCES") + if(qa_download_location) + set(download_location "${qa_download_location}") + elseif(qa_homepage) + set(download_location "${qa_homepage}") + endif() + elseif(arg_TYPE STREQUAL "SYSTEM_LIBRARY") + # Try to get package url that was set using CMake's set_package_properties function. + # Relies on querying the internal global property name that CMake sets in its + # implementation. + get_cmake_property(target_url _CMAKE_${package_name_for_spdx_id}_URL) + if(target_url) + set(download_location "${target_url}") + endif() + if(NOT download_location AND qa_download_location) + set(download_location "${qa_download_location}") + endif() + endif() + + if(download_location) + list(APPEND project_package_options DOWNLOAD_LOCATION "${download_location}") + endif() + + _qt_internal_sbom_get_package_purpose("${arg_TYPE}" package_purpose) + list(APPEND project_package_options PURPOSE "${package_purpose}") + + if(arg_CPE) + list(APPEND project_package_options CPE "${arg_CPE}") + elseif(arg_CPE_VENDOR AND arg_CPE_PRODUCT) + _qt_internal_sbom_compute_security_cpe(custom_cpe + VENDOR "${arg_CPE_VENDOR}" + PRODUCT "${arg_CPE_PRODUCT}" + VERSION "${package_version}") + list(APPEND project_package_options CPE "${custom_cpe}") + elseif(is_qt_entity_type) + _qt_internal_sbom_compute_security_cpe_for_qt(cpe_list) + list(APPEND project_package_options CPE "${cpe_list}") + endif() + + if(is_qt_entity_type + OR arg_TYPE STREQUAL "QT_THIRD_PARTY_MODULE" + OR arg_TYPE STREQUAL "QT_THIRD_PARTY_SOURCES" + ) + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + + set(purl_prefix "PACKAGE-MANAGER purl") + set(purl_suffix + "pkg:generic/${supplier}/${repo_project_name_lowercase}@${QT_SBOM_GIT_VERSION}") + set(package_manager_external_ref + "${purl_prefix} ${purl_suffix}") + list(APPEND project_package_options EXTREF "${package_manager_external_ref}") + endif() + + if(arg_TYPE STREQUAL "QT_THIRD_PARTY_MODULE" + OR arg_TYPE STREQUAL "QT_THIRD_PARTY_SOURCES" + OR arg_TYPE STREQUAL "SYSTEM_LIBRARY" + OR arg_TYPE STREQUAL "THIRD_PARTY_LIBRARY" + OR arg_TYPE STREQUAL "THIRD_PARTY_LIBRARY_WITH_FILES" + ) + if(qa_attribution_name) + string(APPEND package_comment " Name: ${qa_attribution_name}\n") + endif() + + if(qa_description) + string(APPEND package_comment " Description: ${qa_description}\n") + endif() + + if(qa_qt_usage) + string(APPEND package_comment " Qt usage: ${qa_qt_usage}\n") + endif() + + if(qa_chosen_attribution_file_path) + string(APPEND package_comment + " Information extracted from:\n ${qa_chosen_attribution_file_path}\n") + endif() + + if(NOT "${qa_chosen_attribution_entry_index}" STREQUAL "") + string(APPEND package_comment + " Entry index: ${qa_chosen_attribution_entry_index}\n") + endif() + endif() + + if(package_comment) + list(APPEND project_package_options COMMENT "\n${package_comment}") + endif() + + _qt_internal_sbom_handle_target_dependencies("${target}" + SPDX_ID "${package_spdx_id}" + LIBRARIES "${arg_LIBRARIES}" + PUBLIC_LIBRARIES "${arg_PUBLIC_LIBRARIES}" + OUT_RELATIONSHIPS relationships + ) + + get_cmake_property(project_spdx_id _qt_internal_sbom_project_spdx_id) + list(APPEND relationships "${project_spdx_id} CONTAINS ${package_spdx_id}") + + list(REMOVE_DUPLICATES relationships) + list(JOIN relationships "\nRelationship: " relationships) + list(APPEND project_package_options RELATIONSHIP "${relationships}") + + _qt_internal_sbom_generate_add_package( + PACKAGE "${package_name_for_spdx_id}" + SPDXID "${package_spdx_id}" + CONTAINS_FILES + ${project_package_options} + ) + + set(no_install_option "") + if(arg_NO_INSTALL) + set(no_install_option NO_INSTALL) + endif() + + set(framework_option "") + if(APPLE AND NOT target_type STREQUAL "INTERFACE_LIBRARY") + get_target_property(is_framework ${target} FRAMEWORK) + if(is_framework) + set(framework_option "FRAMEWORK") + endif() + endif() + + set(install_prefix_option "") + get_cmake_property(install_prefix _qt_internal_sbom_install_prefix) + if(install_prefix) + set(install_prefix_option INSTALL_PREFIX "${install_prefix}") + endif() + + _qt_internal_forward_function_args( + FORWARD_PREFIX arg + FORWARD_OUT_VAR target_binary_multi_config_args + FORWARD_SINGLE + ${multi_config_single_args} + ) + + _qt_internal_sbom_handle_target_binary_files("${target}" + ${no_install_option} + ${framework_option} + ${install_prefix_option} + TYPE "${arg_TYPE}" + ${target_binary_multi_config_args} + SPDX_ID "${package_spdx_id}" + COPYRIGHTS "${copyrights}" + LICENSE_EXPRESSION "${license_expression}" + ) +endfunction() + +# Walks a target's direct dependencies and assembles a list of relationships between the packages +# of the target dependencies. +# Currently handles various Qt targets and system libraries. +function(_qt_internal_sbom_handle_target_dependencies target) + set(opt_args "") + set(single_args + SPDX_ID + OUT_RELATIONSHIPS + ) + set(multi_args + LIBRARIES + PUBLIC_LIBRARIES + ) + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_SPDX_ID) + message(FATAL_ERROR "SPDX_ID must be set") + endif() + set(package_spdx_id "${arg_SPDX_ID}") + + + set(libraries "") + if(arg_LIBRARIES) + list(APPEND libraries "${arg_LIBRARIES}") + endif() + + get_target_property(extend_libraries "${target}" _qt_extend_target_libraries) + if(extend_libraries) + list(APPEND libraries ${extend_libraries}) + endif() + + get_target_property(target_type ${target} TYPE) + set(valid_target_types + EXECUTABLE + SHARED_LIBRARY + MODULE_LIBRARY + STATIC_LIBRARY + OBJECT_LIBRARY + ) + if(target_type IN_LIST valid_target_types) + get_target_property(link_libraries "${target}" LINK_LIBRARIES) + if(link_libraries) + list(APPEND libraries ${link_Libraries}) + endif() + endif() + + set(public_libraries "") + if(arg_PUBLIC_LIBRARIES) + list(APPEND public_libraries "${arg_PUBLIC_LIBRARIES}") + endif() + + get_target_property(extend_public_libraries "${target}" _qt_extend_target_public_libraries) + if(extend_public_libraries) + list(APPEND public_libraries ${extend_public_libraries}) + endif() + + set(sbom_dependencies "") + if(arg_SBOM_DEPENDENCIES) + list(APPEND sbom_dependencies "${arg_SBOM_DEPENDENCIES}") + endif() + + get_target_property(extend_sbom_dependencies "${target}" _qt_extend_target_sbom_dependencies) + if(extend_sbom_dependencies) + list(APPEND sbom_dependencies ${extend_sbom_dependencies}) + endif() + + list(REMOVE_DUPLICATES libraries) + list(REMOVE_DUPLICATES public_libraries) + list(REMOVE_DUPLICATES sbom_dependencies) + + set(all_direct_libraries ${libraries} ${public_libraries} ${sbom_dependencies}) + list(REMOVE_DUPLICATES all_direct_libraries) + + set(spdx_dependencies "") + set(relationships "") + + # Go through each direct linked lib. + foreach(direct_lib IN LISTS all_direct_libraries) + if(NOT TARGET "${direct_lib}") + continue() + endif() + + # Some targets are Qt modules, even though they are not prefixed with Qt::, targets + # like Bootstrap and QtLibraryInfo. We use the property to differentiate them. + get_target_property(is_marked_as_qt_module "${direct_lib}" _qt_sbom_is_qt_module) + + # Custom sbom targets created by _qt_internal_create_sbom_target are always imported, so we + # need to differentiate them via this property. + get_target_property(is_custom_sbom_target "${direct_lib}" _qt_sbom_is_custom_sbom_target) + + if("${direct_lib}" MATCHES "^(Qt::.*)|(${QT_CMAKE_EXPORT_NAMESPACE}::.*)") + set(is_qt_prefixed TRUE) + else() + set(is_qt_prefixed FALSE) + endif() + + # is_qt_dependency is not strictly only a qt dependency, it applies to custom sbom + # targets as well. But I'm having a hard time to come up with a better name. + if(is_marked_as_qt_module OR is_custom_sbom_target OR is_qt_prefixed) + set(is_qt_dependency TRUE) + else() + set(is_qt_dependency FALSE) + endif() + + # Regular Qt dependency, depend on the relevant package, either within the current + # document or via an external document. + if(is_qt_dependency) + _qt_internal_sbom_is_external_target_dependency("${direct_lib}" + OUT_VAR is_dependency_in_external_document + ) + + if(is_dependency_in_external_document) + # External document case. + _qt_internal_sbom_add_external_target_dependency( + "${package_spdx_id}" "${direct_lib}" + extra_spdx_dependencies + extra_spdx_relationships + ) + if(extra_spdx_dependencies) + list(APPEND spdx_dependencies "${extra_spdx_dependencies}") + endif() + if(extra_spdx_relationships) + list(APPEND relationships "${extra_spdx_relationships}") + endif() + else() + # Dependency is part of current repo build. + _qt_internal_sbom_get_spdx_id_for_target("${direct_lib}" dep_spdx_id) + if(dep_spdx_id) + list(APPEND spdx_dependencies "${dep_spdx_id}") + else() + message(DEBUG "Could not add target dependency on ${direct_lib} " + "because no spdx id could be found") + endif() + endif() + else() + # If it's not a Qt dependency, then it's most likely a 3rd party dependency. + # If we are looking at a FindWrap dependency, we need to depend on either + # the system or vendored lib, whichever one the FindWrap script points to. + # If we are looking at a non-Wrap dependency, it's 99% a system lib. + __qt_internal_walk_libs( + "${direct_lib}" + lib_walked_targets + _discarded_out_var + "sbom_targets" + "collect_targets") + + # Detect if we are dealing with a vendored / bundled lib. + set(bundled_targets_found FALSE) + if(lib_walked_targets) + foreach(lib_walked_target IN LISTS lib_walked_targets) + get_target_property(is_3rdparty_bundled_lib + "${lib_walked_target}" _qt_module_is_3rdparty_library) + _qt_internal_sbom_get_spdx_id_for_target("${lib_walked_target}" lib_spdx_id) + + # Add a dependency on the vendored lib instead of the Wrap target. + if(is_3rdparty_bundled_lib AND lib_spdx_id) + list(APPEND spdx_dependencies "${lib_spdx_id}") + set(bundled_targets_found TRUE) + endif() + endforeach() + endif() + + # If no bundled libs were found as a result of walking the Wrap lib, we consider this + # a system lib, and add a dependency on it directly. + if(NOT bundled_targets_found) + _qt_internal_sbom_get_spdx_id_for_target("${direct_lib}" lib_spdx_id) + _qt_internal_sbom_is_external_target_dependency("${direct_lib}" + SYSTEM_LIBRARY + OUT_VAR is_dependency_in_external_document + ) + + if(lib_spdx_id) + if(NOT is_dependency_in_external_document) + list(APPEND spdx_dependencies "${lib_spdx_id}") + + # Mark the system library is used, so that we later generate an sbom for it. + _qt_internal_append_to_cmake_property_without_duplicates( + _qt_internal_sbom_consumed_system_library_targets + "${direct_lib}" + ) + else() + # Refer to the package in the external document. This can be the case + # in a top-level build, where a system library is reused across repos. + _qt_internal_sbom_add_external_target_dependency( + "${package_spdx_id}" "${direct_lib}" + extra_spdx_dependencies + extra_spdx_relationships + ) + if(extra_spdx_dependencies) + list(APPEND spdx_dependencies "${extra_spdx_dependencies}") + endif() + if(extra_spdx_relationships) + list(APPEND relationships "${extra_spdx_relationships}") + endif() + endif() + else() + message(DEBUG "Could not add target dependency on system library ${direct_lib} " + "because no spdx id could be found") + endif() + endif() + endif() + endforeach() + + foreach(dep_spdx_id IN LISTS spdx_dependencies) + set(relationship + "${package_spdx_id} DEPENDS_ON ${dep_spdx_id}" + ) + list(APPEND relationships "${relationship}") + endforeach() + + set(${arg_OUT_RELATIONSHIPS} "${relationships}" PARENT_SCOPE) +endfunction() + +# Checks whether the current target will have its sbom generated into the current repo sbom +# document, or whether it is present in an external sbom document. +function(_qt_internal_sbom_is_external_target_dependency target) + set(opt_args + SYSTEM_LIBRARY + ) + set(single_args + OUT_VAR + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + get_target_property(is_imported "${target}" IMPORTED) + get_target_property(is_custom_sbom_target "${target}" _qt_sbom_is_custom_sbom_target) + + _qt_internal_sbom_get_root_project_name_lower_case(current_repo_project_name) + get_property(target_repo_project_name TARGET ${target} + PROPERTY _qt_sbom_spdx_repo_project_name_lowercase) + + if(NOT "${target_repo_project_name}" STREQUAL "" + AND NOT "${target_repo_project_name}" STREQUAL "${current_repo_project_name}") + set(part_of_other_repo TRUE) + else() + set(part_of_other_repo FALSE) + endif() + + # A target is in an external document if + # 1) it is imported, and not a custom sbom target, and not a system library + # 2) it was created as part of another repo in a top-level build + if((is_imported AND NOT is_custom_sbom_target AND NOT arg_SYSTEM_LIBRARY) + OR part_of_other_repo) + set(is_dependency_in_external_document TRUE) + else() + set(is_dependency_in_external_document FALSE) + endif() + + set(${arg_OUT_VAR} "${is_dependency_in_external_document}" PARENT_SCOPE) +endfunction() + +# Handles generating an external document reference SDPX element for each target package that is +# located in a different spdx document. +function(_qt_internal_sbom_add_external_target_dependency + current_package_spdx_id + target_dep + out_spdx_dependencies + out_spdx_relationships + ) + set(target "${target_dep}") + + _qt_internal_sbom_get_spdx_id_for_target("${target}" dep_spdx_id) + + if(NOT dep_spdx_id) + message(DEBUG "Could not add external target dependency on ${target} " + "because no spdx id could be found") + set(${out_spdx_dependencies} "" PARENT_SCOPE) + set(${out_spdx_relationships} "" PARENT_SCOPE) + return() + endif() + + set(spdx_dependencies "") + set(spdx_relationships "") + + # Get the external document path and the repo it belongs to for the given target. + get_property(relative_installed_repo_document_path TARGET ${target} + PROPERTY _qt_sbom_spdx_relative_installed_repo_document_path) + + get_property(project_name_lowercase TARGET ${target} + PROPERTY _qt_sbom_spdx_repo_project_name_lowercase) + + if(relative_installed_repo_document_path AND project_name_lowercase) + _qt_internal_sbom_get_external_document_ref_spdx_id( + "${project_name_lowercase}" external_document_ref) + + get_cmake_property(known_external_document + _qt_known_external_documents_${external_document_ref}) + + set(relationship + "${current_package_spdx_id} DEPENDS_ON ${external_document_ref}:${dep_spdx_id}") + + list(APPEND spdx_relationships "${relationship}") + + # Only add a reference to the external document package, if we haven't done so already. + if(NOT known_external_document) + get_cmake_property(install_prefix _qt_internal_sbom_install_prefix) + + set(external_document "${relative_installed_repo_document_path}") + + _qt_internal_sbom_generate_add_external_reference( + NO_AUTO_RELATIONSHIP + EXTERNAL "${dep_spdx_id}" + FILENAME "${external_document}" + SPDXID "${external_document_ref}" + INSTALL_PREFIXES ${install_prefix} + ) + + set_property(GLOBAL PROPERTY + _qt_known_external_documents_${external_document_ref} TRUE) + set_property(GLOBAL APPEND PROPERTY + _qt_known_external_documents "${external_document_ref}") + endif() + else() + message(WARNING "Missing spdx document path for external ref: " + "package_name_for_spdx_id ${package_name_for_spdx_id} direct_lib ${direct_lib}") + endif() + + set(${out_spdx_dependencies} "${spdx_dependencies}" PARENT_SCOPE) + set(${out_spdx_relationships} "${spdx_relationships}" PARENT_SCOPE) +endfunction() + +# Handles addition of binary files SPDX entries for a given target. +# Is multi-config aware. +function(_qt_internal_sbom_handle_target_binary_files target) + set(opt_args + NO_INSTALL + FRAMEWORK + ) + set(single_args + TYPE + SPDX_ID + LICENSE_EXPRESSION + INSTALL_PREFIX + ) + set(multi_args + COPYRIGHTS + ) + + _qt_internal_sbom_get_multi_config_single_args(multi_config_single_args) + list(APPEND single_args ${multi_config_single_args}) + + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(arg_NO_INSTALL) + message(DEBUG "Skipping sbom target file processing ${target} because NO_INSTALL is set") + return() + endif() + + set(supported_types + QT_MODULE + QT_PLUGIN + QT_APP + QT_TOOL + QT_THIRD_PARTY_MODULE + QT_THIRD_PARTY_SOURCES + SYSTEM_LIBRARY + + # This will be meant for user projects, and are not currently used by Qt's sbom. + THIRD_PARTY_LIBRARY + THIRD_PARTY_LIBRARY_WITH_FILES + EXECUTABLE + LIBRARY + ) + + if(NOT arg_TYPE IN_LIST supported_types) + message(FATAL_ERROR "Unsupported target TYPE for SBOM creation: ${arg_TYPE}") + endif() + + set(types_without_files + SYSTEM_LIBRARY + QT_THIRD_PARTY_SOURCES + THIRD_PARTY_LIBRARY + ) + + get_target_property(target_type ${target} TYPE) + + if(arg_TYPE IN_LIST types_without_files) + message(DEBUG "Target ${target} has no binary files to reference in the SBOM " + "because it has the ${arg_TYPE} type.") + return() + endif() + + if(target_type STREQUAL "INTERFACE_LIBRARY") + message(DEBUG "Target ${target} has no binary files to reference in the SBOM " + "because it is an INTERFACE_LIBRARY.") + return() + endif() + + if(NOT arg_SPDX_ID) + message(FATAL_ERROR "SPDX_ID must be set") + endif() + set(package_spdx_id "${arg_SPDX_ID}") + + set(file_common_options "") + + list(APPEND file_common_options PACKAGE_SPDX_ID "${package_spdx_id}") + list(APPEND file_common_options PACKAGE_TYPE "${arg_TYPE}") + + if(arg_COPYRIGHTS) + list(APPEND file_common_options COPYRIGHTS "${arg_COPYRIGHTS}") + endif() + + if(arg_LICENSE_EXPRESSION) + list(APPEND file_common_options LICENSE_EXPRESSION "${arg_LICENSE_EXPRESSION}") + endif() + + if(arg_INSTALL_PREFIX) + list(APPEND file_common_options INSTALL_PREFIX "${arg_INSTALL_PREFIX}") + endif() + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(configs ${CMAKE_CONFIGURATION_TYPES}) + else() + set(configs "${CMAKE_BUILD_TYPE}") + endif() + + set(path_suffix "$") + + if(arg_FRAMEWORK) + set(library_path_kind FRAMEWORK_PATH) + else() + set(library_path_kind LIBRARY_PATH) + endif() + + if(arg_TYPE STREQUAL "QT_TOOL" + OR arg_TYPE STREQUAL "QT_APP" + OR arg_TYPE STREQUAL "EXECUTABLE") + if(NOT target_type STREQUAL "EXECUTABLE") + message(FATAL_ERROR "Unsupported target type: ${target_type}") + endif() + + get_target_property(app_is_bundle ${target} MACOSX_BUNDLE) + if(app_is_bundle) + _qt_internal_get_executable_bundle_info(bundle "${target}") + _qt_internal_path_join(path_suffix "${bundle_contents_binary_dir}" "${path_suffix}") + endif() + + _qt_internal_sbom_handle_multi_config_target_binary_file(${target} + PATH_KIND RUNTIME_PATH + PATH_SUFFIX "${path_suffix}" + OPTIONS ${file_common_options} + ) + elseif(arg_TYPE STREQUAL "QT_PLUGIN") + if(NOT (target_type STREQUAL "SHARED_LIBRARY" + OR target_type STREQUAL "STATIC_LIBRARY" + OR target_type STREQUAL "MODULE_LIBRARY")) + message(FATAL_ERROR "Unsupported target type: ${target_type}") + endif() + + _qt_internal_sbom_handle_multi_config_target_binary_file(${target} + PATH_KIND INSTALL_PATH + PATH_SUFFIX "${path_suffix}" + OPTIONS ${file_common_options} + ) + elseif(arg_TYPE STREQUAL "QT_MODULE" + OR arg_TYPE STREQUAL "QT_THIRD_PARTY_MODULE" + OR arg_TYPE STREQUAL "LIBRARY" + OR arg_TYPE STREQUAL "THIRD_PARTY_LIBRARY_WITH_FILES" + ) + if(WIN32 AND target_type STREQUAL "SHARED_LIBRARY") + _qt_internal_sbom_handle_multi_config_target_binary_file(${target} + PATH_KIND RUNTIME_PATH + PATH_SUFFIX "${path_suffix}" + OPTIONS ${file_common_options} + ) + + _qt_internal_sbom_handle_multi_config_target_binary_file(${target} + PATH_KIND ARCHIVE_PATH + PATH_SUFFIX "$" + OPTIONS + ${file_common_options} + IMPORT_LIBRARY + # OPTIONAL because on Windows the import library might not always be present, + # because no symbols are exported. + OPTIONAL + ) + elseif(target_type STREQUAL "SHARED_LIBRARY" OR target_type STREQUAL "STATIC_LIBRARY") + _qt_internal_sbom_handle_multi_config_target_binary_file(${target} + PATH_KIND "${library_path_kind}" + PATH_SUFFIX "${path_suffix}" + OPTIONS ${file_common_options} + ) + else() + message(FATAL_ERROR "Unsupported target type: ${target_type}") + endif() + endif() +endfunction() + +# Add a binary file of a target to the sbom (e.g a shared library or an executable). +# Adds relationships to the SBOM that the binary file was generated from its source files, +# as well as relationship to the owning package. +function(_qt_internal_sbom_add_binary_file target file_path) + set(opt_args + OPTIONAL + IMPORT_LIBRARY + ) + set(single_args + PACKAGE_SPDX_ID + PACKAGE_TYPE + LICENSE_EXPRESSION + CONFIG + INSTALL_PREFIX + ) + set(multi_args + COPYRIGHTS + ) + cmake_parse_arguments(PARSE_ARGV 2 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_PACKAGE_SPDX_ID) + message(FATAL_ERROR "PACKAGE_SPDX_ID must be set") + endif() + + set(file_common_options "") + + if(arg_COPYRIGHTS) + list(JOIN arg_COPYRIGHTS "\n" copyrights) + list(APPEND file_common_options COPYRIGHT "${copyrights}") + endif() + + if(arg_LICENSE_EXPRESSION) + list(APPEND file_common_options LICENSE "${arg_LICENSE_EXPRESSION}") + endif() + + if(arg_INSTALL_PREFIX) + list(APPEND file_common_options INSTALL_PREFIX "${arg_INSTALL_PREFIX}") + endif() + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(configs ${CMAKE_CONFIGURATION_TYPES}) + else() + set(configs "${CMAKE_BUILD_TYPE}") + endif() + + if(is_multi_config) + set(spdx_id_suffix "${arg_CONFIG}") + set(config_to_install_option CONFIG ${arg_CONFIG}) + else() + set(spdx_id_suffix "") + set(config_to_install_option "") + endif() + + set(file_infix "") + if(arg_IMPORT_LIBRARY) + set(file_infix "-ImportLibrary") + endif() + + # We kind of have to add the package infix into the file spdx, otherwise we get file system + # collisions for cases like the qml tool and Qml library, and apparently cmake's file(GENERATE) + # is case insensitive for file names. + _qt_internal_sbom_get_package_infix("${arg_PACKAGE_TYPE}" package_infix) + + _qt_internal_sbom_get_file_spdx_id( + "${package_infix}-${target}-${file_infix}-${spdx_id_suffix}" spdx_id) + + set(optional "") + if(arg_OPTIONAL) + set(optional OPTIONAL) + endif() + + # Add relationship from owning package. + set(relationships "${arg_PACKAGE_SPDX_ID} CONTAINS ${spdx_id}") + + # Add source file relationships from which the binary file was generated. + _qt_internal_sbom_add_source_files("${target}" "${spdx_id}" source_relationships) + if(source_relationships) + list(APPEND relationships "${source_relationships}") + endif() + + set(glue "\nRelationship: ") + # Replace semicolon with $ to avoid errors when passing into sbom_add. + string(REPLACE ";" "$" relationships "${relationships}") + + # Glue the relationships at generation time, because there some source file relationships + # will be conditional on genexes, and evaluate to an empty value, and we want to discard + # such relationships. + set(relationships "$") + set(relationship_option RELATIONSHIP "${relationships}") + + # Add the actual binary file to the latest package. + _qt_internal_sbom_generate_add_file( + FILENAME "${file_path}" + FILETYPE BINARY ${optional} + SPDXID "${spdx_id}" + ${file_common_options} + ${config_to_install_option} + ${relationship_option} + ) +endfunction() + +# Adds source file "generated from" relationship comments to the sbom for a given target. +function(_qt_internal_sbom_add_source_files target spdx_id out_relationships) + get_target_property(sources ${target} SOURCES) + list(REMOVE_DUPLICATES sources) + + set(relationships "") + + foreach(source IN LISTS sources) + # Filter out $. + if(source MATCHES "^\\$$") + continue() + endif() + + # Filter out prl files. + if(source MATCHES "\.prl$") + continue() + endif() + + set(source_entry +"${spdx_id} GENERATED_FROM NOASSERTION\nRelationshipComment: ${CMAKE_CURRENT_SOURCE_DIR}/${source}" + ) + set(source_non_empty "$") + # Some sources are conditional on genexes, so we evaluate them. + set(relationship "$<${source_non_empty}:$>") + list(APPEND relationships "${relationship}") + endforeach() + + set(${out_relationships} "${relationships}" PARENT_SCOPE) +endfunction() + +# Adds a license id and its text to the sbom. +function(_qt_internal_sbom_add_license) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + set(opt_args + NO_LICENSE_REF_PREFIX + ) + set(single_args + LICENSE_ID + LICENSE_PATH + EXTRACTED_TEXT + ) + 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_LICENSE_ID) + message(FATAL_ERROR "LICENSE_ID must be set") + endif() + + if(NOT arg_TEXT AND NOT arg_LICENSE_PATH) + message(FATAL_ERROR "Either TEXT or LICENSE_PATH must be set") + endif() + + # Sanitize the content a bit. + if(arg_TEXT) + set(text "${arg_TEXT}") + string(REPLACE ";" "$" text "${text}") + string(REPLACE "\"" "\\\"" text "${text}") + else() + file(READ "${arg_LICENSE_PATH}" text) + string(REPLACE ";" "$" text "${text}") + string(REPLACE "\"" "\\\"" text "${text}") + endif() + + set(license_id "${arg_LICENSE_ID}") + if(NOT arg_NO_LICENSE_REF_PREFIX) + set(license_id "LicenseRef-${license_id}") + endif() + + _qt_internal_sbom_generate_add_license( + LICENSE_ID "${license_id}" + EXTRACTED_TEXT "${text}" + ) +endfunction() + +# Records information about a system library target, usually due to a qt_find_package call. +# This information is later used to generate packages for the system libraries, but only after +# confirming that the library was used (linked) into any of the Qt targets. +function(_qt_internal_sbom_record_system_library_usage target) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + set(opt_args "") + set(single_args + TYPE + PACKAGE_VERSION + FRIENDLY_PACKAGE_NAME + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_TYPE) + message(FATAL_ERROR "TYPE must be set") + endif() + + # A package might be looked up more than once, make sure to record it once. + get_property(already_recorded GLOBAL PROPERTY + _qt_internal_sbom_recorded_system_library_target_${target}) + + if(already_recorded) + return() + endif() + + set_property(GLOBAL PROPERTY + _qt_internal_sbom_recorded_system_library_target_${target} TRUE) + + # Defer spdx id creation until _qt_internal_sbom_begin_project is called, so we know the + # project name. The project name is used in the package infix generation of the system library, + # but _qt_internal_sbom_record_system_library_usage might be called before sbom generation + # has started, e.g. during _qt_internal_find_third_party_dependencies. + set(spdx_options + ${target} + TYPE "${arg_TYPE}" + PACKAGE_NAME "${arg_FRIENDLY_PACKAGE_NAME}" + ) + + get_cmake_property(sbom_repo_begin_called _qt_internal_sbom_repo_begin_called) + if(sbom_repo_begin_called) + _qt_internal_sbom_record_system_library_spdx_id(${target} ${spdx_options}) + else() + set_property(GLOBAL PROPERTY + _qt_internal_sbom_recorded_system_library_spdx_options_${target} "${spdx_options}") + endif() + + # Defer sbom info creation until we detect usage of the system library (whether the library is + # linked into any other target). + set_property(GLOBAL APPEND PROPERTY + _qt_internal_sbom_recorded_system_library_targets "${target}") + set_property(GLOBAL PROPERTY + _qt_internal_sbom_recorded_system_library_options_${target} "${ARGN}") +endfunction() + +# Helper to record spdx ids of all system library targets that were found so far. +function(_qt_internal_sbom_record_system_library_spdx_ids) + get_property(recorded_targets GLOBAL PROPERTY _qt_internal_sbom_recorded_system_library_targets) + + if(NOT recorded_targets) + return() + endif() + + foreach(target IN LISTS recorded_targets) + get_property(args GLOBAL PROPERTY + _qt_internal_sbom_recorded_system_library_spdx_options_${target}) + _qt_internal_sbom_record_system_library_spdx_id(${target} ${args}) + endforeach() +endfunction() + +# Helper to record the spdx id of a system library target. +function(_qt_internal_sbom_record_system_library_spdx_id target) + # Save the spdx id before the sbom info is added, so we can refer to it in relationships. + _qt_internal_sbom_record_target_spdx_id(${ARGN} OUT_VAR package_spdx_id) + + if(NOT package_spdx_id) + message(FATAL_ERROR "Could not generate spdx id for system library target: ${target}") + endif() + + set_property(GLOBAL PROPERTY + _qt_internal_sbom_recorded_system_library_package_${target} "${package_spdx_id}") +endfunction() + +# Goes through the list of consumed system libraries (those that were linked in) and creates +# sbom packages for them. +# Uses information from recorded system libraries (calls to qt_find_package). +function(_qt_internal_sbom_add_recorded_system_libraries) + get_property(recorded_targets GLOBAL PROPERTY _qt_internal_sbom_recorded_system_library_targets) + get_property(consumed_targets GLOBAL PROPERTY _qt_internal_sbom_consumed_system_library_targets) + + set(unconsumed_targets "${recorded_targets}") + set(generated_package_names "") + + foreach(target IN LISTS consumed_targets) + get_property(args GLOBAL PROPERTY + _qt_internal_sbom_recorded_system_library_options_${target}) + get_property(package_name GLOBAL PROPERTY + _qt_internal_sbom_recorded_system_library_package_${target}) + + set_property(GLOBAL PROPERTY _qt_internal_sbom_recorded_system_library_target_${target} "") + set_property(GLOBAL PROPERTY _qt_internal_sbom_recorded_system_library_options_${target} "") + set_property(GLOBAL PROPERTY _qt_internal_sbom_recorded_system_library_package_${target} "") + + # Guard against generating a package multiple times. Can happen when multiple targets belong + # to the same package. + if(sbom_generated_${package_name}) + continue() + endif() + + list(APPEND generated_package_names "${package_name}") + set(sbom_generated_${package_name} TRUE) + + _qt_internal_extend_sbom(${target} ${args}) + _qt_internal_finalize_sbom(${target}) + + list(REMOVE_ITEM unconsumed_targets "${target}") + endforeach() + + message(DEBUG "System libraries that were recorded, but not consumed: ${unconsumed_targets}") + message(DEBUG "Generated SBOMs for the following system packages: ${generated_package_names}") + + # Clean up, before configuring next repo project. + set_property(GLOBAL PROPERTY _qt_internal_sbom_consumed_system_library_targets "") + set_property(GLOBAL PROPERTY _qt_internal_sbom_recorded_system_library_targets "") +endfunction() + +# Helper to add sbom information for a possibly non-existing target. +# This will defer the actual sbom generation until the end of the directory scope, unless +# immediate finalization was requested. +function(_qt_internal_add_sbom target) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + set(opt_args + IMMEDIATE_FINALIZATION + ) + set(single_args + TYPE + FRIENDLY_PACKAGE_NAME + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + # No validation on purpose, the other options will be validated later. + + set(forward_args ${ARGN}) + + # Remove the IMMEDIATE_FINALIZATION from the forwarded args. + list(REMOVE_ITEM forward_args IMMEDIATE_FINALIZATION) + + # If a target doesn't exist we create it. + if(NOT TARGET "${target}") + _qt_internal_create_sbom_target("${target}" ${forward_args}) + endif() + + # Save the passed options. + _qt_internal_extend_sbom("${target}" ${forward_args}) + + # Defer finalization. In case it was already deferred, it will be a no-op. + # Some targets need immediate finalization, like the PlatformInternal ones, because otherwise + # they would be finalized after the sbom was already generated. + set(immediate_finalization "") + if(arg_IMMEDIATE_FINALIZATION) + set(immediate_finalization IMMEDIATE_FINALIZATION) + endif() + _qt_internal_defer_sbom_finalization("${target}" ${immediate_finalization}) +endfunction() + +# Helper to add custom sbom information for some kind of dependency that is not backed by an +# existing target. +# Useful for cases like 3rd party dependencies not represented by an already existing imported +# target, or for 3rd party sources that get compiled into a regular Qt target (PCRE sources compiled +# into Bootstrap). +function(_qt_internal_create_sbom_target target) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + set(opt_args "") + set(single_args + TYPE + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + # No validation on purpose, the other options will be validated later. + + if(TARGET "${target}") + message(FATAL_ERROR "The target ${target} already exists.") + endif() + + add_library("${target}" INTERFACE IMPORTED) + set_target_properties(${target} PROPERTIES + _qt_sbom_is_custom_sbom_target "TRUE" + IMPORTED_GLOBAL TRUE + ) + + if(NOT arg_TYPE) + message(FATAL_ERROR "No SBOM TYPE option was provided for target: ${target}") + endif() +endfunction() + +# Helper to add additional sbom information for an existing target. +# Just appends the options to the target's sbom args property, which will will be evaluated +# during finalization. +function(_qt_internal_extend_sbom target) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + if(NOT TARGET "${target}") + message(FATAL_ERROR + "The target ${target} does not exist, use qt_internal_add_sbom to create " + "a target first, or call the function on any other exsiting target.") + endif() + + set(opt_args "") + set(single_args + TYPE + FRIENDLY_PACKAGE_NAME + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + # No validation on purpose, the other options will be validated later. + + # Make sure a spdx id is recorded for the target right now, so it is "known" when handling + # relationships for other targets, even if the target was not yet finalized. + if(arg_TYPE) + # Friendly package name is allowed to be empty. + _qt_internal_sbom_record_target_spdx_id(${target} + TYPE "${arg_TYPE}" + PACKAGE_NAME "${arg_FRIENDLY_PACKAGE_NAME}" + ) + endif() + + set_property(TARGET ${target} APPEND PROPERTY _qt_finalize_sbom_args "${ARGN}") +endfunction() + +# Helper to add additional sbom information to targets created by qt_find_package. +# If the package was not found, and the targets were not created, the functions does nothing. +# This is similar to _qt_internal_extend_sbom, but is explicit in the fact that the targets might +# not exist. +function(_qt_find_package_extend_sbom) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + _qt_internal_get_sbom_add_target_options(sbom_opt_args sbom_single_args sbom_multi_args) + + set(opt_args + ${sbom_opt_args} + ) + set(single_args + ${sbom_single_args} + ) + set(multi_args + TARGETS + ${sbom_multi_args} + ) + + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + # Make sure not to forward TARGETS. + set(sbom_args "") + _qt_internal_forward_function_args( + FORWARD_APPEND + FORWARD_PREFIX arg + FORWARD_OUT_VAR sbom_args + FORWARD_OPTIONS + ${sbom_opt_args} + FORWARD_SINGLE + ${sbom_single_args} + FORWARD_MULTI + ${sbom_multi_args} + ) + + foreach(target IN LISTS arg_TARGETS) + if(TARGET "${target}") + _qt_internal_extend_sbom("${target}" ${sbom_args}) + else() + message(DEBUG "The target ${target} does not exist, skipping extending the sbom info.") + endif() + endforeach() +endfunction() + +# Helper to defer adding sbom information for a target, at the end of the directory scope. +function(_qt_internal_defer_sbom_finalization target) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + set(opt_args + IMMEDIATE_FINALIZATION + ) + set(single_args "") + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + get_target_property(sbom_finalization_requested ${target} _qt_sbom_finalization_requested) + if(sbom_finalization_requested) + # Already requested, nothing to do. + return() + endif() + set_target_properties(${target} PROPERTIES _qt_sbom_finalization_requested TRUE) + + _qt_internal_append_to_cmake_property_without_duplicates( + _qt_internal_sbom_targets_waiting_for_finalization + "${target}" + ) + + set(func "_qt_internal_finalize_sbom") + + if(arg_IMMEDIATE_FINALIZATION) + _qt_internal_finalize_sbom(${target}) + elseif(QT_BUILDING_QT) + qt_add_list_file_finalizer("${func}" "${target}") + elseif(CMAKE_VERSION VERSION_GREATER_EQUAL "3.19") + cmake_language(EVAL CODE "cmake_language(DEFER CALL \"${func}\" \"${target}\")") + else() + message(FATAL_ERROR "Defer adding a sbom target requires CMake version 3.19") + endif() +endfunction() + +# Finalizer to add sbom information for the target. +# Expects the target to exist. +function(_qt_internal_finalize_sbom target) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + get_target_property(sbom_finalization_done ${target} _qt_sbom_finalization_done) + if(sbom_finalization_done) + # Already done, nothing to do. + return() + endif() + set_target_properties(${target} PROPERTIES _qt_sbom_finalization_done TRUE) + + get_target_property(sbom_args ${target} _qt_finalize_sbom_args) + if(NOT sbom_args) + set(sbom_args "") + endif() + _qt_internal_sbom_add_target(${target} ${sbom_args}) +endfunction() + +# Extends the list of targets that are considered dependencies for target. +function(_qt_internal_extend_sbom_dependencies target) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + set(opt_args "") + set(single_args "") + set(multi_args + SBOM_DEPENDENCIES + ) + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT TARGET "${target}") + message(FATAL_ERROR "The target ${target} does not exist.") + endif() + + _qt_internal_append_to_target_property_without_duplicates(${target} + _qt_extend_target_sbom_dependencies "${arg_SBOM_DEPENDENCIES}" + ) +endfunction() + +# Handles attribution information for a target. +# +# If CREATE_SBOM_FOR_EACH_ATTRIBUTION is set, a separate sbom target is created for each parsed +# attribution entry, and the new targets are added as dependencies to the parent target. +# +# If CREATE_SBOM_FOR_EACH_ATTRIBUTION is not set, the information read from the first attribution +# entry is added directly to the parent target, aka the the values are propagated to the outer +# function scope to be read.. The rest of the attribution entries are created as separate targets +# and added as dependencies, as if the option was passed. +# +# Handles multiple attribution files and entries within a file. +# Attribution files can be specified either via directories and direct file paths. +# If ATTRIBUTION_ENTRY_INDEX is set, only that specific attribution entry will be processed +# from the given attribution file. +function(_qt_internal_sbom_handle_qt_attribution_files out_prefix_outer) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + if(CMAKE_VERSION LESS_EQUAL "3.19") + message(DEBUG "CMake version is too low, can't parse attribution.json file.") + return() + endif() + + set(opt_args + CREATE_SBOM_FOR_EACH_ATTRIBUTION + ) + set(single_args + PARENT_TARGET + ATTRIBUTION_ENTRY_INDEX + ) + set(multi_args + ATTRIBUTION_FILE_PATHS + ATTRIBUTION_FILE_DIR_PATHS + ) + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set(attribution_files "") + set(attribution_file_count 0) + + foreach(attribution_file_path IN LISTS arg_ATTRIBUTION_FILE_PATHS) + get_filename_component(real_path "${attribution_file_path}" REALPATH) + list(APPEND attribution_files "${real_path}") + math(EXPR attribution_file_count "${attribution_file_count} + 1") + endforeach() + + foreach(attribution_file_dir_path IN LISTS arg_ATTRIBUTION_FILE_DIR_PATHS) + get_filename_component(real_path + "${attribution_file_dir_path}/qt_attribution.json" REALPATH) + list(APPEND attribution_files "${real_path}") + math(EXPR attribution_file_count "${attribution_file_count} + 1") + endforeach() + + if(arg_CREATE_SBOM_FOR_EACH_ATTRIBUTION) + if(NOT arg_PARENT_TARGET) + message(FATAL_ERROR "PARENT_TARGET must be set") + endif() + endif() + + if(arg_ATTRIBUTION_ENTRY_INDEX AND attribution_file_count GREATER 1) + message(FATAL_ERROR + "ATTRIBUTION_ENTRY_INDEX should only be set if a single attribution " + "file is specified." + ) + endif() + + set(file_index 0) + set(first_attribution_processed FALSE) + foreach(attribution_file_path IN LISTS attribution_files) + # Set a unique out_prefix that will not overlap when multiple entries are processed. + set(out_prefix_file "${out_prefix_outer}_${file_index}") + + # Get the number of entries in the attribution file. + _qt_internal_sbom_read_qt_attribution(${out_prefix_file} + GET_ATTRIBUTION_ENTRY_COUNT + OUT_VAR_VALUE attribution_entry_count + FILE_PATH "${attribution_file_path}" + ) + + # If a specific entry was specified, we will only process it from the file. + if(NOT "${arg_ATTRIBUTION_ENTRY_INDEX}" STREQUAL "") + set(entry_index ${arg_ATTRIBUTION_ENTRY_INDEX}) + else() + set(entry_index 0) + endif() + + # Go through each entry in the attribution file. + while("${entry_index}" LESS "${${out_prefix_file}_attribution_entry_count}") + # If this is the first entry to be processed, or if CREATE_SBOM_FOR_EACH_ATTRIBUTION + # is not set, we read the attribution file entry directly, and propagate the values + # to the parent scope. + if(NOT first_attribution_processed AND NOT arg_CREATE_SBOM_FOR_EACH_ATTRIBUTION) + # Set a prefix without indices, so that the parent scope add_sbom call can + # refer to the values directly with the outer prefix, without any index infix. + set(out_prefix "${out_prefix_outer}") + + _qt_internal_sbom_read_qt_attribution(${out_prefix} + GET_DEFAULT_KEYS + ENTRY_INDEX "${entry_index}" + OUT_VAR_ASSIGNED_VARIABLE_NAMES variable_names + FILE_PATH "${attribution_file_path}" + ) + + # Propagate the values to the outer scope. + foreach(variable_name IN LISTS variable_names) + set(${out_prefix}_${variable_name} "${${out_prefix}_${variable_name}}" + PARENT_SCOPE) + endforeach() + + get_filename_component(relative_attribution_file_path + "${attribution_file_path}" REALPATH) + + set(${out_prefix}_chosen_attribution_file_path "${relative_attribution_file_path}" + PARENT_SCOPE) + set(${out_prefix}_chosen_attribution_entry_index "${entry_index}" + PARENT_SCOPE) + + set(first_attribution_processed TRUE) + if(NOT "${arg_ATTRIBUTION_ENTRY_INDEX}" STREQUAL "") + # We had a specific index to process, so break right after processing it. + break() + endif() + else() + # We are processing the second or later entry, or CREATE_SBOM_FOR_EACH_ATTRIBUTION + # was set. Instead of directly reading all the keys from the attribution file, + # we get the Id, and create a new sbom target for the entry. + # That will recursively call this function with a specific attribution file path + # and index, to process the specific entry. + + set(out_prefix "${out_prefix_outer}_${file_index}_${entry_index}") + + # Get the attribution id. + _qt_internal_sbom_read_qt_attribution(${out_prefix} + GET_KEY + KEY Id + OUT_VAR_VALUE attribution_id + ENTRY_INDEX "${entry_index}" + FILE_PATH "${attribution_file_path}" + ) + + # If no Id was retrieved, just add a numeric one, to make the sbom target + # unique. + set(attribution_target "${arg_PARENT_TARGET}_Attribution_") + if(NOT ${out_prefix}_attribution_id) + string(APPEND attribution_target "${file_index}_${entry_index}") + else() + string(APPEND attribution_target "${${out_prefix}_attribution_id}") + endif() + + # Create another sbom target with the id as a hint for the target name, + # the attribution file passed, and make the new target a dependency of the + # parent one. + _qt_internal_add_sbom("${attribution_target}" + IMMEDIATE_FINALIZATION + TYPE QT_THIRD_PARTY_SOURCES + ATTRIBUTION_FILE_PATHS "${attribution_file_path}" + ATTRIBUTION_ENTRY_INDEX "${entry_index}" + NO_CURRENT_DIR_ATTRIBUTION + ) + + _qt_internal_extend_sbom_dependencies(${arg_PARENT_TARGET} + SBOM_DEPENDENCIES ${attribution_target} + ) + endif() + + math(EXPR entry_index "${entry_index} + 1") + endwhile() + + math(EXPR file_index "${file_index} + 1") + endforeach() +endfunction() + +# Helper to parse a qt_attribution.json file and do various operations: +# - GET_DEFAULT_KEYS extracts the license id, copyrights, version, etc. +# - GET_KEY extracts a single given json key's value, as specified with KEY and saved into +# OUT_VAR_VALUE +# - GET_ATTRIBUTION_ENTRY_COUNT returns the number of entries in the json file, set in +# OUT_VAR_VALUE +# +# ENTRY_INDEX can be used to specify the array index to select a specific entry in the json file. +# +# Any retrieved value is set in the outer scope. +# The variables are prefixed with ${out_prefix}. +# OUT_VAR_ASSIGNED_VARIABLE_NAMES contains the list of variables set in the parent scope, the +# variables names in this list are not prefixed with ${out_prefix}. +# +# Requires cmake 3.19 for json parsing. +function(_qt_internal_sbom_read_qt_attribution out_prefix) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + if(CMAKE_VERSION LESS_EQUAL "3.19") + message(DEBUG "CMake version is too low, can't parse attribution.json file.") + return() + endif() + + set(opt_args + GET_DEFAULT_KEYS + GET_KEY + GET_ATTRIBUTION_ENTRY_COUNT + ) + set(single_args + FILE_PATH + KEY + ENTRY_INDEX + OUT_VAR_VALUE + OUT_VAR_ASSIGNED_VARIABLE_NAMES + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set(file_path "${arg_FILE_PATH}") + + if(NOT file_path) + message(FATAL_ERROR "qt attribution file path not given") + endif() + + file(READ "${file_path}" contents) + if(NOT contents) + message(FATAL_ERROR "qt attribution file is empty: ${file_path}") + endif() + + if(NOT arg_GET_DEFAULT_KEYS AND NOT arg_GET_KEY AND NOT arg_GET_ATTRIBUTION_ENTRY_COUNT) + message(FATAL_ERROR + "No valid operation specified to _qt_internal_sbom_read_qt_attribution call.") + endif() + + if(arg_GET_KEY) + if(NOT arg_KEY) + message(FATAL_ERROR "KEY must be set") + endif() + if(NOT arg_OUT_VAR_VALUE) + message(FATAL_ERROR "OUT_VAR_VALUE must be set") + endif() + endif() + + get_filename_component(attribution_file_dir "${file_path}" DIRECTORY) + + # Parse the json file. + # The first element might be an array, or an object. We need to detect which one. + # Do that by trying to query index 0 of the potential root array. + # If the index is found, that means the root is an array, and elem_error is set to NOTFOUND, + # because there was no error. + # Otherwise elem_error will be something like 'member '0' not found', and we can assume the + # root is an object. + string(JSON first_elem_type ERROR_VARIABLE elem_error TYPE "${contents}" 0) + if(elem_error STREQUAL "NOTFOUND") + # Root is an array. The attribution file might contain multiple entries. + # Pick the first one if no specific index was specified, otherwise use the given index. + if(NOT "${arg_ENTRY_INDEX}" STREQUAL "") + set(indices "${arg_ENTRY_INDEX}") + else() + set(indices "0") + endif() + set(is_array TRUE) + else() + # Root is an object, not an array, which means the file has a single entry. + set(indices "") + set(is_array FALSE) + endif() + + set(variable_names "") + + if(arg_GET_KEY) + _qt_internal_sbom_get_attribution_key(${arg_KEY} ${arg_OUT_VAR_VALUE} ${out_prefix}) + endif() + + if(arg_GET_ATTRIBUTION_ENTRY_COUNT) + if(NOT arg_OUT_VAR_VALUE) + message(FATAL_ERROR "OUT_VAR_VALUE must be set") + endif() + + if(is_array) + string(JSON attribution_entry_count ERROR_VARIABLE elem_error LENGTH "${contents}") + # There was an error getting the length of the array, so we assume it's empty. + if(NOT elem_error STREQUAL "NOTFOUND") + set(attribution_entry_count 0) + endif() + else() + set(attribution_entry_count 1) + endif() + + set(${out_prefix}_${arg_OUT_VAR_VALUE} "${attribution_entry_count}" PARENT_SCOPE) + endif() + + if(arg_GET_DEFAULT_KEYS) + # Some calls are currently commented out, to save on json parsing time because we don't have + # a usage for them yet. + # _qt_internal_sbom_get_attribution_key(License license) + _qt_internal_sbom_get_attribution_key(LicenseId license_id "${out_prefix}") + _qt_internal_sbom_get_attribution_key(Version version "${out_prefix}") + _qt_internal_sbom_get_attribution_key(Homepage homepage "${out_prefix}") + _qt_internal_sbom_get_attribution_key(Name attribution_name "${out_prefix}") + _qt_internal_sbom_get_attribution_key(Description description "${out_prefix}") + _qt_internal_sbom_get_attribution_key(QtUsage qt_usage "${out_prefix}") + _qt_internal_sbom_get_attribution_key(DownloadLocation download_location "${out_prefix}") + _qt_internal_sbom_get_attribution_key(Copyright copyrights "${out_prefix}") + _qt_internal_sbom_get_attribution_key(CopyrightFile copyright_file "${out_prefix}") + + # In some attribution files (like harfbuzz) Copyright contains an array of copyrights rather + # than a single string. Extract all of them. + if(copyrights) + string(JSON copyright_type TYPE "${contents}" ${indices} Copyright) + if(copyright_type STREQUAL "ARRAY") + set(copyright_json_array "${copyrights}") + string(JSON array_len LENGTH "${copyright_json_array}") + + set(copyright_list "") + set(index 0) + while(index LESS array_len) + string(JSON copyright GET "${copyright_json_array}" ${index}) + if(copyright) + list(APPEND copyright_list "${copyright}") + endif() + math(EXPR index "${index} + 1") + endwhile() + + if(copyright_list) + set(${out_prefix}_copyrights "${copyright_list}" PARENT_SCOPE) + list(APPEND variable_names "copyrights") + endif() + endif() + endif() + + # Some attribution files contain a copyright file that contains the actual list of + # copyrights. Read it and use it. + set(copyright_file_path "${attribution_file_dir}/${copyright_file}") + get_filename_component(copyright_file_path "${copyright_file_path}" REALPATH) + if(NOT copyrights AND copyright_file AND EXISTS "${copyright_file_path}") + file(READ "${copyright_file_path}" copyright_contents) + if(copyright_contents) + set(copyright_contents "${copyright_contents}") + set(copyrights "${copyright_contents}") + set(${out_prefix}_copyrights "${copyright_contents}" PARENT_SCOPE) + list(APPEND variable_names "copyrights") + endif() + endif() + endif() + + if(arg_OUT_VAR_ASSIGNED_VARIABLE_NAMES) + set(${arg_OUT_VAR_ASSIGNED_VARIABLE_NAMES} "${variable_names}" PARENT_SCOPE) + endif() +endfunction() + +# Escapes various characters in json content, so that the generate cmake code to append the content +# to the spdx document is syntactically valid. +function(_qt_internal_sbom_escape_json_content content out_var) + # Escape backslashes + string(REPLACE "\\" "\\\\" escaped_content "${content}") + + # Escape quotes + string(REPLACE "\"" "\\\"" escaped_content "${escaped_content}") + + set(${out_var} "${escaped_content}" PARENT_SCOPE) +endfunction() + +# Reads a json key from a qt_attribution.json file, and assigns it to out_var. +# Also adds the out_var to the parent scope ${variable_names}. +# Expects contents, indices and out_prefix to be set in parent scope. +macro(_qt_internal_sbom_get_attribution_key json_key out_var out_prefix) + string(JSON "${out_var}" ERROR_VARIABLE get_error GET "${contents}" ${indices} "${json_key}") + if(NOT "${${out_var}}" STREQUAL "" AND NOT get_error) + _qt_internal_sbom_escape_json_content("${${out_var}}" escaped_content) + set(${out_prefix}_${out_var} "${escaped_content}" PARENT_SCOPE) + list(APPEND variable_names "${out_var}") + endif() +endmacro() + +# Set sbom project name for the root project. +function(_qt_internal_sbom_set_root_project_name project_name) + set_property(GLOBAL PROPERTY _qt_internal_sbom_repo_project_name "${project_name}") +endfunction() + +# Get repo project_name spdx id reference, needs to start with Package- to be NTIA compliant. +function(_qt_internal_sbom_get_root_project_name_for_spdx_id out_var) + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + set(sbom_repo_project_name "Package-${repo_project_name_lowercase}") + set(${out_var} "${sbom_repo_project_name}" PARENT_SCOPE) +endfunction() + +# Just a lower case sbom project name. +function(_qt_internal_sbom_get_root_project_name_lower_case out_var) + get_cmake_property(project_name _qt_internal_sbom_repo_project_name) + + if(NOT project_name) + message(FATAL_ERROR "No SBOM project name was set.") + endif() + + string(TOLOWER "${project_name}" repo_project_name_lowercase) + set(${out_var} "${repo_project_name_lowercase}" PARENT_SCOPE) +endfunction() + +# Get a spdx id to reference an external document. +function(_qt_internal_sbom_get_external_document_ref_spdx_id repo_name out_var) + set(${out_var} "DocumentRef-${repo_name}" PARENT_SCOPE) +endfunction() + +# Sanitize a given value to be used as a SPDX id. +function(_qt_internal_sbom_get_sanitized_spdx_id out_var hint) + # Only allow alphanumeric characters and dashes. + string(REGEX REPLACE "[^a-zA-Z0-9]+" "-" spdx_id "${hint}") + + # Remove all trailing dashes. + string(REGEX REPLACE "-+$" "" spdx_id "${spdx_id}") + + set(${out_var} "${spdx_id}" PARENT_SCOPE) +endfunction() + +# Generates a spdx id for a target and saves it its properties. +function(_qt_internal_sbom_record_target_spdx_id target) + set(opt_args "") + set(single_args + PACKAGE_NAME + TYPE + OUT_VAR + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + _qt_internal_sbom_get_spdx_id_for_target("${target}" spdx_id) + + if(spdx_id) + # Return early if the target was already recorded and has a spdx id. + if(arg_OUT_VAR) + set(${arg_OUT_VAR} "${spdx_id}" PARENT_SCOPE) + endif() + return() + endif() + + if(arg_PACKAGE_NAME) + set(package_name_for_spdx_id "${arg_PACKAGE_NAME}") + else() + set(package_name_for_spdx_id "${target}") + endif() + + _qt_internal_sbom_generate_target_package_spdx_id(package_spdx_id + TYPE "${arg_TYPE}" + PACKAGE_NAME "${package_name_for_spdx_id}" + ) + _qt_internal_sbom_save_spdx_id_for_target("${target}" "${package_spdx_id}") + + _qt_internal_sbom_is_qt_entity_type("${arg_TYPE}" is_qt_entity_type) + _qt_internal_sbom_save_spdx_id_for_qt_entity_type( + "${target}" "${is_qt_entity_type}" "${package_spdx_id}") + + if(arg_OUT_VAR) + set(${arg_OUT_VAR} "${package_spdx_id}" PARENT_SCOPE) + endif() +endfunction() + +# Generates a sanitized spdx id for a target (package) of a specific type. +function(_qt_internal_sbom_generate_target_package_spdx_id out_var) + set(opt_args "") + set(single_args + PACKAGE_NAME + TYPE + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_PACKAGE_NAME) + message(FATAL_ERROR "PACKAGE_NAME must be set") + endif() + if(NOT arg_TYPE) + message(FATAL_ERROR "TYPE must be set") + endif() + + _qt_internal_sbom_get_root_project_name_for_spdx_id(repo_project_name_spdx_id) + _qt_internal_sbom_get_package_infix("${arg_TYPE}" package_infix) + + _qt_internal_sbom_get_sanitized_spdx_id(spdx_id + "SPDXRef-${repo_project_name_spdx_id}-${package_infix}-${arg_PACKAGE_NAME}") + + set(${out_var} "${spdx_id}" PARENT_SCOPE) +endfunction() + +# Save a spdx id for a target inside its target properties. +# Also saves the repo document namespace and relative installed repo document path. +# These are used when generating a SPDX external document reference for exported targets, to +# include them in relationships. +function(_qt_internal_sbom_save_spdx_id_for_target target spdx_id) + message(DEBUG "Saving spdx id for target ${target}: ${spdx_id}") + + set(target_unaliased "${target}") + get_target_property(aliased_target "${target}" ALIASED_TARGET) + if(aliased_target) + set(target_unaliased ${aliased_target}) + endif() + + set_target_properties(${target_unaliased} PROPERTIES + _qt_sbom_spdx_id "${spdx_id}") + + # Retrieve repo specific properties. + get_property(repo_document_namespace + GLOBAL PROPERTY _qt_internal_sbom_repo_document_namespace) + + get_property(relative_installed_repo_document_path + GLOBAL PROPERTY _qt_internal_sbom_relative_installed_repo_document_path) + + get_property(project_name_lowercase + GLOBAL PROPERTY _qt_internal_sbom_repo_project_name_lowercase) + + # And save them on the target. + set_property(TARGET ${target_unaliased} PROPERTY + _qt_sbom_spdx_repo_document_namespace + "${repo_document_namespace}") + + set_property(TARGET ${target_unaliased} PROPERTY + _qt_sbom_spdx_relative_installed_repo_document_path + "${relative_installed_repo_document_path}") + + set_property(TARGET ${target_unaliased} PROPERTY + _qt_sbom_spdx_repo_project_name_lowercase + "${project_name_lowercase}") + + # Export the properties, so they can be queried by other repos. + # We also do it for versionless targets. + set(export_properties + _qt_sbom_spdx_id + _qt_sbom_spdx_repo_document_namespace + _qt_sbom_spdx_relative_installed_repo_document_path + _qt_sbom_spdx_repo_project_name_lowercase + ) + set_property(TARGET "${target_unaliased}" APPEND PROPERTY + EXPORT_PROPERTIES "${export_properties}") +endfunction() + +# Returns whether the given sbom type is considered to be a Qt type like a module or a tool. +function(_qt_internal_sbom_is_qt_entity_type sbom_type out_var) + set(qt_entity_types + QT_MODULE + QT_PLUGIN + QT_APP + QT_TOOL + ) + + set(is_qt_entity_type FALSE) + if(sbom_type IN_LIST qt_entity_types) + set(is_qt_entity_type TRUE) + endif() + + set(${out_var} ${is_qt_entity_type} PARENT_SCOPE) +endfunction() + +# Save a spdx id for all known related target names of a given Qt target. +# Related being the namespaced and versionless variants of a Qt target. +# All the related targets will contain the same spdx id. +# So Core, CorePrivate, Qt6::Core, Qt6::CorePrivate, Qt::Core, Qt::CorePrivate will all be +# referred to by the same spdx id. +function(_qt_internal_sbom_save_spdx_id_for_qt_entity_type target is_qt_entity_type package_spdx_id) + # Assign the spdx id to all known related target names of given the given Qt target. + set(target_names "") + + if(is_qt_entity_type) + set(namespaced_target "${QT_CMAKE_EXPORT_NAMESPACE}::${target}") + set(namespaced_private_target "${QT_CMAKE_EXPORT_NAMESPACE}::${target}Private") + set(versionless_target "Qt::${target}") + set(versionless_private_target "Qt::${target}Private") + + list(APPEND target_names + namespaced_target + namespaced_private_target + versionless_target + versionless_private_target + ) + endif() + + foreach(target_name IN LISTS ${target_names}) + if(TARGET "${target_name}") + _qt_internal_sbom_save_spdx_id_for_target("${target_name}" "${package_spdx_id}") + endif() + endforeach() +endfunction() + +# Retrieves a saved spdx id from the target. Might be empty. +function(_qt_internal_sbom_get_spdx_id_for_target target out_var) + get_target_property(spdx_id ${target} _qt_sbom_spdx_id) + set(${out_var} "${spdx_id}" PARENT_SCOPE) +endfunction() + +# Get a sanitized spdx id for a file. +# For consistency, we prefix the id with SPDXRef-PackagedFile-. This is not a requirement. +function(_qt_internal_sbom_get_file_spdx_id target out_var) + _qt_internal_sbom_get_sanitized_spdx_id(spdx_id "SPDXRef-PackagedFile-${target}") + set(${out_var} "${spdx_id}" PARENT_SCOPE) +endfunction() + +# Returns a package infix for a given target sbom type to be used in spdx package id generation. +function(_qt_internal_sbom_get_package_infix type out_infix) + if(type STREQUAL "QT_MODULE") + set(package_infix "qt-module") + elseif(type STREQUAL "QT_PLUGIN") + set(package_infix "qt-plugin") + elseif(type STREQUAL "QML_PLUGIN") + set(package_infix "qt-qml-plugin") # not used at the moment + elseif(type STREQUAL "QT_TOOL") + set(package_infix "qt-tool") + elseif(type STREQUAL "QT_APP") + set(package_infix "qt-app") + elseif(type STREQUAL "QT_THIRD_PARTY_MODULE") + set(package_infix "qt-bundled-3rdparty-module") + elseif(type STREQUAL "QT_THIRD_PARTY_SOURCES") + set(package_infix "qt-3rdparty-sources") + elseif(type STREQUAL "SYSTEM_LIBRARY") + set(package_infix "system-3rdparty") + elseif(type STREQUAL "EXECUTABLE") + set(package_infix "executable") + elseif(type STREQUAL "LIBRARY") + set(package_infix "library") + elseif(type STREQUAL "THIRD_PARTY_LIBRARY") + set(package_infix "3rdparty-library") + elseif(type STREQUAL "THIRD_PARTY_LIBRARY_WITH_FILES") + set(package_infix "3rdparty-library-with-files") + else() + message(DEBUG "No package infix due to unknown type: ${type}") + set(package_infix "") + endif() + set(${out_infix} "${package_infix}" PARENT_SCOPE) +endfunction() + +# Returns a package purpose for a given target sbom type. +function(_qt_internal_sbom_get_package_purpose type out_purpose) + if(type STREQUAL "QT_MODULE") + set(package_purpose "LIBRARY") + elseif(type STREQUAL "QT_PLUGIN") + set(package_purpose "LIBRARY") + elseif(type STREQUAL "QML_PLUGIN") + set(package_purpose "LIBRARY") + elseif(type STREQUAL "QT_TOOL") + set(package_purpose "APPLICATION") + elseif(type STREQUAL "QT_APP") + set(package_purpose "APPLICATION") + elseif(type STREQUAL "QT_THIRD_PARTY_MODULE") + set(package_purpose "LIBRARY") + elseif(type STREQUAL "QT_THIRD_PARTY_SOURCES") + set(package_purpose "LIBRARY") + elseif(type STREQUAL "SYSTEM_LIBRARY") + set(package_purpose "LIBRARY") + elseif(type STREQUAL "EXECUTABLE") + set(package_purpose "APPLICATION") + elseif(type STREQUAL "LIBRARY") + set(package_purpose "LIBRARY") + elseif(type STREQUAL "THIRD_PARTY_LIBRARY") + set(package_purpose "LIBRARY") + elseif(type STREQUAL "THIRD_PARTY_LIBRARY_WITH_FILES") + set(package_purpose "LIBRARY") + else() + set(package_purpose "OTHER") + endif() + set(${out_purpose} "${package_purpose}" PARENT_SCOPE) +endfunction() + +# Get the default qt spdx license expression. +function(_qt_internal_sbom_get_default_qt_license_id out_var) + set(${out_var} + "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only" + PARENT_SCOPE) +endfunction() + +# Get the default qt copyright. +function(_qt_internal_sbom_get_default_qt_copyright_header out_var) + set(${out_var} + "Copyright (C) 2024 The Qt Company Ltd." + PARENT_SCOPE) +endfunction() + +# Get the default qt package version. +function(_qt_internal_sbom_get_default_qt_package_version out_var) + set(${out_var} "${QT_REPO_MODULE_VERSION}" PARENT_SCOPE) +endfunction() + +# Get the default qt supplier. +function(_qt_internal_sbom_get_default_supplier out_var) + set(${out_var} "TheQtCompany" PARENT_SCOPE) +endfunction() + +# Get the default qt supplier url. +function(_qt_internal_sbom_get_default_supplier_url out_var) + set(${out_var} "https://qt.io" PARENT_SCOPE) +endfunction() + +# Get the default qt download location. +# If git info is available, includes the hash. +function(_qt_internal_sbom_get_qt_repo_source_download_location out_var) + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + set(download_location "git://code.qt.io/qt/${repo_project_name_lowercase}.git") + if(QT_SBOM_GIT_HASH) + string(APPEND download_location "@${QT_SBOM_GIT_HASH}") + endif() + set(${out_var} "${download_location}" PARENT_SCOPE) +endfunction() + +# Computes a security CPE for a given set of attributes. +# +# When a part is not specified, a wildcard is added. +# +# References: +# https://spdx.github.io/spdx-spec/v2.3/external-repository-identifiers/#f22-cpe23type +# https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7695.pdf +# https://nvd.nist.gov/products/cpe +# +# Each attribute means: +# 1. part +# 2. vendor +# 3. product +# 4. version +# 5. update +# 6. edition +# 7. language +# 8. sw_edition +# 9. target_sw +# 10. target_hw +# 11. other +function(_qt_internal_sbom_compute_security_cpe out_cpe) + set(opt_args "") + set(single_args + PART + VENDOR + PRODUCT + VERSION + UPDATE + EDITION + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set(cpe_template "cpe:2.3:PART:VENDOR:PRODUCT:VERSION:UPDATE:EDITION:*:*:*:*:*") + + set(cpe "${cpe_template}") + foreach(attribute_name IN LISTS single_args) + if(arg_${attribute_name}) + set(${attribute_name}_value "${arg_${attribute_name}}") + else() + if(attribute_name STREQUAL "PART") + set(${attribute_name}_value "a") + else() + set(${attribute_name}_value "*") + endif() + endif() + string(REPLACE "${attribute_name}" "${${attribute_name}_value}" cpe "${cpe}") + endforeach() + + set(${out_cpe} "${cpe}" PARENT_SCOPE) +endfunction() + +# Computes the default security CPE for the Qt framework. +function(_qt_internal_sbom_get_cpe_qt out_var) + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + _qt_internal_sbom_compute_security_cpe(repo_cpe + VENDOR "qt" + PRODUCT "${repo_project_name_lowercase}" + VERSION "${QT_REPO_MODULE_VERSION}" + ) + set(${out_var} "${repo_cpe}" PARENT_SCOPE) +endfunction() + +# Computes the default security CPE for a given qt repository. +function(_qt_internal_sbom_get_cpe_qt_repo out_var) + _qt_internal_sbom_compute_security_cpe(qt_cpe + VENDOR "qt" + PRODUCT "qt" + VERSION "${QT_REPO_MODULE_VERSION}" + ) + set(${out_var} "${qt_cpe}" PARENT_SCOPE) +endfunction() + +# Computes the list of security CPEs for Qt, including both the repo-specific one and generic one. +function(_qt_internal_sbom_compute_security_cpe_for_qt out_cpe_list) + set(cpe_list "") + + _qt_internal_sbom_get_cpe_qt(repo_cpe) + list(APPEND cpe_list "${repo_cpe}") + + _qt_internal_sbom_get_cpe_qt_repo(qt_cpe) + list(APPEND cpe_list "${qt_cpe}") + + set(${out_cpe_list} "${cpe_list}" PARENT_SCOPE) +endfunction() + +# Collects app bundle related information and paths from an executable's target properties. +# Output variables: +# _name bundle base name, e.g. 'Linguist'. +# _dir_name bundle dir name, e.g. 'Linguist.app'. +# _contents_dir bundle contents dir, e.g. 'Linguist.app/Contents' +# _contents_binary_dir bundle contents dir, e.g. 'Linguist.app/Contents/MacOS' +function(_qt_internal_get_executable_bundle_info out_var target) + get_target_property(target_type ${target} TYPE) + if(NOT "${target_type}" STREQUAL "EXECUTABLE") + message(FATAL_ERROR "The target ${target} is not an executable") + endif() + + get_target_property(output_name ${target} OUTPUT_NAME) + if(NOT output_name) + set(output_name "${target}") + endif() + + set(${out_var}_name "${output_name}") + set(${out_var}_dir_name "${${out_var}_name}.app") + set(${out_var}_contents_dir "${${out_var}_dir_name}/Contents") + set(${out_var}_contents_binary_dir "${${out_var}_contents_dir}/MacOS") + + set(${out_var}_name "${${out_var}_name}" PARENT_SCOPE) + set(${out_var}_dir_name "${${out_var}_dir_name}" PARENT_SCOPE) + set(${out_var}_contents_dir "${${out_var}_contents_dir}" PARENT_SCOPE) + set(${out_var}_contents_binary_dir "${${out_var}_contents_binary_dir}" PARENT_SCOPE) +endfunction() + +# Helper function to add binary file to the sbom, while handling multi-config and different +# kind of paths. +# In multi-config builds, we assume that the non-default config file will be optional, because it +# might not be installed (the case for debug tools and apps in debug-and-release builds). +function(_qt_internal_sbom_handle_multi_config_target_binary_file target) + set(opt_args "") + set(single_args + PATH_KIND + PATH_SUFFIX + ) + set(multi_args + OPTIONS + ) + + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(configs ${CMAKE_CONFIGURATION_TYPES}) + else() + set(configs "${CMAKE_BUILD_TYPE}") + endif() + + foreach(config IN LISTS configs) + _qt_internal_sbom_get_and_check_multi_config_aware_single_arg_option( + arg "${arg_PATH_KIND}" "${config}" resolved_path) + _qt_internal_sbom_get_target_file_is_optional_in_multi_config("${config}" is_optional) + _qt_internal_path_join(file_path "${resolved_path}" "${arg_PATH_SUFFIX}") + _qt_internal_sbom_add_binary_file( + "${target}" + "${file_path}" + ${arg_OPTIONS} + ${is_optional} + CONFIG ${config} + ) + endforeach() +endfunction() + +# Helper to retrieve a list of multi-config aware option names that can be parsed by the binary +# file handling function. +# For example in single config we need to parse RUNTIME_PATH, in multi-config we need to parse +# RUNTIME_PATH_DEBUG and RUNTIME_PATH_RELEASE. +# +# Result is cached in a global property. +function(_qt_internal_sbom_get_multi_config_single_args out_var) + get_cmake_property(single_args + _qt_internal_sbom_multi_config_single_args) + + if(single_args) + set(${out_var} ${single_args} PARENT_SCOPE) + return() + endif() + + set(single_args "") + + set(single_args_to_process + INSTALL_PATH + RUNTIME_PATH + LIBRARY_PATH + ARCHIVE_PATH + FRAMEWORK_PATH + ) + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + set(configs ${CMAKE_CONFIGURATION_TYPES}) + foreach(config IN LISTS configs) + string(TOUPPER ${config} config_upper) + foreach(single_arg IN LISTS single_args_to_process) + list(APPEND single_args "${single_arg}_${config_upper}") + endforeach() + endforeach() + else() + list(APPEND single_args "${single_args_to_process}") + endif() + + set_property(GLOBAL PROPERTY + _qt_internal_sbom_multi_config_single_args "${single_args}") + set(${out_var} ${single_args} PARENT_SCOPE) +endfunction() + +# Helper to apped a an option and a value to a list of options, while being multi-config aware. +# It appends e.g. either RUNTIME_PATH foo or RUNTIME_PATH_DEBUG foo to the out_var_args variable. +function(_qt_internal_sbom_append_multi_config_aware_single_arg_option + arg_name arg_value config out_var_args) + set(values "${${out_var_args}}") + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + string(TOUPPER ${config} config_upper) + list(APPEND values "${arg_name}_${config_upper}" "${arg_value}") + else() + list(APPEND values "${arg_name}" "${arg_value}") + endif() + + set(${out_var_args} "${values}" PARENT_SCOPE) +endfunction() + +# Helper to check whether a given option was set in the outer scope, while being multi-config +# aware. +# It checks e.g. if either arg_RUNTIME_PATH or arg_RUNTIME_PATH_DEBUG is set in the outer scope. +function(_qt_internal_sbom_get_and_check_multi_config_aware_single_arg_option + arg_prefix arg_name config out_var) + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + + if(is_multi_config) + string(TOUPPER ${config} config_upper) + set(outer_scope_var_name "${arg_prefix}_${arg_name}_${config_upper}") + set(option_name "${arg_name}_${config_upper}") + else() + set(outer_scope_var_name "${arg_prefix}_${arg_name}") + set(option_name "${arg_name}") + endif() + + if(NOT DEFINED ${outer_scope_var_name}) + message(FATAL_ERROR "Missing ${option_name}") + endif() + + set(${out_var} "${${outer_scope_var_name}}" PARENT_SCOPE) +endfunction() + +# Checks if given config is not the first config in a multi-config build, and thus file installation +# for that config should be optional. +function(_qt_internal_sbom_is_config_optional_in_multi_config config out_var) + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + + if(QT_MULTI_CONFIG_FIRST_CONFIG) + set(first_config_type "${QT_MULTI_CONFIG_FIRST_CONFIG}") + elseif(CMAKE_CONFIGURATION_TYPES) + list(GET CMAKE_CONFIGURATION_TYPES 0 first_config_type) + endif() + + if(is_multi_config AND NOT (cmake_config STREQUAL first_config_type)) + set(is_optional TRUE) + else() + set(is_optional FALSE) + endif() + + set(${out_var} "${is_optional}" PARENT_SCOPE) +endfunction() + +# Checks if given config is not the first config in a multi-config build, and thus file installation +# for that config should be optional, sets the actual option name. +function(_qt_internal_sbom_get_target_file_is_optional_in_multi_config config out_var) + _qt_internal_sbom_is_config_optional_in_multi_config("${config}" is_optional) + + if(is_optional) + set(option "OPTIONAL") + else() + set(option "") + endif() + + set(${out_var} "${option}" PARENT_SCOPE) +endfunction() + +# Joins two license IDs with the given ${op}, avoiding parenthesis when possible. +function(_qt_internal_sbom_join_two_license_ids_with_op left_id op right_id out_var) + if(NOT left_id) + set(${out_var} "${right_id}" PARENT_SCOPE) + return() + endif() + + if(NOT right_id) + set(${out_var} "${left_id}" PARENT_SCOPE) + return() + endif() + + set(value "(${left_id}) ${op} (${right_id})") + set(${out_var} "${value}" PARENT_SCOPE) +endfunction() diff --git a/cmake/QtSbomHelpers.cmake b/cmake/QtSbomHelpers.cmake new file mode 100644 index 00000000000..340354a759b --- /dev/null +++ b/cmake/QtSbomHelpers.cmake @@ -0,0 +1,24 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# For now these are simple internal forwarding wrappers for the public counterparts, which are +# meant to be used in qt repo CMakeLists.txt files. +function(qt_internal_add_sbom) + _qt_internal_add_sbom(${ARGN}) +endfunction() + +function(qt_internal_extend_sbom) + _qt_internal_extend_sbom(${ARGN}) +endfunction() + +function(qt_internal_sbom_add_license) + _qt_internal_sbom_add_license(${ARGN}) +endfunction() + +function(qt_internal_extend_sbom_dependencies) + _qt_internal_extend_sbom_dependencies(${ARGN}) +endfunction() + +function(qt_find_package_extend_sbom) + _qt_find_package_extend_sbom(${ARGN}) +endfunction() diff --git a/cmake/QtScopeFinalizerHelpers.cmake b/cmake/QtScopeFinalizerHelpers.cmake index 9e13bec26d5..fbce324db6a 100644 --- a/cmake/QtScopeFinalizerHelpers.cmake +++ b/cmake/QtScopeFinalizerHelpers.cmake @@ -77,9 +77,17 @@ function(qt_watch_current_list_dir variable access value current_list_file stack qt_finalize_plugin(${a1} ${a2} ${a3} ${a4} ${a5} ${a6} ${a7} ${a8} ${a9}) elseif(func STREQUAL "qt_internal_finalize_app") qt_internal_finalize_app(${a1} ${a2} ${a3} ${a4} ${a5} ${a6} ${a7} ${a8} ${a9}) + elseif(func STREQUAL "qt_internal_finalize_tool") + qt_internal_finalize_tool(${a1} ${a2} ${a3} ${a4} ${a5} ${a6} ${a7} ${a8} ${a9}) + elseif(func STREQUAL "qt_internal_finalize_3rdparty_library") + qt_internal_finalize_3rdparty_library( + ${a1} ${a2} ${a3} ${a4} ${a5} ${a6} ${a7} ${a8} ${a9}) elseif(func STREQUAL "qt_internal_export_additional_targets_file_finalizer") qt_internal_export_additional_targets_file_finalizer( ${a1} ${a2} ${a3} ${a4} ${a5} ${a6} ${a7} ${a8} ${a9}) + elseif(func STREQUAL "_qt_internal_finalize_sbom") + _qt_internal_finalize_sbom( + ${a1} ${a2} ${a3} ${a4} ${a5} ${a6} ${a7} ${a8} ${a9}) else() message(FATAL_ERROR "qt_watch_current_list_dir doesn't know about ${func}. Consider adding it.") endif() diff --git a/cmake/QtTargetHelpers.cmake b/cmake/QtTargetHelpers.cmake index d361a8096d1..b90b95870d9 100644 --- a/cmake/QtTargetHelpers.cmake +++ b/cmake/QtTargetHelpers.cmake @@ -45,6 +45,7 @@ function(qt_internal_extend_target target) set(single_args PRECOMPILED_HEADER EXTRA_LINKER_SCRIPT_CONTENT + ATTRIBUTION_ENTRY_INDEX ) set(multi_args ${__default_public_args} @@ -54,6 +55,9 @@ function(qt_internal_extend_target target) CONDITION_INDEPENDENT_SOURCES COMPILE_FLAGS EXTRA_LINKER_SCRIPT_EXPORTS + SBOM_DEPENDENCIES + ATTRIBUTION_FILE_PATHS + ATTRIBUTION_FILE_DIR_PATHS ) cmake_parse_arguments(PARSE_ARGV 1 arg @@ -210,6 +214,42 @@ function(qt_internal_extend_target target) endif() endforeach() + if(arg_LIBRARIES) + _qt_internal_append_to_target_property_without_duplicates(${target} + _qt_extend_target_libraries "${arg_LIBRARIES}" + ) + endif() + + if(arg_PUBLIC_LIBRARIES) + _qt_internal_append_to_target_property_without_duplicates(${target} + _qt_extend_target_public_libraries "${arg_PUBLIC_LIBRARIES}" + ) + endif() + + if(arg_SBOM_DEPENDENCIES) + _qt_internal_extend_sbom_dependencies(${target} + SBOM_DEPENDENCIES ${arg_SBOM_DEPENDENCIES} + ) + endif() + + if(NOT "${arg_ATTRIBUTION_ENTRY_INDEX}" STREQUAL "") + _qt_internal_extend_sbom(${target} + ATTRIBUTION_ENTRY_INDEX "${arg_ATTRIBUTION_ENTRY_INDEX}" + ) + endif() + + if(arg_ATTRIBUTION_FILE_PATHS) + _qt_internal_extend_sbom(${target} + ATTRIBUTION_FILE_PATHS ${arg_ATTRIBUTION_FILE_PATHS} + ) + endif() + + if(arg_ATTRIBUTION_FILE_DIR_PATHS) + _qt_internal_extend_sbom(${target} + ATTRIBUTION_FILE_DIR_PATHS ${arg_ATTRIBUTION_FILE_DIR_PATHS} + ) + endif() + set(target_private "${target}Private") get_target_property(is_internal_module ${target} _qt_is_internal_module) # Internal modules don't have Private targets but we still need to @@ -298,7 +338,7 @@ endfunction() function(qt_get_install_target_default_args) cmake_parse_arguments(PARSE_ARGV 0 arg "" - "OUT_VAR;CMAKE_CONFIG;RUNTIME;LIBRARY;ARCHIVE;INCLUDES;BUNDLE" + "OUT_VAR;OUT_VAR_RUNTIME;CMAKE_CONFIG;RUNTIME;LIBRARY;ARCHIVE;INCLUDES;BUNDLE" "ALL_CMAKE_CONFIGS") _qt_internal_validate_all_args_are_parsed(arg) @@ -348,6 +388,13 @@ function(qt_get_install_target_default_args) BUNDLE DESTINATION "${bundle}${suffix}" INCLUDES DESTINATION "${includes}${suffix}") set(${arg_OUT_VAR} "${args}" PARENT_SCOPE) + + if(arg_OUT_VAR_RUNTIME) + set(args + "${runtime}${suffix}" + ) + set(${arg_OUT_VAR_RUNTIME} "${args}" PARENT_SCOPE) + endif() endfunction() macro(qt_internal_setup_default_target_function_options) @@ -391,6 +438,15 @@ macro(qt_internal_setup_default_target_function_options) TARGET_COPYRIGHT ) + set(__qt_internal_sbom_single_args + ATTRIBUTION_ENTRY_INDEX + ) + set(__qt_internal_sbom_multi_args + SBOM_DEPENDENCIES + ATTRIBUTION_FILE_PATHS + ATTRIBUTION_FILE_DIR_PATHS + ) + # Collection of arguments so they can be shared across qt_internal_add_executable # and qt_internal_add_test_helper. set(__qt_internal_add_executable_optional_args @@ -408,10 +464,12 @@ macro(qt_internal_setup_default_target_function_options) INSTALL_DIRECTORY VERSION ${__default_target_info_args} + ${__qt_internal_sbom_single_args} ) set(__qt_internal_add_executable_multi_args ${__default_private_args} ${__default_public_args} + ${__qt_internal_sbom_multi_args} ) endmacro() diff --git a/cmake/QtToolHelpers.cmake b/cmake/QtToolHelpers.cmake index 0aef6a43a3d..dd865c7d9c3 100644 --- a/cmake/QtToolHelpers.cmake +++ b/cmake/QtToolHelpers.cmake @@ -56,12 +56,16 @@ function(qt_internal_add_tool target_name) INSTALL_DIR CORE_LIBRARY TRY_RUN_FLAGS - ${__default_target_info_args}) + ${__default_target_info_args} + ${__qt_internal_sbom_single_args} + ) set(multi_value_keywords EXTRA_CMAKE_FILES EXTRA_CMAKE_INCLUDES PUBLIC_LIBRARIES - ${__default_private_args}) + ${__default_private_args} + ${__qt_internal_sbom_multi_args} + ) cmake_parse_arguments(PARSE_ARGV 1 arg "${option_keywords}" @@ -128,6 +132,10 @@ function(qt_internal_add_tool target_name) LINK_OPTIONS ${arg_LINK_OPTIONS} MOC_OPTIONS ${arg_MOC_OPTIONS} DISABLE_AUTOGEN_TOOLS ${disable_autogen_tools} + ATTRIBUTION_ENTRY_INDEX "${arg_ATTRIBUTION_ENTRY_INDEX}" + ATTRIBUTION_FILE_PATHS ${arg_ATTRIBUTION_FILE_PATHS} + ATTRIBUTION_FILE_DIR_PATHS ${arg_ATTRIBUTION_FILE_DIR_PATHS} + SBOM_DEPENDENCIES ${arg_SBOM_DEPENDENCIES} TARGET_VERSION ${arg_TARGET_VERSION} TARGET_PRODUCT ${arg_TARGET_PRODUCT} TARGET_DESCRIPTION ${arg_TARGET_DESCRIPTION} @@ -186,8 +194,21 @@ function(qt_internal_add_tool target_name) set_property(GLOBAL APPEND PROPERTY QT_USER_FACING_TOOL_TARGETS ${target_name}) endif() + if(QT_GENERATE_SBOM) + set(sbom_args "") + list(APPEND sbom_args TYPE QT_TOOL) + endif() if(NOT arg_NO_INSTALL AND arg_TOOLS_TARGET) + set(will_install TRUE) + else() + set(will_install FALSE) + if(QT_GENERATE_SBOM) + list(APPEND sbom_args NO_INSTALL) + endif() + endif() + + if(will_install) # Assign a tool to an export set, and mark the module to which the tool belongs. qt_internal_append_known_modules_with_tools("${arg_TOOLS_TARGET}") @@ -202,6 +223,7 @@ function(qt_internal_add_tool target_name) foreach(cmake_config ${cmake_configs}) qt_get_install_target_default_args( OUT_VAR install_targets_default_args + OUT_VAR_RUNTIME runtime_install_destination RUNTIME "${install_dir}" CMAKE_CONFIG "${cmake_config}" ALL_CMAKE_CONFIGS ${cmake_configs}) @@ -214,6 +236,15 @@ function(qt_internal_add_tool target_name) unset(install_optional_arg) endif() + if(QT_GENERATE_SBOM) + _qt_internal_sbom_append_multi_config_aware_single_arg_option( + RUNTIME_PATH + "${runtime_install_destination}" + "${cmake_config}" + sbom_args + ) + endif() + qt_install(TARGETS "${target_name}" ${install_initial_call_args} ${install_optional_arg} @@ -240,6 +271,16 @@ function(qt_internal_add_tool target_name) qt_enable_separate_debug_info(${target_name} "${install_dir}" QT_EXECUTABLE) qt_internal_install_pdb_files(${target_name} "${install_dir}") + + if(QT_GENERATE_SBOM) + _qt_internal_extend_sbom(${target_name} ${sbom_args}) + endif() + + qt_add_list_file_finalizer(qt_internal_finalize_tool ${target_name}) +endfunction() + +function(qt_internal_finalize_tool target) + _qt_internal_finalize_sbom(${target}) endfunction() function(_qt_internal_add_try_run_post_build target try_run_flags) diff --git a/cmake/configure-cmake-mapping.md b/cmake/configure-cmake-mapping.md index 6a184f61192..2a265198ba7 100644 --- a/cmake/configure-cmake-mapping.md +++ b/cmake/configure-cmake-mapping.md @@ -45,6 +45,7 @@ The following table describes the mapping of configure options to CMake argument | -device-option | -DQT_QMAKE_DEVICE_OPTIONS=key1=value1;key2=value2 | Only used for generation qmake-compatibility files. | | | | The device options are written into mkspecs/qdevice.pri. | | -appstore-compliant | -DFEATURE_appstore_compliant=ON | | +| -sbom | -DQT_GENERATE_SBOM=ON | Enables generation and installation of an SBOM | | -qtinlinenamespace | -DQT_INLINE_NAMESPACE=ON | Make the namespace specified by -qtnamespace an inline one. | | -qtnamespace | -DQT_NAMESPACE= | | | -qtlibinfix | -DQT_LIBINFIX= | | diff --git a/config_help.txt b/config_help.txt index 263195d7fdd..276cb550231 100644 --- a/config_help.txt +++ b/config_help.txt @@ -25,6 +25,8 @@ except -sysconfdir should be located under -prefix: -libexecdir ..... Helper programs [ARCHDATADIR/bin on Windows, ARCHDATADIR/libexec otherwise] -qmldir ......... QML imports [ARCHDATADIR/qml] + -sbomdir ....... Software Bill of Materials (SBOM) + installation directory [ARCHDATADIR/sbom] -datadir ........ Arch-independent data [PREFIX] -docdir ......... Documentation [DATADIR/doc] -translationdir . Translations [DATADIR/translations] @@ -99,6 +101,9 @@ Build options: through an app store by default, in particular Android, iOS, tvOS, and watchOS. [auto] + -sbom ................ Enable Software Bill of Materials (SBOM) generation + [no] + -qt-host-path . Specify path to a Qt host build for cross-compiling. -qtnamespace .. Wrap all Qt library code in 'namespace {...}'. -qtinlinenamespace ... Make -qtnamespace an inline namespace