qtbase/cmake/QtPublicSbomAttributionHelpers.cmake
Alexandru Croitor cf9f09cd60 CMake: Include license and license file attribution in SBOM comment
Some attribution entries don't have a SPDX license id specified, in
that case it's good to at least include the free-form license name and
file path.

Pick-to: 6.8 6.9
Task-number: QTBUG-122899
Change-Id: I75bb5c30645684ea74fe94da92ea30eb29965ad4
Reviewed-by: Alexey Edelev <alexey.edelev@qt.io>
2025-02-28 14:51:19 +01:00

597 lines
25 KiB
CMake

# 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 "")
set(single_args "")
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 is likely not a
# 3rd party library, so each attribution entry should create a separate attribution target.
# 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___QT_INTERNAL_HANDLE_QT_ENTITY_ATTRIBUTION_FILES)
_qt_internal_sbom_is_qt_entity_type("${arg_TYPE}" is_qt_entity_type)
if(is_qt_entity_type)
set(arg_CREATE_SBOM_FOR_EACH_ATTRIBUTION TRUE)
endif()
endif()
if(arg_CREATE_SBOM_FOR_EACH_ATTRIBUTION)
set(propagate_sbom_options_to_new_attribution_targets FALSE)
if(NOT arg_ATTRIBUTION_PARENT_TARGET)
message(FATAL_ERROR "ATTRIBUTION_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(ids_to_add "")
set(ids_found "")
if(arg_ATTRIBUTION_IDS)
set(ids_to_add ${arg_ATTRIBUTION_IDS})
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}"
)
# Check if we need to filter for specific ids.
if(ids_to_add AND ${out_prefix}_attribution_id)
if("${${out_prefix}_attribution_id}" IN_LIST ids_to_add)
list(APPEND ids_found "${${out_prefix}_attribution_id}")
else()
# Skip to next entry.
math(EXPR entry_index "${entry_index} + 1")
continue()
endif()
endif()
# 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}"
)
# Check if we need to filter for specific ids
if(ids_to_add AND ${out_prefix}_attribution_id)
if("${${out_prefix}_attribution_id}" IN_LIST ids_to_add)
list(APPEND ids_found "${${out_prefix}_attribution_id}")
else()
# Skip to next entry.
math(EXPR entry_index "${entry_index} + 1")
continue()
endif()
endif()
# If no Id was retrieved, just add a numeric one, to make the sbom target
# unique.
set(attribution_target "${arg_ATTRIBUTION_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()
# Sanitize the target name, to avoid issues with slashes and other unsupported chars
# in target names.
string(REGEX REPLACE "[^a-zA-Z0-9_-]" "_"
attribution_target "${attribution_target}")
set(sbom_args "")
# Always propagate the package supplier, because we assume the supplier for 3rd
# party libs is the same as the current project supplier.
# Also propagate the internal qt entity type values like CPE, supplier, PURL
# handling options, attribution file values, if set.
_qt_internal_forward_function_args(
FORWARD_APPEND
FORWARD_PREFIX arg
FORWARD_OUT_VAR sbom_args
FORWARD_OPTIONS
USE_ATTRIBUTION_FILES
__QT_INTERNAL_HANDLE_QT_ENTITY_TYPE_CPE
__QT_INTERNAL_HANDLE_QT_ENTITY_TYPE_SUPPLIER
__QT_INTERNAL_HANDLE_QT_ENTITY_TYPE_PURL
__QT_INTERNAL_HANDLE_QT_ENTITY_ATTRIBUTION_FILES
FORWARD_SINGLE
SUPPLIER
)
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
CREATE_SBOM_FOR_EACH_ATTRIBUTION
)
list(REMOVE_ITEM sbom_single_args ATTRIBUTION_ENTRY_INDEX)
list(REMOVE_ITEM sbom_multi_args
ATTRIBUTION_IDS
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.
if(arg___QT_INTERNAL_HANDLE_QT_ENTITY_ATTRIBUTION_FILES)
set(attribution_entity_type QT_THIRD_PARTY_SOURCES)
else()
set(attribution_entity_type THIRD_PARTY_SOURCES)
endif()
_qt_internal_add_sbom("${attribution_target}"
IMMEDIATE_FINALIZATION
TYPE "${attribution_entity_type}"
ATTRIBUTION_FILE_PATHS "${attribution_file_path}"
ATTRIBUTION_ENTRY_INDEX "${entry_index}"
NO_CURRENT_DIR_ATTRIBUTION
${sbom_args}
)
_qt_internal_extend_sbom_dependencies(${arg_ATTRIBUTION_PARENT_TARGET}
SBOM_DEPENDENCIES ${attribution_target}
)
endif()
math(EXPR entry_index "${entry_index} + 1")
endwhile()
math(EXPR file_index "${file_index} + 1")
endforeach()
# Show an error if an id is unaccounted for, it might be it has moved to a different file, that
# is not referenced.
if(ids_to_add)
set(attribution_ids_diff ${ids_to_add})
list(REMOVE_ITEM attribution_ids_diff ${ids_found})
if(attribution_ids_diff)
set(error_message
"The following required attribution ids were not found in the attribution files")
if(arg_ATTRIBUTION_PARENT_TARGET)
string(APPEND error_message " for target: ${arg_ATTRIBUTION_PARENT_TARGET}")
endif()
string(APPEND error_message " ids: ${attribution_ids_diff}")
message(FATAL_ERROR "${error_message}")
endif()
endif()
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)
_qt_internal_sbom_get_attribution_key(Id attribution_id "${out_prefix}")
_qt_internal_sbom_get_attribution_key(LicenseId license_id "${out_prefix}")
_qt_internal_sbom_get_attribution_key(License license "${out_prefix}")
_qt_internal_sbom_get_attribution_key(LicenseFile license_file "${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:
# $<VERSION> - Replaces occurrences of the placeholder with the value passed to the VERSION option.
# $<VERSION_DASHED> - 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 "$<VERSION>" "${arg_VERSION}" value "${value}")
string(REPLACE "$<VERSION_DASHED>" "${dashed_version}" value "${value}")
endif()
list(APPEND result "${value}")
endforeach()
set(${arg_OUT_VAR} "${result}" PARENT_SCOPE)
endfunction()