Merge topic 'ci-os-arch'

6a38c5dccb gitlab-ci: replace 'linux' tags with 'linux-x86_64' in upload jobs

Acked-by: Kitware Robot <kwrobot@kitware.com>
Acked-by: Ben Boeckel <ben.boeckel@kitware.com>
Merge-request: !7976
diff --git a/.gitlab/ci/ctest_exclusions.cmake b/.gitlab/ci/ctest_exclusions.cmake
index b29e785..a2789c3 100644
--- a/.gitlab/ci/ctest_exclusions.cmake
+++ b/.gitlab/ci/ctest_exclusions.cmake
@@ -31,9 +31,6 @@
   list(APPEND test_exclusions
     # FIXME(#24187): This test fails with NVHPC as the CUDA host compiler.
     "^CudaOnly.SeparateCompilationPTX$"
-
-    # FIXME(#24188): FindCUDAToolkit breaks on some symlink layouts.
-    "^Cuda.Toolkit$"
     )
 endif()
 
diff --git a/Modules/CMakeMSYSFindMake.cmake b/Modules/CMakeMSYSFindMake.cmake
index 33b02c9..96fdb37 100644
--- a/Modules/CMakeMSYSFindMake.cmake
+++ b/Modules/CMakeMSYSFindMake.cmake
@@ -3,8 +3,13 @@
 
 
 find_program(CMAKE_MAKE_PROGRAM make
+  REGISTRY_VIEW 32
   PATHS
-  "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MSYS-1.0_is1;Inno Setup: App Path]/bin"
-  "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MinGW;InstallLocation]/bin"
-  c:/msys/1.0/bin /msys/1.0/bin)
+      # Typical install path for 32-bit MSYS2 (https://repo.msys2.org/distrib/msys2-i686-latest.sfx.exe)
+      "C:/msys32/usr"
+      # Typical install path for MINGW32 (https://sourceforge.net/projects/mingw)
+      "C:/mingw/msys"
+      # Git for Windows 32-bit (https://gitforwindows.org/)
+      "[HKEY_LOCAL_MACHINE\\SOFTWARE\\GitForWindows;InstallPath]/usr")
+
 mark_as_advanced(CMAKE_MAKE_PROGRAM)
diff --git a/Modules/FindCUDAToolkit.cmake b/Modules/FindCUDAToolkit.cmake
index a4cebe4..5df31da 100644
--- a/Modules/FindCUDAToolkit.cmake
+++ b/Modules/FindCUDAToolkit.cmake
@@ -894,8 +894,25 @@
 if(CUDAToolkit_FOUND)
   set(CUDAToolkit_INCLUDE_DIRS ${CUDAToolkit_INCLUDE_DIR})
   get_filename_component(CUDAToolkit_LIBRARY_DIR ${CUDA_CUDART} DIRECTORY ABSOLUTE)
+
+  # Build search paths without any symlinks
+  file(REAL_PATH "${CUDAToolkit_LIBRARY_DIR}" _cmake_search_dir)
+  set(CUDAToolkit_LIBRARY_SEARCH_DIRS "${_cmake_search_dir}")
+
+  # Detect we are in a splayed nvhpc toolkit layout and add extra
+  # search paths without symlinks
+  if(CUDAToolkit_LIBRARY_DIR  MATCHES ".*/cuda/${CUDAToolkit_VERSION_MAJOR}.${CUDAToolkit_VERSION_MINOR}/lib64$")
+    # Search location for math_libs/
+    file(REAL_PATH "${CUDAToolkit_LIBRARY_DIR}/../../../" _cmake_search_dir)
+    list(APPEND CUDAToolkit_LIBRARY_SEARCH_DIRS "${_cmake_search_dir}")
+
+    # Search location for extras like cupti
+    file(REAL_PATH "${CUDAToolkit_LIBRARY_DIR}/../" _cmake_search_dir)
+    list(APPEND CUDAToolkit_LIBRARY_SEARCH_DIRS "${_cmake_search_dir}")
+  endif()
 endif()
 
+
 #-----------------------------------------------------------------------------
 # Construct import targets
 if(CUDAToolkit_FOUND)
@@ -907,21 +924,21 @@
 
     find_library(CUDA_${lib_name}_LIBRARY
       NAMES ${search_names}
-      HINTS ${CUDAToolkit_LIBRARY_DIR}
+      HINTS ${CUDAToolkit_LIBRARY_SEARCH_DIRS}
             ENV CUDA_PATH
       PATH_SUFFIXES nvidia/current lib64 lib/x64 lib
+                    # Support NVHPC splayed math library layout
+                    math_libs/${CUDAToolkit_VERSION_MAJOR}.${CUDAToolkit_VERSION_MINOR}/lib64
+                    math_libs/lib64
                     ${arg_EXTRA_PATH_SUFFIXES}
     )
     # Don't try any stub directories until we have exhausted all other
     # search locations.
     find_library(CUDA_${lib_name}_LIBRARY
       NAMES ${search_names}
-      HINTS ${CUDAToolkit_LIBRARY_DIR}
+      HINTS ${CUDAToolkit_LIBRARY_SEARCH_DIRS}
             ENV CUDA_PATH
       PATH_SUFFIXES lib64/stubs lib/x64/stubs lib/stubs stubs
-                    # Support NVHPC splayed math library layout
-                    ../../math_libs/${CUDAToolkit_VERSION_MAJOR}.${CUDAToolkit_VERSION_MINOR}/lib64
-                    ../../math_libs/lib64
     )
 
     mark_as_advanced(CUDA_${lib_name}_LIBRARY)
