소스 검색

Merge topic 'cps-export'

b2dc1bd149 CMake: Suppress IBM XL optimization warning
1a846c8849 Tests: Add tests for install(PACKAGE_INFO)
3d52d70b84 export: Add initial CPS support

Acked-by: Kitware Robot <[email protected]>
Merge-request: !9693
Brad King 1 년 전
부모
커밋
72607d3402
72개의 변경된 파일1556개의 추가작업 그리고 3개의 파일을 삭제
  1. 7 1
      CompileFlags.cmake
  2. 59 0
      Help/command/install.rst
  3. 20 0
      Help/dev/experimental.rst
  4. 6 0
      Source/CMakeLists.txt
  5. 10 0
      Source/cmExperimental.cxx
  6. 1 0
      Source/cmExperimental.h
  7. 12 2
      Source/cmExportFileGenerator.cxx
  8. 5 0
      Source/cmExportFileGenerator.h
  9. 197 0
      Source/cmExportInstallPackageInfoGenerator.cxx
  10. 66 0
      Source/cmExportInstallPackageInfoGenerator.h
  11. 452 0
      Source/cmExportPackageInfoGenerator.cxx
  12. 116 0
      Source/cmExportPackageInfoGenerator.h
  13. 139 0
      Source/cmInstallCommand.cxx
  14. 36 0
      Source/cmInstallPackageInfoExportGenerator.cxx
  15. 36 0
      Source/cmInstallPackageInfoExportGenerator.h
  16. 1 0
      Tests/RunCMake/CMakeLists.txt
  17. 16 0
      Tests/RunCMake/PackageInfo/Appendix-check.cmake
  18. 9 0
      Tests/RunCMake/PackageInfo/Appendix.cmake
  19. 34 0
      Tests/RunCMake/PackageInfo/Assertions.cmake
  20. 1 0
      Tests/RunCMake/PackageInfo/BadArgs1-result.txt
  21. 2 0
      Tests/RunCMake/PackageInfo/BadArgs1-stderr.txt
  22. 3 0
      Tests/RunCMake/PackageInfo/BadArgs1.cmake
  23. 1 0
      Tests/RunCMake/PackageInfo/BadArgs2-result.txt
  24. 2 0
      Tests/RunCMake/PackageInfo/BadArgs2-stderr.txt
  25. 3 0
      Tests/RunCMake/PackageInfo/BadArgs2.cmake
  26. 1 0
      Tests/RunCMake/PackageInfo/BadArgs3-result.txt
  27. 2 0
      Tests/RunCMake/PackageInfo/BadArgs3-stderr.txt
  28. 3 0
      Tests/RunCMake/PackageInfo/BadArgs3.cmake
  29. 1 0
      Tests/RunCMake/PackageInfo/BadArgs4-result.txt
  30. 2 0
      Tests/RunCMake/PackageInfo/BadArgs4-stderr.txt
  31. 3 0
      Tests/RunCMake/PackageInfo/BadArgs4.cmake
  32. 1 0
      Tests/RunCMake/PackageInfo/BadArgs5-result.txt
  33. 2 0
      Tests/RunCMake/PackageInfo/BadArgs5-stderr.txt
  34. 3 0
      Tests/RunCMake/PackageInfo/BadArgs5.cmake
  35. 1 0
      Tests/RunCMake/PackageInfo/BadDefaultTarget-result.txt
  36. 2 0
      Tests/RunCMake/PackageInfo/BadDefaultTarget-stderr.txt
  37. 5 0
      Tests/RunCMake/PackageInfo/BadDefaultTarget.cmake
  38. 3 0
      Tests/RunCMake/PackageInfo/CMakeLists.txt
  39. 1 0
      Tests/RunCMake/PackageInfo/ExperimentalGate-result.txt
  40. 2 0
      Tests/RunCMake/PackageInfo/ExperimentalGate-stderr.txt
  41. 5 0
      Tests/RunCMake/PackageInfo/ExperimentalGate.cmake
  42. 7 0
      Tests/RunCMake/PackageInfo/ExperimentalWarning-stderr.txt
  43. 8 0
      Tests/RunCMake/PackageInfo/ExperimentalWarning.cmake
  44. 24 0
      Tests/RunCMake/PackageInfo/InterfaceProperties-check.cmake
  45. 15 0
      Tests/RunCMake/PackageInfo/InterfaceProperties.cmake
  46. 9 0
      Tests/RunCMake/PackageInfo/LowerCaseFile-check.cmake
  47. 4 0
      Tests/RunCMake/PackageInfo/LowerCaseFile.cmake
  48. 16 0
      Tests/RunCMake/PackageInfo/Metadata-check.cmake
  49. 12 0
      Tests/RunCMake/PackageInfo/Metadata.cmake
  50. 18 0
      Tests/RunCMake/PackageInfo/Minimal-check.cmake
  51. 3 0
      Tests/RunCMake/PackageInfo/Minimal.cmake
  52. 1 0
      Tests/RunCMake/PackageInfo/ReferencesNonExportedTarget-result.txt
  53. 1 0
      Tests/RunCMake/PackageInfo/ReferencesNonExportedTarget-stderr.txt
  54. 6 0
      Tests/RunCMake/PackageInfo/ReferencesNonExportedTarget.cmake
  55. 1 0
      Tests/RunCMake/PackageInfo/ReferencesWronglyExportedTarget-result.txt
  56. 7 0
      Tests/RunCMake/PackageInfo/ReferencesWronglyExportedTarget-stderr.txt
  57. 14 0
      Tests/RunCMake/PackageInfo/ReferencesWronglyExportedTarget.cmake
  58. 1 0
      Tests/RunCMake/PackageInfo/ReferencesWronglyImportedTarget-result.txt
  59. 3 0
      Tests/RunCMake/PackageInfo/ReferencesWronglyImportedTarget-stderr.txt
  60. 7 0
      Tests/RunCMake/PackageInfo/ReferencesWronglyImportedTarget.cmake
  61. 1 0
      Tests/RunCMake/PackageInfo/ReferencesWronglyNamespacedTarget-result.txt
  62. 4 0
      Tests/RunCMake/PackageInfo/ReferencesWronglyNamespacedTarget-stderr.txt
  63. 11 0
      Tests/RunCMake/PackageInfo/ReferencesWronglyNamespacedTarget.cmake
  64. 20 0
      Tests/RunCMake/PackageInfo/Requirements-check.cmake
  65. 20 0
      Tests/RunCMake/PackageInfo/Requirements.cmake
  66. 32 0
      Tests/RunCMake/PackageInfo/RunCMakeTest.cmake
  67. 11 0
      Tests/RunCMake/PackageInfo/TargetTypes-check.cmake
  68. 20 0
      Tests/RunCMake/PackageInfo/TargetTypes.cmake
  69. 1 0
      Tests/RunCMake/PackageInfo/broken-config.cmake
  70. 3 0
      Tests/RunCMake/PackageInfo/foo.cxx
  71. 1 0
      Tests/RunCMake/PackageInfo/test-config.cmake
  72. 4 0
      Tests/RunCMake/PackageInfo/test.cxx

+ 7 - 1
CompileFlags.cmake

