|
|
@@ -0,0 +1,383 @@
|
|
|
+/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
|
|
+ file LICENSE.rst or https://cmake.org/licensing for details. */
|
|
|
+#include "cmExportSbomGenerator.h"
|
|
|
+
|
|
|
+#include <array>
|
|
|
+#include <map>
|
|
|
+#include <set>
|
|
|
+#include <string>
|
|
|
+#include <utility>
|
|
|
+#include <vector>
|
|
|
+
|
|
|
+#include <cm/optional>
|
|
|
+#include <cmext/algorithm>
|
|
|
+
|
|
|
+#include "cmArgumentParserTypes.h"
|
|
|
+#include "cmFindPackageStack.h"
|
|
|
+#include "cmGeneratorExpression.h"
|
|
|
+#include "cmGeneratorTarget.h"
|
|
|
+#include "cmList.h"
|
|
|
+#include "cmMakefile.h"
|
|
|
+#include "cmMessageType.h"
|
|
|
+#include "cmSbomArguments.h"
|
|
|
+#include "cmSbomObject.h"
|
|
|
+#include "cmSpdx.h"
|
|
|
+#include "cmSpdxSerializer.h"
|
|
|
+#include "cmStateTypes.h"
|
|
|
+#include "cmStringAlgorithms.h"
|
|
|
+#include "cmSystemTools.h"
|
|
|
+#include "cmTarget.h"
|
|
|
+#include "cmValue.h"
|
|
|
+
|
|
|
+cmSpdxPackage::PurposeId GetPurpose(cmStateEnums::TargetType type)
|
|
|
+{
|
|
|
+ switch (type) {
|
|
|
+ case cmStateEnums::TargetType::EXECUTABLE:
|
|
|
+ return cmSpdxPackage::PurposeId::APPLICATION;
|
|
|
+ case cmStateEnums::TargetType::STATIC_LIBRARY:
|
|
|
+ case cmStateEnums::TargetType::SHARED_LIBRARY:
|
|
|
+ case cmStateEnums::TargetType::MODULE_LIBRARY:
|
|
|
+ case cmStateEnums::TargetType::OBJECT_LIBRARY:
|
|
|
+ case cmStateEnums::TargetType::INTERFACE_LIBRARY:
|
|
|
+ return cmSpdxPackage::PurposeId::LIBRARY;
|
|
|
+ case cmStateEnums::TargetType::UTILITY:
|
|
|
+ return cmSpdxPackage::PurposeId::SOURCE;
|
|
|
+ case cmStateEnums::TargetType::GLOBAL_TARGET:
|
|
|
+ case cmStateEnums::TargetType::UNKNOWN_LIBRARY:
|
|
|
+ default:
|
|
|
+ return cmSpdxPackage::PurposeId::ARCHIVE;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+cmExportSbomGenerator::cmExportSbomGenerator(cmSbomArguments args)
|
|
|
+ : PackageName(std::move(args.PackageName))
|
|
|
+ , PackageVersion(std::move(args.Version))
|
|
|
+ , PackageDescription(std::move(args.Description))
|
|
|
+ , PackageWebsite(std::move(args.Website))
|
|
|
+ , PackageLicense(std::move(args.License))
|
|
|
+ , PackageFormat(args.GetFormat())
|
|
|
+{
|
|
|
+}
|
|
|
+
|
|
|
+bool cmExportSbomGenerator::GenerateImportFile(std::ostream& os)
|
|
|
+{
|
|
|
+ return this->GenerateMainFile(os);
|
|
|
+}
|
|
|
+
|
|
|
+void cmExportSbomGenerator::WriteSbom(cmSbomDocument& doc,
|
|
|
+ std::ostream& os) const
|
|
|
+{
|
|
|
+ switch (this->PackageFormat) {
|
|
|
+ case cmSbomArguments::SbomFormat::SPDX_3_0_JSON:
|
|
|
+ cmSpdxSerializer{}.WriteSbom(os, cmSbomObject(doc));
|
|
|
+ break;
|
|
|
+ case cmSbomArguments::SbomFormat::NONE:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool cmExportSbomGenerator::AddPackageInformation(
|
|
|
+ cmSpdxPackage& artifact, std::string const& name,
|
|
|
+ cmPackageInformation const& package) const
|
|
|
+{
|
|
|
+ if (name.empty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ cmSpdxOrganization org;
|
|
|
+ org.Name = name;
|
|
|
+ artifact.OriginatedBy.emplace_back(std::move(org));
|
|
|
+
|
|
|
+ if (package.Description) {
|
|
|
+ artifact.Description = *package.Description;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (package.Version) {
|
|
|
+ artifact.PackageVersion = *package.Version;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (package.PackageUrl) {
|
|
|
+ artifact.PackageUrl = *package.PackageUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (package.License) {
|
|
|
+ artifact.CopyrightText = *package.License;
|
|
|
+ }
|
|
|
+
|
|
|
+ artifact.BuiltTime = cmSystemTools::GetCurrentDateTime("%FT%TZ");
|
|
|
+ cmSpdxExternalRef externalRef;
|
|
|
+ externalRef.Locator = cmStrCat("cmake:find_package(", name, ")");
|
|
|
+ externalRef.ExternalRefType = "buildSystem";
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+cmSpdxDocument cmExportSbomGenerator::GenerateSbom() const
|
|
|
+{
|
|
|
+ cmSpdxTool tool;
|
|
|
+ tool.SpdxId = "CMake#Agent";
|
|
|
+ tool.Name = "CMake";
|
|
|
+
|
|
|
+ cmSpdxCreationInfo ci;
|
|
|
+ ci.Created = cmSystemTools::GetCurrentDateTime("%FT%TZ");
|
|
|
+ ci.CreatedUsing = { tool };
|
|
|
+ ci.Comment = "This SBOM was generated from the CMakeLists.txt File";
|
|
|
+
|
|
|
+ cmSpdxDocument proj;
|
|
|
+ proj.Name = PackageName;
|
|
|
+ proj.SpdxId = cmStrCat(PackageName, "#SPDXDocument");
|
|
|
+ proj.ProfileConformance = { "core", "software" };
|
|
|
+ proj.CreationInfo = ci;
|
|
|
+
|
|
|
+ if (!this->PackageDescription.empty()) {
|
|
|
+ proj.Description = this->PackageDescription;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this->PackageLicense.empty()) {
|
|
|
+ proj.DataLicense = this->PackageLicense;
|
|
|
+ }
|
|
|
+
|
|
|
+ return proj;
|
|
|
+}
|
|
|
+
|
|
|
+cmSpdxPackage cmExportSbomGenerator::GenerateImportTarget(
|
|
|
+ cmGeneratorTarget const* target) const
|
|
|
+{
|
|
|
+ cmSpdxPackage package;
|
|
|
+ package.SpdxId = cmStrCat(target->GetName(), "#Package");
|
|
|
+ package.Name = target->GetName();
|
|
|
+ package.PrimaryPurpose = GetPurpose(target->GetType());
|
|
|
+
|
|
|
+ cmSpdxExternalRef buildSystem;
|
|
|
+ buildSystem.Locator = "CMake#Agent";
|
|
|
+ buildSystem.ExternalRefType = "buildSystem";
|
|
|
+ buildSystem.Comment = "Build System used for this target";
|
|
|
+ package.ExternalRef = { buildSystem };
|
|
|
+
|
|
|
+ if (!this->PackageVersion.empty()) {
|
|
|
+ package.PackageVersion = this->PackageVersion;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this->PackageWebsite.empty()) {
|
|
|
+ package.Homepage = this->PackageWebsite;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this->PackageUrl.empty()) {
|
|
|
+ package.DownloadLocation = this->PackageUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ return package;
|
|
|
+}
|
|
|
+
|
|
|
+void cmExportSbomGenerator::GenerateLinkProperties(
|
|
|
+ cmSbomDocument& doc, cmSpdxDocument* project, std::string const& libraries,
|
|
|
+ TargetProperties const& current,
|
|
|
+ std::vector<TargetProperties> const& allTargets) const
|
|
|
+{
|
|
|
+ auto itProp = current.Properties.find(libraries);
|
|
|
+ if (itProp == current.Properties.end()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::map<std::string, std::vector<std::string>> allowList = { { "LINK_ONLY",
|
|
|
+ {} } };
|
|
|
+ std::string interfaceLinkLibraries;
|
|
|
+ if (!cmGeneratorExpression::ForbidGeneratorExpressions(
|
|
|
+ current.Target, itProp->first, itProp->second, interfaceLinkLibraries,
|
|
|
+ allowList)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto makeRel = [&](char const* desc) {
|
|
|
+ cmSpdxRelationship r;
|
|
|
+ r.RelationshipType = cmSpdxRelationship::RelationshipTypeId::DEPENDS_ON;
|
|
|
+ r.Description = desc;
|
|
|
+ r.From = current.Package;
|
|
|
+ return r;
|
|
|
+ };
|
|
|
+
|
|
|
+ auto linkLibraries = makeRel("Linked Libraries");
|
|
|
+ auto linkRequires = makeRel("Required Runtime Libraries");
|
|
|
+ auto buildRequires = makeRel("Required Build-Time Libraries");
|
|
|
+
|
|
|
+ auto addArtifact =
|
|
|
+ [&](std::string const& name) -> std::pair<bool, cmSpdxPackage const*> {
|
|
|
+ auto it = this->LinkTargets.find(name);
|
|
|
+ if (it != this->LinkTargets.end()) {
|
|
|
+ LinkInfo const& linkInfo = it->second;
|
|
|
+ if (linkInfo.Package.empty()) {
|
|
|
+ for (auto const& t : allTargets) {
|
|
|
+ if (t.Target->GetName() == linkInfo.Component) {
|
|
|
+ return { true, t.Package };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ std::string pkgName =
|
|
|
+ cmStrCat(linkInfo.Package, ":", linkInfo.Component);
|
|
|
+ cmSpdxPackage pkg;
|
|
|
+ pkg.Name = pkgName;
|
|
|
+ pkg.SpdxId = cmStrCat(pkgName, "#Package");
|
|
|
+ if (!linkInfo.Package.empty()) {
|
|
|
+ auto const& pkgIt = this->Requirements.find(linkInfo.Package);
|
|
|
+ if (pkgIt != this->Requirements.end() &&
|
|
|
+ pkgIt->second.Components.count(linkInfo.Component) > 0) {
|
|
|
+ this->AddPackageInformation(pkg, pkgIt->first, pkgIt->second);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { true, insert_back(project->Elements, std::move(pkg)) };
|
|
|
+ }
|
|
|
+
|
|
|
+ cmSpdxPackage pkg;
|
|
|
+ pkg.SpdxId = cmStrCat(name, "#Package");
|
|
|
+ pkg.Name = name;
|
|
|
+ return { false, insert_back(project->Elements, std::move(pkg)) };
|
|
|
+ };
|
|
|
+
|
|
|
+ auto handleDependencies = [&](std::vector<std::string> const& names,
|
|
|
+ cmSpdxRelationship& internalDeps,
|
|
|
+ cmSpdxRelationship& externalDeps) {
|
|
|
+ for (auto const& n : names) {
|
|
|
+ auto res = addArtifact(n);
|
|
|
+ if (!res.second) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (res.first) {
|
|
|
+ internalDeps.To.push_back(res.second);
|
|
|
+ } else {
|
|
|
+ externalDeps.To.push_back(res.second);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ handleDependencies(allowList["LINK_ONLY"], linkLibraries, linkRequires);
|
|
|
+ handleDependencies(cmList{ interfaceLinkLibraries }, linkLibraries,
|
|
|
+ buildRequires);
|
|
|
+
|
|
|
+ if (!linkLibraries.To.empty()) {
|
|
|
+ insert_back(doc.Graph, std::move(linkLibraries));
|
|
|
+ }
|
|
|
+ if (!linkRequires.To.empty()) {
|
|
|
+ insert_back(doc.Graph, std::move(linkRequires));
|
|
|
+ }
|
|
|
+ if (!buildRequires.To.empty()) {
|
|
|
+ insert_back(doc.Graph, std::move(buildRequires));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool cmExportSbomGenerator::GenerateProperties(
|
|
|
+ cmSbomDocument& doc, cmSpdxDocument* proj, TargetProperties const& current,
|
|
|
+ std::vector<TargetProperties> const& allTargets) const
|
|
|
+{
|
|
|
+ this->GenerateLinkProperties(doc, proj, "LINK_LIBRARIES", current,
|
|
|
+ allTargets);
|
|
|
+ this->GenerateLinkProperties(doc, proj, "INTERFACE_LINK_LIBRARIES", current,
|
|
|
+ allTargets);
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+bool cmExportSbomGenerator::PopulateLinkLibrariesProperty(
|
|
|
+ cmGeneratorTarget const* target,
|
|
|
+ cmGeneratorExpression::PreprocessContext preprocessRule,
|
|
|
+ ImportPropertyMap& properties)
|
|
|
+{
|
|
|
+ static std::array<std::string, 3> const linkIfaceProps = {
|
|
|
+ { "LINK_LIBRARIES", "LINK_LIBRARIES_DIRECT",
|
|
|
+ "LINK_LIBRARIES_DIRECT_EXCLUDE" }
|
|
|
+ };
|
|
|
+ bool hadLINK_LIBRARIES = false;
|
|
|
+ for (std::string const& linkIfaceProp : linkIfaceProps) {
|
|
|
+ if (cmValue input = target->GetProperty(linkIfaceProp)) {
|
|
|
+ std::string prepro =
|
|
|
+ cmGeneratorExpression::Preprocess(*input, preprocessRule);
|
|
|
+ if (!prepro.empty()) {
|
|
|
+ this->ResolveTargetsInGeneratorExpressions(prepro, target,
|
|
|
+ ReplaceFreeTargets);
|
|
|
+ properties[linkIfaceProp] = prepro;
|
|
|
+ hadLINK_LIBRARIES = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return hadLINK_LIBRARIES;
|
|
|
+}
|
|
|
+
|
|
|
+bool cmExportSbomGenerator::NoteLinkedTarget(
|
|
|
+ cmGeneratorTarget const* target, std::string const& linkedName,
|
|
|
+ cmGeneratorTarget const* linkedTarget)
|
|
|
+{
|
|
|
+ if (cm::contains(this->ExportedTargets, linkedTarget)) {
|
|
|
+ this->LinkTargets.emplace(linkedName,
|
|
|
+ LinkInfo{ "", linkedTarget->GetExportName() });
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (linkedTarget->IsImported()) {
|
|
|
+ using Package = cm::optional<std::pair<std::string, cmPackageInformation>>;
|
|
|
+ auto pkgInfo = [](cmTarget* t) -> Package {
|
|
|
+ cmFindPackageStack pkgStack = t->GetFindPackageStack();
|
|
|
+ if (!pkgStack.Empty()) {
|
|
|
+ return std::make_pair(pkgStack.Top().Name, pkgStack.Top().PackageInfo);
|
|
|
+ }
|
|
|
+ std::string const pkgName =
|
|
|
+ t->GetSafeProperty("EXPORT_FIND_PACKAGE_NAME");
|
|
|
+ if (pkgName.empty()) {
|
|
|
+ return cm::nullopt;
|
|
|
+ }
|
|
|
+ cmPackageInformation package;
|
|
|
+ return std::make_pair(pkgName, package);
|
|
|
+ }(linkedTarget->Target);
|
|
|
+
|
|
|
+ if (!pkgInfo) {
|
|
|
+ target->Makefile->IssueMessage(
|
|
|
+ MessageType::AUTHOR_WARNING,
|
|
|
+ cmStrCat("Target \"", target->GetName(),
|
|
|
+ "\" references imported target \"", linkedName,
|
|
|
+ "\" which does not come from any known package."));
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string const& pkgName = pkgInfo->first;
|
|
|
+ auto const& prefix = cmStrCat(pkgName, "::");
|
|
|
+ std::string component;
|
|
|
+ if (!cmHasPrefix(linkedName, prefix)) {
|
|
|
+ component = linkedName;
|
|
|
+ } else {
|
|
|
+ component = linkedName.substr(prefix.length());
|
|
|
+ }
|
|
|
+ this->LinkTargets.emplace(linkedName, LinkInfo{ pkgName, component });
|
|
|
+ cmPackageInformation& req =
|
|
|
+ this->Requirements.insert(std::move(*pkgInfo)).first->second;
|
|
|
+ req.Components.emplace(std::move(component));
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Target belongs to another export from this build.
|
|
|
+ auto const& exportInfo = this->FindExportInfo(linkedTarget);
|
|
|
+ if (exportInfo.Namespaces.size() == 1 && exportInfo.Sets.size() == 1) {
|
|
|
+ auto const& linkNamespace = *exportInfo.Namespaces.begin();
|
|
|
+ if (!cmHasSuffix(linkNamespace, "::")) {
|
|
|
+ target->Makefile->IssueMessage(
|
|
|
+ MessageType::AUTHOR_WARNING,
|
|
|
+ cmStrCat("Target \"", target->GetName(), "\" references target \"",
|
|
|
+ linkedName,
|
|
|
+ "\", which does not use the standard namespace separator. "
|
|
|
+ "This is not allowed."));
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string pkgName{ linkNamespace.data(), linkNamespace.size() - 2 };
|
|
|
+ std::string component = linkedTarget->GetExportName();
|
|
|
+ if (pkgName == this->GetPackageName()) {
|
|
|
+ this->LinkTargets.emplace(linkedName, LinkInfo{ "", component });
|
|
|
+ } else {
|
|
|
+ this->LinkTargets.emplace(linkedName, LinkInfo{ pkgName, component });
|
|
|
+ this->Requirements[pkgName].Components.emplace(std::move(component));
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Target belongs to multiple namespaces or multiple export sets.
|
|
|
+ // cmExportFileGenerator::HandleMissingTarget should have complained about
|
|
|
+ // this already.
|
|
|
+ return false;
|
|
|
+}
|