diff options
Diffstat (limited to 'cmake/QtPublicSbomCycloneDXHelpers.cmake')
| -rw-r--r-- | cmake/QtPublicSbomCycloneDXHelpers.cmake | 382 |
1 files changed, 382 insertions, 0 deletions
diff --git a/cmake/QtPublicSbomCycloneDXHelpers.cmake b/cmake/QtPublicSbomCycloneDXHelpers.cmake new file mode 100644 index 00000000000..a3dc52d4e39 --- /dev/null +++ b/cmake/QtPublicSbomCycloneDXHelpers.cmake @@ -0,0 +1,382 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Gets the helper python script name and relative dir in the source dir. +function(_qt_internal_sbom_get_cyclone_dx_generator_script_name + out_var_generator_name + out_var_generator_relative_dir) + set(generator_name "qt_cyclonedx_generator.py") + + _qt_internal_path_join(generator_relative_dir + "util" "sbom" "cyclonedx" "qt_cyclonedx_generator") + + set(${out_var_generator_name} "${generator_name}" PARENT_SCOPE) + set(${out_var_generator_relative_dir} "${generator_relative_dir}" PARENT_SCOPE) +endfunction() + +# Ges the path to the helper python script, which should be used to generate CycloneDX document. +# Prefers the source path over the installed path, for easier development of the script. +function(_qt_internal_sbom_get_cyclone_dx_generator_path out_var) + _qt_internal_sbom_get_cyclone_dx_generator_script_name(generator_name generator_relative_dir) + + _qt_internal_path_join(qtbase_script_path + "${QT_SOURCE_TREE}" "${generator_relative_dir}" "${generator_name}") + _qt_internal_path_join(installed_script_path + "${QT6_INSTALL_PREFIX}" "${QT6_INSTALL_LIBEXECS}" "${generator_name}") + + # qtbase sources available, always use them, regardless if it's a prefix or non-prefix build. + # Makes development easier. + if(EXISTS "${qtbase_script_path}") + set(script_path "${qtbase_script_path}") + + # qtbase sources unavailable, use installed files. + elseif(EXISTS "${installed_script_path}") + set(script_path "${installed_script_path}") + else() + message(FATAL_ERROR "Can't find ${generator_name} file.") + endif() + + set(${out_var} "${script_path}" PARENT_SCOPE) +endfunction() + +# Parses the options for a single CYDX_PROPERTY_ENTRY, and creates a toml snippet to add a +# CycloneDX property to the final toml document. +function(_qt_internal_sbom_parse_cydx_property_entry_options) + set(opt_args "") + set(single_args + CYDX_PROPERTY_NAME + CYDX_PROPERTY_VALUE + 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(NOT arg_CYDX_PROPERTY_NAME) + message(FATAL_ERROR "CYDX_PROPERTY_NAME is required.") + endif() + + if(NOT arg_CYDX_PROPERTY_VALUE) + message(FATAL_ERROR "CYDX_PROPERTY_VALUE is required.") + endif() + + if(NOT arg_OUT_VAR) + message(FATAL_ERROR "OUT_VAR is required.") + endif() + + set(${arg_OUT_VAR} " +[[components.properties]] +name = \\\"${arg_CYDX_PROPERTY_NAME}\\\" +value = \\\"${arg_CYDX_PROPERTY_VALUE}\\\" +" PARENT_SCOPE) +endfunction() + +# Processes a list of CycloneDX property entries, and creates their toml representation as output. +function(_qt_internal_sbom_handle_cydx_properties) + set(opt_args "") + set(single_args + OUT_VAR_CYDX_PROPERTIES_STRING + ) + set(multi_args + CYDX_PROPERTIES + ) + 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_CYDX_PROPERTIES_STRING) + message(FATAL_ERROR "OUT_VAR_CYDX_PROPERTIES_STRING is required.") + endif() + + # Collect each CYDX_PROPERTY_ENTRY args into a separate variable. + set(prop_idx -1) + set(prop_entry_indices "") + + foreach(prop_arg IN LISTS arg_CYDX_PROPERTIES) + if(prop_arg STREQUAL "CYDX_PROPERTY_ENTRY") + math(EXPR prop_idx "${prop_idx}+1") + list(APPEND prop_entry_indices "${prop_idx}") + elseif(prop_idx GREATER_EQUAL 0) + list(APPEND prop_${prop_idx}_args "${prop_arg}") + else() + message(FATAL_ERROR "Missing CYDX_PROPERTY_ENTRY keyword.") + endif() + endforeach() + + set(properties_string "") + + foreach(prop_idx IN LISTS prop_entry_indices) + _qt_internal_sbom_parse_cydx_property_entry_options( + ${prop_${prop_idx}_args} + OUT_VAR property_tuple + ) + + string(APPEND properties_string "${property_tuple}") + endforeach() + + set(${arg_OUT_VAR_CYDX_PROPERTIES_STRING} "${properties_string}" PARENT_SCOPE) +endfunction() + +# Outputs extra Cyclone DX properties based on the sbom entity type. +function(_qt_internal_sbom_handle_qt_entity_cydx_properties) + set(opt_args "") + set(single_args + SBOM_ENTITY_TYPE + OUT_CYDX_PROPERTIES + ) + 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_SBOM_ENTITY_TYPE) + message(FATAL_ERROR "SBOM_ENTITY_TYPE is required.") + endif() + + if(NOT arg_OUT_CYDX_PROPERTIES) + message(FATAL_ERROR "OUT_CYDX_PROPERTIES is required.") + endif() + + set(cydx_properties "") + list(APPEND cydx_properties + CYDX_PROPERTY_ENTRY + CYDX_PROPERTY_NAME "qt:sbom:entity_type" + CYDX_PROPERTY_VALUE "${arg_SBOM_ENTITY_TYPE}" + ) + + _qt_internal_sbom_is_qt_entity_type("${arg_SBOM_ENTITY_TYPE}" is_qt_entity_type) + if(is_qt_entity_type) + list(APPEND cydx_properties + CYDX_PROPERTY_ENTRY + CYDX_PROPERTY_NAME "qt:sbom:is_qt_entity_type" + CYDX_PROPERTY_VALUE "true" + ) + endif() + _qt_internal_sbom_is_qt_3rd_party_entity_type("${arg_SBOM_ENTITY_TYPE}" + is_qt_3rd_party_entity_type) + if(is_qt_3rd_party_entity_type) + list(APPEND cydx_properties + CYDX_PROPERTY_ENTRY + CYDX_PROPERTY_NAME "qt:sbom:is_qt_3rd_party_entity_type" + CYDX_PROPERTY_VALUE "true" + ) + endif() + + set(${arg_OUT_CYDX_PROPERTIES} "${cydx_properties}" PARENT_SCOPE) +endfunction() + +# Maps an sbom entity type to a cyclone dx component type. +function(_qt_internal_sbom_get_cyclone_component_type out_var sbom_entity_type) + set(library_types + "QT_MODULE" + "QT_PLUGIN" + "QML_PLUGIN" + "QT_THIRD_PARTY_MODULE" + "QT_THIRD_PARTY_SOURCES" + "SYSTEM_LIBRARY" + "LIBRARY" + "THIRD_PARTY_LIBRARY" + "THIRD_PARTY_LIBRARY_WITH_FILES" + "THIRD_PARTY_SOURCES" + ) + + set(application_types + "QT_TOOL" + "QT_APP" + "EXECUTABLE" + ) + + if(sbom_entity_type IN_LIST library_types) + set(component_type "library") + elseif(sbom_entity_type IN_LIST application_types) + set(component_type "application") + else() + # Default to library for now, because it's unclear what would be a better default. + set(component_type "library") + endif() + + set(${out_var} "${component_type}" PARENT_SCOPE) +endfunction() + +# Generates a pseudo-unique serial number for a CycloneDX sbom document. +# +# The spec says that a BOM serial number must conform to RFC 4122, but doesn't specify which +# kind of uuid version should be generated. +# The upstream python library generates a version 4 uuid, which is fully random. +# CMake can only generate version 3 and 5 uuids, which are fully deterministic based on the given +# NAMESPACE and NAME values. +# Generating a fully random uuid prevents build reproducibility. The maintainer of the Cyclone DX +# spec even mentions that here: +# https://github.com/CycloneDX/specification/issues/97#issuecomment-955904904 +# And yet to to do component-wise inter-document linking using the BOM-Link mechanism, you have to +# use serial numbers. +# +# Because the spec doesn't explicitly prohibit it, we will generate a version 5 uuid based on the +# SPDX_NAMESPACE passed to the function, which is supposed to be unique enough, because it contains +# the project / document name (e.g. qtbase) and its version or git version. +# This should alleviate the reproducibility problem as well. +function(_qt_internal_sbom_get_cyclone_bom_serial_number) + set(opt_args "") + set(single_args + SPDX_NAMESPACE + OUT_VAR_UUID + OUT_VAR_SERIAL_NUMBER + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + _qt_internal_sbom_set_default_option_value_and_error_if_empty(SPDX_NAMESPACE "") + + # This is a randomly generated uuid v4 value. To be used for all eternity. Until we change the + # implementation of the function. + set(uuid_namespace "c024642f-9853-45b2-9bfd-ab3f061a05bb") + + string(UUID uuid NAMESPACE "${uuid_namespace}" NAME "${arg_SPDX_NAMESPACE}" TYPE SHA1) + set(cyclone_dx_serial_number "urn:cdx:${uuid}") + + if(arg_OUT_VAR_UUID) + set("${arg_OUT_VAR_UUID}" "${uuid}" PARENT_SCOPE) + endif() + if(arg_OUT_VAR_SERIAL_NUMBER) + set("${arg_OUT_VAR_SERIAL_NUMBER}" "${cyclone_dx_serial_number}" PARENT_SCOPE) + endif() +endfunction() + +# See https://github.com/CycloneDX/guides/blob/main/SBOM/en/0x52-Linking.md +function(_qt_internal_sbom_get_cydx_external_bom_link target out_var) + get_target_property(spdx_id "${target}" _qt_sbom_spdx_id) + get_target_property(bom_serial_number "${target}" _qt_sbom_cydx_bom_serial_number_uuid) + + set(bom_version "1") + set(bom_link "urn:cdx:${bom_serial_number}/${bom_version}#${spdx_id}") + + set(${out_var} "${bom_link}" PARENT_SCOPE) +endfunction() + +# Records necessary details of external target dependencies in global properties, to later create +# the CycloneDX packages for them. The info collection needs to be done immediately in the directory +# scope where the targets were found, because they might not be global, and thus can't be accessed +# later. +function(_qt_internal_sbom_record_external_target_dependecies) + set(opt_args "") + set(single_args "") + set(multi_args + TARGETS + ) + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + if(NOT arg_TARGETS) + return() + endif() + + get_property(existing_ids GLOBAL PROPERTY _qt_internal_sbom_external_target_dep_ids) + if(NOT existing_ids) + set(existing_ids "") + endif() + + foreach(target IN LISTS arg_TARGETS) + # Use the full spdx id (one prefixed with the containing DocumentRef-) because that's what + # our spdx dependency relationships use at the moment. + # Both Foo and FooPrivate map to the same spdx_id, so we need to avoid duplicates on spdx id + # level. + get_target_property(spdx_id "${target}" _qt_sbom_spdx_id) + + if(spdx_id IN_LIST existing_ids) + continue() + endif() + + list(APPEND existing_ids "${spdx_id}") + set_property(GLOBAL APPEND PROPERTY _qt_internal_sbom_external_target_dep_ids "${spdx_id}") + + # This is checked in _qt_internal_sbom_add_target, to prevent duplicate creation of + # system library targets. + set_property(GLOBAL APPEND PROPERTY _qt_internal_sbom_external_target_dependencies + "${target}") + + get_target_property(package_name "${target}" _qt_sbom_package_name) + get_target_property(sbom_entity_type "${target}" _qt_sbom_entity_type) + get_target_property(package_version "${target}" _qt_sbom_package_version) + _qt_internal_sbom_get_cydx_external_bom_link("${target}" external_bom_link) + + set_property(GLOBAL + PROPERTY "_qt_internal_sbom_external_target_dep_${spdx_id}_target" + "${target}") + set_property(GLOBAL + PROPERTY "_qt_internal_sbom_external_target_dep_${spdx_id}_package_name" + "${package_name}") + set_property(GLOBAL + PROPERTY "_qt_internal_sbom_external_target_dep_${spdx_id}_sbom_entity_type" + "${sbom_entity_type}") + set_property(GLOBAL + PROPERTY "_qt_internal_sbom_external_target_dep_${spdx_id}_package_version" + "${package_version}") + set_property(GLOBAL + PROPERTY "_qt_internal_sbom_external_target_dep_${spdx_id}_external_bom_link" + "${external_bom_link}") + endforeach() +endfunction() + +# Goes through the list of recorded external target dependencies collected during target +# dependency analysis, and adds them as CycloneDX packages to the CycloneDX document. +# This is different from SPDX v2.3, which doesn't require creating a package for dependencies that +# are defined in a different document. +function(_qt_internal_sbom_add_cydx_external_target_dependencies) + get_property(spdx_ids GLOBAL PROPERTY _qt_internal_sbom_external_target_dep_ids) + if(NOT spdx_ids) + # Clean up external target dependencies, before configuring next repo project. + set_property(GLOBAL PROPERTY _qt_internal_sbom_external_target_dep_ids "") + set_property(GLOBAL PROPERTY _qt_internal_sbom_external_target_dependencies "") + return() + endif() + + # Just in case, don't add duplicates. + set(visited_spdx_ids "") + + foreach(spdx_id IN LISTS spdx_ids) + if(spdx_id IN_LIST visited_spdx_ids) + continue() + endif() + + get_cmake_property(package_name + "_qt_internal_sbom_external_target_dep_${spdx_id}_package_name") + get_cmake_property(sbom_entity_type + "_qt_internal_sbom_external_target_dep_${spdx_id}_sbom_entity_type") + get_cmake_property(package_version + "_qt_internal_sbom_external_target_dep_${spdx_id}_package_version") + get_cmake_property(external_bom_link + "_qt_internal_sbom_external_target_dep_${spdx_id}_external_bom_link") + + _qt_internal_sbom_generate_cyclone_add_package( + PACKAGE "${package_name}" + SPDXID "${spdx_id}" + SBOM_ENTITY_TYPE "${sbom_entity_type}" + VERSION "${package_version}" + EXTERNAL_BOM_LINK "${external_bom_link}" + ) + + list(APPEND visited_spdx_ids "${spdx_id}") + endforeach() + + # Clean up external target dependencies, before configuring next repo project. + set_property(GLOBAL PROPERTY _qt_internal_sbom_external_target_dep_ids "") + set_property(GLOBAL PROPERTY _qt_internal_sbom_external_target_dependencies "") +endfunction() + +# Records a license id and its text in global properties, to be added to the CycloneDX document +# later. +function(_qt_internal_sbom_record_license_cydx) + set(opt_args "") + set(single_args + LICENSE_ID + EXTRACTED_TEXT + ) + set(multi_args "") + cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}") + _qt_internal_validate_all_args_are_parsed(arg) + + set_property(GLOBAL APPEND PROPERTY + _qt_internal_sbom_cydx_licenses "${arg_LICENSE_ID}") + set_property(GLOBAL PROPERTY + _qt_internal_sbom_cydx_licenses_${arg_LICENSE_ID}_text "${arg_EXTRACTED_TEXT}" + ) +endfunction() |