@@ -1054,11 +1071,15 @@
 
   if(CUDAToolkit_CUPTI_INCLUDE_DIR)
     _CUDAToolkit_find_and_add_import_lib(cupti
-                                        EXTRA_PATH_SUFFIXES ../extras/CUPTI/lib64/
+                                        EXTRA_PATH_SUFFIXES extras/CUPTI/lib64/
+                                                            extras/CUPTI/lib/
+                                                            ../extras/CUPTI/lib64/
                                                             ../extras/CUPTI/lib/
                                         EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}")
     _CUDAToolkit_find_and_add_import_lib(cupti_static
-                                        EXTRA_PATH_SUFFIXES ../extras/CUPTI/lib64/
+                                        EXTRA_PATH_SUFFIXES extras/CUPTI/lib64/
+                                                            extras/CUPTI/lib/
+                                                            ../extras/CUPTI/lib64/
                                                             ../extras/CUPTI/lib/
                                         EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}")
   endif()
diff --git a/Modules/FindMsys.cmake b/Modules/FindMsys.cmake
index b4796d2..86597c2 100644
--- a/Modules/FindMsys.cmake
+++ b/Modules/FindMsys.cmake
@@ -19,11 +19,12 @@
   find_program(MSYS_CMD
     NAMES msys2_shell.cmd
     PATHS
-      "C:/msys64"
+      # Typical install path for MSYS2 (https://repo.msys2.org/distrib/msys2-i686-latest.sfx.exe)
       "C:/msys32"
-      "C:/MSYS"
-      "[HKEY_LOCAL_MACHINE\\SOFTWARE\\MSYS\\setup;rootdir]"
-      "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Cygnus Solutions\\MSYS\\mounts v2\\/;native]"
+      # Typical install path for MSYS2 (https://repo.msys2.org/distrib/msys2-x86_64-latest.sfx.exe)
+      "C:/msys64"
+      # Git for Windows (https://gitforwindows.org/)
+      "[HKEY_LOCAL_MACHINE\\SOFTWARE\\GitForWindows;InstallPath]"
   )
   get_filename_component(MSYS_INSTALL_PATH "${MSYS_CMD}" DIRECTORY)
   mark_as_advanced(MSYS_CMD)
diff --git a/Modules/Platform/Windows-OpenWatcom.cmake b/Modules/Platform/Windows-OpenWatcom.cmake
index 657a923..4e73160 100644
--- a/Modules/Platform/Windows-OpenWatcom.cmake
+++ b/Modules/Platform/Windows-OpenWatcom.cmake
@@ -6,8 +6,13 @@
 
 set(CMAKE_BUILD_TYPE_INIT Debug)
 
-string(APPEND CMAKE_SHARED_LINKER_FLAGS_INIT " system nt_dll")
-string(APPEND CMAKE_MODULE_LINKER_FLAGS_INIT " system nt_dll")
+if(DEFINED CMAKE_SYSTEM_PROCESSOR AND CMAKE_SYSTEM_PROCESSOR STREQUAL "I86")
+  string(APPEND CMAKE_SHARED_LINKER_FLAGS_INIT " system windows")
+  string(APPEND CMAKE_MODULE_LINKER_FLAGS_INIT " system windows")
+else()
+  string(APPEND CMAKE_SHARED_LINKER_FLAGS_INIT " system nt_dll")
+  string(APPEND CMAKE_MODULE_LINKER_FLAGS_INIT " system nt_dll")
+endif()
 
 set(CMAKE_C_COMPILE_OPTIONS_DLL "-bd") # Note: This variable is a ';' separated list
 set(CMAKE_SHARED_LIBRARY_C_FLAGS "-bd") # ... while this is a space separated string.
@@ -23,22 +28,45 @@
   set(_br_bm "-br -bm")
 endif()
 
-string(APPEND CMAKE_C_FLAGS_INIT " -bt=nt -dWIN32 ${_br_bm}")
-string(APPEND CMAKE_CXX_FLAGS_INIT " -bt=nt -xs -dWIN32 ${_br_bm}")
+if(DEFINED CMAKE_SYSTEM_PROCESSOR AND CMAKE_SYSTEM_PROCESSOR STREQUAL "I86")
+  string(APPEND CMAKE_C_FLAGS_INIT " -bt=windows ")
+  string(APPEND CMAKE_CXX_FLAGS_INIT " -bt=windows ")
+else()
+  string(APPEND CMAKE_C_FLAGS_INIT " -bt=nt -dWIN32 ${_br_bm}")
+  string(APPEND CMAKE_CXX_FLAGS_INIT " -bt=nt -xs -dWIN32 ${_br_bm}")
+endif()
 
 unset(__WINDOWS_WATCOM_CMP0136)
 unset(_br_bm)
 
 if(CMAKE_CROSSCOMPILING)
-  if(NOT CMAKE_C_STANDARD_INCLUDE_DIRECTORIES)
-    set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES $ENV{WATCOM}/h $ENV{WATCOM}/h/nt)
-  endif()
-  if(NOT CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES)
-    set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES $ENV{WATCOM}/h $ENV{WATCOM}/h/nt)
+  if(DEFINED CMAKE_SYSTEM_PROCESSOR AND CMAKE_SYSTEM_PROCESSOR STREQUAL "I86")
+    if(NOT CMAKE_C_STANDARD_INCLUDE_DIRECTORIES)
+      set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES $ENV{WATCOM}/h $ENV{WATCOM}/h/win)
+    endif()
+    if(NOT CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES)
+      set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES $ENV{WATCOM}/h $ENV{WATCOM}/h/win)
+    endif()
+  else()
+    if(NOT CMAKE_C_STANDARD_INCLUDE_DIRECTORIES)
+      set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES $ENV{WATCOM}/h $ENV{WATCOM}/h/nt)
+    endif()
+    if(NOT CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES)
+      set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES $ENV{WATCOM}/h $ENV{WATCOM}/h/nt)
+    endif()
   endif()
 endif()
 
 macro(__windows_open_watcom lang)
