Browse Source

Merge topic 'cpp-named-module-export-infra'

f62c3c3c72 RunCMake/CXXModules: test public modules requiring private modules
c5d4dd713f RunCMake/CXXModules: add tests which export BMIs
4d55f1422e RunCMake/CXXModules: test installation of BMIs and interfaces
eff45f790d RunCMake/CXXModules: fix example follow-on case names
a87c39dad1 RunCMake/CXXModules: output example test output upon failure
727e3db07a RunCMake/CXXModules: append to the test options
f899563ae4 cmGlobalNinjaGenerator: verify that private sources stay private
9ecd3e771b cmGlobalNinjaGenerator: generate install rules for BMI files
...

Acked-by: Kitware Robot <[email protected]>
Tested-by: buildbot <[email protected]>
Acked-by: Alex <[email protected]>
Merge-request: !7224
Brad King 3 years ago
parent
commit
4c50f639c7
100 changed files with 2709 additions and 32 deletions
  1. 14 2
      Help/command/export.rst
  2. 28 3
      Help/command/install.rst
  3. 2 2
      Help/command/target_sources.rst
  4. 1 1
      Help/dev/experimental.rst
  5. 2 0
      Source/CMakeLists.txt
  6. 1 1
      Source/cmExperimental.cxx
  7. 66 0
      Source/cmExportBuildFileGenerator.cxx
  8. 16 0
      Source/cmExportBuildFileGenerator.h
  9. 9 0
      Source/cmExportCommand.cxx
  10. 25 0
      Source/cmExportFileGenerator.cxx
  11. 5 0
      Source/cmExportFileGenerator.h
  12. 1 2
      Source/cmExportInstallAndroidMKGenerator.cxx
  13. 73 1
      Source/cmExportInstallFileGenerator.cxx
  14. 25 0
      Source/cmExportInstallFileGenerator.h
  15. 3 0
      Source/cmExportTryCompileFileGenerator.h
  16. 16 0
      Source/cmFileAPICodemodel.cxx
  17. 371 3
      Source/cmGlobalNinjaGenerator.cxx
  18. 3 1
      Source/cmGlobalNinjaGenerator.h
  19. 70 8
      Source/cmInstallCommand.cxx
  20. 75 0
      Source/cmInstallCxxModuleBmiGenerator.cxx
  21. 52 0
      Source/cmInstallCxxModuleBmiGenerator.h
  22. 72 1
      Source/cmInstallExportGenerator.cxx
  23. 7 1
      Source/cmInstallExportGenerator.h
  24. 216 0
      Source/cmNinjaTargetGenerator.cxx
  25. 2 0
      Source/cmTargetExport.h
  26. 2 1
      Source/cmTargetSourcesCommand.cxx
  27. 1 1
      Tests/RunCMake/CXXModules/CMakeLists.txt
  28. 40 0
      Tests/RunCMake/CXXModules/ExportBuildCxxModules-check.cmake
  29. 11 0
      Tests/RunCMake/CXXModules/ExportBuildCxxModules-stderr.txt
  30. 22 0
      Tests/RunCMake/CXXModules/ExportBuildCxxModules.cmake
  31. 35 0
      Tests/RunCMake/CXXModules/ExportInstallCxxModules-check.cmake
  32. 11 0
      Tests/RunCMake/CXXModules/ExportInstallCxxModules-stderr.txt
  33. 22 0
      Tests/RunCMake/CXXModules/ExportInstallCxxModules.cmake
  34. 6 0
      Tests/RunCMake/CXXModules/FileSetModuleHeaderUnitsInterfaceImported-stderr.txt
  35. 8 0
      Tests/RunCMake/CXXModules/FileSetModuleHeaderUnitsInterfaceImported.cmake
  36. 6 0
      Tests/RunCMake/CXXModules/FileSetModulesInterfaceImported-stderr.txt
  37. 8 0
      Tests/RunCMake/CXXModules/FileSetModulesInterfaceImported.cmake
  38. 24 0
      Tests/RunCMake/CXXModules/InstallBMI-check.cmake
  39. 6 0
      Tests/RunCMake/CXXModules/InstallBMI-stderr.txt
  40. 23 0
      Tests/RunCMake/CXXModules/InstallBMI.cmake
  41. 8 0
      Tests/RunCMake/CXXModules/InstallBMIGenericArgs-check.cmake
  42. 6 0
      Tests/RunCMake/CXXModules/InstallBMIGenericArgs-stderr.txt
  43. 9 0
      Tests/RunCMake/CXXModules/InstallBMIGenericArgs.cmake
  44. 13 0
      Tests/RunCMake/CXXModules/InstallBMIIgnore-check.cmake
  45. 6 0
      Tests/RunCMake/CXXModules/InstallBMIIgnore-stderr.txt
  46. 9 0
      Tests/RunCMake/CXXModules/InstallBMIIgnore.cmake
  47. 8 0
      Tests/RunCMake/CXXModules/InstallBMINoGenericArgs-check.cmake
  48. 34 0
      Tests/RunCMake/CXXModules/NinjaDependInfoBMIInstall-check.cmake
  49. 11 0
      Tests/RunCMake/CXXModules/NinjaDependInfoBMIInstall-stderr.txt
  50. 76 0
      Tests/RunCMake/CXXModules/NinjaDependInfoBMIInstall.cmake
  51. 34 0
      Tests/RunCMake/CXXModules/NinjaDependInfoExport-check.cmake
  52. 11 0
      Tests/RunCMake/CXXModules/NinjaDependInfoExport-stderr.txt
  53. 85 0
      Tests/RunCMake/CXXModules/NinjaDependInfoExport.cmake
  54. 34 0
      Tests/RunCMake/CXXModules/NinjaDependInfoFileSet-check.cmake
  55. 11 0
      Tests/RunCMake/CXXModules/NinjaDependInfoFileSet-stderr.txt
  56. 59 0
      Tests/RunCMake/CXXModules/NinjaDependInfoFileSet.cmake
  57. 52 3
      Tests/RunCMake/CXXModules/RunCMakeTest.cmake
  58. 160 0
      Tests/RunCMake/CXXModules/check-json.cmake
  59. 22 0
      Tests/RunCMake/CXXModules/examples/cxx-modules-find-bmi-and-interfaces.cmake
  60. 27 0
      Tests/RunCMake/CXXModules/examples/cxx-modules-find-bmi.cmake
  61. 1 1
      Tests/RunCMake/CXXModules/examples/cxx-modules-rules.cmake
  62. 9 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build-stderr.txt
  63. 56 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build/CMakeLists.txt
  64. 6 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build/forward.cxx
  65. 8 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build/importable.cxx
  66. 6 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build/private.cxx
  67. 32 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build/test/CMakeLists.txt
  68. 9 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install-stderr.txt
  69. 59 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install/CMakeLists.txt
  70. 6 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install/forward.cxx
  71. 8 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install/importable.cxx
  72. 6 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install/private.cxx
  73. 32 0
      Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install/test/CMakeLists.txt
  74. 9 0
      Tests/RunCMake/CXXModules/examples/export-interface-build-stderr.txt
  75. 53 0
      Tests/RunCMake/CXXModules/examples/export-interface-build/CMakeLists.txt
  76. 6 0
      Tests/RunCMake/CXXModules/examples/export-interface-build/forward.cxx
  77. 8 0
      Tests/RunCMake/CXXModules/examples/export-interface-build/importable.cxx
  78. 6 0
      Tests/RunCMake/CXXModules/examples/export-interface-build/private.cxx
  79. 32 0
      Tests/RunCMake/CXXModules/examples/export-interface-build/test/CMakeLists.txt
  80. 9 0
      Tests/RunCMake/CXXModules/examples/export-interface-install-stderr.txt
  81. 56 0
      Tests/RunCMake/CXXModules/examples/export-interface-install/CMakeLists.txt
  82. 6 0
      Tests/RunCMake/CXXModules/examples/export-interface-install/forward.cxx
  83. 8 0
      Tests/RunCMake/CXXModules/examples/export-interface-install/importable.cxx
  84. 6 0
      Tests/RunCMake/CXXModules/examples/export-interface-install/private.cxx
  85. 32 0
      Tests/RunCMake/CXXModules/examples/export-interface-install/test/CMakeLists.txt
  86. 9 0
      Tests/RunCMake/CXXModules/examples/install-bmi-and-interfaces-stderr.txt
  87. 27 0
      Tests/RunCMake/CXXModules/examples/install-bmi-and-interfaces/CMakeLists.txt
  88. 7 0
      Tests/RunCMake/CXXModules/examples/install-bmi-and-interfaces/check-for-bmi.cmake
  89. 6 0
      Tests/RunCMake/CXXModules/examples/install-bmi-and-interfaces/importable.cxx
  90. 9 0
      Tests/RunCMake/CXXModules/examples/install-bmi-stderr.txt
  91. 25 0
      Tests/RunCMake/CXXModules/examples/install-bmi/CMakeLists.txt
  92. 4 0
      Tests/RunCMake/CXXModules/examples/install-bmi/check-for-bmi.cmake
  93. 6 0
      Tests/RunCMake/CXXModules/examples/install-bmi/importable.cxx
  94. 1 0
      Tests/RunCMake/CXXModules/examples/public-req-private-build-result.txt
  95. 1 0
      Tests/RunCMake/CXXModules/examples/public-req-private-build-stdout.txt
  96. 9 0
      Tests/RunCMake/CXXModules/examples/public-req-private-stderr.txt
  97. 22 0
      Tests/RunCMake/CXXModules/examples/public-req-private/CMakeLists.txt
  98. 6 0
      Tests/RunCMake/CXXModules/examples/public-req-private/priv.cxx
  99. 8 0
      Tests/RunCMake/CXXModules/examples/public-req-private/pub.cxx
  100. 45 0
      Tests/RunCMake/CXXModules/expect/NinjaDependInfoBMIInstall-private.json

+ 14 - 2
Help/command/export.rst

@@ -25,7 +25,8 @@ Exporting Targets
 .. code-block:: cmake
 
   export(TARGETS <target>... [NAMESPACE <namespace>]
-         [APPEND] FILE <filename> [EXPORT_LINK_INTERFACE_LIBRARIES])
+         [APPEND] FILE <filename> [EXPORT_LINK_INTERFACE_LIBRARIES]
+         [CXX_MODULES_DIRECTORY <directory>])
 
 Creates a file ``<filename>`` that may be included by outside projects to
 import targets named by ``<target>...`` from the current project's build tree.
@@ -52,6 +53,16 @@ The options are:
   in the export, even when policy :policy:`CMP0022` is NEW.  This is useful
   to support consumers using CMake versions older than 2.8.12.
 
+``CXX_MODULES_DIRECTORY <directory>``
+
+.. note ::
+
+  Experimental. Gated by ``CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API``
+
+  Export C++ module properties to files under the given directory. Each file
+  will be named according to the target's export name (without any namespace).
+  These files will automatically be included from the export file.
+
 This signature requires all targets to be listed explicitly.  If a library
 target is included in the export, but a target to which it links is not
 included, the behavior is unspecified.  See the `export(EXPORT)`_ signature
@@ -95,7 +106,8 @@ Exporting Targets matching install(EXPORT)
 
 .. code-block:: cmake
 
-  export(EXPORT <export-name> [NAMESPACE <namespace>] [FILE <filename>])
+  export(EXPORT <export-name> [NAMESPACE <namespace>] [FILE <filename>]
+         [CXX_MODULES_DIRECTORY <directory>])
 
 Creates a file ``<filename>`` that may be included by outside projects to
 import targets from the current project's build tree.  This is the same

+ 28 - 3
Help/command/install.rst