@@ -37,7 +37,13 @@ elseif(_CLANG_MSVC_WINDOWS AND "x${CMAKE_CXX_COMPILER_FRONTEND_VARIANT}" STREQUA
   string(APPEND CMAKE_EXE_LINKER_FLAGS " -Xlinker -stack:20000000")
 endif()
 
-#silence duplicate symbol warnings on AIX
+# Silence "Additional optimization may be attained by recompiling and
+# specifying MAXMEM option" warning on XLC (AIX)
+if(CMAKE_CXX_COMPILER_ID MATCHES "^(XL|XLClang)$")
+  string(APPEND CMAKE_CXX_FLAGS " -qmaxmem=-1")
+endif()
+
+# Silence duplicate symbol warnings on AIX
 if(CMAKE_SYSTEM_NAME MATCHES "AIX")
   if(NOT CMAKE_COMPILER_IS_GNUCXX)
     set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -bhalt:5 ")

+ 59 - 0
Help/command/install.rst

@@ -19,6 +19,7 @@ Synopsis
   install(`SCRIPT`_ <file> [...])
   install(`CODE`_ <code> [...])
   install(`EXPORT`_ <export-name> [...])
+  install(`PACKAGE_INFO`_ <package-name> [...])
   install(`RUNTIME_DEPENDENCY_SET`_ <set-name> [...])
 
 Introduction
@@ -904,6 +905,61 @@ Signatures
   executable from the installation tree using the imported target name
   ``mp_myexe`` as if the target were built in its own tree.
 
+.. signature::
+  install(PACKAGE_INFO <package-name> [...])
+
+  .. versionadded:: 3.31
+  .. note::
+
+    Experimental. Gated by ``CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO``.
+
+  Installs a |CPS|_ file exporting targets for dependent projects:
+
+  .. code-block:: cmake
+
+    install(PACKAGE_INFO <package-name> EXPORT <export-name>
+            [APPENDIX <appendix-name>]
+            [DESTINATION <dir>]
+            [LOWER_CASE_FILE]
+            [VERSION <version>
+             [COMPAT_VERSION <version>]
+             [VERSION_SCHEMA <string>]]
+            [DEFAULT_TARGETS <target>...]
+            [DEFAULT_CONFIGURATIONS <config>...]
+            [PERMISSIONS <permission>...]
+            [CONFIGURATIONS <config>...]
+            [COMPONENT <component>]
+            [EXCLUDE_FROM_ALL])
+
+  The ``PACKAGE_INFO`` form generates and installs a |CPS| file which describes
+  installed targets such that they can be consumed by another project.
+  Target installations are associated with the export ``<export-name>``
+  using the ``EXPORT`` option of the :command:`install(TARGETS)` signature
+  documented above.  Unlike :command:`install(EXPORT)`, this information is not
+  expressed in CMake code, and can be consumed by tools other than CMake.  When
+  imported into another CMake project, the imported targets will be prefixed
+  with ``<package-name>::``.  By default, the generated file will be called
+  ``<package-name>[-<appendix-name>].cps``.  If ``LOWER_CASE_FILE`` is given,
+  the package name as it appears on disk (in both the file name and install
+  destination) will be first converted to lower case.
+
+  If ``DESTINATION`` is not specified, a platform-specific default is used.
+
+  If ``APPENDIX`` is specified, rather than generating a top level package
+  specification, the specified targets will be exported as an appendix to the
+  named package.  Appendices may be used to separate less commonly used targets
+  (along with their external dependencies) from the rest of a package.  This
+  enables consumers to ignore transitive dependencies for targets that they
+  don't use, and also allows a single logical "package" to be composed of
+  artifacts produced by multiple build trees.
+
+  Appendices are not permitted to change basic package metadata; therefore,
+  none of ``VERSION``, ``COMPAT_VERSION``, ``VERSION_SCHEMA``,
+  ``DEFAULT_TARGETS`` or ``DEFAULT_CONFIGURATIONS`` may be specified in
+  combination with ``APPENDIX``.  Additionally, it is strongly recommended that
+  use of ``LOWER_CASE_FILE`` should be consistent between the main package and
+  any appendices.
+
 .. signature::
   install(RUNTIME_DEPENDENCY_SET <set-name> [...])
 
@@ -1096,3 +1152,6 @@ and by CPack. You can also invoke this script manually with
   This is an environment variable rather than a CMake variable. It allows you
   to change the installation prefix on UNIX systems. See :envvar:`DESTDIR` for
   details.
+
+.. _CPS: https://cps-org.github.io/cps/
+.. |CPS| replace:: Common Package Specification

+ 20 - 0
Help/dev/experimental.rst

@@ -39,6 +39,23 @@ When activated, this experimental feature provides the following:
   using the ``CMAKE_EXPORT_FIND_PACKAGE_NAME`` variable and/or
 ``EXPORT_FIND_PACKAGE_NAME`` target property.
 
+Export |CPS| Package Information
+================================
+
+In order to activate support for this experimental feature, set
+
+* variable ``CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO`` to
+* value ``b80be207-778e-46ba-8080-b23bba22639e``.
+
+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(PACKAGE_INFO)`` command is available to export
+  package information in the |CPS|_ format.
+
 C++ ``import std`` support
 ==========================
 
@@ -60,3 +77,6 @@ When activated, this experimental feature provides the following:
 
 * Targets with the property set to a true value and at least ``cxx_std_23``
   may use ``import std;`` in any scanned C++ source file.
+
+.. _CPS: https://cps-org.github.io/cps/
+.. |CPS| replace:: Common Package Specification

+ 6 - 0
Source/CMakeLists.txt

@@ -217,6 +217,10 @@ add_library(
   cmExportInstallCMakeConfigGenerator.cxx
   cmExportInstallFileGenerator.h
   cmExportInstallFileGenerator.cxx
+  cmExportInstallPackageInfoGenerator.h
+  cmExportInstallPackageInfoGenerator.cxx
+  cmExportPackageInfoGenerator.h
+  cmExportPackageInfoGenerator.cxx
   cmExportTryCompileFileGenerator.h
   cmExportTryCompileFileGenerator.cxx
   cmExportSet.h
@@ -336,6 +340,8 @@ add_library(
   cmInstallFilesGenerator.cxx
   cmInstallImportedRuntimeArtifactsGenerator.h
   cmInstallImportedRuntimeArtifactsGenerator.cxx
+  cmInstallPackageInfoExportGenerator.h
+  cmInstallPackageInfoExportGenerator.cxx
   cmInstallRuntimeDependencySet.h
   cmInstallRuntimeDependencySet.cxx
   cmInstallRuntimeDependencySetGenerator.h

+ 10 - 0
Source/cmExperimental.cxx

@@ -46,6 +46,16 @@ cmExperimental::FeatureData LookupTable[] = {
     {},
     cmExperimental::TryCompileCondition::Always,
     false },
+  // ExportPackageInfo
+  { "ExportPackageInfo",
+    "b80be207-778e-46ba-8080-b23bba22639e",
+    "CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO",
+    "CMake's support for exporting package information in the Common Package "
+    "Specification format. It is meant only for experimentation and feedback "
+    "to CMake developers.",
+    {},
+    cmExperimental::TryCompileCondition::Always,
+    false },
 };
 static_assert(sizeof(LookupTable) / sizeof(LookupTable[0]) ==
                 static_cast<size_t>(cmExperimental::Feature::Sentinel),

+ 1 - 0
Source/cmExperimental.h

@@ -20,6 +20,7 @@ public:
     ExportPackageDependencies,
     WindowsKernelModeDriver,
     CxxImportStd,
+    ExportPackageInfo,
 
     Sentinel,
   };

+ 12 - 2
Source/cmExportFileGenerator.cxx

@@ -331,6 +331,14 @@ void cmExportFileGenerator::PopulateCustomTransitiveInterfaceProperties(
   }
 }
 
+bool cmExportFileGenerator::NoteLinkedTarget(
+  cmGeneratorTarget const* /*target*/, std::string const& /*linkedName*/,
+  cmGeneratorTarget const* /*linkedTarget*/)
+{
+  // Default implementation does nothing; only needed by some generators.
+  return true;
+}
+
 bool cmExportFileGenerator::AddTargetNamespace(std::string& input,
                                                cmGeneratorTarget const* target,
                                                cmLocalGenerator const* lg)
@@ -352,8 +360,9 @@ bool cmExportFileGenerator::AddTargetNamespace(std::string& input,
 
   if (tgt->IsImported()) {
     input = tgt->GetName();
-    return true;
+    return this->NoteLinkedTarget(target, input, tgt);
   }
+
   if (this->ExportedTargets.find(tgt) != this->ExportedTargets.end()) {
     input = this->Namespace + tgt->GetExportName();
   } else {
@@ -365,7 +374,8 @@ bool cmExportFileGenerator::AddTargetNamespace(std::string& input,
       input = tgt->GetName();
     }
   }
-  return true;
+
+  return this->NoteLinkedTarget(target, input, tgt);
 }
 
 void cmExportFileGenerator::ResolveTargetsInGeneratorExpressions(

+ 5 - 0
Source/cmExportFileGenerator.h

@@ -95,6 +95,11 @@ protected:
                                            std::string const& config,
                                            std::string const& suffix) = 0;
 
+  /** Record a target referenced by an exported target. */
+  virtual bool NoteLinkedTarget(cmGeneratorTarget const* target,
+                                std::string const& linkedName,
+                                cmGeneratorTarget const* linkedTarget);
+
   /** Each subclass knows how to deal with a target that is  missing from an
    *  export set.  */
   virtual void HandleMissingTarget(std::string& link_libs,

+ 197 - 0
Source/cmExportInstallPackageInfoGenerator.cxx

@@ -0,0 +1,197 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmExportInstallPackageInfoGenerator.h"
+
+#include <memory>
+#include <set>
+#include <utility>
+#include <vector>
+
+#include <cm3p/json/value.h>
+
+#include "cmExportSet.h"
+#include "cmGeneratorExpression.h"
+#include "cmGeneratorTarget.h"
+#include "cmInstallExportGenerator.h"
+#include "cmLocalGenerator.h"
+#include "cmMakefile.h"
+#include "cmStateTypes.h"
+#include "cmStringAlgorithms.h"
+#include "cmSystemTools.h"
+#include "cmTarget.h"
+#include "cmTargetExport.h"
+
+cmExportInstallPackageInfoGenerator::cmExportInstallPackageInfoGenerator(
+  cmInstallExportGenerator* iegen, std::string packageName,
+  std::string version, std::string versionCompat, std::string versionSchema,
+  std::vector<std::string> defaultTargets,
+  std::vector<std::string> defaultConfigurations)
+  : cmExportPackageInfoGenerator(
+      std::move(packageName), std::move(version), std::move(versionCompat),
+      std::move(versionSchema), std::move(defaultTargets),
+      std::move(defaultConfigurations))
+  , cmExportInstallFileGenerator(iegen)
+{
+}
+
+std::string cmExportInstallPackageInfoGenerator::GetConfigImportFileGlob()
+  const
+{
+  std::string glob = cmStrCat(this->FileBase, "@*", this->FileExt);
+  return glob;
+}
+
+std::string const& cmExportInstallPackageInfoGenerator::GetExportName() const
+{
+  return this->GetPackageName();
+}
+
+bool cmExportInstallPackageInfoGenerator::GenerateMainFile(std::ostream& os)
+{
+  std::vector<cmTargetExport const*> allTargets;
+  {
+    auto visitor = [&](cmTargetExport const* te) { allTargets.push_back(te); };
+
+    if (!this->CollectExports(visitor)) {
+      return false;
+    }
+  }
+
+  if (!this->CheckDefaultTargets()) {
+    return false;
+  }
+
+  Json::Value root = this->GeneratePackageInfo();
+  Json::Value& components = root["components"];
+
+  // Compute the relative import prefix for the file
+  std::string const& packagePath = this->GenerateImportPrefix();
+  if (packagePath.empty()) {
+    return false;
+  }
+  root["cps_path"] = packagePath;
+
+  bool requiresConfigFiles = false;
+  // Create all the imported targets.
+  for (cmTargetExport const* te : allTargets) {
+    cmGeneratorTarget* gt = te->Target;
+    cmStateEnums::TargetType targetType = this->GetExportTargetType(te);
+
+    Json::Value* const component =
+      this->GenerateImportTarget(components, gt, targetType);
+    if (!component) {
+      return false;
+    }
+
+    ImportPropertyMap properties;
+    if (!this->PopulateInterfaceProperties(te, properties)) {
+      return false;
+    }
+    this->PopulateInterfaceLinkLibrariesProperty(
+      gt, cmGeneratorExpression::InstallInterface, properties);
+
+    if (targetType != cmStateEnums::INTERFACE_LIBRARY) {
+      requiresConfigFiles = true;
+    }
+
+    // Set configuration-agnostic properties for component.
+    this->GenerateInterfaceProperties(*component, gt, properties);
+  }
+
+  this->GeneratePackageRequires(root);
+
+  // Write the primary packing information file.
+  this->WritePackageInfo(root, os);
+
+  bool result = true;
+
+  // Generate an import file for each configuration.
+  if (requiresConfigFiles) {
+    for (std::string const& c : this->Configurations) {
+      if (!this->GenerateImportFileConfig(c)) {
+        result = false;
+      }
+    }
+  }
+
+  return result;
+}
+
+void cmExportInstallPackageInfoGenerator::GenerateImportTargetsConfig(
+  std::ostream& os, std::string const& config, std::string const& suffix)
+{
+  Json::Value root;
+  root["name"] = this->GetPackageName();
+  root["configuration"] = config;
+
+  Json::Value& components = root["components"];
+
+  for (auto const& te : this->GetExportSet()->GetTargetExports()) {
+    // Collect import properties for this target.
+    if (this->GetExportTargetType(te.get()) ==
+        cmStateEnums::INTERFACE_LIBRARY) {
+      continue;
+    }
+
+    ImportPropertyMap properties;
+    std::set<std::string> importedLocations;
+
+    this->PopulateImportProperties(config, suffix, te.get(), properties,
+                                   importedLocations);
+
+    this->GenerateInterfaceConfigProperties(components, te->Target, suffix,
+                                            properties);
+  }
+
+  this->WritePackageInfo(root, os);
+}
+
+std::string cmExportInstallPackageInfoGenerator::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(PACKAGE_INFO \"", 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);
+}
+
+std::string cmExportInstallPackageInfoGenerator::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 cmExportInstallPackageInfoGenerator::GetCxxModulesDirectory() const
+{
+  // TODO: Implement a not-CMake-specific mechanism for providing module
+  // information.
+  // return IEGen->GetCxxModuleDirectory();
+  return {};
+}

+ 66 - 0
Source/cmExportInstallPackageInfoGenerator.h

@@ -0,0 +1,66 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <iosfwd>
+#include <string>
+#include <vector>
+
+#include "cmExportInstallFileGenerator.h"
+#include "cmExportPackageInfoGenerator.h"
+
+class cmGeneratorTarget;
+class cmInstallExportGenerator;
+
+/** \class cmExportInstallPackageInfoGenerator
+ * \brief Generate files exporting targets from an install tree.
+ *
+ * cmExportInstallPackageInfoGenerator generates files exporting targets from
+ * an installation tree.  The files are placed in a temporary location for
+ * installation by cmInstallExportGenerator.  The file format is the Common
+ * Package Specification (https://cps-org.github.io/cps/).
+ *
+ * One main file is generated that describes the imported targets.  Additional,
+ * per-configuration files describe target locations and settings for each
+ * configuration.
+ *
+ * This is used to implement the INSTALL(PACKAGE_INFO) command.
+ */
+class cmExportInstallPackageInfoGenerator
+  : public cmExportPackageInfoGenerator
+  , public cmExportInstallFileGenerator
+{
+public:
+  /** Construct with the export installer that will install the
+      files.  */
+  cmExportInstallPackageInfoGenerator(
+    cmInstallExportGenerator* iegen, std::string packageName,
+    std::string version, std::string versionCompat, std::string versionSchema,
+    std::vector<std::string> defaultTargets,
+    std::vector<std::string> defaultConfigurations);
+
+  /** 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;
+
+  // 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;
+
+  char GetConfigFileNameSeparator() const override { return '@'; }
+
+  /** Generate the cps_path, which determines the import prefix.  */
+  std::string GenerateImportPrefix() const;
+
+  std::string InstallNameDir(cmGeneratorTarget const* target,
+                             std::string const& config) override;
+
+  std::string GetCxxModulesDirectory() const override;
+  // TODO: Generate C++ module info in a not-CMake-specific format.
+};

+ 452 - 0
Source/cmExportPackageInfoGenerator.cxx

@@ -0,0 +1,452 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmExportPackageInfoGenerator.h"
+
+#include <memory>
+#include <set>
+#include <utility>
+#include <vector>
+
+#include <cm/string_view>
+#include <cmext/algorithm>
+#include <cmext/string_view>
+
+#include <cm3p/json/value.h>
+#include <cm3p/json/writer.h>
+
+#include "cmExportSet.h"
+#include "cmFindPackageStack.h"
+#include "cmGeneratorExpression.h"
+#include "cmGeneratorTarget.h"
+#include "cmList.h"
+#include "cmMakefile.h"
+#include "cmMessageType.h"
+#include "cmStringAlgorithms.h"
+#include "cmSystemTools.h"
+#include "cmTarget.h"
+#include "cmValue.h"
+
+constexpr char const* cmExportPackageInfoGenerator::CPS_VERSION_STR;
+
+cmExportPackageInfoGenerator::cmExportPackageInfoGenerator(
+  std::string packageName, std::string version, std::string versionCompat,
+  std::string versionSchema, std::vector<std::string> defaultTargets,
+  std::vector<std::string> defaultConfigurations)
+  : PackageName(std::move(packageName))
+  , PackageVersion(std::move(version))
+  , PackageVersionCompat(std::move(versionCompat))
+  , PackageVersionSchema(std::move(versionSchema))
+  , DefaultTargets(std::move(defaultTargets))
+  , DefaultConfigurations(std::move(defaultConfigurations))
+{
+}
+
+cm::string_view cmExportPackageInfoGenerator::GetImportPrefixWithSlash() const
+{
+  return "@prefix@/"_s;
+}
+
+bool cmExportPackageInfoGenerator::GenerateImportFile(std::ostream& os)
+{
+  return this->GenerateMainFile(os);
+}
+
+void cmExportPackageInfoGenerator::WritePackageInfo(
+  Json::Value const& packageInfo, std::ostream& os) const
+{
+  Json::StreamWriterBuilder builder;
+  builder["indentation"] = "  ";
+  builder["commentStyle"] = "None";
+  std::unique_ptr<Json::StreamWriter> const writer(builder.newStreamWriter());
+  writer->write(packageInfo, &os);
+}
+
+namespace {
+template <typename T>
+void buildArray(Json::Value& object, std::string const& property,
+                T const& values)
+{
+  if (!values.empty()) {
+    Json::Value& array = object[property];
+    for (auto const& item : values) {
+      array.append(item);
+    }
+  }
+}
+}
+
+bool cmExportPackageInfoGenerator::CheckDefaultTargets() const
+{
+  bool result = true;
+  std::set<std::string> exportedTargetNames;
+  for (auto const* te : this->ExportedTargets) {
+    exportedTargetNames.emplace(te->GetExportName());
+  }
+
+  for (auto const& name : this->DefaultTargets) {
+    if (!cm::contains(exportedTargetNames, name)) {
+      this->ReportError(
+        cmStrCat("Package \"", this->GetPackageName(),
+                 "\" specifies DEFAULT_TARGETS \"", name,
+                 "\", which is not a target in the export set \"",
+                 this->GetExportSet()->GetName(), "\"."));
+      result = false;
+    }
+  }
+
+  return result;
+}
+
+Json::Value cmExportPackageInfoGenerator::GeneratePackageInfo() const
+{
+  Json::Value package;
+
+  package["name"] = this->GetPackageName();
+  package["cps_version"] = this->CPS_VERSION_STR;
+
+  if (!this->PackageVersion.empty()) {
+    package["version"] = this->PackageVersion;
+    if (!this->PackageVersion.empty()) {
+      package["compat_version"] = this->PackageVersionCompat;
+    }
+    if (!this->PackageVersion.empty()) {
+      package["version_schema"] = this->PackageVersionSchema;
+    }
+  }
+
+  buildArray(package, "default_components", this->DefaultTargets);
+  buildArray(package, "configurations", this->DefaultConfigurations);
+
+  // TODO: description, website, license
+
+  return package;
+}
+
+void cmExportPackageInfoGenerator::GeneratePackageRequires(
+  Json::Value& package) const
+{
+  if (!this->Requirements.empty()) {
+    Json::Value& requirements = package["requires"];
+    for (auto const& requirement : this->Requirements) {
+      // TODO: version, hint
+      requirements[requirement] = Json::Value{};
+    }
+  }
+}
+
+Json::Value* cmExportPackageInfoGenerator::GenerateImportTarget(
+  Json::Value& components, cmGeneratorTarget const* target,
+  cmStateEnums::TargetType targetType) const
+{
+  auto const& name = target->GetExportName();
+  if (name.empty()) {
+    return nullptr;
+  }
+
+  Json::Value& component = components[name];
+  Json::Value& type = component["type"];
+  switch (targetType) {
+    case cmStateEnums::EXECUTABLE:
+      type = "executable";
+      break;
+    case cmStateEnums::STATIC_LIBRARY:
+      type = "archive";
+      break;
+    case cmStateEnums::SHARED_LIBRARY:
+      type = "dylib";
+      break;
+    case cmStateEnums::MODULE_LIBRARY:
+      type = "module";
+      break;
+    case cmStateEnums::INTERFACE_LIBRARY:
+      type = "interface";
+      break;
+    default:
+      type = "unknown";
+      break;
+  }
+  return &component;
+}
+
+bool cmExportPackageInfoGenerator::GenerateInterfaceProperties(
+  Json::Value& component, cmGeneratorTarget const* target,
+  ImportPropertyMap const& properties) const
+{
+  bool result = true;
+
+  this->GenerateInterfaceLinkProperties(result, component, target, properties);
+
+  this->GenerateInterfaceCompileFeatures(result, component, target,
+                                         properties);
+  this->GenerateInterfaceCompileDefines(result, component, target, properties);
+
+  this->GenerateInterfaceListProperty(result, component, target,
+                                      "compile_flags", "COMPILE_OPTIONS"_s,
+                                      properties);
+  this->GenerateInterfaceListProperty(result, component, target, "link_flags",
+                                      "LINK_OPTIONS"_s, properties);
+  this->GenerateInterfaceListProperty(result, component, target,
+                                      "link_directories", "LINK_DIRECTORIES"_s,
+                                      properties);
+  this->GenerateInterfaceListProperty(result, component, target, "includes",
+                                      "INCLUDE_DIRECTORIES"_s, properties);
+
+  // TODO: description, license
+
+  return result;
+}
+
+namespace {
+bool forbidGeneratorExpressions(std::string const& propertyName,
+                                std::string const& propertyValue,
+                                cmGeneratorTarget const* target)
+{
+  std::string const& evaluatedValue = cmGeneratorExpression::Preprocess(
+    propertyValue, cmGeneratorExpression::StripAllGeneratorExpressions);
+  if (evaluatedValue != propertyValue) {
+    target->Makefile->IssueMessage(
+      MessageType::FATAL_ERROR,
+      cmStrCat("Property \"", propertyName, "\" of target \"",
+               target->GetName(),
+               "\" contains a generator expression. This is not allowed."));
+    return false;
+  }
+  return true;
+}
+}
+
+bool cmExportPackageInfoGenerator::NoteLinkedTarget(
+  cmGeneratorTarget const* target, std::string const& linkedName,
+  cmGeneratorTarget const* linkedTarget)
+{
+  if (cm::contains(this->ExportedTargets, linkedTarget)) {
+    // Target is internal to this package.
+    this->LinkTargets.emplace(linkedName,
+                              cmStrCat(':', linkedTarget->GetExportName()));
+    return true;
+  }
+
+  if (linkedTarget->IsImported()) {
+    // Target is imported from a found package.
+    auto pkgName = [linkedTarget]() -> std::string {
+      auto const& pkgStack = linkedTarget->Target->GetFindPackageStack();
+      if (!pkgStack.Empty()) {
+        return pkgStack.Top().Name;
+      }
+
+      return linkedTarget->Target->GetProperty("EXPORT_FIND_PACKAGE_NAME");
+    }();
+
+    if (pkgName.empty()) {
+      target->Makefile->IssueMessage(
+        MessageType::FATAL_ERROR,
+        cmStrCat("Target \"", target->GetName(),
+                 "\" references imported target \"", linkedName,
+                 "\" which does not come from any known package."));
+      return false;
+    }
+
+    auto const& prefix = cmStrCat(pkgName, "::");
+    if (!cmHasPrefix(linkedName, prefix)) {
+      target->Makefile->IssueMessage(
+        MessageType::FATAL_ERROR,
+        cmStrCat("Target \"", target->GetName(), "\" references target \"",
+                 linkedName, "\", which comes from the \"", pkgName,
+                 "\" package, but does not belong to the package's "
+                 "canonical namespace. This is not allowed."));
+      return false;
+    }
+
+    // TODO: Record package version, hint.
+    this->Requirements.emplace(pkgName);
+    this->LinkTargets.emplace(
+      linkedName, cmStrCat(pkgName, ':', linkedName.substr(prefix.length())));
+    return true;
+  }
+
+  // Target belongs to another export from this build.
+  auto const& exportInfo = this->FindExportInfo(linkedTarget);
+  if (exportInfo.first.size() == 1) {
+    auto const& linkNamespace = exportInfo.second;
+    if (!cmHasSuffix(linkNamespace, "::")) {
+      target->Makefile->IssueMessage(
+        MessageType::FATAL_ERROR,
+        cmStrCat("Target \"", target->GetName(), "\" references target \"",
+                 linkedName,
+                 "\", which does not use the standard namespace separator. "
+                 "This is not allowed."));
+      return false;
+    }
+
+    auto pkgName =
+      cm::string_view{ linkNamespace.data(), linkNamespace.size() - 2 };
+
+    if (pkgName == this->GetPackageName()) {
+      this->LinkTargets.emplace(linkedName,
+                                cmStrCat(':', linkedTarget->GetExportName()));
+    } else {
+      this->Requirements.emplace(pkgName);
+      this->LinkTargets.emplace(
+        linkedName, cmStrCat(pkgName, ':', linkedTarget->GetExportName()));
+    }
+    return true;
+  }
+
+  // cmExportFileGenerator::HandleMissingTarget should have complained about
+  // this already. (In fact, we probably shouldn't ever get here.)
+  return false;
+}
+
+void cmExportPackageInfoGenerator::GenerateInterfaceLinkProperties(
+  bool& result, Json::Value& component, cmGeneratorTarget const* target,
+  ImportPropertyMap const& properties) const
+{
+  auto const& iter = properties.find("INTERFACE_LINK_LIBRARIES");
+  if (iter == properties.end()) {
+    return;
+  }
+
+  // TODO: Support $<LINK_ONLY>.
+  if (!forbidGeneratorExpressions(iter->first, iter->second, target)) {
+    result = false;
+    return;
+  }
+
+  std::vector<std::string> buildRequires;
+  // std::vector<std::string> linkRequires; TODO
+  std::vector<std::string> linkLibraries;
+
+  for (auto const& name : cmList{ iter->second }) {
+    auto const& ti = this->LinkTargets.find(name);
+    if (ti != this->LinkTargets.end()) {
+      if (ti->second.empty()) {
+        result = false;
+      } else {
+        buildRequires.emplace_back(ti->second);
+      }
+    } else {
+      linkLibraries.emplace_back(name);
+    }
+  }
+
+  buildArray(component, "requires", buildRequires);
+  // buildArray(component, "link_requires", linkRequires); TODO
+  buildArray(component, "link_libraries", linkLibraries);
+}
+
+void cmExportPackageInfoGenerator::GenerateInterfaceCompileFeatures(
+  bool& result, Json::Value& component, cmGeneratorTarget const* target,
+  ImportPropertyMap const& properties) const
+{
+  auto const& iter = properties.find("INTERFACE_COMPILE_FEATURES");
+  if (iter == properties.end()) {
+    return;
+  }
+
+  if (!forbidGeneratorExpressions(iter->first, iter->second, target)) {
+    result = false;
+    return;
+  }
+
+  std::set<std::string> features;
+  for (auto const& value : cmList{ iter->second }) {
+    if (cmHasLiteralPrefix(value, "c_std_")) {
+      auto suffix = cm::string_view{ value }.substr(6, 2);
+      features.emplace(cmStrCat("cxx", suffix));
+    } else if (cmHasLiteralPrefix(value, "cxx_std_")) {
+      auto suffix = cm::string_view{ value }.substr(8, 2);
+      features.emplace(cmStrCat("c++", suffix));
+    }
+  }
+
+  buildArray(component, "compile_features", features);
+}
+
+void cmExportPackageInfoGenerator::GenerateInterfaceCompileDefines(
+  bool& result, Json::Value& component, cmGeneratorTarget const* target,
+  ImportPropertyMap const& properties) const
+{
+  auto const& iter = properties.find("INTERFACE_COMPILE_DEFINITIONS");
+  if (iter == properties.end()) {
+    return;
+  }
+
+  // TODO: Support language-specific defines.
+  if (!forbidGeneratorExpressions(iter->first, iter->second, target)) {
+    result = false;
+    return;
+  }
+
+  Json::Value defines;
+  for (auto const& def : cmList{ iter->second }) {
+    auto const n = def.find('=');
+    if (n == std::string::npos) {
+      defines[def] = Json::Value{};
+    } else {
+      defines[def.substr(0, n)] = def.substr(n + 1);
+    }
+  }
+
+  if (!defines.empty()) {
+    component["compile_definitions"]["*"] = std::move(defines);
+  }
+}
+
+void cmExportPackageInfoGenerator::GenerateInterfaceListProperty(
+  bool& result, Json::Value& component, cmGeneratorTarget const* target,
+  std::string const& outName, cm::string_view inName,
+  ImportPropertyMap const& properties) const
+{
+  auto const& prop = cmStrCat("INTERFACE_", inName);
+  auto const& iter = properties.find(prop);
+  if (iter == properties.end()) {
+    return;
+  }
+
+  if (!forbidGeneratorExpressions(prop, iter->second, target)) {
+    result = false;
+    return;
+  }
+
+  Json::Value& array = component[outName];
+  for (auto const& value : cmList{ iter->second }) {
+    array.append(value);
+  }
+}
+
+void cmExportPackageInfoGenerator::GenerateInterfaceConfigProperties(
+  Json::Value& components, cmGeneratorTarget const* target,
+  std::string const& suffix, ImportPropertyMap const& properties) const
+{
+  Json::Value component;
+  auto const suffixLength = suffix.length();
+
+  for (auto const& p : properties) {
+    if (!cmHasSuffix(p.first, suffix)) {
+      continue;
+    }
+    auto const n = p.first.length() - suffixLength - 9;
+    auto const prop = cm::string_view{ p.first }.substr(9, n);
+
+    if (prop == "LOCATION") {
+      component["location"] = p.second;
+    } else if (prop == "IMPLIB") {
+      component["link_location"] = p.second;
+    } else if (prop == "LINK_INTERFACE_LANGUAGES") {
+      std::vector<std::string> languages;
+      for (auto const& lang : cmList{ p.second }) {
+        auto ll = cmSystemTools::LowerCase(lang);
+        if (ll == "cxx") {
+          languages.emplace_back("cpp");
+        } else {
+          languages.emplace_back(std::move(ll));
+        }
+      }
+      buildArray(component, "link_languages", languages);
+    }
+  }
+
+  if (!component.empty()) {
+    components[target->GetExportName()] = component;
+  }
+}

+ 116 - 0
Source/cmExportPackageInfoGenerator.h

@@ -0,0 +1,116 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <iosfwd>
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <cm/string_view>
+
+#include "cmExportFileGenerator.h"
+#include "cmStateTypes.h"
+
+class cmGeneratorTarget;
+namespace Json {
+class Value;
+}
+
+/** \class cmExportPackageInfoGenerator
+ * \brief Generate Common Package Specification package information files
+ * exporting targets from a build or install tree.
+ *
+ * cmExportPackageInfoGenerator is the superclass for
+ * cmExportBuildPackageInfoGenerator and cmExportInstallPackageInfoGenerator.
+ * It contains common code generation routines for the two kinds of export
+ * implementations.
+ */
+class cmExportPackageInfoGenerator : virtual public cmExportFileGenerator
+{
+public:
+  cmExportPackageInfoGenerator(std::string packageName, std::string version,
+                               std::string versionCompat,
+                               std::string versionSchema,
+                               std::vector<std::string> defaultTargets,
+                               std::vector<std::string> defaultConfigurations);
+
+  using cmExportFileGenerator::GenerateImportFile;
+
+protected:
+  std::string const& GetPackageName() const { return this->PackageName; }
+
+  void WritePackageInfo(Json::Value const& packageInfo,
+                        std::ostream& os) const;
+
+  // Methods to implement export file code generation.
+  bool GenerateImportFile(std::ostream& os) override;
+
+  bool CheckDefaultTargets() const;
+
+  Json::Value GeneratePackageInfo() const;
+  Json::Value* GenerateImportTarget(Json::Value& components,
+                                    cmGeneratorTarget const* target,
+                                    cmStateEnums::TargetType targetType) const;
+
+  void GeneratePackageRequires(Json::Value& package) const;
+
+  using ImportPropertyMap = std::map<std::string, std::string>;
+  bool GenerateInterfaceProperties(Json::Value& component,
+                                   cmGeneratorTarget const* target,
+                                   ImportPropertyMap const& properties) const;
+  void GenerateInterfaceConfigProperties(
+    Json::Value& components, cmGeneratorTarget const* target,
+    std::string const& suffix, ImportPropertyMap const& properties) const;
+
+  cm::string_view GetImportPrefixWithSlash() const override;
+
+  std::string GetCxxModuleFile(std::string const& /*name*/) const override
+  {
+    // TODO
+    return {};
+  }
+
+  void GenerateCxxModuleConfigInformation(std::string const& /*name*/,
+                                          std::ostream& /*os*/) const override
+  {
+    // TODO
+  }
+
+  bool NoteLinkedTarget(cmGeneratorTarget const* target,
+                        std::string const& linkedName,
+                        cmGeneratorTarget const* linkedTarget) override;
+
+private:
+  void GenerateInterfaceLinkProperties(
+    bool& result, Json::Value& component, cmGeneratorTarget const* target,
+    ImportPropertyMap const& properties) const;
+
+  void GenerateInterfaceCompileFeatures(
+    bool& result, Json::Value& component, cmGeneratorTarget const* target,
+    ImportPropertyMap const& properties) const;
+
+  void GenerateInterfaceCompileDefines(
+    bool& result, Json::Value& component, cmGeneratorTarget const* target,
+    ImportPropertyMap const& properties) const;
+
+  void GenerateInterfaceListProperty(
+    bool& result, Json::Value& component, cmGeneratorTarget const* target,
+    std::string const& outName, cm::string_view inName,
+    ImportPropertyMap const& properties) const;
+
+  std::string const PackageName;
+  std::string const PackageVersion;
+  std::string const PackageVersionCompat;
+  std::string const PackageVersionSchema;
+  std::vector<std::string> DefaultTargets;
+  std::vector<std::string> DefaultConfigurations;
+
+  std::map<std::string, std::string> LinkTargets;
+  std::set<std::string> Requirements;
+
+  static constexpr char const* CPS_VERSION_STR = "0.12.0";
+};

+ 139 - 0
Source/cmInstallCommand.cxx

@@ -36,6 +36,7 @@
 #include "cmInstallGenerator.h"
 #include "cmInstallGetRuntimeDependenciesGenerator.h"
 #include "cmInstallImportedRuntimeArtifactsGenerator.h"
+#include "cmInstallPackageInfoExportGenerator.h"
 #include "cmInstallRuntimeDependencySet.h"
 #include "cmInstallRuntimeDependencySetGenerator.h"
 #include "cmInstallScriptGenerator.h"
@@ -2162,6 +2163,143 @@ bool HandleExportMode(std::vector<std::string> const& args,
   return true;
 }
 
+bool HandlePackageInfoMode(std::vector<std::string> const& args,
+                           cmExecutionStatus& status)
+{
+#ifndef CMAKE_BOOTSTRAP
+  if (!cmExperimental::HasSupportEnabled(
+        status.GetMakefile(), cmExperimental::Feature::ExportPackageInfo)) {
+    status.SetError("does not recognize sub-command PACKAGE_INFO");
+    return false;
+  }
+
+  Helper helper(status);
+
+  // This is the PACKAGE_INFO mode.
+  cmInstallCommandArguments ica(helper.DefaultComponentName);
+
+  ArgumentParser::NonEmpty<std::string> pkg;
+  ArgumentParser::NonEmpty<std::string> appendix;
+  ArgumentParser::NonEmpty<std::string> exportName;
+  bool lowerCase = false;
+  ArgumentParser::NonEmpty<std::string> version;
+  ArgumentParser::NonEmpty<std::string> versionCompat;
+  ArgumentParser::NonEmpty<std::string> versionSchema;
+  ArgumentParser::NonEmpty<std::vector<std::string>> defaultTargets;
+  ArgumentParser::NonEmpty<std::vector<std::string>> defaultConfigs;
+  ArgumentParser::NonEmpty<std::string> cxxModulesDirectory;
+
+  // TODO: Support DESTINATION.
+  ica.Bind("PACKAGE_INFO"_s, pkg);
+  ica.Bind("EXPORT"_s, exportName);
+  ica.Bind("APPENDIX"_s, appendix);
+  ica.Bind("LOWER_CASE_FILE"_s, lowerCase);
+  ica.Bind("VERSION"_s, version);
+  ica.Bind("COMPAT_VERSION"_s, versionCompat);
+  ica.Bind("VERSION_SCHEMA"_s, versionSchema);
+  ica.Bind("DEFAULT_TARGETS"_s, defaultTargets);
+  ica.Bind("DEFAULT_CONFIGURATIONS"_s, defaultConfigs);
+  // ica.Bind("CXX_MODULES_DIRECTORY"_s, cxxModulesDirectory); TODO?
+
+  std::vector<std::string> unknownArgs;
+  ica.Parse(args, &unknownArgs);
+
+  if (!unknownArgs.empty()) {
+    // Unknown argument.
+    status.SetError(
+      cmStrCat(args[0], " given unknown argument \"", unknownArgs[0], "\"."));
+    return false;
+  }
+
+  if (!ica.Finalize()) {
+    return false;
+  }
+
+  if (exportName.empty()) {
+    status.SetError(cmStrCat(args[0], " missing EXPORT."));
+    return false;
+  }
+
+  if (version.empty()) {
+    if (!versionCompat.empty()) {
+      status.SetError("COMPAT_VERSION requires VERSION.");
+      return false;
+    }
+    if (!versionSchema.empty()) {
+      status.SetError("VERSION_SCHEMA requires VERSION.");
+      return false;
+    }
+  } else {
+    if (!appendix.empty()) {
+      status.SetError("APPENDIX and VERSION are mutually exclusive.");
+      return false;
+    }
+  }
+  if (!appendix.empty()) {
+    if (!defaultTargets.empty()) {
+      status.SetError("APPENDIX and DEFAULT_TARGETS are mutually exclusive.");
+      return false;
+    }
+    if (!defaultConfigs.empty()) {
+      status.SetError("APPENDIX and DEFAULT_CONFIGURATIONS "
+                      "are mutually exclusive.");
+      return false;
+    }
+  }
+
+  // Validate the package name.
+  if (!cmGeneratorExpression::IsValidTargetName(pkg) ||
+      pkg.find(':') != std::string::npos) {
+    status.SetError(
+      cmStrCat(args[0], " given invalid package name \"", pkg, "\"."));
+    return false;
+  }
+
+  // Construct the case-normalized package name and the file name.
+  std::string const pkgNameOnDisk =
+    (lowerCase ? cmSystemTools::LowerCase(pkg) : pkg);
+  std::string pkgFileName = [&]() -> std::string {
+    if (appendix.empty()) {
+      return cmStrCat(pkgNameOnDisk, ".cps");
+    }
+    return cmStrCat(pkgNameOnDisk, '-', appendix, ".cps");
+  }();
+
+  // 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{ "cps"_s };
+    } else {
+      dest = cmStrCat(helper.GetLibraryDestination(nullptr), "/cps/",
+                      pkgNameOnDisk);
+    }
+  }
+
+  cmExportSet& exportSet =
+    helper.Makefile->GetGlobalGenerator()->GetExportSets()[exportName];
+
+  cmInstallGenerator::MessageLevel message =
+    cmInstallGenerator::SelectMessageLevel(helper.Makefile);
+
+  // Create the export install generator.
+  helper.Makefile->AddInstallGenerator(
+    cm::make_unique<cmInstallPackageInfoExportGenerator>(
+      &exportSet, dest, ica.GetPermissions(), ica.GetConfigurations(),
+      ica.GetComponent(), message, ica.GetExcludeFromAll(),
+      std::move(pkgFileName), std::move(pkg), std::move(version),
+      std::move(versionCompat), std::move(versionSchema),
+      std::move(defaultTargets), std::move(defaultConfigs),
+      std::move(cxxModulesDirectory), helper.Makefile->GetBacktrace()));
+
+  return true;
+#else
+  static_cast<void>(args);
+  status.SetError("PACKAGE_INFO not supported in bootstrap cmake");
+  return false;
+#endif
+}
+
 bool HandleRuntimeDependencySetMode(std::vector<std::string> const& args,
                                     cmExecutionStatus& status)
 {
@@ -2525,6 +2663,7 @@ bool cmInstallCommand(std::vector<std::string> const& args,
     { "DIRECTORY"_s, HandleDirectoryMode },
     { "EXPORT"_s, HandleExportMode },
     { "EXPORT_ANDROID_MK"_s, HandleExportAndroidMKMode },
+    { "PACKAGE_INFO"_s, HandlePackageInfoMode },
     { "RUNTIME_DEPENDENCY_SET"_s, HandleRuntimeDependencySetMode },
   };
 

+ 36 - 0
Source/cmInstallPackageInfoExportGenerator.cxx

@@ -0,0 +1,36 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmInstallPackageInfoExportGenerator.h"
+
+#include <utility>
+
+#include <cm/memory>
+
+#include "cmExportInstallFileGenerator.h"
+#include "cmExportInstallPackageInfoGenerator.h"
+#include "cmListFileCache.h"
+
+class cmExportSet;
+
+cmInstallPackageInfoExportGenerator::cmInstallPackageInfoExportGenerator(
+  cmExportSet* exportSet, std::string destination, std::string filePermissions,
+  std::vector<std::string> const& configurations, std::string component,
+  MessageLevel message, bool excludeFromAll, std::string filename,
+  std::string packageName, std::string version, std::string versionCompat,
+  std::string versionSchema, std::vector<std::string> defaultTargets,
+  std::vector<std::string> defaultConfigurations,
+  std::string cxxModulesDirectory, cmListFileBacktrace backtrace)
+  : cmInstallExportGenerator(
+      exportSet, std::move(destination), std::move(filePermissions),
+      configurations, std::move(component), message, excludeFromAll,
+      std::move(filename), packageName + "::", std::move(cxxModulesDirectory),
+      std::move(backtrace))
+{
+  this->EFGen = cm::make_unique<cmExportInstallPackageInfoGenerator>(
+    this, std::move(packageName), std::move(version), std::move(versionCompat),
+    std::move(versionSchema), std::move(defaultTargets),
+    std::move(defaultConfigurations));
+}
+
+cmInstallPackageInfoExportGenerator::~cmInstallPackageInfoExportGenerator() =
+  default;

+ 36 - 0
Source/cmInstallPackageInfoExportGenerator.h

@@ -0,0 +1,36 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include "cmInstallExportGenerator.h"
+
+class cmExportSet;
+class cmListFileBacktrace;
+
+/** \class cmInstallPackageInfoGenerator
+ * \brief Generate rules for creating CPS package info files.
+ */
+class cmInstallPackageInfoExportGenerator : public cmInstallExportGenerator
+{
+public:
+  cmInstallPackageInfoExportGenerator(
+    cmExportSet* exportSet, std::string destination,
+    std::string filePermissions,
+    std::vector<std::string> const& configurations, std::string component,
+    MessageLevel message, bool excludeFromAll, std::string filename,
+    std::string packageName, std::string version, std::string versionCompat,
+    std::string versionSchema, std::vector<std::string> defaultTargets,
+    std::vector<std::string> defaultConfigurations,
+    std::string cxxModulesDirectory, cmListFileBacktrace backtrace);
+  cmInstallPackageInfoExportGenerator(
+    cmInstallPackageInfoExportGenerator const&) = delete;
+  ~cmInstallPackageInfoExportGenerator() override;
+
+  cmInstallPackageInfoExportGenerator& operator=(
+    cmInstallPackageInfoExportGenerator const&) = delete;
+
+  char const* InstallSubcommand() const override { return "PACKAGE_INFO"; }
+};

+ 1 - 0
Tests/RunCMake/CMakeLists.txt

@@ -1177,6 +1177,7 @@ add_RunCMake_test(AutoExportDll
   )
 
 add_RunCMake_test(AndroidMK)
+add_RunCMake_test(PackageInfo)
 
 if(CMake_TEST_ANDROID_NDK OR CMake_TEST_ANDROID_STANDALONE_TOOLCHAIN)
   if(NOT "${CMAKE_GENERATOR}" MATCHES "Make|Ninja|Visual Studio 1[456]")

+ 16 - 0
Tests/RunCMake/PackageInfo/Appendix-check.cmake

@@ -0,0 +1,16 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(out_dir "${RunCMake_BINARY_DIR}/Appendix-build/CMakeFiles/Export/510c5684a4a8a792eadfb55bc9744983")
+
+file(READ "${out_dir}/foo.cps" content)
+expect_value("${content}" "foo" "name")
+expect_value("${content}" "interface" "components" "mammal" "type")
+expect_value("${content}" "1.0" "version")
+
+file(READ "${out_dir}/foo-dog.cps" content)
+expect_value("${content}" "foo" "name")
+expect_value("${content}" "interface" "components" "canine" "type")
+expect_missing("${content}" "version")
+
+expect_array("${content}" 1 "components" "canine" "requires")
+expect_value("${content}" ":mammal" "components" "canine" "requires" 0)

+ 9 - 0
Tests/RunCMake/PackageInfo/Appendix.cmake

@@ -0,0 +1,9 @@
+add_library(mammal INTERFACE)
+add_library(canine INTERFACE)
+target_link_libraries(canine INTERFACE mammal)
+
+install(TARGETS mammal EXPORT mammal DESTINATION .)
+install(TARGETS canine EXPORT canine DESTINATION .)
+
+install(PACKAGE_INFO foo DESTINATION cps EXPORT mammal VERSION 1.0)
+install(PACKAGE_INFO foo DESTINATION cps EXPORT canine APPENDIX dog)

+ 34 - 0
Tests/RunCMake/PackageInfo/Assertions.cmake

@@ -0,0 +1,34 @@
+macro(_expect entity op actual expected)
+  if(NOT "${actual}" ${op} "${expected}")
+    list(JOIN ARGN "." name)
+    set(RunCMake_TEST_FAILED
+      "Attribute '${name}' ${entity} '${actual}' does not match expected ${entity} '${expected}'" PARENT_SCOPE)
+    return()
+  endif()
+endmacro()
+
+function(expect_value content expected_value)
+  string(JSON actual_value GET "${content}" ${ARGN})
+  _expect("value" STREQUAL "${actual_value}" "${expected_value}" ${ARGN})
+endfunction()
+
+function(expect_array content expected_length)
+  string(JSON actual_type TYPE "${content}" ${ARGN})
+  _expect("type" STREQUAL "${actual_type}" "ARRAY" ${ARGN})
+
+  string(JSON actual_length LENGTH "${content}" ${ARGN})
+  _expect("length" EQUAL "${actual_length}" "${expected_length}" ${ARGN})
+endfunction()
+
+function(expect_null content)
+  string(JSON actual_type TYPE "${content}" ${ARGN})
+  _expect("type" STREQUAL "${actual_type}" "NULL" ${ARGN})
+endfunction()
+
+function(expect_missing content)
+  string(JSON value ERROR_VARIABLE error GET "${content}" ${ARGN})
+  if(NOT value MATCHES "^(.*-)?NOTFOUND$")
+    set(RunCMake_TEST_FAILED
+      "Attribute '${ARGN}' is unexpectedly present" PARENT_SCOPE)
+  endif()
+endfunction()

+ 1 - 0
Tests/RunCMake/PackageInfo/BadArgs1-result.txt

@@ -0,0 +1 @@
+1

+ 2 - 0
Tests/RunCMake/PackageInfo/BadArgs1-stderr.txt

@@ -0,0 +1,2 @@
+CMake Error at BadArgs1.cmake:3 \(install\):
+  install COMPAT_VERSION requires VERSION.

+ 3 - 0
Tests/RunCMake/PackageInfo/BadArgs1.cmake

@@ -0,0 +1,3 @@
+add_library(foo INTERFACE)
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO test EXPORT foo COMPAT_VERSION 1.0)