+if(DEFINED CMAKE_SYSTEM_PROCESSOR AND CMAKE_SYSTEM_PROCESSOR STREQUAL "I86")
+  set(CMAKE_${lang}_CREATE_WIN32_EXE "system windows")
+  set(CMAKE_${lang}_CREATE_CONSOLE_EXE "system windows")
+
+  set(CMAKE_${lang}_COMPILE_OPTIONS_WATCOM_RUNTIME_LIBRARY_SingleThreaded  "")
+  set(CMAKE_${lang}_COMPILE_OPTIONS_WATCOM_RUNTIME_LIBRARY_SingleThreadedDLL "")
+  set(CMAKE_${lang}_COMPILE_OPTIONS_WATCOM_RUNTIME_LIBRARY_MultiThreaded "")
+  set(CMAKE_${lang}_COMPILE_OPTIONS_WATCOM_RUNTIME_LIBRARY_MultiThreadedDLL "")
+else()
   set(CMAKE_${lang}_CREATE_WIN32_EXE "system nt_win")
   set(CMAKE_${lang}_CREATE_CONSOLE_EXE "system nt")
 
@@ -46,4 +74,5 @@
   set(CMAKE_${lang}_COMPILE_OPTIONS_WATCOM_RUNTIME_LIBRARY_SingleThreadedDLL      -br)
   set(CMAKE_${lang}_COMPILE_OPTIONS_WATCOM_RUNTIME_LIBRARY_MultiThreaded          -bm)
   set(CMAKE_${lang}_COMPILE_OPTIONS_WATCOM_RUNTIME_LIBRARY_MultiThreadedDLL       -bm -br)
+endif()
 endmacro()
diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index c0709c6..41a901a 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -198,6 +198,8 @@
   cmDocumentationFormatter.cxx
   cmDynamicLoader.cxx
   cmDynamicLoader.h
+  cmDyndepCollation.cxx
+  cmDyndepCollation.h
   cmELF.h
   cmELF.cxx
   cmExprParserHelper.cxx
diff --git a/Source/CMakeVersion.cmake b/Source/CMakeVersion.cmake
index d2236b1..c3c47ae 100644
--- a/Source/CMakeVersion.cmake
+++ b/Source/CMakeVersion.cmake
@@ -1,7 +1,7 @@
 # CMake version number components.
 set(CMake_VERSION_MAJOR 3)
 set(CMake_VERSION_MINOR 25)
-set(CMake_VERSION_PATCH 20221201)
+set(CMake_VERSION_PATCH 20221202)
 #set(CMake_VERSION_RC 0)
 set(CMake_VERSION_IS_DIRTY 0)
 