@@ -132,7 +132,7 @@ Installing Targets
   install(TARGETS targets... [EXPORT <export-name>]
           [RUNTIME_DEPENDENCIES args...|RUNTIME_DEPENDENCY_SET <set-name>]
           [[ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|
-            PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE|FILE_SET <set-name>]
+            PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE|FILE_SET <set-name>|CXX_MODULES_BMI]
            [DESTINATION <dir>]
            [PERMISSIONS permissions...]
            [CONFIGURATIONS [Debug|Release|...]]
@@ -215,6 +215,18 @@ that may be installed:
   ``/blah/include/myproj/here.h`` with a base directory ``/blah/include``
   would be installed to ``myproj/here.h`` below the destination.
 
+``CXX_MODULES_BMI``
+
+.. note ::
+
+  Experimental. Gated by ``CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API``
+
+  Any module files from C++ modules from ``PUBLIC`` sources in a file set of
+  type ``CXX_MODULES`` will be installed to the given ``DESTINATION``. All
+  modules are placed directly in the destination as no directory structure is
+  derived from the names of the modules. An empty ``DESTINATION`` may be used
+  to suppress installing these files (for use in generic code).
+
 For each of these arguments given, the arguments following them only apply
 to the target or file type specified in the argument. If none is given, the
 installation properties apply to all target types. If only one is given then
@@ -778,9 +790,10 @@ Installing Exports
 .. code-block:: cmake
 
   install(EXPORT <export-name> DESTINATION <dir>
-          [NAMESPACE <namespace>] [[FILE <name>.cmake]|
+          [NAMESPACE <namespace>] [FILE <name>.cmake]
           [PERMISSIONS permissions...]
-          [CONFIGURATIONS [Debug|Release|...]]
+          [CONFIGURATIONS [Debug|Release|...]
+          [CXX_MODULES_DIRECTORY <directory>]
           [EXPORT_LINK_INTERFACE_LIBRARIES]
           [COMPONENT <component>]
           [EXCLUDE_FROM_ALL])
@@ -836,6 +849,18 @@ library is always installed if the headers and CMake export file are present.
   to an ndk build system complete with transitive dependencies, include flags
   and defines required to use the libraries.
 
+``CXX_MODULES_DIRECTORY``
+
+.. note ::
+
+  Experimental. Gated by ``CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API``
+
+  Specify a subdirectory to store C++ module information for targets in the
+  export set. This directory will be populated with files which add the
+  necessary target property information to the relevant targets. Note that
+  without this information, none of the C++ modules which are part of the
+  targets in the export set will support being imported in consuming targets.
+
 The ``EXPORT`` form is useful to help outside projects use targets built
 and installed by the current project.  For example, the code
 

+ 2 - 2
Help/command/target_sources.rst

@@ -89,7 +89,7 @@ files within those directories. The acceptable types include:
 
   Sources which contain C++ interface module or partition units (i.e., those
   using the ``export`` keyword). This file set type may not have an
-  ``INTERFACE`` scope.
+  ``INTERFACE`` scope except on ``IMPORTED`` targets.
 
 ``CXX_MODULE_HEADER_UNITS``
 
@@ -98,7 +98,7 @@ files within those directories. The acceptable types include:
   Experimental. Gated by ``CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API``
 
   C++ header sources which may be imported by other C++ source code. This file
-  set type may not have an ``INTERFACE`` scope.
+  set type may not have an ``INTERFACE`` scope except on ``IMPORTED`` targets.
 
 The optional default file sets are named after their type. The target may not
 be a custom target or :prop_tgt:`FRAMEWORK` target.

+ 1 - 1
Help/dev/experimental.rst

@@ -18,7 +18,7 @@ C++20 Module APIs
 =================
 
 Variable: ``CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API``
-Value: ``17be90bd-a850-44e0-be50-448de847d652``
+Value: ``3c375311-a3c9-4396-a187-3227ef642046``
 
 In order to support C++20 modules, there are a number of behaviors that have
 CMake APIs to provide the required features to build and export them from a

+ 2 - 0
Source/CMakeLists.txt

@@ -607,6 +607,8 @@ set(SRCS
   cmInstallCommand.h
   cmInstallCommandArguments.cxx
   cmInstallCommandArguments.h
+  cmInstallCxxModuleBmiGenerator.cxx
+  cmInstallCxxModuleBmiGenerator.h
   cmInstallFilesCommand.cxx
   cmInstallFilesCommand.h
   cmInstallProgramsCommand.cxx

+ 1 - 1
Source/cmExperimental.cxx

@@ -27,7 +27,7 @@ struct FeatureData
   bool Warned;
 } LookupTable[] = {
   // CxxModuleCMakeApi
-  { "17be90bd-a850-44e0-be50-448de847d652",
+  { "3c375311-a3c9-4396-a187-3227ef642046",
     "CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API",
     "CMake's C++ module support is experimental. It is meant only for "
     "experimentation and feedback to CMake developers.",

+ 66 - 0
Source/cmExportBuildFileGenerator.cxx

@@ -15,6 +15,7 @@
 
 #include "cmExportSet.h"
 #include "cmFileSet.h"
+#include "cmGeneratedFileStream.h"
 #include "cmGeneratorExpression.h"
 #include "cmGeneratorTarget.h"
 #include "cmGlobalGenerator.h"
@@ -25,6 +26,7 @@
 #include "cmPolicies.h"
 #include "cmStateTypes.h"
 #include "cmStringAlgorithms.h"
+#include "cmSystemTools.h"
 #include "cmTarget.h"
 #include "cmTargetExport.h"
 #include "cmValue.h"
@@ -141,11 +143,18 @@ bool cmExportBuildFileGenerator::GenerateMainFile(std::ostream& os)
     this->GenerateTargetFileSets(gte, os);
   }
 
+  this->GenerateCxxModuleInformation(os);
+
   // Generate import file content for each configuration.
   for (std::string const& c : this->Configurations) {
     this->GenerateImportConfig(os, c);
   }
 
+  // Generate import file content for each configuration.
+  for (std::string const& c : this->Configurations) {
+    this->GenerateImportCxxModuleConfigTargetInclusion(c);
+  }
+
   this->GenerateMissingTargetsCheckCode(os);
 
   return true;
@@ -479,3 +488,60 @@ std::string cmExportBuildFileGenerator::GetFileSetFiles(cmGeneratorTarget* gte,
 
   return cmJoin(resultVector, " ");
 }
+
+std::string cmExportBuildFileGenerator::GetCxxModulesDirectory() const
+{
+  return this->CxxModulesDirectory;
+}
+
+void cmExportBuildFileGenerator::GenerateCxxModuleConfigInformation(
+  std::ostream& os) const
+{
+  const char* opt = "";
+  if (this->Configurations.size() > 1) {
+    // With more than one configuration, each individual file is optional.
+    opt = " OPTIONAL";
+  }
+
+  // Generate import file content for each configuration.
+  for (std::string c : this->Configurations) {
+    if (c.empty()) {
+      c = "noconfig";
+    }
+    os << "include(\"${CMAKE_CURRENT_LIST_DIR}/cxx-modules-" << c << ".cmake\""
+       << opt << ")\n";
+  }
+}
+
+bool cmExportBuildFileGenerator::GenerateImportCxxModuleConfigTargetInclusion(
+  std::string config) const
+{
+  auto cxx_modules_dirname = this->GetCxxModulesDirectory();
+  if (cxx_modules_dirname.empty()) {
+    return true;
+  }
+
+  if (config.empty()) {
+    config = "noconfig";
+  }
+
+  std::string fileName = cmStrCat(this->FileDir, '/', cxx_modules_dirname,
+                                  "/cxx-modules-", config, ".cmake");
+
+  cmGeneratedFileStream os(fileName, true);
+  if (!os) {
+    std::string se = cmSystemTools::GetLastSystemError();
+    std::ostringstream e;
+    e << "cannot write to file \"" << fileName << "\": " << se;
+    cmSystemTools::Error(e.str());
+    return false;
+  }
+  os.SetCopyIfDifferent(true);
+
+  for (auto const* tgt : this->ExportedTargets) {
+    os << "include(\"${CMAKE_CURRENT_LIST_DIR}/target-" << tgt->GetExportName()
+       << '-' << config << ".cmake\")\n";
+  }
+
+  return true;
+}

+ 16 - 0
Source/cmExportBuildFileGenerator.h

@@ -47,6 +47,16 @@ public:
   }
   void SetExportSet(cmExportSet*);
 
+  /** Set the name of the C++ module directory.  */
+  void SetCxxModuleDirectory(std::string cxx_module_dir)
+  {
+    this->CxxModulesDirectory = std::move(cxx_module_dir);
+  }
+  const std::string& GetCxxModuleDirectory() const
+  {
+    return this->CxxModulesDirectory;
+  }
+
   /** Set whether to append generated code to the output file.  */
   void SetAppendMode(bool append) { this->AppendMode = append; }
 
@@ -81,6 +91,10 @@ protected:
   std::string GetFileSetFiles(cmGeneratorTarget* gte, cmFileSet* fileSet,
                               cmTargetExport* te) override;
 
+  std::string GetCxxModulesDirectory() const override;
+  void GenerateCxxModuleConfigInformation(std::ostream&) const override;
+  bool GenerateImportCxxModuleConfigTargetInclusion(std::string) const;
+
   std::pair<std::vector<std::string>, std::string> FindBuildExportInfo(
     cmGlobalGenerator* gg, const std::string& name);
 
@@ -88,4 +102,6 @@ protected:
   cmExportSet* ExportSet;
   std::vector<cmGeneratorTarget*> Exports;
   cmLocalGenerator* LG;
+  // The directory for C++ module information.
+  std::string CxxModulesDirectory;
 };

+ 9 - 0
Source/cmExportCommand.cxx

@@ -14,6 +14,7 @@
 
 #include "cmArgumentParser.h"
 #include "cmExecutionStatus.h"
+#include "cmExperimental.h"
 #include "cmExportBuildAndroidMKGenerator.h"
 #include "cmExportBuildFileGenerator.h"
 #include "cmExportSet.h"
@@ -61,6 +62,7 @@ bool cmExportCommand(std::vector<std::string> const& args,
     std::string Namespace;
     std::string Filename;
     std::string AndroidMKFile;
+    std::string CxxModulesDirectory;
     bool Append = false;
     bool ExportOld = false;
   };
@@ -69,6 +71,12 @@ bool cmExportCommand(std::vector<std::string> const& args,
                   .Bind("NAMESPACE"_s, &Arguments::Namespace)
                   .Bind("FILE"_s, &Arguments::Filename);
 
+  bool const supportCxx20FileSetTypes = cmExperimental::HasSupportEnabled(
+    status.GetMakefile(), cmExperimental::Feature::CxxModuleCMakeApi);
+  if (supportCxx20FileSetTypes) {
+    parser.Bind("CXX_MODULES_DIRECTORY"_s, &Arguments::CxxModulesDirectory);
+  }
+
   if (args[0] == "EXPORT") {
     parser.Bind("EXPORT"_s, &Arguments::ExportSetName);
   } else {
@@ -211,6 +219,7 @@ bool cmExportCommand(std::vector<std::string> const& args,
   }
   ebfg->SetExportFile(fname.c_str());
   ebfg->SetNamespace(arguments.Namespace);
+  ebfg->SetCxxModuleDirectory(arguments.CxxModulesDirectory);
   ebfg->SetAppendMode(arguments.Append);
   if (exportSet != nullptr) {
     ebfg->SetExportSet(exportSet);

+ 25 - 0
Source/cmExportFileGenerator.cxx

@@ -1305,3 +1305,28 @@ void cmExportFileGenerator::GenerateTargetFileSets(cmGeneratorTarget* gte,
     os << "  )\nendif()\n\n";
   }
 }
+
+void cmExportFileGenerator::GenerateCxxModuleInformation(std::ostream& os)
+{
+  auto const cxx_module_dirname = this->GetCxxModulesDirectory();
+  if (cxx_module_dirname.empty()) {
+    return;
+  }
+
+  // Write the include.
+  os << "# Include C++ module properties\n"
+     << "include(\"${CMAKE_CURRENT_LIST_DIR}/" << cxx_module_dirname
+     << "/cxx-modules.cmake\")\n\n";
+
+  // Get the path to the file we're going to write.
+  std::string path = this->MainImportFile;
+  path = cmSystemTools::GetFilenamePath(path);
+  auto trampoline_path =
+    cmStrCat(path, '/', cxx_module_dirname, "/cxx-modules.cmake");
+
+  // Include all configuration-specific include files.
+  cmGeneratedFileStream ap(trampoline_path, true);
+  ap.SetCopyIfDifferent(true);
+
+  this->GenerateCxxModuleConfigInformation(ap);
+}

+ 5 - 0
Source/cmExportFileGenerator.h

@@ -182,6 +182,8 @@ protected:
   void GenerateTargetFileSets(cmGeneratorTarget* gte, std::ostream& os,
                               cmTargetExport* te = nullptr);
 
+  void GenerateCxxModuleInformation(std::ostream& os);
+
   virtual std::string GetFileSetDirectories(cmGeneratorTarget* gte,
                                             cmFileSet* fileSet,
                                             cmTargetExport* te) = 0;
@@ -226,4 +228,7 @@ private:
 
   virtual std::string InstallNameDir(cmGeneratorTarget const* target,
                                      const std::string& config) = 0;
+
+  virtual std::string GetCxxModulesDirectory() const = 0;
+  virtual void GenerateCxxModuleConfigInformation(std::ostream& os) const = 0;
 };

+ 1 - 2
Source/cmExportInstallAndroidMKGenerator.cxx

@@ -35,8 +35,7 @@ void cmExportInstallAndroidMKGenerator::GenerateImportHeaderCode(
   for (size_t n = 0; n < numDotDot; n++) {
     path += "/..";
   }
-  os << "_IMPORT_PREFIX := "
-     << "$(LOCAL_PATH)" << path << "\n\n";
+  os << "_IMPORT_PREFIX := $(LOCAL_PATH)" << path << "\n\n";
   for (std::unique_ptr<cmTargetExport> const& te :
        this->IEGen->GetExportSet()->GetTargetExports()) {
     // Collect import properties for this target.

+ 73 - 1
Source/cmExportInstallFileGenerator.cxx

@@ -166,10 +166,20 @@ bool cmExportInstallFileGenerator::GenerateMainFile(std::ostream& os)
 
   this->LoadConfigFiles(os);
 
+  bool result = true;
+
+  this->GenerateCxxModuleInformation(os);
+  if (requiresConfigFiles) {
+    for (std::string const& c : this->Configurations) {
+      if (!this->GenerateImportCxxModuleConfigTargetInclusion(c)) {
+        result = false;
+      }
+    }
+  }
+
   this->CleanupTemporaryVariables(os);
   this->GenerateImportedFileCheckLoop(os);
 
-  bool result = true;
   // Generate an import file for each configuration.
   // Don't do this if we only export INTERFACE_LIBRARY targets.
   if (requiresConfigFiles) {
@@ -669,3 +679,65 @@ std::string cmExportInstallFileGenerator::GetFileSetFiles(
 
   return cmJoin(resultVector, " ");
 }
+
+std::string cmExportInstallFileGenerator::GetCxxModulesDirectory() const
+{
+  return IEGen->GetCxxModuleDirectory();
+}
+
+void cmExportInstallFileGenerator::GenerateCxxModuleConfigInformation(
+  std::ostream& os) const
+{
+  // Now load per-configuration properties for them.
+  /* clang-format off */
+  os << "# Load information for each installed configuration.\n"
+        "file(GLOB _cmake_cxx_module_includes \"${CMAKE_CURRENT_LIST_DIR}/cxx-modules-*.cmake\")\n"
+        "foreach(_cmake_cxx_module_include IN LISTS _cmake_cxx_module_includes)\n"
+        "  include(\"${_cmake_cxx_module_include}\")\n"
+        "endforeach()\n"
+        "unset(_cmake_cxx_module_include)\n"
+        "unset(_cmake_cxx_module_includes)\n";
+  /* clang-format on */
+}
+
+bool cmExportInstallFileGenerator::
+  GenerateImportCxxModuleConfigTargetInclusion(std::string const& config)
+{
+  auto cxx_modules_dirname = this->GetCxxModulesDirectory();
+  if (cxx_modules_dirname.empty()) {
+    return true;
+  }
+
+  std::string filename_config = config;
+  if (filename_config.empty()) {
+    filename_config = "noconfig";
+  }
+
+  std::string const dest =
+    cmStrCat(this->FileDir, '/', cxx_modules_dirname, '/');
+  std::string fileName =
+    cmStrCat(dest, "cxx-modules-", filename_config, ".cmake");
+
+  cmGeneratedFileStream os(fileName, true);
+  if (!os) {
+    std::string se = cmSystemTools::GetLastSystemError();
+    std::ostringstream e;
+    e << "cannot write to file \"" << fileName << "\": " << se;
+    cmSystemTools::Error(e.str());
+    return false;
+  }
+  os.SetCopyIfDifferent(true);
+
+  // Record this per-config import file.
+  this->ConfigCxxModuleFiles[config] = fileName;
+
+  auto& prop_files = this->ConfigCxxModuleTargetFiles[config];
+  for (auto const* tgt : this->ExportedTargets) {
+    auto prop_filename = cmStrCat("target-", tgt->GetExportName(), '-',
+                                  filename_config, ".cmake");
+    prop_files.emplace_back(cmStrCat(dest, prop_filename));
+    os << "include(\"${CMAKE_CURRENT_LIST_DIR}/" << prop_filename << "\")\n";
+  }
+
+  return true;
+}

+ 25 - 0
Source/cmExportInstallFileGenerator.h

@@ -50,6 +50,23 @@ public:
     return this->ConfigImportFiles;
   }
 
+  /** Get the per-config C++ module file generated for each configuration.
+      This maps from the configuration name to the file temporary location
+      for installation.  */
+  std::map<std::string, std::string> const& GetConfigCxxModuleFiles()
+  {
+    return this->ConfigCxxModuleFiles;
+  }
+
+  /** Get the per-config C++ module file generated for each configuration.
+      This maps from the configuration name to the file temporary location
+      for installation for each target in the export set.  */
+  std::map<std::string, std::vector<std::string>> const&
+  GetConfigCxxModuleTargetFiles()
+  {
+    return this->ConfigCxxModuleTargetFiles;
+  }
+
   /** Compute the globbing expression used to load per-config import
       files from the main file.  */
   std::string GetConfigImportFileGlob();
@@ -100,8 +117,16 @@ protected:
   std::string GetFileSetFiles(cmGeneratorTarget* gte, cmFileSet* fileSet,
                               cmTargetExport* te) override;
 
+  std::string GetCxxModulesDirectory() const override;
+  void GenerateCxxModuleConfigInformation(std::ostream&) const override;
+  bool GenerateImportCxxModuleConfigTargetInclusion(std::string const&);
+
   cmInstallExportGenerator* IEGen;
 
   // The import file generated for each configuration.
   std::map<std::string, std::string> ConfigImportFiles;
+  // The C++ module property file generated for each configuration.
+  std::map<std::string, std::string> ConfigCxxModuleFiles;
+  // The C++ module property target files generated for each configuration.
+  std::map<std::string, std::vector<std::string>> ConfigCxxModuleTargetFiles;
 };

+ 3 - 0
Source/cmExportTryCompileFileGenerator.h

@@ -55,6 +55,9 @@ protected:
   std::string GetFileSetFiles(cmGeneratorTarget* target, cmFileSet* fileSet,
                               cmTargetExport* te) override;
 
+  std::string GetCxxModulesDirectory() const override { return {}; }
+  void GenerateCxxModuleConfigInformation(std::ostream&) const override {}
+
 private:
   std::string FindTargets(const std::string& prop,
                           const cmGeneratorTarget* tgt,

+ 16 - 0
Source/cmFileAPICodemodel.cxx

@@ -27,6 +27,7 @@
 #include "cmGeneratorExpression.h"
 #include "cmGeneratorTarget.h"
 #include "cmGlobalGenerator.h"
+#include "cmInstallCxxModuleBmiGenerator.h"
 #include "cmInstallDirectoryGenerator.h"
 #include "cmInstallExportGenerator.h"
 #include "cmInstallFileSetGenerator.h"
@@ -1092,6 +1093,21 @@ Json::Value DirectoryObject::DumpInstaller(cmInstallGenerator* gen)
     if (installFileSet->GetOptional()) {
       installer["isOptional"] = true;
     }
+  } else if (auto* cxxModuleBmi =
+               dynamic_cast<cmInstallCxxModuleBmiGenerator*>(gen)) {
+    installer["type"] = "cxxModuleBmi";
+    installer["destination"] = cxxModuleBmi->GetDestination(this->Config);
+
+    auto const* target = cxxModuleBmi->GetTarget();
+    installer["cxxModuleBmiTarget"] = Json::objectValue;
+    installer["cxxModuleBmiTarget"]["id"] = TargetId(target, this->TopBuild);
+    installer["cxxModuleBmiTarget"]["index"] = this->TargetIndexMap[target];
+
+    // FIXME: Parse FilePermissions.
+    // FIXME: Parse MessageLevel.
+    if (cxxModuleBmi->GetOptional()) {
+      installer["isOptional"] = true;
+    }
   }
 
   // Add fields common to all install generators.

+ 371 - 3
Source/cmGlobalNinjaGenerator.cxx

@@ -15,6 +15,7 @@
 #include <cm/string_view>
 #include <cmext/algorithm>
 #include <cmext/memory>
+#include <cmext/string_view>
 
 #include <cm3p/json/reader.h>
 #include <cm3p/json/value.h>
@@ -24,6 +25,7 @@
 
 #include "cmCxxModuleMapper.h"
 #include "cmDocumentationEntry.h"
+#include "cmFileSet.h"
 #include "cmFortranParser.h"
 #include "cmGeneratedFileStream.h"
 #include "cmGeneratorExpressionEvaluationFile.h"
@@ -2482,13 +2484,53 @@ cm::optional<cmSourceInfo> cmcmd_cmake_ninja_depends_fortran(
 }
 }
 
+struct CxxModuleFileSet
+{
+  std::string Name;
+  std::string RelativeDirectory;
+  std::string SourcePath;
+  std::string Type;
+  cmFileSetVisibility Visibility;
+  cm::optional<std::string> Destination;
+};
+
+struct CxxModuleBmiInstall
+{
+  std::string Component;
+  std::string Destination;
+  bool ExcludeFromAll;
+  bool Optional;
+  std::string Permissions;
+  std::string MessageLevel;
+  std::string ScriptLocation;
+};
+
+struct CxxModuleExport
+{
+  std::string Name;
+  std::string Destination;
+  std::string Prefix;
+  std::string CxxModuleInfoDir;
+  std::string Namespace;
+  bool Install;
+};
+
+struct cmGlobalNinjaGenerator::CxxModuleExportInfo
+{
+  std::map<std::string, CxxModuleFileSet> ObjectToFileSet;
+  cm::optional<CxxModuleBmiInstall> BmiInstallation;
+  std::vector<CxxModuleExport> Exports;
+  std::string Config;
+};
+
 bool cmGlobalNinjaGenerator::WriteDyndepFile(
   std::string const& dir_top_src, std::string const& dir_top_bld,
   std::string const& dir_cur_src, std::string const& dir_cur_bld,
   std::string const& arg_dd, std::vector<std::string> const& arg_ddis,
   std::string const& module_dir,
   std::vector<std::string> const& linked_target_dirs,
-  std::string const& arg_lang, std::string const& arg_modmapfmt)
+  std::string const& arg_lang, std::string const& arg_modmapfmt,
+  CxxModuleExportInfo const& export_info)
 {
   // Setup path conversions.
   {
@@ -2636,7 +2678,279 @@ bool cmGlobalNinjaGenerator::WriteDyndepFile(
   cmGeneratedFileStream tmf(target_mods_file);
   tmf << tm;
 
-  return true;
+  bool result = true;
+
+  // Fortran doesn't support any of the file-set or BMI installation considered
+  // below.
+  if (arg_lang != "Fortran"_s) {
+    // Prepare the export information blocks.
+    std::string const config_upper =
+      cmSystemTools::UpperCase(export_info.Config);
+    std::vector<std::pair<std::unique_ptr<cmGeneratedFileStream>,
+                          CxxModuleExport const*>>
+      exports;
+    for (auto const& exp : export_info.Exports) {
+      std::unique_ptr<cmGeneratedFileStream> properties;
+
+      std::string const export_dir =
+        cmStrCat(exp.Prefix, '/', exp.CxxModuleInfoDir, '/');
+      std::string const property_file_path = cmStrCat(
+        export_dir, "target-", exp.Name, '-', export_info.Config, ".cmake");
+      properties = cm::make_unique<cmGeneratedFileStream>(property_file_path);
+
+      // Set up the preamble.
+      *properties << "set_property(TARGET \"" << exp.Namespace << exp.Name
+                  << "\"\n"
+                  << "  PROPERTY IMPORTED_CXX_MODULES_" << config_upper
+                  << '\n';
+
+      exports.emplace_back(std::move(properties), &exp);
+    }
+
+    std::unique_ptr<cmGeneratedFileStream> bmi_install_script;
+    if (export_info.BmiInstallation) {
+      bmi_install_script = cm::make_unique<cmGeneratedFileStream>(
+        export_info.BmiInstallation->ScriptLocation);
+    }
+
+    auto cmEscape = [](cm::string_view str) {
+      return cmOutputConverter::EscapeForCMake(
+        str, cmOutputConverter::WrapQuotes::NoWrap);
+    };
+    auto install_destination =
+      [&cmEscape](std::string const& dest) -> std::pair<bool, std::string> {
+      if (cmSystemTools::FileIsFullPath(dest)) {
+        return std::make_pair(true, cmEscape(dest));
+      }
+      return std::make_pair(false,
+                            cmStrCat("${_IMPORT_PREFIX}/", cmEscape(dest)));
+    };
+
+    // public/private requirement tracking.
+    std::set<std::string> private_modules;
+    std::map<std::string, std::set<std::string>> public_source_requires;
+
+    for (cmScanDepInfo const& object : objects) {
+      // Convert to forward slashes.
+      auto output_path = object.PrimaryOutput;
+#  ifdef _WIN32
+      cmSystemTools::ConvertToUnixSlashes(output_path);
+#  endif
+      // Find the fileset for this object.
+      auto fileset_info_itr = export_info.ObjectToFileSet.find(output_path);
+      bool const has_provides = !object.Provides.empty();
+      if (fileset_info_itr == export_info.ObjectToFileSet.end()) {
+        // If it provides anything, it should have a `CXX_MODULES` or
+        // `CXX_MODULE_INTERNAL_PARTITIONS` type and be present.
+        if (has_provides) {
+          // Take the first module provided to provide context.
+          auto const& provides = object.Provides[0];
+          char const* ok_types = "`CXX_MODULES`";
+          if (provides.LogicalName.find(':') != std::string::npos) {
+            ok_types = "`CXX_MODULES` (or `CXX_MODULE_INTERNAL_PARTITIONS` if "
+                       "it is not `export`ed)";
+          }
+          cmSystemTools::Error(
+            cmStrCat("Output ", object.PrimaryOutput, " provides the `",
+                     provides.LogicalName,
+                     "` module but it is not found in a `FILE_SET` of type ",
+                     ok_types));
+          result = false;
+        }
+
+        // This object file does not provide anything, so nothing more needs to
+        // be done.
+        continue;
+      }
+
+      auto const& file_set = fileset_info_itr->second;
+
+      // Verify the fileset type for the object.
+      if (file_set.Type == "CXX_MODULES"_s) {
+        if (!has_provides) {
+          cmSystemTools::Error(cmStrCat(
+            "Output ", object.PrimaryOutput,
+            " is of type `CXX_MODULES` but does not provide a module"));
+          result = false;
+          continue;
+        }
+      } else if (file_set.Type == "CXX_MODULE_INTERNAL_PARTITIONS"_s) {
+        if (!has_provides) {
+          cmSystemTools::Error(cmStrCat(
+            "Source ", file_set.SourcePath,
+            " is of type `CXX_MODULE_INTERNAL_PARTITIONS` but does not "
+            "provide a module"));
+          result = false;
+          continue;
+        }
+        auto const& provides = object.Provides[0];
+        if (provides.LogicalName.find(':') == std::string::npos) {
+          cmSystemTools::Error(cmStrCat(
+            "Source ", file_set.SourcePath,
+            " is of type `CXX_MODULE_INTERNAL_PARTITIONS` but does not "
+            "provide a module partition"));
+          result = false;
+          continue;
+        }
+      } else if (file_set.Type == "CXX_MODULE_HEADERS"_s) {
+        // TODO.
+      } else {
+        if (has_provides) {
+          auto const& provides = object.Provides[0];
+          char const* ok_types = "`CXX_MODULES`";
+          if (provides.LogicalName.find(':') != std::string::npos) {
+            ok_types = "`CXX_MODULES` (or `CXX_MODULE_INTERNAL_PARTITIONS` if "
+                       "it is not `export`ed)";
+          }
+          cmSystemTools::Error(cmStrCat(
+            "Source ", file_set.SourcePath, " provides the `",
+            provides.LogicalName, "` C++ module but is of type `",
+            file_set.Type, "` module but must be of type ", ok_types));
+          result = false;
+        }
+
+        // Not a C++ module; ignore.
+        continue;
+      }
+
+      if (!cmFileSetVisibilityIsForInterface(file_set.Visibility)) {
+        // Nothing needs to be conveyed about non-`PUBLIC` modules.
+        for (auto const& p : object.Provides) {
+          private_modules.insert(p.LogicalName);
+        }
+        continue;
+      }
+
+      // The module is public. Record what it directly requires.
+      {
+        auto& reqs = public_source_requires[file_set.SourcePath];
+        for (auto const& r : object.Requires) {
+          reqs.insert(r.LogicalName);
+        }
+      }
+
+      // Write out properties and install rules for any exports.
+      for (auto const& p : object.Provides) {
+        bool bmi_dest_is_abs = false;
+        std::string bmi_destination;
+        if (export_info.BmiInstallation) {
+          auto dest =
+            install_destination(export_info.BmiInstallation->Destination);
+          bmi_dest_is_abs = dest.first;
+          bmi_destination = cmStrCat(dest.second, '/');
+        }
+
+        std::string install_bmi_path;
+        std::string build_bmi_path;
+        auto m = mod_files.find(p.LogicalName);
+        if (m != mod_files.end()) {
+          install_bmi_path =
+            cmStrCat(bmi_destination,
+                     cmEscape(cmSystemTools::GetFilenameName(m->second)));
+          build_bmi_path = cmEscape(m->second);
+        }
+
+        for (auto const& exp : exports) {
+          std::string iface_source;
+          if (exp.second->Install && file_set.Destination) {
+            auto dest = install_destination(*file_set.Destination);
+            iface_source = cmStrCat(
+              dest.second, '/', cmEscape(file_set.RelativeDirectory),
+              cmEscape(cmSystemTools::GetFilenameName(file_set.SourcePath)));
+          } else {
+            iface_source = cmEscape(file_set.SourcePath);
+          }
+
+          std::string bmi_path;
+          if (exp.second->Install && export_info.BmiInstallation) {
+            bmi_path = install_bmi_path;
+          } else if (!exp.second->Install) {
+            bmi_path = build_bmi_path;
+          }
+
+          if (iface_source.empty()) {
+            // No destination for the C++ module source; ignore this property
+            // value.
+            continue;
+          }
+
+          *exp.first << "    \"" << cmEscape(p.LogicalName) << '='
+                     << iface_source;
+          if (!bmi_path.empty()) {
+            *exp.first << ',' << bmi_path;
+          }
+          *exp.first << "\"\n";
+        }
+
+        if (bmi_install_script) {
+          auto const& bmi_install = *export_info.BmiInstallation;
+
+          *bmi_install_script << "if (CMAKE_INSTALL_COMPONENT STREQUAL \""
+                              << cmEscape(bmi_install.Component) << '\"';
+          if (!bmi_install.ExcludeFromAll) {
+            *bmi_install_script << " OR NOT CMAKE_INSTALL_COMPONENT";
+          }
+          *bmi_install_script << ")\n";
+          *bmi_install_script << "  file(INSTALL\n"
+                                 "    DESTINATION \"";
+          if (!bmi_dest_is_abs) {
+            *bmi_install_script << "${CMAKE_INSTALL_PREFIX}/";
+          }
+          *bmi_install_script << cmEscape(bmi_install.Destination)
+                              << "\"\n"
+                                 "    TYPE FILE\n";
+          if (bmi_install.Optional) {
+            *bmi_install_script << "    OPTIONAL\n";
+          }
+          if (!bmi_install.MessageLevel.empty()) {
+            *bmi_install_script << "    " << bmi_install.MessageLevel << "\n";
+          }
+          if (!bmi_install.Permissions.empty()) {
+            *bmi_install_script << "    PERMISSIONS" << bmi_install.Permissions
+                                << "\n";
+          }
+          *bmi_install_script << "    FILES \"" << m->second << "\")\n";
+          if (bmi_dest_is_abs) {
+            *bmi_install_script
+              << "  list(APPEND CMAKE_ABSOLUTE_DESTINATION_FILES\n"
+                 "    \""
+              << cmEscape(cmSystemTools::GetFilenameName(m->second))
+              << "\")\n"
+                 "  if (CMAKE_WARN_ON_ABSOLUTE_INSTALL_DESTINATION)\n"
+                 "    message(WARNING\n"
+                 "      \"ABSOLUTE path INSTALL DESTINATION : "
+                 "${CMAKE_ABSOLUTE_DESTINATION_FILES}\")\n"
+                 "  endif ()\n"
+                 "  if (CMAKE_ERROR_ON_ABSOLUTE_INSTALL_DESTINATION)\n"
+                 "    message(FATAL_ERROR\n"
+                 "      \"ABSOLUTE path INSTALL DESTINATION forbidden (by "
+                 "caller): ${CMAKE_ABSOLUTE_DESTINATION_FILES}\")\n"
+                 "  endif ()\n";
+          }
+          *bmi_install_script << "endif ()\n";
+        }
+      }
+    }
+
+    // Add trailing parenthesis for the `set_property` call.
+    for (auto const& exp : exports) {
+      *exp.first << ")\n";
+    }
+
+    // Check that public sources only require public modules.
+    for (auto const& pub_reqs : public_source_requires) {
+      for (auto const& req : pub_reqs.second) {
+        if (private_modules.count(req)) {
+          cmSystemTools::Error(cmStrCat(
+            "Public C++ module source `", pub_reqs.first, "` requires the `",
+            req, "` C++ module which is provided by a private source"));
+          result = false;
+        }
+      }
+    }
+  }
+
+  return result;
 }
 
 int cmcmd_cmake_ninja_dyndep(std::vector<std::string>::const_iterator argBeg,
@@ -2710,6 +3024,59 @@ int cmcmd_cmake_ninja_dyndep(std::vector<std::string>::const_iterator argBeg,
     }
   }
 
+  cmGlobalNinjaGenerator::CxxModuleExportInfo export_info;
+  export_info.Config = tdi["config"].asString();
+  if (export_info.Config.empty()) {
+    export_info.Config = "noconfig";
+  }
+  Json::Value const& tdi_exports = tdi["exports"];
+  if (tdi_exports.isArray()) {
+    for (auto const& tdi_export : tdi_exports) {
+      CxxModuleExport exp;
+      exp.Install = tdi_export["install"].asBool();
+      exp.Name = tdi_export["export-name"].asString();
+      exp.Destination = tdi_export["destination"].asString();
+      exp.Prefix = tdi_export["export-prefix"].asString();
+      exp.CxxModuleInfoDir = tdi_export["cxx-module-info-dir"].asString();
+      exp.Namespace = tdi_export["namespace"].asString();
+
+      export_info.Exports.push_back(exp);
+    }
+  }
+  auto const& bmi_installation = tdi["bmi-installation"];
+  if (bmi_installation.isObject()) {
+    CxxModuleBmiInstall bmi_install;
+
+    bmi_install.Component = bmi_installation["component"].asString();
+    bmi_install.Destination = bmi_installation["destination"].asString();
+    bmi_install.ExcludeFromAll = bmi_installation["exclude-from-all"].asBool();
+    bmi_install.Optional = bmi_installation["optional"].asBool();
+    bmi_install.Permissions = bmi_installation["permissions"].asString();
+    bmi_install.MessageLevel = bmi_installation["message-level"].asString();
+    bmi_install.ScriptLocation =
+      bmi_installation["script-location"].asString();
+
+    export_info.BmiInstallation = bmi_install;
+  }
+  Json::Value const& tdi_cxx_modules = tdi["cxx-modules"];
+  if (tdi_cxx_modules.isObject()) {
+    for (auto i = tdi_cxx_modules.begin(); i != tdi_cxx_modules.end(); ++i) {
+      CxxModuleFileSet& fsi = export_info.ObjectToFileSet[i.key().asString()];
+      auto const& tdi_cxx_module_info = *i;
+      fsi.Name = tdi_cxx_module_info["name"].asString();
+      fsi.RelativeDirectory =
+        tdi_cxx_module_info["relative-directory"].asString();
+      fsi.SourcePath = tdi_cxx_module_info["source"].asString();
+      fsi.Type = tdi_cxx_module_info["type"].asString();
+      fsi.Visibility = cmFileSetVisibilityFromName(
+        tdi_cxx_module_info["visibility"].asString(), nullptr);
+      auto const& tdi_fs_dest = tdi_cxx_module_info["destination"];
+      if (tdi_fs_dest.isString()) {
+        fsi.Destination = tdi_fs_dest.asString();
+      }
+    }
+  }
+
   cmake cm(cmake::RoleInternal, cmState::Unknown);
   cm.SetHomeDirectory(dir_top_src);
   cm.SetHomeOutputDirectory(dir_top_bld);
@@ -2717,7 +3084,8 @@ int cmcmd_cmake_ninja_dyndep(std::vector<std::string>::const_iterator argBeg,
   if (!ggd ||
       !cm::static_reference_cast<cmGlobalNinjaGenerator>(ggd).WriteDyndepFile(
         dir_top_src, dir_top_bld, dir_cur_src, dir_cur_bld, arg_dd, arg_ddis,
-        module_dir, linked_target_dirs, arg_lang, arg_modmapfmt)) {
+        module_dir, linked_target_dirs, arg_lang, arg_modmapfmt,
+        export_info)) {
     return 1;
   }
   return 0;

+ 3 - 1
Source/cmGlobalNinjaGenerator.h

@@ -417,13 +417,15 @@ public:
   bool HasOutputPathPrefix() const { return !this->OutputPathPrefix.empty(); }
   void StripNinjaOutputPathPrefixAsSuffix(std::string& path);
 
+  struct CxxModuleExportInfo;
   bool WriteDyndepFile(
     std::string const& dir_top_src, std::string const& dir_top_bld,
     std::string const& dir_cur_src, std::string const& dir_cur_bld,
     std::string const& arg_dd, std::vector<std::string> const& arg_ddis,
     std::string const& module_dir,
     std::vector<std::string> const& linked_target_dirs,
-    std::string const& arg_lang, std::string const& arg_modmapfmt);
+    std::string const& arg_lang, std::string const& arg_modmapfmt,
+    CxxModuleExportInfo const& export_info);
 
   virtual std::string BuildAlias(const std::string& alias,
                                  const std::string& /*config*/) const

+ 70 - 8
Source/cmInstallCommand.cxx

@@ -20,11 +20,13 @@
 
 #include "cmArgumentParser.h"
 #include "cmExecutionStatus.h"
+#include "cmExperimental.h"
 #include "cmExportSet.h"
 #include "cmFileSet.h"
 #include "cmGeneratorExpression.h"
 #include "cmGlobalGenerator.h"
 #include "cmInstallCommandArguments.h"
+#include "cmInstallCxxModuleBmiGenerator.h"
 #include "cmInstallDirectoryGenerator.h"
 #include "cmInstallExportGenerator.h"
 #include "cmInstallFileSetGenerator.h"
@@ -110,6 +112,8 @@ public:
     const cmInstallCommandArguments* args) const;
   std::string GetLibraryDestination(
     const cmInstallCommandArguments* args) const;
+  std::string GetCxxModulesBmiDestination(
+    const cmInstallCommandArguments* args) const;
   std::string GetIncludeDestination(
     const cmInstallCommandArguments* args) const;
   std::string GetSysconfDestination(
@@ -413,6 +417,7 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
     std::vector<std::string> PublicHeader;
     std::vector<std::string> Resource;
     std::vector<std::vector<std::string>> FileSets;
+    std::vector<std::string> CxxModulesBmi;
   };
 
   static auto const argHelper =
@@ -427,7 +432,8 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
       .Bind("PRIVATE_HEADER"_s, &ArgVectors::PrivateHeader)
       .Bind("PUBLIC_HEADER"_s, &ArgVectors::PublicHeader)
       .Bind("RESOURCE"_s, &ArgVectors::Resource)
-      .Bind("FILE_SET"_s, &ArgVectors::FileSets);
+      .Bind("FILE_SET"_s, &ArgVectors::FileSets)
+      .Bind("CXX_MODULES_BMI"_s, &ArgVectors::CxxModulesBmi);
 
   std::vector<std::string> genericArgVector;
   ArgVectors const argVectors = argHelper.Parse(args, &genericArgVector);
@@ -466,6 +472,7 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
   cmInstallCommandIncludesArgument includesArgs;
   std::vector<cmInstallCommandFileSetArguments> fileSetArgs(
     argVectors.FileSets.size(), { helper.DefaultComponentName });
+  cmInstallCommandArguments cxxModuleBmiArgs(helper.DefaultComponentName);
 
   // now parse the args for specific parts of the target (e.g. LIBRARY,
   // RUNTIME, ARCHIVE etc.
@@ -489,6 +496,15 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
     fileSetArgs[i] = std::move(fileSetArg);
   }
 
+  bool const supportCxx20FileSetTypes = cmExperimental::HasSupportEnabled(
+    *helper.Makefile, cmExperimental::Feature::CxxModuleCMakeApi);
+  if (!supportCxx20FileSetTypes) {
+    std::copy(argVectors.CxxModulesBmi.begin(), argVectors.CxxModulesBmi.end(),
+              std::back_inserter(unknownArgs));
+  } else {
+    cxxModuleBmiArgs.Parse(argVectors.CxxModulesBmi, &unknownArgs);
+  }
+
   if (!unknownArgs.empty()) {
     // Unknown argument.
     status.SetError(
@@ -509,6 +525,7 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
   for (auto& fileSetArg : fileSetArgs) {
     fileSetArg.SetGenericArguments(&genericArgs);
   }
+  cxxModuleBmiArgs.SetGenericArguments(&genericArgs);
 
   success = success && archiveArgs.Finalize();
   success = success && libraryArgs.Finalize();
@@ -522,6 +539,9 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
   for (auto& fileSetArg : fileSetArgs) {
     success = success && fileSetArg.Finalize();
   }
+  if (supportCxx20FileSetTypes) {
+    success = success && cxxModuleBmiArgs.Finalize();
+  }
 
   if (!success) {
     return false;
@@ -535,7 +555,8 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
       publicHeaderArgs.GetNamelinkOnly() || resourceArgs.GetNamelinkOnly() ||
       std::any_of(fileSetArgs.begin(), fileSetArgs.end(),
                   [](const cmInstallCommandFileSetArguments& fileSetArg)
-                    -> bool { return fileSetArg.GetNamelinkOnly(); })) {
+                    -> bool { return fileSetArg.GetNamelinkOnly(); }) ||
+      cxxModuleBmiArgs.GetNamelinkOnly()) {
     status.SetError(
       "TARGETS given NAMELINK_ONLY option not in LIBRARY group.  "
       "The NAMELINK_ONLY option may be specified only following LIBRARY.");
@@ -547,7 +568,8 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
       publicHeaderArgs.GetNamelinkSkip() || resourceArgs.GetNamelinkSkip() ||
       std::any_of(fileSetArgs.begin(), fileSetArgs.end(),
                   [](const cmInstallCommandFileSetArguments& fileSetArg)
-                    -> bool { return fileSetArg.GetNamelinkSkip(); })) {
+                    -> bool { return fileSetArg.GetNamelinkSkip(); }) ||
+      cxxModuleBmiArgs.GetNamelinkSkip()) {
     status.SetError(
       "TARGETS given NAMELINK_SKIP option not in LIBRARY group.  "
       "The NAMELINK_SKIP option may be specified only following LIBRARY.");
@@ -563,7 +585,8 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
       resourceArgs.HasNamelinkComponent() ||
       std::any_of(fileSetArgs.begin(), fileSetArgs.end(),
                   [](const cmInstallCommandFileSetArguments& fileSetArg)
-                    -> bool { return fileSetArg.HasNamelinkComponent(); })) {
+                    -> bool { return fileSetArg.HasNamelinkComponent(); }) ||
+      cxxModuleBmiArgs.HasNamelinkComponent()) {
     status.SetError(
       "TARGETS given NAMELINK_COMPONENT option not in LIBRARY group.  "
       "The NAMELINK_COMPONENT option may be specified only following "
@@ -582,7 +605,8 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
       !publicHeaderArgs.GetType().empty() || !resourceArgs.GetType().empty() ||
       std::any_of(fileSetArgs.begin(), fileSetArgs.end(),
                   [](const cmInstallCommandFileSetArguments& fileSetArg)
-                    -> bool { return !fileSetArg.GetType().empty(); })) {
+                    -> bool { return !fileSetArg.GetType().empty(); }) ||
+      !cxxModuleBmiArgs.GetType().empty()) {
     status.SetError(
       "TARGETS given TYPE option. The TYPE option may only be specified in "
       " install(FILES) and install(DIRECTORIES).");
@@ -705,6 +729,7 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
   bool installsPublicHeader = false;
   bool installsResource = false;
   std::vector<bool> installsFileSet(fileSetArgs.size(), false);
+  bool installsCxxModuleBmi = false;
 
   // Generate install script code to install the given targets.
   for (cmTarget* ti : targets) {
@@ -721,6 +746,7 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
     std::unique_ptr<cmInstallFilesGenerator> publicHeaderGenerator;
     std::unique_ptr<cmInstallFilesGenerator> resourceGenerator;
     std::vector<std::unique_ptr<cmInstallFileSetGenerator>> fileSetGenerators;
+    std::unique_ptr<cmInstallCxxModuleBmiGenerator> cxxModuleBmiGenerator;
 
     // Avoid selecting default destinations for PUBLIC_HEADER and
     // PRIVATE_HEADER if any artifacts are specified.
@@ -759,6 +785,7 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
         for (auto const& gen : fileSetGenerators) {
           te->FileSetGenerators[gen->GetFileSet()] = gen.get();
         }
+        te->CxxModuleBmiGenerator = cxxModuleBmiGenerator.get();
         target.AddInstallIncludeDirectories(
           *te, cmMakeRange(includesArgs.GetIncludeDirs()));
         te->NamelinkOnly = namelinkOnly;
@@ -1104,6 +1131,19 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
       }
     }
 
+    if (supportCxx20FileSetTypes &&
+        !cxxModuleBmiArgs.GetDestination().empty()) {
+      cxxModuleBmiGenerator = cm::make_unique<cmInstallCxxModuleBmiGenerator>(
+        target.GetName(),
+        helper.GetCxxModulesBmiDestination(&cxxModuleBmiArgs),
+        cxxModuleBmiArgs.GetPermissions(),
+        cxxModuleBmiArgs.GetConfigurations(), cxxModuleBmiArgs.GetComponent(),
+        cmInstallGenerator::SelectMessageLevel(target.GetMakefile()),
+        cxxModuleBmiArgs.GetExcludeFromAll(), cxxModuleBmiArgs.GetOptional(),
+        helper.Makefile->GetBacktrace());
+      target.SetHaveInstallRule(true);
+    }
+
     // Add this install rule to an export if one was specified.
     if (!addTargetExport()) {
       return false;
@@ -1120,6 +1160,7 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
     installsPrivateHeader = installsPrivateHeader || privateHeaderGenerator;
     installsPublicHeader = installsPublicHeader || publicHeaderGenerator;
     installsResource = installsResource || resourceGenerator;
+    installsCxxModuleBmi = installsCxxModuleBmi || cxxModuleBmiGenerator;
 
     helper.Makefile->AddInstallGenerator(std::move(archiveGenerator));
     helper.Makefile->AddInstallGenerator(std::move(libraryGenerator));
@@ -1134,6 +1175,7 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
     for (auto& gen : fileSetGenerators) {
       helper.Makefile->AddInstallGenerator(std::move(gen));
     }
+    helper.Makefile->AddInstallGenerator(std::move(cxxModuleBmiGenerator));
   }
 
   if (runtimeDependenciesArgVector && !runtimeDependencySet->Empty()) {
@@ -1191,6 +1233,10 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
         fileSetArgs[i].GetComponent());
     }
   }