+ 1 - 0
Tests/RunCMake/PackageInfo/BadArgs2-result.txt

@@ -0,0 +1 @@
+1

+ 2 - 0
Tests/RunCMake/PackageInfo/BadArgs2-stderr.txt

@@ -0,0 +1,2 @@
+CMake Error at BadArgs2.cmake:3 \(install\):
+  install VERSION_SCHEMA requires VERSION.

+ 3 - 0
Tests/RunCMake/PackageInfo/BadArgs2.cmake

@@ -0,0 +1,3 @@
+add_library(foo INTERFACE)
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO test EXPORT foo VERSION_SCHEMA simple)

+ 1 - 0
Tests/RunCMake/PackageInfo/BadArgs3-result.txt

@@ -0,0 +1 @@
+1

+ 2 - 0
Tests/RunCMake/PackageInfo/BadArgs3-stderr.txt

@@ -0,0 +1,2 @@
+CMake Error at BadArgs3.cmake:3 \(install\):
+  install APPENDIX and VERSION are mutually exclusive.

+ 3 - 0
Tests/RunCMake/PackageInfo/BadArgs3.cmake

@@ -0,0 +1,3 @@
+add_library(foo INTERFACE)
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO test EXPORT foo APPENDIX test VERSION 1.0)

+ 1 - 0
Tests/RunCMake/PackageInfo/BadArgs4-result.txt

