From 27d2b54b5d2bc5a69edc2de703b2ca34cb2637dc Mon Sep 17 00:00:00 2001 From: Alexandru Croitor Date: Fri, 10 Jan 2025 12:45:31 +0100 Subject: [PATCH] CMake: Split SBOM implementation into separate files The SBOM implementation got somewhat large. Split the code into several new QtPublicSbomFooHelpers.cmake files, to make it more manageable. No code or behavior was changed. Pick-to: 6.8 6.9 Task-number: QTBUG-122899 Change-Id: Ia0ca1792eec21d12c4bb4cabe63279e1f5c07e3d Reviewed-by: Alexey Edelev --- cmake/QtBuildHelpers.cmake | 9 + cmake/QtPublicSbomAttributionHelpers.cmake | 516 ++++ cmake/QtPublicSbomCpeHelpers.cmake | 90 + cmake/QtPublicSbomDepHelpers.cmake | 327 +++ cmake/QtPublicSbomFileHelpers.cmake | 1083 ++++++++ cmake/QtPublicSbomGenerationHelpers.cmake | 822 ------ cmake/QtPublicSbomHelpers.cmake | 2716 -------------------- cmake/QtPublicSbomLicenseHelpers.cmake | 108 + cmake/QtPublicSbomOpsHelpers.cmake | 579 +++++ cmake/QtPublicSbomPurlHelpers.cmake | 444 ++++ cmake/QtPublicSbomPythonHelpers.cmake | 250 ++ cmake/QtPublicSbomSystemDepHelpers.cmake | 162 ++ licenseRule.json | 5 + 13 files changed, 3573 insertions(+), 3538 deletions(-) create mode 100644 cmake/QtPublicSbomAttributionHelpers.cmake create mode 100644 cmake/QtPublicSbomCpeHelpers.cmake create mode 100644 cmake/QtPublicSbomDepHelpers.cmake create mode 100644 cmake/QtPublicSbomFileHelpers.cmake create mode 100644 cmake/QtPublicSbomLicenseHelpers.cmake create mode 100644 cmake/QtPublicSbomOpsHelpers.cmake create mode 100644 cmake/QtPublicSbomPurlHelpers.cmake create mode 100644 cmake/QtPublicSbomPythonHelpers.cmake create mode 100644 cmake/QtPublicSbomSystemDepHelpers.cmake diff --git a/cmake/QtBuildHelpers.cmake b/cmake/QtBuildHelpers.cmake index 8ef788f8741..d3d508de43a 100644 --- a/cmake/QtBuildHelpers.cmake +++ b/cmake/QtBuildHelpers.cmake @@ -293,8 +293,17 @@ function(qt_internal_get_qt_build_public_helpers out_var) QtPublicGitHelpers QtPublicPluginHelpers QtPublicPluginHelpers_v2 + QtPublicSbomAttributionHelpers + QtPublicSbomCpeHelpers + QtPublicSbomDepHelpers + QtPublicSbomFileHelpers QtPublicSbomGenerationHelpers QtPublicSbomHelpers + QtPublicSbomLicenseHelpers + QtPublicSbomOpsHelpers + QtPublicSbomPurlHelpers + QtPublicSbomPythonHelpers + QtPublicSbomSystemDepHelpers QtPublicTargetHelpers QtPublicTestHelpers QtPublicToolHelpers diff --git a/cmake/QtPublicSbomAttributionHelpers.cmake b/cmake/QtPublicSbomAttributionHelpers.cmake new file mode 100644 index 00000000000..142be7dce27 --- /dev/null +++ b/cmake/QtPublicSbomAttributionHelpers.cmake @@ -0,0 +1,516 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# 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 + ) + set(multi_args "") + + _qt_internal_get_sbom_specific_options(sbom_opt_args sbom_single_args sbom_multi_args) + list(APPEND opt_args ${sbom_opt_args}) + list(APPEND single_args ${sbom_single_args}) + list(APPEND multi_args ${sbom_multi_args}) + + 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 CREATE_SBOM_FOR_EACH_ATTRIBUTION is set, that means the parent target was a qt entity, + # and not a 3rd party library. + # In which case we don't want to proagate options like CPE to the child attribution targets, + # because the CPE is meant for the parent target. + set(propagate_sbom_options_to_new_attribution_targets TRUE) + if(arg_CREATE_SBOM_FOR_EACH_ATTRIBUTION) + set(propagate_sbom_options_to_new_attribution_targets FALSE) + 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) + # Collect all processed attribution files to later create a configure-time dependency on + # them so that the SBOM is regenerated (and CMake is re-ran) if they are modified. + set_property(GLOBAL APPEND PROPERTY _qt_internal_project_attribution_files + "${attribution_file_path}") + + # 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() + + set(sbom_args "") + + if(propagate_sbom_options_to_new_attribution_targets) + # Filter out the attributtion options, they will be passed mnaually + # depending on which file and index is currently being processed. + _qt_internal_get_sbom_specific_options( + sbom_opt_args sbom_single_args sbom_multi_args) + list(REMOVE_ITEM sbom_opt_args NO_CURRENT_DIR_ATTRIBUTION) + list(REMOVE_ITEM sbom_single_args ATTRIBUTION_ENTRY_INDEX) + list(REMOVE_ITEM sbom_multi_args + ATTRIBUTION_FILE_PATHS + ATTRIBUTION_FILE_DIR_PATHS + ) + + # Also filter out the FRIENDLY_PACKAGE_NAME option, otherwise we'd try to + # file(GENERATE) multiple times with the same file name, but different content. + list(REMOVE_ITEM sbom_single_args FRIENDLY_PACKAGE_NAME) + + _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} + ) + 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 + ${sbom_args} + ) + + _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}" IS_MULTI_VALUE) + _qt_internal_sbom_get_attribution_key(CopyrightFile copyright_file "${out_prefix}") + _qt_internal_sbom_get_attribution_key(PURL purls "${out_prefix}" IS_MULTI_VALUE) + _qt_internal_sbom_get_attribution_key(CPE cpes "${out_prefix}" IS_MULTI_VALUE) + + # 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() + +# Extracts a string or an array of strings from a json index path, depending on the extracted value +# type. +# +# Given the 'contents' of the whole json document and the EXTRACTED_VALUE of a json key specified +# by the INDICES path, it tries to determine whether the value is an array, in which case the array +# is converted to a cmake list and assigned to ${out_var} in the parent scope. +# Otherwise the function assumes the EXTRACTED_VALUE was not an array, and just assigns the value +# of EXTRACTED_VALUE to ${out_var} +function(_qt_internal_sbom_handle_attribution_json_array contents) + set(opt_args "") + set(single_args + EXTRACTED_VALUE + OUT_VAR + ) + set(multi_args + INDICES + ) + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + # Write the original value to the parent scope, in case it was not an array. + set(${arg_OUT_VAR} "${arg_EXTRACTED_VALUE}" PARENT_SCOPE) + + if(NOT arg_EXTRACTED_VALUE) + return() + endif() + + string(JSON element_type TYPE "${contents}" ${arg_INDICES}) + + if(NOT element_type STREQUAL "ARRAY") + return() + endif() + + set(json_array "${arg_EXTRACTED_VALUE}") + string(JSON array_len LENGTH "${json_array}") + + set(value_list "") + + math(EXPR array_len "${array_len} - 1") + foreach(index RANGE 0 "${array_len}") + string(JSON value GET "${json_array}" ${index}) + if(value) + list(APPEND value_list "${value}") + endif() + endforeach() + + if(value_list) + set(${arg_OUT_VAR} "${value_list}" 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() + +# This macro reads a json key from a qt_attribution.json file, and assigns the escaped value to +# out_var. +# Also appends the name of the out_var to the parent scope 'variable_names' var. +# +# Expects 'contents' and 'indices' to already be set in the calling scope. +# +# If IS_MULTI_VALUE is set, handles the key as if it contained an array of +# values, by converting the array of json values to a cmake list. +macro(_qt_internal_sbom_get_attribution_key json_key out_var out_prefix) + cmake_parse_arguments(arg "IS_MULTI_VALUE" "" "" ${ARGN}) + + string(JSON "${out_var}" ERROR_VARIABLE get_error GET "${contents}" ${indices} "${json_key}") + if(NOT "${${out_var}}" STREQUAL "" AND NOT get_error) + set(extracted_value "${${out_var}}") + + if(arg_IS_MULTI_VALUE) + _qt_internal_sbom_handle_attribution_json_array("${contents}" + EXTRACTED_VALUE "${extracted_value}" + INDICES ${indices} ${json_key} + OUT_VAR value_list + ) + if(value_list) + set(extracted_value "${value_list}") + endif() + endif() + + _qt_internal_sbom_escape_json_content("${extracted_value}" escaped_content) + + set(${out_prefix}_${out_var} "${escaped_content}" PARENT_SCOPE) + list(APPEND variable_names "${out_var}") + + unset(extracted_value) + unset(escaped_content) + unset(value_list) + endif() +endmacro() + +# Replaces placeholders in CPE and PURL strings read from qt_attribution.json files. +# +# VALUES - list of CPE or PURL strings +# OUT_VAR - variable to store the replaced values +# VERSION - version to replace in the placeholders + +# Known placeholders: +# $ - Replaces occurrences of the placeholder with the value passed to the VERSION option. +# $ - Replaces occurrences of the placeholder with the value passed to the VERSION +# option, but with dots replaced by dashes. +function(_qt_internal_sbom_replace_qa_placeholders) + set(opt_args "") + set(single_args + OUT_VAR + VERSION + ) + set(multi_args + VALUES + ) + + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_OUT_VAR) + message(FATAL_ERROR "OUT_VAR must be set") + endif() + + set(result "") + + if(arg_VERSION) + string(REPLACE "." "-" dashed_version "${arg_VERSION}") + endif() + + foreach(value IN LISTS arg_VALUES) + if(arg_VERSION) + string(REPLACE "$" "${arg_VERSION}" value "${value}") + string(REPLACE "$" "${dashed_version}" value "${value}") + endif() + + list(APPEND result "${value}") + endforeach() + + set(${arg_OUT_VAR} "${result}" PARENT_SCOPE) +endfunction() diff --git a/cmake/QtPublicSbomCpeHelpers.cmake b/cmake/QtPublicSbomCpeHelpers.cmake new file mode 100644 index 00000000000..212a552450e --- /dev/null +++ b/cmake/QtPublicSbomCpeHelpers.cmake @@ -0,0 +1,90 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# 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() diff --git a/cmake/QtPublicSbomDepHelpers.cmake b/cmake/QtPublicSbomDepHelpers.cmake new file mode 100644 index 00000000000..be8031c5927 --- /dev/null +++ b/cmake/QtPublicSbomDepHelpers.cmake @@ -0,0 +1,327 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# 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) + set(install_prefixes "") + + get_cmake_property(install_prefix _qt_internal_sbom_install_prefix) + list(APPEND install_prefixes "${install_prefix}") + + set(external_document "${relative_installed_repo_document_path}") + + _qt_internal_sbom_generate_add_external_reference( + EXTERNAL_DOCUMENT_FILE_PATH "${external_document}" + EXTERNAL_DOCUMENT_INSTALL_PREFIXES ${install_prefixes} + EXTERNAL_DOCUMENT_SPDX_ID "${external_document_ref}" + ) + + 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() diff --git a/cmake/QtPublicSbomFileHelpers.cmake b/cmake/QtPublicSbomFileHelpers.cmake new file mode 100644 index 00000000000..8365ddbcfec --- /dev/null +++ b/cmake/QtPublicSbomFileHelpers.cmake @@ -0,0 +1,1083 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# 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 + QT_TRANSLATIONS + QT_RESOURCES + QT_CUSTOM + QT_CUSTOM_NO_INFIX + + # 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 + TRANSLATIONS + RESOURCES + CUSTOM + CUSTOM_NO_INFIX + ) + + if(NOT arg_TYPE IN_LIST supported_types) + message(FATAL_ERROR "Unsupported target TYPE for SBOM creation: ${arg_TYPE}") + endif() + + set(types_without_binary_files + QT_THIRD_PARTY_SOURCES + QT_TRANSLATIONS + QT_RESOURCES + QT_CUSTOM + QT_CUSTOM_NO_INFIX + SYSTEM_LIBRARY + THIRD_PARTY_LIBRARY + TRANSLATIONS + RESOURCES + CUSTOM + CUSTOM_NO_INFIX + ) + + get_target_property(target_type ${target} TYPE) + + if(arg_TYPE IN_LIST types_without_binary_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() + + get_target_property(excluded ${target} _qt_internal_excluded_from_default_target) + if(excluded) + message(DEBUG "Target ${target} has no binary files to reference in the SBOM " + "because it was excluded from the default 'all' target.") + 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() + + 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") + + set(valid_executable_types + "EXECUTABLE" + ) + if(ANDROID) + list(APPEND valid_executable_types "MODULE_LIBRARY") + endif() + if(NOT target_type IN_LIST valid_executable_types) + 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. +# TODO: Consider merging the common parts with _qt_internal_sbom_add_custom_file somehow. +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(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_target_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() + +# Add a list of to-be-installed files that should appear in the files section of the target's +# SBOM document. +# Supports multiple calls with the same target name. +# Each call is handled as a separate set of files. +# For options that can be passed, see the doc-comment of +# _qt_internal_sbom_handle_target_custom_file_set. +function(_qt_internal_sbom_add_files target) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + if(NOT TARGET "${target}") + message(FATAL_ERROR "The target ${target} does not exist.") + endif() + + get_target_property(file_sets_count "${target}" _qt_sbom_custom_file_sets_count) + if(NOT file_sets_count) + set(file_sets_count 0) + endif() + + set_property(TARGET "${target}" + APPEND PROPERTY _qt_sbom_custom_file_set_${file_sets_count} "${ARGN}") + + math(EXPR file_sets_count "${file_sets_count}+1") + set_property(TARGET "${target}" PROPERTY _qt_sbom_custom_file_sets_count "${file_sets_count}") +endfunction() + +# Handles addition of custom file SPDX entries for a given target, by processing all the collected +# file sets so far. +# Applies the passed license and copyright info to all collected files. +# +# Options that can be passed. +# +# NO_INSTALL - if set, custom file processing is skipped, because the files will not be installed. +# +# PACKAGE_TYPE - the type of the package that the files belong to, is used to compute an infix +# for the file spdx id. +# +# PACKAGE_SPDX_ID - the package spdx id is used to add a relationship between the package and file. +# +# LICENSE_EXPRESSION - a license expression to apply to the files. +# +# INSTALL_PREFIX - the install prefix for the files, this is usually the install prefix of qt. +# +# COPYRIGHTS - a list of copyright strings to apply to the files. +function(_qt_internal_sbom_handle_target_custom_files target) + set(opt_args + NO_INSTALL + ) + set(single_args + PACKAGE_TYPE + PACKAGE_SPDX_ID + LICENSE_EXPRESSION + INSTALL_PREFIX + ) + set(multi_args + COPYRIGHTS + ) + + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + # Nothing to process if no file sets were defined. + get_target_property(file_sets_count "${target}" _qt_sbom_custom_file_sets_count) + if(NOT file_sets_count) + return() + endif() + + if(arg_NO_INSTALL) + message(DEBUG "Skipping sbom custom file processing for ${target} because NO_INSTALL is " + "set") + return() + endif() + + if(NOT arg_PACKAGE_SPDX_ID) + message(FATAL_ERROR "PACKAGE_SPDX_ID must be set") + endif() + + _qt_internal_forward_function_args( + FORWARD_PREFIX arg + FORWARD_OUT_VAR file_common_options + FORWARD_SINGLE + PACKAGE_TYPE + PACKAGE_SPDX_ID + LICENSE_EXPRESSION + INSTALL_PREFIX + FORWARD_MULTI + COPYRIGHTS + ) + + # Subtract -1, because foreach(RANGE is inclusive). + math(EXPR file_sets_count "${file_sets_count}-1") + foreach(file_set_index RANGE ${file_sets_count}) + get_target_property(file_set_args "${target}" _qt_sbom_custom_file_set_${file_set_index}) + + if(NOT file_set_args) + message(FATAL_ERROR "No arguments were specified for SBOM custom file set for " + "${target}") + endif() + + _qt_internal_sbom_handle_target_custom_file_set("${target}" + ${file_common_options} ${file_set_args}) + endforeach() +endfunction() + +# Processes a file set of custom files for which to include SBOM info. +# The +# NO_INSTALL +# PACKAGE_TYPE +# PACKAGE_SPDX_ID +# LICENSE_EXPRESSION +# INSTALL_PREFIX +# COPYRIGHTS +# options are the same as in _qt_internal_sbom_handle_target_custom_files, and are actually +# forwarded from that function, because they might be set at the target level. +# +# In addition, more options can be passed when called via qt_internal_sbom_add_custom_files: +# +# They are: +# +# FILE_TYPE - the type of each provided file. Supported types can be found in the implementation of +# _qt_internal_sbom_get_spdx_v2_3_file_type_for_file(). +# Some examples are QT_TRANSLATION, QT_TRANSLATIONS_CATALOG, QT_RESOURCE. +# +# FILES - a list of file paths to include in the SBOM. Only the file name is currently used. +# +# SOURCE_FILES - which source files were used to generate the custom files. All source files apply +# to each input file. +# +# SOURCE_FILES_PER_INPUT_FILE - for each index i in FILES, the corresponding source files are +# in SOURCE_FILES[i], so that each input gets exactly one source file. +# This is provided as an option, to prevent performance overhead from having to add a +# custom file set for each new source file, when dealing with translations that have a +# 1-to-1 ts->qm relationship. +# +# There is also a set of multi config aware options that can be set, like +# INSTALL_PATH +# INSTALL_PATH_ +# which should be the relative install dir path where the +# files will be installed, relative to $ENV{DESTDIR}/${CMAKE_INSTALL_PREFIX}. +function(_qt_internal_sbom_handle_target_custom_file_set target) + set(opt_args + "" + ) + set(single_args + FILE_TYPE + PACKAGE_SPDX_ID + PACKAGE_TYPE + LICENSE_EXPRESSION + INSTALL_PREFIX + ) + set(multi_args + COPYRIGHTS + FILES + SOURCE_FILES + SOURCE_FILES_PER_INPUT_FILE + ) + + # Don't explicitly forward the multi config single args, but still parse them. + # They will be accessed from the current scope directly. + set(single_args_without_multi_config_args "${single_args}") + _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) + + # No custom files to process. + if(NOT arg_FILES) + return() + endif() + + # Don't forward the FILES option. + set(multi_args_without_files "${multi_args}") + list(REMOVE_ITEM multi_args_without_files FILES) + + # Handle the case where we have one source file per input file. + if(arg_SOURCE_FILES_PER_INPUT_FILE) + list(LENGTH arg_FILES files_count) + list(LENGTH arg_SOURCE_FILES_PER_INPUT_FILE source_files_count) + if(NOT files_count EQUAL source_files_count) + message(FATAL_ERROR "The number of files passed to SOURCE_FILES must match the number" + "of files passed to SOURCE_FILES_PER_INPUT_FILE.") + endif() + + # Don't forward all of the source files, but rather one per input file. + list(REMOVE_ITEM multi_args_without_files SOURCE_FILES_PER_INPUT_FILE) + endif() + + _qt_internal_forward_function_args( + FORWARD_PREFIX arg + FORWARD_OUT_VAR forward_args + FORWARD_SINGLE + ${single_args_without_multi_config_args} + FORWARD_MULTI + ${multi_args_without_files} + ) + + set(file_index 0) + foreach(file_path IN LISTS arg_FILES) + set(per_file_forward_args "") + + # We don't currently use the file path for anything other than getting the file name, to + # embed it into the spdx document entry. + # What matters in the end is the location where the file is installed, which is handled + # by the PATH_KIND option. + get_filename_component(file_name "${file_path}" NAME) + + if(arg_SOURCE_FILES_PER_INPUT_FILE) + list(GET arg_SOURCE_FILES_PER_INPUT_FILE "${file_index}" source_file) + list(APPEND per_file_forward_args SOURCE_FILES "${source_file}") + endif() + + # The multi_config_single_args are deliberately not forwarded, but are available in this + # function scope, for direct access in the called function scope, because + # cmake_parse_arguments can't handle: + # PATH_KIND "INSTALL_PATH" + # INSTALL_PATH "/some_path" + # the parsing gets confused by what's the option and what's the value. + _qt_internal_sbom_handle_multi_config_custom_file(${target} + PATH_KIND "INSTALL_PATH" + PATH_SUFFIX "${file_name}" + OPTIONS + ${forward_args} + ${per_file_forward_args} + ) + math(EXPR file_index "${file_index}+1") + endforeach() +endfunction() + +# Helper function to add a custom 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. +# +# Expects the parent scope to contain the ${PATH_KIND} and ${PATH_KIND}_ variables. +# Examples are INSTALL_PATH or INSTALL_PATH_DEBUG. +# They can't be forwarded as options because of cmake_parse_arguments parsing issues when a value +# can be the same as a key. See comment in implementation of +#_qt_internal_sbom_handle_target_custom_file_set. +function(_qt_internal_sbom_handle_multi_config_custom_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_custom_file( + "${target}" + "${file_path}" + ${arg_OPTIONS} + ${is_optional} + CONFIG ${config} + ) + endforeach() +endfunction() + +# Adds one custom file with the given relative install path into the SBOM document. +# Will embed GENERATED_FROM source file relationships if a list of source files is specified. +function(_qt_internal_sbom_add_custom_file target installed_file_relative_path) + set(opt_args + OPTIONAL + ) + set(single_args + PACKAGE_SPDX_ID + PACKAGE_TYPE + LICENSE_EXPRESSION + INSTALL_PREFIX + FILE_TYPE + CONFIG + ) + set(multi_args + COPYRIGHTS + SOURCE_FILES + ) + + 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(spdx_id_suffix "${arg_CONFIG}") + set(config_to_install_option CONFIG ${arg_CONFIG}) + else() + set(spdx_id_suffix "") + set(config_to_install_option "") + endif() + + if(arg_FILE_TYPE) + _qt_internal_sbom_get_spdx_v2_3_file_type_for_file(file_type "${arg_FILE_TYPE}") + else() + set(file_type "OTHER") + endif() + + get_filename_component(file_name "${installed_file_relative_path}" NAME) + + _qt_internal_sbom_get_package_infix("${arg_PACKAGE_TYPE}" package_infix) + + _qt_internal_sbom_get_file_spdx_id( + "${package_infix}-${target}-${file_name}-${spdx_id_suffix}" spdx_id) + + # Add relationship from owning package. + set(relationships "${arg_PACKAGE_SPDX_ID} CONTAINS ${spdx_id}") + + # Add source file relationships from which the custom file was generated. + set(sources_option "") + + if(arg_SOURCE_FILES) + set(sources_option SOURCES ${arg_SOURCE_FILES}) + endif() + + _qt_internal_sbom_add_source_files( + ${sources_option} + SPDX_ID "${spdx_id}" + OUT_RELATIONSHIPS_VAR 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}") + + _qt_internal_sbom_generate_add_file( + FILENAME "${installed_file_relative_path}" + FILETYPE "${file_type}" ${optional} + SPDXID "${spdx_id}" + ${file_common_options} + ${config_to_install_option} + ${relationship_option} + ) +endfunction() + +# Maps an arbitrary file type to a spdx v2.3 file type. +# There is a list of known SPDX types, and custom Qt ones. +# Any other type is mapped to OTHER. +# The mapping might change when we start generating spdx v3.0 documents. +function(_qt_internal_sbom_get_spdx_v2_3_file_type_for_file out_var file_type_in) + set(spdx_v2_3_file_types + SOURCE + BINARY + ARCHIVE + APPLICATION + AUDIO + IMAGE + TEXT + VIDEO + DOCUMENTATION + SPDX + OTHER + ) + + # No semantic meaning at the moment, but we might want to map the values to something else + # when we port to SPDX v3.0+. + set(qt_file_types + QT_TRANSLATION + QT_TRANSLATIONS_CATALOG + QT_RESOURCE + TRANSLATION + RESOURCE + CUSTOM + ) + + if(file_type_in IN_LIST spdx_v2_3_file_types) + set(file_type "${file_type_in}") + elseif(file_type_in IN_LIST qt_file_types) + set(file_type OTHER) + else() + set(file_type OTHER) + endif() + + set(${out_var} "${file_type}" PARENT_SCOPE) +endfunction() + +# Takes a relative or absolute path and maps it to a reproducible path that is relative to +# the project source or build dir. +function(_qt_internal_sbom_map_path_to_reproducible_relative_path out_var) + set(opt_args "") + set(single_args + PATH + REPO_PROJECT_NAME_LOWERCASE + OUT_SUCCESS + ) + 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(is_in_source_dir FALSE) + set(is_in_build_dir FALSE) + + if(NOT arg_REPO_PROJECT_NAME_LOWERCASE) + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name) + else() + set(repo_project_name "${arg_REPO_PROJECT_NAME_LOWERCASE}") + endif() + + if(NOT DEFINED arg_PATH) + message(FATAL_ERROR "PATH must be set") + endif() + set(path "${arg_PATH}") + + set(handled FALSE) + + if(path MATCHES "$<.+>") + # TODO: Paths wrapped in genexes are usually absolute paths that we also want to handle, + # but can't at configure time. We'll need a separate processing step at build time. + # Keep these as is for now, but signal failure of handling. + set(path_out "${path}") + else() + if(IS_ABSOLUTE "${path}") + set(path_in "${path}") + + string(FIND "${path}" "${PROJECT_SOURCE_DIR}/" src_idx) + string(FIND "${path}" "${PROJECT_BINARY_DIR}/" dest_idx) + + if(src_idx EQUAL "0") + set(is_in_source_dir TRUE) + elseif(dest_idx EQUAL "0") + set(is_in_build_dir TRUE) + endif() + else() + # We consider relative paths to be relative to the current source dir. + set(is_in_source_dir TRUE) + set(path_in "${CMAKE_CURRENT_SOURCE_DIR}/${path}") + endif() + + # Resolve any .. and replace the absolute path with a path relative to the source dir + # or build dir, prefixed with a root marker. + get_filename_component(path_in_real "${path_in}" REALPATH) + + # Replace the absolute prefixes with markers. + if(is_in_source_dir) + set(handled TRUE) + set(marker "/src_dir") + string(REPLACE "${PROJECT_SOURCE_DIR}/" "${marker}/${repo_project_name}/" + path_out "${path_in_real}") + elseif(is_in_build_dir) + set(handled TRUE) + set(marker "/build_dir") + string(REPLACE "${PROJECT_BINARY_DIR}/" "${marker}/${repo_project_name}/" + path_out "${path_in_real}") + else() + # If it's not a source dir or a build dir, it might be some kind of weird genex + # or marker that we don't handle yet. + set(path_out "${path_in_real}") + endif() + endif() + + set(${out_var} "${path_out}" PARENT_SCOPE) + if(arg_OUT_SUCCESS) + set(${arg_OUT_SUCCESS} "${handled}" PARENT_SCOPE) + endif() +endfunction() + +# Collect source file "generated from" relationship comments for a given target file. +function(_qt_internal_sbom_add_target_source_files target spdx_id out_relationships) + get_target_property(sources ${target} SOURCES) + if(NOT sources) + set(sources "") + endif() + list(REMOVE_DUPLICATES sources) + + set(sources_option "") + if(sources) + set(sources_option SOURCES ${sources}) + endif() + + _qt_internal_sbom_add_source_files( + ${sources_option} + SPDX_ID "${spdx_id}" + OUT_RELATIONSHIPS_VAR relationships + ) + + set(${out_relationships} "${relationships}" PARENT_SCOPE) +endfunction() + +# Collect source file "generated from" relationship comments for the given sources. +function(_qt_internal_sbom_add_source_files) + set(opt_args "") + set(single_args + SPDX_ID + OUT_RELATIONSHIPS_VAR + ) + set(multi_args + SOURCES + ) + cmake_parse_arguments(PARSE_ARGV 0 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() + + if(NOT arg_OUT_RELATIONSHIPS_VAR) + message(FATAL_ERROR "OUT_RELATIONSHIPS_VAR must be set") + endif() + + if(NOT arg_SOURCES) + set(sources "") + else() + set(sources "${arg_SOURCES}") + endif() + + set(relationships "") + + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + + foreach(source IN LISTS sources) + # Filter out $. + if(source MATCHES "^\\$$") + continue() + endif() + + # Filter out prl files. + if(source MATCHES "\.prl$") + continue() + endif() + + # Filter out pkg-config. pc files. + if(source MATCHES "\.pc$") + continue() + endif() + + # Filter out metatypes .json.gen files. + if(source MATCHES "\.json\.gen$") + continue() + endif() + + _qt_internal_sbom_map_path_to_reproducible_relative_path(source_path + PATH "${source}" + REPO_PROJECT_NAME_LOWERCASE "${repo_project_name_lowercase}" + ) + + set(source_entry +"${spdx_id} GENERATED_FROM NOASSERTION\nRelationshipComment: ${source_path}" + ) + 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(${arg_OUT_RELATIONSHIPS_VAR} "${relationships}" 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 +# file handling functions. +# 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 + ) + + list(APPEND single_args "${single_args_to_process}") + + # We need to process multi config args even in a single config build, because there might be API + # calls that specify them directly. Use a default set of multi config configs. + set(configs Release RelWithDebInfo MinSizeRel Debug) + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config AND CMAKE_CONFIGURATION_TYPES) + list(APPEND configs ${CMAKE_CONFIGURATION_TYPES}) + endif() + + list(REMOVE_DUPLICATES configs) + + 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() + + 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) + + # Prefer the multi config option if it is set. + 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}") + endif() + + if(NOT is_multi_config OR NOT DEFINED ${outer_scope_var_name}) + 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() + +# 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() diff --git a/cmake/QtPublicSbomGenerationHelpers.cmake b/cmake/QtPublicSbomGenerationHelpers.cmake index bcd16b1e812..b78b30593f5 100644 --- a/cmake/QtPublicSbomGenerationHelpers.cmake +++ b/cmake/QtPublicSbomGenerationHelpers.cmake @@ -256,94 +256,6 @@ Relationship: SPDXRef-DOCUMENT DESCRIBES ${project_spdx_id} set_property(GLOBAL PROPERTY _qt_sbom_relationship_counter 0) endfunction() -# Handles the look up of Python, Python spdx dependencies and other various post-installation steps -# like NTIA validation, auditing, json generation, etc. -function(_qt_internal_sbom_setup_project_ops_generation) - set(opt_args - GENERATE_JSON - GENERATE_JSON_REQUIRED - GENERATE_SOURCE_SBOM - VERIFY_SBOM - VERIFY_SBOM_REQUIRED - VERIFY_NTIA_COMPLIANT - LINT_SOURCE_SBOM - LINT_SOURCE_SBOM_NO_ERROR - 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) - - if(arg_GENERATE_JSON AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) - set(op_args - OP_KEY "GENERATE_JSON" - OUT_VAR_DEPS_FOUND deps_found - ) - if(arg_GENERATE_JSON_REQUIRED) - list(APPEND op_args REQUIRED) - endif() - - _qt_internal_sbom_find_and_handle_sbom_op_dependencies(${op_args}) - if(deps_found) - _qt_internal_sbom_generate_json() - endif() - endif() - - if(arg_VERIFY_SBOM AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) - set(op_args - OP_KEY "VERIFY_SBOM" - OUT_VAR_DEPS_FOUND deps_found - ) - if(arg_VERIFY_SBOM_REQUIRED) - list(APPEND op_args REQUIRED) - endif() - - _qt_internal_sbom_find_and_handle_sbom_op_dependencies(${op_args}) - if(deps_found) - _qt_internal_sbom_verify_valid() - endif() - endif() - - if(arg_VERIFY_NTIA_COMPLIANT AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) - _qt_internal_sbom_find_and_handle_sbom_op_dependencies(REQUIRED OP_KEY "RUN_NTIA") - _qt_internal_sbom_verify_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() - - if(arg_GENERATE_SOURCE_SBOM AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) - _qt_internal_sbom_find_python_dependency_program(NAME reuse REQUIRED) - _qt_internal_sbom_generate_reuse_source_sbom() - endif() - - if(arg_LINT_SOURCE_SBOM AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) - set(lint_no_error_option "") - if(arg_LINT_SOURCE_SBOM_NO_ERROR) - set(lint_no_error_option NO_ERROR) - endif() - _qt_internal_sbom_find_python_dependency_program(NAME reuse REQUIRED) - _qt_internal_sbom_run_reuse_lint( - ${lint_no_error_option} - BUILD_TIME_SCRIPT_PATH_OUT_VAR reuse_lint_script - ) - endif() -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. @@ -1255,738 +1167,4 @@ function(_qt_internal_sbom_get_and_check_spdx_id) set(${arg_VARIABLE} "${id}" PARENT_SCOPE) endfunction() -# Helper to find a python interpreter and a specific python dependency, e.g. to be able to generate -# a SPDX JSON SBOM, or run post-installation steps like NTIA verification. -# The exact dependency should be specified as the OP_KEY. -# -# Caches the found python executable in a separate cache var QT_INTERNAL_SBOM_PYTHON_EXECUTABLE, to -# avoid conflicts with any other found python interpreter. -function(_qt_internal_sbom_find_and_handle_sbom_op_dependencies) - set(opt_args - REQUIRED - ) - set(single_args - OP_KEY - OUT_VAR_DEPS_FOUND - ) - 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_OP_KEY) - message(FATAL_ERROR "OP_KEY is required") - endif() - - set(supported_ops "GENERATE_JSON" "VERIFY_SBOM" "RUN_NTIA") - - if(arg_OP_KEY STREQUAL "GENERATE_JSON" OR arg_OP_KEY STREQUAL "VERIFY_SBOM") - set(import_statement "import spdx_tools.spdx.clitools.pyspdxtools") - elseif(arg_OP_KEY STREQUAL "RUN_NTIA") - set(import_statement "import ntia_conformance_checker.main") - else() - message(FATAL_ERROR "OP_KEY must be one of ${supported_ops}") - endif() - - # Return early if we found the dependencies. - if(QT_INTERNAL_SBOM_DEPS_FOUND_FOR_${arg_OP_KEY}) - if(arg_OUT_VAR_DEPS_FOUND) - set(${arg_OUT_VAR_DEPS_FOUND} TRUE PARENT_SCOPE) - endif() - return() - endif() - - # NTIA-compliance checker requires Python 3.9 or later, so we use it as the minimum for all - # SBOM OPs. - set(required_version "3.9") - - set(python_common_args - VERSION "${required_version}" - ) - - set(everything_found FALSE) - - # On macOS FindPython prefers looking in the system framework location, but that usually would - # not have the required dependencies. So we first look in it, and then fallback to any other - # non-framework python found. - if(CMAKE_HOST_APPLE) - set(extra_python_args SEARCH_IN_FRAMEWORKS QUIET) - _qt_internal_sbom_find_python_and_dependency_helper_lambda() - endif() - - if(NOT everything_found) - set(extra_python_args QUIET) - _qt_internal_sbom_find_python_and_dependency_helper_lambda() - endif() - - if(NOT everything_found) - if(arg_REQUIRED) - set(message_type "FATAL_ERROR") - else() - set(message_type "DEBUG") - endif() - - if(NOT python_found) - # Look for python one more time, this time without QUIET, to show an error why it - # wasn't found. - if(arg_REQUIRED) - _qt_internal_sbom_find_python_helper(${python_common_args} - OUT_VAR_PYTHON_PATH unused_python - OUT_VAR_PYTHON_FOUND unused_found - ) - endif() - message(${message_type} "Python ${required_version} for running SBOM ops not found.") - elseif(NOT dep_found) - message(${message_type} "Python dependency for running SBOM op ${arg_OP_KEY} " - "not found:\n Python: ${python_path} \n Output: \n${dep_find_output}") - endif() - else() - message(DEBUG "Using Python ${python_path} for running SBOM ops.") - - if(NOT QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) - set(QT_INTERNAL_SBOM_PYTHON_EXECUTABLE "${python_path}" CACHE INTERNAL - "Python interpeter used for SBOM generation.") - endif() - - set(QT_INTERNAL_SBOM_DEPS_FOUND_FOR_${arg_OP_KEY} "TRUE" CACHE INTERNAL - "All dependencies found to run SBOM OP ${arg_OP_KEY}") - endif() - - if(arg_OUT_VAR_DEPS_FOUND) - set(${arg_OUT_VAR_DEPS_FOUND} "${QT_INTERNAL_SBOM_DEPS_FOUND_FOR_${arg_OP_KEY}}" - PARENT_SCOPE) - endif() -endfunction() - -# Helper macro to find python and a given dependency. Expects the caller to set all of the vars. -# Meant to reduce the line noise due to the repeated calls. -macro(_qt_internal_sbom_find_python_and_dependency_helper_lambda) - _qt_internal_sbom_find_python_and_dependency_helper( - PYTHON_ARGS - ${extra_python_args} - ${python_common_args} - DEPENDENCY_ARGS - DEPENDENCY_IMPORT_STATEMENT "${import_statement}" - OUT_VAR_PYTHON_PATH python_path - OUT_VAR_PYTHON_FOUND python_found - OUT_VAR_DEP_FOUND dep_found - OUT_VAR_PYTHON_AND_DEP_FOUND everything_found - OUT_VAR_DEP_FIND_OUTPUT dep_find_output - ) -endmacro() - -# Tries to find python and a given dependency based on the args passed to PYTHON_ARGS and -# DEPENDENCY_ARGS which are forwarded to the respective finding functions. -# Returns the path to the python interpreter, whether it was found, whether the dependency was -# found, whether both were found, and the reason why the dependency might not be found. -function(_qt_internal_sbom_find_python_and_dependency_helper) - set(opt_args) - set(single_args - OUT_VAR_PYTHON_PATH - OUT_VAR_PYTHON_FOUND - OUT_VAR_DEP_FOUND - OUT_VAR_PYTHON_AND_DEP_FOUND - OUT_VAR_DEP_FIND_OUTPUT - ) - set(multi_args - PYTHON_ARGS - DEPENDENCY_ARGS - ) - cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") - _qt_internal_validate_all_args_are_parsed(arg) - - set(everything_found_inner FALSE) - set(deps_find_output_inner "") - - if(NOT arg_OUT_VAR_PYTHON_PATH) - message(FATAL_ERROR "OUT_VAR_PYTHON_PATH var is required") - endif() - - if(NOT arg_OUT_VAR_PYTHON_FOUND) - message(FATAL_ERROR "OUT_VAR_PYTHON_FOUND var is required") - endif() - - if(NOT arg_OUT_VAR_DEP_FOUND) - message(FATAL_ERROR "OUT_VAR_DEP_FOUND var is required") - endif() - - if(NOT arg_OUT_VAR_PYTHON_AND_DEP_FOUND) - message(FATAL_ERROR "OUT_VAR_PYTHON_AND_DEP_FOUND var is required") - endif() - - if(NOT arg_OUT_VAR_DEP_FIND_OUTPUT) - message(FATAL_ERROR "OUT_VAR_DEP_FIND_OUTPUT var is required") - endif() - - _qt_internal_sbom_find_python_helper( - ${arg_PYTHON_ARGS} - OUT_VAR_PYTHON_PATH python_path_inner - OUT_VAR_PYTHON_FOUND python_found_inner - ) - - if(python_found_inner AND python_path_inner) - _qt_internal_sbom_find_python_dependency_helper( - ${arg_DEPENDENCY_ARGS} - PYTHON_PATH "${python_path_inner}" - OUT_VAR_FOUND dep_found_inner - OUT_VAR_OUTPUT dep_find_output_inner - ) - - if(dep_found_inner) - set(everything_found_inner TRUE) - endif() - endif() - - set(${arg_OUT_VAR_PYTHON_PATH} "${python_path_inner}" PARENT_SCOPE) - set(${arg_OUT_VAR_PYTHON_FOUND} "${python_found_inner}" PARENT_SCOPE) - set(${arg_OUT_VAR_DEP_FOUND} "${dep_found_inner}" PARENT_SCOPE) - set(${arg_OUT_VAR_PYTHON_AND_DEP_FOUND} "${everything_found_inner}" PARENT_SCOPE) - set(${arg_OUT_VAR_DEP_FIND_OUTPUT} "${dep_find_output_inner}" PARENT_SCOPE) -endfunction() - -# Tries to find the python intrepreter, given the QT_SBOM_PYTHON_INTERP path hint, as well as -# other options. -# Ignores any previously found python. -# Returns the python interpreter path and whether it was successfully found. -# -# This is intentionally a function, and not a macro, to prevent overriding the Python3_EXECUTABLE -# non-cache variable in a global scope in case if a different python is found and used for a -# different purpose (e.g. qtwebengine or qtinterfaceframework). -# The reason to use a different python is that an already found python might not be the version we -# need, or might lack the dependencies we need. -# https://gitlab.kitware.com/cmake/cmake/-/issues/21797#note_901621 claims that finding multiple -# python versions in separate directory scopes is possible, and I claim a function scope is as -# good as a directory scope. -function(_qt_internal_sbom_find_python_helper) - set(opt_args - SEARCH_IN_FRAMEWORKS - QUIET - ) - set(single_args - VERSION - OUT_VAR_PYTHON_PATH - OUT_VAR_PYTHON_FOUND - ) - 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_OUT_VAR_PYTHON_PATH) - message(FATAL_ERROR "OUT_VAR_PYTHON_PATH var is required") - endif() - - if(NOT arg_OUT_VAR_PYTHON_FOUND) - message(FATAL_ERROR "OUT_VAR_PYTHON_FOUND var is required") - endif() - - # Allow disabling looking for a python interpreter shipped as part of a macOS system framework. - if(NOT arg_SEARCH_IN_FRAMEWORKS) - set(Python3_FIND_FRAMEWORK NEVER) - endif() - - set(required_version "") - if(arg_VERSION) - set(required_version "${arg_VERSION}") - endif() - - set(find_quiet "") - if(arg_QUIET) - set(find_quiet "QUIET") - endif() - - # Locally reset any executable that was possibly already found. - # We do this to ensure we always re-do the lookup/ - # This needs to be set to an empty string, to override any cache variable - set(Python3_EXECUTABLE "") - - # This needs to be unset, because the Python module checks whether the variable is defined, not - # whether it is empty. - unset(_Python3_EXECUTABLE) - - if(QT_SBOM_PYTHON_INTERP) - set(Python3_ROOT_DIR ${QT_SBOM_PYTHON_INTERP}) - endif() - - find_package(Python3 ${required_version} ${find_quiet} COMPONENTS Interpreter) - - set(${arg_OUT_VAR_PYTHON_PATH} "${Python3_EXECUTABLE}" PARENT_SCOPE) - set(${arg_OUT_VAR_PYTHON_FOUND} "${Python3_Interpreter_FOUND}" PARENT_SCOPE) -endfunction() - -# Helper that takes an python import statement to run using the given python interpreter path, -# to confirm that the given python dependency can be found. -# Returns whether the dependency was found and the output of running the import, for error handling. -function(_qt_internal_sbom_find_python_dependency_helper) - set(opt_args "") - set(single_args - DEPENDENCY_IMPORT_STATEMENT - PYTHON_PATH - OUT_VAR_FOUND - OUT_VAR_OUTPUT - ) - 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_PYTHON_PATH) - message(FATAL_ERROR "Python interpreter path not given.") - endif() - - if(NOT arg_DEPENDENCY_IMPORT_STATEMENT) - message(FATAL_ERROR "Python depdendency import statement not given.") - endif() - - if(NOT arg_OUT_VAR_FOUND) - message(FATAL_ERROR "Out var found variable not given.") - endif() - - set(python_path "${arg_PYTHON_PATH}") - execute_process( - COMMAND - ${python_path} -c "${arg_DEPENDENCY_IMPORT_STATEMENT}" - RESULT_VARIABLE res - OUTPUT_VARIABLE output - ERROR_VARIABLE output - ) - - if("${res}" STREQUAL "0") - set(found TRUE) - set(output "${output}") - else() - set(found FALSE) - string(CONCAT output "SBOM Python dependency ${arg_DEPENDENCY_IMPORT_STATEMENT} not found. " - "Error:\n${output}") - endif() - - set(${arg_OUT_VAR_FOUND} "${found}" PARENT_SCOPE) - if(arg_OUT_VAR_OUTPUT) - set(${arg_OUT_VAR_OUTPUT} "${output}" PARENT_SCOPE) - 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}") - - set(hints "") - - # The path to python installed apps is different on Windows compared to UNIX, so we use - # a different path than where the python interpreter might be located. - if(QT_SBOM_PYTHON_APPS_PATH) - list(APPEND hints ${QT_SBOM_PYTHON_APPS_PATH}) - endif() - - find_program(${cache_var} - NAMES ${program_name} - HINTS ${hints} - ) - - 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 SPDX JSON file from a tag/value format file. -# This also implies some additional validity checks, useful to ensure a proper sbom file. -function(_qt_internal_sbom_generate_json) - if(NOT QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) - message(FATAL_ERROR "Python interpreter not found for generating SBOM json file.") - endif() - if(NOT QT_INTERNAL_SBOM_DEPS_FOUND_FOR_GENERATE_JSON) - message(FATAL_ERROR "Python dependencies not found for generating SBOM json file.") - endif() - - set(content " - message(STATUS \"Generating JSON: \${QT_SBOM_OUTPUT_PATH}.json\") - execute_process( - COMMAND ${QT_INTERNAL_SBOM_PYTHON_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 generate a tag/value SPDX file from a SPDX JSON format file. -# -# Will be used by WebEngine to convert the Chromium JSON file to a tag/value SPDX file. -# -# This conversion needs to happen before the document is referenced in the SBOM generation process, -# so that the file already exists when it is parsed for its unique id and namespace. -# It also needs to happen before verification codes are computed for the current document -# that will depend on the target one, to ensure the the file exists and its checksum can be -# computed. -# -# OPERATION_ID - a unique id for the operation, used to generate a unique cmake file name for -# the SBOM generation process. -# -# INPUT_JSON_PATH - the absolute path to the input JSON file. -# -# OUTPUT_FILE_PATH - the absolute path where to create the output tag/value SPDX file. -# Note that if the output file path is set, it is up to the caller to also copy / install the file -# into the build and install directories where the build system expects to find all external -# document references. -# -# OUTPUT_FILE_NAME - when OUTPUT_FILE_PATH is not specified, the output directory is automatically -# set to the SBOM output directory. In this case OUTPUT_FILE_NAME can be used to override the -# outout file name. If not specified, it will be derived from the input file name. -# -# OUT_VAR_OUTPUT_FILE_NAME - output variable where to store the output file. -# -# OUT_VAR_OUTPUT_ABSOLUTE_FILE_PATH - output variable where to store the output file path. -# Note that the path will contain an unresolved '${QT_SBOM_OUTPUT_DIR}' which only has a value at -# install time. So the path can't be used sensibly during configure time. -function(_qt_internal_sbom_generate_tag_value_spdx_document) - if(NOT QT_GENERATE_SBOM) - return() - endif() - - set(opt_args "") - set(single_args - OPERATION_ID - INPUT_JSON_FILE_PATH - OUTPUT_FILE_PATH - OUTPUT_FILE_NAME - OUT_VAR_OUTPUT_FILE_NAME - OUT_VAR_OUTPUT_ABSOLUTE_FILE_PATH - ) - 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 QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) - message(FATAL_ERROR "Python interpreter not found for generating tag/value file from JSON.") - endif() - if(NOT QT_INTERNAL_SBOM_DEPS_FOUND_FOR_GENERATE_JSON) - message(FATAL_ERROR - "Python dependencies not found for generating tag/value file from JSON.") - endif() - - if(NOT arg_OPERATION_ID) - message(FATAL_ERROR "OPERATION_ID is required") - endif() - - if(NOT arg_INPUT_JSON_FILE_PATH) - message(FATAL_ERROR "INPUT_JSON_FILE_PATH is required") - endif() - - if(arg_OUTPUT_FILE_PATH) - set(output_path "${arg_OUTPUT_FILE_PATH}") - else() - if(arg_OUTPUT_FILE_NAME) - set(output_name "${arg_OUTPUT_FILE_NAME}") - else() - # Use the input file name without the last extension (without .json) as the output name. - get_filename_component(output_name "${arg_INPUT_JSON_FILE_PATH}" NAME_WLE) - endif() - set(output_path "\${QT_SBOM_OUTPUT_DIR}/${output_name}") - endif() - - if(arg_OUT_VAR_OUTPUT_FILE_NAME) - get_filename_component(output_name_resolved "${output_path}" NAME) - set(${arg_OUT_VAR_OUTPUT_FILE_NAME} "${output_name_resolved}" PARENT_SCOPE) - endif() - - if(arg_OUT_VAR_OUTPUT_ABSOLUTE_FILE_PATH) - set(${arg_OUT_VAR_OUTPUT_ABSOLUTE_FILE_PATH} "${output_path}" PARENT_SCOPE) - endif() - - set(content " - message(STATUS - \"Generating tag/value SPDX document: ${output_path} from \" - \"${arg_INPUT_JSON_FILE_PATH}\") - execute_process( - COMMAND ${QT_INTERNAL_SBOM_PYTHON_EXECUTABLE} -m spdx_tools.spdx.clitools.pyspdxtools - -i \"${arg_INPUT_JSON_FILE_PATH}\" -o \"${output_path}\" - RESULT_VARIABLE res - ) - if(NOT res EQUAL 0) - message(FATAL_ERROR \"SBOM conversion to tag/value failed: \${res}\") - endif() -") - - _qt_internal_get_current_project_sbom_dir(sbom_dir) - set(convert_sbom "${sbom_dir}/convert_to_tag_value_${arg_OPERATION_ID}.cmake") - file(GENERATE OUTPUT "${convert_sbom}" CONTENT "${content}") - - set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_include_files - "${convert_sbom}") -endfunction() - -# Helper to verify the generated sbom is valid. -function(_qt_internal_sbom_verify_valid) - if(NOT QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) - message(FATAL_ERROR "Python interpreter not found for verifying SBOM file.") - endif() - - if(NOT QT_INTERNAL_SBOM_DEPS_FOUND_FOR_VERIFY_SBOM) - message(FATAL_ERROR "Python dependencies not found for verifying SBOM file") - endif() - - set(content " - message(STATUS \"Verifying: \${QT_SBOM_OUTPUT_PATH}\") - execute_process( - COMMAND ${QT_INTERNAL_SBOM_PYTHON_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() -") - - _qt_internal_get_current_project_sbom_dir(sbom_dir) - set(verify_sbom "${sbom_dir}/verify_valid.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 NTIA compliant. -function(_qt_internal_sbom_verify_ntia_compliant) - if(NOT QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) - message(FATAL_ERROR "Python interpreter not found for verifying SBOM file.") - endif() - - if(NOT QT_INTERNAL_SBOM_DEPS_FOUND_FOR_RUN_NTIA) - message(FATAL_ERROR "Python dependencies not found for running the SBOM NTIA checker.") - endif() - - set(content " - message(STATUS \"Checking for NTIA compliance: \${QT_SBOM_OUTPUT_PATH}\") - execute_process( - COMMAND ${QT_INTERNAL_SBOM_PYTHON_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_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(extra_code_begin "") - if(DEFINED ENV{COIN_UNIQUE_JOB_ID}) - # The output of the process dynamically adjusts the width of the shown table based on the - # console width. In the CI, the width is very short for some reason, and thus the output - # is truncated in the CI log. Explicitly set a bigger width to avoid this. - set(extra_code_begin " -set(backup_env_columns \$ENV{COLUMNS}) -set(ENV{COLUMNS} 150) -") -set(extra_code_end " -set(ENV{COLUMNS} \${backup_env_columns}) -") - endif() - - set(content " - message(STATUS \"Showing main SBOM document info: \${QT_SBOM_OUTPUT_PATH}\") - - ${extra_code_begin} - execute_process( - COMMAND ${QT_SBOM_PROGRAM_SBOM2DOC} -i \"\${QT_SBOM_OUTPUT_PATH}\" - RESULT_VARIABLE res - ) - ${extra_code_end} - 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 ${QT_SBOM_PROGRAM_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() - -# Returns path to project's potential root source reuse.toml file. -function(_qt_internal_sbom_get_project_reuse_toml_path out_var) - set(reuse_toml_path "${PROJECT_SOURCE_DIR}/REUSE.toml") - set(${out_var} "${reuse_toml_path}" PARENT_SCOPE) -endfunction() - -# Helper to generate and install a source SBOM using reuse. -function(_qt_internal_sbom_generate_reuse_source_sbom) - 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) - - _qt_internal_get_current_project_sbom_dir(sbom_dir) - set(file_op "${sbom_dir}/generate_reuse_source_sbom.cmake") - - _qt_internal_sbom_get_project_reuse_toml_path(reuse_toml_path) - if(NOT EXISTS "${reuse_toml_path}" AND NOT QT_FORCE_SOURCE_SBOM_GENERATION) - set(skip_message - "Skipping source SBOM generation: No reuse.toml file found at '${reuse_toml_path}'.") - message(STATUS "${skip_message}") - - set(content " - message(STATUS \"${skip_message}\") -") - - file(GENERATE OUTPUT "${file_op}" CONTENT "${content}") - set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_post_generation_include_files - "${file_op}") - return() - endif() - - set(handle_error "") - if(NOT arg_NO_ERROR) - set(handle_error " - if(NOT res EQUAL 0) - message(FATAL_ERROR \"Source SBOM generation using reuse tool failed: \${res}\") - endif() -") - endif() - - set(source_sbom_path "\${QT_SBOM_OUTPUT_PATH_WITHOUT_EXT}.source.spdx") - - set(content " - message(STATUS \"Generating source SBOM using reuse tool: ${source_sbom_path}\") - execute_process( - COMMAND ${QT_SBOM_PROGRAM_REUSE} --root \"${PROJECT_SOURCE_DIR}\" spdx - -o ${source_sbom_path} - RESULT_VARIABLE res - ) - ${handle_error} -") - - file(GENERATE OUTPUT "${file_op}" CONTENT "${content}") - - set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_post_generation_include_files "${file_op}") -endfunction() - -# Helper to run 'reuse lint' on the project source dir. -function(_qt_internal_sbom_run_reuse_lint) - set(opt_args - NO_ERROR - ) - set(single_args - BUILD_TIME_SCRIPT_PATH_OUT_VAR - ) - 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 no reuse.toml file exists, it means the repo is likely not reuse compliant yet, - # so we shouldn't error out during installation when running the lint. - _qt_internal_sbom_get_project_reuse_toml_path(reuse_toml_path) - if(NOT EXISTS "${reuse_toml_path}" AND NOT QT_FORCE_REUSE_LINT_ERROR) - set(arg_NO_ERROR TRUE) - endif() - - set(handle_error "") - if(NOT arg_NO_ERROR) - set(handle_error " - if(NOT res EQUAL 0) - message(FATAL_ERROR \"Running 'reuse lint' failed: \${res}\") - endif() -") - endif() - - set(content " - message(STATUS \"Running 'reuse lint' in '${PROJECT_SOURCE_DIR}'.\") - execute_process( - COMMAND ${QT_SBOM_PROGRAM_REUSE} --root \"${PROJECT_SOURCE_DIR}\" lint - RESULT_VARIABLE res - ) - ${handle_error} -") - - _qt_internal_get_current_project_sbom_dir(sbom_dir) - set(file_op_build "${sbom_dir}/run_reuse_lint_build.cmake") - file(GENERATE OUTPUT "${file_op_build}" CONTENT "${content}") - - # Allow skipping running 'reuse lint' during installation. But still allow running it during - # build time. This is a fail safe opt-out in case some repo needs it. - if(QT_FORCE_SKIP_REUSE_LINT_ON_INSTALL) - set(skip_message "Skipping running 'reuse lint' in '${PROJECT_SOURCE_DIR}'.") - - set(content " - message(STATUS \"${skip_message}\") -") - set(file_op_install "${sbom_dir}/run_reuse_lint_install.cmake") - file(GENERATE OUTPUT "${file_op_install}" CONTENT "${content}") - else() - # Just reuse the already generated script for installation as well. - set(file_op_install "${file_op_build}") - endif() - - set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_verify_include_files "${file_op_install}") - - if(arg_BUILD_TIME_SCRIPT_PATH_OUT_VAR) - set(${arg_BUILD_TIME_SCRIPT_PATH_OUT_VAR} "${file_op_build}" PARENT_SCOPE) - endif() -endfunction() diff --git a/cmake/QtPublicSbomHelpers.cmake b/cmake/QtPublicSbomHelpers.cmake index 4edb67624c1..82685935eac 100644 --- a/cmake/QtPublicSbomHelpers.cmake +++ b/cmake/QtPublicSbomHelpers.cmake @@ -1036,1420 +1036,6 @@ function(_qt_internal_sbom_add_target target) ) 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) - set(install_prefixes "") - - get_cmake_property(install_prefix _qt_internal_sbom_install_prefix) - list(APPEND install_prefixes "${install_prefix}") - - set(external_document "${relative_installed_repo_document_path}") - - _qt_internal_sbom_generate_add_external_reference( - EXTERNAL_DOCUMENT_FILE_PATH "${external_document}" - EXTERNAL_DOCUMENT_INSTALL_PREFIXES ${install_prefixes} - EXTERNAL_DOCUMENT_SPDX_ID "${external_document_ref}" - ) - - 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 - QT_TRANSLATIONS - QT_RESOURCES - QT_CUSTOM - QT_CUSTOM_NO_INFIX - - # 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 - TRANSLATIONS - RESOURCES - CUSTOM - CUSTOM_NO_INFIX - ) - - if(NOT arg_TYPE IN_LIST supported_types) - message(FATAL_ERROR "Unsupported target TYPE for SBOM creation: ${arg_TYPE}") - endif() - - set(types_without_binary_files - QT_THIRD_PARTY_SOURCES - QT_TRANSLATIONS - QT_RESOURCES - QT_CUSTOM - QT_CUSTOM_NO_INFIX - SYSTEM_LIBRARY - THIRD_PARTY_LIBRARY - TRANSLATIONS - RESOURCES - CUSTOM - CUSTOM_NO_INFIX - ) - - get_target_property(target_type ${target} TYPE) - - if(arg_TYPE IN_LIST types_without_binary_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() - - get_target_property(excluded ${target} _qt_internal_excluded_from_default_target) - if(excluded) - message(DEBUG "Target ${target} has no binary files to reference in the SBOM " - "because it was excluded from the default 'all' target.") - 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() - - 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") - - set(valid_executable_types - "EXECUTABLE" - ) - if(ANDROID) - list(APPEND valid_executable_types "MODULE_LIBRARY") - endif() - if(NOT target_type IN_LIST valid_executable_types) - 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. -# TODO: Consider merging the common parts with _qt_internal_sbom_add_custom_file somehow. -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(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_target_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() - -# Add a list of to-be-installed files that should appear in the files section of the target's -# SBOM document. -# Supports multiple calls with the same target name. -# Each call is handled as a separate set of files. -# For options that can be passed, see the doc-comment of -# _qt_internal_sbom_handle_target_custom_file_set. -function(_qt_internal_sbom_add_files target) - if(NOT QT_GENERATE_SBOM) - return() - endif() - - if(NOT TARGET "${target}") - message(FATAL_ERROR "The target ${target} does not exist.") - endif() - - get_target_property(file_sets_count "${target}" _qt_sbom_custom_file_sets_count) - if(NOT file_sets_count) - set(file_sets_count 0) - endif() - - set_property(TARGET "${target}" - APPEND PROPERTY _qt_sbom_custom_file_set_${file_sets_count} "${ARGN}") - - math(EXPR file_sets_count "${file_sets_count}+1") - set_property(TARGET "${target}" PROPERTY _qt_sbom_custom_file_sets_count "${file_sets_count}") -endfunction() - -# Handles addition of custom file SPDX entries for a given target, by processing all the collected -# file sets so far. -# Applies the passed license and copyright info to all collected files. -# -# Options that can be passed. -# -# NO_INSTALL - if set, custom file processing is skipped, because the files will not be installed. -# -# PACKAGE_TYPE - the type of the package that the files belong to, is used to compute an infix -# for the file spdx id. -# -# PACKAGE_SPDX_ID - the package spdx id is used to add a relationship between the package and file. -# -# LICENSE_EXPRESSION - a license expression to apply to the files. -# -# INSTALL_PREFIX - the install prefix for the files, this is usually the install prefix of qt. -# -# COPYRIGHTS - a list of copyright strings to apply to the files. -function(_qt_internal_sbom_handle_target_custom_files target) - set(opt_args - NO_INSTALL - ) - set(single_args - PACKAGE_TYPE - PACKAGE_SPDX_ID - LICENSE_EXPRESSION - INSTALL_PREFIX - ) - set(multi_args - COPYRIGHTS - ) - - cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") - _qt_internal_validate_all_args_are_parsed(arg) - - # Nothing to process if no file sets were defined. - get_target_property(file_sets_count "${target}" _qt_sbom_custom_file_sets_count) - if(NOT file_sets_count) - return() - endif() - - if(arg_NO_INSTALL) - message(DEBUG "Skipping sbom custom file processing for ${target} because NO_INSTALL is " - "set") - return() - endif() - - if(NOT arg_PACKAGE_SPDX_ID) - message(FATAL_ERROR "PACKAGE_SPDX_ID must be set") - endif() - - _qt_internal_forward_function_args( - FORWARD_PREFIX arg - FORWARD_OUT_VAR file_common_options - FORWARD_SINGLE - PACKAGE_TYPE - PACKAGE_SPDX_ID - LICENSE_EXPRESSION - INSTALL_PREFIX - FORWARD_MULTI - COPYRIGHTS - ) - - # Subtract -1, because foreach(RANGE is inclusive). - math(EXPR file_sets_count "${file_sets_count}-1") - foreach(file_set_index RANGE ${file_sets_count}) - get_target_property(file_set_args "${target}" _qt_sbom_custom_file_set_${file_set_index}) - - if(NOT file_set_args) - message(FATAL_ERROR "No arguments were specified for SBOM custom file set for " - "${target}") - endif() - - _qt_internal_sbom_handle_target_custom_file_set("${target}" - ${file_common_options} ${file_set_args}) - endforeach() -endfunction() - -# Processes a file set of custom files for which to include SBOM info. -# The -# NO_INSTALL -# PACKAGE_TYPE -# PACKAGE_SPDX_ID -# LICENSE_EXPRESSION -# INSTALL_PREFIX -# COPYRIGHTS -# options are the same as in _qt_internal_sbom_handle_target_custom_files, and are actually -# forwarded from that function, because they might be set at the target level. -# -# In addition, more options can be passed when called via qt_internal_sbom_add_custom_files: -# -# They are: -# -# FILE_TYPE - the type of each provided file. Supported types can be found in the implementation of -# _qt_internal_sbom_get_spdx_v2_3_file_type_for_file(). -# Some examples are QT_TRANSLATION, QT_TRANSLATIONS_CATALOG, QT_RESOURCE. -# -# FILES - a list of file paths to include in the SBOM. Only the file name is currently used. -# -# SOURCE_FILES - which source files were used to generate the custom files. All source files apply -# to each input file. -# -# SOURCE_FILES_PER_INPUT_FILE - for each index i in FILES, the corresponding source files are -# in SOURCE_FILES[i], so that each input gets exactly one source file. -# This is provided as an option, to prevent performance overhead from having to add a -# custom file set for each new source file, when dealing with translations that have a -# 1-to-1 ts->qm relationship. -# -# There is also a set of multi config aware options that can be set, like -# INSTALL_PATH -# INSTALL_PATH_ -# which should be the relative install dir path where the -# files will be installed, relative to $ENV{DESTDIR}/${CMAKE_INSTALL_PREFIX}. -function(_qt_internal_sbom_handle_target_custom_file_set target) - set(opt_args - "" - ) - set(single_args - FILE_TYPE - PACKAGE_SPDX_ID - PACKAGE_TYPE - LICENSE_EXPRESSION - INSTALL_PREFIX - ) - set(multi_args - COPYRIGHTS - FILES - SOURCE_FILES - SOURCE_FILES_PER_INPUT_FILE - ) - - # Don't explicitly forward the multi config single args, but still parse them. - # They will be accessed from the current scope directly. - set(single_args_without_multi_config_args "${single_args}") - _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) - - # No custom files to process. - if(NOT arg_FILES) - return() - endif() - - # Don't forward the FILES option. - set(multi_args_without_files "${multi_args}") - list(REMOVE_ITEM multi_args_without_files FILES) - - # Handle the case where we have one source file per input file. - if(arg_SOURCE_FILES_PER_INPUT_FILE) - list(LENGTH arg_FILES files_count) - list(LENGTH arg_SOURCE_FILES_PER_INPUT_FILE source_files_count) - if(NOT files_count EQUAL source_files_count) - message(FATAL_ERROR "The number of files passed to SOURCE_FILES must match the number" - "of files passed to SOURCE_FILES_PER_INPUT_FILE.") - endif() - - # Don't forward all of the source files, but rather one per input file. - list(REMOVE_ITEM multi_args_without_files SOURCE_FILES_PER_INPUT_FILE) - endif() - - _qt_internal_forward_function_args( - FORWARD_PREFIX arg - FORWARD_OUT_VAR forward_args - FORWARD_SINGLE - ${single_args_without_multi_config_args} - FORWARD_MULTI - ${multi_args_without_files} - ) - - set(file_index 0) - foreach(file_path IN LISTS arg_FILES) - set(per_file_forward_args "") - - # We don't currently use the file path for anything other than getting the file name, to - # embed it into the spdx document entry. - # What matters in the end is the location where the file is installed, which is handled - # by the PATH_KIND option. - get_filename_component(file_name "${file_path}" NAME) - - if(arg_SOURCE_FILES_PER_INPUT_FILE) - list(GET arg_SOURCE_FILES_PER_INPUT_FILE "${file_index}" source_file) - list(APPEND per_file_forward_args SOURCE_FILES "${source_file}") - endif() - - # The multi_config_single_args are deliberately not forwarded, but are available in this - # function scope, for direct access in the called function scope, because - # cmake_parse_arguments can't handle: - # PATH_KIND "INSTALL_PATH" - # INSTALL_PATH "/some_path" - # the parsing gets confused by what's the option and what's the value. - _qt_internal_sbom_handle_multi_config_custom_file(${target} - PATH_KIND "INSTALL_PATH" - PATH_SUFFIX "${file_name}" - OPTIONS - ${forward_args} - ${per_file_forward_args} - ) - math(EXPR file_index "${file_index}+1") - endforeach() -endfunction() - -# Helper function to add a custom 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. -# -# Expects the parent scope to contain the ${PATH_KIND} and ${PATH_KIND}_ variables. -# Examples are INSTALL_PATH or INSTALL_PATH_DEBUG. -# They can't be forwarded as options because of cmake_parse_arguments parsing issues when a value -# can be the same as a key. See comment in implementation of -#_qt_internal_sbom_handle_target_custom_file_set. -function(_qt_internal_sbom_handle_multi_config_custom_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_custom_file( - "${target}" - "${file_path}" - ${arg_OPTIONS} - ${is_optional} - CONFIG ${config} - ) - endforeach() -endfunction() - -# Adds one custom file with the given relative install path into the SBOM document. -# Will embed GENERATED_FROM source file relationships if a list of source files is specified. -function(_qt_internal_sbom_add_custom_file target installed_file_relative_path) - set(opt_args - OPTIONAL - ) - set(single_args - PACKAGE_SPDX_ID - PACKAGE_TYPE - LICENSE_EXPRESSION - INSTALL_PREFIX - FILE_TYPE - CONFIG - ) - set(multi_args - COPYRIGHTS - SOURCE_FILES - ) - - 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(spdx_id_suffix "${arg_CONFIG}") - set(config_to_install_option CONFIG ${arg_CONFIG}) - else() - set(spdx_id_suffix "") - set(config_to_install_option "") - endif() - - if(arg_FILE_TYPE) - _qt_internal_sbom_get_spdx_v2_3_file_type_for_file(file_type "${arg_FILE_TYPE}") - else() - set(file_type "OTHER") - endif() - - get_filename_component(file_name "${installed_file_relative_path}" NAME) - - _qt_internal_sbom_get_package_infix("${arg_PACKAGE_TYPE}" package_infix) - - _qt_internal_sbom_get_file_spdx_id( - "${package_infix}-${target}-${file_name}-${spdx_id_suffix}" spdx_id) - - # Add relationship from owning package. - set(relationships "${arg_PACKAGE_SPDX_ID} CONTAINS ${spdx_id}") - - # Add source file relationships from which the custom file was generated. - set(sources_option "") - - if(arg_SOURCE_FILES) - set(sources_option SOURCES ${arg_SOURCE_FILES}) - endif() - - _qt_internal_sbom_add_source_files( - ${sources_option} - SPDX_ID "${spdx_id}" - OUT_RELATIONSHIPS_VAR 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}") - - _qt_internal_sbom_generate_add_file( - FILENAME "${installed_file_relative_path}" - FILETYPE "${file_type}" ${optional} - SPDXID "${spdx_id}" - ${file_common_options} - ${config_to_install_option} - ${relationship_option} - ) -endfunction() - -# Maps an arbitrary file type to a spdx v2.3 file type. -# There is a list of known SPDX types, and custom Qt ones. -# Any other type is mapped to OTHER. -# The mapping might change when we start generating spdx v3.0 documents. -function(_qt_internal_sbom_get_spdx_v2_3_file_type_for_file out_var file_type_in) - set(spdx_v2_3_file_types - SOURCE - BINARY - ARCHIVE - APPLICATION - AUDIO - IMAGE - TEXT - VIDEO - DOCUMENTATION - SPDX - OTHER - ) - - # No semantic meaning at the moment, but we might want to map the values to something else - # when we port to SPDX v3.0+. - set(qt_file_types - QT_TRANSLATION - QT_TRANSLATIONS_CATALOG - QT_RESOURCE - TRANSLATION - RESOURCE - CUSTOM - ) - - if(file_type_in IN_LIST spdx_v2_3_file_types) - set(file_type "${file_type_in}") - elseif(file_type_in IN_LIST qt_file_types) - set(file_type OTHER) - else() - set(file_type OTHER) - endif() - - set(${out_var} "${file_type}" PARENT_SCOPE) -endfunction() - -# Takes a relative or absolute path and maps it to a reproducible path that is relative to -# the project source or build dir. -function(_qt_internal_sbom_map_path_to_reproducible_relative_path out_var) - set(opt_args "") - set(single_args - PATH - REPO_PROJECT_NAME_LOWERCASE - OUT_SUCCESS - ) - 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(is_in_source_dir FALSE) - set(is_in_build_dir FALSE) - - if(NOT arg_REPO_PROJECT_NAME_LOWERCASE) - _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name) - else() - set(repo_project_name "${arg_REPO_PROJECT_NAME_LOWERCASE}") - endif() - - if(NOT DEFINED arg_PATH) - message(FATAL_ERROR "PATH must be set") - endif() - set(path "${arg_PATH}") - - set(handled FALSE) - - if(path MATCHES "$<.+>") - # TODO: Paths wrapped in genexes are usually absolute paths that we also want to handle, - # but can't at configure time. We'll need a separate processing step at build time. - # Keep these as is for now, but signal failure of handling. - set(path_out "${path}") - else() - if(IS_ABSOLUTE "${path}") - set(path_in "${path}") - - string(FIND "${path}" "${PROJECT_SOURCE_DIR}/" src_idx) - string(FIND "${path}" "${PROJECT_BINARY_DIR}/" dest_idx) - - if(src_idx EQUAL "0") - set(is_in_source_dir TRUE) - elseif(dest_idx EQUAL "0") - set(is_in_build_dir TRUE) - endif() - else() - # We consider relative paths to be relative to the current source dir. - set(is_in_source_dir TRUE) - set(path_in "${CMAKE_CURRENT_SOURCE_DIR}/${path}") - endif() - - # Resolve any .. and replace the absolute path with a path relative to the source dir - # or build dir, prefixed with a root marker. - get_filename_component(path_in_real "${path_in}" REALPATH) - - # Replace the absolute prefixes with markers. - if(is_in_source_dir) - set(handled TRUE) - set(marker "/src_dir") - string(REPLACE "${PROJECT_SOURCE_DIR}/" "${marker}/${repo_project_name}/" - path_out "${path_in_real}") - elseif(is_in_build_dir) - set(handled TRUE) - set(marker "/build_dir") - string(REPLACE "${PROJECT_BINARY_DIR}/" "${marker}/${repo_project_name}/" - path_out "${path_in_real}") - else() - # If it's not a source dir or a build dir, it might be some kind of weird genex - # or marker that we don't handle yet. - set(path_out "${path_in_real}") - endif() - endif() - - set(${out_var} "${path_out}" PARENT_SCOPE) - if(arg_OUT_SUCCESS) - set(${arg_OUT_SUCCESS} "${handled}" PARENT_SCOPE) - endif() -endfunction() - -# Collect source file "generated from" relationship comments for a given target file. -function(_qt_internal_sbom_add_target_source_files target spdx_id out_relationships) - get_target_property(sources ${target} SOURCES) - if(NOT sources) - set(sources "") - endif() - list(REMOVE_DUPLICATES sources) - - set(sources_option "") - if(sources) - set(sources_option SOURCES ${sources}) - endif() - - _qt_internal_sbom_add_source_files( - ${sources_option} - SPDX_ID "${spdx_id}" - OUT_RELATIONSHIPS_VAR relationships - ) - - set(${out_relationships} "${relationships}" PARENT_SCOPE) -endfunction() - -# Collect source file "generated from" relationship comments for the given sources. -function(_qt_internal_sbom_add_source_files) - set(opt_args "") - set(single_args - SPDX_ID - OUT_RELATIONSHIPS_VAR - ) - set(multi_args - SOURCES - ) - cmake_parse_arguments(PARSE_ARGV 0 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() - - if(NOT arg_OUT_RELATIONSHIPS_VAR) - message(FATAL_ERROR "OUT_RELATIONSHIPS_VAR must be set") - endif() - - if(NOT arg_SOURCES) - set(sources "") - else() - set(sources "${arg_SOURCES}") - endif() - - set(relationships "") - - _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) - - foreach(source IN LISTS sources) - # Filter out $. - if(source MATCHES "^\\$$") - continue() - endif() - - # Filter out prl files. - if(source MATCHES "\.prl$") - continue() - endif() - - # Filter out pkg-config. pc files. - if(source MATCHES "\.pc$") - continue() - endif() - - # Filter out metatypes .json.gen files. - if(source MATCHES "\.json\.gen$") - continue() - endif() - - _qt_internal_sbom_map_path_to_reproducible_relative_path(source_path - PATH "${source}" - REPO_PROJECT_NAME_LOWERCASE "${repo_project_name_lowercase}" - ) - - set(source_entry -"${spdx_id} GENERATED_FROM NOASSERTION\nRelationshipComment: ${source_path}" - ) - 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(${arg_OUT_RELATIONSHIPS_VAR} "${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 AND TARGET "${target}") - _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_find_package PROVIDED_TARGETS might refer to non-existent targets in certain cases, - # like zstd::libzstd_shared for qt_find_package(WrapZSTD), because we are not sure what - # kind of zstd build was done. Make sure to check if the target exists before recording it. - if(TARGET "${target}") - set(target_unaliased "${target}") - get_target_property(aliased_target "${target}" ALIASED_TARGET) - if(aliased_target) - set(target_unaliased ${aliased_target}) - endif() - - _qt_internal_sbom_record_system_library_spdx_id(${target_unaliased} ${args}) - else() - message(DEBUG - "Skipping recording system library for SBOM because target does not exist: " - " ${target}") - endif() - 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) - # Some system targets like qtspeech SpeechDispatcher::SpeechDispatcher might be aliased, - # and we can't set properties on them, so unalias the target name. - set(target_original "${target}") - get_target_property(aliased_target "${target}" ALIASED_TARGET) - if(aliased_target) - set(target ${aliased_target}) - endif() - - 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() - - # Automatic system library sbom recording happens at project root source dir scope, which - # means it might accidentally pick up a qt_attribution.json file from the project root, - # that is not intended to be use for system libraries. - # For now, explicitly disable using the root attribution file. - list(APPEND args NO_CURRENT_DIR_ATTRIBUTION) - - 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_original}") - 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. @@ -2697,475 +1283,6 @@ function(_qt_internal_extend_sbom_dependencies target) ) 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 - ) - set(multi_args "") - - _qt_internal_get_sbom_specific_options(sbom_opt_args sbom_single_args sbom_multi_args) - list(APPEND opt_args ${sbom_opt_args}) - list(APPEND single_args ${sbom_single_args}) - list(APPEND multi_args ${sbom_multi_args}) - - 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 CREATE_SBOM_FOR_EACH_ATTRIBUTION is set, that means the parent target was a qt entity, - # and not a 3rd party library. - # In which case we don't want to proagate options like CPE to the child attribution targets, - # because the CPE is meant for the parent target. - set(propagate_sbom_options_to_new_attribution_targets TRUE) - if(arg_CREATE_SBOM_FOR_EACH_ATTRIBUTION) - set(propagate_sbom_options_to_new_attribution_targets FALSE) - 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) - # Collect all processed attribution files to later create a configure-time dependency on - # them so that the SBOM is regenerated (and CMake is re-ran) if they are modified. - set_property(GLOBAL APPEND PROPERTY _qt_internal_project_attribution_files - "${attribution_file_path}") - - # 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() - - set(sbom_args "") - - if(propagate_sbom_options_to_new_attribution_targets) - # Filter out the attributtion options, they will be passed mnaually - # depending on which file and index is currently being processed. - _qt_internal_get_sbom_specific_options( - sbom_opt_args sbom_single_args sbom_multi_args) - list(REMOVE_ITEM sbom_opt_args NO_CURRENT_DIR_ATTRIBUTION) - list(REMOVE_ITEM sbom_single_args ATTRIBUTION_ENTRY_INDEX) - list(REMOVE_ITEM sbom_multi_args - ATTRIBUTION_FILE_PATHS - ATTRIBUTION_FILE_DIR_PATHS - ) - - # Also filter out the FRIENDLY_PACKAGE_NAME option, otherwise we'd try to - # file(GENERATE) multiple times with the same file name, but different content. - list(REMOVE_ITEM sbom_single_args FRIENDLY_PACKAGE_NAME) - - _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} - ) - 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 - ${sbom_args} - ) - - _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}" IS_MULTI_VALUE) - _qt_internal_sbom_get_attribution_key(CopyrightFile copyright_file "${out_prefix}") - _qt_internal_sbom_get_attribution_key(PURL purls "${out_prefix}" IS_MULTI_VALUE) - _qt_internal_sbom_get_attribution_key(CPE cpes "${out_prefix}" IS_MULTI_VALUE) - - # 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() - -# Extracts a string or an array of strings from a json index path, depending on the extracted value -# type. -# -# Given the 'contents' of the whole json document and the EXTRACTED_VALUE of a json key specified -# by the INDICES path, it tries to determine whether the value is an array, in which case the array -# is converted to a cmake list and assigned to ${out_var} in the parent scope. -# Otherwise the function assumes the EXTRACTED_VALUE was not an array, and just assigns the value -# of EXTRACTED_VALUE to ${out_var} -function(_qt_internal_sbom_handle_attribution_json_array contents) - set(opt_args "") - set(single_args - EXTRACTED_VALUE - OUT_VAR - ) - set(multi_args - INDICES - ) - cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") - _qt_internal_validate_all_args_are_parsed(arg) - - # Write the original value to the parent scope, in case it was not an array. - set(${arg_OUT_VAR} "${arg_EXTRACTED_VALUE}" PARENT_SCOPE) - - if(NOT arg_EXTRACTED_VALUE) - return() - endif() - - string(JSON element_type TYPE "${contents}" ${arg_INDICES}) - - if(NOT element_type STREQUAL "ARRAY") - return() - endif() - - set(json_array "${arg_EXTRACTED_VALUE}") - string(JSON array_len LENGTH "${json_array}") - - set(value_list "") - - math(EXPR array_len "${array_len} - 1") - foreach(index RANGE 0 "${array_len}") - string(JSON value GET "${json_array}" ${index}) - if(value) - list(APPEND value_list "${value}") - endif() - endforeach() - - if(value_list) - set(${arg_OUT_VAR} "${value_list}" 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() - -# This macro reads a json key from a qt_attribution.json file, and assigns the escaped value to -# out_var. -# Also appends the name of the out_var to the parent scope 'variable_names' var. -# -# Expects 'contents' and 'indices' to already be set in the calling scope. -# -# If IS_MULTI_VALUE is set, handles the key as if it contained an array of -# values, by converting the array of json values to a cmake list. -macro(_qt_internal_sbom_get_attribution_key json_key out_var out_prefix) - cmake_parse_arguments(arg "IS_MULTI_VALUE" "" "" ${ARGN}) - - string(JSON "${out_var}" ERROR_VARIABLE get_error GET "${contents}" ${indices} "${json_key}") - if(NOT "${${out_var}}" STREQUAL "" AND NOT get_error) - set(extracted_value "${${out_var}}") - - if(arg_IS_MULTI_VALUE) - _qt_internal_sbom_handle_attribution_json_array("${contents}" - EXTRACTED_VALUE "${extracted_value}" - INDICES ${indices} ${json_key} - OUT_VAR value_list - ) - if(value_list) - set(extracted_value "${value_list}") - endif() - endif() - - _qt_internal_sbom_escape_json_content("${extracted_value}" escaped_content) - - set(${out_prefix}_${out_var} "${escaped_content}" PARENT_SCOPE) - list(APPEND variable_names "${out_var}") - - unset(extracted_value) - unset(escaped_content) - unset(value_list) - endif() -endmacro() - # Sets the 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}") @@ -3418,13 +1535,6 @@ function(_qt_internal_sbom_get_spdx_id_for_target target out_var) 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") @@ -3522,47 +1632,6 @@ function(_qt_internal_sbom_get_package_purpose type out_purpose) set(${out_purpose} "${package_purpose}" PARENT_SCOPE) endfunction() -# Get a qt spdx license expression given the id. -function(_qt_internal_sbom_get_spdx_license_expression id out_var) - set(license "") - - # The default for modules / plugins - if(id STREQUAL "QT_DEFAULT" OR id STREQUAL "QT_COMMERCIAL_OR_LGPL3") - set(license "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only") - - # For commercial only entities - elseif(id STREQUAL "QT_COMMERCIAL") - set(license "LicenseRef-Qt-Commercial") - - # For GPL3 only modules - elseif(id STREQUAL "QT_COMMERCIAL_OR_GPL3") - set(license "LicenseRef-Qt-Commercial OR GPL-3.0-only") - - # For tools and apps - elseif(id STREQUAL "QT_COMMERCIAL_OR_GPL3_WITH_EXCEPTION") - set(license "LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0") - - # For things like the qtmain library - elseif(id STREQUAL "QT_COMMERCIAL_OR_BSD3") - set(license "LicenseRef-Qt-Commercial OR BSD-3-Clause") - - # For documentation - elseif(id STREQUAL "QT_COMMERCIAL_OR_GFDL1_3") - set(license "LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only") - - # For examples and the like - elseif(id STREQUAL "BSD3") - set(license "BSD-3-Clause") - - endif() - - if(NOT license) - message(FATAL_ERROR "No SPDX license expression found for id: ${id}") - endif() - - set(${out_var} "${license}" PARENT_SCOPE) -endfunction() - # Get the default qt copyright. function(_qt_internal_sbom_get_default_qt_copyright_header out_var) set(${out_var} @@ -3596,791 +1665,6 @@ function(_qt_internal_sbom_get_qt_repo_source_download_location out_var) 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() - -# Parse purl arguments for a specific purl variant, e.g. for parsing all values of arg_PURL_QT_ARGS. -# arguments_var_name is the variable name that contains the args. -macro(_qt_internal_sbom_parse_purl_variant_options prefix arguments_var_name) - _qt_internal_get_sbom_purl_parsing_options(purl_opt_args purl_single_args purl_multi_args) - - cmake_parse_arguments(arg "${purl_opt_args}" "${purl_single_args}" "${purl_multi_args}" - ${${arguments_var_name}}) - _qt_internal_validate_all_args_are_parsed(arg) -endmacro() - -# Returns a vcs url where for purls where qt entities of the current repo are hosted. -function(_qt_internal_sbom_get_qt_entity_vcs_url target) - set(opt_args "") - set(single_args - REPO_NAME - VERSION - 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) - - if(NOT arg_REPO_NAME) - message(FATAL_ERROR "REPO_NAME must be set") - endif() - - if(NOT arg_OUT_VAR) - message(FATAL_ERROR "OUT_VAR must be set") - endif() - - set(version_part "") - if(arg_VERSION) - set(version_part "@${arg_VERSION}") - endif() - - set(vcs_url "https://code.qt.io/qt/${arg_REPO_NAME}.git${version_part}") - set(${arg_OUT_VAR} "${vcs_url}" PARENT_SCOPE) -endfunction() - -# Returns a relative path to the source where the target was created, to be embedded into a -# mirror purl as a subpath. -function(_qt_internal_sbom_get_qt_entity_repo_source_dir target) - set(opt_args "") - 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) - - if(NOT arg_OUT_VAR) - message(FATAL_ERROR "OUT_VAR must be set") - endif() - - get_target_property(repo_source_dir "${target}" SOURCE_DIR) - - # Get the path relative to the PROJECT_SOURCE_DIR - file(RELATIVE_PATH relative_repo_source_dir "${PROJECT_SOURCE_DIR}" "${repo_source_dir}") - - set(sub_path "${relative_repo_source_dir}") - set(${arg_OUT_VAR} "${sub_path}" PARENT_SCOPE) -endfunction() - -# Handles purl arguments specified to functions like qt_internal_add_sbom. -# Currently accepts arguments for 3 variants of purls, each of which will generate a separate purl. -# If no arguments are specified, for qt entity types, default values will be chosen. -# -# Purl variants: -# - PURL_QT_ARGS -# args to override Qt's generic purl for Qt modules or patched 3rd party libs -# defaults to something like pkg:generic/TheQtCompany/${repo_name}-${target}@SHA1 -# - PURL_MIRROR_ARGS -# args to override Qt's mirror purl, which is hosted on github -# defaults to something like pkg:github/qt/${repo_name}@SHA1 -# - PURL_3RDPARTY_UPSTREAM_ARGS -# args to specify a purl pointing to an upstream repo, usually to github or another forge -# no defaults, but could look like: pkg:github/harfbuzz/harfbuzz@v8.5.0 -# Example values for harfbuzz: -# PURL_3RDPARTY_UPSTREAM_ARGS -# PURL_TYPE "github" -# PURL_NAMESPACE "harfbuzz" -# PURL_NAME "harfbuzz" -# PURL_VERSION "v8.5.0" # tag -function(_qt_internal_sbom_handle_purl_values target) - _qt_internal_get_sbom_purl_handling_options(opt_args single_args multi_args) - list(APPEND single_args OUT_VAR) - - cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") - _qt_internal_validate_all_args_are_parsed(arg) - - if(NOT arg_OUT_VAR) - message(FATAL_ERROR "OUT_VAR must be set") - endif() - - # List of purl variants to process. - set(purl_variants "") - - set(third_party_types - QT_THIRD_PARTY_MODULE - QT_THIRD_PARTY_SOURCES - ) - - if(arg_IS_QT_ENTITY_TYPE) - # Qt entities have two purls by default, a QT generic one and a MIRROR hosted on github. - list(APPEND purl_variants MIRROR QT) - elseif(arg_TYPE IN_LIST third_party_types) - # Third party libraries vendored in Qt also have at least two purls, like regular Qt - # libraries, but might also have an upstream one. - - # The order in which the purls are generated matters for tools that consume the SBOM. Some - # tools can only handle one PURL per package, so the first one should be the important one. - # For now, I deem that the upstream one if present. Otherwise the github mirror. - if(arg_PURL_3RDPARTY_UPSTREAM_ARGS) - list(APPEND purl_variants 3RDPARTY_UPSTREAM) - endif() - - list(APPEND purl_variants MIRROR QT) - else() - # If handling another entity type, handle based on whether any of the purl arguments are - # set. - set(known_purl_variants QT MIRROR 3RDPARTY_UPSTREAM) - foreach(known_purl_variant IN LISTS known_purl_variants) - if(arg_PURL_${known_purl_variant}_ARGS) - list(APPEND purl_variants ${known_purl_variant}) - endif() - endforeach() - endif() - - if(arg_IS_QT_ENTITY_TYPE - OR arg_TYPE STREQUAL "QT_THIRD_PARTY_MODULE" - OR arg_TYPE STREQUAL "QT_THIRD_PARTY_SOURCES" - ) - set(is_qt_purl_entity_type TRUE) - else() - set(is_qt_purl_entity_type FALSE) - endif() - - _qt_internal_get_sbom_purl_parsing_options(purl_opt_args purl_single_args purl_multi_args) - - set(project_package_options "") - - foreach(purl_variant IN LISTS purl_variants) - # Clear previous values. - foreach(option_name IN LISTS purl_opt_args purl_single_args purl_multi_args) - unset(arg_${option_name}) - endforeach() - - _qt_internal_sbom_parse_purl_variant_options(arg arg_PURL_${purl_variant}_ARGS) - - # Check if custom purl args were specified. - set(purl_args_available FALSE) - if(arg_PURL_${purl_variant}_ARGS) - set(purl_args_available TRUE) - endif() - - # We want to create a purl either if it's one of Qt's entities and one of it's default - # purl types, or if custom args were specified. - set(consider_purl_processing FALSE) - if((purl_args_available OR is_qt_purl_entity_type) AND NOT arg_NO_PURL) - set(consider_purl_processing TRUE) - endif() - - if(consider_purl_processing) - set(purl_args "") - - # Override the purl version with the package version. - if(arg_PURL_USE_PACKAGE_VERSION AND arg_VERSION) - set(arg_PURL_VERSION "${arg_VERSION}") - endif() - - # Append a vcs_url to the qualifiers if specified. - if(arg_PURL_VCS_URL) - list(APPEND arg_PURL_QUALIFIERS "vcs_url=${arg_PURL_VCS_URL}") - endif() - - _qt_internal_forward_function_args( - FORWARD_APPEND - FORWARD_PREFIX arg - FORWARD_OUT_VAR purl_args - FORWARD_OPTIONS - ${purl_opt_args} - FORWARD_SINGLE - ${purl_single_args} - FORWARD_MULTI - ${purl_multi_args} - ) - - # Qt entity types get special treatment purl. - if(is_qt_purl_entity_type AND NOT arg_NO_DEFAULT_QT_PURL AND - (purl_variant STREQUAL "QT" OR purl_variant STREQUAL "MIRROR")) - _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) - - # Add a vcs_url to the generic QT variant. - if(purl_variant STREQUAL "QT") - set(entity_vcs_url_version_option "") - # Can be empty. - if(QT_SBOM_GIT_HASH_SHORT) - set(entity_vcs_url_version_option VERSION "${QT_SBOM_GIT_HASH_SHORT}") - endif() - - _qt_internal_sbom_get_qt_entity_vcs_url(${target} - REPO_NAME "${repo_project_name_lowercase}" - ${entity_vcs_url_version_option} - OUT_VAR vcs_url) - list(APPEND purl_args PURL_QUALIFIERS "vcs_url=${vcs_url}") - endif() - - # Add the subdirectory path where the target was created as a custom qualifier. - _qt_internal_sbom_get_qt_entity_repo_source_dir(${target} OUT_VAR sub_path) - if(sub_path) - list(APPEND purl_args PURL_SUBPATH "${sub_path}") - endif() - - # Add the target name as a custom qualifer. - list(APPEND purl_args PURL_QUALIFIERS "library_name=${target}") - - # Can be empty. - if(QT_SBOM_GIT_HASH_SHORT) - list(APPEND purl_args VERSION "${QT_SBOM_GIT_HASH_SHORT}") - endif() - - # Get purl args the Qt entity type, taking into account defaults. - _qt_internal_sbom_get_qt_entity_purl_args(${target} - NAME "${repo_project_name_lowercase}-${target}" - REPO_NAME "${repo_project_name_lowercase}" - SUPPLIER "${arg_SUPPLIER}" - PURL_VARIANT "${purl_variant}" - ${purl_args} - OUT_VAR purl_args - ) - endif() - - _qt_internal_sbom_assemble_purl(${target} - ${purl_args} - OUT_VAR package_manager_external_ref - ) - list(APPEND project_package_options ${package_manager_external_ref}) - endif() - endforeach() - - set(direct_values - PURL_QT_VALUES - PURL_MIRROR_VALUES - PURL_3RDPARTY_UPSTREAM_VALUES - ) - - foreach(direct_value IN LISTS direct_values) - if(arg_${direct_value}) - set(direct_values_per_type "") - foreach(direct_value IN LISTS arg_${direct_value}) - _qt_internal_sbom_get_purl_value_extref( - VALUE "${direct_value}" OUT_VAR package_manager_external_ref) - - list(APPEND direct_values_per_type ${package_manager_external_ref}) - endforeach() - # The order in which the purls are generated, matters for tools that consume the SBOM. - # Some tools can only handle one PURL per package, so the first one should be the - # important one. - # For now, I deem that the directly specified ones (probably via a qt_attribution.json - # file) are the more important ones. So we prepend them. - list(PREPEND project_package_options ${direct_values_per_type}) - endif() - endforeach() - - set(${arg_OUT_VAR} "${project_package_options}" PARENT_SCOPE) -endfunction() - -# Gets a list of arguments to pass to _qt_internal_sbom_assemble_purl when handling a Qt entity -# type. The purl for Qt entity types have Qt-specific defaults, but can be overridden per purl -# component. -# The arguments are saved in OUT_VAR. -function(_qt_internal_sbom_get_qt_entity_purl_args target) - set(opt_args "") - set(single_args - NAME - REPO_NAME - SUPPLIER - VERSION - PURL_VARIANT - OUT_VAR - ) - set(multi_args "") - - _qt_internal_get_sbom_purl_parsing_options(purl_opt_args purl_single_args purl_multi_args) - list(APPEND opt_args ${purl_opt_args}) - list(APPEND single_args ${purl_single_args}) - list(APPEND multi_args ${purl_multi_args}) - - cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") - _qt_internal_validate_all_args_are_parsed(arg) - - set(supported_purl_variants QT MIRROR) - if(NOT arg_PURL_VARIANT IN_LIST supported_purl_variants) - message(FATAL_ERROR "PURL_VARIANT unknown: ${arg_PURL_VARIANT}") - endif() - - if(arg_PURL_VARIANT STREQUAL "QT") - set(purl_type "generic") - set(purl_namespace "${arg_SUPPLIER}") - set(purl_name "${arg_NAME}") - set(purl_version "${arg_VERSION}") - elseif(arg_PURL_VARIANT STREQUAL "MIRROR") - set(purl_type "github") - set(purl_namespace "qt") - set(purl_name "${arg_REPO_NAME}") - set(purl_version "${arg_VERSION}") - endif() - - if(arg_PURL_TYPE) - set(purl_type "${arg_PURL_TYPE}") - endif() - - if(arg_PURL_NAMESPACE) - set(purl_namespace "${arg_PURL_NAMESPACE}") - endif() - - if(arg_PURL_NAME) - set(purl_name "${arg_PURL_NAME}") - endif() - - if(arg_PURL_VERSION) - set(purl_version "${arg_PURL_VERSION}") - endif() - - set(purl_version_option "") - if(purl_version) - set(purl_version_option PURL_VERSION "${purl_version}") - endif() - - set(purl_args - PURL_TYPE "${purl_type}" - PURL_NAMESPACE "${purl_namespace}" - PURL_NAME "${purl_name}" - ${purl_version_option} - ) - - if(arg_PURL_QUALIFIERS) - list(APPEND purl_args PURL_QUALIFIERS "${arg_PURL_QUALIFIERS}") - endif() - - if(arg_PURL_SUBPATH) - list(APPEND purl_args PURL_SUBPATH "${arg_PURL_SUBPATH}") - endif() - - set(${arg_OUT_VAR} "${purl_args}" PARENT_SCOPE) -endfunction() - -# Assembles an external reference purl identifier. -# PURL_TYPE and PURL_NAME are required. -# Stores the result in the OUT_VAR. -# Accepted options: -# PURL_TYPE -# PURL_NAME -# PURL_NAMESPACE -# PURL_VERSION -# PURL_SUBPATH -# PURL_QUALIFIERS -function(_qt_internal_sbom_assemble_purl target) - set(opt_args "") - set(single_args - OUT_VAR - ) - set(multi_args "") - - _qt_internal_get_sbom_purl_parsing_options(purl_opt_args purl_single_args purl_multi_args) - list(APPEND opt_args ${purl_opt_args}) - list(APPEND single_args ${purl_single_args}) - list(APPEND multi_args ${purl_multi_args}) - - cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") - _qt_internal_validate_all_args_are_parsed(arg) - - set(purl_scheme "pkg") - - if(NOT arg_PURL_TYPE) - message(FATAL_ERROR "PURL_TYPE must be set") - endif() - - if(NOT arg_PURL_NAME) - message(FATAL_ERROR "PURL_NAME must be set") - endif() - - if(NOT arg_OUT_VAR) - message(FATAL_ERROR "OUT_VAR must be set") - endif() - - # https://github.com/package-url/purl-spec - # Spec is 'scheme:type/namespace/name@version?qualifiers#subpath' - set(purl "${purl_scheme}:${arg_PURL_TYPE}") - - if(arg_PURL_NAMESPACE) - string(APPEND purl "/${arg_PURL_NAMESPACE}") - endif() - - string(APPEND purl "/${arg_PURL_NAME}") - - if(arg_PURL_VERSION) - string(APPEND purl "@${arg_PURL_VERSION}") - endif() - - if(arg_PURL_QUALIFIERS) - # TODO: Note that the qualifiers are expected to be URL encoded, which this implementation - # is not doing at the moment. - list(JOIN arg_PURL_QUALIFIERS "&" qualifiers) - string(APPEND purl "?${qualifiers}") - endif() - - if(arg_PURL_SUBPATH) - string(APPEND purl "#${arg_PURL_SUBPATH}") - endif() - - _qt_internal_sbom_get_purl_value_extref(VALUE "${purl}" OUT_VAR result) - - set(${arg_OUT_VAR} "${result}" PARENT_SCOPE) -endfunction() - -# Takes a PURL VALUE and returns an SBOM purl external reference in OUT_VAR. -function(_qt_internal_sbom_get_purl_value_extref) - set(opt_args "") - set(single_args - OUT_VAR - VALUE - ) - 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_OUT_VAR) - message(FATAL_ERROR "OUT_VAR must be set") - endif() - - if(NOT arg_VALUE) - message(FATAL_ERROR "VALUE must be set") - endif() - - # SPDX SBOM External reference type. - set(ext_ref_prefix "PACKAGE-MANAGER purl") - set(external_ref "${ext_ref_prefix} ${arg_VALUE}") - set(result "EXTREF" "${external_ref}") - set(${arg_OUT_VAR} "${result}" 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 -# file handling functions. -# 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 - ) - - list(APPEND single_args "${single_args_to_process}") - - # We need to process multi config args even in a single config build, because there might be API - # calls that specify them directly. Use a default set of multi config configs. - set(configs Release RelWithDebInfo MinSizeRel Debug) - - get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) - if(is_multi_config AND CMAKE_CONFIGURATION_TYPES) - list(APPEND configs ${CMAKE_CONFIGURATION_TYPES}) - endif() - - list(REMOVE_DUPLICATES configs) - - 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() - - 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) - - # Prefer the multi config option if it is set. - 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}") - endif() - - if(NOT is_multi_config OR NOT DEFINED ${outer_scope_var_name}) - 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() - -# Replaces placeholders in CPE and PURL strings read from qt_attribution.json files. -# -# VALUES - list of CPE or PURL strings -# OUT_VAR - variable to store the replaced values -# VERSION - version to replace in the placeholders - -# Known placeholders: -# $ - Replaces occurrences of the placeholder with the value passed to the VERSION option. -# $ - Replaces occurrences of the placeholder with the value passed to the VERSION -# option, but with dots replaced by dashes. -function(_qt_internal_sbom_replace_qa_placeholders) - set(opt_args "") - set(single_args - OUT_VAR - VERSION - ) - set(multi_args - VALUES - ) - - cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") - _qt_internal_validate_all_args_are_parsed(arg) - - if(NOT arg_OUT_VAR) - message(FATAL_ERROR "OUT_VAR must be set") - endif() - - set(result "") - - if(arg_VERSION) - string(REPLACE "." "-" dashed_version "${arg_VERSION}") - endif() - - foreach(value IN LISTS arg_VALUES) - if(arg_VERSION) - string(REPLACE "$" "${arg_VERSION}" value "${value}") - string(REPLACE "$" "${dashed_version}" value "${value}") - endif() - - list(APPEND result "${value}") - endforeach() - - set(${arg_OUT_VAR} "${result}" PARENT_SCOPE) -endfunction() - # Returns the configure line used to configure the current repo or top-level build, by reading # the config.opt file that the configure script writes out. # Returns an empty string if configure was not called, but CMake was called directly. diff --git a/cmake/QtPublicSbomLicenseHelpers.cmake b/cmake/QtPublicSbomLicenseHelpers.cmake new file mode 100644 index 00000000000..a91f4c2cefa --- /dev/null +++ b/cmake/QtPublicSbomLicenseHelpers.cmake @@ -0,0 +1,108 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# 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() + +# Get a qt spdx license expression given the id. +function(_qt_internal_sbom_get_spdx_license_expression id out_var) + set(license "") + + # The default for modules / plugins + if(id STREQUAL "QT_DEFAULT" OR id STREQUAL "QT_COMMERCIAL_OR_LGPL3") + set(license "LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only") + + # For commercial only entities + elseif(id STREQUAL "QT_COMMERCIAL") + set(license "LicenseRef-Qt-Commercial") + + # For GPL3 only modules + elseif(id STREQUAL "QT_COMMERCIAL_OR_GPL3") + set(license "LicenseRef-Qt-Commercial OR GPL-3.0-only") + + # For tools and apps + elseif(id STREQUAL "QT_COMMERCIAL_OR_GPL3_WITH_EXCEPTION") + set(license "LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0") + + # For things like the qtmain library + elseif(id STREQUAL "QT_COMMERCIAL_OR_BSD3") + set(license "LicenseRef-Qt-Commercial OR BSD-3-Clause") + + # For documentation + elseif(id STREQUAL "QT_COMMERCIAL_OR_GFDL1_3") + set(license "LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only") + + # For examples and the like + elseif(id STREQUAL "BSD3") + set(license "BSD-3-Clause") + + endif() + + if(NOT license) + message(FATAL_ERROR "No SPDX license expression found for id: ${id}") + endif() + + set(${out_var} "${license}" 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/QtPublicSbomOpsHelpers.cmake b/cmake/QtPublicSbomOpsHelpers.cmake new file mode 100644 index 00000000000..f870581f049 --- /dev/null +++ b/cmake/QtPublicSbomOpsHelpers.cmake @@ -0,0 +1,579 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# Copyright (C) 2023-2024 Jochem Rutgers +# SPDX-License-Identifier: MIT AND BSD-3-Clause + +# Handles the look up of Python, Python spdx dependencies and other various post-installation steps +# like NTIA validation, auditing, json generation, etc. +function(_qt_internal_sbom_setup_project_ops_generation) + set(opt_args + GENERATE_JSON + GENERATE_JSON_REQUIRED + GENERATE_SOURCE_SBOM + VERIFY_SBOM + VERIFY_SBOM_REQUIRED + VERIFY_NTIA_COMPLIANT + LINT_SOURCE_SBOM + LINT_SOURCE_SBOM_NO_ERROR + 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) + + if(arg_GENERATE_JSON AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) + set(op_args + OP_KEY "GENERATE_JSON" + OUT_VAR_DEPS_FOUND deps_found + ) + if(arg_GENERATE_JSON_REQUIRED) + list(APPEND op_args REQUIRED) + endif() + + _qt_internal_sbom_find_and_handle_sbom_op_dependencies(${op_args}) + if(deps_found) + _qt_internal_sbom_generate_json() + endif() + endif() + + if(arg_VERIFY_SBOM AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) + set(op_args + OP_KEY "VERIFY_SBOM" + OUT_VAR_DEPS_FOUND deps_found + ) + if(arg_VERIFY_SBOM_REQUIRED) + list(APPEND op_args REQUIRED) + endif() + + _qt_internal_sbom_find_and_handle_sbom_op_dependencies(${op_args}) + if(deps_found) + _qt_internal_sbom_verify_valid() + endif() + endif() + + if(arg_VERIFY_NTIA_COMPLIANT AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) + _qt_internal_sbom_find_and_handle_sbom_op_dependencies(REQUIRED OP_KEY "RUN_NTIA") + _qt_internal_sbom_verify_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() + + if(arg_GENERATE_SOURCE_SBOM AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) + _qt_internal_sbom_find_python_dependency_program(NAME reuse REQUIRED) + _qt_internal_sbom_generate_reuse_source_sbom() + endif() + + if(arg_LINT_SOURCE_SBOM AND NOT QT_INTERNAL_NO_SBOM_PYTHON_OPS) + set(lint_no_error_option "") + if(arg_LINT_SOURCE_SBOM_NO_ERROR) + set(lint_no_error_option NO_ERROR) + endif() + _qt_internal_sbom_find_python_dependency_program(NAME reuse REQUIRED) + _qt_internal_sbom_run_reuse_lint( + ${lint_no_error_option} + BUILD_TIME_SCRIPT_PATH_OUT_VAR reuse_lint_script + ) + endif() +endfunction() + +# Helper to find a python interpreter and a specific python dependency, e.g. to be able to generate +# a SPDX JSON SBOM, or run post-installation steps like NTIA verification. +# The exact dependency should be specified as the OP_KEY. +# +# Caches the found python executable in a separate cache var QT_INTERNAL_SBOM_PYTHON_EXECUTABLE, to +# avoid conflicts with any other found python interpreter. +function(_qt_internal_sbom_find_and_handle_sbom_op_dependencies) + set(opt_args + REQUIRED + ) + set(single_args + OP_KEY + OUT_VAR_DEPS_FOUND + ) + 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_OP_KEY) + message(FATAL_ERROR "OP_KEY is required") + endif() + + set(supported_ops "GENERATE_JSON" "VERIFY_SBOM" "RUN_NTIA") + + if(arg_OP_KEY STREQUAL "GENERATE_JSON" OR arg_OP_KEY STREQUAL "VERIFY_SBOM") + set(import_statement "import spdx_tools.spdx.clitools.pyspdxtools") + elseif(arg_OP_KEY STREQUAL "RUN_NTIA") + set(import_statement "import ntia_conformance_checker.main") + else() + message(FATAL_ERROR "OP_KEY must be one of ${supported_ops}") + endif() + + # Return early if we found the dependencies. + if(QT_INTERNAL_SBOM_DEPS_FOUND_FOR_${arg_OP_KEY}) + if(arg_OUT_VAR_DEPS_FOUND) + set(${arg_OUT_VAR_DEPS_FOUND} TRUE PARENT_SCOPE) + endif() + return() + endif() + + # NTIA-compliance checker requires Python 3.9 or later, so we use it as the minimum for all + # SBOM OPs. + set(required_version "3.9") + + set(python_common_args + VERSION "${required_version}" + ) + + set(everything_found FALSE) + + # On macOS FindPython prefers looking in the system framework location, but that usually would + # not have the required dependencies. So we first look in it, and then fallback to any other + # non-framework python found. + if(CMAKE_HOST_APPLE) + set(extra_python_args SEARCH_IN_FRAMEWORKS QUIET) + _qt_internal_sbom_find_python_and_dependency_helper_lambda() + endif() + + if(NOT everything_found) + set(extra_python_args QUIET) + _qt_internal_sbom_find_python_and_dependency_helper_lambda() + endif() + + if(NOT everything_found) + if(arg_REQUIRED) + set(message_type "FATAL_ERROR") + else() + set(message_type "DEBUG") + endif() + + if(NOT python_found) + # Look for python one more time, this time without QUIET, to show an error why it + # wasn't found. + if(arg_REQUIRED) + _qt_internal_sbom_find_python_helper(${python_common_args} + OUT_VAR_PYTHON_PATH unused_python + OUT_VAR_PYTHON_FOUND unused_found + ) + endif() + message(${message_type} "Python ${required_version} for running SBOM ops not found.") + elseif(NOT dep_found) + message(${message_type} "Python dependency for running SBOM op ${arg_OP_KEY} " + "not found:\n Python: ${python_path} \n Output: \n${dep_find_output}") + endif() + else() + message(DEBUG "Using Python ${python_path} for running SBOM ops.") + + if(NOT QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) + set(QT_INTERNAL_SBOM_PYTHON_EXECUTABLE "${python_path}" CACHE INTERNAL + "Python interpeter used for SBOM generation.") + endif() + + set(QT_INTERNAL_SBOM_DEPS_FOUND_FOR_${arg_OP_KEY} "TRUE" CACHE INTERNAL + "All dependencies found to run SBOM OP ${arg_OP_KEY}") + endif() + + if(arg_OUT_VAR_DEPS_FOUND) + set(${arg_OUT_VAR_DEPS_FOUND} "${QT_INTERNAL_SBOM_DEPS_FOUND_FOR_${arg_OP_KEY}}" + PARENT_SCOPE) + endif() +endfunction() + +# Helper to generate a SPDX JSON file from a tag/value format file. +# This also implies some additional validity checks, useful to ensure a proper sbom file. +function(_qt_internal_sbom_generate_json) + if(NOT QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) + message(FATAL_ERROR "Python interpreter not found for generating SBOM json file.") + endif() + if(NOT QT_INTERNAL_SBOM_DEPS_FOUND_FOR_GENERATE_JSON) + message(FATAL_ERROR "Python dependencies not found for generating SBOM json file.") + endif() + + set(content " + message(STATUS \"Generating JSON: \${QT_SBOM_OUTPUT_PATH}.json\") + execute_process( + COMMAND ${QT_INTERNAL_SBOM_PYTHON_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 generate a tag/value SPDX file from a SPDX JSON format file. +# +# Will be used by WebEngine to convert the Chromium JSON file to a tag/value SPDX file. +# +# This conversion needs to happen before the document is referenced in the SBOM generation process, +# so that the file already exists when it is parsed for its unique id and namespace. +# It also needs to happen before verification codes are computed for the current document +# that will depend on the target one, to ensure the the file exists and its checksum can be +# computed. +# +# OPERATION_ID - a unique id for the operation, used to generate a unique cmake file name for +# the SBOM generation process. +# +# INPUT_JSON_PATH - the absolute path to the input JSON file. +# +# OUTPUT_FILE_PATH - the absolute path where to create the output tag/value SPDX file. +# Note that if the output file path is set, it is up to the caller to also copy / install the file +# into the build and install directories where the build system expects to find all external +# document references. +# +# OUTPUT_FILE_NAME - when OUTPUT_FILE_PATH is not specified, the output directory is automatically +# set to the SBOM output directory. In this case OUTPUT_FILE_NAME can be used to override the +# outout file name. If not specified, it will be derived from the input file name. +# +# OUT_VAR_OUTPUT_FILE_NAME - output variable where to store the output file. +# +# OUT_VAR_OUTPUT_ABSOLUTE_FILE_PATH - output variable where to store the output file path. +# Note that the path will contain an unresolved '${QT_SBOM_OUTPUT_DIR}' which only has a value at +# install time. So the path can't be used sensibly during configure time. +function(_qt_internal_sbom_generate_tag_value_spdx_document) + if(NOT QT_GENERATE_SBOM) + return() + endif() + + set(opt_args "") + set(single_args + OPERATION_ID + INPUT_JSON_FILE_PATH + OUTPUT_FILE_PATH + OUTPUT_FILE_NAME + OUT_VAR_OUTPUT_FILE_NAME + OUT_VAR_OUTPUT_ABSOLUTE_FILE_PATH + ) + 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 QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) + message(FATAL_ERROR "Python interpreter not found for generating tag/value file from JSON.") + endif() + if(NOT QT_INTERNAL_SBOM_DEPS_FOUND_FOR_GENERATE_JSON) + message(FATAL_ERROR + "Python dependencies not found for generating tag/value file from JSON.") + endif() + + if(NOT arg_OPERATION_ID) + message(FATAL_ERROR "OPERATION_ID is required") + endif() + + if(NOT arg_INPUT_JSON_FILE_PATH) + message(FATAL_ERROR "INPUT_JSON_FILE_PATH is required") + endif() + + if(arg_OUTPUT_FILE_PATH) + set(output_path "${arg_OUTPUT_FILE_PATH}") + else() + if(arg_OUTPUT_FILE_NAME) + set(output_name "${arg_OUTPUT_FILE_NAME}") + else() + # Use the input file name without the last extension (without .json) as the output name. + get_filename_component(output_name "${arg_INPUT_JSON_FILE_PATH}" NAME_WLE) + endif() + set(output_path "\${QT_SBOM_OUTPUT_DIR}/${output_name}") + endif() + + if(arg_OUT_VAR_OUTPUT_FILE_NAME) + get_filename_component(output_name_resolved "${output_path}" NAME) + set(${arg_OUT_VAR_OUTPUT_FILE_NAME} "${output_name_resolved}" PARENT_SCOPE) + endif() + + if(arg_OUT_VAR_OUTPUT_ABSOLUTE_FILE_PATH) + set(${arg_OUT_VAR_OUTPUT_ABSOLUTE_FILE_PATH} "${output_path}" PARENT_SCOPE) + endif() + + set(content " + message(STATUS + \"Generating tag/value SPDX document: ${output_path} from \" + \"${arg_INPUT_JSON_FILE_PATH}\") + execute_process( + COMMAND ${QT_INTERNAL_SBOM_PYTHON_EXECUTABLE} -m spdx_tools.spdx.clitools.pyspdxtools + -i \"${arg_INPUT_JSON_FILE_PATH}\" -o \"${output_path}\" + RESULT_VARIABLE res + ) + if(NOT res EQUAL 0) + message(FATAL_ERROR \"SBOM conversion to tag/value failed: \${res}\") + endif() +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(convert_sbom "${sbom_dir}/convert_to_tag_value_${arg_OPERATION_ID}.cmake") + file(GENERATE OUTPUT "${convert_sbom}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_include_files + "${convert_sbom}") +endfunction() + +# Helper to verify the generated sbom is valid. +function(_qt_internal_sbom_verify_valid) + if(NOT QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) + message(FATAL_ERROR "Python interpreter not found for verifying SBOM file.") + endif() + + if(NOT QT_INTERNAL_SBOM_DEPS_FOUND_FOR_VERIFY_SBOM) + message(FATAL_ERROR "Python dependencies not found for verifying SBOM file") + endif() + + set(content " + message(STATUS \"Verifying: \${QT_SBOM_OUTPUT_PATH}\") + execute_process( + COMMAND ${QT_INTERNAL_SBOM_PYTHON_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() +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(verify_sbom "${sbom_dir}/verify_valid.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 NTIA compliant. +function(_qt_internal_sbom_verify_ntia_compliant) + if(NOT QT_INTERNAL_SBOM_PYTHON_EXECUTABLE) + message(FATAL_ERROR "Python interpreter not found for verifying SBOM file.") + endif() + + if(NOT QT_INTERNAL_SBOM_DEPS_FOUND_FOR_RUN_NTIA) + message(FATAL_ERROR "Python dependencies not found for running the SBOM NTIA checker.") + endif() + + set(content " + message(STATUS \"Checking for NTIA compliance: \${QT_SBOM_OUTPUT_PATH}\") + execute_process( + COMMAND ${QT_INTERNAL_SBOM_PYTHON_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_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(extra_code_begin "") + if(DEFINED ENV{COIN_UNIQUE_JOB_ID}) + # The output of the process dynamically adjusts the width of the shown table based on the + # console width. In the CI, the width is very short for some reason, and thus the output + # is truncated in the CI log. Explicitly set a bigger width to avoid this. + set(extra_code_begin " +set(backup_env_columns \$ENV{COLUMNS}) +set(ENV{COLUMNS} 150) +") +set(extra_code_end " +set(ENV{COLUMNS} \${backup_env_columns}) +") + endif() + + set(content " + message(STATUS \"Showing main SBOM document info: \${QT_SBOM_OUTPUT_PATH}\") + + ${extra_code_begin} + execute_process( + COMMAND ${QT_SBOM_PROGRAM_SBOM2DOC} -i \"\${QT_SBOM_OUTPUT_PATH}\" + RESULT_VARIABLE res + ) + ${extra_code_end} + 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 ${QT_SBOM_PROGRAM_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() + +# Returns path to project's potential root source reuse.toml file. +function(_qt_internal_sbom_get_project_reuse_toml_path out_var) + set(reuse_toml_path "${PROJECT_SOURCE_DIR}/REUSE.toml") + set(${out_var} "${reuse_toml_path}" PARENT_SCOPE) +endfunction() + +# Helper to generate and install a source SBOM using reuse. +function(_qt_internal_sbom_generate_reuse_source_sbom) + 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) + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(file_op "${sbom_dir}/generate_reuse_source_sbom.cmake") + + _qt_internal_sbom_get_project_reuse_toml_path(reuse_toml_path) + if(NOT EXISTS "${reuse_toml_path}" AND NOT QT_FORCE_SOURCE_SBOM_GENERATION) + set(skip_message + "Skipping source SBOM generation: No reuse.toml file found at '${reuse_toml_path}'.") + message(STATUS "${skip_message}") + + set(content " + message(STATUS \"${skip_message}\") +") + + file(GENERATE OUTPUT "${file_op}" CONTENT "${content}") + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_post_generation_include_files + "${file_op}") + return() + endif() + + set(handle_error "") + if(NOT arg_NO_ERROR) + set(handle_error " + if(NOT res EQUAL 0) + message(FATAL_ERROR \"Source SBOM generation using reuse tool failed: \${res}\") + endif() +") + endif() + + set(source_sbom_path "\${QT_SBOM_OUTPUT_PATH_WITHOUT_EXT}.source.spdx") + + set(content " + message(STATUS \"Generating source SBOM using reuse tool: ${source_sbom_path}\") + execute_process( + COMMAND ${QT_SBOM_PROGRAM_REUSE} --root \"${PROJECT_SOURCE_DIR}\" spdx + -o ${source_sbom_path} + RESULT_VARIABLE res + ) + ${handle_error} +") + + file(GENERATE OUTPUT "${file_op}" CONTENT "${content}") + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_post_generation_include_files "${file_op}") +endfunction() + +# Helper to run 'reuse lint' on the project source dir. +function(_qt_internal_sbom_run_reuse_lint) + set(opt_args + NO_ERROR + ) + set(single_args + BUILD_TIME_SCRIPT_PATH_OUT_VAR + ) + 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 no reuse.toml file exists, it means the repo is likely not reuse compliant yet, + # so we shouldn't error out during installation when running the lint. + _qt_internal_sbom_get_project_reuse_toml_path(reuse_toml_path) + if(NOT EXISTS "${reuse_toml_path}" AND NOT QT_FORCE_REUSE_LINT_ERROR) + set(arg_NO_ERROR TRUE) + endif() + + set(handle_error "") + if(NOT arg_NO_ERROR) + set(handle_error " + if(NOT res EQUAL 0) + message(FATAL_ERROR \"Running 'reuse lint' failed: \${res}\") + endif() +") + endif() + + set(content " + message(STATUS \"Running 'reuse lint' in '${PROJECT_SOURCE_DIR}'.\") + execute_process( + COMMAND ${QT_SBOM_PROGRAM_REUSE} --root \"${PROJECT_SOURCE_DIR}\" lint + RESULT_VARIABLE res + ) + ${handle_error} +") + + _qt_internal_get_current_project_sbom_dir(sbom_dir) + set(file_op_build "${sbom_dir}/run_reuse_lint_build.cmake") + file(GENERATE OUTPUT "${file_op_build}" CONTENT "${content}") + + # Allow skipping running 'reuse lint' during installation. But still allow running it during + # build time. This is a fail safe opt-out in case some repo needs it. + if(QT_FORCE_SKIP_REUSE_LINT_ON_INSTALL) + set(skip_message "Skipping running 'reuse lint' in '${PROJECT_SOURCE_DIR}'.") + + set(content " + message(STATUS \"${skip_message}\") +") + set(file_op_install "${sbom_dir}/run_reuse_lint_install.cmake") + file(GENERATE OUTPUT "${file_op_install}" CONTENT "${content}") + else() + # Just reuse the already generated script for installation as well. + set(file_op_install "${file_op_build}") + endif() + + set_property(GLOBAL APPEND PROPERTY _qt_sbom_cmake_verify_include_files "${file_op_install}") + + if(arg_BUILD_TIME_SCRIPT_PATH_OUT_VAR) + set(${arg_BUILD_TIME_SCRIPT_PATH_OUT_VAR} "${file_op_build}" PARENT_SCOPE) + endif() +endfunction() diff --git a/cmake/QtPublicSbomPurlHelpers.cmake b/cmake/QtPublicSbomPurlHelpers.cmake new file mode 100644 index 00000000000..f8ea421b73c --- /dev/null +++ b/cmake/QtPublicSbomPurlHelpers.cmake @@ -0,0 +1,444 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Parse purl arguments for a specific purl variant, e.g. for parsing all values of arg_PURL_QT_ARGS. +# arguments_var_name is the variable name that contains the args. +macro(_qt_internal_sbom_parse_purl_variant_options prefix arguments_var_name) + _qt_internal_get_sbom_purl_parsing_options(purl_opt_args purl_single_args purl_multi_args) + + cmake_parse_arguments(arg "${purl_opt_args}" "${purl_single_args}" "${purl_multi_args}" + ${${arguments_var_name}}) + _qt_internal_validate_all_args_are_parsed(arg) +endmacro() + +# Returns a vcs url where for purls where qt entities of the current repo are hosted. +function(_qt_internal_sbom_get_qt_entity_vcs_url target) + set(opt_args "") + set(single_args + REPO_NAME + VERSION + 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) + + if(NOT arg_REPO_NAME) + message(FATAL_ERROR "REPO_NAME must be set") + endif() + + if(NOT arg_OUT_VAR) + message(FATAL_ERROR "OUT_VAR must be set") + endif() + + set(version_part "") + if(arg_VERSION) + set(version_part "@${arg_VERSION}") + endif() + + set(vcs_url "https://code.qt.io/qt/${arg_REPO_NAME}.git${version_part}") + set(${arg_OUT_VAR} "${vcs_url}" PARENT_SCOPE) +endfunction() + +# Returns a relative path to the source where the target was created, to be embedded into a +# mirror purl as a subpath. +function(_qt_internal_sbom_get_qt_entity_repo_source_dir target) + set(opt_args "") + 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) + + if(NOT arg_OUT_VAR) + message(FATAL_ERROR "OUT_VAR must be set") + endif() + + get_target_property(repo_source_dir "${target}" SOURCE_DIR) + + # Get the path relative to the PROJECT_SOURCE_DIR + file(RELATIVE_PATH relative_repo_source_dir "${PROJECT_SOURCE_DIR}" "${repo_source_dir}") + + set(sub_path "${relative_repo_source_dir}") + set(${arg_OUT_VAR} "${sub_path}" PARENT_SCOPE) +endfunction() + +# Handles purl arguments specified to functions like qt_internal_add_sbom. +# Currently accepts arguments for 3 variants of purls, each of which will generate a separate purl. +# If no arguments are specified, for qt entity types, default values will be chosen. +# +# Purl variants: +# - PURL_QT_ARGS +# args to override Qt's generic purl for Qt modules or patched 3rd party libs +# defaults to something like pkg:generic/TheQtCompany/${repo_name}-${target}@SHA1 +# - PURL_MIRROR_ARGS +# args to override Qt's mirror purl, which is hosted on github +# defaults to something like pkg:github/qt/${repo_name}@SHA1 +# - PURL_3RDPARTY_UPSTREAM_ARGS +# args to specify a purl pointing to an upstream repo, usually to github or another forge +# no defaults, but could look like: pkg:github/harfbuzz/harfbuzz@v8.5.0 +# Example values for harfbuzz: +# PURL_3RDPARTY_UPSTREAM_ARGS +# PURL_TYPE "github" +# PURL_NAMESPACE "harfbuzz" +# PURL_NAME "harfbuzz" +# PURL_VERSION "v8.5.0" # tag +function(_qt_internal_sbom_handle_purl_values target) + _qt_internal_get_sbom_purl_handling_options(opt_args single_args multi_args) + list(APPEND single_args OUT_VAR) + + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_OUT_VAR) + message(FATAL_ERROR "OUT_VAR must be set") + endif() + + # List of purl variants to process. + set(purl_variants "") + + set(third_party_types + QT_THIRD_PARTY_MODULE + QT_THIRD_PARTY_SOURCES + ) + + if(arg_IS_QT_ENTITY_TYPE) + # Qt entities have two purls by default, a QT generic one and a MIRROR hosted on github. + list(APPEND purl_variants MIRROR QT) + elseif(arg_TYPE IN_LIST third_party_types) + # Third party libraries vendored in Qt also have at least two purls, like regular Qt + # libraries, but might also have an upstream one. + + # The order in which the purls are generated matters for tools that consume the SBOM. Some + # tools can only handle one PURL per package, so the first one should be the important one. + # For now, I deem that the upstream one if present. Otherwise the github mirror. + if(arg_PURL_3RDPARTY_UPSTREAM_ARGS) + list(APPEND purl_variants 3RDPARTY_UPSTREAM) + endif() + + list(APPEND purl_variants MIRROR QT) + else() + # If handling another entity type, handle based on whether any of the purl arguments are + # set. + set(known_purl_variants QT MIRROR 3RDPARTY_UPSTREAM) + foreach(known_purl_variant IN LISTS known_purl_variants) + if(arg_PURL_${known_purl_variant}_ARGS) + list(APPEND purl_variants ${known_purl_variant}) + endif() + endforeach() + endif() + + if(arg_IS_QT_ENTITY_TYPE + OR arg_TYPE STREQUAL "QT_THIRD_PARTY_MODULE" + OR arg_TYPE STREQUAL "QT_THIRD_PARTY_SOURCES" + ) + set(is_qt_purl_entity_type TRUE) + else() + set(is_qt_purl_entity_type FALSE) + endif() + + _qt_internal_get_sbom_purl_parsing_options(purl_opt_args purl_single_args purl_multi_args) + + set(project_package_options "") + + foreach(purl_variant IN LISTS purl_variants) + # Clear previous values. + foreach(option_name IN LISTS purl_opt_args purl_single_args purl_multi_args) + unset(arg_${option_name}) + endforeach() + + _qt_internal_sbom_parse_purl_variant_options(arg arg_PURL_${purl_variant}_ARGS) + + # Check if custom purl args were specified. + set(purl_args_available FALSE) + if(arg_PURL_${purl_variant}_ARGS) + set(purl_args_available TRUE) + endif() + + # We want to create a purl either if it's one of Qt's entities and one of it's default + # purl types, or if custom args were specified. + set(consider_purl_processing FALSE) + if((purl_args_available OR is_qt_purl_entity_type) AND NOT arg_NO_PURL) + set(consider_purl_processing TRUE) + endif() + + if(consider_purl_processing) + set(purl_args "") + + # Override the purl version with the package version. + if(arg_PURL_USE_PACKAGE_VERSION AND arg_VERSION) + set(arg_PURL_VERSION "${arg_VERSION}") + endif() + + # Append a vcs_url to the qualifiers if specified. + if(arg_PURL_VCS_URL) + list(APPEND arg_PURL_QUALIFIERS "vcs_url=${arg_PURL_VCS_URL}") + endif() + + _qt_internal_forward_function_args( + FORWARD_APPEND + FORWARD_PREFIX arg + FORWARD_OUT_VAR purl_args + FORWARD_OPTIONS + ${purl_opt_args} + FORWARD_SINGLE + ${purl_single_args} + FORWARD_MULTI + ${purl_multi_args} + ) + + # Qt entity types get special treatment purl. + if(is_qt_purl_entity_type AND NOT arg_NO_DEFAULT_QT_PURL AND + (purl_variant STREQUAL "QT" OR purl_variant STREQUAL "MIRROR")) + _qt_internal_sbom_get_root_project_name_lower_case(repo_project_name_lowercase) + + # Add a vcs_url to the generic QT variant. + if(purl_variant STREQUAL "QT") + set(entity_vcs_url_version_option "") + # Can be empty. + if(QT_SBOM_GIT_HASH_SHORT) + set(entity_vcs_url_version_option VERSION "${QT_SBOM_GIT_HASH_SHORT}") + endif() + + _qt_internal_sbom_get_qt_entity_vcs_url(${target} + REPO_NAME "${repo_project_name_lowercase}" + ${entity_vcs_url_version_option} + OUT_VAR vcs_url) + list(APPEND purl_args PURL_QUALIFIERS "vcs_url=${vcs_url}") + endif() + + # Add the subdirectory path where the target was created as a custom qualifier. + _qt_internal_sbom_get_qt_entity_repo_source_dir(${target} OUT_VAR sub_path) + if(sub_path) + list(APPEND purl_args PURL_SUBPATH "${sub_path}") + endif() + + # Add the target name as a custom qualifer. + list(APPEND purl_args PURL_QUALIFIERS "library_name=${target}") + + # Can be empty. + if(QT_SBOM_GIT_HASH_SHORT) + list(APPEND purl_args VERSION "${QT_SBOM_GIT_HASH_SHORT}") + endif() + + # Get purl args the Qt entity type, taking into account defaults. + _qt_internal_sbom_get_qt_entity_purl_args(${target} + NAME "${repo_project_name_lowercase}-${target}" + REPO_NAME "${repo_project_name_lowercase}" + SUPPLIER "${arg_SUPPLIER}" + PURL_VARIANT "${purl_variant}" + ${purl_args} + OUT_VAR purl_args + ) + endif() + + _qt_internal_sbom_assemble_purl(${target} + ${purl_args} + OUT_VAR package_manager_external_ref + ) + list(APPEND project_package_options ${package_manager_external_ref}) + endif() + endforeach() + + set(direct_values + PURL_QT_VALUES + PURL_MIRROR_VALUES + PURL_3RDPARTY_UPSTREAM_VALUES + ) + + foreach(direct_value IN LISTS direct_values) + if(arg_${direct_value}) + set(direct_values_per_type "") + foreach(direct_value IN LISTS arg_${direct_value}) + _qt_internal_sbom_get_purl_value_extref( + VALUE "${direct_value}" OUT_VAR package_manager_external_ref) + + list(APPEND direct_values_per_type ${package_manager_external_ref}) + endforeach() + # The order in which the purls are generated, matters for tools that consume the SBOM. + # Some tools can only handle one PURL per package, so the first one should be the + # important one. + # For now, I deem that the directly specified ones (probably via a qt_attribution.json + # file) are the more important ones. So we prepend them. + list(PREPEND project_package_options ${direct_values_per_type}) + endif() + endforeach() + + set(${arg_OUT_VAR} "${project_package_options}" PARENT_SCOPE) +endfunction() + +# Gets a list of arguments to pass to _qt_internal_sbom_assemble_purl when handling a Qt entity +# type. The purl for Qt entity types have Qt-specific defaults, but can be overridden per purl +# component. +# The arguments are saved in OUT_VAR. +function(_qt_internal_sbom_get_qt_entity_purl_args target) + set(opt_args "") + set(single_args + NAME + REPO_NAME + SUPPLIER + VERSION + PURL_VARIANT + OUT_VAR + ) + set(multi_args "") + + _qt_internal_get_sbom_purl_parsing_options(purl_opt_args purl_single_args purl_multi_args) + list(APPEND opt_args ${purl_opt_args}) + list(APPEND single_args ${purl_single_args}) + list(APPEND multi_args ${purl_multi_args}) + + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set(supported_purl_variants QT MIRROR) + if(NOT arg_PURL_VARIANT IN_LIST supported_purl_variants) + message(FATAL_ERROR "PURL_VARIANT unknown: ${arg_PURL_VARIANT}") + endif() + + if(arg_PURL_VARIANT STREQUAL "QT") + set(purl_type "generic") + set(purl_namespace "${arg_SUPPLIER}") + set(purl_name "${arg_NAME}") + set(purl_version "${arg_VERSION}") + elseif(arg_PURL_VARIANT STREQUAL "MIRROR") + set(purl_type "github") + set(purl_namespace "qt") + set(purl_name "${arg_REPO_NAME}") + set(purl_version "${arg_VERSION}") + endif() + + if(arg_PURL_TYPE) + set(purl_type "${arg_PURL_TYPE}") + endif() + + if(arg_PURL_NAMESPACE) + set(purl_namespace "${arg_PURL_NAMESPACE}") + endif() + + if(arg_PURL_NAME) + set(purl_name "${arg_PURL_NAME}") + endif() + + if(arg_PURL_VERSION) + set(purl_version "${arg_PURL_VERSION}") + endif() + + set(purl_version_option "") + if(purl_version) + set(purl_version_option PURL_VERSION "${purl_version}") + endif() + + set(purl_args + PURL_TYPE "${purl_type}" + PURL_NAMESPACE "${purl_namespace}" + PURL_NAME "${purl_name}" + ${purl_version_option} + ) + + if(arg_PURL_QUALIFIERS) + list(APPEND purl_args PURL_QUALIFIERS "${arg_PURL_QUALIFIERS}") + endif() + + if(arg_PURL_SUBPATH) + list(APPEND purl_args PURL_SUBPATH "${arg_PURL_SUBPATH}") + endif() + + set(${arg_OUT_VAR} "${purl_args}" PARENT_SCOPE) +endfunction() + +# Assembles an external reference purl identifier. +# PURL_TYPE and PURL_NAME are required. +# Stores the result in the OUT_VAR. +# Accepted options: +# PURL_TYPE +# PURL_NAME +# PURL_NAMESPACE +# PURL_VERSION +# PURL_SUBPATH +# PURL_QUALIFIERS +function(_qt_internal_sbom_assemble_purl target) + set(opt_args "") + set(single_args + OUT_VAR + ) + set(multi_args "") + + _qt_internal_get_sbom_purl_parsing_options(purl_opt_args purl_single_args purl_multi_args) + list(APPEND opt_args ${purl_opt_args}) + list(APPEND single_args ${purl_single_args}) + list(APPEND multi_args ${purl_multi_args}) + + cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set(purl_scheme "pkg") + + if(NOT arg_PURL_TYPE) + message(FATAL_ERROR "PURL_TYPE must be set") + endif() + + if(NOT arg_PURL_NAME) + message(FATAL_ERROR "PURL_NAME must be set") + endif() + + if(NOT arg_OUT_VAR) + message(FATAL_ERROR "OUT_VAR must be set") + endif() + + # https://github.com/package-url/purl-spec + # Spec is 'scheme:type/namespace/name@version?qualifiers#subpath' + set(purl "${purl_scheme}:${arg_PURL_TYPE}") + + if(arg_PURL_NAMESPACE) + string(APPEND purl "/${arg_PURL_NAMESPACE}") + endif() + + string(APPEND purl "/${arg_PURL_NAME}") + + if(arg_PURL_VERSION) + string(APPEND purl "@${arg_PURL_VERSION}") + endif() + + if(arg_PURL_QUALIFIERS) + # TODO: Note that the qualifiers are expected to be URL encoded, which this implementation + # is not doing at the moment. + list(JOIN arg_PURL_QUALIFIERS "&" qualifiers) + string(APPEND purl "?${qualifiers}") + endif() + + if(arg_PURL_SUBPATH) + string(APPEND purl "#${arg_PURL_SUBPATH}") + endif() + + _qt_internal_sbom_get_purl_value_extref(VALUE "${purl}" OUT_VAR result) + + set(${arg_OUT_VAR} "${result}" PARENT_SCOPE) +endfunction() + +# Takes a PURL VALUE and returns an SBOM purl external reference in OUT_VAR. +function(_qt_internal_sbom_get_purl_value_extref) + set(opt_args "") + set(single_args + OUT_VAR + VALUE + ) + 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_OUT_VAR) + message(FATAL_ERROR "OUT_VAR must be set") + endif() + + if(NOT arg_VALUE) + message(FATAL_ERROR "VALUE must be set") + endif() + + # SPDX SBOM External reference type. + set(ext_ref_prefix "PACKAGE-MANAGER purl") + set(external_ref "${ext_ref_prefix} ${arg_VALUE}") + set(result "EXTREF" "${external_ref}") + set(${arg_OUT_VAR} "${result}" PARENT_SCOPE) +endfunction() diff --git a/cmake/QtPublicSbomPythonHelpers.cmake b/cmake/QtPublicSbomPythonHelpers.cmake new file mode 100644 index 00000000000..f83314916f9 --- /dev/null +++ b/cmake/QtPublicSbomPythonHelpers.cmake @@ -0,0 +1,250 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Helper macro to find python and a given dependency. Expects the caller to set all of the vars. +# Meant to reduce the line noise due to the repeated calls. +macro(_qt_internal_sbom_find_python_and_dependency_helper_lambda) + _qt_internal_sbom_find_python_and_dependency_helper( + PYTHON_ARGS + ${extra_python_args} + ${python_common_args} + DEPENDENCY_ARGS + DEPENDENCY_IMPORT_STATEMENT "${import_statement}" + OUT_VAR_PYTHON_PATH python_path + OUT_VAR_PYTHON_FOUND python_found + OUT_VAR_DEP_FOUND dep_found + OUT_VAR_PYTHON_AND_DEP_FOUND everything_found + OUT_VAR_DEP_FIND_OUTPUT dep_find_output + ) +endmacro() + +# Tries to find python and a given dependency based on the args passed to PYTHON_ARGS and +# DEPENDENCY_ARGS which are forwarded to the respective finding functions. +# Returns the path to the python interpreter, whether it was found, whether the dependency was +# found, whether both were found, and the reason why the dependency might not be found. +function(_qt_internal_sbom_find_python_and_dependency_helper) + set(opt_args) + set(single_args + OUT_VAR_PYTHON_PATH + OUT_VAR_PYTHON_FOUND + OUT_VAR_DEP_FOUND + OUT_VAR_PYTHON_AND_DEP_FOUND + OUT_VAR_DEP_FIND_OUTPUT + ) + set(multi_args + PYTHON_ARGS + DEPENDENCY_ARGS + ) + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set(everything_found_inner FALSE) + set(deps_find_output_inner "") + + if(NOT arg_OUT_VAR_PYTHON_PATH) + message(FATAL_ERROR "OUT_VAR_PYTHON_PATH var is required") + endif() + + if(NOT arg_OUT_VAR_PYTHON_FOUND) + message(FATAL_ERROR "OUT_VAR_PYTHON_FOUND var is required") + endif() + + if(NOT arg_OUT_VAR_DEP_FOUND) + message(FATAL_ERROR "OUT_VAR_DEP_FOUND var is required") + endif() + + if(NOT arg_OUT_VAR_PYTHON_AND_DEP_FOUND) + message(FATAL_ERROR "OUT_VAR_PYTHON_AND_DEP_FOUND var is required") + endif() + + if(NOT arg_OUT_VAR_DEP_FIND_OUTPUT) + message(FATAL_ERROR "OUT_VAR_DEP_FIND_OUTPUT var is required") + endif() + + _qt_internal_sbom_find_python_helper( + ${arg_PYTHON_ARGS} + OUT_VAR_PYTHON_PATH python_path_inner + OUT_VAR_PYTHON_FOUND python_found_inner + ) + + if(python_found_inner AND python_path_inner) + _qt_internal_sbom_find_python_dependency_helper( + ${arg_DEPENDENCY_ARGS} + PYTHON_PATH "${python_path_inner}" + OUT_VAR_FOUND dep_found_inner + OUT_VAR_OUTPUT dep_find_output_inner + ) + + if(dep_found_inner) + set(everything_found_inner TRUE) + endif() + endif() + + set(${arg_OUT_VAR_PYTHON_PATH} "${python_path_inner}" PARENT_SCOPE) + set(${arg_OUT_VAR_PYTHON_FOUND} "${python_found_inner}" PARENT_SCOPE) + set(${arg_OUT_VAR_DEP_FOUND} "${dep_found_inner}" PARENT_SCOPE) + set(${arg_OUT_VAR_PYTHON_AND_DEP_FOUND} "${everything_found_inner}" PARENT_SCOPE) + set(${arg_OUT_VAR_DEP_FIND_OUTPUT} "${dep_find_output_inner}" PARENT_SCOPE) +endfunction() + +# Tries to find the python intrepreter, given the QT_SBOM_PYTHON_INTERP path hint, as well as +# other options. +# Ignores any previously found python. +# Returns the python interpreter path and whether it was successfully found. +# +# This is intentionally a function, and not a macro, to prevent overriding the Python3_EXECUTABLE +# non-cache variable in a global scope in case if a different python is found and used for a +# different purpose (e.g. qtwebengine or qtinterfaceframework). +# The reason to use a different python is that an already found python might not be the version we +# need, or might lack the dependencies we need. +# https://gitlab.kitware.com/cmake/cmake/-/issues/21797#note_901621 claims that finding multiple +# python versions in separate directory scopes is possible, and I claim a function scope is as +# good as a directory scope. +function(_qt_internal_sbom_find_python_helper) + set(opt_args + SEARCH_IN_FRAMEWORKS + QUIET + ) + set(single_args + VERSION + OUT_VAR_PYTHON_PATH + OUT_VAR_PYTHON_FOUND + ) + 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_OUT_VAR_PYTHON_PATH) + message(FATAL_ERROR "OUT_VAR_PYTHON_PATH var is required") + endif() + + if(NOT arg_OUT_VAR_PYTHON_FOUND) + message(FATAL_ERROR "OUT_VAR_PYTHON_FOUND var is required") + endif() + + # Allow disabling looking for a python interpreter shipped as part of a macOS system framework. + if(NOT arg_SEARCH_IN_FRAMEWORKS) + set(Python3_FIND_FRAMEWORK NEVER) + endif() + + set(required_version "") + if(arg_VERSION) + set(required_version "${arg_VERSION}") + endif() + + set(find_quiet "") + if(arg_QUIET) + set(find_quiet "QUIET") + endif() + + # Locally reset any executable that was possibly already found. + # We do this to ensure we always re-do the lookup/ + # This needs to be set to an empty string, to override any cache variable + set(Python3_EXECUTABLE "") + + # This needs to be unset, because the Python module checks whether the variable is defined, not + # whether it is empty. + unset(_Python3_EXECUTABLE) + + if(QT_SBOM_PYTHON_INTERP) + set(Python3_ROOT_DIR ${QT_SBOM_PYTHON_INTERP}) + endif() + + find_package(Python3 ${required_version} ${find_quiet} COMPONENTS Interpreter) + + set(${arg_OUT_VAR_PYTHON_PATH} "${Python3_EXECUTABLE}" PARENT_SCOPE) + set(${arg_OUT_VAR_PYTHON_FOUND} "${Python3_Interpreter_FOUND}" PARENT_SCOPE) +endfunction() + +# Helper that takes an python import statement to run using the given python interpreter path, +# to confirm that the given python dependency can be found. +# Returns whether the dependency was found and the output of running the import, for error handling. +function(_qt_internal_sbom_find_python_dependency_helper) + set(opt_args "") + set(single_args + DEPENDENCY_IMPORT_STATEMENT + PYTHON_PATH + OUT_VAR_FOUND + OUT_VAR_OUTPUT + ) + 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_PYTHON_PATH) + message(FATAL_ERROR "Python interpreter path not given.") + endif() + + if(NOT arg_DEPENDENCY_IMPORT_STATEMENT) + message(FATAL_ERROR "Python depdendency import statement not given.") + endif() + + if(NOT arg_OUT_VAR_FOUND) + message(FATAL_ERROR "Out var found variable not given.") + endif() + + set(python_path "${arg_PYTHON_PATH}") + execute_process( + COMMAND + ${python_path} -c "${arg_DEPENDENCY_IMPORT_STATEMENT}" + RESULT_VARIABLE res + OUTPUT_VARIABLE output + ERROR_VARIABLE output + ) + + if("${res}" STREQUAL "0") + set(found TRUE) + set(output "${output}") + else() + set(found FALSE) + string(CONCAT output "SBOM Python dependency ${arg_DEPENDENCY_IMPORT_STATEMENT} not found. " + "Error:\n${output}") + endif() + + set(${arg_OUT_VAR_FOUND} "${found}" PARENT_SCOPE) + if(arg_OUT_VAR_OUTPUT) + set(${arg_OUT_VAR_OUTPUT} "${output}" PARENT_SCOPE) + 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}") + + set(hints "") + + # The path to python installed apps is different on Windows compared to UNIX, so we use + # a different path than where the python interpreter might be located. + if(QT_SBOM_PYTHON_APPS_PATH) + list(APPEND hints ${QT_SBOM_PYTHON_APPS_PATH}) + endif() + + find_program(${cache_var} + NAMES ${program_name} + HINTS ${hints} + ) + + 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() diff --git a/cmake/QtPublicSbomSystemDepHelpers.cmake b/cmake/QtPublicSbomSystemDepHelpers.cmake new file mode 100644 index 00000000000..278a7cdf731 --- /dev/null +++ b/cmake/QtPublicSbomSystemDepHelpers.cmake @@ -0,0 +1,162 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# 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 AND TARGET "${target}") + _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_find_package PROVIDED_TARGETS might refer to non-existent targets in certain cases, + # like zstd::libzstd_shared for qt_find_package(WrapZSTD), because we are not sure what + # kind of zstd build was done. Make sure to check if the target exists before recording it. + if(TARGET "${target}") + set(target_unaliased "${target}") + get_target_property(aliased_target "${target}" ALIASED_TARGET) + if(aliased_target) + set(target_unaliased ${aliased_target}) + endif() + + _qt_internal_sbom_record_system_library_spdx_id(${target_unaliased} ${args}) + else() + message(DEBUG + "Skipping recording system library for SBOM because target does not exist: " + " ${target}") + endif() + 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) + # Some system targets like qtspeech SpeechDispatcher::SpeechDispatcher might be aliased, + # and we can't set properties on them, so unalias the target name. + set(target_original "${target}") + get_target_property(aliased_target "${target}" ALIASED_TARGET) + if(aliased_target) + set(target ${aliased_target}) + endif() + + 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() + + # Automatic system library sbom recording happens at project root source dir scope, which + # means it might accidentally pick up a qt_attribution.json file from the project root, + # that is not intended to be use for system libraries. + # For now, explicitly disable using the root attribution file. + list(APPEND args NO_CURRENT_DIR_ATTRIBUTION) + + 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_original}") + 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() diff --git a/licenseRule.json b/licenseRule.json index 9ea709c0162..b57f877305d 100644 --- a/licenseRule.json +++ b/licenseRule.json @@ -32,6 +32,11 @@ "file type" : "build system", "spdx" : ["MIT AND BSD-3-Clause"] }, + "cmake/QtPublicSbomOpsHelpers.cmake" : { + "comment" : "MIT licensed copied parts", + "file type" : "build system", + "spdx" : ["MIT AND BSD-3-Clause"] + }, "tests/auto/cmake/test_plugin_shared_static_flavor\\.cmake" : { "comment" : "Exception. This is a test file.", "file type" : "test",