Quellcode durchsuchen

Merge topic 'sbom-export-generators'

8ec5a595cd cmSbom: Add documentation for `install(SBOM)` and `export(SBOM)`
83671f2d87 cmSbom: Add `export(SBOM)` build generators and unit tests
f2027a886b cmSbom: Add `install(SBOM)` generator and unit tests
dcadde3662 CPS: Refactor ForbidGeneratorExpressions into cmGeneratorExpression
bcb6f689a4 cmExportInstallFileGenerator: Allow overriding of CheckInterfaceDirs
6d6c37c90d cmPackageInfoArguments: Extract common values
8c6676b2ea cmFindPackage: Fix PACKAGE_VERSION not being set when rebuilding

Acked-by: Kitware Robot <[email protected]>
Tested-by: buildbot <[email protected]>
Merge-request: !11396
Brad King vor 2 Monaten
Ursprung
Commit
da84370cff
88 geänderte Dateien mit 3061 neuen und 203 gelöschten Zeilen
  1. 26 0
      Help/command/export.rst
  2. 84 0
      Help/command/install.rst
  3. 19 0
      Help/dev/experimental.rst
  4. 12 0
      Source/CMakeLists.txt
  5. 8 0
      Source/cmExperimental.cxx
  6. 1 0
      Source/cmExperimental.h
  7. 86 0
      Source/cmExportBuildSbomGenerator.cxx
  8. 41 0
      Source/cmExportBuildSbomGenerator.h
  9. 40 1
      Source/cmExportCommand.cxx
  10. 4 3
      Source/cmExportInstallFileGenerator.h
  11. 247 0
      Source/cmExportInstallSbomGenerator.cxx
  12. 68 0
      Source/cmExportInstallSbomGenerator.h
  13. 11 52
      Source/cmExportPackageInfoGenerator.cxx
  14. 383 0
      Source/cmExportSbomGenerator.cxx
  15. 83 0
      Source/cmExportSbomGenerator.h
  16. 6 2
      Source/cmFindPackageCommand.cxx
  17. 48 0
      Source/cmGeneratorExpression.cxx
  18. 9 0
      Source/cmGeneratorExpression.h
  19. 89 0
      Source/cmInstallCommand.cxx
  20. 30 0
      Source/cmInstallSbomExportGenerator.cxx
  21. 31 0
      Source/cmInstallSbomExportGenerator.h
  22. 4 98
      Source/cmPackageInfoArguments.cxx
  23. 22 47
      Source/cmPackageInfoArguments.h
  24. 129 0
      Source/cmProjectInfoArguments.cxx
  25. 103 0
      Source/cmProjectInfoArguments.h
  26. 104 0
      Source/cmSbomArguments.cxx
  27. 61 0
      Source/cmSbomArguments.h
  28. 2 0
      Tests/RunCMake/CMakeLists.txt
  29. 2 0
      Tests/RunCMake/ExportSbom/ApplicationTarget-install-check.cmake
  30. 7 0
      Tests/RunCMake/ExportSbom/ApplicationTarget.cmake
  31. 3 0
      Tests/RunCMake/ExportSbom/CMakeLists.txt
  32. 2 0
      Tests/RunCMake/ExportSbom/InterfaceTarget-install-check.cmake
  33. 6 0
      Tests/RunCMake/ExportSbom/InterfaceTarget.cmake
  34. 2 0
      Tests/RunCMake/ExportSbom/MissingPackageNamespace-install-check.cmake
  35. 7 0
      Tests/RunCMake/ExportSbom/MissingPackageNamespace.cmake
  36. 2 0
      Tests/RunCMake/ExportSbom/ProjectMetadata-install-check.cmake
  37. 10 0
      Tests/RunCMake/ExportSbom/ProjectMetadata.cmake
  38. 2 0
      Tests/RunCMake/ExportSbom/ReferencesNonExportedTarget-install-check.cmake
  39. 3 0
      Tests/RunCMake/ExportSbom/ReferencesNonExportedTarget.cmake
  40. 3 0
      Tests/RunCMake/ExportSbom/Requirements-install-check.cmake
  41. 4 0
      Tests/RunCMake/ExportSbom/Requirements.cmake
  42. 32 0
      Tests/RunCMake/ExportSbom/RunCMakeTest.cmake
  43. 2 0
      Tests/RunCMake/ExportSbom/SharedTarget-install-check.cmake
  44. 6 0
      Tests/RunCMake/ExportSbom/SharedTarget.cmake
  45. 2 0
      Tests/RunCMake/InstallSbom/ApplicationTarget-install-check.cmake
  46. 7 0
      Tests/RunCMake/InstallSbom/ApplicationTarget.cmake
  47. 3 0
      Tests/RunCMake/InstallSbom/CMakeLists.txt
  48. 19 0
      Tests/RunCMake/InstallSbom/IgnoresInterfaceDirs.cmake
  49. 2 0
      Tests/RunCMake/InstallSbom/InterfaceTarget-install-check.cmake
  50. 6 0
      Tests/RunCMake/InstallSbom/InterfaceTarget.cmake
  51. 2 0
      Tests/RunCMake/InstallSbom/MissingPackageNamespace-install-check.cmake
  52. 7 0
      Tests/RunCMake/InstallSbom/MissingPackageNamespace.cmake
  53. 2 0
      Tests/RunCMake/InstallSbom/ProjectMetadata-install-check.cmake
  54. 11 0
      Tests/RunCMake/InstallSbom/ProjectMetadata.cmake
  55. 2 0
      Tests/RunCMake/InstallSbom/ReferencesNonExportedTarget-install-check.cmake
  56. 3 0
      Tests/RunCMake/InstallSbom/ReferencesNonExportedTarget.cmake
  57. 3 0
      Tests/RunCMake/InstallSbom/Requirements-install-check.cmake
  58. 4 0
      Tests/RunCMake/InstallSbom/Requirements.cmake
  59. 33 0
      Tests/RunCMake/InstallSbom/RunCMakeTest.cmake
  60. 2 0
      Tests/RunCMake/InstallSbom/SharedTarget-install-check.cmake
  61. 7 0
      Tests/RunCMake/InstallSbom/SharedTarget.cmake
  62. 93 0
      Tests/RunCMake/Sbom/ApplicationTarget-install-check.cmake
  63. 19 0
      Tests/RunCMake/Sbom/ApplicationTarget.cmake
  64. 223 0
      Tests/RunCMake/Sbom/Assertions.cmake
  65. 93 0
      Tests/RunCMake/Sbom/InterfaceTarget-install-check.cmake
  66. 19 0
      Tests/RunCMake/Sbom/InterfaceTarget.cmake
  67. 2 0
      Tests/RunCMake/Sbom/LICENSE.txt
  68. 93 0
      Tests/RunCMake/Sbom/MissingPackageNamespace-install-check.cmake
  69. 21 0
      Tests/RunCMake/Sbom/MissingPackageNamespace.cmake
  70. 51 0
      Tests/RunCMake/Sbom/ProjectMetadata-install-check.cmake
  71. 11 0
      Tests/RunCMake/Sbom/ProjectMetadata.cmake
  72. 74 0
      Tests/RunCMake/Sbom/ReferencesNonExportedTarget-install-check.cmake
  73. 11 0
      Tests/RunCMake/Sbom/ReferencesNonExportedTarget.cmake
  74. 137 0
      Tests/RunCMake/Sbom/Requirements-install-check.cmake
  75. 25 0
      Tests/RunCMake/Sbom/Requirements.cmake
  76. 10 0
      Tests/RunCMake/Sbom/Setup.cmake
  77. 93 0
      Tests/RunCMake/Sbom/SharedTarget-install-check.cmake
  78. 14 0
      Tests/RunCMake/Sbom/SharedTarget.cmake
  79. 29 0
      Tests/RunCMake/Sbom/cmake/bar-config-version.cmake
  80. 1 0
      Tests/RunCMake/Sbom/cmake/bar-config.cmake
  81. 12 0
      Tests/RunCMake/Sbom/cmake/bar-config.cps-meta
  82. 29 0
      Tests/RunCMake/Sbom/cmake/baz-config-version.cmake
  83. 1 0
      Tests/RunCMake/Sbom/cmake/baz-config.cmake
  84. 12 0
      Tests/RunCMake/Sbom/cmake/baz-config.cps-meta
  85. 1 0
      Tests/RunCMake/Sbom/cmake/test-config.cmake
  86. 21 0
      Tests/RunCMake/Sbom/cps/foo.cps
  87. 4 0
      Tests/RunCMake/Sbom/main.c
  88. 3 0
      Tests/RunCMake/Sbom/test.c

+ 26 - 0
Help/command/export.rst

@@ -162,6 +162,32 @@ converted to lower case.
 
 See :command:`install(PACKAGE_INFO)` for a description of the other options.
 