@@ -0,0 +1 @@
+1

+ 2 - 0
Tests/RunCMake/PackageInfo/BadArgs4-stderr.txt

@@ -0,0 +1,2 @@
+CMake Error at BadArgs4.cmake:3 \(install\):
+  install APPENDIX and DEFAULT_TARGETS are mutually exclusive.

+ 3 - 0
Tests/RunCMake/PackageInfo/BadArgs4.cmake

@@ -0,0 +1,3 @@
+add_library(foo INTERFACE)
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO test EXPORT foo APPENDIX test DEFAULT_TARGETS foo)

+ 1 - 0
Tests/RunCMake/PackageInfo/BadArgs5-result.txt

@@ -0,0 +1 @@
+1

+ 2 - 0
Tests/RunCMake/PackageInfo/BadArgs5-stderr.txt

@@ -0,0 +1,2 @@
+CMake Error at BadArgs5.cmake:3 \(install\):
+  install APPENDIX and DEFAULT_CONFIGURATIONS are mutually exclusive.

+ 3 - 0
Tests/RunCMake/PackageInfo/BadArgs5.cmake

@@ -0,0 +1,3 @@
+add_library(foo INTERFACE)
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO test EXPORT foo APPENDIX test DEFAULT_CONFIGURATIONS test)

+ 1 - 0
Tests/RunCMake/PackageInfo/BadDefaultTarget-result.txt