diff --git a/Source/cmDyndepCollation.cxx b/Source/cmDyndepCollation.cxx
new file mode 100644
index 0000000..2827659
--- /dev/null
+++ b/Source/cmDyndepCollation.cxx
@@ -0,0 +1,652 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmDyndepCollation.h"
+
+#include <algorithm>
+#include <map>
+#include <ostream>
+#include <set>
+#include <utility>
+#include <vector>
+
+#include <cm/memory>
+#include <cm/string_view>
+#include <cmext/string_view>
+
+#include <cm3p/json/value.h>
+
+#include "cmExportBuildFileGenerator.h"
+#include "cmExportSet.h"
+#include "cmFileSet.h"
+#include "cmGeneratedFileStream.h"
+#include "cmGeneratorExpression.h" // IWYU pragma: keep
+#include "cmGeneratorTarget.h"
+#include "cmGlobalGenerator.h"
+#include "cmInstallCxxModuleBmiGenerator.h"
+#include "cmInstallExportGenerator.h"
+#include "cmInstallFileSetGenerator.h"
+#include "cmInstallGenerator.h"
+#include "cmMakefile.h"
+#include "cmMessageType.h"
+#include "cmOutputConverter.h"
+#include "cmScanDepFormat.h"
+#include "cmSourceFile.h"
+#include "cmStringAlgorithms.h"
+#include "cmSystemTools.h"
+#include "cmTarget.h"
+#include "cmTargetExport.h"
+
+namespace {
+
+Json::Value CollationInformationCxxModules(
+  cmGeneratorTarget const* gt, std::string const& config,
+  cmDyndepGeneratorCallbacks const& cb)
+{
+  cmTarget const* tgt = gt->Target;
+  auto all_file_sets = tgt->GetAllFileSetNames();
+  Json::Value tdi_cxx_module_info = Json::objectValue;
+  for (auto const& file_set_name : all_file_sets) {
+    auto const* file_set = tgt->GetFileSet(file_set_name);
+    if (!file_set) {
+      gt->Makefile->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, gt->LocalGenerator, config, gt);
+    std::map<std::string, std::vector<std::string>> files_per_dirs;
+    for (auto const& entry : fileEntries) {
+      file_set->EvaluateFileEntry(directories, files_per_dirs, entry,
+                                  gt->LocalGenerator, config, gt);
+    }
+
+    std::map<std::string, cmSourceFile const*> sf_map;
+    {
+      std::vector<cmSourceFile const*> objectSources;
+      gt->GetObjectSources(objectSources, config);
+      for (auto const* sf : objectSources) {
+        auto full_path = sf->GetFullPath();
+        if (full_path.empty()) {
+          gt->Makefile->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 : gt->Makefile->GetInstallGenerators()) {
+      if (auto const* fsg =
+            dynamic_cast<cmInstallFileSetGenerator const*>(ig.get())) {
+        if (fsg->GetTarget() == gt && 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()) {
+          gt->Makefile->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) {
+          gt->Makefile->IssueMessage(
+            MessageType::INTERNAL_ERROR,
+            cmStrCat("Target \"", tgt->GetName(), "\" has source file \"",
+                     file, "\" which has not been tracked properly."));
+          continue;
+        }
+
+        auto obj_path = cb.ObjectFilePath(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;
+      }
+    }
+  }
+
+  return tdi_cxx_module_info;
+}
+
+Json::Value CollationInformationBmiInstallation(cmGeneratorTarget const* gt,
+                                                std::string const& config)
+{
+  cmInstallCxxModuleBmiGenerator const* bmi_gen = nullptr;
+  for (auto const& ig : gt->Makefile->GetInstallGenerators()) {
+    if (auto const* bmig =
+          dynamic_cast<cmInstallCxxModuleBmiGenerator const*>(ig.get())) {
+      if (bmig->GetTarget() == gt) {
+        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);
+
+    return tdi_bmi_info;
+  }
+  return Json::nullValue;
+}
+
+Json::Value CollationInformationExports(cmGeneratorTarget const* gt)
+{
+  Json::Value tdi_exports = Json::arrayValue;
+  std::string export_name = gt->GetExportName();
+
+  auto const& all_install_exports = gt->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(),
+                   [gt](std::unique_ptr<cmTargetExport> const& te) {
+                     return te->Target == gt;
+                   });
+    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 = gt->Makefile->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 = gt->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);
+  }
+
+  return tdi_exports;
+}
+}
+
+void cmDyndepCollation::AddCollationInformation(
+  Json::Value& tdi, cmGeneratorTarget const* gt, std::string const& config,
+  cmDyndepGeneratorCallbacks const& cb)
+{
+  tdi["cxx-modules"] = CollationInformationCxxModules(gt, config, cb);
+  tdi["bmi-installation"] = CollationInformationBmiInstallation(gt, config);
+  tdi["exports"] = CollationInformationExports(gt);
+  tdi["config"] = config;
+}
+
+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 cmCxxModuleExportInfo
+{
+  std::map<std::string, CxxModuleFileSet> ObjectToFileSet;
+  cm::optional<CxxModuleBmiInstall> BmiInstallation;
+  std::vector<CxxModuleExport> Exports;
+  std::string Config;
+};
+
+void cmCxxModuleExportInfoDeleter::operator()(cmCxxModuleExportInfo* ei) const
+{
+  delete ei;
+}
+
+std::unique_ptr<cmCxxModuleExportInfo, cmCxxModuleExportInfoDeleter>
+cmDyndepCollation::ParseExportInfo(Json::Value const& tdi)
+{
+  auto export_info =
+    std::unique_ptr<cmCxxModuleExportInfo, cmCxxModuleExportInfoDeleter>(
+      new cmCxxModuleExportInfo);
+
+  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();
+      }
+    }
+  }
+
+  return export_info;
+}
+
+bool cmDyndepCollation::WriteDyndepMetadata(
+  std::string const& lang, std::vector<cmScanDepInfo> const& objects,
+  cmCxxModuleExportInfo const& export_info,
+  cmDyndepMetadataCallbacks const& cb)
+{
+  // Only C++ supports any of the file-set or BMI installation considered
+  // below.
+  if (lang != "CXX"_s) {
+    return true;
+  }
+
+  bool result = true;
+
+  // 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 = cb.ModuleFile(p.LogicalName);
+      if (m) {
+        install_bmi_path = cmStrCat(
+          bmi_destination, cmEscape(cmSystemTools::GetFilenameName(*m)));
+        build_bmi_path = cmEscape(*m);
+      }
+
+      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 << "\")\n";
+        if (bmi_dest_is_abs) {
+          *bmi_install_script
+            << "  list(APPEND CMAKE_ABSOLUTE_DESTINATION_FILES\n"
+               "    \""
+            << cmEscape(cmSystemTools::GetFilenameName(*m))
+            << "\")\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;
+}
diff --git a/Source/cmDyndepCollation.h b/Source/cmDyndepCollation.h
new file mode 100644
index 0000000..e70ac09
--- /dev/null
+++ b/Source/cmDyndepCollation.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 <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <cm/optional>
+
+class cmGeneratorTarget;
+struct cmScanDepInfo;
+class cmSourceFile;
+
+namespace Json {
+class Value;
+}
+
+struct cmDyndepGeneratorCallbacks
+{
+  std::function<std::string(cmSourceFile const* sf, std::string const& config)>
+    ObjectFilePath;
+};
+
+struct cmDyndepMetadataCallbacks
+{
+  std::function<cm::optional<std::string>(std::string const& name)> ModuleFile;
+};
+
+struct cmCxxModuleExportInfo;
+struct cmCxxModuleExportInfoDeleter
+{
+  void operator()(cmCxxModuleExportInfo* ei) const;
+};
+
+struct cmDyndepCollation
+{
+  static void AddCollationInformation(Json::Value& tdi,
+                                      cmGeneratorTarget const* gt,
+                                      std::string const& config,
+                                      cmDyndepGeneratorCallbacks const& cb);
+
+  static std::unique_ptr<cmCxxModuleExportInfo, cmCxxModuleExportInfoDeleter>
+  ParseExportInfo(Json::Value const& tdi);
+  static bool WriteDyndepMetadata(std::string const& lang,
+                                  std::vector<cmScanDepInfo> const& objects,
+                                  cmCxxModuleExportInfo const& export_info,
+                                  cmDyndepMetadataCallbacks const& cb);
+};
diff --git a/Source/cmGlobalNinjaGenerator.cxx b/Source/cmGlobalNinjaGenerator.cxx
index 618dfb7..4500f33 100644
--- a/Source/cmGlobalNinjaGenerator.cxx
+++ b/Source/cmGlobalNinjaGenerator.cxx
@@ -25,7 +25,7 @@
 #include "cmsys/FStream.hxx"
 
 #include "cmCxxModuleMapper.h"
-#include "cmFileSet.h"
+#include "cmDyndepCollation.h"
 #include "cmFortranParser.h"
 #include "cmGeneratedFileStream.h"
 #include "cmGeneratorExpressionEvaluationFile.h"
@@ -2468,45 +2468,6 @@
 }
 }
 
