Browse Source

Merge topic 'cpp-named-module-support-msvc'

4f95e6b284 ci: test BMI exporting and installation with GCC in CI
c49d5f137b RunCMake/CXXModules: add a "deep-chain" test
297e0f4dce cmCxxModuleMapper: support MSVC module map format
b3c2880cb2 cmCxxModuleMapper: track transitive modules for MSVC
a43713d615 CTestCustom: ignore `cm::optional` uninitialized memory false positive
b90de0b492 RunCMake/CXXModules: support MSVC extensions
a84c186a7d cmScanDepFormat: support the MSVC 17.3 toolchain temporarily
d7f5064ff7 cmScanDepFormat: support P1689R5
...

Acked-by: Kitware Robot <[email protected]>
Merge-request: !7481
Brad King 3 years ago
parent
commit
e2112b3778

+ 1 - 1
.gitlab/ci/configure_linux_gcc_cxx_modules_ninja.cmake

@@ -1,4 +1,4 @@
-set(CMake_TEST_MODULE_COMPILATION "named,partitions,internal_partitions" CACHE STRING "")
+set(CMake_TEST_MODULE_COMPILATION "named,partitions,internal_partitions,export_bmi,install_bmi" CACHE STRING "")
 set(CMake_TEST_MODULE_COMPILATION_RULES "${CMAKE_CURRENT_LIST_DIR}/cxx_modules_rules_gcc.cmake" CACHE STRING "")
 
 include("${CMAKE_CURRENT_LIST_DIR}/configure_external_test.cmake")

+ 1 - 1
.gitlab/ci/configure_linux_gcc_cxx_modules_ninja_multi.cmake

@@ -1,4 +1,4 @@
-set(CMake_TEST_MODULE_COMPILATION "named,partitions,internal_partitions" CACHE STRING "")
+set(CMake_TEST_MODULE_COMPILATION "named,partitions,internal_partitions,export_bmi,install_bmi" CACHE STRING "")
 set(CMake_TEST_MODULE_COMPILATION_RULES "${CMAKE_CURRENT_LIST_DIR}/cxx_modules_rules_gcc.cmake" CACHE STRING "")
 
 include("${CMAKE_CURRENT_LIST_DIR}/configure_external_test.cmake")

+ 5 - 0
CTestCustom.cmake.in