+  if (installsCxxModuleBmi) {
+    helper.Makefile->GetGlobalGenerator()->AddInstallComponent(
+      cxxModuleBmiArgs.GetComponent());
+  }
 
   return true;
 }
@@ -1949,7 +1995,7 @@ bool HandleExportAndroidMKMode(std::vector<std::string> const& args,
     cm::make_unique<cmInstallExportGenerator>(
       &exportSet, ica.GetDestination(), ica.GetPermissions(),
       ica.GetConfigurations(), ica.GetComponent(), message,
-      ica.GetExcludeFromAll(), fname, name_space, exportOld, true,
+      ica.GetExcludeFromAll(), fname, name_space, "", exportOld, true,
       helper.Makefile->GetBacktrace()));
 
   return true;
@@ -1972,12 +2018,19 @@ bool HandleExportMode(std::vector<std::string> const& args,
   std::string name_space;
   bool exportOld = false;
   std::string filename;
+  std::string cxx_modules_directory;
 
   ica.Bind("EXPORT"_s, exp);
   ica.Bind("NAMESPACE"_s, name_space);
   ica.Bind("EXPORT_LINK_INTERFACE_LIBRARIES"_s, exportOld);
   ica.Bind("FILE"_s, filename);
 
+  bool const supportCxx20FileSetTypes = cmExperimental::HasSupportEnabled(
+    *helper.Makefile, cmExperimental::Feature::CxxModuleCMakeApi);
+  if (supportCxx20FileSetTypes) {
+    ica.Bind("CXX_MODULES_DIRECTORY"_s, cxx_modules_directory);
+  }
+
   std::vector<std::string> unknownArgs;
   ica.Parse(args, &unknownArgs);
 
@@ -2063,8 +2116,8 @@ bool HandleExportMode(std::vector<std::string> const& args,
     cm::make_unique<cmInstallExportGenerator>(
       &exportSet, ica.GetDestination(), ica.GetPermissions(),
       ica.GetConfigurations(), ica.GetComponent(), message,
-      ica.GetExcludeFromAll(), fname, name_space, exportOld, false,
-      helper.Makefile->GetBacktrace()));
+      ica.GetExcludeFromAll(), fname, name_space, cxx_modules_directory,
+      exportOld, false, helper.Makefile->GetBacktrace()));
 
   return true;
 }
