From 65bf57ce7cac8d3fdcb7c626a0e8ce41a849e48a Mon Sep 17 00:00:00 2001 From: Alexandru Croitor Date: Thu, 7 Mar 2024 18:02:56 +0100 Subject: [PATCH] CMake: Generate an SPDX v2.3 SBOM file for each built repository This change adds a new -sbom configure option to allow generating and installing an SPDX v2.3 SBOM file when building a qt repo. The -sbom-dir option can be used to configure the location where each repo sbom file will be installed. By default it is installed into $prefix/$archdatadir/sbom/$sbom_lower_project_name.sdpx which is basically ~/Qt/sbom/qtbase-6.8.0.spdx The file is installed as part of the default installation rules, but it can also be installed manually using the "sbom" installation component, or "sbom_$lower_project_name" in a top-level build. For example: cmake install . --component sbom_qtbase CMake 3.19+ is needed to read the qt_attribution.json files for copyrights, license info, etc. When using an older cmake version, configuration will error out. It is possible to opt into using an older cmake version, but the generated sbom will lack all the attribution file information. Using an older cmake version is untested and not officially supported. Implementation notes. The bulk of the implementation is split into 4 new files: - QtPublicSbomHelpers.cmake - for Qt-specific collecting, processing and dispatching the generation of various pieces of the SBOM document e.g. a SDPX package associated with a target like Core, a SDPX file entry for each target binary file (per-config shared library, archive, executable, etc) - QtPublicSbomGenerationHelpers.cmake - for non-Qt specific implementation of SPDX generation. This also has some code that was taken from the cmake-sbom 3rd party project, so it is dual licensed under the usual Qt build system BSD license, as well as the MIT license of the 3rd party project - QtPublicGitHelpers.cmake - for git related features, mainly to embed queried hashes or tags into version strings, is dual-licensed for the same reasons as QtPublicSbomGenerationHelpers.cmake - QtSbomHelpers.cmake - Qt-specific functions that just forward arguments to the public functions. These are meant to be used in our Qt CMakeLists.txt instead of the public _qt_internal_add_sbom ones for naming consistency. These function would mostly be used to annotate 3rd party libraries with sbom info and to add sbom info for unusual target setups (like the Bootstrap library), because most of the handling is already done automatically via qt_internal_add_module/plugin/etc. The files are put into Public cmake files, with the future hope of making this available to user projects in some capacity. The distinction of Qt-specific and non-Qt specific code might blur a bit, and thus the separation across files might not always be consistent, but it was best effort. The main purpose of the code is to collect various information about targets and their relationships and generate equivalent SPDX info. Collection is currently done for the following targets: Qt modules, plugins, apps, tools, system libraries, bundled 3rd party libraries and partial 3rd party sources compiled directly as part of Qt targets. Each target has an equivalent SPDX package generated with information like version, license, copyright, CPE (common vulnerability identifier), files that belong to the package, and relationships on other SPDX packages (associated cmake targets), mostly gathered from direct linking dependencies. Each package might also contain files, e.g. libQt6Core.so for the Core target. Each file also has info like license id, copyrights, but also the list of source files that were used to generate the file and a sha1 checksum. SPDX documents can also refer to packages in other SPDX documents, and those are referred to via external document references. This is the case when building qtdeclarative and we refer to Core. For qt provided targets, we have complete information regarding licenses, and copyrights. For bundled 3rd party libraries, we should also have most information, which is usually parsed from the src/3rdparty/libfoo/qt_attribution.json files. If there are multiple attribution files, or if the files have multiple entries, we create a separate SBOM package for each of those entries, because each might have a separate copyright or version, and an sbom package can have only one version (although many copyrights). For system libraries we usually lack the information because we don't have attribution files for Find scripts. So the info needs to be manually annotated via arguments to the sbom function calls, or the FindFoo.cmake scripts expose that information in some form and we can query it. There are also corner cases like 3rdparty sources being directly included in a Qt library, like the m4dc files for Gui, or PCRE2 for Bootstrap. Or QtWebEngine libraries (either Qt bundled or Chromium bundled or system libraries) which get linked in by GN instead of CMake, so there are no direct targets for them. The information for these need to be annotated manually as well. There is also a distinction to be made for static Qt builds (or any static Qt library in a shared build), where the system libraries found during the Qt build might not be the same that are linked into the final user application or library. The actual generation of the SBOM is done by file(GENERATE)-ing one .cmake file for each target, file, external ref, etc, which will be included in a top-level cmake script. The top-level cmake script will run through each included file, to append to a "staging" spdx file, which will then be used in a configure_file() call to replace some final variables, like embedding a file checksum. There are install rules to generate a complete SBOM during installation, and an optional 'sbom' custom target that allows building an incomplete SBOM during the build step. The build target is just for convenience and faster development iteration time. It is incomplete because it is missing the installed file SHA1 checksums and the document verification code (the sha1 of all sha1s). We can't compute those during the build before the files are actually installed. A complete SBOM can only be achieved at installation time. The install script will include all the generated helper files, but also set some additional variables to ensure checksumming happens, and also handle multi-config installation, among other small things. For multi-config builds, CMake doesn't offer a way to run code after all configs are installed, because they might not always be installed, someone might choose to install just Release. To handle that, we rely on ninja installing each config sequentially (because ninja places the install rules into the 'console' pool which runs one task at a time). For each installed config we create a config-specific marker file. Once all marker files are present, whichever config ends up being installed as the last one, we run the sbom generation once, and then delete all marker files. There are a few internal variables that can be set during configuration to enable various checks (and other features) on the generated spdx files: - QT_INTERNAL_SBOM_VERIFY - QT_INTERNAL_SBOM_AUDIT - QT_INTERNAL_SBOM_AUDIT_NO_ERROR - QT_INTERNAL_SBOM_GENERATE_JSON - QT_INTERNAL_SBOM_SHOW_TABLE - QT_INTERNAL_SBOM_DEFAULT_CHECKS These use 3rd party python tools, so they are not enabled by default. If enabled, they run at installation time after the sbom is installed. We will hopefully enable them in CI. Overall, the code is still a bit messy in a few places, due to time constraints, but can be improved later. Some possible TODOs for the future: - Do we need to handle 3rd party libs linked into a Qt static library in a Qt shared build, where the Qt static lib is not installed, but linked into a Qt shared library, somehow specially? We can record a package for it, but we can't create a spdx file record for it (and associated source relationships) because we don't install the file, and spdx requires the file to be installed and checksummed. Perhaps we can consider adding some free-form text snippet to the package itself? - Do we want to add parsing of .cpp source files for Copyrights, to embed them into the packages? This will likely slow down configuration quite a bit. - Currently sbom info attached to WrapFoo packages in one repo is not exported / available in other repos. E.g. If we annotate WrapZLIB in qtbase with CPE_VENDOR zlib, this info will not be available when looking up WrapZLIB in qtimageformats. This is because they are IMPORTED libraries, and are not exported. We might want to record this info in the future. [ChangeLog][Build System] A new -sbom configure option can be used to generate and install a SPDX SBOM (Software Bill of Materials) file for each built Qt repository. Task-number: QTBUG-122899 Change-Id: I9c730a6bbc47e02ce1836fccf00a14ec8eb1a5f4 Reviewed-by: Joerg Bornemann Reviewed-by: Alexey Edelev (cherry picked from commit 37a5e001277db9e1392a242171ab2b88cb6c3049) Reviewed-by: Qt Cherry-pick Bot --- cmake/Qt3rdPartyLibraryHelpers.cmake | 81 + cmake/QtAppHelpers.cmake | 60 +- cmake/QtBaseGlobalTargets.cmake | 4 + cmake/QtBuildHelpers.cmake | 4 + cmake/QtBuildPathsHelpers.cmake | 2 + cmake/QtBuildRepoHelpers.cmake | 8 + cmake/QtExecutableHelpers.cmake | 4 + cmake/QtFindPackageHelpers.cmake | 19 +- cmake/QtInternalTargets.cmake | 20 + cmake/QtModuleHelpers.cmake | 48 + cmake/QtPlatformTargetHelpers.cmake | 5 + cmake/QtPluginHelpers.cmake | 26 + cmake/QtPostProcessHelpers.cmake | 2 +- cmake/QtProcessConfigureArgs.cmake | 8 + cmake/QtPublicCMakeHelpers.cmake | 4 + cmake/QtPublicDependencyHelpers.cmake | 31 + cmake/QtPublicGitHelpers.cmake | 153 ++ cmake/QtPublicSbomGenerationHelpers.cmake | 1139 +++++++++ cmake/QtPublicSbomHelpers.cmake | 2644 +++++++++++++++++++++ cmake/QtSbomHelpers.cmake | 24 + cmake/QtScopeFinalizerHelpers.cmake | 8 + cmake/QtTargetHelpers.cmake | 60 +- cmake/QtToolHelpers.cmake | 45 +- cmake/configure-cmake-mapping.md | 1 + config_help.txt | 5 + 25 files changed, 4397 insertions(+), 8 deletions(-) create mode 100644 cmake/QtPublicGitHelpers.cmake create mode 100644 cmake/QtPublicSbomGenerationHelpers.cmake create mode 100644 cmake/QtPublicSbomHelpers.cmake create mode 100644 cmake/QtSbomHelpers.cmake 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