@@ -84,6 +84,11 @@ list(APPEND CTEST_CUSTOM_WARNING_EXCEPTION
   "compilation completed with warnings" # PGI
   "[0-9]+ Warning\\(s\\) detected" # SunPro
 
+  # Ignore false positive on `cm::optional` usage from GCC
+  "cmGlobalNinjaGenerator.cxx:[0-9]*:[0-9]*: warning: '.*cm::optional<CxxModuleMapFormat>::_mem\\)\\)' may be used uninitialized \\[-Wmaybe-uninitialized\\]"
+  "cmGlobalNinjaGenerator.cxx:[0-9]*:[0-9]*: note: '.*cm::optional<CxxModuleMapFormat>::_mem\\)\\)' was declared here"
+  "cmGlobalNinjaGenerator.cxx:[0-9]*:[0-9]*: warning: '\\*\\(\\(void\\*\\)& modmap_fmt \\+4\\)' may be used uninitialized in this function \\[-Wmaybe-uninitialized\\]"
+
   # clang-analyzer exceptions
   "cmListFileLexer.c:[0-9]+:[0-9]+: warning: Array subscript is undefined"
   "jsoncpp/src/.*:[0-9]+:[0-9]+: warning: Value stored to .* is never read"

+ 2 - 2
Help/dev/experimental.rst

@@ -58,7 +58,7 @@ dependencies to the file specified by the ``<DYNDEP_FILE>`` placeholder. The
 for scandep rules which use ``msvc``-style dependency reporting.
 
 The module dependencies should be written in the format described
-by the `P1689r4`_ paper.
+by the `P1689r5`_ paper.
 
 Compiler writers may try out their scanning functionality using
 the `cxx-modules-sandbox`_ test project, modified to set variables
@@ -85,5 +85,5 @@ the GCC documentation, but the relevant section for the purposes of CMake is:
     -- GCC module mapper documentation
 
 .. _`D1483r1`: https://mathstuf.fedorapeople.org/fortran-modules/fortran-modules.html
-.. _`P1689r4`: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1689r4.html
+.. _`P1689r5`: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1689r5.html
 .. _`cxx-modules-sandbox`: https://github.com/mathstuf/cxx-modules-sandbox

+ 6 - 0
Help/release/dev/p1689r5.rst

@@ -0,0 +1,6 @@
+p1689r5
+-------
+
+* C++ module scanning now supports the latest revision, `P1689R5`_.
+
+.. _`P1689r5`: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1689r5.html

+ 233 - 1
Source/cmCxxModuleMapper.cxx

@@ -3,10 +3,19 @@
 #include "cmCxxModuleMapper.h"
 
 #include <cassert>
+#include <cstddef>
+#include <set>
 #include <sstream>
+#include <string>
+#include <utility>
 #include <vector>
 
+#include <cm/string_view>
+#include <cmext/string_view>
+
 #include "cmScanDepFormat.h"
+#include "cmStringAlgorithms.h"
+#include "cmSystemTools.h"
 
 cm::optional<std::string> CxxModuleLocations::BmiGeneratorPathForModule(
   std::string const& logical_name) const
@@ -47,6 +56,122 @@ std::string CxxModuleMapContentGcc(CxxModuleLocations const& loc,
 
   return mm.str();
 }
+
+std::string CxxModuleMapContentMsvc(CxxModuleLocations const& loc,
+                                    cmScanDepInfo const& obj,
+                                    CxxModuleUsage const& usages)
+{
+  std::stringstream mm;
+
+  // A response file of `-reference NAME=PATH` arguments.
+
+  // MSVC's command line only supports a single output. If more than one is
+  // expected, we cannot make a useful module map file.
+  if (obj.Provides.size() > 1) {
+    return {};
+  }
+
+  auto flag_for_method = [](LookupMethod method) -> cm::static_string_view {
+    switch (method) {
+      case LookupMethod::ByName:
+        return "-reference"_s;
+      case LookupMethod::IncludeAngle:
+        return "-headerUnit:angle"_s;
+      case LookupMethod::IncludeQuote:
+        return "-headerUnit:quote"_s;
+    }
+    assert(false && "unsupported lookup method");
+    return ""_s;
+  };
+
+  for (auto const& p : obj.Provides) {
+    if (p.IsInterface) {
+      mm << "-interface\n";
+    } else {
+      mm << "-internalPartition\n";
+    }
+
+    if (auto bmi_loc = loc.BmiGeneratorPathForModule(p.LogicalName)) {
+      mm << "-ifcOutput " << *bmi_loc << '\n';
+    }
+  }
+
+  std::set<std::string> transitive_usage_directs;
+  std::set<std::string> transitive_usage_names;
+
+  for (auto const& r : obj.Requires) {
+    if (auto bmi_loc = loc.BmiGeneratorPathForModule(r.LogicalName)) {
+      auto flag = flag_for_method(r.Method);
+
+      mm << flag << ' ' << r.LogicalName << '=' << *bmi_loc << "\n";
+      transitive_usage_directs.insert(r.LogicalName);
+
+      // Insert transitive usages.
+      auto transitive_usages = usages.Usage.find(r.LogicalName);
+      if (transitive_usages != usages.Usage.end()) {
+        transitive_usage_names.insert(transitive_usages->second.begin(),
+                                      transitive_usages->second.end());
+      }
+    }
+  }
+
+  for (auto const& transitive_name : transitive_usage_names) {
+    if (transitive_usage_directs.count(transitive_name)) {
+      continue;
+    }
+
+    auto module_ref = usages.Reference.find(transitive_name);
+    if (module_ref != usages.Reference.end()) {
+      auto flag = flag_for_method(module_ref->second.Method);
+      mm << flag << ' ' << transitive_name << '=' << module_ref->second.Path
+         << "\n";
+    }
+  }
+
+  return mm.str();
+}
+}
+
+bool CxxModuleUsage::AddReference(std::string const& logical,
+                                  std::string const& loc, LookupMethod method)
+{
+  auto r = this->Reference.find(logical);
+  if (r != this->Reference.end()) {
+    auto& ref = r->second;
+
+    if (ref.Path == loc && ref.Method == method) {
+      return true;
+    }
+
+    auto method_name = [](LookupMethod m) -> cm::static_string_view {
+      switch (m) {
+        case LookupMethod::ByName:
+          return "by-name"_s;
+        case LookupMethod::IncludeAngle:
+          return "include-angle"_s;
+        case LookupMethod::IncludeQuote:
+          return "include-quote"_s;
+      }
+      assert(false && "unsupported lookup method");
+      return ""_s;
+    };
+
+    cmSystemTools::Error(cmStrCat("Disagreement of the location of the '",
+                                  logical,
+                                  "' module. "
+                                  "Location A: '",
+                                  ref.Path, "' via ", method_name(ref.Method),
+                                  "; "
+                                  "Location B: '",
+                                  loc, "' via ", method_name(method), "."));
+    return false;
+  }
+
+  auto& ref = this->Reference[logical];
+  ref.Path = loc;
+  ref.Method = method;
+
+  return true;
 }
 
 cm::static_string_view CxxModuleMapExtension(
@@ -56,19 +181,126 @@ cm::static_string_view CxxModuleMapExtension(
     switch (*format) {
       case CxxModuleMapFormat::Gcc:
         return ".gcm"_s;
+      case CxxModuleMapFormat::Msvc:
+        return ".ifc"_s;
     }
   }
 
   return ".bmi"_s;
 }
 
