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",