-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,
@@ -2514,7 +2475,7 @@
   std::string const& module_dir,
   std::vector<std::string> const& linked_target_dirs,
   std::string const& arg_lang, std::string const& arg_modmapfmt,
-  CxxModuleExportInfo const& export_info)
+  cmCxxModuleExportInfo const& export_info)
 {
   // Setup path conversions.
   {
@@ -2749,279 +2710,18 @@
   cmGeneratedFileStream tmf(target_mods_file);
   tmf << target_module_info;
 
-  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);
+  cmDyndepMetadataCallbacks cb;
+  cb.ModuleFile =
+    [mod_files](std::string const& name) -> cm::optional<std::string> {
+    auto m = mod_files.find(name);
+    if (m != mod_files.end()) {
+      return m->second;
     }
+    return {};
+  };
 
-    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;
+  return cmDyndepCollation::WriteDyndepMetadata(arg_lang, objects, export_info,
+                                                cb);
 }
 
 int cmcmd_cmake_ninja_dyndep(std::vector<std::string>::const_iterator argBeg,
@@ -3095,58 +2795,7 @@
     }
   }
 
-  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();
-      }
-    }
-  }
+  auto export_info = cmDyndepCollation::ParseExportInfo(tdi);
 
   cmake cm(cmake::RoleInternal, cmState::Unknown);
   cm.SetHomeDirectory(dir_top_src);
@@ -3156,7 +2805,7 @@
       !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,
-        export_info)) {
+        *export_info)) {
     return 1;
   }
   return 0;
diff --git a/Source/cmGlobalNinjaGenerator.h b/Source/cmGlobalNinjaGenerator.h
index 6f654f6..775e792 100644
--- a/Source/cmGlobalNinjaGenerator.h
+++ b/Source/cmGlobalNinjaGenerator.h
@@ -35,6 +35,7 @@
 class cmOutputConverter;
 class cmStateDirectory;
 class cmake;
+struct cmCxxModuleExportInfo;
 
 /**
  * \class cmGlobalNinjaGenerator
@@ -417,7 +418,6 @@
   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,
@@ -425,7 +425,7 @@
     std::string const& module_dir,
     std::vector<std::string> const& linked_target_dirs,
     std::string const& arg_lang, std::string const& arg_modmapfmt,
-    CxxModuleExportInfo const& export_info);
+    cmCxxModuleExportInfo const& export_info);
 
   virtual std::string BuildAlias(const std::string& alias,
                                  const std::string& /*config*/) const
diff --git a/Source/cmNinjaTargetGenerator.cxx b/Source/cmNinjaTargetGenerator.cxx
index 095bf40..85a6fc2 100644
--- a/Source/cmNinjaTargetGenerator.cxx
+++ b/Source/cmNinjaTargetGenerator.cxx
@@ -4,6 +4,7 @@
 
 #include <algorithm>
 #include <cassert>
+#include <functional>
 #include <iterator>
 #include <map>
 #include <ostream>
@@ -21,17 +22,12 @@
 
 #include "cmComputeLinkInformation.h"
 #include "cmCustomCommandGenerator.h"
-#include "cmExportBuildFileGenerator.h"
-#include "cmExportSet.h"
+#include "cmDyndepCollation.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"
@@ -47,7 +43,6 @@
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
 #include "cmTarget.h"
-#include "cmTargetExport.h"
 #include "cmValue.h"
 #include "cmake.h"
 
@@ -1637,214 +1632,15 @@
     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;
-    }
+  cmDyndepGeneratorCallbacks cb;
+  cb.ObjectFilePath = [this](cmSourceFile const* sf, std::string const& cnf) {
+    return this->GetObjectFilePath(sf, cnf);
+  };
 
-    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);
-  }
+#if !defined(CMAKE_BOOTSTRAP)
+  cmDyndepCollation::AddCollationInformation(tdi, this->GeneratorTarget,
+                                             config, cb);
+#endif
 
   std::string const tdin = this->GetTargetDependInfoPath(lang, config);
   cmGeneratedFileStream tdif(tdin);
diff --git a/Source/cmStringAlgorithms.cxx b/Source/cmStringAlgorithms.cxx
index f73c854..e559cfa 100644
--- a/Source/cmStringAlgorithms.cxx
+++ b/Source/cmStringAlgorithms.cxx
@@ -203,15 +203,23 @@
   MakeDigits(this->View_, this->Digits_, "%g", val);
 }
 