+std::set<std::string> CxxModuleUsageSeed(
+  CxxModuleLocations const& loc, std::vector<cmScanDepInfo> const& objects,
+  CxxModuleUsage& usages)
+{
+  // Track inner usages to populate usages from internal bits.
+  //
+  // This is a map of modules that required some other module that was not
+  // found to those that were not found.
+  std::map<std::string, std::set<std::string>> internal_usages;
+  std::set<std::string> unresolved;
+
+  for (cmScanDepInfo const& object : objects) {
+    // Add references for each of the provided modules.
+    for (auto const& p : object.Provides) {
+      if (auto bmi_loc = loc.BmiGeneratorPathForModule(p.LogicalName)) {
+        // XXX(cxx-modules): How to support header units?
+        usages.AddReference(p.LogicalName, loc.PathForGenerator(*bmi_loc),
+                            LookupMethod::ByName);
+      }
+    }
+
+    // For each requires, pull in what is required.
+    for (auto const& r : object.Requires) {
+      // Find transitive usages.
+      auto transitive_usages = usages.Usage.find(r.LogicalName);
+      // Find the required name in the current target.
+      auto bmi_loc = loc.BmiGeneratorPathForModule(r.LogicalName);
+
+      for (auto const& p : object.Provides) {
+        auto& this_usages = usages.Usage[p.LogicalName];
+
+        // Add the direct usage.
+        this_usages.insert(r.LogicalName);
+
+        // Add the transitive usage.
+        if (transitive_usages != usages.Usage.end()) {
+          this_usages.insert(transitive_usages->second.begin(),
+                             transitive_usages->second.end());
+        } else if (bmi_loc) {
+          // Mark that we need to update transitive usages later.
+          internal_usages[p.LogicalName].insert(r.LogicalName);
+        }
+      }
+
+      if (bmi_loc) {
+        usages.AddReference(r.LogicalName, loc.PathForGenerator(*bmi_loc),
+                            r.Method);
+      }
+    }
+  }
+
+  // While we have internal usages to manage.
+  while (!internal_usages.empty()) {
+    size_t starting_size = internal_usages.size();
+
+    // For each internal usage.
+    for (auto usage = internal_usages.begin(); usage != internal_usages.end();
+         /* see end of loop */) {
+      auto& this_usages = usages.Usage[usage->first];
+
+      for (auto use = usage->second.begin(); use != usage->second.end();
+           /* see end of loop */) {
+        // Check if this required module uses other internal modules; defer
+        // if so.
+        if (internal_usages.count(*use)) {
+          // Advance the iterator.
+          ++use;
+          continue;
+        }
+
+        auto transitive_usages = usages.Usage.find(*use);
+        if (transitive_usages != usages.Usage.end()) {
+          this_usages.insert(transitive_usages->second.begin(),
+                             transitive_usages->second.end());
+        }
+
+        // Remove the entry and advance the iterator.
+        use = usage->second.erase(use);
+      }
+
+      // Erase the entry if it doesn't have any remaining usages.
+      if (usage->second.empty()) {
+        usage = internal_usages.erase(usage);
+      } else {
+        ++usage;
+      }
+    }
+
+    // Check that at least one usage was resolved.
+    if (starting_size == internal_usages.size()) {
+      // Nothing could be resolved this loop; we have a cycle, so record the
+      // cycle and exit.
+      for (auto const& usage : internal_usages) {
+        unresolved.insert(usage.first);
+      }
+      break;
+    }
+  }
+
+  return unresolved;
+}
+
 std::string CxxModuleMapContent(CxxModuleMapFormat format,
                                 CxxModuleLocations const& loc,
-                                cmScanDepInfo const& obj)
+                                cmScanDepInfo const& obj,
+                                CxxModuleUsage const& usages)
 {
   switch (format) {
     case CxxModuleMapFormat::Gcc:
       return CxxModuleMapContentGcc(loc, obj);
+    case CxxModuleMapFormat::Msvc:
+      return CxxModuleMapContentMsvc(loc, obj, usages);
   }
 
   assert(false);

+ 39 - 2
Source/cmCxxModuleMapper.h

@@ -5,16 +5,20 @@
 #include "cmConfigure.h" // IWYU pragma: keep
 
 #include <functional>
+#include <map>
+#include <set>
 #include <string>
+#include <vector>
 
 #include <cm/optional>
 #include <cmext/string_view>
 
-struct cmScanDepInfo;
+#include "cmScanDepFormat.h"
 
 enum class CxxModuleMapFormat
 {
   Gcc,
+  Msvc,
 };
 
 struct CxxModuleLocations
@@ -37,12 +41,45 @@ struct CxxModuleLocations
     std::string const& logical_name) const;
 };
 
