Browse Source

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

Taylor Sasser 3 months ago
parent
commit
83671f2d87

+ 2 - 0
Source/CMakeLists.txt

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

+ 86 - 0
Source/cmExportBuildSbomGenerator.cxx

@@ -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
+}

+ 41 - 0
Source/cmExportBuildSbomGenerator.h

@@ -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;
+};

+ 40 - 1
Source/cmExportCommand.cxx

@@ -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);

+ 3 - 3
Source/cmExportInstallSbomGenerator.h

@@ -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 '@'; }
 

+ 1 - 1
Source/cmExportSbomGenerator.cxx

@@ -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>

+ 1 - 0
Tests/RunCMake/CMakeLists.txt

@@ -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)

+ 2 - 0
Tests/RunCMake/ExportSbom/ApplicationTarget-install-check.cmake

@@ -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)

+ 7 - 0
Tests/RunCMake/ExportSbom/ApplicationTarget.cmake

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

+ 3 - 0
Tests/RunCMake/ExportSbom/CMakeLists.txt

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

+ 2 - 0
Tests/RunCMake/ExportSbom/InterfaceTarget-install-check.cmake

@@ -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)

+ 6 - 0
Tests/RunCMake/ExportSbom/InterfaceTarget.cmake

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

+ 2 - 0
Tests/RunCMake/ExportSbom/MissingPackageNamespace-install-check.cmake

@@ -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)

+ 7 - 0
Tests/RunCMake/ExportSbom/MissingPackageNamespace.cmake

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

+ 2 - 0
Tests/RunCMake/ExportSbom/ProjectMetadata-install-check.cmake

@@ -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)

+ 10 - 0
Tests/RunCMake/ExportSbom/ProjectMetadata.cmake

@@ -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"
+)

+ 2 - 0
Tests/RunCMake/ExportSbom/ReferencesNonExportedTarget-install-check.cmake

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

+ 3 - 0
Tests/RunCMake/ExportSbom/ReferencesNonExportedTarget.cmake

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

+ 3 - 0
Tests/RunCMake/ExportSbom/Requirements-install-check.cmake

@@ -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)

+ 4 - 0
Tests/RunCMake/ExportSbom/Requirements.cmake

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

+ 32 - 0
Tests/RunCMake/ExportSbom/RunCMakeTest.cmake

@@ -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)

+ 2 - 0
Tests/RunCMake/ExportSbom/SharedTarget-install-check.cmake

@@ -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)

+ 6 - 0
Tests/RunCMake/ExportSbom/SharedTarget.cmake

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