-std::string cmCatViews(std::initializer_list<cm::string_view> views)
+std::string cmCatViews(cm::optional<std::string>&& first,
+                       std::initializer_list<cm::string_view> views)
 {
-  std::size_t total_size = 0;
+  std::size_t totalSize = 0;
   for (cm::string_view const& view : views) {
-    total_size += view.size();
+    totalSize += view.size();
   }
 
-  std::string result(total_size, '\0');
-  std::string::iterator sit = result.begin();
+  std::string result;
+  std::string::size_type initialLen = 0;
+  if (first) {
+    totalSize += first->length();
+    initialLen = first->length();
+    result = std::move(*first);
+  }
+  result.resize(totalSize);
+  std::string::iterator sit = result.begin() + initialLen;
   for (cm::string_view const& view : views) {
     sit = std::copy_n(view.data(), view.size(), sit);
   }
diff --git a/Source/cmStringAlgorithms.h b/Source/cmStringAlgorithms.h
index 83938bc..bff2eda 100644
--- a/Source/cmStringAlgorithms.h
+++ b/Source/cmStringAlgorithms.h
@@ -12,6 +12,7 @@
 #include <utility>
 #include <vector>
 
+#include <cm/optional>
 #include <cm/string_view>
 
 #include "cmRange.h"
@@ -146,7 +147,8 @@
 }
 
 /** Concatenate string pieces into a single string.  */
-std::string cmCatViews(std::initializer_list<cm::string_view> views);
+std::string cmCatViews(cm::optional<std::string>&& first,
+                       std::initializer_list<cm::string_view> views);
 
 /** Utility class for cmStrCat.  */
 class cmAlphaNum
@@ -189,13 +191,38 @@
   char Digits_[32];
 };
 
-/** Concatenate string pieces and numbers into a single string.  */
-template <typename... AV>
-inline std::string cmStrCat(cmAlphaNum const& a, cmAlphaNum const& b,
-                            AV const&... args)
+template <typename A, typename B, typename... AV>
+class cmStrCatHelper
 {
-  return cmCatViews(
-    { a.View(), b.View(), static_cast<cmAlphaNum const&>(args).View()... });
+public:
+  static std::string Compute(cmAlphaNum const& a, cmAlphaNum const& b,
+                             AV const&... args)
+  {
+    return cmCatViews(
+      cm::nullopt,
+      { a.View(), b.View(), static_cast<cmAlphaNum const&>(args).View()... });
+  }
+};
+
+template <typename B, typename... AV>
+class cmStrCatHelper<std::string, B, AV...>
+{
+public:
+  static std::string Compute(std::string&& a, cmAlphaNum const& b,
+                             AV const&... args)
+  {
+    return cmCatViews(
+      std::move(a),
+      { b.View(), static_cast<cmAlphaNum const&>(args).View()... });
+  }
+};
+
+/** Concatenate string pieces and numbers into a single string.  */
+template <typename A, typename B, typename... AV>
+inline std::string cmStrCat(A&& a, B&& b, AV&&... args)
+{
+  return cmStrCatHelper<A, B, AV...>::Compute(
+    std::forward<A>(a), std::forward<B>(b), std::forward<AV>(args)...);
 }
 
 /** Joins wrapped elements of a range with separator into a single string.  */
@@ -207,7 +234,9 @@
     return std::string();
   }
   return cmCatViews(
-    { prefix, cmJoin(rng, cmCatViews({ suffix, sep, prefix })), suffix });
+    cm::nullopt,
+    { prefix, cmJoin(rng, cmCatViews(cm::nullopt, { suffix, sep, prefix })),
+      suffix });
 }
 
 /** Joins wrapped elements of a range with separator into a single string.  */
diff --git a/Source/cmStringCommand.cxx b/Source/cmStringCommand.cxx
index c12d1fe..5a64588 100644
--- a/Source/cmStringCommand.cxx
+++ b/Source/cmStringCommand.cxx
@@ -9,7 +9,6 @@
 #include <cctype>
 #include <cstdio>
 #include <cstdlib>
-#include <initializer_list>
 #include <limits>
 #include <memory>
 #include <stdexcept>