@@ -2279,6 +2332,15 @@ std::string Helper::GetLibraryDestination(
   return this->GetDestination(args, "CMAKE_INSTALL_LIBDIR", "lib");
 }
 
+std::string Helper::GetCxxModulesBmiDestination(
+  const cmInstallCommandArguments* args) const
+{
+  if (args) {
+    return args->GetDestination();
+  }
+  return {};
+}
+
 std::string Helper::GetIncludeDestination(
   const cmInstallCommandArguments* args) const
 {

+ 75 - 0
Source/cmInstallCxxModuleBmiGenerator.cxx

@@ -0,0 +1,75 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmInstallCxxModuleBmiGenerator.h"
+
+#include <ostream>
+#include <utility>
+
+#include "cmGeneratorExpression.h"
+#include "cmGeneratorTarget.h"
+#include "cmGlobalGenerator.h"
+#include "cmListFileCache.h"
+#include "cmLocalGenerator.h"
+#include "cmOutputConverter.h"
+#include "cmStringAlgorithms.h"
+
+cmInstallCxxModuleBmiGenerator::cmInstallCxxModuleBmiGenerator(
+  std::string target, std::string const& dest, std::string file_permissions,
+  std::vector<std::string> const& configurations, std::string const& component,
+  MessageLevel message, bool exclude_from_all, bool optional,
+  cmListFileBacktrace backtrace)
+  : cmInstallGenerator(dest, configurations, component, message,
+                       exclude_from_all, false, std::move(backtrace))
+  , TargetName(std::move(target))
+  , FilePermissions(std::move(file_permissions))
+  , Optional(optional)
+{
+  this->ActionsPerConfig = true;
+}
+
+cmInstallCxxModuleBmiGenerator::~cmInstallCxxModuleBmiGenerator() = default;
+
+bool cmInstallCxxModuleBmiGenerator::Compute(cmLocalGenerator* lg)
+{
+  this->LocalGenerator = lg;
+
+  this->Target = lg->FindLocalNonAliasGeneratorTarget(this->TargetName);
+  if (!this->Target) {
+    // If no local target has been found, find it in the global scope.
+    this->Target =
+      lg->GetGlobalGenerator()->FindGeneratorTarget(this->TargetName);
+  }
+
+  return true;
+}
+
+std::string cmInstallCxxModuleBmiGenerator::GetScriptLocation(
+  std::string const& config) const
+{
+  char const* config_name = config.c_str();
+  if (config.empty()) {
+    config_name = "noconfig";
+  }
+  return cmStrCat(this->Target->GetSupportDirectory(),
+                  "/install-cxx-module-bmi-", config_name, ".cmake");
+}
+
+std::string cmInstallCxxModuleBmiGenerator::GetDestination(
+  std::string const& config) const
+{
+  return cmGeneratorExpression::Evaluate(this->Destination,
+                                         this->LocalGenerator, config);
+}
+
+void cmInstallCxxModuleBmiGenerator::GenerateScriptForConfig(
+  std::ostream& os, const std::string& config, Indent indent)
+{
+  auto const& loc = this->GetScriptLocation(config);
+  if (loc.empty()) {
+    return;
+  }
+  os << indent << "include(\""
+     << cmOutputConverter::EscapeForCMake(
+          loc, cmOutputConverter::WrapQuotes::NoWrap)
+     << "\" OPTIONAL)\n";
+}

+ 52 - 0
Source/cmInstallCxxModuleBmiGenerator.h

@@ -0,0 +1,52 @@
+/* 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 "cmInstallGenerator.h"
+#include "cmScriptGenerator.h"
+
+class cmGeneratorTarget;
+class cmListFileBacktrace;
+class cmLocalGenerator;
+
+/** \class cmInstallCxxModuleBmiGenerator
+ * \brief Generate C++ module BMI installation rules.
+ */
+class cmInstallCxxModuleBmiGenerator : public cmInstallGenerator
+{
+public:
+  cmInstallCxxModuleBmiGenerator(
+    std::string target, std::string const& dest, std::string file_permissions,
+    std::vector<std::string> const& configurations,
+    std::string const& component, MessageLevel message, bool exclude_from_all,
+    bool optional, cmListFileBacktrace backtrace);
+  ~cmInstallCxxModuleBmiGenerator() override;
+
+  bool Compute(cmLocalGenerator* lg) override;
+
+  std::string const& GetFilePermissions() const
+  {
+    return this->FilePermissions;
+  }
+  std::string GetDestination(std::string const& config) const;
+  std::string GetScriptLocation(std::string const& config) const;
+  cmGeneratorTarget const* GetTarget() const { return this->Target; }
+  bool GetOptional() const { return this->Optional; }
+  MessageLevel GetMessageLevel() const { return this->Message; }
+
+protected:
+  void GenerateScriptForConfig(std::ostream& os, const std::string& config,
+                               Indent indent) override;
+
+  std::string const TargetName;
+  cmGeneratorTarget const* Target = nullptr;
+  cmLocalGenerator* LocalGenerator = nullptr;
+  std::string const FilePermissions;
+  bool const Optional;
+};

+ 72 - 1
Source/cmInstallExportGenerator.cxx

@@ -23,7 +23,8 @@ cmInstallExportGenerator::cmInstallExportGenerator(
   cmExportSet* exportSet, std::string const& destination,
   std::string file_permissions, std::vector<std::string> const& configurations,
   std::string const& component, MessageLevel message, bool exclude_from_all,
-  std::string filename, std::string name_space, bool exportOld, bool android,
+  std::string filename, std::string name_space,
+  std::string cxx_modules_directory, bool exportOld, bool android,
   cmListFileBacktrace backtrace)
   : cmInstallGenerator(destination, configurations, component, message,
                        exclude_from_all, false, std::move(backtrace))
@@ -31,6 +32,7 @@ cmInstallExportGenerator::cmInstallExportGenerator(
   , FilePermissions(std::move(file_permissions))
   , FileName(std::move(filename))
   , Namespace(std::move(name_space))
+  , CxxModulesDirectory(std::move(cxx_modules_directory))
   , ExportOld(exportOld)
 {
   if (android) {
@@ -141,6 +143,75 @@ void cmInstallExportGenerator::GenerateScriptConfigs(std::ostream& os,
     os << indent << "endif()\n";
     files.clear();
   }
+
+  // Now create a configuration-specific install rule for the C++ module import
+  // property file of each configuration.
+  auto cxx_module_dest =
+    cmStrCat(this->Destination, '/', this->CxxModulesDirectory);
+  std::string config_file_example;
+  for (auto const& i : this->EFGen->GetConfigCxxModuleFiles()) {
+    config_file_example = i.second;
+    break;
+  }
+  if (!config_file_example.empty()) {
+    // Remove old per-configuration export files if the main changes.
+    std::string installedDir = cmStrCat(
+      "$ENV{DESTDIR}", ConvertToAbsoluteDestination(cxx_module_dest), '/');
+    std::string installedFile = cmStrCat(installedDir, "/cxx-modules.cmake");
+    std::string toInstallFile =
+      cmStrCat(cmSystemTools::GetFilenamePath(config_file_example),
+               "/cxx-modules.cmake");
+    os << indent << "if(EXISTS \"" << installedFile << "\")\n";
+    Indent indentN = indent.Next();
+    Indent indentNN = indentN.Next();
+    Indent indentNNN = indentNN.Next();
+    /* clang-format off */
+    os << indentN << "file(DIFFERENT _cmake_export_file_changed FILES\n"
+       << indentN << "     \"" << installedFile << "\"\n"
+       << indentN << "     \"" << toInstallFile << "\")\n";
+    os << indentN << "if(_cmake_export_file_changed)\n";
+    os << indentNN << "file(GLOB _cmake_old_config_files \"" << installedDir
+       << this->EFGen->GetConfigImportFileGlob() << "\")\n";
+    os << indentNN << "if(_cmake_old_config_files)\n";
+    os << indentNNN << "string(REPLACE \";\" \", \" _cmake_old_config_files_text \"${_cmake_old_config_files}\")\n";
+    os << indentNNN << R"(message(STATUS "Old C++ module export file \")" << installedFile
+       << "\\\" will be replaced.  Removing files [${_cmake_old_config_files_text}].\")\n";
+    os << indentNNN << "unset(_cmake_old_config_files_text)\n";
+    os << indentNNN << "file(REMOVE ${_cmake_old_config_files})\n";
+    os << indentNN << "endif()\n";
+    os << indentNN << "unset(_cmake_old_config_files)\n";
+    os << indentN << "endif()\n";
+    os << indentN << "unset(_cmake_export_file_changed)\n";
+    os << indent << "endif()\n";
+    /* clang-format on */
+
+    // All of these files are siblings; get its location to know where the
+    // "anchor" file is.
+    files.push_back(toInstallFile);
+    this->AddInstallRule(os, cxx_module_dest, cmInstallType_FILES, files,
+                         false, this->FilePermissions.c_str(), nullptr,
+                         nullptr, nullptr, indent);
+    files.clear();
+  }
+  for (auto const& i : this->EFGen->GetConfigCxxModuleFiles()) {
+    files.push_back(i.second);
+    std::string config_test = this->CreateConfigTest(i.first);
+    os << indent << "if(" << config_test << ")\n";
+    this->AddInstallRule(os, cxx_module_dest, cmInstallType_FILES, files,
+                         false, this->FilePermissions.c_str(), nullptr,
+                         nullptr, nullptr, indent.Next());
+    os << indent << "endif()\n";
+    files.clear();
+  }
+  for (auto const& i : this->EFGen->GetConfigCxxModuleTargetFiles()) {
+    std::string config_test = this->CreateConfigTest(i.first);
+    os << indent << "if(" << config_test << ")\n";
+    this->AddInstallRule(os, cxx_module_dest, cmInstallType_FILES, i.second,
+                         false, this->FilePermissions.c_str(), nullptr,
+                         nullptr, nullptr, indent.Next());
+    os << indent << "endif()\n";
+    files.clear();
+  }
 }
 
 void cmInstallExportGenerator::GenerateScriptActions(std::ostream& os,

+ 7 - 1
Source/cmInstallExportGenerator.h

@@ -28,7 +28,8 @@ public:
                            const std::vector<std::string>& configurations,
                            std::string const& component, MessageLevel message,
                            bool exclude_from_all, std::string filename,
-                           std::string name_space, bool exportOld,
+                           std::string name_space,
+                           std::string cxx_modules_directory, bool exportOld,
                            bool android, cmListFileBacktrace backtrace);
   cmInstallExportGenerator(const cmInstallExportGenerator&) = delete;
   ~cmInstallExportGenerator() override;
@@ -50,6 +51,10 @@ public:
   std::string GetDestinationFile() const;
   std::string GetFileName() const { return this->FileName; }
   std::string GetTempDir() const;
+  std::string GetCxxModuleDirectory() const
+  {
+    return this->CxxModulesDirectory;
+  }
 
 protected:
   void GenerateScript(std::ostream& os) override;
@@ -64,6 +69,7 @@ protected:
   std::string const FilePermissions;
   std::string const FileName;
   std::string const Namespace;
+  std::string const CxxModulesDirectory;
   bool const ExportOld;
   cmLocalGenerator* LocalGenerator = nullptr;
 

+ 216 - 0
Source/cmNinjaTargetGenerator.cxx

@@ -21,11 +21,17 @@
 
 #include "cmComputeLinkInformation.h"
 #include "cmCustomCommandGenerator.h"
+#include "cmExportBuildFileGenerator.h"
+#include "cmExportSet.h"
 #include "cmFileSet.h"
 #include "cmGeneratedFileStream.h"
 #include "cmGeneratorExpression.h"
 #include "cmGeneratorTarget.h"
 #include "cmGlobalNinjaGenerator.h"
+#include "cmInstallCxxModuleBmiGenerator.h"
+#include "cmInstallExportGenerator.h"
+#include "cmInstallFileSetGenerator.h"
+#include "cmInstallGenerator.h"
 #include "cmLocalGenerator.h"
 #include "cmLocalNinjaGenerator.h"
 #include "cmMakefile.h"
@@ -41,6 +47,7 @@
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
 #include "cmTarget.h"
+#include "cmTargetExport.h"
 #include "cmValue.h"
 #include "cmake.h"
 
@@ -1653,6 +1660,215 @@ void cmNinjaTargetGenerator::WriteTargetDependInfo(std::string const& lang,
     tdi_linked_target_dirs.append(l);
   }
 
+  cmTarget* tgt = this->GeneratorTarget->Target;
+  auto all_file_sets = tgt->GetAllFileSetNames();
+  Json::Value& tdi_cxx_module_info = tdi["cxx-modules"] = Json::objectValue;
+  for (auto const& file_set_name : all_file_sets) {
+    auto* file_set = tgt->GetFileSet(file_set_name);
+    if (!file_set) {
+      this->GetMakefile()->IssueMessage(
+        MessageType::INTERNAL_ERROR,
+        cmStrCat("Target \"", tgt->GetName(),
+                 "\" is tracked to have file set \"", file_set_name,
+                 "\", but it was not found."));
+      continue;
+    }
+    auto fs_type = file_set->GetType();
+    // We only care about C++ module sources here.
+    if (fs_type != "CXX_MODULES"_s) {
+      continue;
+    }
+
+    auto fileEntries = file_set->CompileFileEntries();
+    auto directoryEntries = file_set->CompileDirectoryEntries();
+
+    auto directories = file_set->EvaluateDirectoryEntries(
+      directoryEntries, this->GeneratorTarget->LocalGenerator, config,
+      this->GeneratorTarget);
+    std::map<std::string, std::vector<std::string>> files_per_dirs;
+    for (auto const& entry : fileEntries) {
+      file_set->EvaluateFileEntry(directories, files_per_dirs, entry,
+                                  this->GeneratorTarget->LocalGenerator,
+                                  config, this->GeneratorTarget);
+    }
+
+    std::map<std::string, cmSourceFile const*> sf_map;
+    {
+      std::vector<cmSourceFile const*> objectSources;
+      this->GeneratorTarget->GetObjectSources(objectSources, config);
+      for (auto const* sf : objectSources) {
+        auto full_path = sf->GetFullPath();
+        if (full_path.empty()) {
+          this->GetMakefile()->IssueMessage(
+            MessageType::INTERNAL_ERROR,
+            cmStrCat("Target \"", tgt->GetName(),
+                     "\" has a full path-less source file."));
+          continue;
+        }
+        sf_map[full_path] = sf;
+      }
+    }
+
+    Json::Value fs_dest = Json::nullValue;
+    for (auto const& ig : this->GetMakefile()->GetInstallGenerators()) {
+      if (auto const* fsg =
+            dynamic_cast<cmInstallFileSetGenerator const*>(ig.get())) {
+        if (fsg->GetTarget() == this->GeneratorTarget &&
+            fsg->GetFileSet() == file_set) {
+          fs_dest = fsg->GetDestination(config);
+          continue;
+        }
+      }
+    }
+
+    for (auto const& files_per_dir : files_per_dirs) {
+      for (auto const& file : files_per_dir.second) {
+        auto lookup = sf_map.find(file);
+        if (lookup == sf_map.end()) {
+          this->GetMakefile()->IssueMessage(
+            MessageType::INTERNAL_ERROR,
+            cmStrCat("Target \"", tgt->GetName(), "\" has source file \"",
+                     file,
+                     R"(" which is not in any of its "FILE_SET BASE_DIRS".)"));
+          continue;
+        }
+
+        auto const* sf = lookup->second;
+
+        if (!sf) {
+          this->GetMakefile()->IssueMessage(
+            MessageType::INTERNAL_ERROR,
+            cmStrCat("Target \"", tgt->GetName(), "\" has source file \"",
+                     file, "\" which has not been tracked properly."));
+          continue;
+        }
+
+        auto obj_path = this->GetObjectFilePath(sf, config);
+        Json::Value& tdi_module_info = tdi_cxx_module_info[obj_path] =
+          Json::objectValue;
+
+        tdi_module_info["source"] = file;
+        tdi_module_info["relative-directory"] = files_per_dir.first;
+        tdi_module_info["name"] = file_set->GetName();
+        tdi_module_info["type"] = file_set->GetType();
+        tdi_module_info["visibility"] =
+          std::string(cmFileSetVisibilityToName(file_set->GetVisibility()));
+        tdi_module_info["destination"] = fs_dest;
+      }
+    }
+  }
+
+  tdi["config"] = config;
+
+  // Add information about the export sets that this target is a member of.
+  Json::Value& tdi_exports = tdi["exports"] = Json::arrayValue;
+  std::string export_name = this->GeneratorTarget->GetExportName();
+
+  cmInstallCxxModuleBmiGenerator const* bmi_gen = nullptr;
+  for (auto const& ig : this->GetMakefile()->GetInstallGenerators()) {
+    if (auto const* bmig =
+          dynamic_cast<cmInstallCxxModuleBmiGenerator const*>(ig.get())) {
+      if (bmig->GetTarget() == this->GeneratorTarget) {
+        bmi_gen = bmig;
+        continue;
+      }
+    }
+  }
+  if (bmi_gen) {
+    Json::Value tdi_bmi_info = Json::objectValue;
+
+    tdi_bmi_info["permissions"] = bmi_gen->GetFilePermissions();
+    tdi_bmi_info["destination"] = bmi_gen->GetDestination(config);
+    const char* msg_level = "";
+    switch (bmi_gen->GetMessageLevel()) {
+      case cmInstallGenerator::MessageDefault:
+        break;
+      case cmInstallGenerator::MessageAlways:
+        msg_level = "MESSAGE_ALWAYS";
+        break;
+      case cmInstallGenerator::MessageLazy:
+        msg_level = "MESSAGE_LAZY";
+        break;
+      case cmInstallGenerator::MessageNever:
+        msg_level = "MESSAGE_NEVER";
+        break;
+    }
+    tdi_bmi_info["message-level"] = msg_level;
+    tdi_bmi_info["script-location"] = bmi_gen->GetScriptLocation(config);
+
+    tdi["bmi-installation"] = tdi_bmi_info;
+  } else {
+    tdi["bmi-installation"] = Json::nullValue;
+  }
+
+  auto const& all_install_exports =
+    this->GetGlobalGenerator()->GetExportSets();
+  for (auto const& exp : all_install_exports) {
+    // Ignore exports sets which are not for this target.
+    auto const& targets = exp.second.GetTargetExports();
+    auto tgt_export =
+      std::find_if(targets.begin(), targets.end(),
+                   [this](std::unique_ptr<cmTargetExport> const& te) {
+                     return te->Target == this->GeneratorTarget;
+                   });
+    if (tgt_export == targets.end()) {
+      continue;
+    }
+
+    auto const* installs = exp.second.GetInstallations();
+    for (auto const* install : *installs) {
+      Json::Value tdi_export_info = Json::objectValue;
+
+      auto const& ns = install->GetNamespace();
+      auto const& dest = install->GetDestination();
+      auto const& cxxm_dir = install->GetCxxModuleDirectory();
+      auto const& export_prefix = install->GetTempDir();
+
+      tdi_export_info["namespace"] = ns;
+      tdi_export_info["export-name"] = export_name;
+      tdi_export_info["destination"] = dest;
+      tdi_export_info["cxx-module-info-dir"] = cxxm_dir;
+      tdi_export_info["export-prefix"] = export_prefix;
+      tdi_export_info["install"] = true;
+
+      tdi_exports.append(tdi_export_info);
+    }
+  }
+
+  auto const& all_build_exports =
+    this->GetMakefile()->GetExportBuildFileGenerators();
+  for (auto const& exp : all_build_exports) {
+    std::vector<std::string> targets;
+    exp->GetTargets(targets);
+
+    // Ignore exports sets which are not for this target.
+    auto const& name = this->GeneratorTarget->GetName();
+    bool has_current_target =
+      std::any_of(targets.begin(), targets.end(),
+                  [name](std::string const& tname) { return tname == name; });
+    if (!has_current_target) {
+      continue;
+    }
+
+    Json::Value tdi_export_info = Json::objectValue;
+
+    auto const& ns = exp->GetNamespace();
+    auto const& main_fn = exp->GetMainExportFileName();
+    auto const& cxxm_dir = exp->GetCxxModuleDirectory();
+    auto dest = cmsys::SystemTools::GetParentDirectory(main_fn);
+    auto const& export_prefix =
+      cmSystemTools::GetFilenamePath(exp->GetMainExportFileName());
+
+    tdi_export_info["namespace"] = ns;
+    tdi_export_info["export-name"] = export_name;
+    tdi_export_info["destination"] = dest;
+    tdi_export_info["cxx-module-info-dir"] = cxxm_dir;
+    tdi_export_info["export-prefix"] = export_prefix;
+    tdi_export_info["install"] = false;
+
+    tdi_exports.append(tdi_export_info);
+  }
+
   std::string const tdin = this->GetTargetDependInfoPath(lang, config);
   cmGeneratedFileStream tdif(tdin);
   tdif << tdi;

+ 2 - 0
Source/cmTargetExport.h

@@ -8,6 +8,7 @@
 
 class cmFileSet;
 class cmGeneratorTarget;
+class cmInstallCxxModuleBmiGenerator;
 class cmInstallFileSetGenerator;
 class cmInstallFilesGenerator;
 class cmInstallTargetGenerator;
@@ -32,6 +33,7 @@ public:
   cmInstallTargetGenerator* BundleGenerator;
   cmInstallFilesGenerator* HeaderGenerator;
   std::map<cmFileSet*, cmInstallFileSetGenerator*> FileSetGenerators;
+  cmInstallCxxModuleBmiGenerator* CxxModuleBmiGenerator;
   ///@}
 
   bool NamelinkOnly = false;

+ 2 - 1
Source/cmTargetSourcesCommand.cxx

@@ -269,7 +269,8 @@ bool TargetSourcesImpl::HandleOneFileSet(
       }
 
       if (cmFileSetVisibilityIsForInterface(visibility) &&
-          !cmFileSetVisibilityIsForSelf(visibility)) {
+          !cmFileSetVisibilityIsForSelf(visibility) &&
+          !this->Target->IsImported()) {
         if (type == "CXX_MODULES"_s || type == "CXX_MODULE_HEADER_UNITS"_s) {
           this->SetError(
             R"(File set TYPEs "CXX_MODULES" and "CXX_MODULE_HEADER_UNITS" may not have "INTERFACE" visibility)");

+ 1 - 1
Tests/RunCMake/CXXModules/CMakeLists.txt

@@ -1,6 +1,6 @@
 cmake_minimum_required(VERSION 3.23)
 project(${RunCMake_TEST} NONE)
 
-set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "17be90bd-a850-44e0-be50-448de847d652")
+set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "3c375311-a3c9-4396-a187-3227ef642046")
 
 include(${RunCMake_TEST}.cmake)

+ 40 - 0
Tests/RunCMake/CXXModules/ExportBuildCxxModules-check.cmake

@@ -0,0 +1,40 @@
+file(READ "${RunCMake_TEST_BINARY_DIR}/lib/cmake/export-modules/export-modules-targets.cmake" export_script)
+
+if (NOT export_script MATCHES [[include\("\${CMAKE_CURRENT_LIST_DIR}/cxx-modules/cxx-modules\.cmake"\)]])
+  list(APPEND RunCMake_TEST_FAILED
+    "Could not find C++ module property script inclusion")
+endif ()
+
+file(READ "${RunCMake_TEST_BINARY_DIR}/lib/cmake/export-modules/cxx-modules/cxx-modules.cmake" trampoline_script)
+
+if (RunCMake_GENERATOR_IS_MULTI_CONFIG)
+  if (NOT trampoline_script MATCHES [[include\("\${CMAKE_CURRENT_LIST_DIR}/cxx-modules-[^.]*\.cmake" OPTIONAL\)]])
+    list(APPEND RunCMake_TEST_FAILED
+      "Could not find C++ module property per-config script inclusion(s)")
+  endif ()
+else ()
+  if (NOT trampoline_script MATCHES [[include\("\${CMAKE_CURRENT_LIST_DIR}/cxx-modules-[^.]*\.cmake"\)]])
+    list(APPEND RunCMake_TEST_FAILED
+      "Could not find C++ module property per-config script inclusion(s)")
+  endif ()
+endif ()
+
+set(any_exists 0)
+foreach (config IN ITEMS noconfig Debug Release RelWithDebInfo MinSizeRel)
+  if (NOT EXISTS "${RunCMake_TEST_BINARY_DIR}/lib/cmake/export-modules/cxx-modules/cxx-modules-${config}.cmake")
+    continue ()
+  endif ()
+  set(any_exists 1)
+
+  file(READ "${RunCMake_TEST_BINARY_DIR}/lib/cmake/export-modules/cxx-modules/cxx-modules-${config}.cmake" config_script)
+
+  if (NOT config_script MATCHES "include\\(\"\\\${CMAKE_CURRENT_LIST_DIR}/target-export-name-${config}\\.cmake\"\\)")
+    list(APPEND RunCMake_TEST_FAILED
+      "Could not find C++ module per-target property script inclusion")
+  endif ()
+endforeach ()
+
+if (NOT any_exists)
+  list(APPEND RunCMake_TEST_FAILED
+    "No per-configuration target files exist.")
+endif ()

+ 11 - 0
Tests/RunCMake/CXXModules/ExportBuildCxxModules-stderr.txt

@@ -0,0 +1,11 @@
+CMake Warning \(dev\) at ExportBuildCxxModules.cmake:6 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:6 \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 22 - 0
Tests/RunCMake/CXXModules/ExportBuildCxxModules.cmake

@@ -0,0 +1,22 @@
+enable_language(CXX)
+set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
+set(CMAKE_EXPERIMENTAL_CXX_SCANDEP_SOURCE "")
+
+add_library(export-modules)
+target_sources(export-modules
+  PUBLIC
+    FILE_SET fs TYPE CXX_MODULES FILES
+      sources/module.cxx)
+target_compile_features(export-modules
+  PRIVATE
+    cxx_std_20)
+set_property(TARGET export-modules
+  PROPERTY EXPORT_NAME export-name)
+
+install(TARGETS export-modules
+  EXPORT exp
+  FILE_SET fs DESTINATION "include/cxx/export-modules")
+
+export(EXPORT exp
+  FILE "${CMAKE_BINARY_DIR}/lib/cmake/export-modules/export-modules-targets.cmake"
+  CXX_MODULES_DIRECTORY "cxx-modules")

+ 35 - 0
Tests/RunCMake/CXXModules/ExportInstallCxxModules-check.cmake

@@ -0,0 +1,35 @@
+file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/Export/eee57a7e91412f1be699e9b63fa9d601/exp.cmake" export_script)
+
+if (NOT export_script MATCHES [[include\("\${CMAKE_CURRENT_LIST_DIR}/cxx-modules/cxx-modules\.cmake"\)]])
+  list(APPEND RunCMake_TEST_FAILED
+    "Could not find C++ module property script inclusion")
+endif ()
+
+file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/Export/eee57a7e91412f1be699e9b63fa9d601/cxx-modules/cxx-modules.cmake" trampoline_script)
+
+if (NOT trampoline_script MATCHES [[file\(GLOB _cmake_cxx_module_includes "\${CMAKE_CURRENT_LIST_DIR}/cxx-modules-\*\.cmake"\)]])
+  list(APPEND RunCMake_TEST_FAILED
+    "Could not find C++ module property per-config script inclusion(s)")
+endif ()
+
+set(any_exists 0)
+foreach (config IN ITEMS noconfig Debug Release RelWithDebInfo MinSizeRel)
+  if (NOT EXISTS "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/Export/eee57a7e91412f1be699e9b63fa9d601/cxx-modules/cxx-modules-${config}.cmake")
+    continue ()
+  endif ()
+  set(any_exists 1)
+
+  file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/Export/eee57a7e91412f1be699e9b63fa9d601/cxx-modules/cxx-modules-${config}.cmake" config_script)
+
+  if (NOT config_script MATCHES "include\\(\"\\\${CMAKE_CURRENT_LIST_DIR}/target-export-name-${config}\\.cmake\"\\)")
+    list(APPEND RunCMake_TEST_FAILED
+      "Could not find C++ module per-target property script inclusion")
+  endif ()
+endforeach ()
+
+if (NOT any_exists)
+  list(APPEND RunCMake_TEST_FAILED
+    "No per-configuration target files exist.")
+endif ()
+
+string(REPLACE ";" "; " RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}")

+ 11 - 0
Tests/RunCMake/CXXModules/ExportInstallCxxModules-stderr.txt

@@ -0,0 +1,11 @@
+CMake Warning \(dev\) at ExportInstallCxxModules.cmake:6 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:6 \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 22 - 0
Tests/RunCMake/CXXModules/ExportInstallCxxModules.cmake

@@ -0,0 +1,22 @@
+enable_language(CXX)
+set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
+set(CMAKE_EXPERIMENTAL_CXX_SCANDEP_SOURCE "")
+
+add_library(export-modules)
+target_sources(export-modules
+  PUBLIC
+    FILE_SET fs TYPE CXX_MODULES FILES
+      sources/module.cxx)
+target_compile_features(export-modules
+  PRIVATE
+    cxx_std_20)
+set_property(TARGET export-modules
+  PROPERTY EXPORT_NAME export-name)
+
+install(TARGETS export-modules
+  EXPORT exp
+  FILE_SET fs DESTINATION "include/cxx/export-modules")
+
+install(EXPORT exp
+  DESTINATION "lib/cmake/export-modules"
+  CXX_MODULES_DIRECTORY "cxx-modules")

+ 6 - 0
Tests/RunCMake/CXXModules/FileSetModuleHeaderUnitsInterfaceImported-stderr.txt

@@ -0,0 +1,6 @@
+CMake Warning \(dev\) at FileSetModuleHeaderUnitsInterfaceImported.cmake:2 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:6 \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 8 - 0
Tests/RunCMake/CXXModules/FileSetModuleHeaderUnitsInterfaceImported.cmake

@@ -0,0 +1,8 @@
+add_library(module-header SHARED IMPORTED)
+target_sources(module-header
+  INTERFACE
+    FILE_SET fs TYPE CXX_MODULE_HEADER_UNITS FILES
+      sources/module-header.h)
+target_compile_features(module-header
+  INTERFACE
+    cxx_std_20)

+ 6 - 0
Tests/RunCMake/CXXModules/FileSetModulesInterfaceImported-stderr.txt

@@ -0,0 +1,6 @@
+CMake Warning \(dev\) at FileSetModulesInterfaceImported.cmake:2 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:6 \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 8 - 0
Tests/RunCMake/CXXModules/FileSetModulesInterfaceImported.cmake

@@ -0,0 +1,8 @@
+add_library(module SHARED IMPORTED)
+target_sources(module
+  INTERFACE
+    FILE_SET fs TYPE CXX_MODULES FILES
+      sources/module.cxx)
+target_compile_features(module
+  INTERFACE
+    cxx_std_20)

+ 24 - 0
Tests/RunCMake/CXXModules/InstallBMI-check.cmake

@@ -0,0 +1,24 @@
+file(READ "${RunCMake_TEST_BINARY_DIR}/cmake_install.cmake" install_script)
+
+if (NOT install_script MATCHES [[\(CMAKE_INSTALL_COMPONENT STREQUAL "bmi" OR NOT CMAKE_INSTALL_COMPONENT\)]])
+  list(APPEND RunCMake_TEST_FAILED
+    "Could not find BMI install script component for `bmi`")
+endif ()
+
+if (NOT install_script MATCHES [[include\("[^)]*/CMakeFiles/install-bmi\.dir/install-cxx-module-bmi-[^.]*\.cmake" OPTIONAL\)]])
+  list(APPEND RunCMake_TEST_FAILED
+    "Could not find BMI install script inclusion")
+endif ()
+
+if (NOT install_script MATCHES [[\(CMAKE_INSTALL_COMPONENT STREQUAL "bmi-optional"\)]])
+  list(APPEND RunCMake_TEST_FAILED
+    "Could not find BMI install script component for `bmi-optional`")
+endif ()
+
+if (NOT install_script MATCHES [[\(CMAKE_INSTALL_COMPONENT STREQUAL "bmi-only-debug" OR NOT CMAKE_INSTALL_COMPONENT\)
+  if\(CMAKE_INSTALL_CONFIG_NAME MATCHES "\^\(\[Dd\]\[Ee\]\[Bb\]\[Uu\]\[Gg\]\)\$"\)]])
+  list(APPEND RunCMake_TEST_FAILED
+    "Could not find BMI install script component for `bmi-only-debug`")
+endif ()
+
+string(REPLACE ";" "; " RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}")

+ 6 - 0
Tests/RunCMake/CXXModules/InstallBMI-stderr.txt

@@ -0,0 +1,6 @@
+CMake Warning \(dev\) at InstallBMI.cmake:8 \(install\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:6 \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 23 - 0
Tests/RunCMake/CXXModules/InstallBMI.cmake

@@ -0,0 +1,23 @@
+enable_language(CXX)
+
+add_library(install-bmi)
+target_sources(install-bmi
+  PRIVATE
+    sources/cxx-anchor.cxx)
+
+install(TARGETS install-bmi
+  CXX_MODULES_BMI
+    DESTINATION "lib/bmi"
+    COMPONENT "bmi")
+
+install(TARGETS install-bmi
+  CXX_MODULES_BMI
+    DESTINATION "lib/bmi"
+    EXCLUDE_FROM_ALL
+    COMPONENT "bmi-optional")
+
+install(TARGETS install-bmi
+  CXX_MODULES_BMI
+    DESTINATION "lib/bmi"
+    CONFIGURATIONS Debug
+    COMPONENT "bmi-only-debug")

+ 8 - 0
Tests/RunCMake/CXXModules/InstallBMIGenericArgs-check.cmake

@@ -0,0 +1,8 @@
+file(READ "${RunCMake_TEST_BINARY_DIR}/cmake_install.cmake" install_script)
+
+if (NOT install_script MATCHES [[include\("[^)]*/CMakeFiles/install-bmi-generic-args\.dir/install-cxx-module-bmi-[^.]*\.cmake" OPTIONAL\)]])
+  list(APPEND RunCMake_TEST_FAILED
+    "Could not find BMI install script inclusion")
+endif ()
+
+string(REPLACE ";" "; " RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}")

+ 6 - 0
Tests/RunCMake/CXXModules/InstallBMIGenericArgs-stderr.txt

@@ -0,0 +1,6 @@
+CMake Warning \(dev\) at InstallBMIGenericArgs.cmake:8 \(install\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:6 \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 9 - 0
Tests/RunCMake/CXXModules/InstallBMIGenericArgs.cmake

@@ -0,0 +1,9 @@
+enable_language(CXX)
+
+add_library(install-bmi-generic-args)
+target_sources(install-bmi-generic-args
+  PRIVATE
+    sources/cxx-anchor.cxx)
+
+install(TARGETS install-bmi-generic-args
+  DESTINATION "bin")

+ 13 - 0
Tests/RunCMake/CXXModules/InstallBMIIgnore-check.cmake

@@ -0,0 +1,13 @@
+file(READ "${RunCMake_TEST_BINARY_DIR}/cmake_install.cmake" install_script)
+
+if (install_script MATCHES [[\(CMAKE_INSTALL_COMPONENT STREQUAL "bmi" OR NOT CMAKE_INSTALL_COMPONENT\)]])
+  list(APPEND RunCMake_TEST_FAILED
+    "Found BMI install script component for `bmi`")
+endif ()
+
+if (install_script MATCHES [[include\("[^)]*/CMakeFiles/install-bmi-ignore\.dir/install-cxx-module-bmi-[^.]*\.cmake" OPTIONAL\)]])
+  list(APPEND RunCMake_TEST_FAILED
+    "Found BMI install script inclusion")
+endif ()
+
+string(REPLACE ";" "; " RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}")

+ 6 - 0
Tests/RunCMake/CXXModules/InstallBMIIgnore-stderr.txt

@@ -0,0 +1,6 @@
+CMake Warning \(dev\) at InstallBMIIgnore.cmake:5 \(install\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:6 \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 9 - 0
Tests/RunCMake/CXXModules/InstallBMIIgnore.cmake

@@ -0,0 +1,9 @@
+enable_language(CXX)
+
+add_library(install-bmi-ignore INTERFACE)
+
+install(TARGETS install-bmi-ignore
+  CXX_MODULES_BMI
+    # An empty destination ignores BMI installation.
+    DESTINATION ""
+    COMPONENT "bmi")

+ 8 - 0
Tests/RunCMake/CXXModules/InstallBMINoGenericArgs-check.cmake

@@ -0,0 +1,8 @@
+file(READ "${RunCMake_TEST_BINARY_DIR}/cmake_install.cmake" install_script)
+
+if (install_script MATCHES [[include\("[^)]*/CMakeFiles/install-bmi-generic-args\.dir/install-cxx-module-bmi-[^.]*\.cmake" OPTIONAL\)]])
+  list(APPEND RunCMake_TEST_FAILED
+    "Found BMI install script inclusion")
+endif ()
+
+string(REPLACE ";" "; " RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}")

+ 34 - 0
Tests/RunCMake/CXXModules/NinjaDependInfoBMIInstall-check.cmake

@@ -0,0 +1,34 @@
+include("${CMAKE_CURRENT_LIST_DIR}/check-json.cmake")
+
+if (RunCMake_GENERATOR_IS_MULTI_CONFIG)
+  set(have_file 0)
+  foreach (config IN ITEMS Release Debug RelWithDebInfo MinSizeRel)
+    if (NOT EXISTS "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-bmi-install-public.dir/${config}/CXXDependInfo.json")
+      continue ()
+    endif ()
+    set(have_file 1)
+
+    set(CMAKE_BUILD_TYPE "${config}")
+
+    file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-bmi-install-public.dir/${config}/CXXDependInfo.json" actual_contents)
+    file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoBMIInstall-public.json" expect_contents)
+    check_json("${actual_contents}" "${expect_contents}")
+
+    file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-bmi-install-private.dir/${config}/CXXDependInfo.json" actual_contents)
+    file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoBMIInstall-private.json" expect_contents)
+    check_json("${actual_contents}" "${expect_contents}")
+  endforeach ()
+
+  if (NOT have_file)
+    list(APPEND RunCMake_TEST_FAILED
+      "No recognized build configurations found.")
+  endif ()
+else ()
+  file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-bmi-install-public.dir/CXXDependInfo.json" actual_contents)
+  file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoBMIInstall-public.json" expect_contents)
+  check_json("${actual_contents}" "${expect_contents}")
+
+  file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-bmi-install-private.dir/CXXDependInfo.json" actual_contents)
+  file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoBMIInstall-private.json" expect_contents)
+  check_json("${actual_contents}" "${expect_contents}")
+endif ()

+ 11 - 0
Tests/RunCMake/CXXModules/NinjaDependInfoBMIInstall-stderr.txt

@@ -0,0 +1,11 @@
+CMake Warning \(dev\) at NinjaDependInfoBMIInstall.cmake:14 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:6 \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 76 - 0
Tests/RunCMake/CXXModules/NinjaDependInfoBMIInstall.cmake

@@ -0,0 +1,76 @@
+# Fake out that we have dyndep; we only need to generate, not actually build
+# here.
+set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
+set(CMAKE_EXPERIMENTAL_CXX_SCANDEP_SOURCE "")
+
+enable_language(CXX)
+
+if (NOT CMAKE_GENERATOR MATCHES "Ninja")
+  message(FATAL_ERROR
+    "This test requires a 'Ninja' generator to be used.")
+endif ()
+
+add_library(ninja-bmi-install-public)
+target_sources(ninja-bmi-install-public
+  PRIVATE
+    sources/module-impl.cxx
+    sources/module-internal-part-impl.cxx
+    sources/module-part-impl.cxx
+    sources/module-use.cxx
+  PUBLIC
+    FILE_SET modules TYPE CXX_MODULES
+    BASE_DIRS
+      "${CMAKE_CURRENT_SOURCE_DIR}/sources"
+    FILES
+      sources/module.cxx
+      sources/module-part.cxx
+    FILE_SET internal_partitions TYPE CXX_MODULES FILES
+      sources/module-internal-part.cxx)
+target_compile_features(ninja-bmi-install-public
+  PRIVATE
+    cxx_std_20)
+set_property(TARGET ninja-bmi-install-public
+  PROPERTY EXPORT_NAME "with-public")
+
+install(TARGETS ninja-bmi-install-public
+  FILE_SET modules
+    DESTINATION "lib/cxx"
+    COMPONENT "modules"
+  FILE_SET internal_partitions
+    DESTINATION "lib/cxx/internals"
+    COMPONENT "modules-internal"
+  CXX_MODULES_BMI
+    DESTINATION "lib/cxx/modules/$<CONFIG>"
+    COMPONENT "bmi")
+
+add_library(ninja-bmi-install-private)
+target_sources(ninja-bmi-install-private
+  PRIVATE
+    sources/module-impl.cxx
+    sources/module-internal-part-impl.cxx
+    sources/module-part-impl.cxx
+    sources/module-use.cxx
+  PRIVATE
+    FILE_SET modules TYPE CXX_MODULES
+    BASE_DIRS
+      "${CMAKE_CURRENT_SOURCE_DIR}/sources"
+    FILES
+      sources/module.cxx
+      sources/module-part.cxx
+    FILE_SET internal_partitions TYPE CXX_MODULES FILES
+      sources/module-internal-part.cxx)
+target_compile_features(ninja-bmi-install-private
+  PRIVATE
+    cxx_std_20)
+set_property(TARGET ninja-bmi-install-private
+  PROPERTY EXPORT_NAME "with-private")
+
+set(CMAKE_INSTALL_MESSAGE LAZY)
+install(TARGETS ninja-bmi-install-private
+  CXX_MODULES_BMI
+    DESTINATION "lib/cxx/modules/private/$<CONFIG>"
+    PERMISSIONS
+      OWNER_READ OWNER_WRITE
+      GROUP_READ
+      WORLD_READ
+    COMPONENT "bmi")

+ 34 - 0
Tests/RunCMake/CXXModules/NinjaDependInfoExport-check.cmake

@@ -0,0 +1,34 @@
+include("${CMAKE_CURRENT_LIST_DIR}/check-json.cmake")
+
+if (RunCMake_GENERATOR_IS_MULTI_CONFIG)
+  set(have_file 0)
+  foreach (config IN ITEMS Release Debug RelWithDebInfo MinSizeRel)
+    if (NOT EXISTS "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-exports-public.dir/${config}/CXXDependInfo.json")
+      continue ()
+    endif ()
+    set(have_file 1)
+
+    set(CMAKE_BUILD_TYPE "${config}")
+
+    file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-exports-public.dir/${config}/CXXDependInfo.json" actual_contents)
+    file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoExport-public.json" expect_contents)
+    check_json("${actual_contents}" "${expect_contents}")
+
+    file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-exports-private.dir/${config}/CXXDependInfo.json" actual_contents)
+    file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoExport-private.json" expect_contents)
+    check_json("${actual_contents}" "${expect_contents}")
+  endforeach ()
+
+  if (NOT have_file)
+    list(APPEND RunCMake_TEST_FAILED
+      "No recognized build configurations found.")
+  endif ()
+else ()
+  file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-exports-public.dir/CXXDependInfo.json" actual_contents)
+  file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoExport-public.json" expect_contents)
+  check_json("${actual_contents}" "${expect_contents}")
+
+  file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-exports-private.dir/CXXDependInfo.json" actual_contents)
+  file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoExport-private.json" expect_contents)
+  check_json("${actual_contents}" "${expect_contents}")
+endif ()

+ 11 - 0
Tests/RunCMake/CXXModules/NinjaDependInfoExport-stderr.txt

@@ -0,0 +1,11 @@
+CMake Warning \(dev\) at NinjaDependInfoExport.cmake:14 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:6 \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 85 - 0
Tests/RunCMake/CXXModules/NinjaDependInfoExport.cmake

@@ -0,0 +1,85 @@
+# Fake out that we have dyndep; we only need to generate, not actually build
+# here.
+set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
+set(CMAKE_EXPERIMENTAL_CXX_SCANDEP_SOURCE "")
+
+enable_language(CXX)
+
+if (NOT CMAKE_GENERATOR MATCHES "Ninja")
+  message(FATAL_ERROR
+    "This test requires a 'Ninja' generator to be used.")
+endif ()
+
+add_library(ninja-exports-public)
+target_sources(ninja-exports-public
+  PRIVATE
+    sources/module-impl.cxx
+    sources/module-internal-part-impl.cxx
+    sources/module-part-impl.cxx
+    sources/module-use.cxx
+  PUBLIC
+    FILE_SET modules TYPE CXX_MODULES
+    BASE_DIRS
+      "${CMAKE_CURRENT_SOURCE_DIR}/sources"
+    FILES
+      sources/module.cxx
+      sources/module-part.cxx
+    FILE_SET internal_partitions TYPE CXX_MODULES FILES
+      sources/module-internal-part.cxx)
+target_compile_features(ninja-exports-public
+  PRIVATE
+    cxx_std_20)
+set_property(TARGET ninja-exports-public
+  PROPERTY EXPORT_NAME "with-public")
+
+install(TARGETS ninja-exports-public
+  EXPORT exp
+  FILE_SET modules
+    DESTINATION "lib/cxx"
+    COMPONENT "modules"
+  FILE_SET internal_partitions
+    DESTINATION "lib/cxx/internals"
+    COMPONENT "modules-internal")
+
+add_library(ninja-exports-private)
+target_sources(ninja-exports-private
+  PRIVATE
+    sources/module-impl.cxx
+    sources/module-internal-part-impl.cxx
+    sources/module-part-impl.cxx
+    sources/module-use.cxx
+  PRIVATE
+    FILE_SET modules TYPE CXX_MODULES
+    BASE_DIRS
+      "${CMAKE_CURRENT_SOURCE_DIR}/sources"
+    FILES
+      sources/module.cxx
+      sources/module-part.cxx
+    FILE_SET internal_partitions TYPE CXX_MODULES FILES
+      sources/module-internal-part.cxx)
+target_compile_features(ninja-exports-private
+  PRIVATE
+    cxx_std_20)
+set_property(TARGET ninja-exports-private
+  PROPERTY EXPORT_NAME "with-private")
+
+install(TARGETS ninja-exports-private
+  EXPORT exp)
+
+# Test multiple build exports.
+export(EXPORT exp
+  FILE "${CMAKE_BINARY_DIR}/lib/cmake/export1/export1-targets.cmake"
+  NAMESPACE export1::
+  CXX_MODULES_DIRECTORY "cxx-modules")
+export(EXPORT exp
+  FILE "${CMAKE_BINARY_DIR}/lib/cmake/export2/export2-targets.cmake"
+  CXX_MODULES_DIRECTORY "cxx-modules")
+
+# Test multiple install exports.
+install(EXPORT exp
+  DESTINATION "lib/cmake/export1"
+  NAMESPACE export1::
+  CXX_MODULES_DIRECTORY "cxx-modules")
+install(EXPORT exp
+  DESTINATION "lib/cmake/export2"
+  CXX_MODULES_DIRECTORY "cxx-modules")

+ 34 - 0
Tests/RunCMake/CXXModules/NinjaDependInfoFileSet-check.cmake

@@ -0,0 +1,34 @@
+include("${CMAKE_CURRENT_LIST_DIR}/check-json.cmake")
+
+if (RunCMake_GENERATOR_IS_MULTI_CONFIG)
+  set(have_file 0)
+  foreach (config IN ITEMS Release Debug RelWithDebInfo MinSizeRel)
+    if (NOT EXISTS "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-file-sets-public.dir/${config}/CXXDependInfo.json")
+      continue ()
+    endif ()
+    set(have_file 1)
+
+    set(CMAKE_BUILD_TYPE "${config}")
+
+    file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-file-sets-public.dir/${config}/CXXDependInfo.json" actual_contents)
+    file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoFileSet-public.json" expect_contents)
+    check_json("${actual_contents}" "${expect_contents}")
+
+    file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-file-sets-private.dir/${config}/CXXDependInfo.json" actual_contents)
+    file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoFileSet-private.json" expect_contents)
+    check_json("${actual_contents}" "${expect_contents}")
+  endforeach ()
+
+  if (NOT have_file)
+    list(APPEND RunCMake_TEST_FAILED
+      "No recognized build configurations found.")
+  endif ()
+else ()
+  file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-file-sets-public.dir/CXXDependInfo.json" actual_contents)
+  file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoFileSet-public.json" expect_contents)
+  check_json("${actual_contents}" "${expect_contents}")
+
+  file(READ "${RunCMake_TEST_BINARY_DIR}/CMakeFiles/ninja-file-sets-private.dir/CXXDependInfo.json" actual_contents)
+  file(READ "${CMAKE_CURRENT_LIST_DIR}/expect/NinjaDependInfoFileSet-private.json" expect_contents)
+  check_json("${actual_contents}" "${expect_contents}")
+endif ()

+ 11 - 0
Tests/RunCMake/CXXModules/NinjaDependInfoFileSet-stderr.txt

@@ -0,0 +1,11 @@
+CMake Warning \(dev\) at NinjaDependInfoFileSet.cmake:14 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:6 \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 59 - 0
Tests/RunCMake/CXXModules/NinjaDependInfoFileSet.cmake

@@ -0,0 +1,59 @@
+# Fake out that we have dyndep; we only need to generate, not actually build
+# here.
+set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
+set(CMAKE_EXPERIMENTAL_CXX_SCANDEP_SOURCE "")
+
+enable_language(CXX)
+
+if (NOT CMAKE_GENERATOR MATCHES "Ninja")
+  message(FATAL_ERROR
+    "This test requires a 'Ninja' generator to be used.")
+endif ()
+
+add_library(ninja-file-sets-public)
+target_sources(ninja-file-sets-public
+  PRIVATE
+    sources/module-impl.cxx
+    sources/module-internal-part-impl.cxx
+    sources/module-part-impl.cxx
+    sources/module-use.cxx
+  PUBLIC
+    FILE_SET modules TYPE CXX_MODULES
+    BASE_DIRS
+      "${CMAKE_CURRENT_SOURCE_DIR}/sources"
+    FILES
+      sources/module.cxx
+      sources/module-part.cxx
+    FILE_SET internal_partitions TYPE CXX_MODULES FILES
+      sources/module-internal-part.cxx)
+target_compile_features(ninja-file-sets-public
+  PRIVATE
+    cxx_std_20)
+
+install(TARGETS ninja-file-sets-public
+  FILE_SET modules
+    DESTINATION "lib/cxx"
+    COMPONENT "modules"
+  FILE_SET internal_partitions
+    DESTINATION "lib/cxx/internals"
+    COMPONENT "modules-internal")
+
+add_library(ninja-file-sets-private)
+target_sources(ninja-file-sets-private
+  PRIVATE
+    sources/module-impl.cxx
+    sources/module-internal-part-impl.cxx
+    sources/module-part-impl.cxx
+    sources/module-use.cxx
+  PRIVATE
+    FILE_SET modules TYPE CXX_MODULES
+    BASE_DIRS
+      "${CMAKE_CURRENT_SOURCE_DIR}/sources"
+    FILES
+      sources/module.cxx
+      sources/module-part.cxx
+    FILE_SET internal_partitions TYPE CXX_MODULES FILES
+      sources/module-internal-part.cxx)
+target_compile_features(ninja-file-sets-private
+  PRIVATE
+    cxx_std_20)

+ 52 - 3
Tests/RunCMake/CXXModules/RunCMakeTest.cmake

@@ -60,12 +60,30 @@ foreach (fileset_type IN LISTS fileset_types)
   foreach (scope IN LISTS scopes)
     run_cmake("FileSet${fileset_type}${scope}")
   endforeach ()
+  run_cmake("FileSet${fileset_type}InterfaceImported")
 
   # Test the error message when a non-C++ source file is found in the source
   # list.
   run_cmake("NotCXXSource${fileset_type}")
 endforeach ()
 
+run_cmake(InstallBMI)
+run_cmake(InstallBMIGenericArgs)
+run_cmake(InstallBMIIgnore)
+
+run_cmake(ExportBuildCxxModules)
+run_cmake(ExportInstallCxxModules)
+
+# Generator-specific tests.
+if (RunCMake_GENERATOR MATCHES "Ninja")
+  run_cmake(NinjaDependInfoFileSet)
+  run_cmake(NinjaDependInfoExport)
+  run_cmake(NinjaDependInfoBMIInstall)
+else ()
+  message(FATAL_ERROR
+    "Please add 'DependInfo' tests for the '${RunCMake_GENERATOR}' generator.")
+endif ()
+
 # Actual compilation tests.
 if (NOT CMake_TEST_MODULE_COMPILATION)
   return ()
@@ -86,13 +104,23 @@ function (run_cxx_module_test directory)
     set(RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Debug)
   endif ()
 
-  set(RunCMake_TEST_OPTIONS
+  if (RunCMake_CXXModules_INSTALL)
+    set(prefix "${RunCMake_BINARY_DIR}/examples/${test_name}-install")
+    file(REMOVE_RECURSE "${prefix}")
+    list(APPEND RunCMake_TEST_OPTIONS
+      "-DCMAKE_INSTALL_PREFIX=${prefix}")
+  endif ()
+
+  list(APPEND RunCMake_TEST_OPTIONS
     "-DCMake_TEST_MODULE_COMPILATION_RULES=${CMake_TEST_MODULE_COMPILATION_RULES}"
     ${ARGN})
   run_cmake("examples/${test_name}")
   set(RunCMake_TEST_NO_CLEAN 1)
-  run_cmake_command("${test_name}-build" "${CMAKE_COMMAND}" --build . --config Debug)
-  run_cmake_command("${test_name}-test" "${CMAKE_CTEST_COMMAND}" -C Debug)
+  run_cmake_command("examples/${test_name}-build" "${CMAKE_COMMAND}" --build . --config Debug)
+  if (RunCMake_CXXModules_INSTALL)
+    run_cmake_command("examples/${test_name}-install" "${CMAKE_COMMAND}" --build . --target install --config Debug)
+  endif ()
+  run_cmake_command("examples/${test_name}-test" "${CMAKE_CTEST_COMMAND}" -C Debug --output-on-failure)
 endfunction ()
 
 string(REPLACE "," ";" CMake_TEST_MODULE_COMPILATION "${CMake_TEST_MODULE_COMPILATION}")
@@ -102,6 +130,7 @@ if ("named" IN_LIST CMake_TEST_MODULE_COMPILATION)
   run_cxx_module_test(simple)
   run_cxx_module_test(library library-static -DBUILD_SHARED_LIBS=OFF)
   run_cxx_module_test(generated)
+  run_cxx_module_test(public-req-private)
 endif ()
 
 # Tests which use named modules in shared libraries.
@@ -118,3 +147,23 @@ endif ()
 if ("internal_partitions" IN_LIST CMake_TEST_MODULE_COMPILATION)
   run_cxx_module_test(internal-partitions)
 endif ()
+
+# Tests which install BMIs
+if ("export_bmi" IN_LIST CMake_TEST_MODULE_COMPILATION)
+  run_cxx_module_test(export-interface-build)
+  run_cxx_module_test(export-bmi-and-interface-build)
+endif ()
+
+# All of the following tests perform installation.
+set(RunCMake_CXXModules_INSTALL 1)
+
+# Tests which install BMIs
+if ("install_bmi" IN_LIST CMake_TEST_MODULE_COMPILATION)
+  run_cxx_module_test(install-bmi)
+  run_cxx_module_test(install-bmi-and-interfaces)
+
+  if ("export_bmi" IN_LIST CMake_TEST_MODULE_COMPILATION)
+    run_cxx_module_test(export-interface-install)
+    run_cxx_module_test(export-bmi-and-interface-install)
+  endif ()
+endif ()

+ 160 - 0
Tests/RunCMake/CXXModules/check-json.cmake

@@ -0,0 +1,160 @@
+cmake_policy(PUSH)
+cmake_policy(SET CMP0057 NEW)
+
+function (json_placeholders in out)
+  string(REPLACE "<CONFIG>" "${CMAKE_BUILD_TYPE}" in "${in}")
+  if (RunCMake_GENERATOR_IS_MULTI_CONFIG)
+    string(REPLACE "<CONFIG_DIR>" "${CMAKE_BUILD_TYPE}/" in "${in}")
+  else ()
+    string(REPLACE "<CONFIG_DIR>" "" in "${in}")
+  endif ()
+  if (CMAKE_BUILD_TYPE)
+    string(REPLACE "<CONFIG_FORCE>" "${CMAKE_BUILD_TYPE}" in "${in}")
+  else ()
+    string(REPLACE "<CONFIG_FORCE>" "noconfig" in "${in}")
+  endif ()
+  string(REPLACE "<SOURCE_DIR>" "${RunCMake_SOURCE_DIR}" in "${in}")
+  string(REPLACE "<BINARY_DIR>" "${RunCMake_TEST_BINARY_DIR}" in "${in}")
+  set("${out}" "${in}" PARENT_SCOPE)
+endfunction ()
+
+function (check_json_value path actual_type expect_type actual_value expect_value)
+  if (NOT actual_type STREQUAL expect_type)
+    list(APPEND RunCMake_TEST_FAILED
+      "Type mismatch at ${path}: ${actual_type} vs. ${expect_type}")
+    return ()
+  endif ()
+
+  if (actual_type STREQUAL NULL)
+    # Nothing to check
+  elseif (actual_type STREQUAL BOOLEAN)
+    if (NOT actual_value STREQUAL expect_value)
+      list(APPEND RunCMake_TEST_FAILED
+        "Boolean mismatch at ${path}: ${actual_value} vs. ${expect_value}")
+    endif ()
+  elseif (actual_type STREQUAL NUMBER)
+    if (NOT actual_value EQUAL expect_value)
+      list(APPEND RunCMake_TEST_FAILED
+        "Number mismatch at ${path}: ${actual_value} vs. ${expect_value}")
+    endif ()
+  elseif (actual_type STREQUAL STRING)
+    # Allow some values to be ignored.
+    if (expect_value STREQUAL "<IGNORE>")
+      return ()
+    endif ()
+
+    json_placeholders("${expect_value}" expect_value_expanded)
+    if (NOT actual_value STREQUAL expect_value_expanded)
+      list(APPEND RunCMake_TEST_FAILED
+        "String mismatch at ${path}: ${actual_value} vs. ${expect_value_expanded}")
+    endif ()
+  elseif (actual_type STREQUAL ARRAY)
+    check_json_array("${path}" "${actual_value}" "${expect_value}")
+  elseif (actual_type STREQUAL OBJECT)
+    check_json_object("${path}" "${actual_value}" "${expect_value}")
+  endif ()
+endfunction ()
+
+# Check that two arrays are the same.
+function (check_json_array path actual expect)
+  string(JSON actual_len LENGTH "${actual}")
+  string(JSON expect_len LENGTH "${expect}")
+
+  set(iter_len "${actual_len}")
+  if (actual_len LESS expect_len)
+    list(APPEND RunCMake_TEST_FAILED
+      "Missing array items at ${path}")
+  elseif (expect_len LESS actual_len)
+    list(APPEND RunCMake_TEST_FAILED
+      "Extra array items at ${path}")
+    set(iter_len "${expect_len}")
+  endif ()
+
+  foreach (idx RANGE "${iter_len}")
+    if (idx EQUAL iter_len)
+      break ()
+    endif ()
+
+    set(new_path "${path}[${idx}]")
+    string(JSON actual_type TYPE "${actual}" "${idx}")
+    string(JSON expect_type TYPE "${expect}" "${idx}")
+    string(JSON actual_value GET "${actual}" "${idx}")
+    string(JSON expect_value GET "${expect}" "${idx}")
+    check_json_value("${new_path}" "${actual_type}" "${expect_type}" "${actual_value}" "${expect_value}")
+  endforeach ()
+endfunction ()
+
+# Check that two inner objects are the same.
+function (check_json_object path actual expect)
+  string(JSON actual_len LENGTH "${actual}")
+  string(JSON expect_len LENGTH "${expect}")
+
+  set(actual_keys "")
+  set(expect_keys "")
+  foreach (idx RANGE "${actual_len}")
+    if (idx EQUAL actual_len)
+      break ()
+    endif ()
+
+    string(JSON actual_key MEMBER "${actual}" "${idx}")
+    list(APPEND actual_keys "${actual_key}")
+  endforeach ()
+  foreach (idx RANGE "${expect_len}")
+    if (idx EQUAL expect_len)
+      break ()
+    endif ()
+
+    string(JSON expect_key MEMBER "${expect}" "${idx}")
+    list(APPEND expect_keys "${expect_key}")
+  endforeach ()
+
+  json_placeholders("${expect_keys}" expect_keys_expanded)
+
+  set(actual_keys_missed "${actual_keys}")
+  set(expect_keys_missed "${expect_keys}")
+
+  set(common_keys "")
+  set(expect_keys_stack "${expect_keys}")
+  while (expect_keys_stack)
+    list(POP_BACK expect_keys_stack expect_key)
+    json_placeholders("${expect_key}" expect_key_expanded)
+
+    if (expect_key_expanded IN_LIST actual_keys_missed AND
+        expect_key IN_LIST expect_keys_missed)
+      list(APPEND common_keys "${expect_key}")
+    endif ()
+
+    list(REMOVE_ITEM actual_keys_missed "${expect_key_expanded}")
+    list(REMOVE_ITEM expect_keys_missed "${expect_key}")
+  endwhile ()
+
+  if (actual_keys_missed)
+    string(REPLACE ";" ", " actual_keys_missed_text "${actual_keys_missed}")
+    list(APPEND RunCMake_TEST_FAILED
+      "Missing expected members at ${path}: ${actual_keys_missed_text}")
+  endif ()
+  if (expect_keys_missed)
+    string(REPLACE ";" ", " expect_keys_missed_text "${expect_keys_missed}")
+    list(APPEND RunCMake_TEST_FAILED
+      "Extra unexpected members at ${path}: ${expect_keys_missed_text}")
+  endif ()
+
+  foreach (key IN LISTS common_keys)
+    json_placeholders("${key}" key_expanded)
+    set(new_path "${path}.${key_expanded}")
+    string(JSON actual_type TYPE "${actual}" "${key_expanded}")
+    string(JSON expect_type TYPE "${expect}" "${key}")
+    string(JSON actual_value GET "${actual}" "${key_expanded}")
+    string(JSON expect_value GET "${expect}" "${key}")
+    check_json_value("${new_path}" "${actual_type}" "${expect_type}" "${actual_value}" "${expect_value}")
+  endforeach ()
+endfunction ()
+
+# Check that two JSON objects are the same.
+function (check_json actual expect)
+  check_json_object("" "${actual}" "${expect}")
+endfunction ()
+
+string(REPLACE ";" "; " RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}")
+
+cmake_policy(POP)

+ 22 - 0
Tests/RunCMake/CXXModules/examples/cxx-modules-find-bmi-and-interfaces.cmake

@@ -0,0 +1,22 @@
+function (check_for_bmi prefix destination name)
+  set(found 0)
+  foreach (ext IN ITEMS gcm)
+    if (EXISTS "${prefix}/${destination}/${name}.${ext}")
+      set(found 1)
+      break ()
+    endif ()
+  endforeach ()
+
+  if (NOT found)
+    message(SEND_ERROR
+      "Failed to find the ${name} BMI")
+  endif ()
+endfunction ()
+
+function (check_for_interface prefix destination subdir name)
+  set(found 0)
+  if (NOT EXISTS "${prefix}/${destination}/${subdir}/${name}")
+    message(SEND_ERROR
+      "Failed to find the ${name} module interface")
+  endif ()
+endfunction ()

+ 27 - 0
Tests/RunCMake/CXXModules/examples/cxx-modules-find-bmi.cmake

@@ -0,0 +1,27 @@
+function (check_for_bmi prefix destination name)
+  set(found 0)
+  foreach (ext IN ITEMS gcm)
+    if (EXISTS "${prefix}/${destination}/${name}.${ext}")
+      set(found 1)
+      break ()
+    endif ()
+  endforeach ()
+
+  if (NOT found)
+    message(SEND_ERROR
+      "Failed to find the ${name} BMI")
+  endif ()
+endfunction ()
+
+function (check_for_interface prefix destination subdir name)
+  set(found 0)
+  if (NOT EXISTS "${prefix}/${destination}/${subdir}/${name}")
+    message(SEND_ERROR
+      "Failed to find the ${name} module interface")
+  endif ()
+endfunction ()
+
+function (report_dirs prefix destination)
+  message("prefix: ${prefix}")
+  message("destination: ${destination}")
+endfunction ()

+ 1 - 1
Tests/RunCMake/CXXModules/examples/cxx-modules-rules.cmake

@@ -1,4 +1,4 @@
-set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "17be90bd-a850-44e0-be50-448de847d652")
+set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "3c375311-a3c9-4396-a187-3227ef642046")
 
 if (NOT EXISTS "${CMake_TEST_MODULE_COMPILATION_RULES}")
   message(FATAL_ERROR

+ 9 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build-stderr.txt

@@ -0,0 +1,9 @@
+CMake Warning \(dev\) at CMakeLists.txt:7 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 56 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build/CMakeLists.txt

@@ -0,0 +1,56 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_export_bmi_and_interfaces CXX)
+
+include("${CMAKE_SOURCE_DIR}/../cxx-modules-rules.cmake")
+
+add_library(export_bmi_and_interfaces STATIC)
+target_sources(export_bmi_and_interfaces
+  PRIVATE
+    forward.cxx
+  PRIVATE
+    FILE_SET modules_private TYPE CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        private.cxx
+  PUBLIC
+    FILE_SET modules TYPE CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        importable.cxx)
+target_compile_features(export_bmi_and_interfaces PUBLIC cxx_std_20)
+
+install(TARGETS export_bmi_and_interfaces
+  EXPORT CXXModules
+  FILE_SET modules DESTINATION "lib/cxx/miu"
+  CXX_MODULES_BMI DESTINATION "lib/cxx/bmi")
+export(EXPORT CXXModules
+  NAMESPACE CXXModules::
+  FILE "${CMAKE_CURRENT_BINARY_DIR}/export_bmi_and_interfaces-targets.cmake"
+  CXX_MODULES_DIRECTORY "export_bmi_and_interfaces-cxx-modules")
+file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/export_bmi_and_interfaces-config.cmake"
+  "include(\"\${CMAKE_CURRENT_LIST_DIR}/export_bmi_and_interfaces-targets.cmake\")
+set(\${CMAKE_FIND_PACKAGE_NAME}_FOUND 1)
+")
+
+set(generator
+  -G "${CMAKE_GENERATOR}")
+if (CMAKE_GENERATOR_TOOLSET)
+  list(APPEND generator
+    -T "${CMAKE_GENERATOR_TOOLSET}")
+endif ()
+if (CMAKE_GENERATOR_PLATFORM)
+  list(APPEND generator
+    -A "${CMAKE_GENERATOR_PLATFORM}")
+endif ()
+
+add_test(NAME export_bmi_and_interfaces_build
+  COMMAND
+    "${CMAKE_COMMAND}"
+    "-Dexpected_source_dir=${CMAKE_CURRENT_SOURCE_DIR}"
+    "-Dexpected_binary_dir=${CMAKE_CURRENT_BINARY_DIR}"
+    "-Dexport_bmi_and_interfaces_DIR=${CMAKE_CURRENT_BINARY_DIR}"
+    ${generator}
+    -S "${CMAKE_CURRENT_SOURCE_DIR}/test"
+    -B "${CMAKE_CURRENT_BINARY_DIR}/test")

+ 6 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build/forward.cxx

@@ -0,0 +1,6 @@
+import priv;
+
+int forwarding()
+{
+  return from_private();
+}

+ 8 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build/importable.cxx

@@ -0,0 +1,8 @@
+export module importable;
+
+int forwarding();
+
+export int from_import()
+{
+  return forwarding();
+}

+ 6 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build/private.cxx

@@ -0,0 +1,6 @@
+export module priv;
+
+export int from_private()
+{
+  return 0;
+}

+ 32 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-build/test/CMakeLists.txt

@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_library NONE)
+
+set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "3c375311-a3c9-4396-a187-3227ef642046")
+
+find_package(export_bmi_and_interfaces REQUIRED)
+
+if (NOT TARGET CXXModules::export_bmi_and_interfaces)
+  message(FATAL_ERROR
+    "Missing imported target")
+endif ()
+
+get_property(file_sets TARGET CXXModules::export_bmi_and_interfaces
+  PROPERTY INTERFACE_CXX_MODULE_SETS)
+if (NOT file_sets STREQUAL "modules")
+  message(FATAL_ERROR
+    "Incorrect exported file sets in `CXXModules::export_bmi_and_interfaces`: `${file_sets}`")
+endif ()
+
+get_property(file_set_files TARGET CXXModules::export_bmi_and_interfaces
+  PROPERTY CXX_MODULE_SET_modules)
+if (NOT file_set_files STREQUAL "${expected_source_dir}/importable.cxx")
+  message(FATAL_ERROR
+    "Incorrect exported file set paths in CXXModules::export_bmi_and_interfaces`: `${file_set_files}`")
+endif ()
+
+get_property(imported_modules TARGET CXXModules::export_bmi_and_interfaces
+  PROPERTY IMPORTED_CXX_MODULES_DEBUG)
+if (NOT imported_modules MATCHES "importable=${expected_source_dir}/importable.cxx,${expected_binary_dir}/CMakeFiles/export_bmi_and_interfaces.dir(/Debug)?/importable.(gcm|pcm|ifc)")
+  message(FATAL_ERROR
+    "Incorrect exported modules in CXXModules::export_bmi_and_interfaces`: `${imported_modules}`")
+endif ()

+ 9 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install-stderr.txt

@@ -0,0 +1,9 @@
+CMake Warning \(dev\) at CMakeLists.txt:7 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 59 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install/CMakeLists.txt

@@ -0,0 +1,59 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_export_bmi_and_interfaces CXX)
+
+include("${CMAKE_SOURCE_DIR}/../cxx-modules-rules.cmake")
+
+add_library(export_bmi_and_interfaces STATIC)
+target_sources(export_bmi_and_interfaces
+  PRIVATE
+    forward.cxx
+  PRIVATE
+    FILE_SET modules_private TYPE CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        private.cxx
+  PUBLIC
+    FILE_SET modules TYPE CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        importable.cxx)
+target_compile_features(export_bmi_and_interfaces PUBLIC cxx_std_20)
+
+install(TARGETS export_bmi_and_interfaces
+  EXPORT CXXModules
+  FILE_SET modules DESTINATION "lib/cxx/miu"
+  CXX_MODULES_BMI DESTINATION "lib/cxx/bmi")
+install(EXPORT CXXModules
+  NAMESPACE CXXModules::
+  DESTINATION "lib/cmake/export_bmi_and_interfaces"
+  FILE "export_bmi_and_interfaces-targets.cmake"
+  CXX_MODULES_DIRECTORY "export_bmi_and_interfaces-cxx-modules")
+file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/export_bmi_and_interfaces-config.cmake"
+  "include(\"\${CMAKE_CURRENT_LIST_DIR}/export_bmi_and_interfaces-targets.cmake\")
+set(\${CMAKE_FIND_PACKAGE_NAME}_FOUND 1)
+")
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/export_bmi_and_interfaces-config.cmake"
+  DESTINATION "lib/cmake/export_bmi_and_interfaces")
+
+set(generator
+  -G "${CMAKE_GENERATOR}")
+if (CMAKE_GENERATOR_TOOLSET)
+  list(APPEND generator
+    -T "${CMAKE_GENERATOR_TOOLSET}")
+endif ()
+if (CMAKE_GENERATOR_PLATFORM)
+  list(APPEND generator
+    -A "${CMAKE_GENERATOR_PLATFORM}")
+endif ()
+
+add_test(NAME export_bmi_and_interfaces_build
+  COMMAND
+    "${CMAKE_COMMAND}"
+    "-Dexpected_source_dir=${CMAKE_INSTALL_PREFIX}/lib/cxx/miu"
+    "-Dexpected_binary_dir=${CMAKE_INSTALL_PREFIX}/lib/cxx/bmi"
+    "-Dexport_bmi_and_interfaces_DIR=${CMAKE_INSTALL_PREFIX}/lib/cmake/export_bmi_and_interfaces"
+    ${generator}
+    -S "${CMAKE_CURRENT_SOURCE_DIR}/test"
+    -B "${CMAKE_CURRENT_BINARY_DIR}/test")

+ 6 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install/forward.cxx

@@ -0,0 +1,6 @@
+import priv;
+
+int forwarding()
+{
+  return from_private();
+}

+ 8 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install/importable.cxx

@@ -0,0 +1,8 @@
+export module importable;
+
+int forwarding();
+
+export int from_import()
+{
+  return forwarding();
+}

+ 6 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install/private.cxx

@@ -0,0 +1,6 @@
+export module priv;
+
+export int from_private()
+{
+  return 0;
+}

+ 32 - 0
Tests/RunCMake/CXXModules/examples/export-bmi-and-interface-install/test/CMakeLists.txt

@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_library NONE)
+
+set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "3c375311-a3c9-4396-a187-3227ef642046")
+
+find_package(export_bmi_and_interfaces REQUIRED)
+
+if (NOT TARGET CXXModules::export_bmi_and_interfaces)
+  message(FATAL_ERROR
+    "Missing imported target")
+endif ()
+
+get_property(file_sets TARGET CXXModules::export_bmi_and_interfaces
+  PROPERTY INTERFACE_CXX_MODULE_SETS)
+if (NOT file_sets STREQUAL "modules")
+  message(FATAL_ERROR
+    "Incorrect exported file sets in `CXXModules::export_bmi_and_interfaces`: `${file_sets}`")
+endif ()
+
+get_property(file_set_files TARGET CXXModules::export_bmi_and_interfaces
+  PROPERTY CXX_MODULE_SET_modules)
+if (NOT file_set_files STREQUAL "${expected_source_dir}/importable.cxx")
+  message(FATAL_ERROR
+    "Incorrect exported file set paths in CXXModules::export_bmi_and_interfaces`: `${file_set_files}`")
+endif ()
+
+get_property(imported_modules TARGET CXXModules::export_bmi_and_interfaces
+  PROPERTY IMPORTED_CXX_MODULES_DEBUG)
+if (NOT imported_modules MATCHES "importable=${expected_source_dir}/importable.cxx,${expected_binary_dir}/importable.(gcm|pcm|ifc)")
+  message(FATAL_ERROR
+    "Incorrect exported modules in CXXModules::export_bmi_and_interfaces`: `${imported_modules}`")
+endif ()