+struct CxxModuleReference
+{
+  // The path to the module file used.
+  std::string Path;
+  // How the module was looked up.
+  LookupMethod Method;
+};
+
+struct CxxModuleUsage
+{
+  // The usage requirements for this object.
+  std::map<std::string, std::set<std::string>> Usage;
+
+  // The references for this object.
+  std::map<std::string, CxxModuleReference> Reference;
+
+  // Add a reference to a module.
+  //
+  // Returns `true` if it matches how it was found previously, `false` if it
+  // conflicts.
+  bool AddReference(std::string const& logical, std::string const& loc,
+                    LookupMethod method);
+};
+
 // Return the extension to use for a given modulemap format.
 cm::static_string_view CxxModuleMapExtension(
   cm::optional<CxxModuleMapFormat> format);
 
+// Fill in module usage information for internal usages.
+//
+// Returns the set of unresolved module usage requirements (these form an
+// import cycle).
+std::set<std::string> CxxModuleUsageSeed(
+  CxxModuleLocations const& loc, std::vector<cmScanDepInfo> const& objects,
+  CxxModuleUsage& usages);
+
 // Return the contents of the module map in the given format for the
 // object file.
 std::string CxxModuleMapContent(CxxModuleMapFormat format,
                                 CxxModuleLocations const& loc,
-                                cmScanDepInfo const& obj);
+                                cmScanDepInfo const& obj,
+                                CxxModuleUsage const& usages);

+ 94 - 6
Source/cmGlobalNinjaGenerator.cxx

@@ -3,6 +3,7 @@
 #include "cmGlobalNinjaGenerator.h"
 
 #include <algorithm>
+#include <cassert>
 #include <cctype>
 #include <cstdio>
 #include <functional>
@@ -2555,6 +2556,8 @@ bool cmGlobalNinjaGenerator::WriteDyndepFile(
     objects.push_back(std::move(info));
   }
 
+  CxxModuleUsage usages;
+
   // Map from module name to module file path, if known.
   std::map<std::string, std::string> mod_files;
 