@@ -950,9 +949,9 @@
 class json_error : public std::runtime_error
 {
 public:
-  json_error(std::initializer_list<cm::string_view> message,
+  json_error(const std::string& message,
              cm::optional<Args> errorPath = cm::nullopt)
-    : std::runtime_error(cmCatViews(message))
+    : std::runtime_error(message)
     , ErrorPath{
       std::move(errorPath) // NOLINT(performance-move-const-arg)
     }
@@ -964,7 +963,7 @@
 const std::string& Args::PopFront(cm::string_view error)
 {
   if (this->empty()) {
-    throw json_error({ error });
+    throw json_error(std::string(error));
   }
   const std::string& res = *this->begin();
   this->advance(1);
@@ -974,7 +973,7 @@
 const std::string& Args::PopBack(cm::string_view error)
 {
   if (this->empty()) {
-    throw json_error({ error });
+    throw json_error(std::string(error));
   }
   const std::string& res = *(this->end() - 1);
   this->retreat(1);
@@ -999,7 +998,7 @@
     case Json::ValueType::objectValue:
       return "OBJECT"_s;
   }
-  throw json_error({ "invalid JSON type found"_s });
+  throw json_error("invalid JSON type found");
 }
 
 int ParseIndex(
@@ -1008,14 +1007,14 @@
 {
   unsigned long lindex;
   if (!cmStrToULong(str, &lindex)) {
-    throw json_error({ "expected an array index, got: '"_s, str, "'"_s },
+    throw json_error(cmStrCat("expected an array index, got: '"_s, str, "'"_s),
                      progress);
   }
   Json::ArrayIndex index = static_cast<Json::ArrayIndex>(lindex);
   if (index >= max) {
     cmAlphaNum sizeStr{ max };
-    throw json_error({ "expected an index less than "_s, sizeStr.View(),
-                       " got '"_s, str, "'"_s },
+    throw json_error(cmStrCat("expected an index less than "_s, sizeStr.View(),
+                              " got '"_s, str, "'"_s),
                      progress);
   }
   return index;
@@ -1036,16 +1035,16 @@
     } else if (search->isObject()) {
       if (!search->isMember(field)) {
         const auto progressStr = cmJoin(progress, " "_s);
-        throw json_error({ "member '"_s, progressStr, "' not found"_s },
+        throw json_error(cmStrCat("member '"_s, progressStr, "' not found"_s),
                          progress);
       }
       search = &(*search)[field];
     } else {
       const auto progressStr = cmJoin(progress, " "_s);
       throw json_error(
-        { "invalid path '"_s, progressStr,
-          "', need element of OBJECT or ARRAY type to lookup '"_s, field,
-          "' got "_s, JsonTypeToString(search->type()) },
+        cmStrCat("invalid path '"_s, progressStr,
+                 "', need element of OBJECT or ARRAY type to lookup '"_s,
+                 field, "' got "_s, JsonTypeToString(search->type())),
         progress);
     }
   }
@@ -1061,7 +1060,7 @@
   std::string error;
   if (!jsonReader->parse(jsonstr.data(), jsonstr.data() + jsonstr.size(),
                          &json, &error)) {
-    throw json_error({ "failed parsing json string: "_s, error });
+    throw json_error(cmStrCat("failed parsing json string: "_s, error));
   }
   return json;
 }
@@ -1101,9 +1100,9 @@
         mode != "LENGTH"_s && mode != "REMOVE"_s && mode != "SET"_s &&
         mode != "EQUAL"_s) {
       throw json_error(
-        { "got an invalid mode '"_s, mode,
-          "', expected one of GET, TYPE, MEMBER, LENGTH, REMOVE, SET, "
-          " EQUAL"_s });
+        cmStrCat("got an invalid mode '"_s, mode,
+                 "', expected one of GET, TYPE, MEMBER, LENGTH, REMOVE, SET, "
+                 " EQUAL"_s));
     }
 
     const auto& jsonstr = args.PopFront("missing json string argument"_s);
@@ -1127,10 +1126,11 @@
       const auto& indexStr = args.PopBack("missing member index"_s);
       const auto& value = ResolvePath(json, args);
       if (!value.isObject()) {
-        throw json_error({ "MEMBER needs to be called with an element of "
-                           "type OBJECT, got "_s,
-                           JsonTypeToString(value.type()) },
-                         args);
+        throw json_error(
+          cmStrCat("MEMBER needs to be called with an element of "
+                   "type OBJECT, got "_s,
+                   JsonTypeToString(value.type())),
+          args);
       }
       const auto index = ParseIndex(
         indexStr, Args{ args.begin(), args.end() + 1 }, value.size());
@@ -1140,9 +1140,9 @@
     } else if (mode == "LENGTH"_s) {
       const auto& value = ResolvePath(json, args);
       if (!value.isArray() && !value.isObject()) {
-        throw json_error({ "LENGTH needs to be called with an "
-                           "element of type ARRAY or OBJECT, got "_s,
-                           JsonTypeToString(value.type()) },
+        throw json_error(cmStrCat("LENGTH needs to be called with an "
+                                  "element of type ARRAY or OBJECT, got "_s,
+                                  JsonTypeToString(value.type())),
                          args);
       }
 
@@ -1165,9 +1165,9 @@
         value.removeMember(toRemove, &removed);
 
       } else {
-        throw json_error({ "REMOVE needs to be called with an "
-                           "element of type ARRAY or OBJECT, got "_s,
-                           JsonTypeToString(value.type()) },
+        throw json_error(cmStrCat("REMOVE needs to be called with an "
+                                  "element of type ARRAY or OBJECT, got "_s,
+                                  JsonTypeToString(value.type())),
                          args);
       }
       makefile.AddDefinition(*outputVariable, WriteJson(json));
@@ -1189,9 +1189,9 @@
           value.append(newValue);
         }
       } else {
-        throw json_error({ "SET needs to be called with an "
-                           "element of type OBJECT or ARRAY, got "_s,
-                           JsonTypeToString(value.type()) });
+        throw json_error(cmStrCat("SET needs to be called with an "
+                                  "element of type OBJECT or ARRAY, got "_s,
+                                  JsonTypeToString(value.type())));
       }
 
       makefile.AddDefinition(*outputVariable, WriteJson(json));
@@ -1207,7 +1207,7 @@
     if (outputVariable && e.ErrorPath) {
       const auto errorPath = cmJoin(*e.ErrorPath, "-");
       makefile.AddDefinition(*outputVariable,
-                             cmCatViews({ errorPath, "-NOTFOUND"_s }));
+                             cmStrCat(errorPath, "-NOTFOUND"_s));
     } else if (outputVariable) {
       makefile.AddDefinition(*outputVariable, "NOTFOUND"_s);
     }
@@ -1215,7 +1215,7 @@
     if (errorVariable) {
       makefile.AddDefinition(*errorVariable, e.what());
     } else {
-      status.SetError(cmCatViews({ "sub-command JSON "_s, e.what(), "."_s }));
+      status.SetError(cmStrCat("sub-command JSON "_s, e.what(), "."_s));
       success = false;
     }
   }