+ 9 - 0
Tests/RunCMake/CXXModules/examples/export-interface-build-stderr.txt

@@ -0,0 +1,9 @@
+CMake Warning \(dev\) at CMakeLists.txt:7 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 53 - 0
Tests/RunCMake/CXXModules/examples/export-interface-build/CMakeLists.txt

@@ -0,0 +1,53 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_export_interfaces CXX)
+
+include("${CMAKE_SOURCE_DIR}/../cxx-modules-rules.cmake")
+
+add_library(export_interfaces STATIC)
+target_sources(export_interfaces
+  PRIVATE
+    forward.cxx
+  PRIVATE
+    FILE_SET modules_private TYPE CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        private.cxx
+  PUBLIC
+    FILE_SET modules TYPE CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        importable.cxx)
+target_compile_features(export_interfaces PUBLIC cxx_std_20)
+
+install(TARGETS export_interfaces
+  EXPORT CXXModules
+  FILE_SET modules DESTINATION "lib/cxx/miu")
+export(EXPORT CXXModules
+  NAMESPACE CXXModules::
+  FILE "${CMAKE_CURRENT_BINARY_DIR}/export_interfaces-targets.cmake")
+file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/export_interfaces-config.cmake"
+  "include(\"\${CMAKE_CURRENT_LIST_DIR}/export_interfaces-targets.cmake\")
+set(\${CMAKE_FIND_PACKAGE_NAME}_FOUND 1)
+")
+
+set(generator
+  -G "${CMAKE_GENERATOR}")
+if (CMAKE_GENERATOR_TOOLSET)
+  list(APPEND generator
+    -T "${CMAKE_GENERATOR_TOOLSET}")
+endif ()
+if (CMAKE_GENERATOR_PLATFORM)
+  list(APPEND generator
+    -A "${CMAKE_GENERATOR_PLATFORM}")
+endif ()
+
+add_test(NAME export_interfaces_build
+  COMMAND
+    "${CMAKE_COMMAND}"
+    "-Dexpected_dir=${CMAKE_CURRENT_SOURCE_DIR}"
+    "-Dexport_interfaces_DIR=${CMAKE_CURRENT_BINARY_DIR}"
+    ${generator}
+    -S "${CMAKE_CURRENT_SOURCE_DIR}/test"
+    -B "${CMAKE_CURRENT_BINARY_DIR}/test")