@@ -0,0 +1 @@
+1

+ 2 - 0
Tests/RunCMake/PackageInfo/BadDefaultTarget-stderr.txt

@@ -0,0 +1,2 @@
+CMake Error: Package "test" specifies DEFAULT_TARGETS "dog", which is not a target in the export set "foo".
+CMake Error: Package "test" specifies DEFAULT_TARGETS "cat", which is not a target in the export set "foo".

+ 5 - 0
Tests/RunCMake/PackageInfo/BadDefaultTarget.cmake

@@ -0,0 +1,5 @@
+add_library(foo INTERFACE)
+add_library(dog INTERFACE)
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(TARGETS dog EXPORT dog DESTINATION .)
+install(PACKAGE_INFO test EXPORT foo DEFAULT_TARGETS dog cat)

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

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

+ 1 - 0
Tests/RunCMake/PackageInfo/ExperimentalGate-result.txt

@@ -0,0 +1 @@
+1

+ 2 - 0
Tests/RunCMake/PackageInfo/ExperimentalGate-stderr.txt

@@ -0,0 +1,2 @@
+CMake Error at ExperimentalGate.cmake:5 \(install\):
+  install does not recognize sub-command PACKAGE_INFO

+ 5 - 0
Tests/RunCMake/PackageInfo/ExperimentalGate.cmake

