cmSbom: Add export(SBOM) build generators and unit tests

This commit is contained in:
Taylor Sasser
2025-11-19 10:57:48 -05:00
parent f2027a886b
commit 83671f2d87
23 changed files with 267 additions and 5 deletions

View File

@@ -203,6 +203,8 @@ add_library(
cmExportBuildFileGenerator.cxx
cmExportBuildPackageInfoGenerator.h
cmExportBuildPackageInfoGenerator.cxx
cmExportBuildSbomGenerator.h
cmExportBuildSbomGenerator.cxx
cmExportCMakeConfigGenerator.h
cmExportCMakeConfigGenerator.cxx
cmExportFileGenerator.h

View File

@@ -0,0 +1,86 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmExportBuildSbomGenerator.h"
#include <functional>
#include <utility>
#include <vector>
#include <cmext/string_view>
#include "cmGeneratorExpression.h"
#include "cmSbomArguments.h"
#include "cmSbomObject.h"
#include "cmSpdx.h"
#include "cmStringAlgorithms.h"
class cmGeneratorTarget;
cmExportBuildSbomGenerator::cmExportBuildSbomGenerator(cmSbomArguments args)
: cmExportSbomGenerator(args)
{
this->SetNamespace(cmStrCat(this->GetPackageName(), "::"_s));
}
bool cmExportBuildSbomGenerator::GenerateMainFile(std::ostream& os)
{
if (!this->CollectExports([&](cmGeneratorTarget const*) {})) {
return false;
}
cmSbomDocument doc;
doc.Graph.reserve(256);
cmSpdxDocument* project = insert_back(doc.Graph, this->GenerateSbom());
std::vector<TargetProperties> targets;
for (auto const& exp : this->Exports) {
cmGeneratorTarget const* target = exp.Target;
ImportPropertyMap properties;
this->PopulateInterfaceProperties(target, properties);
this->PopulateInterfaceLinkLibrariesProperty(
target, cmGeneratorExpression::BuildInterface, properties);
this->PopulateLinkLibrariesProperty(
target, cmGeneratorExpression::BuildInterface, properties);
targets.push_back(TargetProperties{
insert_back(project->RootElements, this->GenerateImportTarget(target)),
target, std::move(properties) });
}
for (auto const& target : targets) {
this->GenerateProperties(doc, project, target, targets);
}
this->WriteSbom(doc, os);
return true;
}
void cmExportBuildSbomGenerator::HandleMissingTarget(
std::string& /* link_libs */, cmGeneratorTarget const* /* depender */,
cmGeneratorTarget* /* dependee */)
{
}
std::string cmExportBuildSbomGenerator::GetCxxModulesDirectory() const
{
return {};
}
cm::string_view cmExportBuildSbomGenerator::GetImportPrefixWithSlash() const
{
return "";
}
std::string cmExportBuildSbomGenerator::GetCxxModuleFile(
std::string const& /*name*/) const
{
return {};
}
void cmExportBuildSbomGenerator::GenerateCxxModuleConfigInformation(
std::string const& /*name*/, std::ostream& /*os*/) const
{
// TODO
}

View File

@@ -0,0 +1,41 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#pragma once
#include "cmConfigure.h" // IWYU pragma: keep
#include <iosfwd>
#include <string>
#include <cm/string_view>
#include "cmExportBuildFileGenerator.h"
#include "cmExportSbomGenerator.h"
class cmSbomArguments;
class cmExportBuildSbomGenerator
: public cmExportBuildFileGenerator
, public cmExportSbomGenerator
{
public:
cmExportBuildSbomGenerator(cmSbomArguments args);
protected:
void HandleMissingTarget(std::string& link_libs,
cmGeneratorTarget const* depender,
cmGeneratorTarget* dependee) override;
bool GenerateMainFile(std::ostream& os) override;
void GenerateImportTargetsConfig(std::ostream&, std::string const&,
std::string const&) override
{
}
std::string GetCxxModulesDirectory() const override;
cm::string_view GetImportPrefixWithSlash() const override;
std::string GetCxxModuleFile(std::string const& /*name*/) const override;
void GenerateCxxModuleConfigInformation(std::string const& /*name*/,
std::ostream& /*os*/) const override;
};

View File

@@ -22,6 +22,7 @@
#include "cmExportBuildCMakeConfigGenerator.h"
#include "cmExportBuildFileGenerator.h"
#include "cmExportBuildPackageInfoGenerator.h"
#include "cmExportBuildSbomGenerator.h"
#include "cmExportSet.h"
#include "cmGeneratedFileStream.h"
#include "cmGlobalGenerator.h"
@@ -30,6 +31,7 @@
#include "cmPackageInfoArguments.h"
#include "cmPolicies.h"
#include "cmRange.h"
#include "cmSbomArguments.h"
#include "cmStateTypes.h"
#include "cmStringAlgorithms.h"
#include "cmSubcommandTable.h"
@@ -217,6 +219,7 @@ static bool HandleExportMode(std::vector<std::string> const& args,
ArgumentParser::NonEmpty<std::string> Filename;
ArgumentParser::NonEmpty<std::string> CxxModulesDirectory;
cm::optional<cmPackageInfoArguments> PackageInfo;
cm::optional<cmSbomArguments> Sbom;
bool ExportPackageDependencies = false;
};
@@ -243,16 +246,29 @@ static bool HandleExportMode(std::vector<std::string> const& args,
&ExportArguments::PackageInfo);
}
cmArgumentParser<cmSbomArguments> sbomParser;
cmSbomArguments::Bind(sbomParser);
if (cmExperimental::HasSupportEnabled(
status.GetMakefile(), cmExperimental::Feature::GenerateSbom)) {
parser.BindSubParser("SBOM"_s, sbomParser, &ExportArguments::Sbom);
}
std::vector<std::string> unknownArgs;
ExportArguments arguments = parser.Parse(args, &unknownArgs);
cmMakefile& mf = status.GetMakefile();
cmGlobalGenerator* gg = mf.GetGlobalGenerator();
if (arguments.PackageInfo && arguments.Sbom) {
status.SetError("PACKAGE_INFO and SBOM are mutually exclusive.");
return false;
}
if (!arguments.Check(args[0], &unknownArgs, status)) {
cmPolicies::PolicyStatus const p =
status.GetMakefile().GetPolicyStatus(cmPolicies::CMP0208);
if (arguments.PackageInfo || !unknownArgs.empty() ||
if (arguments.PackageInfo || arguments.Sbom || !unknownArgs.empty() ||
p == cmPolicies::NEW) {
return false;
}
@@ -279,11 +295,31 @@ static bool HandleExportMode(std::vector<std::string> const& args,
return false;
}
}
if (arguments.Sbom) {
if (arguments.Sbom->PackageName.empty()) {
status.SetError("SBOM missing required value.");
return false;
}
if (!arguments.Filename.empty()) {
status.SetError("SBOM and FILE are mutually exclusive.");
return false;
}
if (!arguments.Namespace.empty()) {
status.SetError("SBOM and NAMESPACE are mutually exclusive.");
return false;
}
if (!arguments.Sbom->Check(status) ||
!arguments.Sbom->SetMetadataFromProject(status)) {
return false;
}
}
std::string fname;
if (arguments.Filename.empty()) {
if (arguments.PackageInfo) {
fname = arguments.PackageInfo->GetPackageFileName();
} else if (arguments.Sbom) {
fname = arguments.Sbom->GetPackageFileName();
} else {
fname = arguments.ExportSetName + ".cmake";
}
@@ -338,6 +374,9 @@ static bool HandleExportMode(std::vector<std::string> const& args,
auto ebpg = cm::make_unique<cmExportBuildPackageInfoGenerator>(
*arguments.PackageInfo);
ebfg = std::move(ebpg);
} else if (arguments.Sbom) {
auto ebsg = cm::make_unique<cmExportBuildSbomGenerator>(*arguments.Sbom);
ebfg = std::move(ebsg);
} else {
auto ebcg = cm::make_unique<cmExportBuildCMakeConfigGenerator>();
ebcg->SetNamespace(arguments.Namespace);

View File

@@ -50,9 +50,9 @@ protected:
cmGeneratorTarget const* /* depender */,
cmGeneratorTarget* /* dependee */) override;
bool CheckInterfaceDirs(std::string const& prepro,
cmGeneratorTarget const* target,
std::string const& prop) const override;
bool CheckInterfaceDirs(std::string const& /* prepro */,
cmGeneratorTarget const* /* target */,
std::string const& /* prop */) const override;
char GetConfigFileNameSeparator() const override { return '@'; }

View File

@@ -1,5 +1,5 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmExportSbomGenerator.h"
#include <array>

View File

@@ -1341,6 +1341,7 @@ add_RunCMake_test(AutoExportDll
add_RunCMake_test(AndroidMK)
add_RunCMake_test(ExportPackageInfo)
add_RunCMake_test(ExportSbom)
add_RunCMake_test(InstallPackageInfo)
add_RunCMake_test(InstallExportsAsPackageInfo)
add_RunCMake_test(InstallSbom)

View File

@@ -0,0 +1,2 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/application_targets.spdx.json" content)
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/ApplicationTarget-install-check.cmake)

View File

@@ -0,0 +1,7 @@
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/ApplicationTarget.cmake)
export(
EXPORT application_targets
SBOM application_targets
FORMAT "spdx-3.0+json"
)

View File

@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.30)
project(${RunCMake_TEST} NONE)
include(${RunCMake_TEST}.cmake)