+ 6 - 0
Tests/RunCMake/CXXModules/examples/export-interface-build/forward.cxx

@@ -0,0 +1,6 @@
+import priv;
+
+int forwarding()
+{
+  return from_private();
+}

+ 8 - 0
Tests/RunCMake/CXXModules/examples/export-interface-build/importable.cxx

@@ -0,0 +1,8 @@
+export module importable;
+
+int forwarding();
+
+export int from_import()
+{
+  return forwarding();
+}

+ 6 - 0
Tests/RunCMake/CXXModules/examples/export-interface-build/private.cxx

@@ -0,0 +1,6 @@
+export module priv;
+
+export int from_private()
+{
+  return 0;
+}

+ 32 - 0
Tests/RunCMake/CXXModules/examples/export-interface-build/test/CMakeLists.txt

@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_library NONE)
+
+set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "3c375311-a3c9-4396-a187-3227ef642046")
+
+find_package(export_interfaces REQUIRED)
+
+if (NOT TARGET CXXModules::export_interfaces)
+  message(FATAL_ERROR
+    "Missing imported target")
+endif ()
+
+get_property(file_sets TARGET CXXModules::export_interfaces
+  PROPERTY INTERFACE_CXX_MODULE_SETS)
+if (NOT file_sets STREQUAL "modules")
+  message(FATAL_ERROR
+    "Incorrect exported file sets in `CXXModules::export_interfaces`: `${file_sets}`")
+endif ()
+
+get_property(file_set_files TARGET CXXModules::export_interfaces
+  PROPERTY CXX_MODULE_SET_modules)
+if (NOT file_set_files STREQUAL "${expected_dir}/importable.cxx")
+  message(FATAL_ERROR
+    "Incorrect exported file set paths in CXXModules::export_interfaces`: `${file_set_files}`")
+endif ()
+
+get_property(imported_modules_set TARGET CXXModules::export_interfaces
+  PROPERTY IMPORTED_CXX_MODULES_DEBUG SET)
+if (imported_modules_set)
+  message(FATAL_ERROR
+    "Unexpected C++ modules specified.")
+endif ()