@@ -0,0 +1,5 @@
+unset(CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO)
+
+add_library(foo INTERFACE)
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO foo DESTINATION cps EXPORT foo)

+ 7 - 0
Tests/RunCMake/PackageInfo/ExperimentalWarning-stderr.txt

@@ -0,0 +1,7 @@
+CMake Warning \(dev\) at ExperimentalWarning.cmake:8 \(install\):
+  CMake's support for exporting package information in the Common Package
+  Specification format.  It is meant only for experimentation and feedback to
+  CMake developers.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 8 - 0
Tests/RunCMake/PackageInfo/ExperimentalWarning.cmake

@@ -0,0 +1,8 @@
+set(
+  CMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO
+  "b80be207-778e-46ba-8080-b23bba22639e"
+  )
+
+add_library(foo INTERFACE)
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO foo DESTINATION cps EXPORT foo)

+ 24 - 0
Tests/RunCMake/PackageInfo/InterfaceProperties-check.cmake

@@ -0,0 +1,24 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(out_dir "${RunCMake_BINARY_DIR}/InterfaceProperties-build/CMakeFiles/Export/510c5684a4a8a792eadfb55bc9744983")
+
+file(READ "${out_dir}/foo.cps" content)
+expect_value("${content}" "foo" "name")
+
+string(JSON component GET "${content}" "components" "foo")
+
+expect_value("${component}" "interface" "type")
+expect_array("${component}" 1 "includes")
+expect_value("${component}" "@prefix@/include/foo" "includes" 0)
+expect_array("${component}" 1 "compile_features")
+expect_value("${component}" "c++23" "compile_features" 0)
+expect_array("${component}" 1 "compile_flags")
+expect_value("${component}" "-ffast-math" "compile_flags" 0)
+expect_null("${component}" "compile_definitions" "*" "FOO")
+expect_value("${component}" "BAR" "compile_definitions" "*" "BAR")
+expect_array("${component}" 1 "link_directories")
+expect_value("${component}" "/opt/foo/lib" "link_directories" 0)
+expect_array("${component}" 1 "link_flags")
+expect_value("${component}" "--needed" "link_flags" 0)
+expect_array("${component}" 1 "link_libraries")
+expect_value("${component}" "/usr/lib/libm.so" "link_libraries" 0)

