Jelajahi Sumber

export: Add initial CPS support

Add initial support for exporting (install only, for now) Common Package
Specification (https://cps-org.github.io/cps/) format package
descriptions. This has some limitations, such as not supporting
generator expressions (as these cannot be portably exported), and only
partially supporting transitive dependencies, but should be usable for
at least some simple cases. (Actually, $<LINK_ONLY> is theoretically
supportable, but is not yet implemented.)

This still needs tests; these will be added in the next commit. Other
potential improvements include support for language-specific compile
definitions and inferring some package properties from project
properties. Additionally, there is no module support yet; this is partly
pending on having a tool agnostic format for providing the necessary
information.
Matthew Woehlke 1 tahun lalu
induk
melakukan
3d52d70b84

+ 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
@@ -905,6 +906,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> [...])
 
@@ -1097,3 +1153,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"; }
+};