View File

@@ -0,0 +1,2 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/interface_targets.spdx.json" content)
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/InterfaceTarget-install-check.cmake)

View File

@@ -0,0 +1,6 @@
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/InterfaceTarget.cmake)
export(
EXPORT interface_targets
SBOM interface_targets
)

View File

@@ -0,0 +1,2 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/interface_targets.spdx.json" content)
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/MissingPackageNamespace-install-check.cmake)

View File

@@ -0,0 +1,7 @@
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/MissingPackageNamespace.cmake)
export(
EXPORT test_targets
SBOM interface_targets
VERSION 1.0.2
)

View File

@@ -0,0 +1,2 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/test_targets.spdx.json" content)
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/ProjectMetadata-install-check.cmake)

View File

@@ -0,0 +1,10 @@
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/ProjectMetadata.cmake)
export(
EXPORT test_targets
SBOM test_targets
DESCRIPTION "An eloquent description"
LICENSE "BSD-3"
HOMEPAGE_URL "www.example.com"
VERSION "1.3.4"
)

View File

@@ -0,0 +1,2 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/dog.spdx.json" content)
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/ReferencesNonExportedTarget-install-check.cmake)

View File

@@ -0,0 +1,3 @@
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/ReferencesNonExportedTarget.cmake)
export(EXPORT dog SBOM dog)