+ 15 - 0
Tests/RunCMake/PackageInfo/InterfaceProperties.cmake

@@ -0,0 +1,15 @@
+add_library(foo INTERFACE)
+
+target_compile_features(foo INTERFACE cxx_std_23)
+target_compile_options(foo INTERFACE -ffast-math)
+target_compile_definitions(foo INTERFACE -DFOO -DBAR=BAR)
+target_include_directories(
+  foo INTERFACE
+  $<INSTALL_INTERFACE:include/foo>
+  )
+target_link_directories(foo INTERFACE /opt/foo/lib)
+target_link_options(foo INTERFACE --needed)
+target_link_libraries(foo INTERFACE /usr/lib/libm.so)
+
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO foo DESTINATION cps EXPORT foo)

+ 9 - 0
Tests/RunCMake/PackageInfo/LowerCaseFile-check.cmake

@@ -0,0 +1,9 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(out_dir "${RunCMake_BINARY_DIR}/LowerCaseFile-build/CMakeFiles/Export/510c5684a4a8a792eadfb55bc9744983")
+
+file(READ "${out_dir}/lowercase.cps" content)
+expect_value("${content}" "LowerCase" "name")
+
+file(READ "${out_dir}/PreserveCase.cps" content)
+expect_value("${content}" "PreserveCase" "name")

+ 4 - 0
Tests/RunCMake/PackageInfo/LowerCaseFile.cmake

@@ -0,0 +1,4 @@
+add_library(foo INTERFACE)
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO LowerCase DESTINATION cps EXPORT foo LOWER_CASE_FILE)
+install(PACKAGE_INFO PreserveCase DESTINATION cps EXPORT foo)

+ 16 - 0
Tests/RunCMake/PackageInfo/Metadata-check.cmake

@@ -0,0 +1,16 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(out_dir "${RunCMake_BINARY_DIR}/Metadata-build/CMakeFiles/Export/510c5684a4a8a792eadfb55bc9744983")
+
+file(READ "${out_dir}/foo.cps" content)
+expect_value("${content}" "foo" "name")
+expect_value("${content}" "1.2.3" "version")
+expect_value("${content}" "1.2.0" "compat_version")
+expect_value("${content}" "simple" "version_schema")
+
+expect_array("${content}" 1 "default_components")
+expect_value("${content}" "foo" "default_components" 0)
+
+expect_array("${content}" 2 "configurations")
+expect_value("${content}" "release" "configurations" 0)
+expect_value("${content}" "debug" "configurations" 1)

+ 12 - 0
Tests/RunCMake/PackageInfo/Metadata.cmake

@@ -0,0 +1,12 @@
+add_library(foo INTERFACE)
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(
+  PACKAGE_INFO foo
+  DESTINATION cps
+  EXPORT foo
+  VERSION 1.2.3
+  VERSION_SCHEMA simple
+  COMPAT_VERSION 1.2.0
+  DEFAULT_TARGETS foo
+  DEFAULT_CONFIGURATIONS release debug
+  )