diff --git a/Tests/CMakeLib/testStringAlgorithms.cxx b/Tests/CMakeLib/testStringAlgorithms.cxx
index 1e6b611..f73e62a 100644
--- a/Tests/CMakeLib/testStringAlgorithms.cxx
+++ b/Tests/CMakeLib/testStringAlgorithms.cxx
@@ -6,6 +6,8 @@
 #include <iostream>
 #include <sstream>
 #include <string>
+#include <type_traits>
+#include <utility>
 #include <vector>
 
 #include <cm/string_view>
@@ -144,6 +146,18 @@
     d -= val;
     assert_ok((d < div) && (d > -div), "cmStrCat double");
   }
+  {
+    std::string val;
+    std::string expect;
+    val.reserve(120 * cmStrLen("cmStrCat move"));
+    auto data = val.data();
+    for (int i = 0; i < 100; i++) {
+      val = cmStrCat(std::move(val), "cmStrCat move");
+      expect += "cmStrCat move";
+    }
+    assert_ok((val.data() == data), "cmStrCat move");
+    assert_string(val, expect, "cmStrCat move");
+  }
 
   // ----------------------------------------------------------------------
   // Test cmWrap
diff --git a/Tests/Cuda/Toolkit/CMakeLists.txt b/Tests/Cuda/Toolkit/CMakeLists.txt
index 4df29fa..e6b298d 100644
--- a/Tests/Cuda/Toolkit/CMakeLists.txt
+++ b/Tests/Cuda/Toolkit/CMakeLists.txt
@@ -30,6 +30,8 @@
 foreach (cuda_lib IN LISTS cuda_libs)
   if(NOT CUDA_${cuda_lib}_LIBRARY)
     message(FATAL_ERROR "expected CUDAToolkit variable CUDA_${cuda_lib}_LIBRARY not found")
+  elseif(CUDA_${cuda_lib}_LIBRARY MATCHES [[\.\./]])
+    message(FATAL_ERROR "expected CUDAToolkit variable CUDA_${cuda_lib}_LIBRARY[\"${CUDA_${cuda_lib}_LIBRARY}\"] to not contain /..")
   endif()
   if(NOT TARGET CUDA::${cuda_lib})
     message(FATAL_ERROR "expected CUDAToolkit target CUDA::${cuda_lib} not found")
@@ -41,6 +43,9 @@
   list(APPEND npp_libs nppicom)
 endif()
 foreach (cuda_lib IN LISTS npp_libs)
+  if(CUDA_${cuda_lib}_LIBRARY MATCHES [[\.\./]])
+    message(FATAL_ERROR "expected CUDAToolkit variable CUDA_${cuda_lib}_LIBRARY[\"${CUDA_${cuda_lib}_LIBRARY}\"] to not contain /..")
+  endif()
   if(NOT TARGET CUDA::${cuda_lib})
     message(FATAL_ERROR "The CUDA::${cuda_lib} target was expected but couldn't be found")
   endif()
diff --git a/Tests/CudaOnly/Toolkit/CMakeLists.txt b/Tests/CudaOnly/Toolkit/CMakeLists.txt
index 1486c1a..ddf940b 100644
--- a/Tests/CudaOnly/Toolkit/CMakeLists.txt
+++ b/Tests/CudaOnly/Toolkit/CMakeLists.txt
@@ -28,6 +28,8 @@
 foreach (cuda_lib IN LISTS cuda_libs)
   if(NOT CUDA_${cuda_lib}_LIBRARY)
     message(FATAL_ERROR "expected CUDAToolkit variable CUDA_${cuda_lib}_LIBRARY not found")
+  elseif(CUDA_${cuda_lib}_LIBRARY MATCHES [[\.\./]])
+    message(FATAL_ERROR "expected CUDAToolkit variable CUDA_${cuda_lib}_LIBRARY[\"${CUDA_${cuda_lib}_LIBRARY}\"] to not contain /..")
   endif()
   if(NOT TARGET CUDA::${cuda_lib})
     message(FATAL_ERROR "expected CUDAToolkit target CUDA::${cuda_lib} not found")
@@ -41,6 +43,8 @@
 foreach (cuda_lib )
   if(NOT CUDA_${cuda_lib}_LIBRARY)
     message(FATAL_ERROR "expected CUDAToolkit variable CUDA_${cuda_lib}_LIBRARY not found")
+  elseif(CUDA_${cuda_lib}_LIBRARY MATCHES [[\.\./]])
+    message(FATAL_ERROR "expected CUDAToolkit variable CUDA_${cuda_lib}_LIBRARY[\"${CUDA_${cuda_lib}_LIBRARY}\"] to not contain /..")
   endif()
   if(NOT TARGET CUDA::${cuda_lib})
     message(FATAL_ERROR "expected CUDAToolkit target CUDA::${cuda_lib} not found")
@@ -50,6 +54,8 @@
 foreach (cuda_lib nvrtc nvToolsExt OpenCL)
   if(NOT CUDA_${cuda_lib}_LIBRARY)
     message(FATAL_ERROR "expected CUDAToolkit variable CUDA_${cuda_lib}_LIBRARY not found")
+  elseif(CUDA_${cuda_lib}_LIBRARY MATCHES [[\.\./]])
+    message(FATAL_ERROR "expected CUDAToolkit variable CUDA_${cuda_lib}_LIBRARY[\"${CUDA_${cuda_lib}_LIBRARY}\"] to not contain /..")
   endif()
 
   if(NOT TARGET CUDA::${cuda_lib})