cmSbom: Generate SPDX data denoting project licenses

This commit is contained in:
Daniel Tierney
2026-06-15 14:40:26 -04:00
parent 36a47953a6
commit b533f229b8
23 changed files with 232 additions and 33 deletions
+2 -1
View File
@@ -178,7 +178,8 @@ Exporting Software Bill of Materials (SBOM) Documents
[FORMAT <string>]
[PROJECT <project-name>|NO_PROJECT_METADATA]
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[LICENSE <license-string>]
[DATA_LICENSE <license-string>]
[DEFAULT_LICENSE <license-string>]
[DESCRIPTION <description-string>]
[HOMEPAGE_URL <url-string>]
[PACKAGE_URL <url-string>])
+14 -7
View File
@@ -1217,7 +1217,8 @@ Signatures
[PROJECT <project-name>|NO_PROJECT_METADATA]
[DESTINATION <dir>]
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[LICENSE <license-string>]
[DATA_LICENSE <license-string>]
[DEFAULT_LICENSE <license-string>]
[DESCRIPTION <description-string>]
[HOMEPAGE_URL <url-string>]
[PACKAGE_URL <url-string>]
@@ -1260,16 +1261,21 @@ Signatures
An informational canonical package URL for the project.
``LICENSE <license-string>``
``DATA_LICENSE <license-string>``
A |SPDX|_ (SPDX) `License Expression`_ that describes the license(s) of the
project as a whole, including documentation, resources, or other materials
distributed with the project, in addition to software artifacts. See the
SPDX `License List`_ for a list of commonly used licenses and their
identifiers.
SBOM data itself in the generated document. See the SPDX `License List`_
for a list of commonly used licenses and their identifiers. In the SPDX
format, it corresponds to the `dataLicense`_ property.
The license of individual components is taken from the
:prop_tgt:`SPDX_LICENSE` property of their respective targets.
:prop_tgt:`SPDX_LICENSE` property of their respective targets if set and
the ``DEFAULT_LICENSE`` otherwise.
``DEFAULT_LICENSE <license-string>``
A |SPDX|_ (SPDX) `License Expression`_ that describes the license(s) of any
components which do not otherwise specify their license(s).
``DESCRIPTION <description-string>``
@@ -1420,5 +1426,6 @@ and by CPack. You can also invoke this script manually with
.. _License Expression: https://spdx.github.io/spdx-spec/v3.0.1/annexes/spdx-license-expressions/
.. _License List: https://spdx.org/licenses/
.. _dataLicense: https://spdx.github.io/spdx-spec/v3.0.1/model/Core/Properties/dataLicense/
.. |SBOM| replace:: Software Bill of Material
+1 -1
View File
@@ -109,7 +109,7 @@ In order to activate support for the :command:`install(SBOM)` command,
set
* variable ``CMAKE_EXPERIMENTAL_GENERATE_SBOM`` to
* value ``2d856d6d-53e8-488b-a17f-d486d2cac317``.
* value ``248471c2-d905-4c9e-81b5-b89cd27965e1``.
This UUID may change in future versions of CMake. Be sure to use the value
documented here by the source tree of the version of CMake with which you are
+1 -1
View File
@@ -55,7 +55,7 @@ cmExperimental::FeatureData const LookupTable[] = {
{},
cmExperimental::TryCompileCondition::Never },
{ "GenerateSbom",
"2d856d6d-53e8-488b-a17f-d486d2cac317",
"248471c2-d905-4c9e-81b5-b89cd27965e1",
"CMAKE_EXPERIMENTAL_GENERATE_SBOM",
"CMake's support for generating software bill of materials (Sbom) "
"information in SPDX format is experimental. It is meant only for "
+8
View File
@@ -65,6 +65,14 @@ bool cmSbomArguments::Check(cmExecutionStatus& status) const
return false;
}
}
if (!this->License.empty()) {
// Do not allow the license argument to be provided as SBOM only accepts a
// DEFAULT_LICENSE
status.SetError("SBOM given unknown argument: \"LICENSE\".");
return false;
}
return true;
}
+4
View File
@@ -41,6 +41,8 @@ public:
ArgumentParser::NonEmpty<std::string> Format;
ArgumentParser::NonEmpty<std::string> PackageUrl;
ArgumentParser::NonEmpty<std::string> DefaultLicense;
ArgumentParser::NonEmpty<std::string> DataLicense;
protected:
cm::string_view CommandName() const override;
@@ -55,6 +57,8 @@ private:
Bind(base, parser, "SBOM"_s, &cmProjectInfoArguments::PackageName);
Bind(self, parser, "FORMAT"_s, &cmSbomArguments::Format);
Bind(self, parser, "PACKAGE_URL"_s, &cmSbomArguments::PackageUrl);
Bind(self, parser, "DEFAULT_LICENSE"_s, &cmSbomArguments::DefaultLicense);
Bind(self, parser, "DATA_LICENSE"_s, &cmSbomArguments::DataLicense);
cmProjectInfoArguments::Bind(parser, self);
}
};
+77 -11
View File
@@ -70,7 +70,8 @@ cmSbomBuilder::cmSbomBuilder(cmSbomArguments args,
, PackageDescription(std::move(args.Description))
, PackageWebsite(std::move(args.Website))
, PackageUrl(std::move(args.PackageUrl))
, PackageLicense(std::move(args.License))
, DataLicense(std::move(args.DataLicense))
, DefaultLicense(std::move(args.DefaultLicense))
, PackageFormat(args.GetFormat())
{
}
@@ -222,8 +223,12 @@ cmSpdxDocument cmSbomBuilder::GenerateSbom(cmSpdxCreationInfo const* ci) const
proj.Description = this->PackageDescription;
}
if (!this->PackageLicense.empty()) {
proj.DataLicense = this->PackageLicense;
if (!this->DataLicense.empty()) {
cmSpdxLicenseExpression license;
license.SpdxId = cmStrCat("urn:", PackageName, "#LicenseExpression");
license.CreationInfo = ci;
license.LicenseExpression = this->DataLicense;
proj.DataLicense = license;
}
return proj;
@@ -319,7 +324,31 @@ bool cmSbomBuilder::GenerateLinkProperties(
this->AddPackageInformation(pkg, pkgIt->first, pkgIt->second);
}
}
return { true, insert_back(project->Elements, std::move(pkg)) };
cmSpdxPackage const* pkgPtr =
insert_back(project->Elements, std::move(pkg));
if (!linkInfo.License.empty() &&
!cm::contains(this->GeneratedLinkLicenses, name)) {
this->GeneratedLinkLicenses.emplace(name);
cmSpdxLicenseExpression license;
license.SpdxId = cmStrCat("urn:", name, "#LicenseExpression");
license.CreationInfo = ci;
license.LicenseExpression = linkInfo.License;
cmSpdxRelationship relHasLicense;
relHasLicense.SpdxId =
cmStrCat("urn:", name, "#DeclaredLicenseRelationship");
relHasLicense.CreationInfo = ci;
relHasLicense.RelationshipType =
cmSpdxRelationship::HAS_DECLARED_LICENSE;
relHasLicense.From = pkgPtr;
relHasLicense.To.emplace_back(std::move(license));
insert_back(doc.Graph, std::move(relHasLicense));
}
return { true, pkgPtr };
}
cmSpdxPackage pkg;
@@ -368,10 +397,40 @@ bool cmSbomBuilder::GenerateProperties(
current, allTargets, config);
status &= this->GenerateLinkProperties(
doc, proj, ci, "INTERFACE_LINK_LIBRARIES", current, allTargets, config);
status &= this->GenerateMetaProperties(doc, proj, ci, current);
return status;
}
bool cmSbomBuilder::GenerateMetaProperties(
cmSbomDocument& doc, cmSpdxDocument* /*project*/,
cmSpdxCreationInfo const* ci, TargetProperties const& current) const
{
std::string licenseExpr = this->DefaultLicense;
cmValue licenseExprProp = current.Target->GetProperty("SPDX_LICENSE");
if (licenseExprProp) {
licenseExpr = licenseExprProp;
}
if (!licenseExpr.empty()) {
auto const& tgtName = current.Target->GetName();
cmSpdxLicenseExpression license;
license.SpdxId = cmStrCat("urn:", tgtName, "#LicenseExpression");
license.CreationInfo = ci;
license.LicenseExpression = std::move(licenseExpr);
cmSpdxRelationship relHasLicense;
relHasLicense.SpdxId =
cmStrCat("urn:", tgtName, "#DeclaredLicenseRelationship");
relHasLicense.CreationInfo = ci;
relHasLicense.RelationshipType = cmSpdxRelationship::HAS_DECLARED_LICENSE;
relHasLicense.From = current.Package;
relHasLicense.To.emplace_back(std::move(license));
insert_back(doc.Graph, std::move(relHasLicense));
}
return true;
}
bool cmSbomBuilder::PopulateLinkLibrariesProperty(
cmGeneratorTarget const* target,
cmGeneratorExpression::PreprocessContext preprocessRule,
@@ -428,9 +487,12 @@ bool cmSbomBuilder::NoteLinkedTarget(cmGeneratorTarget const* target,
std::string const& linkedName,
cmGeneratorTarget const* linkedTarget)
{
auto linkedLicense = linkedTarget->GetSafeProperty("SPDX_LICENSE");
if (cm::contains(this->SbomTargets, linkedTarget)) {
this->LinkTargets.emplace(linkedName,
LinkInfo{ "", linkedTarget->GetExportName() });
this->LinkTargets.emplace(
linkedName,
LinkInfo{ "", linkedTarget->GetExportName(), linkedLicense });
return true;
}
@@ -468,7 +530,8 @@ bool cmSbomBuilder::NoteLinkedTarget(cmGeneratorTarget const* target,
} else {
component = linkedName.substr(prefix.length());
}
this->LinkTargets.emplace(linkedName, LinkInfo{ pkgName, component });
this->LinkTargets.emplace(linkedName,
LinkInfo{ pkgName, component, linkedLicense });
cmPackageInformation& req =
this->Requirements.insert(std::move(*pkgInfo)).first->second;
req.Components.emplace(std::move(component));
@@ -494,9 +557,11 @@ bool cmSbomBuilder::NoteLinkedTarget(cmGeneratorTarget const* target,
std::string pkgName{ linkNamespace.data(), linkNamespace.size() - 2 };
std::string component = linkedTarget->GetExportName();
if (pkgName == this->GetPackageName()) {
this->LinkTargets.emplace(linkedName, LinkInfo{ "", component });
this->LinkTargets.emplace(linkedName,
LinkInfo{ "", component, linkedLicense });
} else {
this->LinkTargets.emplace(linkedName, LinkInfo{ pkgName, component });
this->LinkTargets.emplace(linkedName,
LinkInfo{ pkgName, component, linkedLicense });
this->Requirements[pkgName].Components.emplace(std::move(component));
}
return true;
@@ -535,7 +600,8 @@ bool cmSbomBuilder::NoteLinkedTarget(cmGeneratorTarget const* target,
"\" (first alphabetically)."));
}
std::string component = linkedTarget->GetExportName();
this->LinkTargets.emplace(linkedName, LinkInfo{ pkgName, component });
this->LinkTargets.emplace(linkedName,
LinkInfo{ pkgName, component, linkedLicense });
this->Requirements[pkgName].Components.emplace(std::move(component));
return true;
}
+9 -1
View File
@@ -129,6 +129,9 @@ protected:
TargetProperties const& current,
std::vector<TargetProperties> const& allTargets,
std::string const& config) const;
bool GenerateMetaProperties(cmSbomDocument& doc, cmSpdxDocument* project,
cmSpdxCreationInfo const* ci,
TargetProperties const& current) const;
bool NoteLinkedTarget(cmGeneratorTarget const* target,
std::string const& linkedName,
@@ -163,6 +166,7 @@ private:
{
std::string Package;
std::string Component;
std::string License;
};
// Metadata
@@ -172,7 +176,8 @@ private:
std::string const PackageDescription;
std::string const PackageWebsite;
std::string const PackageUrl;
std::string const PackageLicense;
std::string const DataLicense;
std::string const DefaultLicense;
cmSbomArguments::SbomFormat const PackageFormat;
// Derived from inputs at generate time
@@ -182,4 +187,7 @@ private:
std::map<std::string, LinkInfo> LinkTargets;
std::map<std::string, cmPackageInformation> Requirements;
std::vector<std::string> Configurations;
// Targets for which license data has been generated
mutable std::set<std::string> GeneratedLinkLicenses;
};
+19 -1
View File
@@ -128,6 +128,8 @@ std::string to_string(cmSpdxRelationship::RelationshipTypeId id)
return "contains";
case cmSpdxRelationship::RelationshipTypeId::DEPENDS_ON:
return "dependsOn";
case cmSpdxRelationship::RelationshipTypeId::HAS_DECLARED_LICENSE:
return "hasDeclaredLicense";
case cmSpdxRelationship::RelationshipTypeId::OTHER:
return "other";
}
@@ -497,6 +499,20 @@ void cmSpdxSnippet::Serialize(cmSbomSerializer& serializer) const
}
}
void cmSpdxAnyLicenseInfo::Serialize(cmSbomSerializer& serializer) const
{
cmSpdxElement::Serialize(serializer);
serializer.AddString("type", "simplelicensing_AnyLicenseInfo");
}
void cmSpdxLicenseExpression::Serialize(cmSbomSerializer& serializer) const
{
cmSpdxAnyLicenseInfo::Serialize(serializer);
serializer.AddString("type", "simplelicensing_LicenseExpression");
SerializeIfPresent(serializer, "simplelicensing_licenseExpression",
LicenseExpression);
}
void cmSpdxDocument::Serialize(cmSbomSerializer& serializer) const
{
cmSpdxElementCollection::Serialize(serializer);
@@ -508,7 +524,9 @@ void cmSpdxDocument::Serialize(cmSbomSerializer& serializer) const
if (NamespaceMap) {
serializer.AddVisitable("namespaceMap", *NamespaceMap);
}
SerializeIfPresent(serializer, "dataLicense", DataLicense);
if (DataLicense) {
serializer.AddVisitable("dataLicense", *DataLicense);
}
}
void cmSbomDocument::Serialize(cmSbomSerializer& serializer) const
+14 -1
View File
@@ -159,6 +159,7 @@ struct cmSpdxRelationship : cmSpdxElement
DESCRIBES,
CONTAINS,
DEPENDS_ON,
HAS_DECLARED_LICENSE,
OTHER
};
@@ -343,11 +344,23 @@ struct cmSpdxPackage final : cmSpdxSoftwareArtifact
void Serialize(cmSbomSerializer&) const override;
};
struct cmSpdxAnyLicenseInfo : cmSpdxElement
{
void Serialize(cmSbomSerializer&) const override;
};
struct cmSpdxLicenseExpression final : cmSpdxAnyLicenseInfo
{
cm::optional<std::string> LicenseExpression;
void Serialize(cmSbomSerializer&) const override;
};
struct cmSpdxDocument final : cmSpdxElementCollection
{
cm::optional<cmSbomObject> ExternalMap;
cm::optional<cmSbomObject> NamespaceMap;
std::string DataLicense;
cm::optional<cmSbomObject> DataLicense;
void Serialize(cmSbomSerializer& serializer) const override;
};
-1
View File
@@ -28,7 +28,6 @@ bool testSerializeSpdxJson()
cmSpdxDocument spdxValue;
spdxValue.SpdxId = "_:SPDXRef-Document";
spdxValue.DataLicense = "CC0-1.0";
auto spdx = insert_back(doc.Graph, std::move(spdxValue));
{
+1 -1
View File
@@ -2,7 +2,7 @@ include(RunCMake)
set(common_test_options
-Wno-author
"-DCMAKE_EXPERIMENTAL_GENERATE_SBOM:STRING=2d856d6d-53e8-488b-a17f-d486d2cac317"
"-DCMAKE_EXPERIMENTAL_GENERATE_SBOM:STRING=248471c2-d905-4c9e-81b5-b89cd27965e1"
"-DCMAKE_INSTALL_SBOM_FORMATS:STRING=JSON"
"-DCMAKE_INSTALL_LIBDIR=lib"
)
@@ -4,7 +4,7 @@ export(
SBOM test_targets
EXPORTS test_targets
DESCRIPTION "An eloquent description"
LICENSE "BSD-3"
DATA_LICENSE "BSD-3"
HOMEPAGE_URL "www.example.com"
PACKAGE_URL "https://example.com/test_targets.tar.gz"
VERSION "1.3.4"
+1 -1
View File
@@ -2,7 +2,7 @@ include(RunCMake)
set(common_test_options
-Wno-author
"-DCMAKE_EXPERIMENTAL_GENERATE_SBOM:STRING=2d856d6d-53e8-488b-a17f-d486d2cac317"
"-DCMAKE_EXPERIMENTAL_GENERATE_SBOM:STRING=248471c2-d905-4c9e-81b5-b89cd27965e1"
)
function(run_cmake_error test)
@@ -3,7 +3,7 @@ include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/ProjectMetadata.cmake)
install(
SBOM test_targets
DESCRIPTION "An eloquent description"
LICENSE "BSD-3"
DATA_LICENSE "BSD-3"
HOMEPAGE_URL "www.example.com"
PACKAGE_URL "https://example.com/test_targets.tar.gz"
VERSION "1.3.4"
@@ -2,7 +2,7 @@ include(RunCMake)
set(common_test_options
-Wno-author
"-DCMAKE_EXPERIMENTAL_GENERATE_SBOM:STRING=2d856d6d-53e8-488b-a17f-d486d2cac317"
"-DCMAKE_EXPERIMENTAL_GENERATE_SBOM:STRING=248471c2-d905-4c9e-81b5-b89cd27965e1"
)
function(run_cmake_error test)
@@ -36,6 +36,25 @@ set(APPLICATION_EXPECTED [=[
}
]=])
set(LICENSE_EXPECTED [=[
{
"creationInfo" : "_:Build#CreationInfo",
"from" : "urn:test#Package",
"relationshipType" : "hasDeclaredLicense",
"spdxId" : "urn:test#DeclaredLicenseRelationship",
"to" :
[
{
"creationInfo" : "_:Build#CreationInfo",
"simplelicensing_licenseExpression" : "license-for-test-target",
"spdxId" : "urn:test#LicenseExpression",
"type" : "simplelicensing_LicenseExpression"
}
],
"type" : "Relationship"
}
]=])
expect_value("${content}" "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" "@context")
string(JSON CREATION_INFO GET "${content}" "@graph" "0")
@@ -44,3 +63,6 @@ expect_object("${CREATION_INFO}" CREATION_INFO_EXPECTED)
string(JSON SPDX_DOCUMENT GET "${content}" "@graph" "1")
expect_object("${SPDX_DOCUMENT}" SPDX_DOCUMENT_EXPECTED)
expect_object("${SPDX_DOCUMENT}" APPLICATION_EXPECTED "rootElement")
string(JSON LICENSE_RELATIONSHIP GET "${content}" "@graph" "2")
expect_object("${LICENSE_RELATIONSHIP}" LICENSE_EXPECTED)
@@ -10,6 +10,7 @@ project(test LANGUAGES C
VERSION 1.2.0)
add_library(test INTERFACE)
set_target_properties(test PROPERTIES SPDX_LICENSE license-for-test-target)
install(
TARGETS test
@@ -3,7 +3,13 @@ include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
set(DOCUMENT_METADATA_EXPECTED [=[
{
"description" : "An eloquent description",
"dataLicense" : "BSD-3"
"dataLicense" :
{
"creationInfo" : "_:Build#CreationInfo",
"simplelicensing_licenseExpression" : "BSD-3",
"spdxId" : "urn:test_targets#LicenseExpression",
"type" : "simplelicensing_LicenseExpression"
}
}
]=])
@@ -119,6 +119,24 @@ set(BAR_DEPENDENCY_FOO [=[
}
]=])
set(TEST_LIBA_LINKED_LICENSE_EXPECTED [=[
{
"creationInfo" : "_:Build#CreationInfo",
"from" : "urn:test:liba#Package",
"relationshipType" : "hasDeclaredLicense",
"spdxId" : "urn:test::liba#DeclaredLicenseRelationship",
"to" :
[
{
"creationInfo" : "_:Build#CreationInfo",
"simplelicensing_licenseExpression" : "license-for-test-liba",
"spdxId" : "urn:test::liba#LicenseExpression",
"type" : "simplelicensing_LicenseExpression"
}
],
"type" : "Relationship"
}
]=])
set(CREATION_INFO_EXPECTED [=[
{
@@ -147,3 +165,7 @@ expect_object("${BAR_SPDX_DOCUMENT}" BAR_LIBC "rootElement")
expect_object("${BAR_SPDX_DOCUMENT}" BAR_LIBD "rootElement")
expect_object("${BAR_SPDX_DOCUMENT}" BAR_DEPENDENCY_TEST "element")
expect_object("${BAR_SPDX_DOCUMENT}" BAR_DEPENDENCY_FOO "element")
# Check that the license target property imported from cmake config is reported
string(JSON LICENSE_RELATIONSHIP GET "${BAR_CONTENT}" "@graph" "2")
expect_object("${LICENSE_RELATIONSHIP}" TEST_LIBA_LINKED_LICENSE_EXPECTED)
@@ -67,6 +67,25 @@ set(BUILD_LINKED_LIBRARIES_EXPECTED [=[
}
]=])
set(LICENSE_EXPECTED [=[
{
"creationInfo" : "_:Build#CreationInfo",
"from" : "urn:foo:foo#Package",
"relationshipType" : "hasDeclaredLicense",
"spdxId" : "urn:foo::foo#DeclaredLicenseRelationship",
"to" :
[
{
"creationInfo" : "_:Build#CreationInfo",
"simplelicensing_licenseExpression" : "BSD-3-Clause",
"spdxId" : "urn:foo::foo#LicenseExpression",
"type" : "simplelicensing_LicenseExpression"
}
],
"type" : "Relationship"
}
]=])
expect_value("${content}" "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" "@context")
string(JSON CREATION_INFO GET "${content}" "@graph" "0")
@@ -76,5 +95,9 @@ string(JSON SPDX_DOCUMENT GET "${content}" "@graph" "1")
expect_object("${SPDX_DOCUMENT}" SPDX_DOCUMENT_EXPECTED)
expect_object("${SPDX_DOCUMENT}" APPLICATION_EXPECTED "rootElement")
expect_object("${SPDX_DOCUMENT}" DEPENDENCY_EXPECTED "element")
string(JSON LINKED_LIBRARIES GET "${content}" "@graph" "2")
string(JSON LINKED_LIBRARIES GET "${content}" "@graph" "3")
expect_object("${LINKED_LIBRARIES}" BUILD_LINKED_LIBRARIES_EXPECTED)
# Check that default_license from the imported CPS package is reported
string(JSON LICENSE_RELATIONSHIP GET "${content}" "@graph" "2")
expect_object("${LICENSE_RELATIONSHIP}" LICENSE_EXPECTED)
@@ -1 +1,2 @@
add_library(test::liba INTERFACE IMPORTED)
set_target_properties(test::liba PROPERTIES SPDX_LICENSE license-for-test-liba)
+1 -1
View File
@@ -95,7 +95,7 @@ run_cmake_with_options(EXPORT_ANDROID_MK-AbsoluteDest-warn -Winstall-absolute-de
run_cmake_with_options(PACKAGE_INFO-AbsoluteDest-error -Werror=install-absolute-destination)
run_cmake_with_options(SBOM-AbsoluteDest-warn
-Winstall-absolute-destination
-DCMAKE_EXPERIMENTAL_GENERATE_SBOM=2d856d6d-53e8-488b-a17f-d486d2cac317)
-DCMAKE_EXPERIMENTAL_GENERATE_SBOM=248471c2-d905-4c9e-81b5-b89cd27965e1)
run_cmake_with_options(IMPORTED_RUNTIME_ARTIFACTS-AbsoluteDest-warn -Winstall-absolute-destination)
if(CMAKE_SYSTEM_NAME MATCHES "^(Linux|Darwin|Windows)$")
run_cmake_with_options(RUNTIME_DEPENDENCY_SET-AbsoluteDest-warn -Winstall-absolute-destination)