View File

@@ -0,0 +1,3 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/bar.spdx.json" BAR_CONTENT)
file(READ "${RunCMake_TEST_BINARY_DIR}/foo.spdx.json" FOO_CONTENT)
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/Requirements-install-check.cmake)

View File

@@ -0,0 +1,4 @@
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/Requirements.cmake)
export(EXPORT foo SBOM foo)
export(EXPORT bar SBOM bar)

View File

@@ -0,0 +1,32 @@
include(RunCMake)
set(common_test_options
-Wno-dev
"-DCMAKE_EXPERIMENTAL_GENERATE_SBOM:STRING=ca494ed3-b261-4205-a01f-603c95e4cae0"
"-DCMAKE_EXPERIMENTAL_FIND_CPS_PACKAGES:STRING=e82e467b-f997-4464-8ace-b00808fff261"
"-DCMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO:STRING=b80be207-778e-46ba-8080-b23bba22639e"
)
function(run_cmake_install test)
set(extra_options ${ARGN})
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-build)
set(RunCMake_TEST_INSTALL_DIR ${RunCMake_BINARY_DIR}/${test}-install)
set(RunCMake_TEST_OPTIONS ${common_test_options} ${extra_options})
list(APPEND RunCMake_TEST_OPTIONS -DCMAKE_INSTALL_PREFIX=${RunCMake_TEST_INSTALL_DIR})
if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
list(APPEND RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=DEBUG)
endif()
run_cmake(${test})
set(RunCMake_TEST_NO_CLEAN TRUE)
run_cmake_command(${test}-build ${CMAKE_COMMAND} --build . --config Debug)
run_cmake_command(${test}-install ${CMAKE_COMMAND} --install . --config Debug)
endfunction()
run_cmake_install(ApplicationTarget)
run_cmake_install(InterfaceTarget)
run_cmake_install(SharedTarget)
run_cmake_install(Requirements)
run_cmake_install(MissingPackageNamespace)
run_cmake_install(ReferencesNonExportedTarget)

View File

@@ -0,0 +1,2 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/shared_targets.spdx.json" content)
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/SharedTarget-install-check.cmake)

View File

@@ -0,0 +1,6 @@
include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/SharedTarget.cmake)
export(
EXPORT shared_targets
SBOM shared_targets
)