@@ -2572,8 +2575,47 @@ bool cmGlobalNinjaGenerator::WriteDyndepFile(
       return false;
     }
     if (ltm.isObject()) {
-      for (Json::Value::iterator i = ltm.begin(); i != ltm.end(); ++i) {
-        mod_files[i.key().asString()] = i->asString();
+      Json::Value const& target_modules = ltm["modules"];
+      if (target_modules.isObject()) {
+        for (auto i = target_modules.begin(); i != target_modules.end(); ++i) {
+          mod_files[i.key().asString()] = i->asString();
+        }
+      }
+      Json::Value const& target_modules_references = ltm["references"];
+      if (target_modules_references.isObject()) {
+        for (auto i = target_modules_references.begin();
+             i != target_modules_references.end(); ++i) {
+          if (i->isObject()) {
+            Json::Value const& reference_path = (*i)["path"];
+            CxxModuleReference module_reference;
+            if (reference_path.isString()) {
+              module_reference.Path = reference_path.asString();
+            }
+            Json::Value const& reference_method = (*i)["lookup-method"];
+            if (reference_method.isString()) {
+              std::string reference = reference_method.asString();
+              if (reference == "by-name") {
+                module_reference.Method = LookupMethod::ByName;
+              } else if (reference == "include-angle") {
+                module_reference.Method = LookupMethod::IncludeAngle;
+              } else if (reference == "include-quote") {
+                module_reference.Method = LookupMethod::IncludeQuote;
+              }
+            }
+            usages.Reference[i.key().asString()] = module_reference;
+          }
+        }
+      }
+      Json::Value const& target_modules_usage = ltm["usages"];
+      if (target_modules_usage.isObject()) {
+        for (auto i = target_modules_usage.begin();
+             i != target_modules_usage.end(); ++i) {
+          if (i->isArray()) {
+            for (auto j = i->begin(); j != i->end(); ++j) {
+              usages.Usage[i.key().asString()].insert(j->asString());
+            }
+          }
+        }
       }
     }
   }