+ 9 - 0
Tests/RunCMake/CXXModules/examples/export-interface-install-stderr.txt

@@ -0,0 +1,9 @@
+CMake Warning \(dev\) at CMakeLists.txt:7 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 56 - 0
Tests/RunCMake/CXXModules/examples/export-interface-install/CMakeLists.txt

@@ -0,0 +1,56 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_export_interfaces CXX)
+
+include("${CMAKE_SOURCE_DIR}/../cxx-modules-rules.cmake")
+
+add_library(export_interfaces STATIC)
+target_sources(export_interfaces
+  PRIVATE
+    forward.cxx
+  PRIVATE
+    FILE_SET modules_private TYPE CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        private.cxx
+  PUBLIC
+    FILE_SET modules TYPE CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        importable.cxx)
+target_compile_features(export_interfaces PUBLIC cxx_std_20)
+
+install(TARGETS export_interfaces
+  EXPORT CXXModules
+  FILE_SET modules DESTINATION "lib/cxx/miu")
+install(EXPORT CXXModules
+  NAMESPACE CXXModules::
+  DESTINATION "lib/cmake/export_interfaces"
+  FILE "export_interfaces-targets.cmake")
+file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/export_interfaces-config.cmake"
+  "include(\"\${CMAKE_CURRENT_LIST_DIR}/export_interfaces-targets.cmake\")
+set(\${CMAKE_FIND_PACKAGE_NAME}_FOUND 1)
+")
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/export_interfaces-config.cmake"
+  DESTINATION "lib/cmake/export_interfaces")
+
+set(generator
+  -G "${CMAKE_GENERATOR}")
+if (CMAKE_GENERATOR_TOOLSET)
+  list(APPEND generator
+    -T "${CMAKE_GENERATOR_TOOLSET}")
+endif ()
+if (CMAKE_GENERATOR_PLATFORM)
+  list(APPEND generator
+    -A "${CMAKE_GENERATOR_PLATFORM}")
+endif ()
+
+add_test(NAME export_interfaces_build
+  COMMAND
+    "${CMAKE_COMMAND}"
+    "-Dexpected_dir=${CMAKE_INSTALL_PREFIX}/lib/cxx/miu"
+    "-Dexport_interfaces_DIR=${CMAKE_INSTALL_PREFIX}/lib/cmake/export_interfaces"
+    ${generator}
+    -S "${CMAKE_CURRENT_SOURCE_DIR}/test"
+    -B "${CMAKE_CURRENT_BINARY_DIR}/test")