+ 18 - 0
Tests/RunCMake/PackageInfo/Minimal-check.cmake

@@ -0,0 +1,18 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(out_dir "${RunCMake_BINARY_DIR}/Minimal-build/CMakeFiles/Export/510c5684a4a8a792eadfb55bc9744983")
+
+file(READ "${out_dir}/foo.cps" content)
+expect_value("${content}" "foo" "name")
+expect_value("${content}" "interface" "components" "foo" "type")
+expect_missing("${content}" "version")
+expect_missing("${content}" "configurations")
+expect_missing("${content}" "default_targets")
+expect_missing("${content}" "components" "foo" "compile_definitions")
+expect_missing("${content}" "components" "foo" "compile_features")
+expect_missing("${content}" "components" "foo" "compile_flags")
+expect_missing("${content}" "components" "foo" "link_directories")
+expect_missing("${content}" "components" "foo" "link_features")
+expect_missing("${content}" "components" "foo" "link_flags")
+expect_missing("${content}" "components" "foo" "link_libraries")
+expect_missing("${content}" "components" "foo" "requires")

+ 3 - 0
Tests/RunCMake/PackageInfo/Minimal.cmake

@@ -0,0 +1,3 @@
+add_library(foo INTERFACE)
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO foo DESTINATION cps EXPORT foo)

+ 1 - 0
Tests/RunCMake/PackageInfo/ReferencesNonExportedTarget-result.txt

@@ -0,0 +1 @@
+1

+ 1 - 0
Tests/RunCMake/PackageInfo/ReferencesNonExportedTarget-stderr.txt

@@ -0,0 +1 @@
+CMake Error: install\(PACKAGE_INFO "dog" \.\.\.\) includes target "canine" which requires target "mammal" that is not in any export set.

+ 6 - 0
Tests/RunCMake/PackageInfo/ReferencesNonExportedTarget.cmake

@@ -0,0 +1,6 @@
+add_library(mammal INTERFACE)
+add_library(canine INTERFACE)
+target_link_libraries(canine INTERFACE mammal)
+
+install(TARGETS canine EXPORT dog DESTINATION .)
+install(PACKAGE_INFO dog EXPORT dog)

+ 1 - 0
Tests/RunCMake/PackageInfo/ReferencesWronglyExportedTarget-result.txt

@@ -0,0 +1 @@
+1

+ 7 - 0
Tests/RunCMake/PackageInfo/ReferencesWronglyExportedTarget-stderr.txt

@@ -0,0 +1,7 @@
+CMake Error in CMakeLists.txt:
+  Target "test" references target "foo", which does not use the standard
+  namespace separator.  This is not allowed.
+.*
+CMake Error in CMakeLists.txt:
+  Target "test" references target "bar_bar", which does not use the standard
+  namespace separator.  This is not allowed.

+ 14 - 0
Tests/RunCMake/PackageInfo/ReferencesWronglyExportedTarget.cmake

@@ -0,0 +1,14 @@
+add_library(foo INTERFACE)
+add_library(bar INTERFACE)
+
+add_library(test INTERFACE)
+target_link_libraries(test INTERFACE foo bar)
+
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(TARGETS bar EXPORT bar DESTINATION .)
+
+install(EXPORT foo DESTINATION .)
+install(EXPORT bar DESTINATION . NAMESPACE bar_)
+
+install(TARGETS test EXPORT test DESTINATION .)
+install(PACKAGE_INFO test EXPORT test)

+ 1 - 0
Tests/RunCMake/PackageInfo/ReferencesWronglyImportedTarget-result.txt

@@ -0,0 +1 @@
+1

+ 3 - 0
Tests/RunCMake/PackageInfo/ReferencesWronglyImportedTarget-stderr.txt

@@ -0,0 +1,3 @@
+CMake Error in CMakeLists.txt:
+  Target "foo" references imported target "bar" which does not come from any
+  known package.

+ 7 - 0
Tests/RunCMake/PackageInfo/ReferencesWronglyImportedTarget.cmake

@@ -0,0 +1,7 @@
+add_library(bar INTERFACE IMPORTED)
+
+add_library(foo INTERFACE)
+target_link_libraries(foo INTERFACE bar)
+
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO foo EXPORT foo)

+ 1 - 0
Tests/RunCMake/PackageInfo/ReferencesWronglyNamespacedTarget-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/PackageInfo/ReferencesWronglyNamespacedTarget-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error in CMakeLists.txt:
+  Target "foo" references target "wrong::lib", which comes from the "broken"
+  package, but does not belong to the package's canonical namespace.  This is
+  not allowed.

+ 11 - 0
Tests/RunCMake/PackageInfo/ReferencesWronglyNamespacedTarget.cmake

@@ -0,0 +1,11 @@
+find_package(
+  broken REQUIRED CONFIG
+  NO_DEFAULT_PATH
+  PATHS ${CMAKE_CURRENT_LIST_DIR}
+  )
+
+add_library(foo INTERFACE)
+target_link_libraries(foo INTERFACE wrong::lib)
+
+install(TARGETS foo EXPORT foo DESTINATION .)
+install(PACKAGE_INFO foo EXPORT foo)

+ 20 - 0
Tests/RunCMake/PackageInfo/Requirements-check.cmake

@@ -0,0 +1,20 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(out_dir "${RunCMake_BINARY_DIR}/Requirements-build/CMakeFiles/Export/510c5684a4a8a792eadfb55bc9744983")
+
+file(READ "${out_dir}/foo.cps" content)
+expect_value("${content}" "foo" "name")
+expect_value("${content}" "interface" "components" "libb" "type")
+
+file(READ "${out_dir}/bar.cps" content)
+expect_value("${content}" "bar" "name")
+expect_null("${content}" "requires" "foo")
+expect_null("${content}" "requires" "test")
+expect_value("${content}" "interface" "components" "libc" "type")
+expect_value("${content}" "interface" "components" "libd" "type")
+
+string(JSON component GET "${content}" "components" "libd")
+expect_array("${component}" 3 "requires")
+expect_value("${component}" "test:liba" "requires" 0)
+expect_value("${component}"  "foo:libb" "requires" 1)
+expect_value("${component}"     ":libc" "requires" 2)

+ 20 - 0
Tests/RunCMake/PackageInfo/Requirements.cmake

@@ -0,0 +1,20 @@
+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(PACKAGE_INFO foo DESTINATION cps EXPORT foo)
+
+install(TARGETS libc libd EXPORT bar DESTINATION .)
+install(PACKAGE_INFO bar DESTINATION cps EXPORT bar)

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

@@ -0,0 +1,32 @@
+include(RunCMake)
+
+# Test experimental gate
+run_cmake(ExperimentalGate)
+run_cmake(ExperimentalWarning)
+
+# Enable experimental feature and suppress warnings
+set(RunCMake_TEST_OPTIONS
+  -Wno-dev
+  "-DCMAKE_EXPERIMENTAL_EXPORT_PACKAGE_INFO:STRING=b80be207-778e-46ba-8080-b23bba22639e"
+  )
+
+# Test incorrect usage
+run_cmake(BadArgs1)
+run_cmake(BadArgs2)
+run_cmake(BadArgs3)
+run_cmake(BadArgs4)
+run_cmake(BadArgs5)
+run_cmake(BadDefaultTarget)
+run_cmake(ReferencesNonExportedTarget)
+run_cmake(ReferencesWronglyExportedTarget)
+run_cmake(ReferencesWronglyImportedTarget)
+run_cmake(ReferencesWronglyNamespacedTarget)
+
+# Test functionality
+run_cmake(Appendix)
+run_cmake(InterfaceProperties)
+run_cmake(Metadata)
+run_cmake(Minimal)
+run_cmake(LowerCaseFile)
+run_cmake(Requirements)
+run_cmake(TargetTypes)

+ 11 - 0
Tests/RunCMake/PackageInfo/TargetTypes-check.cmake

@@ -0,0 +1,11 @@
+include(${CMAKE_CURRENT_LIST_DIR}/Assertions.cmake)
+
+set(out_dir "${RunCMake_BINARY_DIR}/TargetTypes-build/CMakeFiles/Export/510c5684a4a8a792eadfb55bc9744983")
+
+file(READ "${out_dir}/foo.cps" content)
+expect_value("${content}" "foo" "name")
+expect_value("${content}" "archive"    "components" "foo-static" "type")
+expect_value("${content}" "dylib"      "components" "foo-shared" "type")
+expect_value("${content}" "module"     "components" "foo-module" "type")
+expect_value("${content}" "interface"  "components" "bar"        "type")
+expect_value("${content}" "executable" "components" "test"       "type")

+ 20 - 0
Tests/RunCMake/PackageInfo/TargetTypes.cmake

@@ -0,0 +1,20 @@
+project(TargetTypes CXX)
+
+add_library(foo-static STATIC foo.cxx)
+add_library(foo-shared SHARED foo.cxx)
+add_library(foo-module MODULE foo.cxx)
+add_library(bar INTERFACE)
+add_executable(test test.cxx)
+
+install(
+  TARGETS
+    foo-static
+    foo-shared
+    foo-module
+    bar
+    test
+  EXPORT foo
+  DESTINATION .
+  )
+
+install(PACKAGE_INFO foo DESTINATION cps EXPORT foo)

+ 1 - 0
Tests/RunCMake/PackageInfo/broken-config.cmake

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

+ 3 - 0
Tests/RunCMake/PackageInfo/foo.cxx

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

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

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

+ 4 - 0
Tests/RunCMake/PackageInfo/test.cxx

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