+Exporting Software Bill of Materials (SBOM) Documents
+"""""""""""""""""""""""""""""""""""""""""""""""""""""
+
+.. code-block:: cmake
+
+    export(EXPORT <export-name> SBOM <sbom-name>
+           [PROJECT <project-name>|NO_PROJECT_METADATA]
+           [DESTINATION <dir>]
+           [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
+           [LICENSE <license-string>]
+           [DESCRIPTION <description-string>]
+           [HOMEPAGE_URL <url-string>]
+           [PACKAGE_URL <url-string>]
+           [FORMAT <string>])
+
+.. versionadded:: 4.3
+.. note::
+
+  Experimental. Gated by ``CMAKE_EXPERIMENTAL_GENERATE_SBOM``.
+
+Generates a software bill of materials (SBOM) document describing the targets
+in the export ``<export-name>`` and their dependencies in the build tree
+
+See :command:`install(SBOM)` for details about the supported SBOM formats and a
+description of the other options.
+
 Exporting Packages
 ^^^^^^^^^^^^^^^^^^
 

+ 84 - 0
Help/command/install.rst

@@ -21,6 +21,7 @@ Synopsis
   install(`EXPORT`_ <export-name> [...])
   install(`PACKAGE_INFO`_ <package-name> [...])
   install(`RUNTIME_DEPENDENCY_SET`_ <set-name> [...])
+  install(`SBOM`_ <sbom-name> [...])
 
 Introduction
 ^^^^^^^^^^^^
@@ -1192,6 +1193,87 @@ Signatures
   :command:`install_files`, and :command:`install_programs` commands
   is not defined.
 
+.. signature::
+  install(SBOM <sbom-name> [...])
+
+  .. versionadded:: 4.3
+  .. note::
+
+    Experimental. Gated by ``CMAKE_EXPERIMENTAL_GENERATE_SBOM``.
+
+  Installs a |SBOM| or "SBOM" which describes the project:
+
+  .. code-block:: cmake
+
+    install(SBOM <sbom-name> EXPORT <export-name>
+            [PROJECT <project-name>|NO_PROJECT_METADATA]
+            [DESTINATION <dir>]
+            [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
+            [LICENSE <license-string>]
+            [DESCRIPTION <description-string>]
+            [HOMEPAGE_URL <url-string>]
+            [PACKAGE_URL <url-string>]
+            [FORMAT <string>])
+
+  The ``SBOM`` form generates a |SBOM| or "SBOM" file for a given project
+  and installs it as part of the project installation.  A |SBOM| is a
+  machine-readable description of the project's targets, linked libraries,
+  and related metadata, such as versions and license information.  CMake
+  currently generates SBOM files using the |SPDX|_ 3.0 specification in
+  its JSON-LD representation, as selected by the ``FORMAT`` option, but
+  the interface is designed to allow additional SBOM formats or schema
+  versions to be supported in future CMake releases.
+
+  Target installations are associated with the export ``<export-name>``
+  using the ``EXPORT`` option of the :command:`install(TARGETS)` signature
+  documented above. If ``DESTINATION`` is not specified, a platform-specific
+  default is used.
+
+  Several options may be used to specify package metadata:
+
+  ``VERSION <version>``
+    The package version, specified as a series of non-negative  integer components,
+    i.e. <major>[.<minor>[.<patch>[.<tweak>]]].  See :command:`project(VERSION)` for
+    more information.
+
+  ``FORMAT <string>``
+    The format in which the SBOM should be exported, which must be an expression of
+    the form ``<format>[-<version>][+<representation>]``.  CMake currently supports
+    the JSON-LD serialization of |SPDX|_ v3.0.1 (``spdx`` or ``spdx-3.0.1+json``),
+    which is also the default if ``FORMAT`` is not specified.
+
+  ``HOMEPAGE_URL <url-string>``
+
+    An informational canonical home URL for the project.
+
+  ``PACKAGE_URL <url-string>``
+
+    An informational canonical package URL for the project.
+
+  ``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.
+
+    The license of individual components is taken from the
+    :prop_tgt:`SPDX_LICENSE` property of their respective targets.
+
+  ``DESCRIPTION <description-string>``
+
+    An informational description of the project.  It is recommended that this
+    description is a relatively short string, usually no more than a few words.
+
+  By default, if the specified ``<package-name>`` matches the current CMake
+  :variable:`PROJECT_NAME`, sbom metadata will be inherited from the
+  project.  The ``PROJECT <project-name>`` option may be used to specify a
+  different project from which to inherit metadata.  If ``NO_PROJECT_METADATA``
+  is specified, automatic inheritance of sbom metadata will be disabled.
+  In any case, any metadata values specified in the ``install`` command will
+  take precedence.
+
 Examples
 ^^^^^^^^
 
@@ -1323,3 +1405,5 @@ 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/
+
+.. |SBOM| replace:: Software Bill of Material

+ 19 - 0
Help/dev/experimental.rst

@@ -139,3 +139,22 @@ you need to set the following environment variables:
 
 * ``CTEST_USE_INSTRUMENTATION=1``
 * ``CTEST_EXPERIMENTAL_INSTRUMENTATION=ec7aa2dc-b87f-45a3-8022-fe01c5f59984``
+
+Software Bill Of Materials |SBOM|
+=================================
+
+In order to activate support for the :command:`install(SBOM)` command,
+set
+
+* variable ``CMAKE_EXPERIMENTAL_GENERATE_SBOM`` to
+* value ``ca494ed3-b261-4205-a01f-603c95e4cae0``.
+
+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
+experimenting.
+
+When activated, this experimental feature provides the following:
+
+* The experimental ``install(SBOM)`` command is available to generate
+  a Software Bill of Materials or "SBOM" for the current project. See
+  :command:`install(SBOM)` for a complete overview of the command

+ 12 - 0
Source/CMakeLists.txt

@@ -204,6 +204,8 @@ add_library(
   cmExportBuildFileGenerator.cxx
   cmExportBuildPackageInfoGenerator.h
   cmExportBuildPackageInfoGenerator.cxx
+  cmExportBuildSbomGenerator.h
+  cmExportBuildSbomGenerator.cxx
   cmExportCMakeConfigGenerator.h
   cmExportCMakeConfigGenerator.cxx
   cmExportFileGenerator.h
@@ -216,10 +218,14 @@ add_library(
   cmExportInstallFileGenerator.cxx
   cmExportInstallPackageInfoGenerator.h
   cmExportInstallPackageInfoGenerator.cxx
+  cmExportInstallSbomGenerator.h
+  cmExportInstallSbomGenerator.cxx
   cmExportPackageInfoGenerator.h
   cmExportPackageInfoGenerator.cxx
   cmExportTryCompileFileGenerator.h
   cmExportTryCompileFileGenerator.cxx
+  cmExportSbomGenerator.h
+  cmExportSbomGenerator.cxx
   cmExportSet.h
   cmExportSet.cxx
   cmExternalMakefileProjectGenerator.cxx
@@ -345,6 +351,8 @@ add_library(
   cmInstallRuntimeDependencySet.cxx
   cmInstallRuntimeDependencySetGenerator.h
   cmInstallRuntimeDependencySetGenerator.cxx
+  cmInstallSbomExportGenerator.h
+  cmInstallSbomExportGenerator.cxx
   cmInstallScriptGenerator.h
   cmInstallScriptGenerator.cxx
   cmInstallSubdirectoryGenerator.h
@@ -431,6 +439,8 @@ add_library(
   cmProcessOutput.h
   cmProcessTools.cxx
   cmProcessTools.h
+  cmProjectInfoArguments.cxx
+  cmProjectInfoArguments.h
   cmValue.cxx
   cmValue.h
   cmProperty.h
@@ -706,6 +716,8 @@ add_library(
   cmRemoveDefinitionsCommand.h
   cmReturnCommand.cxx
   cmReturnCommand.h
+  cmSbomArguments.h
+  cmSbomArguments.cxx
   cmSbomObject.h
   cmSbomSerializer.h
   cmSearchPath.cxx

+ 8 - 0
Source/cmExperimental.cxx

@@ -80,6 +80,14 @@ cmExperimental::FeatureData const LookupTable[] = {
     "is meant only for experimentation and feedback to CMake developers.",
     {},
     cmExperimental::TryCompileCondition::Never },
+  { "GenerateSbom",
+    "ca494ed3-b261-4205-a01f-603c95e4cae0",
+    "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 "
+    "experimentation and feedback to CMake developers.",
+    {},
+    cmExperimental::TryCompileCondition::Never },
 };
 static_assert(sizeof(LookupTable) / sizeof(LookupTable[0]) ==
                 static_cast<size_t>(cmExperimental::Feature::Sentinel),

+ 1 - 0
Source/cmExperimental.h

@@ -24,6 +24,7 @@ public:
     MappedPackageInfo,
     ExportBuildDatabase,
     Instrumentation,
+    GenerateSbom,
 
     Sentinel,
   };

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

+ 4 - 3
Source/cmExportInstallFileGenerator.h

@@ -135,6 +135,10 @@ protected:
                                 ImportPropertyMap& properties,
                                 std::set<std::string>& importedLocations);
 
+  virtual bool CheckInterfaceDirs(std::string const& prepro,
+                                  cmGeneratorTarget const* target,
+                                  std::string const& prop) const;
+
   cmInstallExportGenerator* IEGen;
 
   // The import file generated for each configuration.
@@ -145,9 +149,6 @@ protected:
   std::map<std::string, std::vector<std::string>> ConfigCxxModuleTargetFiles;
 
 private:
-  bool CheckInterfaceDirs(std::string const& prepro,
-                          cmGeneratorTarget const* target,
-                          std::string const& prop) const;
   void PopulateCompatibleInterfaceProperties(cmGeneratorTarget const* target,
                                              ImportPropertyMap& properties);
   void PopulateCustomTransitiveInterfaceProperties(

+ 247 - 0
Source/cmExportInstallSbomGenerator.cxx

@@ -0,0 +1,247 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file LICENSE.rst or https://cmake.org/licensing for details.  */
+#include "cmExportInstallSbomGenerator.h"
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <set>
+#include <sstream>
+#include <utility>
+#include <vector>
+
+#include <cmext/string_view>
+
+#include "cmExportSet.h"
+#include "cmFileSet.h"
+#include "cmGeneratorExpression.h"
+#include "cmGeneratorTarget.h"
+#include "cmInstallExportGenerator.h"
+#include "cmInstallFileSetGenerator.h"
+#include "cmLocalGenerator.h"
+#include "cmMakefile.h"
+#include "cmMessageType.h"
+#include "cmOutputConverter.h"
+#include "cmSbomArguments.h"
+#include "cmSbomObject.h"
+#include "cmSpdx.h"
+#include "cmStateTypes.h"
+#include "cmStringAlgorithms.h"
+#include "cmSystemTools.h"
+#include "cmTarget.h"
+#include "cmTargetExport.h"
+
+cmExportInstallSbomGenerator::cmExportInstallSbomGenerator(
+  cmInstallExportGenerator* iegen, cmSbomArguments args)
+  : cmExportSbomGenerator(std::move(args))
+  , cmExportInstallFileGenerator(iegen)
+{
+  this->SetNamespace(cmStrCat(this->GetPackageName(), "::"_s));
+}
+
+std::string cmExportInstallSbomGenerator::GetConfigImportFileGlob() const
+{
+  std::string glob = cmStrCat(this->FileBase, "@*", this->FileExt);
+  return glob;
+}
+
+std::string const& cmExportInstallSbomGenerator::GetExportName() const
+{
+  return this->GetPackageName();
+}
+
+cm::string_view cmExportInstallSbomGenerator::GetImportPrefixWithSlash() const
+{
+  return "@prefix@/"_s;
+}
+
+bool cmExportInstallSbomGenerator::GenerateMainFile(std::ostream& os)
+{
+  std::vector<cmTargetExport const*> allTargets;
+  {
+    auto visitor = [&](cmTargetExport const* te) { allTargets.push_back(te); };
+
+    if (!this->CollectExports(visitor)) {
+      return false;
+    }
+  }
+  cmSbomDocument doc;
+  doc.Graph.reserve(256);
+
+  cmSpdxDocument* project = insert_back(doc.Graph, this->GenerateSbom());
+
+  std::vector<TargetProperties> targets;
+  targets.reserve(allTargets.size());
+
+  for (cmTargetExport const* te : allTargets) {
+    cmGeneratorTarget const* gt = te->Target;
+    ImportPropertyMap properties;
+    if (!this->PopulateInterfaceProperties(te, properties)) {
+      return false;
+    }
+    this->PopulateLinkLibrariesProperty(
+      gt, cmGeneratorExpression::InstallInterface, properties);
+    this->PopulateInterfaceLinkLibrariesProperty(
+      gt, cmGeneratorExpression::InstallInterface, properties);
+
+    targets.push_back(
+      TargetProperties{ insert_back(project->RootElements,
+                                    this->GenerateImportTarget(te->Target)),
+                        te->Target, std::move(properties) });
+  }
+
+  for (auto const& target : targets) {
+    this->GenerateProperties(doc, project, target, targets);
+  }
+
+  this->WriteSbom(doc, os);
+  return true;
+}
+
+void cmExportInstallSbomGenerator::GenerateImportTargetsConfig(
+  std::ostream& os, std::string const& config, std::string const& suffix)
+{
+  cmSbomDocument doc;
+  doc.Graph.reserve(256);
+
+  cmSpdxDocument* project = insert_back(doc.Graph, this->GenerateSbom());
+
+  std::vector<TargetProperties> targets;
+  std::string cfg = (config.empty() ? "noconfig" : config);
+
+  for (auto const& te : this->GetExportSet()->GetTargetExports()) {
+    ImportPropertyMap properties;
+    std::set<std::string> importedLocations;
+
+    if (this->GetExportTargetType(te.get()) !=
+        cmStateEnums::INTERFACE_LIBRARY) {
+      this->PopulateImportProperties(config, suffix, te.get(), properties,
+                                     importedLocations);
+    }
+    this->PopulateInterfaceProperties(te.get(), properties);
+    this->PopulateInterfaceLinkLibrariesProperty(
+      te->Target, cmGeneratorExpression::InstallInterface, properties);
+    this->PopulateLinkLibrariesProperty(
+      te->Target, cmGeneratorExpression::InstallInterface, properties);
+
+    targets.push_back(
+      TargetProperties{ insert_back(project->RootElements,
+                                    this->GenerateImportTarget(te->Target)),
+                        te->Target, std::move(properties) });
+  }
+
+  for (auto const& target : targets) {
+    this->GenerateProperties(doc, project, target, targets);
+  }
+
+  this->WriteSbom(doc, os);
+}
+
+std::string cmExportInstallSbomGenerator::GenerateImportPrefix() const
+{
+  std::string expDest = this->IEGen->GetDestination();
+  if (cmSystemTools::FileIsFullPath(expDest)) {
+    std::string const& installPrefix =
+      this->IEGen->GetLocalGenerator()->GetMakefile()->GetSafeDefinition(
+        "CMAKE_INSTALL_PREFIX");
+    if (cmHasPrefix(expDest, installPrefix)) {
+      auto n = installPrefix.length();
+      while (n < expDest.length() && expDest[n] == '/') {
+        ++n;
+      }
+      expDest = expDest.substr(n);
+    } else {
+      this->ReportError(
+        cmStrCat("install(SBOM \"", this->GetExportName(),
+                 "\" ...) specifies DESTINATION \"", expDest,
+                 "\" which is not a subdirectory of the install prefix."));
+      return {};
+    }
+  }
+
+  if (expDest.empty()) {
+    return this->GetInstallPrefix();
+  }
+  return cmStrCat(this->GetImportPrefixWithSlash(), expDest);
+}
+
+void cmExportInstallSbomGenerator::HandleMissingTarget(
+  std::string& /* link_libs */, cmGeneratorTarget const* /* depender */,
+  cmGeneratorTarget* /* dependee */)
+{
+}
+
+bool cmExportInstallSbomGenerator::CheckInterfaceDirs(
+  std::string const& /* prepro */, cmGeneratorTarget const* /* target */,
+  std::string const& /* prop */) const
+{
+  return true;
+}
+
+std::string cmExportInstallSbomGenerator::InstallNameDir(
+  cmGeneratorTarget const* target, std::string const& config)
+{
+  std::string install_name_dir;
+
+  cmMakefile* mf = target->Target->GetMakefile();
+  if (mf->IsOn("CMAKE_PLATFORM_HAS_INSTALLNAME")) {
+    install_name_dir =
+      target->GetInstallNameDirForInstallTree(config, "@prefix@");
+  }
+
+  return install_name_dir;
+}
+
+std::string cmExportInstallSbomGenerator::GetCxxModulesDirectory() const
+{
+  return {};
+}
+
+void cmExportInstallSbomGenerator::GenerateCxxModuleConfigInformation(
+  std::string const&, std::ostream&) const
+{
+}
+
+std::string cmExportInstallSbomGenerator::GetCxxModuleFile(
+  std::string const& /* name */) const
+{
+  return {};
+}
+
+cm::optional<std::string> cmExportInstallSbomGenerator::GetFileSetDirectory(
+  cmGeneratorTarget* gte, cmTargetExport const* te, cmFileSet* fileSet,
+  cm::optional<std::string> const& config)
+{
+  cmGeneratorExpression ge(*gte->Makefile->GetCMakeInstance());
+  auto cge =
+    ge.Parse(te->FileSetGenerators.at(fileSet->GetName())->GetDestination());
+
+  std::string const unescapedDest =
+    cge->Evaluate(gte->LocalGenerator, config.value_or(""), gte);
+  bool const isConfigDependent = cge->GetHadContextSensitiveCondition();
+
+  if (config && !isConfigDependent) {
+    return {};
+  }
+
+  std::string const& type = fileSet->GetType();
+  if (config && (type == "CXX_MODULES"_s)) {
+    cmMakefile* mf = gte->LocalGenerator->GetMakefile();
+    std::ostringstream e;
+    e << "The \"" << gte->GetName() << "\" target's interface file set \""
+      << fileSet->GetName() << "\" of type \"" << type
+      << "\" contains context-sensitive base file entries which is not "
+         "supported.";
+    mf->IssueMessage(MessageType::FATAL_ERROR, e.str());
+    return {};
+  }
+
+  cm::optional<std::string> dest = cmOutputConverter::EscapeForCMake(
+    unescapedDest, cmOutputConverter::WrapQuotes::NoWrap);
+
+  if (!cmSystemTools::FileIsFullPath(unescapedDest)) {
+    dest = cmStrCat("@prefix@/"_s, *dest);
+  }
+
+  return dest;
+}

+ 68 - 0
Source/cmExportInstallSbomGenerator.h

@@ -0,0 +1,68 @@
+/* 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/optional>
+#include <cm/string_view>
+
+#include "cmExportInstallFileGenerator.h"
+#include "cmExportSbomGenerator.h"
+
+class cmFileSet;
+class cmGeneratorTarget;
+class cmInstallExportGenerator;
+class cmSbomArguments;
+class cmTargetExport;
+
+class cmExportInstallSbomGenerator
+  : public cmExportSbomGenerator
+  , public cmExportInstallFileGenerator
+{
+public:
+  /** Construct with the export installer that will install the
+      files.  */
+  cmExportInstallSbomGenerator(cmInstallExportGenerator* iegen,
+                               cmSbomArguments arguments);
+
+  /** Compute the globbing expression used to load per-config import
+      files from the main file.  */
+  std::string GetConfigImportFileGlob() const override;
+
+protected:
+  std::string const& GetExportName() const override;
+
+  cm::string_view GetImportPrefixWithSlash() const override;
+  std::string GetCxxModuleFile(std::string const& name) const override;
+  void GenerateCxxModuleConfigInformation(std::string const&,
+                                          std::ostream& os) const override;
+
+  // Implement virtual methods from the superclass.
+  bool GenerateMainFile(std::ostream& os) override;
+  void GenerateImportTargetsConfig(std::ostream& os, std::string const& config,
+                                   std::string const& suffix) override;
+
+  void HandleMissingTarget(std::string& /* link_libs */,
+                           cmGeneratorTarget const* /* depender */,
+                           cmGeneratorTarget* /* dependee */) override;
+
+  bool CheckInterfaceDirs(std::string const& /* prepro */,
+                          cmGeneratorTarget const* /* target */,
+                          std::string const& /* prop */) const override;
+
+  char GetConfigFileNameSeparator() const override { return '@'; }
+
+  std::string GenerateImportPrefix() const;
+  std::string InstallNameDir(cmGeneratorTarget const* target,
+                             std::string const& config) override;
+
+  std::string GetCxxModulesDirectory() const override;
+
+  cm::optional<std::string> GetFileSetDirectory(
+    cmGeneratorTarget* gte, cmTargetExport const* te, cmFileSet* fileSet,
+    cm::optional<std::string> const& config = {});
+};

+ 11 - 52
Source/cmExportPackageInfoGenerator.cxx

@@ -2,7 +2,6 @@
    file LICENSE.rst or https://cmake.org/licensing for details.  */
 #include "cmExportPackageInfoGenerator.h"
 
-#include <cstddef>
 #include <memory>
 #include <set>
 #include <utility>
@@ -295,51 +294,6 @@ bool cmExportPackageInfoGenerator::GenerateInterfaceProperties(
   return result;
 }
 
-namespace {
-bool ForbidGeneratorExpressions(
-  cmGeneratorTarget const* target, std::string const& propertyName,
-  std::string const& propertyValue, std::string& evaluatedValue,
-  std::map<std::string, std::vector<std::string>>& allowList)
-{
-  size_t const allowedExpressions = allowList.size();
-  evaluatedValue = cmGeneratorExpression::Collect(propertyValue, allowList);
-  if (evaluatedValue != propertyValue &&
-      allowList.size() > allowedExpressions) {
-    target->Makefile->IssueMessage(
-      MessageType::FATAL_ERROR,
-      cmStrCat("Property \"", propertyName, "\" of target \"",
-               target->GetName(),
-               "\" contains a generator expression. This is not allowed."));
-    return false;
-  }
-  // Forbid Nested Generator Expressions
-  for (auto const& genexp : allowList) {
-    for (auto const& value : genexp.second) {
-      if (value.find("$<") != std::string::npos) {
-        target->Makefile->IssueMessage(
-          MessageType::FATAL_ERROR,
-          cmStrCat(
-            "$<", genexp.first, ":...> expression in \"", propertyName,
-            "\" of target \"", target->GetName(),
-            "\" contains a generator expression. This is not allowed."));
-        return false;
-      }
-    }
-  }
-  return true;
-}
-
-bool ForbidGeneratorExpressions(cmGeneratorTarget const* target,
-                                std::string const& propertyName,
-                                std::string const& propertyValue)
-{
-  std::map<std::string, std::vector<std::string>> allowList;
-  std::string evaluatedValue;
-  return ForbidGeneratorExpressions(target, propertyName, propertyValue,
-                                    evaluatedValue, allowList);
-}
-}
-
 bool cmExportPackageInfoGenerator::NoteLinkedTarget(
   cmGeneratorTarget const* target, std::string const& linkedName,
   cmGeneratorTarget const* linkedTarget)
@@ -446,8 +400,9 @@ void cmExportPackageInfoGenerator::GenerateInterfaceLinkProperties(
   std::map<std::string, std::vector<std::string>> allowList = { { "LINK_ONLY",
                                                                   {} } };
   std::string interfaceLinkLibraries;
-  if (!ForbidGeneratorExpressions(target, iter->first, iter->second,
-                                  interfaceLinkLibraries, allowList)) {
+  if (!cmGeneratorExpression::ForbidGeneratorExpressions(
+        target, iter->first, iter->second, interfaceLinkLibraries,
+        allowList)) {
     result = false;
     return;
   }
@@ -490,7 +445,8 @@ void cmExportPackageInfoGenerator::GenerateInterfaceCompileFeatures(
     return;
   }
 
-  if (!ForbidGeneratorExpressions(target, iter->first, iter->second)) {
+  if (!cmGeneratorExpression::ForbidGeneratorExpressions(target, iter->first,
+                                                         iter->second)) {
     result = false;
     return;
   }
@@ -519,7 +475,8 @@ void cmExportPackageInfoGenerator::GenerateInterfaceCompileDefines(
   }
 
   // TODO: Support language-specific defines.
-  if (!ForbidGeneratorExpressions(target, iter->first, iter->second)) {
+  if (!cmGeneratorExpression::ForbidGeneratorExpressions(target, iter->first,
+                                                         iter->second)) {
     result = false;
     return;
   }
@@ -550,7 +507,8 @@ void cmExportPackageInfoGenerator::GenerateInterfaceListProperty(
     return;
   }
 
-  if (!ForbidGeneratorExpressions(target, prop, iter->second)) {
+  if (!cmGeneratorExpression::ForbidGeneratorExpressions(target, prop,
+                                                         iter->second)) {
     result = false;
     return;
   }
@@ -571,7 +529,8 @@ void cmExportPackageInfoGenerator::GenerateProperty(
     return;
   }
 
-  if (!ForbidGeneratorExpressions(target, inName, iter->second)) {
+  if (!cmGeneratorExpression::ForbidGeneratorExpressions(target, inName,
+                                                         iter->second)) {
     result = false;
     return;
   }

+ 383 - 0
Source/cmExportSbomGenerator.cxx

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

+ 83 - 0
Source/cmExportSbomGenerator.h

@@ -0,0 +1,83 @@
+/* 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 <map>
+#include <string>
+#include <vector>
+
+#include "cmExportFileGenerator.h"
+#include "cmFindPackageStack.h"
+#include "cmGeneratorExpression.h"
+#include "cmSbomArguments.h"
+
+class cmGeneratorTarget;
+struct cmSbomDocument;
+struct cmSpdxDocument;
+struct cmSpdxPackage;
+
+class cmExportSbomGenerator : virtual public cmExportFileGenerator
+{
+public:
+  cmExportSbomGenerator(cmSbomArguments args);
+  using cmExportFileGenerator::GenerateImportFile;
+
+protected:
+  using ImportPropertyMap = std::map<std::string, std::string>;
+
+  struct TargetProperties
+  {
+    cmSpdxPackage const* Package;
+    cmGeneratorTarget const* Target;
+    ImportPropertyMap Properties;
+  };
+
+  void WriteSbom(cmSbomDocument& doc, std::ostream& os) const;
+
+  cmSpdxDocument GenerateSbom() const;
+  cmSpdxPackage GenerateImportTarget(cmGeneratorTarget const* target) const;
+
+  std::string const& GetPackageName() const { return this->PackageName; }
+
+  bool GenerateImportFile(std::ostream& os) override;
+  bool AddPackageInformation(cmSpdxPackage& artifact, std::string const& name,
+                             cmPackageInformation const& package) const;
+
+  bool GenerateProperties(
+    cmSbomDocument& doc, cmSpdxDocument* project,
+    TargetProperties const& current,
+    std::vector<TargetProperties> const& allTargets) const;
+
+  void GenerateLinkProperties(
+    cmSbomDocument& doc, cmSpdxDocument* project, std::string const& libraries,
+    TargetProperties const& current,
+    std::vector<TargetProperties> const& allTargets) const;
+
+  bool NoteLinkedTarget(cmGeneratorTarget const* target,
+                        std::string const& linkedName,
+                        cmGeneratorTarget const* linkedTarget) override;
+
+  bool PopulateLinkLibrariesProperty(cmGeneratorTarget const* target,
+                                     cmGeneratorExpression::PreprocessContext,
+                                     ImportPropertyMap& properties);
+
+private:
+  struct LinkInfo
+  {
+    std::string Package;
+    std::string Component;
+  };
+
+  std::string const PackageName;
+  std::string const PackageVersion;
+  std::string const PackageDescription;
+  std::string const PackageWebsite;
+  std::string const PackageLicense;
+  std::string const PackageUrl;
+  cmSbomArguments::SbomFormat const PackageFormat;
+  std::map<std::string, LinkInfo> LinkTargets;
+  std::map<std::string, cmPackageInformation> Requirements;
+};

+ 6 - 2
Source/cmFindPackageCommand.cxx

@@ -1671,6 +1671,12 @@ bool cmFindPackageCommand::HandlePackageMode(
         "fileFound is true but FileFound is empty!");
       fileFound = false;
     }
+
+    if (fileFound) {
+      this->CurrentPackageInfo->Directory =
+        cmSystemTools::GetFilenamePath(this->FileFound);
+      this->CurrentPackageInfo->Version = this->VersionFound;
+    }
   }
 
   std::string const foundVar = cmStrCat(this->Name, "_FOUND");
@@ -1975,8 +1981,6 @@ bool cmFindPackageCommand::FindConfig()
   std::string init;
   if (found) {
     init = cmSystemTools::GetFilenamePath(this->FileFound);
-    this->CurrentPackageInfo->Directory = init;
-    this->CurrentPackageInfo->Version = this->VersionFound;
   } else {
     init = this->Variable + "-NOTFOUND";
   }

+ 48 - 0
Source/cmGeneratorExpression.cxx

@@ -4,6 +4,7 @@
 
 #include <algorithm>
 #include <cassert>
+#include <cstddef>
 #include <memory>
 #include <stack>
 #include <utility>
@@ -18,8 +19,11 @@
 #include "cmGeneratorExpressionEvaluator.h"
 #include "cmGeneratorExpressionLexer.h"
 #include "cmGeneratorExpressionParser.h"
+#include "cmGeneratorTarget.h"
 #include "cmList.h"
 #include "cmLocalGenerator.h"
+#include "cmMakefile.h"
+#include "cmMessageType.h"
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
 #include "cmake.h"
@@ -414,6 +418,50 @@ std::string cmGeneratorExpression::Collect(
   return extractAllGeneratorExpressions(input, &collected);
 }
 
+bool cmGeneratorExpression::ForbidGeneratorExpressions(
+  cmGeneratorTarget const* target, std::string const& propertyName,
+  std::string const& propertyValue)
+{
+  std::map<std::string, std::vector<std::string>> allowList;
+  std::string evaluatedValue;
+  return ForbidGeneratorExpressions(target, propertyName, propertyValue,
+                                    evaluatedValue, allowList);
+}
+
+bool cmGeneratorExpression::ForbidGeneratorExpressions(
+  cmGeneratorTarget const* target, std::string const& propertyName,
+  std::string const& propertyValue, std::string& evaluatedValue,
+  std::map<std::string, std::vector<std::string>>& allowList)
+{
+  size_t const initialAllowedGenExps = allowList.size();
+  evaluatedValue = Collect(propertyValue, allowList);
+  if (evaluatedValue != propertyValue &&
+      allowList.size() > initialAllowedGenExps) {
+    target->Makefile->IssueMessage(
+      MessageType::FATAL_ERROR,
+      cmStrCat("Property \"", propertyName, "\" of target \"",
+               target->GetName(),
+               "\" contains a generator expression. This is not allowed."));
+    return false;
+  }
+
+  // Check for nested generator expressions (e.g., $<LINK_ONLY:$<...>>).
+  for (auto const& genexp : allowList) {
+    for (auto const& value : genexp.second) {
+      if (value.find("$<") != std::string::npos) {
+        target->Makefile->IssueMessage(
+          MessageType::FATAL_ERROR,
+          cmStrCat("$<", genexp.first, ":...> expression in \"", propertyName,
+                   "\" of target \"", target->GetName(),
+                   "\" contains a generator expression. This is not "
+                   "allowed."));
+        return false;
+      }
+    }
+  }
+  return true;
+}
+
 cm::string_view::size_type cmGeneratorExpression::Find(cm::string_view input)
 {
   cm::string_view::size_type const openpos = input.find("$<");

+ 9 - 0
Source/cmGeneratorExpression.h

@@ -58,6 +58,15 @@ public:
     cmGeneratorTarget const* currentTarget = nullptr,
     std::string const& language = std::string());
 
+  static bool ForbidGeneratorExpressions(
+    cmGeneratorTarget const* target, std::string const& propertyName,
+    std::string const& propertyValue, std::string& evaluatedValue,
+    std::map<std::string, std::vector<std::string>>& allowList);
+
+  static bool ForbidGeneratorExpressions(cmGeneratorTarget const* target,
+                                         std::string const& propertyName,
+                                         std::string const& propertyValue);
+
   enum PreprocessContext
   {
     StripAllGeneratorExpressions,

+ 89 - 0
Source/cmInstallCommand.cxx

@@ -39,6 +39,7 @@
 #include "cmInstallPackageInfoExportGenerator.h"
 #include "cmInstallRuntimeDependencySet.h"
 #include "cmInstallRuntimeDependencySetGenerator.h"
+#include "cmInstallSbomExportGenerator.h"
 #include "cmInstallScriptGenerator.h"
 #include "cmInstallTargetGenerator.h"
 #include "cmList.h"
@@ -48,6 +49,7 @@
 #include "cmPolicies.h"
 #include "cmRange.h"
 #include "cmRuntimeDependencyArchive.h"
+#include "cmSbomArguments.h"
 #include "cmStateTypes.h"
 #include "cmStringAlgorithms.h"
 #include "cmSubcommandTable.h"
@@ -2513,6 +2515,92 @@ bool HandleRuntimeDependencySetMode(std::vector<std::string> const& args,
   return true;
 }
 
+bool HandleSbomMode(std::vector<std::string> const& args,
+                    cmExecutionStatus& status)
+{
+#ifndef CMAKE_BOOTSTRAP
+  if (!cmExperimental::HasSupportEnabled(
+        status.GetMakefile(), cmExperimental::Feature::GenerateSbom)) {
+    status.SetError("does not recognize sub-command SBOM");
+    return false;
+  }
+
+  Helper helper(status);
+  cmInstallCommandArguments ica(helper.DefaultComponentName, *helper.Makefile);
+
+  cmSbomArguments arguments;
+  ArgumentParser::NonEmpty<std::string> exportName;
+  ArgumentParser::NonEmpty<std::string> cxxModulesDirectory;
+
+  arguments.Bind(ica);
+  ica.Bind("EXPORT"_s, exportName);
+  // ica.Bind("CXX_MODULES_DIRECTORY"_s, cxxModulesDirectory); TODO?
+
+  std::vector<std::string> unknownArgs;
+  ica.Parse(args, &unknownArgs);
+
+  ArgumentParser::ParseResult result = ica.Parse(args, &unknownArgs);
+  if (!result.Check(args[0], &unknownArgs, status)) {
+    return false;
+  }
+
+  if (!ica.Finalize()) {
+    return false;
+  }
+
+  if (arguments.PackageName.empty()) {
+    // TODO: Fix our use of the parser to enforce this.
+    status.SetError(cmStrCat(args[0], " missing SBOM name."));
+    return false;
+  }
+
+  if (exportName.empty()) {
+    status.SetError(cmStrCat(args[0], " missing EXPORT."));
+    return false;
+  }
+
+  if (!arguments.Check(status) || !arguments.SetMetadataFromProject(status)) {
+    return false;
+  }
+
+  // Get or construct the destination path.
+  std::string dest = ica.GetDestination();
+  if (dest.empty()) {
+    if (helper.Makefile->GetSafeDefinition("CMAKE_SYSTEM_NAME") == "Windows") {
+      dest = std::string{ "/sbom/"_s };
+    } else {
+      dest = cmStrCat(helper.GetLibraryDestination(nullptr), "/sbom/",
+                      arguments.GetPackageDirName());
+    }
+  }
+
+  cmExportSet& exportSet =
+    helper.Makefile->GetGlobalGenerator()->GetExportSets()[exportName];
+
+  cmInstallGenerator::MessageLevel message =
+    cmInstallGenerator::SelectMessageLevel(helper.Makefile);
+
+  // Tell the global generator about any installation component names
+  // specified
+  helper.Makefile->GetGlobalGenerator()->AddInstallComponent(
+    ica.GetComponent());
+
+  // Create the export install generator.
+  helper.Makefile->AddInstallGenerator(
+    cm::make_unique<cmInstallSbomExportGenerator>(
+      &exportSet, dest, ica.GetPermissions(), ica.GetConfigurations(),
+      ica.GetComponent(), message, ica.GetExcludeFromAll(),
+      std::move(arguments), std::move(cxxModulesDirectory),
+      helper.Makefile->GetBacktrace()));
+
+  return true;
+#else
+  static_cast<void>(args);
+  status.SetError("SBOM not supported in bootstrap cmake");
+  return false;
+#endif
+}
+
 bool Helper::MakeFilesFullPath(char const* modeName,
                                std::vector<std::string> const& relFiles,
                                std::vector<std::string>& absFiles)
@@ -2750,6 +2838,7 @@ bool cmInstallCommand(std::vector<std::string> const& args,
     { "EXPORT_ANDROID_MK"_s, HandleExportAndroidMKMode },
     { "PACKAGE_INFO"_s, HandlePackageInfoMode },
     { "RUNTIME_DEPENDENCY_SET"_s, HandleRuntimeDependencySetMode },
+    { "SBOM"_s, HandleSbomMode }
   };
 
   return subcommand(args[0], args, status);

+ 30 - 0
Source/cmInstallSbomExportGenerator.cxx

@@ -0,0 +1,30 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+file LICENSE.rst or https://cmake.org/licensing for details.  */
+#include "cmInstallSbomExportGenerator.h"
+
+#include <utility>
+
+#include <cm/memory>
+
+#include "cmExportInstallFileGenerator.h"
+#include "cmExportInstallSbomGenerator.h"
+#include "cmListFileCache.h"
+#include "cmSbomArguments.h"
+
+class cmExportSet;
+
+cmInstallSbomExportGenerator::cmInstallSbomExportGenerator(
+  cmExportSet* exportSet, std::string destination, std::string filePermissions,
+  std::vector<std::string> const& configurations, std::string component,
+  MessageLevel message, bool excludeFromAll, cmSbomArguments args,
+  std::string cxxModulesDirectory, cmListFileBacktrace backtrace)
+  : cmInstallExportGenerator(
+      exportSet, std::move(destination), std::move(filePermissions),
+      configurations, std::move(component), message, excludeFromAll,
+      args.GetPackageFileName(), args.GetNamespace(),
+      std::move(cxxModulesDirectory), std::move(backtrace))
+{
+  this->EFGen = cm::make_unique<cmExportInstallSbomGenerator>(this, args);
+}
+
+cmInstallSbomExportGenerator::~cmInstallSbomExportGenerator() = default;

+ 31 - 0
Source/cmInstallSbomExportGenerator.h

@@ -0,0 +1,31 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file LICENSE.rst or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include "cmInstallExportGenerator.h"
+
+class cmExportSet;
+class cmListFileBacktrace;
+class cmSbomArguments;
+
+class cmInstallSbomExportGenerator final : public cmInstallExportGenerator
+{
+public:
+  cmInstallSbomExportGenerator(cmExportSet* exportSet, std::string destination,
+                               std::string filePermissions,
+                               std::vector<std::string> const& configurations,
+                               std::string component, MessageLevel message,
+                               bool excludeFromAll, cmSbomArguments arguments,
+                               std::string cxxModulesDirectory,
+                               cmListFileBacktrace backtrace);
+  cmInstallSbomExportGenerator(cmInstallSbomExportGenerator const&) = delete;
+  ~cmInstallSbomExportGenerator() override;
+
+  cmInstallSbomExportGenerator& operator=(
+    cmInstallSbomExportGenerator const&) = delete;
+
+  char const* InstallSubcommand() const override { return "SBOM"; }
+};

+ 4 - 98
Source/cmPackageInfoArguments.cxx

@@ -2,35 +2,20 @@
    file LICENSE.rst or https://cmake.org/licensing for details.  */
 #include "cmPackageInfoArguments.h"
 
-#include <utility>
-
 #include <cm/string_view>
 
 #include "cmExecutionStatus.h"
-#include "cmGeneratorExpression.h"
-#include "cmMakefile.h"
-#include "cmStateSnapshot.h"
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
-#include "cmValue.h"
 
 template void cmPackageInfoArguments::Bind<void>(cmArgumentParser<void>&,
                                                  cmPackageInfoArguments*);
 
-namespace {
-
-bool ArgWasSpecified(std::string const& value)
-{
-  return !value.empty();
-}
-
-bool ArgWasSpecified(std::vector<std::string> const& value)
+cm::string_view cmPackageInfoArguments::CommandName() const
 {
-  return !value.empty();
+  return "PACKAGE_INFO"_s;
 }
 
-} // anonymous namespace
-
 #define ENFORCE_REQUIRES(req, value, arg)                                     \
   do {                                                                        \
     if (ArgWasSpecified(value)) {                                             \
@@ -60,9 +45,6 @@ bool cmPackageInfoArguments::Check(cmExecutionStatus& status) const
                       "DEFAULT_CONFIGURATIONS");
     ENFORCE_EXCLUSIVE("APPENDIX", this->ProjectName, "PROJECT");
   }
-  if (this->NoProjectDefaults) {
-    ENFORCE_EXCLUSIVE("PROJECT", this->ProjectName, "NO_PROJECT_METADATA");
-  }
 
   // Check for options that require other options.
   if (this->Version.empty()) {
@@ -70,67 +52,12 @@ bool cmPackageInfoArguments::Check(cmExecutionStatus& status) const
     ENFORCE_REQUIRES("VERSION", this->VersionSchema, "VERSION_SCHEMA");
   }
 
-  // Validate the package name.
-  if (!this->PackageName.empty()) {
-    if (!cmGeneratorExpression::IsValidTargetName(this->PackageName) ||
-        this->PackageName.find(':') != std::string::npos) {
-      status.SetError(
-        cmStrCat(R"(PACKAGE_INFO given invalid package name ")"_s,
-                 this->PackageName, R"(".)"_s));
-      return false;
-    }
-  }
-
-  return true;
+  return cmProjectInfoArguments::Check(status);
 }
 
 #undef ENFORCE_REQUIRES
 #undef ENFORCE_EXCLUSIVE
 
-bool cmPackageInfoArguments::SetMetadataFromProject(cmExecutionStatus& status)
-{
-  // Determine what project to use for inherited metadata.
-  if (!this->SetEffectiveProject(status)) {
-    return false;
-  }
-
-  if (this->ProjectName.empty()) {
-    // We are not inheriting from a project.
-    return true;
-  }
-
-  cmMakefile& mf = status.GetMakefile();
-  auto mapProjectValue = [&](std::string& arg, cm::string_view suffix) {
-    cmValue const& projectValue =
-      mf.GetDefinition(cmStrCat(this->ProjectName, '_', suffix));
-    if (projectValue) {
-      arg = *projectValue;
-      return true;
-    }
-    return false;
-  };
-
-  if (this->Version.empty()) {
-    if (mapProjectValue(this->Version, "VERSION"_s)) {
-      mapProjectValue(this->VersionCompat, "COMPAT_VERSION"_s);
-    }
-  }
-
-  if (this->License.empty()) {
-    mapProjectValue(this->License, "SPDX_LICENSE"_s);
-  }
-
-  if (this->Description.empty()) {
-    mapProjectValue(this->Description, "DESCRIPTION"_s);
-  }
-
-  if (this->Website.empty()) {
-    mapProjectValue(this->Website, "HOMEPAGE_URL"_s);
-  }
-
-  return true;
-}
-
 bool cmPackageInfoArguments::SetEffectiveProject(cmExecutionStatus& status)
 {
   if (!this->Appendix.empty()) {
@@ -138,28 +65,7 @@ bool cmPackageInfoArguments::SetEffectiveProject(cmExecutionStatus& status)
     return true;
   }
 
-  if (this->NoProjectDefaults) {
-    // User requested that metadata not be inherited.
-    return true;
-  }
-
-  cmMakefile& mf = status.GetMakefile();
-  if (!this->ProjectName.empty()) {
-    // User specified a project; make sure it exists.
-    if (!mf.GetStateSnapshot().CheckProjectName(this->ProjectName)) {
-      status.SetError(cmStrCat(R"(PROJECT given unknown project name ")"_s,
-                               this->ProjectName, R"(".)"_s));
-      return false;
-    }
-  } else {
-    // No project was specified; check if the package name is also a project.
-    std::string project = mf.GetStateSnapshot().GetProjectName();
-    if (this->PackageName == project) {
-      this->ProjectName = std::move(project);
-    }
-  }
-
-  return true;
+  return cmProjectInfoArguments::SetEffectiveProject(status);
 }
 
 std::string cmPackageInfoArguments::GetNamespace() const

+ 22 - 47
Source/cmPackageInfoArguments.h

@@ -7,13 +7,13 @@
 #include <string>
 #include <vector>
 
+#include <cm/string_view>
 #include <cm/type_traits>
 #include <cmext/string_view>
 
 #include "cmArgumentParser.h" // IWYU pragma: keep
 #include "cmArgumentParserTypes.h"
-
-class cmExecutionStatus;
+#include "cmProjectInfoArguments.h"
 
 /** \class cmPackageInfoArguments
  * \brief Convey information about a package.
@@ -23,7 +23,7 @@ class cmExecutionStatus;
  * container, and also provides utilities to obtain this metadata from commands
  * which produce packages (i.e. export and install).
  */
-class cmPackageInfoArguments
+class cmPackageInfoArguments : public cmProjectInfoArguments
 {
 public:
   template <typename T,
@@ -31,7 +31,8 @@ public:
               std::is_base_of<cmPackageInfoArguments, T>::value>>
   static void Bind(cmArgumentParser<T>& parser)
   {
-    cmPackageInfoArguments::Bind(parser, nullptr);
+    cmPackageInfoArguments* const self = nullptr;
+    cmPackageInfoArguments::Bind(parser, self);
   }
 
   void Bind(cmArgumentParser<void>& parser)
@@ -43,76 +44,50 @@ public:
   std::string GetPackageDirName() const;
   std::string GetPackageFileName() const;
 
-  /// Ensure that no conflicting options were specified.
-  bool Check(cmExecutionStatus& status) const;
-
-  /// Set metadata (not already specified) from either the specified project,
-  /// or from the project which matches the package name.
-  bool SetMetadataFromProject(cmExecutionStatus& status);
+  bool Check(cmExecutionStatus& status) const override;
 
-  ArgumentParser::NonEmpty<std::string> PackageName;
   ArgumentParser::NonEmpty<std::string> Appendix;
-  ArgumentParser::NonEmpty<std::string> Version;
-  ArgumentParser::NonEmpty<std::string> VersionCompat;
-  ArgumentParser::NonEmpty<std::string> VersionSchema;
-  ArgumentParser::NonEmpty<std::string> License;
   ArgumentParser::NonEmpty<std::string> DefaultLicense;
-  ArgumentParser::NonEmpty<std::string> Description;
-  ArgumentParser::NonEmpty<std::string> Website;
   ArgumentParser::NonEmpty<std::vector<std::string>> DefaultTargets;
   ArgumentParser::NonEmpty<std::vector<std::string>> DefaultConfigs;
   bool LowerCase = false;
 
-  ArgumentParser::NonEmpty<std::string> ProjectName;
-  bool NoProjectDefaults = false;
+protected:
+  cm::string_view CommandName() const override;
 
-private:
-  bool SetEffectiveProject(cmExecutionStatus& status);
+  bool SetEffectiveProject(cmExecutionStatus& status) override;
 
   template <typename T>
   static void Bind(cmArgumentParser<T>& parser, cmPackageInfoArguments* self)
   {
-    Bind(self, parser, "PACKAGE_INFO"_s, &cmPackageInfoArguments::PackageName);
+    cmProjectInfoArguments* const base = self;
+
+    Bind(base, parser, "PACKAGE_INFO"_s, &cmProjectInfoArguments::PackageName);
     Bind(self, parser, "LOWER_CASE_FILE"_s,
          &cmPackageInfoArguments::LowerCase);
     Bind(self, parser, "APPENDIX"_s, &cmPackageInfoArguments::Appendix);
-    Bind(self, parser, "VERSION"_s, &cmPackageInfoArguments::Version);
-    Bind(self, parser, "COMPAT_VERSION"_s,
-         &cmPackageInfoArguments::VersionCompat);
-    Bind(self, parser, "VERSION_SCHEMA"_s,
-         &cmPackageInfoArguments::VersionSchema);
+
+    Bind(base, parser, "COMPAT_VERSION"_s,
+         &cmProjectInfoArguments::VersionCompat);
+    Bind(base, parser, "VERSION_SCHEMA"_s,
+         &cmProjectInfoArguments::VersionSchema);
     Bind(self, parser, "DEFAULT_TARGETS"_s,
          &cmPackageInfoArguments::DefaultTargets);
     Bind(self, parser, "DEFAULT_CONFIGURATIONS"_s,
          &cmPackageInfoArguments::DefaultConfigs);
-    Bind(self, parser, "LICENSE"_s, &cmPackageInfoArguments::License);
     Bind(self, parser, "DEFAULT_LICENSE"_s,
          &cmPackageInfoArguments::DefaultLicense);
-    Bind(self, parser, "DESCRIPTION"_s, &cmPackageInfoArguments::Description);
-    Bind(self, parser, "HOMEPAGE_URL"_s, &cmPackageInfoArguments::Website);
 
-    Bind(self, parser, "PROJECT"_s, &cmPackageInfoArguments::ProjectName);
-    Bind(self, parser, "NO_PROJECT_METADATA"_s,
-         &cmPackageInfoArguments::NoProjectDefaults);
+    cmProjectInfoArguments::Bind(parser, self);
   }
 
-  template <typename T, typename U,
-            typename = cm::enable_if_t<
-              std::is_base_of<cmPackageInfoArguments, T>::value>>
-  static void Bind(cmPackageInfoArguments*, cmArgumentParser<T>& parser,
-                   cm::static_string_view name,
-                   U cmPackageInfoArguments::*member)
-  {
-    parser.Bind(name, member);
-  }
+  using cmProjectInfoArguments::Bind;
 
-  template <typename U>
-  static void Bind(cmPackageInfoArguments* self,
-                   cmArgumentParser<void>& parser, cm::static_string_view name,
-                   U cmPackageInfoArguments::*member)
+  static bool ArgWasSpecified(std::vector<std::string> const& value)
   {
-    parser.Bind(name, (self)->*member);
+    return !value.empty();
   }
+  using cmProjectInfoArguments::ArgWasSpecified;
 };
 
 extern template void cmPackageInfoArguments::Bind<void>(

+ 129 - 0
Source/cmProjectInfoArguments.cxx

@@ -0,0 +1,129 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file LICENSE.rst or https://cmake.org/licensing for details.  */
+#include "cmProjectInfoArguments.h"
+
+#include <utility>
+
+#include <cm/string_view>
+
+#include "cmExecutionStatus.h"
+#include "cmGeneratorExpression.h"
+#include "cmMakefile.h"
+#include "cmStateSnapshot.h"
+#include "cmStringAlgorithms.h"
+#include "cmValue.h"
+
+template void cmProjectInfoArguments::Bind<void>(cmArgumentParser<void>&,
+                                                 cmProjectInfoArguments*);
+
+#define ENFORCE_REQUIRES(req, value, arg)                                     \
+  do {                                                                        \
+    if (ArgWasSpecified(value)) {                                             \
+      status.SetError(arg " requires " req ".");                              \
+      return false;                                                           \
+    }                                                                         \
+  } while (false)
+
+#define ENFORCE_EXCLUSIVE(arg1, value, arg2)                                  \
+  do {                                                                        \
+    if (ArgWasSpecified(value)) {                                             \
+      status.SetError(arg1 " and " arg2 " are mutually exclusive.");          \
+      return false;                                                           \
+    }                                                                         \
+  } while (false)
+
+cmProjectInfoArguments::cmProjectInfoArguments() = default;
+
+bool cmProjectInfoArguments::Check(cmExecutionStatus& status) const
+{
+  // Check for incompatible options.
+  if (this->NoProjectDefaults) {
+    ENFORCE_EXCLUSIVE("PROJECT", this->ProjectName, "NO_PROJECT_METADATA");
+  }
+
+  // Validate the package name.
+  if (!this->PackageName.empty()) {
+    if (!cmGeneratorExpression::IsValidTargetName(this->PackageName) ||
+        this->PackageName.find(':') != std::string::npos) {
+      status.SetError(cmStrCat(this->CommandName(),
+                               " given invalid package name \""_s,
+                               this->PackageName, "\"."_s));
+      return false;
+    }
+  }
+
+  return true;
+}
+
+#undef ENFORCE_REQUIRES
+#undef ENFORCE_EXCLUSIVE
+
+bool cmProjectInfoArguments::SetMetadataFromProject(cmExecutionStatus& status)
+{
+  // Determine what project to use for inherited metadata.
+  if (!this->SetEffectiveProject(status)) {
+    return false;
+  }
+
+  if (this->ProjectName.empty()) {
+    // We are not inheriting from a project.
+    return true;
+  }
+
+  cmMakefile& mf = status.GetMakefile();
+  auto mapProjectValue = [&](std::string& arg, cm::string_view suffix) {
+    cmValue const& projectValue =
+      mf.GetDefinition(cmStrCat(this->ProjectName, '_', suffix));
+    if (projectValue) {
+      arg = *projectValue;
+      return true;
+    }
+    return false;
+  };
+
+  if (this->Version.empty()) {
+    if (mapProjectValue(this->Version, "VERSION"_s)) {
+      mapProjectValue(this->VersionCompat, "COMPAT_VERSION"_s);
+    }
+  }
+
+  if (this->License.empty()) {
+    mapProjectValue(this->License, "SPDX_LICENSE"_s);
+  }
+
+  if (this->Description.empty()) {
+    mapProjectValue(this->Description, "DESCRIPTION"_s);
+  }
+
+  if (this->Website.empty()) {
+    mapProjectValue(this->Website, "HOMEPAGE_URL"_s);
+  }
+
+  return true;
+}
+
+bool cmProjectInfoArguments::SetEffectiveProject(cmExecutionStatus& status)
+{
+  if (this->NoProjectDefaults) {
+    // User requested that metadata not be inherited.
+    return true;
+  }
+
+  cmMakefile& mf = status.GetMakefile();
+  if (!this->ProjectName.empty()) {
+    // User specified a project; make sure it exists.
+    if (!mf.GetStateSnapshot().CheckProjectName(this->ProjectName)) {
+      status.SetError(cmStrCat(R"(PROJECT given unknown project name ")"_s,
+                               this->ProjectName, R"(".)"_s));
+      return false;
+    }
+  } else {
+    // No project was specified; check if the package name is also a project.
+    std::string project = mf.GetStateSnapshot().GetProjectName();
+    if (this->PackageName == project) {
+      this->ProjectName = std::move(project);
+    }
+  }
+
+  return true;
+}

+ 103 - 0
Source/cmProjectInfoArguments.h

@@ -0,0 +1,103 @@
+/* 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 <string>
+
+#include <cm/string_view>
+#include <cm/type_traits>
+#include <cmext/string_view>
+
+#include "cmArgumentParser.h" // IWYU pragma: keep
+#include "cmArgumentParserTypes.h"
+
+class cmExecutionStatus;
+
+/** \class cmProjectInfoArguments
+ * \brief Convey information about a project.
+ *
+ * This class encapsulates several attributes of project metadata;
+ * specifically, those which can be inherited from the project. It is used as
+ * the base class for classes specific to SBOM and PACKAGE_INFO exports.
+ */
+class cmProjectInfoArguments
+{
+public:
+  cmProjectInfoArguments();
+  cmProjectInfoArguments(cmProjectInfoArguments const&) = default;
+  cmProjectInfoArguments& operator=(cmProjectInfoArguments const&) = default;
+  cmProjectInfoArguments(cmProjectInfoArguments&&) = default;
+  cmProjectInfoArguments& operator=(cmProjectInfoArguments&&) = default;
+
+  virtual ~cmProjectInfoArguments() = default;
+
+  /// Ensure that no conflicting options were specified.
+  virtual bool Check(cmExecutionStatus& status) const;
+
+  /// Set metadata (not already specified) from either the specified project,
+  /// or from the project which matches the package name.
+  bool SetMetadataFromProject(cmExecutionStatus& status);
+
+  ArgumentParser::NonEmpty<std::string> PackageName;
+  ArgumentParser::NonEmpty<std::string> Version;
+  ArgumentParser::NonEmpty<std::string> VersionCompat;
+  ArgumentParser::NonEmpty<std::string> VersionSchema;
+  ArgumentParser::NonEmpty<std::string> License;
+  ArgumentParser::NonEmpty<std::string> Description;
+  ArgumentParser::NonEmpty<std::string> Website;
+
+  ArgumentParser::NonEmpty<std::string> ProjectName;
+  bool NoProjectDefaults = false;
+
+protected:
+  virtual cm::string_view CommandName() const = 0;
+
+  virtual bool SetEffectiveProject(cmExecutionStatus& status);
+
+  template <typename T>
+  static void Bind(cmArgumentParser<T>& parser, cmProjectInfoArguments* self)
+  {
+    Bind(self, parser, "VERSION"_s, &cmProjectInfoArguments::Version);
+    Bind(self, parser, "LICENSE"_s, &cmProjectInfoArguments::License);
+    Bind(self, parser, "DESCRIPTION"_s, &cmProjectInfoArguments::Description);
+    Bind(self, parser, "HOMEPAGE_URL"_s, &cmProjectInfoArguments::Website);
+
+    Bind(self, parser, "PROJECT"_s, &cmProjectInfoArguments::ProjectName);
+    Bind(self, parser, "NO_PROJECT_METADATA"_s,
+         &cmProjectInfoArguments::NoProjectDefaults);
+  }
+
+  template <
+    typename Self, typename T, typename U,
+    typename =
+      cm::enable_if_t<std::is_base_of<cmProjectInfoArguments, Self>::value>,
+    typename =
+      cm::enable_if_t<std::is_base_of<cmProjectInfoArguments, T>::value>>
+  static void Bind(Self*, cmArgumentParser<T>& parser,
+                   cm::static_string_view name, U Self::*member)
+  {
+    parser.Bind(name, member);
+  }
+
+  template <
+    typename Self, typename T, typename U,
+    typename =
+      cm::enable_if_t<std::is_base_of<cmProjectInfoArguments, Self>::value>,
+    typename =
+      cm::enable_if_t<std::is_base_of<cmProjectInfoArguments, T>::value>>
+  static void Bind(Self* self, cmArgumentParser<void>& parser,
+                   cm::static_string_view name, U T::*member)
+  {
+    parser.Bind(name, (self)->*member);
+  }
+
+  static bool ArgWasSpecified(std::string const& value)
+  {
+    return !value.empty();
+  }
+};
+
+extern template void cmProjectInfoArguments::Bind<void>(
+  cmArgumentParser<void>&, cmProjectInfoArguments*);

+ 104 - 0
Source/cmSbomArguments.cxx

@@ -0,0 +1,104 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file LICENSE.rst or https://cmake.org/licensing for details.  */
+#include "cmSbomArguments.h"
+
+#include <algorithm>
+#include <cctype>
+
+#include <cm/string_view>
+
+#include "cmExecutionStatus.h"
+#include "cmGeneratorExpression.h"
+#include "cmStringAlgorithms.h"
+
+struct SbomFormatPattern
+{
+  cm::string_view Name;
+  cm::string_view Alias;
+  cmSbomArguments::SbomFormat Id;
+};
+
+static SbomFormatPattern SbomPatterns[] = {
+  { "spdx-3.0+json", "spdx", cmSbomArguments::SbomFormat::SPDX_3_0_JSON },
+};
+
+cmSbomArguments::SbomFormat ParseSbomFormat(std::string const& input)
+{
+  if (input.empty()) {
+    return cmSbomArguments::SbomFormat::SPDX_3_0_JSON;
+  }
+
+  cm::string_view s(input);
+  for (auto const& p : SbomPatterns) {
+    if (s == p.Name || (!p.Alias.empty() && s == p.Alias)) {
+      return p.Id;
+    }
+  }
+  return cmSbomArguments::SbomFormat::NONE;
+}
+
+std::string GetSbomFileExtension(cmSbomArguments::SbomFormat id)
+{
+  switch (id) {
+    case cmSbomArguments::SbomFormat::SPDX_3_0_JSON:
+      return ".spdx.json";
+    default:
+      return "";
+  }
+}
+
+template void cmSbomArguments::Bind<void>(cmArgumentParser<void>&,
+                                          cmSbomArguments*);
+
+bool cmSbomArguments::Check(cmExecutionStatus& status) const
+{
+  if (!this->PackageName.empty()) {
+    if (!cmGeneratorExpression::IsValidTargetName(this->PackageName) ||
+        this->PackageName.find(':') != std::string::npos) {
+      status.SetError(cmStrCat(R"(SBOM given invalid package name ")"_s,
+                               this->PackageName, R"(".)"_s));
+      return false;
+    }
+  }
+
+  if (!this->Format.empty()) {
+    if (this->GetFormat() == SbomFormat::NONE) {
+      status.SetError(
+        cmStrCat(R"(SBOM given invalid format ")"_s, this->Format, R"(".)"_s));
+      return false;
+    }
+  }
+  return true;
+}
+
+cm::string_view cmSbomArguments::CommandName() const
+{
+  return "SBOM"_s;
+}
+
+std::string cmSbomArguments::GetNamespace() const
+{
+  return cmStrCat(this->PackageName, "::"_s);
+}
+
+std::string cmSbomArguments::GetPackageDirName() const
+{
+  return this->PackageName;
+}
+
+cmSbomArguments::SbomFormat cmSbomArguments::GetFormat() const
+{
+  if (this->Format.empty()) {
+    return SbomFormat::SPDX_3_0_JSON;
+  }
+  return ParseSbomFormat(this->Format);
+}
+
+std::string cmSbomArguments::GetPackageFileName() const
+{
+  std::string const pkgNameOnDisk = this->GetPackageDirName();
+  std::string format = GetSbomFileExtension(this->GetFormat());
+  std::transform(format.begin(), format.end(), format.begin(),
+                 [](char const c) { return std::tolower(c); });
+  return cmStrCat(pkgNameOnDisk, format);
+}

+ 61 - 0
Source/cmSbomArguments.h

@@ -0,0 +1,61 @@
+/* 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 <string>
+
+#include <cm/string_view>
+#include <cm/type_traits>
+#include <cmext/string_view>
+
+#include "cmArgumentParser.h" // IWYU pragma: keep
+#include "cmArgumentParserTypes.h"
+#include "cmProjectInfoArguments.h"
+
+class cmSbomArguments : public cmProjectInfoArguments
+{
+public:
+  enum class SbomFormat
+  {
+    SPDX_3_0_JSON,
+    NONE,
+  };
+
+  template <
+    typename T,
+    typename = cm::enable_if_t<std::is_base_of<cmSbomArguments, T>::value>>
+  static void Bind(cmArgumentParser<T>& parser)
+  {
+    cmSbomArguments* const self = nullptr;
+    cmSbomArguments::Bind(parser, self);
+  }
+  void Bind(cmArgumentParser<void>& parser) { Bind(parser, this); }
+
+  bool Check(cmExecutionStatus& status) const override;
+  std::string GetNamespace() const;
+  std::string GetPackageDirName() const;
+  std::string GetPackageFileName() const;
+  SbomFormat GetFormat() const;
+
+  ArgumentParser::NonEmpty<std::string> Format;
+
+protected:
+  cm::string_view CommandName() const override;
+
+private:
+  using cmProjectInfoArguments::Bind;
+
+  template <typename T>
+  static void Bind(cmArgumentParser<T>& parser, cmSbomArguments* self)
+  {
+    cmProjectInfoArguments* const base = self;
+    Bind(base, parser, "SBOM"_s, &cmProjectInfoArguments::PackageName);
+    Bind(self, parser, "FORMAT"_s, &cmSbomArguments::Format);
+    cmProjectInfoArguments::Bind(parser, self);
+  }
+};
+
+extern template void cmSbomArguments::Bind<void>(cmArgumentParser<void>&,
+                                                 cmSbomArguments*);

+ 2 - 0
Tests/RunCMake/CMakeLists.txt

@@ -1389,8 +1389,10 @@ 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)
 
 if(CMake_TEST_ANDROID_NDK OR CMake_TEST_ANDROID_STANDALONE_TOOLCHAIN)
   if(NOT "${CMAKE_GENERATOR}" MATCHES "Make|Ninja|Visual Studio 1[456]")

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

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

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

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

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

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

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

+ 19 - 0
Tests/RunCMake/InstallSbom/IgnoresInterfaceDirs.cmake

@@ -0,0 +1,19 @@
+include(${CMAKE_CURRENT_LIST_DIR}/../Sbom/Setup.cmake)
+
+
+include(CMakePackageConfigHelpers)
+include(GNUInstallDirs)
+
+add_library(interface INTERFACE)
+
+target_include_directories(interface INTERFACE ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
+
+install(
+  TARGETS interface
+  EXPORT interface_targets
+)
+
+install(SBOM interface_targets
+  EXPORT interface_targets
+  DESTINATION .
+)

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

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

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

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

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

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

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

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

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

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

+ 11 - 0
Tests/RunCMake/InstallSbom/ProjectMetadata.cmake

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

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

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

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

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

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

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

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

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

+ 33 - 0
Tests/RunCMake/InstallSbom/RunCMakeTest.cmake

@@ -0,0 +1,33 @@
+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(IgnoresInterfaceDirs)
+run_cmake_install(MissingPackageNamespace)
+run_cmake_install(ReferencesNonExportedTarget)

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

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

+ 7 - 0
Tests/RunCMake/InstallSbom/SharedTarget.cmake

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

+ 93 - 0
Tests/RunCMake/Sbom/ApplicationTarget-install-check.cmake

@@ -0,0 +1,93 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(CREATION_INFO [=[
+{
+  "comment" : "This SBOM was generated from the CMakeLists.txt File",
+  "createdUsing" :
+  [
+    {
+      "@id" : "CMake#Agent",
+      "name" : "CMake",
+      "type" : "Tool"
+    }
+  ],
+  "type" : "CreationInfo"
+}
+]=])
+
+set(ELEMENTS [=[
+[
+  {
+    "@id" : "bar:bar#Package",
+    "name" : "bar:bar",
+    "type" : "software_Package"
+  }
+]
+]=])
+
+set(SPDX_DOCUMENT [=[
+{
+  "@id" : "application_targets#SPDXDocument",
+  "name": "application_targets",
+  "profileConformance": ["core", "software"],
+  "type": "SpdxDocument"
+}
+]=])
+
+set(APPLICATION [=[
+{
+  "@id" : "application#Package",
+  "externalRef" :
+  [
+    {
+      "comment" : "Build System used for this target",
+      "externalRefType" : "buildSystem",
+      "locator" : "CMake#Agent",
+      "type" : "ExternalRef"
+    }
+  ],
+  "name" : "application",
+  "primaryPurpose" : "APPLICATION",
+  "type" : "software_Package"
+}
+]=])
+
+set(DEPENDENCY [=[
+{
+  "@id" : "bar:bar#Package",
+  "name" : "bar:bar",
+  "originatedBy" :
+  [
+    {
+      "name" : "bar",
+      "type" : "Organization"
+    }
+  ],
+  "packageVersion" : "1.3.5",
+  "type" : "software_Package"
+}
+]=])
+
+set(BUILD_LINKED_LIBRARIES [=[
+{
+  "description" : "Linked Libraries",
+  "from" : "application#Package",
+  "relationshipType" : "DEPENDS_ON",
+  "to" :
+  [
+    "bar:bar#Package"
+  ],
+  "type" : "Relationship"
+}
+]=])
+
+
+expect_value("${content}" "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" "@context")
+string(JSON SPDX_DOCUMENT GET "${content}" "@graph" "0")
+expect_object("${SPDX_DOCUMENT}" SPDX_DOCUMENT)
+expect_object("${SPDX_DOCUMENT}" CREATION_INFO "creationInfo")
+expect_object("${SPDX_DOCUMENT}" APPLICATION "rootElement" "0")
+expect_object("${SPDX_DOCUMENT}" DEPENDENCY "element" "0")
+
+string(JSON LINKED_LIBRARIES GET "${content}" "@graph" "1")
+expect_object("${LINKED_LIBRARIES}" BUILD_LINKED_LIBRARIES)

+ 19 - 0
Tests/RunCMake/Sbom/ApplicationTarget.cmake

@@ -0,0 +1,19 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Setup.cmake)
+
+include(CMakePackageConfigHelpers)
+include(GNUInstallDirs)
+
+add_executable(application ${CMAKE_CURRENT_LIST_DIR}/main.c)
+
+find_package(
+  bar 1.3.4 REQUIRED
+  NO_DEFAULT_PATH
+  PATHS ${CMAKE_CURRENT_LIST_DIR}
+)
+
+target_link_libraries(application PUBLIC bar::bar)
+
+install(
+  TARGETS application
+  EXPORT application_targets
+)

+ 223 - 0
Tests/RunCMake/Sbom/Assertions.cmake

@@ -0,0 +1,223 @@
+function(format_path out)
+  set(path_segments ${ARGN})
+  set(path_string "")
+  foreach(segment IN LISTS path_segments)
+    if(segment MATCHES "^[0-9]+$")
+      string(APPEND path_string "[${segment}]")
+    else()
+      if(path_string STREQUAL "")
+        string(APPEND path_string "${segment}")
+      else()
+        string(APPEND path_string ".${segment}")
+      endif()
+    endif()
+  endforeach()
+  if(path_string STREQUAL "")
+    set(path_string "<root>")
+  endif()
+  set(${out} "${path_string}" PARENT_SCOPE)
+endfunction()
+
+macro(fail_at entity actual expected)
+  format_path(formatted_path ${ARGN})
+  set(RunCMake_TEST_FAILED "Attribute '${formatted_path}' ${entity} '${actual}' does not match expected ${entity} '${expected}'")
+endmacro()
+
+macro(fail_array_subset expected_index)
+  format_path(formatted_path ${ARGN})
+  set(RunCMake_TEST_FAILED "Attribute '${formatted_path}' array element (subset match) had no match for expected index ${expected_index}")
+endmacro()
+
+macro(bubble_error)
+  set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE)
+endmacro()
+
+function(json_matches out actual_node expected_node display_path)
+  set(current_path ${display_path})
+
+  string(JSON actual_type TYPE "${actual_node}")
+  string(JSON expected_type TYPE "${expected_node}")
+  if (NOT actual_type STREQUAL expected_type)
+    fail_at("type" "${actual_type}" "${expected_type}" ${current_path})
+    bubble_error()
+    set(${out} FALSE PARENT_SCOPE)
+    return()
+  endif()
+
+  if (expected_type STREQUAL "OBJECT")
+    string(JSON expected_length LENGTH "${expected_node}")
+    math(EXPR expected_last_index "${expected_length}-1")
+    foreach(key_index RANGE ${expected_last_index})
+      string(JSON key MEMBER "${expected_node}" "${key_index}")
+
+      string(JSON probe ERROR_VARIABLE error GET "${actual_node}" "${key}")
+      if (error STREQUAL "NOT_FOUND")
+        fail_at("object member presence" "<missing>" "${key}" ${current_path} "${key}")
+        bubble_error()
+        set(${out} FALSE PARENT_SCOPE)
+        return()
+      endif()
+
+      string(JSON actual_child_type TYPE "${actual_node}" "${key}")
+      string(JSON expected_child_type TYPE "${expected_node}" "${key}")
+      if (NOT actual_child_type STREQUAL expected_child_type)
+        fail_at("type" "${actual_child_type}" "${expected_child_type}" ${current_path} "${key}")
+        bubble_error()
+        set(${out} FALSE PARENT_SCOPE)
+        return()
+      endif()
+
+      if (expected_child_type STREQUAL "OBJECT" OR expected_child_type STREQUAL "ARRAY")
+        string(JSON actual_child GET "${actual_node}" "${key}")
+        string(JSON expected_child GET "${expected_node}" "${key}")
+        set(next_path ${current_path})
+        list(APPEND next_path "${key}")
+        json_matches(is_ok "${actual_child}" "${expected_child}" "${next_path}")
+        if (NOT is_ok)
+          bubble_error()
+          set(${out} FALSE PARENT_SCOPE)
+          return()
+        endif()
+      else()
+        string(JSON actual_value GET "${actual_node}" "${key}")
+        string(JSON expected_value GET "${expected_node}" "${key}")
+        if (NOT "${actual_value}" STREQUAL "${expected_value}")
+          fail_at("value" "${actual_value}" "${expected_value}" ${current_path} "${key}")
+          bubble_error()
+          set(${out} FALSE PARENT_SCOPE)
+          return()
+        endif()
+      endif()
+    endforeach()
+
+    set(${out} TRUE PARENT_SCOPE)
+    return()
+
+  elseif (expected_type STREQUAL "ARRAY")
+    string(JSON actual_length LENGTH "${actual_node}")
+    string(JSON expected_length LENGTH "${expected_node}")
+    if (actual_length LESS expected_length)
+      fail_at("array length (subset requirement)" "${actual_length}" ">= ${expected_length}" ${current_path})
+      bubble_error()
+      set(${out} FALSE PARENT_SCOPE)
+      return()
+    endif()
+
+    math(EXPR expected_last_index "${expected_length}-1")
+    math(EXPR actual_last_index "${actual_length}-1")
+    foreach(expected_index RANGE ${expected_last_index})
+      set(is_matched FALSE)
+      set(best_error "")
+      set(best_error_set FALSE)
+
+      string(JSON expected_element_type TYPE "${expected_node}" "${expected_index}")
+
+      if (expected_element_type STREQUAL "OBJECT" OR expected_element_type STREQUAL "ARRAY")
+        string(JSON expected_element GET "${expected_node}" "${expected_index}")
+        foreach(actual_index RANGE ${actual_last_index})
+          string(JSON actual_element_type TYPE "${actual_node}" "${actual_index}")
+          if (NOT actual_element_type STREQUAL expected_element_type)
+            continue()
+          endif()
+
+          string(JSON actual_element GET "${actual_node}" "${actual_index}")
+          set(next_path ${current_path})
+          list(APPEND next_path "${actual_index}")
+
+          json_matches(one_ok "${actual_element}" "${expected_element}" "${next_path}")
+          if (one_ok)
+            set(is_matched TRUE)
+            break()
+          else()
+            if (NOT best_error_set)
+              set(best_error "${RunCMake_TEST_FAILED}")
+              set(best_error_set TRUE)
+            endif()
+            set(RunCMake_TEST_FAILED "")
+          endif()
+        endforeach()
+      else()
+        string(JSON expected_value GET "${expected_node}" "${expected_index}")
+        foreach(actual_index RANGE ${actual_last_index})
+          string(JSON actual_element_type TYPE "${actual_node}" "${actual_index}")
+          if (NOT actual_element_type STREQUAL expected_element_type)
+            continue()
+          endif()
+          string(JSON actual_value GET "${actual_node}" "${actual_index}")
+          if ("${actual_value}" STREQUAL "${expected_value}")
+            set(is_matched TRUE)
+            break()
+          endif()
+        endforeach()
+        if (NOT is_matched AND NOT best_error_set)
+          set(best_error "")
+          set(best_error_set TRUE)
+        endif()
+      endif()
+
+      if (NOT is_matched)
+        if (best_error_set AND NOT "${best_error}" STREQUAL "")
+          set(RunCMake_TEST_FAILED "${best_error}")
+        else()
+          fail_array_subset("${expected_index}" ${current_path})
+        endif()
+        bubble_error()
+        set(${out} FALSE PARENT_SCOPE)
+        return()
+      endif()
+    endforeach()
+
+    set(${out} TRUE PARENT_SCOPE)
+    return()
+  endif()
+endfunction()
+
+function(expect_value content expected)
+  string(JSON actual ERROR_VARIABLE error GET "${content}" ${ARGN})
+  if (error STREQUAL "NOT_FOUND")
+    list(JOIN ARGN "." path_name)
+    set(RunCMake_TEST_FAILED "Path '${path_name}' not found in JSON input" PARENT_SCOPE)
+    return()
+  endif()
+  if (NOT "${actual}" STREQUAL "${expected}")
+    fail_at("value" "${actual}" "${expected}" ${ARGN})
+    bubble_error()
+  endif()
+endfunction()
+
+function(expect_array content expected_length)
+  string(JSON value_type ERROR_VARIABLE error TYPE "${content}" ${ARGN})
+  if (error STREQUAL "NOT_FOUND")
+    list(JOIN ARGN "." path_name)
+    set(RunCMake_TEST_FAILED "Path '${path_name}' not found in JSON input" PARENT_SCOPE)
+    return()
+  endif()
+  if (NOT value_type STREQUAL "ARRAY")
+    fail_at("type" "${value_type}" "ARRAY" ${ARGN})
+    bubble_error()
+    return()
+  endif()
+  string(JSON actual_length LENGTH "${content}" ${ARGN})
+  if (NOT actual_length EQUAL "${expected_length}")
+    fail_at("length" "${actual_length}" "${expected_length}" ${ARGN})
+    bubble_error()
+  endif()
+endfunction()
+
+function(expect_object content expected_var)
+  string(JSON actual_node ERROR_VARIABLE error GET "${content}" ${ARGN})
+  if (error STREQUAL "NOT_FOUND")
+    list(JOIN ARGN "." path_name)
+    set(RunCMake_TEST_FAILED "Path '${path_name}' not found in JSON input" PARENT_SCOPE)
+    return()
+  endif()
+
+  set(expected_text "${${expected_var}}")
+  set(display_path ${ARGN})
+
+  json_matches(is_ok "${actual_node}" "${expected_text}" "${display_path}")
+  if (NOT is_ok)
+    bubble_error()
+    return()
+  endif()
+endfunction()

+ 93 - 0
Tests/RunCMake/Sbom/InterfaceTarget-install-check.cmake

@@ -0,0 +1,93 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(CREATION_INFO [=[
+{
+  "comment" : "This SBOM was generated from the CMakeLists.txt File",
+  "createdUsing" :
+  [
+    {
+      "@id" : "CMake#Agent",
+      "name" : "CMake",
+      "type" : "Tool"
+    }
+  ],
+  "type" : "CreationInfo"
+}
+]=])
+
+set(ELEMENTS [=[
+[
+  {
+    "@id" : "bar:bar#Package",
+    "name" : "bar:bar",
+    "type" : "software_Package"
+  }
+]
+]=])
+
+set(SPDX_DOCUMENT [=[
+{
+  "@id" : "interface_targets#SPDXDocument",
+  "name": "interface_targets",
+  "profileConformance": ["core", "software"],
+  "type": "SpdxDocument"
+}
+]=])
+
+set(INTERFACE [=[
+{
+  "@id" : "interface#Package",
+  "externalRef" :
+  [
+    {
+      "comment" : "Build System used for this target",
+      "externalRefType" : "buildSystem",
+      "locator" : "CMake#Agent",
+      "type" : "ExternalRef"
+    }
+  ],
+  "name" : "interface",
+  "primaryPurpose" : "LIBRARY",
+  "type" : "software_Package"
+}
+]=])
+
+set(DEPENDENCY [=[
+{
+  "@id" : "bar:bar#Package",
+  "name" : "bar:bar",
+  "originatedBy" :
+  [
+    {
+      "name" : "bar",
+      "type" : "Organization"
+    }
+  ],
+  "packageVersion" : "1.3.5",
+  "type" : "software_Package"
+}
+]=])
+
+set(BUILD_LINKED_LIBRARIES [=[
+{
+  "description" : "Linked Libraries",
+  "from" : "interface#Package",
+  "relationshipType" : "DEPENDS_ON",
+  "to" :
+  [
+    "bar:bar#Package"
+  ],
+  "type" : "Relationship"
+}
+]=])
+
+
+expect_value("${content}" "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" "@context")
+string(JSON SPDX_DOCUMENT GET "${content}" "@graph" "0")
+expect_object("${SPDX_DOCUMENT}" SPDX_DOCUMENT)
+expect_object("${SPDX_DOCUMENT}" CREATION_INFO "creationInfo")
+expect_object("${SPDX_DOCUMENT}" INTERFACE "rootElement" "0")
+expect_object("${SPDX_DOCUMENT}" DEPENDENCY "element" "0")
+
+string(JSON LINKED_LIBRARIES GET "${content}" "@graph" "1")
+expect_object("${LINKED_LIBRARIES}" BUILD_LINKED_LIBRARIES)

+ 19 - 0
Tests/RunCMake/Sbom/InterfaceTarget.cmake

@@ -0,0 +1,19 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Setup.cmake)
+
+include(CMakePackageConfigHelpers)
+include(GNUInstallDirs)
+
+add_library(interface INTERFACE)
+
+find_package(
+  bar 1.3.4 REQUIRED
+  NO_DEFAULT_PATH
+  PATHS ${CMAKE_CURRENT_LIST_DIR}
+)
+
+target_link_libraries(interface INTERFACE bar::bar)
+
+install(
+  TARGETS interface
+  EXPORT interface_targets
+)

+ 2 - 0
Tests/RunCMake/Sbom/LICENSE.txt

@@ -0,0 +1,2 @@
+This is a sample file to verify the SBOM install files command.
+It's not an actual license.

+ 93 - 0
Tests/RunCMake/Sbom/MissingPackageNamespace-install-check.cmake

@@ -0,0 +1,93 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(CREATION_INFO [=[
+{
+  "comment" : "This SBOM was generated from the CMakeLists.txt File",
+  "createdUsing" :
+  [
+    {
+      "@id" : "CMake#Agent",
+      "name" : "CMake",
+      "type" : "Tool"
+    }
+  ],
+  "type" : "CreationInfo"
+}
+]=])
+
+set(ELEMENTS [=[
+[
+  {
+    "@id" : "bar:bar#Package",
+    "name" : "bar:bar",
+    "type" : "software_Package"
+  }
+]
+]=])
+
+set(SPDX_DOCUMENT [=[
+{
+  "@id" : "test_targets#SPDXDocument",
+  "name": "test_targets",
+  "profileConformance": ["core", "software"],
+  "type": "SpdxDocument"
+}
+]=])
+
+set(TEST [=[
+{
+  "@id" : "test#Package",
+  "externalRef" :
+  [
+    {
+      "comment" : "Build System used for this target",
+      "externalRefType" : "buildSystem",
+      "locator" : "CMake#Agent",
+      "type" : "ExternalRef"
+    }
+  ],
+  "name" : "test",
+  "primaryPurpose" : "APPLICATION",
+  "type" : "software_Package"
+}
+]=])
+
+set(DEPENDENCY [=[
+{
+  "@id" : "baz:baz#Package",
+  "name" : "baz:baz",
+  "originatedBy" :
+  [
+    {
+      "name" : "baz",
+      "type" : "Organization"
+    }
+  ],
+  "packageVersion" : "1.8.5",
+  "type" : "software_Package"
+}
+]=])
+
+set(BUILD_LINKED_LIBRARIES [=[
+{
+  "description" : "Linked Libraries",
+  "from" : "test#Package",
+  "relationshipType" : "DEPENDS_ON",
+  "to" :
+  [
+    "baz:baz#Package"
+  ],
+  "type" : "Relationship"
+}
+]=])
+
+
+expect_value("${content}" "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" "@context")
+string(JSON SPDX_DOCUMENT GET "${content}" "@graph" "0")
+expect_object("${SPDX_DOCUMENT}" SPDX_DOCUMENT)
+expect_object("${SPDX_DOCUMENT}" CREATION_INFO "creationInfo")
+expect_object("${SPDX_DOCUMENT}" TEST "rootElement" "0")
+expect_object("${SPDX_DOCUMENT}" DEPENDENCY "element" "0")
+
+string(JSON LINKED_LIBRARIES GET "${content}" "@graph" "1")
+expect_object("${LINKED_LIBRARIES}" BUILD_LINKED_LIBRARIES)

+ 21 - 0
Tests/RunCMake/Sbom/MissingPackageNamespace.cmake

@@ -0,0 +1,21 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Setup.cmake)
+
+include(CMakePackageConfigHelpers)
+include(GNUInstallDirs)
+
+add_executable(test
+  ${CMAKE_CURRENT_LIST_DIR}/main.c
+)
+
+find_package(
+  baz REQUIRED
+  NO_DEFAULT_PATH
+  PATHS ${CMAKE_CURRENT_LIST_DIR}
+)
+
+target_link_libraries(test PUBLIC baz)
+
+install(
+  TARGETS test
+  EXPORT test_targets
+)

+ 51 - 0
Tests/RunCMake/Sbom/ProjectMetadata-install-check.cmake

@@ -0,0 +1,51 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(CREATION_INFO [=[
+{
+  "comment" : "This SBOM was generated from the CMakeLists.txt File",
+  "createdUsing" :
+  [
+    {
+      "@id" : "CMake#Agent",
+      "name" : "CMake",
+      "type" : "Tool"
+    }
+  ],
+  "type" : "CreationInfo"
+}
+]=])
+
+
+set(SPDX_DOCUMENT [=[
+{
+  "@id" : "test_targets#SPDXDocument",
+  "name": "test_targets",
+  "profileConformance": ["core", "software"],
+  "type": "SpdxDocument"
+}
+]=])
+
+set(TEST [=[
+{
+  "@id" : "test#Package",
+  "externalRef" :
+  [
+    {
+      "comment" : "Build System used for this target",
+      "externalRefType" : "buildSystem",
+      "locator" : "CMake#Agent",
+      "type" : "ExternalRef"
+    }
+  ],
+  "name" : "test",
+  "primaryPurpose" : "LIBRARY",
+  "type" : "software_Package"
+}
+]=])
+
+
+expect_value("${content}" "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" "@context")
+string(JSON SPDX_DOCUMENT GET "${content}" "@graph" "0")
+expect_object("${SPDX_DOCUMENT}" SPDX_DOCUMENT)
+expect_object("${SPDX_DOCUMENT}" CREATION_INFO "creationInfo")
+expect_object("${SPDX_DOCUMENT}" TEST "rootElement" "0")

+ 11 - 0
Tests/RunCMake/Sbom/ProjectMetadata.cmake

@@ -0,0 +1,11 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Setup.cmake)
+
+include(CMakePackageConfigHelpers)
+include(GNUInstallDirs)
+
+add_library(test INTERFACE)
+
+install(
+  TARGETS test
+  EXPORT test_targets
+)

+ 74 - 0
Tests/RunCMake/Sbom/ReferencesNonExportedTarget-install-check.cmake

@@ -0,0 +1,74 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(CREATION_INFO [=[
+{
+  "comment" : "This SBOM was generated from the CMakeLists.txt File",
+  "createdUsing" :
+  [
+    {
+      "@id" : "CMake#Agent",
+      "name" : "CMake",
+      "type" : "Tool"
+    }
+  ],
+  "type" : "CreationInfo"
+}
+]=])
+
+set(SPDX_DOCUMENT [=[
+{
+  "@id": "_:dog#SPDXDocument",
+  "name": "dog",
+  "profileConformance": ["core", "software"],
+  "type": "SpdxDocument"
+}
+]=])
+
+set(CANINE [=[
+{
+  "@id" : "canine#Package",
+  "externalRef" :
+  [
+    {
+      "comment" : "Build System used for this target",
+      "externalRefType" : "buildSystem",
+      "locator" : "CMake#Agent",
+      "type" : "ExternalRef"
+    }
+  ],
+  "name" : "canine",
+  "primaryPurpose" : "LIBRARY",
+  "type" : "software_Package"
+}
+]=])
+
+set(DEPENDENCY [=[
+{
+  "@id" : "mammal#Package",
+  "name" : "mammal",
+  "type" : "software_Package"
+}
+]=])
+
+set(BUILD_LINK_LIBRARIES [=[
+{
+  "description" : "Required Build-Time Libraries",
+  "from" : "canine#Package",
+  "relationshipType" : "DEPENDS_ON",
+  "to" :
+  [
+    "mammal#Package"
+  ],
+  "type" : "Relationship"
+}
+]=])
+
+expect_value("${content}" "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" "@context")
+string(JSON SPDX_DOCUMENT GET "${content}" "@graph" "0")
+expect_object("${SPDX_DOCUMENT}" SPDX_DOCUMENT)
+expect_object("${SPDX_DOCUMENT}" CREATION_INFO "creationInfo")
+expect_object("${SPDX_DOCUMENT}" CANINE "rootElement" "0")
+expect_object("${SPDX_DOCUMENT}" DEPENDENCY "element" "0")
+
+string(JSON LINKED_LIBRARIES GET "${content}" "@graph" "1")
+expect_object("${LINKED_LIBRARIES}" BUILD_LINK_LIBRARIES)

+ 11 - 0
Tests/RunCMake/Sbom/ReferencesNonExportedTarget.cmake

@@ -0,0 +1,11 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Setup.cmake)
+
+include(CMakePackageConfigHelpers)
+include(GNUInstallDirs)
+
+add_library(mammal INTERFACE)
+add_library(canine INTERFACE)
+target_link_libraries(canine INTERFACE mammal)
+
+install(TARGETS canine EXPORT dog DESTINATION .)
+install(SBOM dog EXPORT dog DESTINATION .)

+ 137 - 0
Tests/RunCMake/Sbom/Requirements-install-check.cmake

@@ -0,0 +1,137 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(CREATION_INFO [=[
+{
+  "comment" : "This SBOM was generated from the CMakeLists.txt File",
+  "createdUsing" :
+  [
+    {
+      "@id" : "CMake#Agent",
+      "name" : "CMake",
+      "type" : "Tool"
+    }
+  ],
+  "type" : "CreationInfo"
+}
+]=])
+
+
+set(BAR_SPDX_DOCUMENT [=[
+{
+  "@id" : "bar#SPDXDocument",
+  "name": "bar",
+  "profileConformance": ["core", "software"],
+  "type": "SpdxDocument"
+}
+]=])
+
+set(FOO_SPDX_DOCUMENT [=[
+{
+  "@id" : "foo#SPDXDocument",
+  "name": "foo",
+  "profileConformance": ["core", "software"],
+  "type": "SpdxDocument"
+}
+]=])
+
+
+set(FOO_LIBB [=[
+{
+  "@id" : "libb#Package",
+  "externalRef" :
+  [
+    {
+      "comment" : "Build System used for this target",
+      "externalRefType" : "buildSystem",
+      "locator" : "CMake#Agent",
+      "type" : "ExternalRef"
+    }
+  ],
+  "name" : "libb",
+  "primaryPurpose" : "LIBRARY",
+  "type" : "software_Package"
+}
+]=])
+
+set(BAR_LIBC [=[
+{
+  "@id" : "libc#Package",
+  "externalRef" :
+  [
+    {
+      "comment" : "Build System used for this target",
+      "externalRefType" : "buildSystem",
+      "locator" : "CMake#Agent",
+      "type" : "ExternalRef"
+    }
+  ],
+  "name" : "libc",
+  "primaryPurpose" : "LIBRARY",
+  "type" : "software_Package"
+}
+]=])
+
+set(BAR_LIBD [=[
+{
+  "@id" : "libd#Package",
+  "externalRef" :
+  [
+    {
+      "comment" : "Build System used for this target",
+      "externalRefType" : "buildSystem",
+      "locator" : "CMake#Agent",
+      "type" : "ExternalRef"
+    }
+  ],
+  "name" : "libd",
+  "primaryPurpose" : "LIBRARY",
+  "type" : "software_Package"
+}
+]=])
+
+set(BAR_DEPENDENCY_TEST [=[
+{
+  "@id" : "test:liba#Package",
+  "name" : "test:liba",
+  "originatedBy" :
+  [
+    {
+      "name" : "test",
+      "type" : "Organization"
+    }
+  ],
+  "type" : "software_Package"
+}
+]=])
+
+
+set(BAR_DEPENDENCY_FOO [=[
+{
+  "@id" : "foo:libb#Package",
+  "name" : "foo:libb",
+  "originatedBy" :
+  [
+    {
+      "name" : "foo",
+      "type" : "Organization"
+    }
+  ],
+  "type" : "software_Package"
+}
+]=])
+
+
+expect_value("${FOO_CONTENT}" "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" "@context")
+string(JSON FOO_SPDX_DOCUMENT GET "${FOO_CONTENT}" "@graph" "0")
+expect_object("${FOO_SPDX_DOCUMENT}" FOO_SPDX_DOCUMENT)
+expect_object("${FOO_SPDX_DOCUMENT}" CREATION_INFO "creationInfo")
+expect_object("${FOO_SPDX_DOCUMENT}" FOO_LIBB "rootElement" "0")
+
+expect_value("${BAR_CONTENT}" "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" "@context")
+string(JSON BAR_SPDX_DOCUMENT GET "${BAR_CONTENT}" "@graph" "0")
+expect_object("${BAR_SPDX_DOCUMENT}" BAR_SPDX_DOCUMENT)
+expect_object("${BAR_SPDX_DOCUMENT}" CREATION_INFO "creationInfo")
+expect_object("${BAR_SPDX_DOCUMENT}" BAR_LIBC "rootElement" "0")
+expect_object("${BAR_SPDX_DOCUMENT}" BAR_LIBD "rootElement" "1")
+expect_object("${BAR_SPDX_DOCUMENT}" BAR_DEPENDENCY_TEST "element" "0")
+expect_object("${BAR_SPDX_DOCUMENT}" BAR_DEPENDENCY_FOO "element" "1")

+ 25 - 0
Tests/RunCMake/Sbom/Requirements.cmake

@@ -0,0 +1,25 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Setup.cmake)
+
+include(CMakePackageConfigHelpers)
+include(GNUInstallDirs)
+
+find_package(
+  test REQUIRED CONFIG
+  NO_DEFAULT_PATH
+  PATHS ${CMAKE_CURRENT_LIST_DIR}
+)
+
+add_library(libb INTERFACE)
+add_library(libc INTERFACE)
+add_library(libd INTERFACE)
+
+add_library(foo ALIAS libb)
+add_library(bar ALIAS libc)
+
+target_link_libraries(libd INTERFACE test::liba foo bar)
+
+install(TARGETS libb EXPORT foo DESTINATION .)
+install(SBOM foo EXPORT foo DESTINATION .)
+
+install(TARGETS libc libd EXPORT bar DESTINATION .)
+install(SBOM bar EXPORT bar DESTINATION .)

+ 10 - 0
Tests/RunCMake/Sbom/Setup.cmake

@@ -0,0 +1,10 @@
+# Disable built-in search paths.
+enable_language(C)
+
+set(CMAKE_FIND_USE_PACKAGE_ROOT_PATH OFF)
+set(CMAKE_FIND_USE_CMAKE_ENVIRONMENT_PATH OFF)
+set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH OFF)
+set(CMAKE_FIND_USE_CMAKE_SYSTEM_PATH OFF)
+set(CMAKE_FIND_USE_INSTALL_PREFIX OFF)
+
+set(CMAKE_PREFIX_PATH ${CMAKE_CURRENT_SOURCE_DIR})

+ 93 - 0
Tests/RunCMake/Sbom/SharedTarget-install-check.cmake

@@ -0,0 +1,93 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(CREATION_INFO [=[
+{
+  "comment" : "This SBOM was generated from the CMakeLists.txt File",
+  "createdUsing" :
+  [
+    {
+      "@id" : "CMake#Agent",
+      "name" : "CMake",
+      "type" : "Tool"
+    }
+  ],
+  "type" : "CreationInfo"
+}
+]=])
+
+set(ELEMENTS [=[
+[
+  {
+    "@id" : "foo:foo#Package",
+    "name" : "foo:foo",
+    "type" : "software_Package"
+  }
+]
+]=])
+
+set(SPDX_DOCUMENT [=[
+{
+  "@id" : "shared_targets#SPDXDocument",
+  "name": "shared_targets",
+  "profileConformance": ["core", "software"],
+  "type": "SpdxDocument"
+}
+]=])
+
+set(SHARED [=[
+{
+  "@id" : "shared#Package",
+  "externalRef" :
+  [
+    {
+      "comment" : "Build System used for this target",
+      "externalRefType" : "buildSystem",
+      "locator" : "CMake#Agent",
+      "type" : "ExternalRef"
+    }
+  ],
+  "name" : "shared",
+  "primaryPurpose" : "LIBRARY",
+  "type" : "software_Package"
+}
+]=])
+
+set(DEPENDENCY [=[
+{
+  "@id" : "foo:foo#Package",
+  "name" : "foo:foo",
+  "originatedBy" :
+  [
+    {
+      "name" : "foo",
+      "type" : "Organization"
+    }
+  ],
+  "packageVersion" : "1.2.3",
+  "type" : "software_Package"
+}
+]=])
+
+set(BUILD_LINKED_LIBRARIES [=[
+{
+  "description" : "Linked Libraries",
+  "from" : "shared#Package",
+  "relationshipType" : "DEPENDS_ON",
+  "to" :
+  [
+    "foo:foo#Package"
+  ],
+  "type" : "Relationship"
+}
+]=])
+
+
+expect_value("${content}" "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" "@context")
+string(JSON SPDX_DOCUMENT GET "${content}" "@graph" "0")
+expect_object("${SPDX_DOCUMENT}" SPDX_DOCUMENT)
+expect_object("${SPDX_DOCUMENT}" CREATION_INFO "creationInfo")
+expect_object("${SPDX_DOCUMENT}" SHARED "rootElement" "0")
+expect_object("${SPDX_DOCUMENT}" DEPENDENCY "element" "0")
+
+string(JSON LINKED_LIBRARIES GET "${content}" "@graph" "1")
+expect_object("${LINKED_LIBRARIES}" BUILD_LINKED_LIBRARIES)

+ 14 - 0
Tests/RunCMake/Sbom/SharedTarget.cmake

@@ -0,0 +1,14 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Setup.cmake)
+
+include(CMakePackageConfigHelpers)
+include(GNUInstallDirs)
+
+add_library(shared SHARED ${CMAKE_CURRENT_LIST_DIR}/main.c)
+
+find_package(foo PATHS ${CMAKE_CURRENT_LIST_DIR})
+target_link_libraries(shared PUBLIC foo::foo)
+
+install(
+  TARGETS shared
+  EXPORT shared_targets
+)

+ 29 - 0
Tests/RunCMake/Sbom/cmake/bar-config-version.cmake

@@ -0,0 +1,29 @@
+set(PACKAGE_VERSION "1.3.5")
+
+if (PACKAGE_FIND_VERSION_RANGE)
+  # Check for a version range
+  if (PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION_RANGE_MIN)
+    set(PACKAGE_VERSION_COMPATIBLE FALSE)
+  else ()
+    if (PACKAGE_FIND_VERSION_RANGE_MAX)
+      if (PACKAGE_VERSION VERSION_GREATER PACKAGE_FIND_VERSION_RANGE_MAX)
+        set(PACKAGE_VERSION_COMPATIBLE FALSE)
+      else ()
+        set(PACKAGE_VERSION_COMPATIBLE TRUE)
+      endif ()
+    else ()
+      set(PACKAGE_VERSION_COMPATIBLE TRUE)
+    endif ()
+  endif ()
+
+elseif (PACKAGE_FIND_VERSION)
+  # Check for a specific version or minimum version
+  if (PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION)
+    set(PACKAGE_VERSION_COMPATIBLE FALSE)
+  else ()
+    set(PACKAGE_VERSION_COMPATIBLE TRUE)
+    if (PACKAGE_VERSION VERSION_EQUAL PACKAGE_FIND_VERSION)
+      set(PACKAGE_VERSION_EXACT TRUE)
+    endif ()
+  endif ()
+endif ()

+ 1 - 0
Tests/RunCMake/Sbom/cmake/bar-config.cmake

@@ -0,0 +1 @@
+add_library(bar::bar INTERFACE IMPORTED)

+ 12 - 0
Tests/RunCMake/Sbom/cmake/bar-config.cps-meta

@@ -0,0 +1,12 @@
+{
+  "name": "bar",
+  "cps_path" : "@prefix@",
+  "cps_version" : "0.13.0",
+  "version_schema" : "simple",
+  "version": "1.3.5",
+  "description": "A description",
+  "license": "MIT",
+  "website": "https://https://gitlab.kitware.com/cmake/cmake",
+  "package_url": "https://github.com/Kitware/CMake/releases/download/v4.1.0/cmake-4.1.0.tar.gz",
+  "components" : {}
+}

+ 29 - 0
Tests/RunCMake/Sbom/cmake/baz-config-version.cmake

@@ -0,0 +1,29 @@
+set(PACKAGE_VERSION "1.8.5")
+
+if (PACKAGE_FIND_VERSION_RANGE)
+  # Check for a version range
+  if (PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION_RANGE_MIN)
+    set(PACKAGE_VERSION_COMPATIBLE FALSE)
+  else ()
+    if (PACKAGE_FIND_VERSION_RANGE_MAX)
+      if (PACKAGE_VERSION VERSION_GREATER PACKAGE_FIND_VERSION_RANGE_MAX)
+        set(PACKAGE_VERSION_COMPATIBLE FALSE)
+      else ()
+        set(PACKAGE_VERSION_COMPATIBLE TRUE)
+      endif ()
+    else ()
+      set(PACKAGE_VERSION_COMPATIBLE TRUE)
+    endif ()
+  endif ()
+
+elseif (PACKAGE_FIND_VERSION)
+  # Check for a specific version or minimum version
+  if (PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION)
+    set(PACKAGE_VERSION_COMPATIBLE FALSE)
+  else ()
+    set(PACKAGE_VERSION_COMPATIBLE TRUE)
+    if (PACKAGE_VERSION VERSION_EQUAL PACKAGE_FIND_VERSION)
+      set(PACKAGE_VERSION_EXACT TRUE)
+    endif ()
+  endif ()
+endif ()

+ 1 - 0
Tests/RunCMake/Sbom/cmake/baz-config.cmake

@@ -0,0 +1 @@
+add_library(baz INTERFACE IMPORTED)

+ 12 - 0
Tests/RunCMake/Sbom/cmake/baz-config.cps-meta

@@ -0,0 +1,12 @@
+{
+  "name": "baz",
+  "cps_path" : "@prefix@",
+  "cps_version" : "0.13.0",
+  "version_schema" : "simple",
+  "version": "1.8.5",
+  "description": "A description",
+  "license": "MIT",
+  "website": "https://example.com/baz",
+  "package_url": "https://example.com/baz/releases/download/v1.8.5/baz-1.8.5.tar.gz",
+  "components" : {}
+}

+ 1 - 0
Tests/RunCMake/Sbom/cmake/test-config.cmake

@@ -0,0 +1 @@
+add_library(test::liba INTERFACE IMPORTED)

+ 21 - 0
Tests/RunCMake/Sbom/cps/foo.cps

@@ -0,0 +1,21 @@
+{
+  "compat_version" : "1.2.0",
+  "components" :
+  {
+    "foo" :
+    {
+      "type" : "interface"
+    }
+  },
+  "configurations" : [ "release", "debug" ],
+  "cps_path" : "@prefix@/cps",
+  "cps_version" : "0.13.0",
+  "default_components" : [ "foo" ],
+  "default_license" : "BSD-3-Clause",
+  "description" : "Sample package",
+  "license" : "BSD-3-Clause AND CC-BY-SA-4.0",
+  "name" : "foo",
+  "version" : "1.2.3",
+  "version_schema" : "simple",
+  "website" : "https://www.example.com/package/foo"
+}

+ 4 - 0
Tests/RunCMake/Sbom/main.c

@@ -0,0 +1,4 @@
+int main(void)
+{
+  return 0;
+}

+ 3 - 0
Tests/RunCMake/Sbom/test.c

@@ -0,0 +1,3 @@
+void foo(int a)
+{
+}