+ 6 - 0
Tests/RunCMake/CXXModules/examples/export-interface-install/forward.cxx

@@ -0,0 +1,6 @@
+import priv;
+
+int forwarding()
+{
+  return from_private();
+}

+ 8 - 0
Tests/RunCMake/CXXModules/examples/export-interface-install/importable.cxx

@@ -0,0 +1,8 @@
+export module importable;
+
+int forwarding();
+
+export int from_import()
+{
+  return forwarding();
+}

+ 6 - 0
Tests/RunCMake/CXXModules/examples/export-interface-install/private.cxx

@@ -0,0 +1,6 @@
+export module priv;
+
+export int from_private()
+{
+  return 0;
+}

+ 32 - 0
Tests/RunCMake/CXXModules/examples/export-interface-install/test/CMakeLists.txt

@@ -0,0 +1,32 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_library NONE)
+
+set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "3c375311-a3c9-4396-a187-3227ef642046")
+
+find_package(export_interfaces REQUIRED)
+
+if (NOT TARGET CXXModules::export_interfaces)
+  message(FATAL_ERROR
+    "Missing imported target")
+endif ()
+
+get_property(file_sets TARGET CXXModules::export_interfaces
+  PROPERTY INTERFACE_CXX_MODULE_SETS)
+if (NOT file_sets STREQUAL "modules")
+  message(FATAL_ERROR
+    "Incorrect exported file sets in `CXXModules::export_interfaces`: `${file_sets}`")
+endif ()
+
+get_property(file_set_files TARGET CXXModules::export_interfaces
+  PROPERTY CXX_MODULE_SET_modules)
+if (NOT file_set_files STREQUAL "${expected_dir}/importable.cxx")
+  message(FATAL_ERROR
+    "Incorrect exported file set paths in CXXModules::export_interfaces`: `${file_set_files}`")
+endif ()
+
+get_property(imported_modules_set TARGET CXXModules::export_interfaces
+  PROPERTY IMPORTED_CXX_MODULES_DEBUG SET)
+if (imported_modules_set)
+  message(FATAL_ERROR
+    "Unexpected C++ modules specified.")
+endif ()

+ 9 - 0
Tests/RunCMake/CXXModules/examples/install-bmi-and-interfaces-stderr.txt

@@ -0,0 +1,9 @@
+CMake Warning \(dev\) at CMakeLists.txt:7 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 27 - 0
Tests/RunCMake/CXXModules/examples/install-bmi-and-interfaces/CMakeLists.txt

@@ -0,0 +1,27 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_install_bmi_and_interfaces CXX)
+
+include("${CMAKE_SOURCE_DIR}/../cxx-modules-rules.cmake")
+
+add_library(install_bmi_and_interfaces STATIC)
+target_sources(install_bmi_and_interfaces
+  PUBLIC
+    FILE_SET CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        importable.cxx)
+target_compile_features(install_bmi_and_interfaces PUBLIC cxx_std_20)
+
+install(TARGETS install_bmi_and_interfaces
+  ARCHIVE DESTINATION "lib"
+  CXX_MODULES_BMI DESTINATION "lib/cxx/bmi"
+  FILE_SET CXX_MODULES DESTINATION "lib/cxx/miu")
+
+add_test(NAME check-for-bmi
+  COMMAND
+    "${CMAKE_COMMAND}"
+      "-Dprefix=${CMAKE_INSTALL_PREFIX}"
+      "-Dbmi_destination=lib/cxx/bmi"
+      "-Dfs_destination=lib/cxx/miu"
+      -P "${CMAKE_CURRENT_SOURCE_DIR}/check-for-bmi.cmake")

+ 7 - 0
Tests/RunCMake/CXXModules/examples/install-bmi-and-interfaces/check-for-bmi.cmake

@@ -0,0 +1,7 @@
+include("${CMAKE_CURRENT_LIST_DIR}/../cxx-modules-find-bmi.cmake")
+
+report_dirs("${prefix}" "${bmi_destination}")
+check_for_bmi("${prefix}" "${bmi_destination}" importable)
+
+report_dirs("${prefix}" "${fs_destination}")
+check_for_interface("${prefix}" "${fs_destination}" "" importable.cxx)

+ 6 - 0
Tests/RunCMake/CXXModules/examples/install-bmi-and-interfaces/importable.cxx

@@ -0,0 +1,6 @@
+export module importable;
+
+export int from_import()
+{
+  return 0;
+}

+ 9 - 0
Tests/RunCMake/CXXModules/examples/install-bmi-stderr.txt

@@ -0,0 +1,9 @@
+CMake Warning \(dev\) at CMakeLists.txt:7 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 25 - 0
Tests/RunCMake/CXXModules/examples/install-bmi/CMakeLists.txt

@@ -0,0 +1,25 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_install_bmi CXX)
+
+include("${CMAKE_SOURCE_DIR}/../cxx-modules-rules.cmake")
+
+add_library(install_bmi STATIC)
+target_sources(install_bmi
+  PUBLIC
+    FILE_SET CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        importable.cxx)
+target_compile_features(install_bmi PUBLIC cxx_std_20)
+
+install(TARGETS install_bmi
+  ARCHIVE DESTINATION "lib"
+  CXX_MODULES_BMI DESTINATION "lib/cxx/bmi")
+
+add_test(NAME check-for-bmi
+  COMMAND
+    "${CMAKE_COMMAND}"
+      "-Dprefix=${CMAKE_INSTALL_PREFIX}"
+      "-Ddestination=lib/cxx/bmi"
+      -P "${CMAKE_CURRENT_SOURCE_DIR}/check-for-bmi.cmake")

+ 4 - 0
Tests/RunCMake/CXXModules/examples/install-bmi/check-for-bmi.cmake

@@ -0,0 +1,4 @@
+include("${CMAKE_CURRENT_LIST_DIR}/../cxx-modules-find-bmi.cmake")
+
+report_dirs("${prefix}" "${destination}")
+check_for_bmi("${prefix}" "${destination}" importable)

+ 6 - 0
Tests/RunCMake/CXXModules/examples/install-bmi/importable.cxx

@@ -0,0 +1,6 @@
+export module importable;
+
+export int from_import()
+{
+  return 0;
+}

+ 1 - 0
Tests/RunCMake/CXXModules/examples/public-req-private-build-result.txt

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

+ 1 - 0
Tests/RunCMake/CXXModules/examples/public-req-private-build-stdout.txt

@@ -0,0 +1 @@
+CMake Error: Public C\+\+ module source `.*/Tests/RunCMake/CXXModules/examples/public-req-private/pub.cxx` requires the `priv` C\+\+ module which is provided by a private source

+ 9 - 0
Tests/RunCMake/CXXModules/examples/public-req-private-stderr.txt

@@ -0,0 +1,9 @@
+CMake Warning \(dev\) at CMakeLists.txt:7 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 22 - 0
Tests/RunCMake/CXXModules/examples/public-req-private/CMakeLists.txt

@@ -0,0 +1,22 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_public_req_private CXX)
+
+include("${CMAKE_SOURCE_DIR}/../cxx-modules-rules.cmake")
+
+add_library(public_req_private)
+target_sources(public_req_private
+  PRIVATE
+    FILE_SET private TYPE CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        priv.cxx
+  PUBLIC
+    FILE_SET public TYPE CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        pub.cxx)
+target_compile_features(public_req_private PUBLIC cxx_std_20)
+
+add_test(NAME cmake-version COMMAND "${CMAKE_COMMAND}" --version)

+ 6 - 0
Tests/RunCMake/CXXModules/examples/public-req-private/priv.cxx

@@ -0,0 +1,6 @@
+export module priv;
+
+export int g()
+{
+  return 0;
+}

+ 8 - 0
Tests/RunCMake/CXXModules/examples/public-req-private/pub.cxx

@@ -0,0 +1,8 @@
+export module pub;
+
+import priv;
+
+export int f()
+{
+  return g();
+}

+ 45 - 0
Tests/RunCMake/CXXModules/expect/NinjaDependInfoBMIInstall-private.json

@@ -0,0 +1,45 @@
+{
+  "bmi-installation": {
+    "destination": "lib/cxx/modules/private/<CONFIG>",
+    "message-level": "MESSAGE_LAZY",
+    "permissions": " OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ",
+    "script-location": "<BINARY_DIR>/CMakeFiles/ninja-bmi-install-private.dir/install-cxx-module-bmi-<CONFIG_FORCE>.cmake"
+  },
+  "compiler-id": "<IGNORE>",
+  "config": "<CONFIG>",
+  "cxx-modules": {
+    "CMakeFiles/ninja-bmi-install-private.dir/sources/module-internal-part.cxx.o": {
+      "destination": null,
+      "name": "internal_partitions",
+      "relative-directory": "sources",
+      "source": "<SOURCE_DIR>/sources/module-internal-part.cxx",
+      "type": "CXX_MODULES",
+      "visibility": "PRIVATE"
+    },
+    "CMakeFiles/ninja-bmi-install-private.dir/sources/module-part.cxx.o": {
+      "destination": null,
+      "name": "modules",
+      "relative-directory": "",
+      "source": "<SOURCE_DIR>/sources/module-part.cxx",
+      "type": "CXX_MODULES",
+      "visibility": "PRIVATE"
+    },
+    "CMakeFiles/ninja-bmi-install-private.dir/sources/module.cxx.o": {
+      "destination": null,
+      "name": "modules",
+      "relative-directory": "",
+      "source": "<SOURCE_DIR>/sources/module.cxx",
+      "type": "CXX_MODULES",
+      "visibility": "PRIVATE"
+    }
+  },
+  "dir-cur-bld": "<BINARY_DIR>",
+  "dir-cur-src": "<SOURCE_DIR>",
+  "dir-top-bld": "<BINARY_DIR>",
+  "dir-top-src": "<SOURCE_DIR>",
+  "exports": [],
+  "include-dirs": [],
+  "language": "CXX",
+  "linked-target-dirs": [],
+  "module-dir": "<BINARY_DIR>/CMakeFiles/ninja-bmi-install-private.dir"
+}

Some files were not shown because too many files changed in this diff