qtbase/cmake/QtPublicSbomPurlHelpers.cmake
Alexandru Croitor a28a9e194c CMake: Rework SBOM PURL handling
Previously, only 3 purl entries could be added to a target, which
tightly coupled to Qt's needs: a QT one pointing to code.qt.io,
a MIRROR one pointing to github, and an upstream one pointing to some
upstream third party location.

Rework the implementation to allow for an unlimited number of PURL
entries and to allow more flexibility when adding PURLs in a user
project.

The new syntax for adding PURLs to a target, which is also the basis
for a future public API is as follows:

qt_internal_add_sbom(<target>
    PURLS
        [[PURL_ENTRY
            PURL_ID <id>
            PURL_TYPE <type>
            PURL_NAMESPACE <namespace>
            PURL_NAME <name>
            PURL_VERSION <version>]...]
    PURL_VALUES
        [purl-string...]
)

The PURLS keyword is used to specify multiple PURL entries, each
starting with the PURL_ENTRY keyword. The PURL_VALUES keyword is used
to specify a list of pre-built purl strings.

PURL_ID is an optional argument used to identify a specific purl
entry, which is mostly needed for Qt's needs, to post-process them
further.

The rest of the options are pre-existing from the previous
implementation.

Implementation-wise, there's a new custom parser to be able to parse
and validate PURL_ENTRY arguments.

The VERSION option was renamed to PACKAGE_VERSION, to avoid some
issues in cmake_parse_arguments parsing with nested VERSION options.

The NO_PURL option was removed because it makes no sense in the new
implementation, because if you specify some PURL arguments, there is
already an intention to generate a PURL entry.

Qt entities no longer have a restriction on which specific purl ids
they can have.

The new Qt specific purl IDs have been renamed:
- QT -> GENERIC
- MIRROR -> GITHUB

Amends f7e1123620b623be0c321b54eaba7a1d618a7ce1

Pick-to: 6.8 6.9
Task-number: QTBUG-122899
Change-Id: I050decece1c6d9e6e0e06547043f864d6f497ea7
Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io>
2025-02-21 15:57:17 +01:00

306 lines
10 KiB
CMake

# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
# Parse purl arguments for a specific purl entry.
# arguments_var_name is the variable name that contains the args.
# prefix is the prefix passed to cmake_parse_arguments.
macro(_qt_internal_sbom_parse_purl_entry_options prefix arguments_var_name)
_qt_internal_get_sbom_purl_parsing_options(purl_opt_args purl_single_args purl_multi_args)
cmake_parse_arguments(${prefix} "${purl_opt_args}" "${purl_single_args}" "${purl_multi_args}"
${${arguments_var_name}})
_qt_internal_validate_all_args_are_parsed(${prefix})
endmacro()
# Helper macro to prepare forwarding all set purl options to some other function.
# Expects the options names to be set in the parent scope by calling
# _qt_internal_get_sbom_add_target_options(opt_args single_args multi_args)
macro(_qt_internal_sbom_forward_purl_handling_options args_var_name)
if(NOT opt_args)
message(FATAL_ERROR
"Expected opt_args to be set by _qt_internal_get_sbom_purl_handling_options")
endif()
if(NOT single_args)
message(FATAL_ERROR
"Expected single_args to be set by _qt_internal_get_sbom_purl_handling_options")
endif()
if(NOT multi_args)
message(FATAL_ERROR
"Expected multi_args to be set by _qt_internal_get_sbom_purl_handling_options")
endif()
_qt_internal_forward_function_args(
FORWARD_PREFIX arg
FORWARD_OUT_VAR ${args_var_name}
FORWARD_OPTIONS
${opt_args}
FORWARD_SINGLE
${single_args}
FORWARD_MULTI
${multi_args}
)
endmacro()
# Handles purl arguments specified to functions like qt_internal_add_sbom.
#
# Synopsis
#
# qt_internal_add_sbom(<target>
# PURLS
# [[PURL_ENTRY
# PURL_ID <id>
# PURL_TYPE <type>
# PURL_NAMESPACE <namespace>
# PURL_NAME <name>
# PURL_VERSION <version>]...]
# PURL_VALUES
# [purl-string...]
# )
#
# Example
#
# qt_internal_add_sbom(<target>
# PURLS
# PURL_ENTRY
# PURL_ID "UPSTREAM"
# PURL_TYPE "github"
# PURL_NAMESPACE "harfbuzz"
# PURL_NAME "harfbuzz"
# PURL_VERSION "v8.5.0"
# PURL_ENTRY
# PURL_ID "MIRROR"
# PURL_TYPE "git"
# PURL_NAMESPACE "harfbuzz"
# PURL_NAME "harfbuzz"
# PURL_QUALIFIERS "vcs_url=https://code.qt.io/qt/qtbase"
# ....
# PURL_VALUES
# pkg:git/harfbuzz/harfbuzz@v8.5.0
# pkg:github/harfbuzz/harfbuzz@v8.5.0
# ....
#
#
# PURLS accepts multiple purl entries, each starting with the PURL_ENTRY keyword.
# PURL_VALUES takes a list of pre-built purl strings.
#
# If no arguments are specified, for qt entity types (e.g. libraries built as part of Qt repos),
# default purls will be generated.
#
# There is no limit to the number of purls that can be added to a target.
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()
_qt_internal_get_sbom_purl_parsing_options(purl_opt_args purl_single_args purl_multi_args)
set(project_package_options "")
# Collect each PURL_ENTRY args into a separate variable.
set(purl_idx -1)
set(purl_entry_indices "")
foreach(purl_arg IN LISTS arg_PURLS)
if(purl_arg STREQUAL "PURL_ENTRY")
math(EXPR purl_idx "${purl_idx}+1")
list(APPEND purl_entry_indices "${purl_idx}")
elseif(purl_idx GREATER_EQUAL 0)
list(APPEND purl_${purl_idx}_args "${purl_arg}")
else()
message(FATAL_ERROR "Missing PURL_ENTRY keyword.")
endif()
endforeach()
# Validate the args for each collected entry.
foreach(purl_idx IN LISTS purl_entry_indices)
list(LENGTH purl_${purl_idx}_args num_args)
if(num_args LESS 1)
message(FATAL_ERROR "Empty PURL_ENTRY encountered.")
endif()
_qt_internal_sbom_parse_purl_entry_options(arg purl_${purl_idx}_args)
endforeach()
# Append qt specific placeholder entries when handling Qt entities.
if(arg___QT_INTERNAL_HANDLE_QT_ENTITY_TYPE_PURL)
_qt_internal_sbom_forward_purl_handling_options(purl_handling_args)
_qt_internal_sbom_handle_qt_entity_purl_entries(${purl_handling_args}
OUT_VAR_IDS qt_purl_ids
)
if(qt_purl_ids)
# Create purl placeholder indices for each qt purl id.
foreach(qt_purl_id IN LISTS qt_purl_ids)
math(EXPR purl_idx "${purl_idx}+1")
list(APPEND purl_entry_indices "${purl_idx}")
list(APPEND purl_${purl_idx}_args PURL_ID "${qt_purl_id}")
endforeach()
endif()
endif()
foreach(purl_idx IN LISTS purl_entry_indices)
# 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_entry_options(arg purl_${purl_idx}_args)
set(purl_args "")
# Override the purl version with the package version.
if(arg_PURL_USE_PACKAGE_VERSION AND arg_PACKAGE_VERSION)
set(arg_PURL_VERSION "${arg_PACKAGE_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 to gather the required args.
if(arg___QT_INTERNAL_HANDLE_QT_ENTITY_TYPE_PURL
AND arg_PURL_ID
AND arg_PURL_ID IN_LIST qt_purl_ids)
_qt_internal_sbom_handle_qt_entity_purl("${target}"
${purl_handling_args}
PURL_ID "${arg_PURL_ID}"
OUT_PURL_ARGS qt_purl_args
)
if(qt_purl_args)
list(APPEND purl_args "${qt_purl_args}")
endif()
endif()
_qt_internal_sbom_assemble_purl(${target}
${purl_args}
OUT_VAR package_manager_external_ref
)
list(APPEND project_package_options ${package_manager_external_ref})
endforeach()
foreach(purl_value IN LISTS arg_PURL_VALUES)
_qt_internal_sbom_get_purl_value_extref(
VALUE "${purl_value}" OUT_VAR package_manager_external_ref)
# 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 ${package_manager_external_ref})
endforeach()
set(${arg_OUT_VAR} "${project_package_options}" 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()