@@ -2583,6 +2625,8 @@ bool cmGlobalNinjaGenerator::WriteDyndepFile(
     // nothing to do.
   } else if (arg_modmapfmt == "gcc") {
     modmap_fmt = CxxModuleMapFormat::Gcc;
+  } else if (arg_modmapfmt == "msvc") {
+    modmap_fmt = CxxModuleMapFormat::Msvc;
   } else {
     cmSystemTools::Error(
       cmStrCat("-E cmake_ninja_dyndep does not understand the ", arg_modmapfmt,
@@ -2595,7 +2639,7 @@ bool cmGlobalNinjaGenerator::WriteDyndepFile(
   // Extend the module map with those provided by this target.
   // We do this after loading the modules provided by linked targets
   // in case we have one of the same name that must be preferred.
-  Json::Value tm = Json::objectValue;
+  Json::Value target_modules = Json::objectValue;
   for (cmScanDepInfo const& object : objects) {
     for (auto const& p : object.Provides) {
       std::string mod;
@@ -2614,7 +2658,7 @@ bool cmGlobalNinjaGenerator::WriteDyndepFile(
         mod = cmStrCat(module_dir, safe_logical_name, module_ext);
       }
       mod_files[p.LogicalName] = mod;
-      tm[p.LogicalName] = mod;
+      target_modules[p.LogicalName] = mod;
     }
   }
 
@@ -2636,6 +2680,18 @@ bool cmGlobalNinjaGenerator::WriteDyndepFile(
       return {};
     };
 
+    // Insert information about the current target's modules.
+    if (modmap_fmt) {
+      auto cycle_modules = CxxModuleUsageSeed(locs, objects, usages);
+      if (!cycle_modules.empty()) {
+        cmSystemTools::Error(
+          cmStrCat("Circular dependency detected in the C++ module import "
+                   "graph. See modules named: \"",
+                   cmJoin(cycle_modules, R"(", ")"_s), '"'));
+        return false;
+      }
+    }
+
     cmNinjaBuild build("dyndep");
     build.Outputs.emplace_back("");
     for (cmScanDepInfo const& object : objects) {
@@ -2658,7 +2714,7 @@ bool cmGlobalNinjaGenerator::WriteDyndepFile(
       }
 
       if (modmap_fmt) {
-        auto mm = CxxModuleMapContent(*modmap_fmt, locs, object);
+        auto mm = CxxModuleMapContent(*modmap_fmt, locs, object, usages);
 
         // XXX(modmap): If changing this path construction, change
         // `cmNinjaTargetGenerator::WriteObjectBuildStatements` to generate the
@@ -2671,12 +2727,44 @@ bool cmGlobalNinjaGenerator::WriteDyndepFile(
     }
   }
 
+  Json::Value target_module_info = Json::objectValue;
+  target_module_info["modules"] = target_modules;
+
+  auto& target_usages = target_module_info["usages"] = Json::objectValue;
+  for (auto const& u : usages.Usage) {
+    auto& mod_usage = target_usages[u.first] = Json::arrayValue;
+    for (auto const& v : u.second) {
+      mod_usage.append(v);
+    }
+  }
+
+  auto name_for_method = [](LookupMethod method) -> cm::static_string_view {
+    switch (method) {
+      case LookupMethod::ByName:
+        return "by-name"_s;
+      case LookupMethod::IncludeAngle:
+        return "include-angle"_s;
+      case LookupMethod::IncludeQuote:
+        return "include-quote"_s;
+    }
+    assert(false && "unsupported lookup method");
+    return ""_s;
+  };
+
+  auto& target_references = target_module_info["references"] =
+    Json::objectValue;
+  for (auto const& r : usages.Reference) {
+    auto& mod_ref = target_references[r.first] = Json::objectValue;
+    mod_ref["path"] = r.second.Path;
+    mod_ref["lookup-method"] = std::string(name_for_method(r.second.Method));
+  }
+
   // Store the map of modules provided by this target in a file for
   // use by dependents that reference this target in linked-target-dirs.
   std::string const target_mods_file = cmStrCat(
     cmSystemTools::GetFilenamePath(arg_dd), '/', arg_lang, "Modules.json");
   cmGeneratedFileStream tmf(target_mods_file);
-  tmf << tm;
+  tmf << target_module_info;
 
   bool result = true;
 

+ 37 - 0
Source/cmScanDepFormat.cxx

@@ -5,6 +5,7 @@
 
 #include <cctype>
 #include <cstdio>
+#include <iostream>
 #include <utility>
 
 #include <cm/optional>
@@ -188,6 +189,19 @@ bool cmScanDepFormat_P1689_Parse(std::string const& arg_pp,
             return false;
           }
 
+          if (provide.isMember("is-interface")) {
+            Json::Value const& is_interface = provide["is-interface"];
+            if (!is_interface.isBool()) {
+              cmSystemTools::Error(
+                cmStrCat("-E cmake_ninja_dyndep failed to parse ", arg_pp,
+                         ": is-interface is not a boolean"));
+              return false;
+            }
+            provide_info.IsInterface = is_interface.asBool();
+          } else {
+            provide_info.IsInterface = true;
+          }
+
           info->Provides.push_back(provide_info);
         }
       }
@@ -267,6 +281,27 @@ bool cmScanDepFormat_P1689_Parse(std::string const& arg_pp,
           info->Requires.push_back(require_info);
         }
       }
+
+      // MSVC 17.3 toolchain bug. Remove when 17.4 is available.
+      if (rule.isMember("is-interface")) {
+        std::cerr
+          << "warning: acknowledging an VS 17.3 toolchain bug; accepting "
+             "until a new release which fixes it is available"
+          << std::endl;
+
+        Json::Value const& is_interface_json = rule["is-interface"];
+        if (!is_interface_json.isBool()) {
+          cmSystemTools::Error(
+            cmStrCat("-E cmake_ninja_dyndep failed to parse ", arg_pp,
+                     ": is-interface is not a boolean"));
+          return false;
+        }
+        bool is_interface = is_interface_json.asBool();
+
+        for (auto& provide : info->Provides) {
+          provide.IsInterface = is_interface;
+        }
+      }
     }
   }
 
@@ -308,6 +343,8 @@ bool cmScanDepFormat_P1689_Write(std::string const& path,
       provide_obj["source-path"] = EncodeFilename(provide.SourcePath);
     }
 
+    provide_obj["is-interface"] = provide.IsInterface;
+
     provides.append(provide_obj);
   }
 

+ 5 - 0
Source/cmScanDepFormat.h

@@ -18,6 +18,11 @@ struct cmSourceReqInfo
   std::string SourcePath;
   std::string CompiledModulePath;
   bool UseSourcePath = false;
+
+  // Provides-only fields.
+  bool IsInterface = true;
+
+  // Requires-only fields.
   LookupMethod Method = LookupMethod::ByName;
 };
 

+ 1 - 0
Tests/RunCMake/CXXModules/RunCMakeTest.cmake

@@ -131,6 +131,7 @@ if ("named" IN_LIST CMake_TEST_MODULE_COMPILATION)
   run_cxx_module_test(library library-static -DBUILD_SHARED_LIBS=OFF)
   run_cxx_module_test(generated)
   run_cxx_module_test(public-req-private)
+  run_cxx_module_test(deep-chain)
 endif ()
 
 # Tests which use named modules in shared libraries.

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

@@ -1,6 +1,6 @@
 function (check_for_bmi prefix destination name)
   set(found 0)
-  foreach (ext IN ITEMS gcm)
+  foreach (ext IN ITEMS gcm ifc)
     if (EXISTS "${prefix}/${destination}/${name}.${ext}")
       set(found 1)
       break ()

+ 9 - 0
Tests/RunCMake/CXXModules/examples/deep-chain-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.

+ 66 - 0
Tests/RunCMake/CXXModules/examples/deep-chain/CMakeLists.txt

@@ -0,0 +1,66 @@
+cmake_minimum_required(VERSION 3.24)
+project(cxx_modules_deep_chain CXX)
+
+include("${CMAKE_SOURCE_DIR}/../cxx-modules-rules.cmake")
+
+add_library(a STATIC)
+target_sources(a
+  PUBLIC
+    FILE_SET CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        a.cxx)
+target_compile_features(a PUBLIC cxx_std_20)
+
+add_library(b STATIC)
+target_sources(b
+  PUBLIC
+    FILE_SET CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        b.cxx)
+target_compile_features(b PUBLIC cxx_std_20)
+target_link_libraries(b PUBLIC a)
+
+add_library(c STATIC)
+target_sources(c
+  PUBLIC
+    FILE_SET CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        c.cxx)
+target_compile_features(c PUBLIC cxx_std_20)
+target_link_libraries(c PUBLIC b)
+
+add_library(d STATIC)
+target_sources(d
+  PUBLIC
+    FILE_SET CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        d.cxx)
+target_compile_features(d PUBLIC cxx_std_20)
+target_link_libraries(d PUBLIC c)
+
+add_library(e STATIC)
+target_sources(e
+  PUBLIC
+    FILE_SET CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        e.cxx)
+target_compile_features(e PUBLIC cxx_std_20)
+target_link_libraries(e PUBLIC d)
+
+add_executable(exe)
+target_link_libraries(exe PRIVATE e)
+target_sources(exe
+  PRIVATE
+    main.cxx)
+
+add_test(NAME exe COMMAND exe)

+ 6 - 0
Tests/RunCMake/CXXModules/examples/deep-chain/a.cxx

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

+ 7 - 0
Tests/RunCMake/CXXModules/examples/deep-chain/b.cxx

@@ -0,0 +1,7 @@
+export module b;
+import a;
+
+export int b()
+{
+  return a();
+}

+ 7 - 0
Tests/RunCMake/CXXModules/examples/deep-chain/c.cxx

@@ -0,0 +1,7 @@
+export module c;
+import b;
+
+export int c()
+{
+  return b();
+}

+ 7 - 0
Tests/RunCMake/CXXModules/examples/deep-chain/d.cxx

@@ -0,0 +1,7 @@
+export module d;
+import c;
+
+export int d()
+{
+  return c();
+}

+ 7 - 0
Tests/RunCMake/CXXModules/examples/deep-chain/e.cxx

@@ -0,0 +1,7 @@
+export module e;
+import d;
+
+export int e()
+{
+  return d();
+}

+ 6 - 0
Tests/RunCMake/CXXModules/examples/deep-chain/main.cxx

@@ -0,0 +1,6 @@
+import e;
+
+int main(int argc, char* argv[])
+{
+  return e();
+}

+ 1 - 1
Tests/RunCMake/CXXModules/examples/internal-partitions/importable.cxx

@@ -1,5 +1,5 @@
 export module importable;
-import importable : internal_partition;
+import : internal_partition;
 
 #include "internal-partitions_export.h"
 

+ 1 - 1
Tests/RunCMake/CXXModules/examples/partitions/importable.cxx

@@ -1,5 +1,5 @@
 export module importable;
-export import importable : partition;
+export import : partition;
 
 #include "partitions_export.h"