Просмотр исходного кода

Ninja Multi-Config: Add support for cross-config custom commands

Co-Author: Brad King <[email protected]>
Kyle Edwards 5 лет назад
Родитель
Сommit
dcf9f4d2f7

+ 9 - 0
Help/command/add_custom_command.rst

@@ -398,3 +398,12 @@ after linking.
   will run ``someHasher`` after linking ``myPlugin``, e.g. to produce a ``.c``
   file containing code to check the hash of ``myPlugin`` that the ``myExe``
   executable can use to verify it before loading.
+
+Ninja Multi-Config
+^^^^^^^^^^^^^^^^^^
+
+.. versionadded:: 3.20
+
+  ``add_custom_command`` supports the :generator:`Ninja Multi-Config`
+  generator's cross-config capabilities. See the generator documentation
+  for more information.

+ 9 - 0
Help/command/add_custom_target.rst

@@ -168,3 +168,12 @@ The options are:
   .. versionadded:: 3.13
     Arguments to ``WORKING_DIRECTORY`` may use
     :manual:`generator expressions <cmake-generator-expressions(7)>`.
+
+Ninja Multi-Config
+^^^^^^^^^^^^^^^^^^
+
+.. versionadded:: 3.20
+
+  ``add_custom_target`` supports the :generator:`Ninja Multi-Config`
+  generator's cross-config capabilities. See the generator documentation
+  for more information.

+ 44 - 0
Help/generator/Ninja Multi-Config.rst

@@ -86,3 +86,47 @@ used to generate ``generated.c``, which would be used to build the ``Debug``
 configuration of ``generated``. This is useful for running a release-optimized
 version of a generator utility while still building the debug version of the
 targets built with the generated code.
+
+Custom Commands
+^^^^^^^^^^^^^^^
+
+.. versionadded:: 3.20
+
+The ``Ninja Multi-Config`` generator adds extra capabilities to
+:command:`add_custom_command` and :command:`add_custom_target` through its
+cross-config mode. The ``COMMAND``, ``DEPENDS``, and ``WORKING_DIRECTORY``
+arguments can be evaluated in the context of either the "command config" (the
+"native" configuration of the ``build-<Config>.ninja`` file in use) or the
+"output config" (the configuration used to evaluate the ``OUTPUT`` and
+``BYPRODUCTS``).
+
+If either ``OUTPUT`` or ``BYPRODUCTS`` names a path that is common to
+more than one configuration (e.g. it does not use any generator expressions),
+all arguments are evaluated in the command config by default.
+If all ``OUTPUT`` and ``BYPRODUCTS`` paths are unique to each configuration
+(e.g. by using the ``$<CONFIG>`` generator expression), the first argument of
+``COMMAND`` is still evaluated in the command config by default, while all
+subsequent arguments, as well as the arguments to ``DEPENDS`` and
+``WORKING_DIRECTORY``, are evaluated in the output config. These defaults can
+be overridden with the ``$<OUTPUT_CONFIG:...>`` and ``$<COMMAND_CONFIG:...>``
+generator-expressions. Note that if a target is specified by its name in
+``DEPENDS``, or as the first argument of ``COMMAND``, it is always evaluated
+in the command config, even if it is wrapped in ``$<OUTPUT_CONFIG:...>``
+(because its plain name is not a generator expression).
+
+As an example, consider the following:
+
+.. code-block:: cmake
+
+  add_custom_command(
+    OUTPUT "$<CONFIG>.txt"
+    COMMAND generator "$<CONFIG>.txt" "$<OUTPUT_CONFIG:$<CONFIG>>" "$<COMMAND_CONFIG:$<CONFIG>>"
+    DEPENDS tgt1 "$<TARGET_FILE:tgt2>" "$<OUTPUT_CONFIG:$<TARGET_FILE:tgt3>>" "$<COMMAND_CONFIG:$<TARGET_FILE:tgt4>>"
+    )
+
+Assume that ``generator``, ``tgt1``, ``tgt2``, ``tgt3``, and ``tgt4`` are all
+executable targets, and assume that ``$<CONFIG>.txt`` is built in the ``Debug``
+output config using the ``Release`` command config. The ``Release`` build of
+the ``generator`` target is called with ``Debug.txt Debug Release`` as
+arguments. The command depends on the ``Release`` builds of ``tgt1`` and
+``tgt4``, and the ``Debug`` builds of ``tgt2`` and ``tgt3``.

+ 18 - 0
Help/manual/cmake-generator-expressions.7.rst

@@ -816,6 +816,24 @@ Output-Related Expressions
   ``;`` on Windows).  Be sure to enclose the argument containing this genex
   in double quotes in CMake source code so that ``;`` does not split arguments.
 
+``$<OUTPUT_CONFIG:...>``
+  .. versionadded:: 3.20
+
+  Only valid in :command:`add_custom_command` and :command:`add_custom_target`
+  as the outer-most generator expression in an argument.
+  With the :generator:`Ninja Multi-Config` generator, generator expressions
+  in ``...`` are evaluated using the custom command's "output config".
+  With other generators, the content of ``...`` is evaluated normally.
+
+``$<COMMAND_CONFIG:...>``
+  .. versionadded:: 3.20
+
+  Only valid in :command:`add_custom_command` and :command:`add_custom_target`
+  as the outer-most generator expression in an argument.
+  With the :generator:`Ninja Multi-Config` generator, generator expressions
+  in ``...`` are evaluated using the custom command's "command config".
+  With other generators, the content of ``...`` is evaluated normally.
+
 Debugging
 =========
 

+ 5 - 0
Help/release/dev/custom-command-output-genex.rst

@@ -4,3 +4,8 @@ custom-command-output-genex
 * :command:`add_custom_command` and :command:`add_custom_target` now
   support :manual:`generator expressions <cmake-generator-expressions(7)>`
   in their ``OUTPUT`` and ``BYPRODUCTS`` options.
+
+  Their ``COMMAND``, ``WORKING_DIRECTORY``, and ``DEPENDS`` options gained
+  support for new generator expressions ``$<COMMAND_CONFIG:...>`` and
+  ``$<OUTPUT_CONFIG:...>`` that control cross-config handling when using
+  the :generator:`Ninja Multi-Config` generator.

+ 110 - 22
Source/cmCustomCommandGenerator.cxx

@@ -7,7 +7,9 @@
 #include <utility>
 
 #include <cm/optional>
+#include <cm/string_view>
 #include <cmext/algorithm>
+#include <cmext/string_view>
 
 #include "cmCryptoHash.h"
 #include "cmCustomCommand.h"
@@ -24,15 +26,95 @@
 #include "cmTransformDepfile.h"
 
 namespace {
+std::string EvaluateSplitConfigGenex(
+  cm::string_view input, cmGeneratorExpression const& ge, cmLocalGenerator* lg,
+  bool useOutputConfig, std::string const& outputConfig,
+  std::string const& commandConfig,
+  std::set<BT<std::pair<std::string, bool>>>* utils = nullptr)
+{
+  std::string result;
+
+  while (!input.empty()) {
+    // Copy non-genex content directly to the result.
+    std::string::size_type pos = input.find("$<");
+    result += input.substr(0, pos);
+    if (pos == std::string::npos) {
+      break;
+    }
+    input = input.substr(pos);
+
+    // Find the balanced end of this regex.
+    size_t nestingLevel = 1;
+    for (pos = 2; pos < input.size(); ++pos) {
+      cm::string_view cur = input.substr(pos);
+      if (cmHasLiteralPrefix(cur, "$<")) {
+        ++nestingLevel;
+        ++pos;
+        continue;
+      }
+      if (cmHasLiteralPrefix(cur, ">")) {
+        --nestingLevel;
+        if (nestingLevel == 0) {
+          ++pos;
+          break;
+        }
+      }
+    }
+
+    // Split this genex from following input.
+    cm::string_view genex = input.substr(0, pos);
+    input = input.substr(pos);
+
+    // Convert an outer COMMAND_CONFIG or OUTPUT_CONFIG to the matching config.
+    std::string const* config =
+      useOutputConfig ? &outputConfig : &commandConfig;
+    if (nestingLevel == 0) {
+      static cm::string_view const COMMAND_CONFIG = "$<COMMAND_CONFIG:"_s;
+      static cm::string_view const OUTPUT_CONFIG = "$<OUTPUT_CONFIG:"_s;
+      if (cmHasPrefix(genex, COMMAND_CONFIG)) {
+        genex.remove_prefix(COMMAND_CONFIG.size());
+        genex.remove_suffix(1);
+        useOutputConfig = false;
+        config = &commandConfig;
+      } else if (cmHasPrefix(genex, OUTPUT_CONFIG)) {
+        genex.remove_prefix(OUTPUT_CONFIG.size());
+        genex.remove_suffix(1);
+        useOutputConfig = true;
+        config = &outputConfig;
+      }
+    }
+
+    // Evaluate this genex in the selected configuration.
+    std::unique_ptr<cmCompiledGeneratorExpression> cge =
+      ge.Parse(std::string(genex));
+    result += cge->Evaluate(lg, *config);
+
+    // Record targets referenced by the genex.
+    if (utils) {
+      // FIXME: What is the proper condition for a cross-dependency?
+      bool const cross = !useOutputConfig;
+      for (cmGeneratorTarget* gt : cge->GetTargets()) {
+        utils->emplace(BT<std::pair<std::string, bool>>(
+          { gt->GetName(), cross }, cge->GetBacktrace()));
+      }
+    }
+  }
+
+  return result;
+}
+
 std::vector<std::string> EvaluateDepends(std::vector<std::string> const& paths,
                                          cmGeneratorExpression const& ge,
                                          cmLocalGenerator* lg,
-                                         std::string const& config)
+                                         std::string const& outputConfig,
+                                         std::string const& commandConfig)
 {
   std::vector<std::string> depends;
   for (std::string const& p : paths) {
-    std::unique_ptr<cmCompiledGeneratorExpression> cge = ge.Parse(p);
-    std::string const& ep = cge->Evaluate(lg, config);
+    std::string const& ep =
+      EvaluateSplitConfigGenex(p, ge, lg, /*useOutputConfig=*/true,
+                               /*outputConfig=*/outputConfig,
+                               /*commandConfig=*/commandConfig);
     cm::append(depends, cmExpandedList(ep));
   }
   for (std::string& p : depends) {
@@ -59,12 +141,12 @@ std::vector<std::string> EvaluateOutputs(std::vector<std::string> const& paths,
 }
 }
 
-cmCustomCommandGenerator::cmCustomCommandGenerator(cmCustomCommand const& cc,
-                                                   std::string config,
-                                                   cmLocalGenerator* lg,
-                                                   bool transformDepfile)
+cmCustomCommandGenerator::cmCustomCommandGenerator(
+  cmCustomCommand const& cc, std::string config, cmLocalGenerator* lg,
+  bool transformDepfile, cm::optional<std::string> crossConfig)
   : CC(&cc)
-  , Config(std::move(config))
+  , OutputConfig(crossConfig ? *crossConfig : config)
+  , CommandConfig(std::move(config))
   , LG(lg)
   , OldStyle(cc.GetEscapeOldStyle())
   , MakeVars(cc.GetEscapeAllowMakeVars())
@@ -75,18 +157,20 @@ cmCustomCommandGenerator::cmCustomCommandGenerator(cmCustomCommand const& cc,
   const cmCustomCommandLines& cmdlines = this->CC->GetCommandLines();
   for (cmCustomCommandLine const& cmdline : cmdlines) {
     cmCustomCommandLine argv;
+    // For the command itself, we default to the COMMAND_CONFIG.
+    bool useOutputConfig = false;
     for (std::string const& clarg : cmdline) {
-      std::unique_ptr<cmCompiledGeneratorExpression> cge = ge.Parse(clarg);
-      std::string parsed_arg = cge->Evaluate(this->LG, this->Config);
-      for (cmGeneratorTarget* gt : cge->GetTargets()) {
-        this->Utilities.emplace(BT<std::pair<std::string, bool>>(
-          { gt->GetName(), true }, cge->GetBacktrace()));
-      }
+      std::string parsed_arg = EvaluateSplitConfigGenex(
+        clarg, ge, this->LG, useOutputConfig, this->OutputConfig,
+        this->CommandConfig, &this->Utilities);
       if (this->CC->GetCommandExpandLists()) {
         cm::append(argv, cmExpandedList(parsed_arg));
       } else {
         argv.push_back(std::move(parsed_arg));
       }
+
+      // For remaining arguments, we default to the OUTPUT_CONFIG.
+      useOutputConfig = true;
     }
 
     if (!argv.empty()) {
@@ -94,8 +178,10 @@ cmCustomCommandGenerator::cmCustomCommandGenerator(cmCustomCommand const& cc,
       // collect the target to add a target-level dependency on it.
       cmGeneratorTarget* gt = this->LG->FindGeneratorTargetToUse(argv.front());
       if (gt && gt->GetType() == cmStateEnums::EXECUTABLE) {
+        // FIXME: What is the proper condition for a cross-dependency?
+        bool const cross = true;
         this->Utilities.emplace(BT<std::pair<std::string, bool>>(
-          { gt->GetName(), true }, cc.GetBacktrace()));
+          { gt->GetName(), cross }, cc.GetBacktrace()));
       }
     } else {
       // Later code assumes at least one entry exists, but expanding
@@ -137,16 +223,18 @@ cmCustomCommandGenerator::cmCustomCommandGenerator(cmCustomCommand const& cc,
     this->CommandLines.push_back(std::move(argv));
   }
 
-  this->Outputs = EvaluateOutputs(cc.GetOutputs(), ge, this->LG, this->Config);
+  this->Outputs =
+    EvaluateOutputs(cc.GetOutputs(), ge, this->LG, this->OutputConfig);
   this->Byproducts =
-    EvaluateOutputs(cc.GetByproducts(), ge, this->LG, this->Config);
-  this->Depends = EvaluateDepends(cc.GetDepends(), ge, this->LG, this->Config);
+    EvaluateOutputs(cc.GetByproducts(), ge, this->LG, this->OutputConfig);
+  this->Depends = EvaluateDepends(cc.GetDepends(), ge, this->LG,
+                                  this->OutputConfig, this->CommandConfig);
 
   const std::string& workingdirectory = this->CC->GetWorkingDirectory();
   if (!workingdirectory.empty()) {
-    std::unique_ptr<cmCompiledGeneratorExpression> cge =
-      ge.Parse(workingdirectory);
-    this->WorkingDirectory = cge->Evaluate(this->LG, this->Config);
+    this->WorkingDirectory =
+      EvaluateSplitConfigGenex(workingdirectory, ge, this->LG, true,
+                               this->OutputConfig, this->CommandConfig);
     // Convert working directory to a full path.
     if (!this->WorkingDirectory.empty()) {
       std::string const& build_dir = this->LG->GetCurrentBinaryDirectory();
@@ -203,7 +291,7 @@ const char* cmCustomCommandGenerator::GetArgv0Location(unsigned int c) const
       (target->IsImported() ||
        target->GetProperty("CROSSCOMPILING_EMULATOR") ||
        !this->LG->GetMakefile()->IsOn("CMAKE_CROSSCOMPILING"))) {
-    return target->GetLocation(this->Config).c_str();
+    return target->GetLocation(this->CommandConfig).c_str();
   }
   return nullptr;
 }

+ 9 - 2
Source/cmCustomCommandGenerator.h

@@ -9,6 +9,8 @@
 #include <utility>
 #include <vector>
 
+#include <cm/optional>
+
 #include "cmCustomCommandLines.h"
 #include "cmListFileCache.h"
 
@@ -18,7 +20,8 @@ class cmLocalGenerator;
 class cmCustomCommandGenerator
 {
   cmCustomCommand const* CC;
-  std::string Config;
+  std::string OutputConfig;
+  std::string CommandConfig;
   cmLocalGenerator* LG;
   bool OldStyle;
   bool MakeVars;
@@ -36,7 +39,8 @@ class cmCustomCommandGenerator
 
 public:
   cmCustomCommandGenerator(cmCustomCommand const& cc, std::string config,
-                           cmLocalGenerator* lg, bool transformDepfile = true);
+                           cmLocalGenerator* lg, bool transformDepfile = true,
+                           cm::optional<std::string> crossConfig = {});
   cmCustomCommandGenerator(const cmCustomCommandGenerator&) = delete;
   cmCustomCommandGenerator(cmCustomCommandGenerator&&) = default;
   cmCustomCommandGenerator& operator=(const cmCustomCommandGenerator&) =
@@ -55,4 +59,7 @@ public:
   bool HasOnlyEmptyCommandLines() const;
   std::string GetFullDepfile() const;
   std::string GetInternalDepfile() const;
+
+  const std::string& GetOutputConfig() const { return this->OutputConfig; }
+  const std::string& GetCommandConfig() const { return this->CommandConfig; }
 };

+ 1 - 1
Source/cmGeneratorTarget.cxx

@@ -3064,7 +3064,7 @@ bool cmTargetTraceDependencies::IsUtility(std::string const& dep)
     } else {
       // The original name of the dependency was not a full path.  It
       // must name a target, so add the target-level dependency.
-      this->GeneratorTarget->Target->AddUtility(util, false);
+      this->GeneratorTarget->Target->AddUtility(util, true);
       return true;
     }
   }

+ 83 - 14
Source/cmGlobalNinjaGenerator.cxx

@@ -56,6 +56,52 @@ std::string const cmGlobalNinjaGenerator::SHELL_NOOP = "cd .";
 std::string const cmGlobalNinjaGenerator::SHELL_NOOP = ":";
 #endif
 
+bool operator==(
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& lhs,
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& rhs)
+{
+  return lhs.Target == rhs.Target && lhs.Config == rhs.Config &&
+    lhs.GenexOutput == rhs.GenexOutput;
+}
+
+bool operator!=(
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& lhs,
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& rhs)
+{
+  return !(lhs == rhs);
+}
+
+bool operator<(
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& lhs,
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& rhs)
+{
+  return lhs.Target < rhs.Target ||
+    (lhs.Target == rhs.Target &&
+     (lhs.Config < rhs.Config ||
+      (lhs.Config == rhs.Config && lhs.GenexOutput < rhs.GenexOutput)));
+}
+
+bool operator>(
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& lhs,
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& rhs)
+{
+  return rhs < lhs;
+}
+
+bool operator<=(
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& lhs,
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& rhs)
+{
+  return !(lhs > rhs);
+}
+
+bool operator>=(
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& lhs,
+  const cmGlobalNinjaGenerator::ByConfig::TargetDependsClosureKey& rhs)
+{
+  return rhs <= lhs;
+}
+
 void cmGlobalNinjaGenerator::Indent(std::ostream& os, int count)
 {
   for (int i = 0; i < count; ++i) {
@@ -1206,23 +1252,30 @@ void cmGlobalNinjaGenerator::AppendTargetDepends(
 
 void cmGlobalNinjaGenerator::AppendTargetDependsClosure(
   cmGeneratorTarget const* target, cmNinjaDeps& outputs,
-  const std::string& config)
+  const std::string& config, const std::string& fileConfig, bool genexOutput)
 {
   cmNinjaOuts outs;
-  this->AppendTargetDependsClosure(target, outs, config, true);
+  this->AppendTargetDependsClosure(target, outs, config, fileConfig,
+                                   genexOutput, true);
   cm::append(outputs, outs);
 }
 
 void cmGlobalNinjaGenerator::AppendTargetDependsClosure(
   cmGeneratorTarget const* target, cmNinjaOuts& outputs,
-  const std::string& config, bool omit_self)
+  const std::string& config, const std::string& fileConfig, bool genexOutput,
+  bool omit_self)
 {
 
   // try to locate the target in the cache
-  auto find = this->Configs[config].TargetDependsClosures.lower_bound(target);
+  ByConfig::TargetDependsClosureKey key{
+    target,
+    config,
+    genexOutput,
+  };
+  auto find = this->Configs[fileConfig].TargetDependsClosures.lower_bound(key);
 
-  if (find == this->Configs[config].TargetDependsClosures.end() ||
-      find->first != target) {
+  if (find == this->Configs[fileConfig].TargetDependsClosures.end() ||
+      find->first != key) {
     // We now calculate the closure outputs by inspecting the dependent
     // targets recursively.
     // For that we have to distinguish between a local result set that is only
@@ -1232,18 +1285,27 @@ void cmGlobalNinjaGenerator::AppendTargetDependsClosure(
     cmNinjaOuts this_outs; // this will be the new cache entry
 
     for (auto const& dep_target : this->GetTargetDirectDepends(target)) {
-      if (!dep_target->IsInBuildSystem() ||
-          (target->GetType() != cmStateEnums::UTILITY &&
-           dep_target->GetType() != cmStateEnums::UTILITY &&
-           this->EnableCrossConfigBuild() && !dep_target.IsCross())) {
+      if (!dep_target->IsInBuildSystem()) {
+        continue;
+      }
+
+      if (!this->IsSingleConfigUtility(target) &&
+          !this->IsSingleConfigUtility(dep_target) &&
+          this->EnableCrossConfigBuild() && !dep_target.IsCross() &&
+          !genexOutput) {
         continue;
       }
 
-      // Collect the dependent targets for _this_ target
-      this->AppendTargetDependsClosure(dep_target, this_outs, config, false);
+      if (dep_target.IsCross()) {
+        this->AppendTargetDependsClosure(dep_target, this_outs, fileConfig,
+                                         fileConfig, genexOutput, false);
+      } else {
+        this->AppendTargetDependsClosure(dep_target, this_outs, config,
+                                         fileConfig, genexOutput, false);
+      }
     }
-    find = this->Configs[config].TargetDependsClosures.emplace_hint(
-      find, target, std::move(this_outs));
+    find = this->Configs[fileConfig].TargetDependsClosures.emplace_hint(
+      find, key, std::move(this_outs));
   }
 
   // now fill the outputs of the final result from the newly generated cache
@@ -2490,6 +2552,13 @@ std::set<std::string> cmGlobalNinjaGenerator::GetCrossConfigs(
   return result;
 }
 
+bool cmGlobalNinjaGenerator::IsSingleConfigUtility(
+  cmGeneratorTarget const* target) const
+{
+  return target->GetType() == cmStateEnums::UTILITY &&
+    !this->PerConfigUtilityTargets.count(target->GetName());
+}
+
 const char* cmGlobalNinjaMultiGenerator::NINJA_COMMON_FILE =
   "CMakeFiles/common.ninja";
 const char* cmGlobalNinjaMultiGenerator::NINJA_FILE_EXTENSION = ".ninja";

+ 42 - 3
Source/cmGlobalNinjaGenerator.h

@@ -330,10 +330,14 @@ public:
                            cmNinjaTargetDepends depends);
   void AppendTargetDependsClosure(cmGeneratorTarget const* target,
                                   cmNinjaDeps& outputs,
-                                  const std::string& config);
+                                  const std::string& config,
+                                  const std::string& fileConfig,
+                                  bool genexOutput);
   void AppendTargetDependsClosure(cmGeneratorTarget const* target,
                                   cmNinjaOuts& outputs,
-                                  const std::string& config, bool omit_self);
+                                  const std::string& config,
+                                  const std::string& fileConfig,
+                                  bool genexOutput, bool omit_self);
 
   void AppendDirectoryForConfig(const std::string& prefix,
                                 const std::string& config,
@@ -430,6 +434,18 @@ public:
     return this->DefaultConfigs;
   }
 
+  const std::set<std::string>& GetPerConfigUtilityTargets() const
+  {
+    return this->PerConfigUtilityTargets;
+  }
+
+  void AddPerConfigUtilityTarget(const std::string& name)
+  {
+    this->PerConfigUtilityTargets.insert(name);
+  }
+
+  bool IsSingleConfigUtility(cmGeneratorTarget const* target) const;
+
 protected:
   void Generate() override;
 
@@ -523,6 +539,9 @@ private:
   /// The mapping from source file to assumed dependencies.
   std::map<std::string, std::set<std::string>> AssumedSourceDependencies;
 
+  /// Utility targets which have per-config outputs
+  std::set<std::string> PerConfigUtilityTargets;
+
   struct TargetAlias
   {
     cmGeneratorTarget* GeneratorTarget;
@@ -563,7 +582,14 @@ private:
     /// The set of custom commands we have seen.
     std::set<cmCustomCommand const*> CustomCommands;
 
-    std::map<cmGeneratorTarget const*, cmNinjaOuts> TargetDependsClosures;
+    struct TargetDependsClosureKey
+    {
+      cmGeneratorTarget const* Target;
+      std::string Config;
+      bool GenexOutput;
+    };
+
+    std::map<TargetDependsClosureKey, cmNinjaOuts> TargetDependsClosures;
 
     TargetAliasMap TargetAliases;
 
@@ -572,6 +598,19 @@ private:
   std::map<std::string, ByConfig> Configs;
 
   cmNinjaDeps ByproductsForCleanTarget;
+
+  friend bool operator==(const ByConfig::TargetDependsClosureKey& lhs,
+                         const ByConfig::TargetDependsClosureKey& rhs);
+  friend bool operator!=(const ByConfig::TargetDependsClosureKey& lhs,
+                         const ByConfig::TargetDependsClosureKey& rhs);
+  friend bool operator<(const ByConfig::TargetDependsClosureKey& lhs,
+                        const ByConfig::TargetDependsClosureKey& rhs);
+  friend bool operator>(const ByConfig::TargetDependsClosureKey& lhs,
+                        const ByConfig::TargetDependsClosureKey& rhs);
+  friend bool operator<=(const ByConfig::TargetDependsClosureKey& lhs,
+                         const ByConfig::TargetDependsClosureKey& rhs);
+  friend bool operator>=(const ByConfig::TargetDependsClosureKey& lhs,
+                         const ByConfig::TargetDependsClosureKey& rhs);
 };
 
 class cmGlobalNinjaMultiGenerator : public cmGlobalNinjaGenerator

+ 4 - 2
Source/cmLocalGenerator.cxx

@@ -4128,7 +4128,8 @@ void AddUtilityCommand(cmLocalGenerator& lg, const cmListFileBacktrace& lfbt,
   }
 
   // Create the generated symbolic output name of the utility target.
-  std::string output = lg.CreateUtilityOutput(target->GetName());
+  std::string output =
+    lg.CreateUtilityOutput(target->GetName(), byproducts, lfbt);
 
   std::string no_main_dependency;
   cmImplicitDependsList no_implicit_depends;
@@ -4235,7 +4236,8 @@ cmSourceFile* cmLocalGenerator::GetSourceFileWithOutput(
 }
 
 std::string cmLocalGenerator::CreateUtilityOutput(
-  std::string const& targetName)
+  std::string const& targetName, std::vector<std::string> const&,
+  cmListFileBacktrace const&)
 {
   std::string force =
     cmStrCat(this->GetCurrentBinaryDirectory(), "/CMakeFiles/", targetName);

+ 3 - 1
Source/cmLocalGenerator.h

@@ -364,7 +364,9 @@ public:
     bool command_expand_lists = false, const std::string& job_pool = "",
     bool stdPipesUTF8 = false);
 
-  std::string CreateUtilityOutput(std::string const& targetName);
+  virtual std::string CreateUtilityOutput(
+    std::string const& targetName, std::vector<std::string> const& byproducts,
+    cmListFileBacktrace const& bt);
 
   virtual std::vector<cmCustomCommandGenerator> MakeCustomCommandGenerators(
     cmCustomCommand const& cc, std::string const& config);

+ 152 - 42
Source/cmLocalNinjaGenerator.cxx

@@ -10,6 +10,8 @@
 #include <sstream>
 #include <utility>
 
+#include <cmext/string_view>
+
 #include "cmsys/FStream.hxx"
 
 #include "cmCryptoHash.h"
@@ -567,16 +569,45 @@ void cmLocalNinjaGenerator::AppendCustomCommandLines(
 }
 
 void cmLocalNinjaGenerator::WriteCustomCommandBuildStatement(
-  cmCustomCommand const* cc, const cmNinjaDeps& orderOnlyDeps,
-  const std::string& config)
+  cmCustomCommand const* cc, const std::set<cmGeneratorTarget*>& targets,
+  const std::string& fileConfig)
 {
   cmGlobalNinjaGenerator* gg = this->GetGlobalNinjaGenerator();
-  if (gg->SeenCustomCommand(cc, config)) {
+  if (gg->SeenCustomCommand(cc, fileConfig)) {
     return;
   }
 
-  for (cmCustomCommandGenerator const& ccg :
-       this->MakeCustomCommandGenerators(*cc, config)) {
+  auto ccgs = this->MakeCustomCommandGenerators(*cc, fileConfig);
+  for (cmCustomCommandGenerator const& ccg : ccgs) {
+    cmNinjaDeps orderOnlyDeps;
+
+    // A custom command may appear on multiple targets.  However, some build
+    // systems exist where the target dependencies on some of the targets are
+    // overspecified, leading to a dependency cycle.  If we assume all target
+    // dependencies are a superset of the true target dependencies for this
+    // custom command, we can take the set intersection of all target
+    // dependencies to obtain a correct dependency list.
+    //
+    // FIXME: This won't work in certain obscure scenarios involving indirect
+    // dependencies.
+    auto j = targets.begin();
+    assert(j != targets.end());
+    this->GetGlobalNinjaGenerator()->AppendTargetDependsClosure(
+      *j, orderOnlyDeps, ccg.GetOutputConfig(), fileConfig, ccgs.size() > 1);
+    std::sort(orderOnlyDeps.begin(), orderOnlyDeps.end());
+    ++j;
+
+    for (; j != targets.end(); ++j) {
+      std::vector<std::string> jDeps;
+      std::vector<std::string> depsIntersection;
+      this->GetGlobalNinjaGenerator()->AppendTargetDependsClosure(
+        *j, jDeps, ccg.GetOutputConfig(), fileConfig, ccgs.size() > 1);
+      std::sort(jDeps.begin(), jDeps.end());
+      std::set_intersection(orderOnlyDeps.begin(), orderOnlyDeps.end(),
+                            jDeps.begin(), jDeps.end(),
+                            std::back_inserter(depsIntersection));
+      orderOnlyDeps = depsIntersection;
+    }
 
     const std::vector<std::string>& outputs = ccg.GetOutputs();
     const std::vector<std::string>& byproducts = ccg.GetByproducts();
@@ -603,7 +634,7 @@ void cmLocalNinjaGenerator::WriteCustomCommandBuildStatement(
     }
 
     cmNinjaDeps ninjaDeps;
-    this->AppendCustomCommandDeps(ccg, ninjaDeps, config);
+    this->AppendCustomCommandDeps(ccg, ninjaDeps, fileConfig);
 
     std::vector<std::string> cmdLines;
     this->AppendCustomCommandLines(ccg, cmdLines);
@@ -614,7 +645,7 @@ void cmLocalNinjaGenerator::WriteCustomCommandBuildStatement(
       build.Outputs = std::move(ninjaOutputs);
       build.ExplicitDeps = std::move(ninjaDeps);
       build.OrderOnlyDeps = orderOnlyDeps;
-      gg->WriteBuild(this->GetImplFileStream(config), build);
+      gg->WriteBuild(this->GetImplFileStream(fileConfig), build);
     } else {
       std::string customStep = cmSystemTools::GetFilenameName(ninjaOutputs[0]);
       // Hash full path to make unique.
@@ -652,16 +683,92 @@ void cmLocalNinjaGenerator::WriteCustomCommandBuildStatement(
         this->BuildCommandLine(cmdLines, customStep),
         this->ConstructComment(ccg), "Custom command for " + ninjaOutputs[0],
         depfile, cc->GetJobPool(), cc->GetUsesTerminal(),
-        /*restat*/ !symbolic || !byproducts.empty(), ninjaOutputs, config,
+        /*restat*/ !symbolic || !byproducts.empty(), ninjaOutputs, fileConfig,
         ninjaDeps, orderOnlyDeps);
     }
   }
 }
 
+namespace {
+bool HasUniqueByproducts(cmLocalGenerator& lg,
+                         std::vector<std::string> const& byproducts,
+                         cmListFileBacktrace const& bt)
+{
+  std::vector<std::string> configs =
+    lg.GetMakefile()->GetGeneratorConfigs(cmMakefile::IncludeEmptyConfig);
+  cmGeneratorExpression ge(bt);
+  for (std::string const& p : byproducts) {
+    if (cmGeneratorExpression::Find(p) == std::string::npos) {
+      return false;
+    }
+    std::set<std::string> seen;
+    std::unique_ptr<cmCompiledGeneratorExpression> cge = ge.Parse(p);
+    for (std::string const& config : configs) {
+      for (std::string const& b :
+           lg.ExpandCustomCommandOutputPaths(*cge, config)) {
+        if (!seen.insert(b).second) {
+          return false;
+        }
+      }
+    }
+  }
+  return true;
+}
+
+bool HasUniqueOutputs(std::vector<cmCustomCommandGenerator> const& ccgs)
+{
+  std::set<std::string> allOutputs;
+  std::set<std::string> allByproducts;
+  for (cmCustomCommandGenerator const& ccg : ccgs) {
+    for (std::string const& output : ccg.GetOutputs()) {
+      if (!allOutputs.insert(output).second) {
+        return false;
+      }
+    }
+    for (std::string const& byproduct : ccg.GetByproducts()) {
+      if (!allByproducts.insert(byproduct).second) {
+        return false;
+      }
+    }
+  }
+  return true;
+}
+}
+
+std::string cmLocalNinjaGenerator::CreateUtilityOutput(
+  std::string const& targetName, std::vector<std::string> const& byproducts,
+  cmListFileBacktrace const& bt)
+{
+  // In Ninja Multi-Config, we can only produce cross-config utility
+  // commands if all byproducts are per-config.
+  if (!this->GetGlobalGenerator()->IsMultiConfig() ||
+      !HasUniqueByproducts(*this, byproducts, bt)) {
+    return this->cmLocalGenerator::CreateUtilityOutput(targetName, byproducts,
+                                                       bt);
+  }
+
+  std::string const base = cmStrCat(this->GetCurrentBinaryDirectory(),
+                                    "/CMakeFiles/", targetName, '-');
+  // The output is not actually created so mark it symbolic.
+  for (std::string const& config :
+       this->Makefile->GetGeneratorConfigs(cmMakefile::IncludeEmptyConfig)) {
+    std::string const force = cmStrCat(base, config);
+    if (cmSourceFile* sf = this->Makefile->GetOrCreateGeneratedSource(force)) {
+      sf->SetProperty("SYMBOLIC", "1");
+    } else {
+      cmSystemTools::Error("Could not get source file entry for " + force);
+    }
+  }
+  this->GetGlobalNinjaGenerator()->AddPerConfigUtilityTarget(targetName);
+  return cmStrCat(base, "$<CONFIG>"_s);
+}
+
 std::vector<cmCustomCommandGenerator>
-cmLocalNinjaGenerator::MakeCustomCommandGenerators(cmCustomCommand const& cc,
-                                                   std::string const& config)
+cmLocalNinjaGenerator::MakeCustomCommandGenerators(
+  cmCustomCommand const& cc, std::string const& fileConfig)
 {
+  cmGlobalNinjaGenerator const* gg = this->GetGlobalNinjaGenerator();
+
   bool transformDepfile = false;
   switch (this->GetPolicyStatus(cmPolicies::CMP0116)) {
     case cmPolicies::OLD:
@@ -674,8 +781,40 @@ cmLocalNinjaGenerator::MakeCustomCommandGenerators(cmCustomCommand const& cc,
       break;
   }
 
+  // Start with the build graph's configuration.
   std::vector<cmCustomCommandGenerator> ccgs;
-  ccgs.emplace_back(cc, config, this, transformDepfile);
+  ccgs.emplace_back(cc, fileConfig, this, transformDepfile);
+
+  // Consider adding cross configurations.
+  if (!gg->EnableCrossConfigBuild()) {
+    return ccgs;
+  }
+
+  // Outputs and byproducts must be expressed using generator expressions.
+  for (std::string const& output : cc.GetOutputs()) {
+    if (cmGeneratorExpression::Find(output) == std::string::npos) {
+      return ccgs;
+    }
+  }
+  for (std::string const& byproduct : cc.GetByproducts()) {
+    if (cmGeneratorExpression::Find(byproduct) == std::string::npos) {
+      return ccgs;
+    }
+  }
+
+  // Tentatively add the other cross configurations.
+  for (std::string const& config : gg->GetCrossConfigs(fileConfig)) {
+    if (fileConfig != config) {
+      ccgs.emplace_back(cc, fileConfig, this, transformDepfile, config);
+    }
+  }
+
+  // If outputs and byproducts are not unique to each configuration,
+  // drop the cross configurations.
+  if (!HasUniqueOutputs(ccgs)) {
+    ccgs.erase(ccgs.begin() + 1, ccgs.end());
+  }
+
   return ccgs;
 }
 
@@ -692,42 +831,13 @@ void cmLocalNinjaGenerator::AddCustomCommandTarget(cmCustomCommand const* cc,
 }
 
 void cmLocalNinjaGenerator::WriteCustomCommandBuildStatements(
-  const std::string& config)
+  const std::string& fileConfig)
 {
   for (cmCustomCommand const* customCommand : this->CustomCommands) {
     auto i = this->CustomCommandTargets.find(customCommand);
     assert(i != this->CustomCommandTargets.end());
 
-    // A custom command may appear on multiple targets.  However, some build
-    // systems exist where the target dependencies on some of the targets are
-    // overspecified, leading to a dependency cycle.  If we assume all target
-    // dependencies are a superset of the true target dependencies for this
-    // custom command, we can take the set intersection of all target
-    // dependencies to obtain a correct dependency list.
-    //
-    // FIXME: This won't work in certain obscure scenarios involving indirect
-    // dependencies.
-    auto j = i->second.begin();
-    assert(j != i->second.end());
-    std::vector<std::string> ccTargetDeps;
-    this->GetGlobalNinjaGenerator()->AppendTargetDependsClosure(
-      *j, ccTargetDeps, config);
-    std::sort(ccTargetDeps.begin(), ccTargetDeps.end());
-    ++j;
-
-    for (; j != i->second.end(); ++j) {
-      std::vector<std::string> jDeps;
-      std::vector<std::string> depsIntersection;
-      this->GetGlobalNinjaGenerator()->AppendTargetDependsClosure(*j, jDeps,
-                                                                  config);
-      std::sort(jDeps.begin(), jDeps.end());
-      std::set_intersection(ccTargetDeps.begin(), ccTargetDeps.end(),
-                            jDeps.begin(), jDeps.end(),
-                            std::back_inserter(depsIntersection));
-      ccTargetDeps = depsIntersection;
-    }
-
-    this->WriteCustomCommandBuildStatement(i->first, ccTargetDeps, config);
+    this->WriteCustomCommandBuildStatement(i->first, i->second, fileConfig);
   }
 }
 

+ 8 - 3
Source/cmLocalNinjaGenerator.h

@@ -10,6 +10,7 @@
 #include <string>
 #include <vector>
 
+#include "cmListFileCache.h"
 #include "cmLocalCommonGenerator.h"
 #include "cmNinjaTypes.h"
 #include "cmOutputConverter.h"
@@ -70,6 +71,10 @@ public:
                            const std::string& fileConfig,
                            cmNinjaTargetDepends depends);
 
+  std::string CreateUtilityOutput(std::string const& targetName,
+                                  std::vector<std::string> const& byproducts,
+                                  cmListFileBacktrace const& bt) override;
+
   std::vector<cmCustomCommandGenerator> MakeCustomCommandGenerators(
     cmCustomCommand const& cc, std::string const& config) override;
 
@@ -102,9 +107,9 @@ private:
   void WriteProcessedMakefile(std::ostream& os);
   void WritePools(std::ostream& os);
 
-  void WriteCustomCommandBuildStatement(cmCustomCommand const* cc,
-                                        const cmNinjaDeps& orderOnlyDeps,
-                                        const std::string& config);
+  void WriteCustomCommandBuildStatement(
+    cmCustomCommand const* cc, const std::set<cmGeneratorTarget*>& targets,
+    const std::string& config);
 
   void WriteCustomCommandBuildStatements(const std::string& config);
 

+ 42 - 11
Source/cmNinjaUtilityTargetGenerator.cxx

@@ -5,6 +5,7 @@
 #include <algorithm>
 #include <array>
 #include <iterator>
+#include <set>
 #include <string>
 #include <utility>
 #include <vector>
@@ -33,6 +34,23 @@ cmNinjaUtilityTargetGenerator::cmNinjaUtilityTargetGenerator(
 cmNinjaUtilityTargetGenerator::~cmNinjaUtilityTargetGenerator() = default;
 
 void cmNinjaUtilityTargetGenerator::Generate(const std::string& config)
+{
+  for (auto const& fileConfig : this->GetConfigNames()) {
+    if (!this->GetGlobalGenerator()
+           ->GetCrossConfigs(fileConfig)
+           .count(config)) {
+      continue;
+    }
+    if (fileConfig != config &&
+        this->GetGeneratorTarget()->GetType() == cmStateEnums::GLOBAL_TARGET) {
+      continue;
+    }
+    this->WriteUtilBuildStatements(config, fileConfig);
+  }
+}
+
+void cmNinjaUtilityTargetGenerator::WriteUtilBuildStatements(
+  std::string const& config, std::string const& fileConfig)
 {
   cmGlobalNinjaGenerator* gg = this->GetGlobalGenerator();
   cmLocalNinjaGenerator* lg = this->GetLocalGenerator();
@@ -40,7 +58,7 @@ void cmNinjaUtilityTargetGenerator::Generate(const std::string& config)
 
   std::string configDir;
   if (genTarget->Target->IsPerConfig()) {
-    configDir = gg->ConfigDirectory(config);
+    configDir = gg->ConfigDirectory(fileConfig);
   }
   std::string utilCommandName =
     cmStrCat(lg->GetCurrentBinaryDirectory(), "/CMakeFiles", configDir, "/",
@@ -60,8 +78,8 @@ void cmNinjaUtilityTargetGenerator::Generate(const std::string& config)
 
     for (std::vector<cmCustomCommand> const* cmdList : cmdLists) {
       for (cmCustomCommand const& ci : *cmdList) {
-        cmCustomCommandGenerator ccg(ci, config, lg);
-        lg->AppendCustomCommandDeps(ccg, deps, config);
+        cmCustomCommandGenerator ccg(ci, fileConfig, lg);
+        lg->AppendCustomCommandDeps(ccg, deps, fileConfig);
         lg->AppendCustomCommandLines(ccg, commands);
         std::vector<std::string> const& ccByproducts = ccg.GetByproducts();
         std::transform(ccByproducts.begin(), ccByproducts.end(),
@@ -103,13 +121,19 @@ void cmNinjaUtilityTargetGenerator::Generate(const std::string& config)
     std::copy(util_outputs.begin(), util_outputs.end(),
               std::back_inserter(gg->GetByproductsForCleanTarget()));
   }
-  lg->AppendTargetDepends(genTarget, deps, config, config,
+  // TODO: Does this need an output config?
+  // Does this need to go in impl-<config>.ninja?
+  lg->AppendTargetDepends(genTarget, deps, config, fileConfig,
                           DependOnTargetArtifact);
 
   if (commands.empty()) {
     phonyBuild.Comment = "Utility command for " + this->GetTargetName();
     phonyBuild.ExplicitDeps = std::move(deps);
-    gg->WriteBuild(this->GetCommonFileStream(), phonyBuild);
+    if (genTarget->GetType() != cmStateEnums::GLOBAL_TARGET) {
+      gg->WriteBuild(this->GetImplFileStream(fileConfig), phonyBuild);
+    } else {
+      gg->WriteBuild(this->GetCommonFileStream(), phonyBuild);
+    }
   } else {
     std::string command =
       lg->BuildCommandLine(commands, "utility", this->GeneratorTarget);
@@ -145,15 +169,22 @@ void cmNinjaUtilityTargetGenerator::Generate(const std::string& config)
     std::string ccConfig;
     if (genTarget->Target->IsPerConfig() &&
         genTarget->GetType() != cmStateEnums::GLOBAL_TARGET) {
-      ccConfig = config;
+      ccConfig = fileConfig;
+    }
+    if (config == fileConfig ||
+        gg->GetPerConfigUtilityTargets().count(genTarget->GetName())) {
+      gg->WriteCustomCommandBuild(
+        command, desc, "Utility command for " + this->GetTargetName(),
+        /*depfile*/ "", /*job_pool*/ "", uses_terminal,
+        /*restat*/ true, util_outputs, ccConfig, deps);
     }
-    gg->WriteCustomCommandBuild(command, desc,
-                                "Utility command for " + this->GetTargetName(),
-                                /*depfile*/ "", /*job_pool*/ "", uses_terminal,
-                                /*restat*/ true, util_outputs, ccConfig, deps);
 
     phonyBuild.ExplicitDeps.push_back(utilCommandName);
-    gg->WriteBuild(this->GetCommonFileStream(), phonyBuild);
+    if (genTarget->GetType() != cmStateEnums::GLOBAL_TARGET) {
+      gg->WriteBuild(this->GetImplFileStream(fileConfig), phonyBuild);
+    } else {
+      gg->WriteBuild(this->GetCommonFileStream(), phonyBuild);
+    }
   }
 
   // Find ADDITIONAL_CLEAN_FILES

+ 4 - 0
Source/cmNinjaUtilityTargetGenerator.h

@@ -17,4 +17,8 @@ public:
   ~cmNinjaUtilityTargetGenerator() override;
 
   void Generate(const std::string& config) override;
+
+private:
+  void WriteUtilBuildStatements(std::string const& config,
+                                std::string const& fileConfig);
 };

+ 4 - 4
Tests/RunCMake/FileAPI/codemodel-v2-data/targets/custom_tgt.json

@@ -7,7 +7,7 @@
     "isGeneratorProvided": null,
     "sources": [
         {
-            "path": "^.*/Tests/RunCMake/FileAPI/codemodel-v2-build/custom/CMakeFiles/custom_tgt$",
+            "path": "^.*/Tests/RunCMake/FileAPI/codemodel-v2-build/custom/CMakeFiles/custom_tgt(-(Debug|Release|RelWithDebInfo|MinSizeRel))?$",
             "isGenerated": true,
             "sourceGroupName": "",
             "compileGroupLanguage": null,
@@ -27,7 +27,7 @@
             ]
         },
         {
-            "path": "^.*/Tests/RunCMake/FileAPI/codemodel-v2-build/(custom/)?CMakeFiles/([0-9a-f]+/)?custom_tgt\\.rule$",
+            "path": "^.*/Tests/RunCMake/FileAPI/codemodel-v2-build/(custom/)?CMakeFiles/([0-9a-f]+/)?custom_tgt(-\\(CONFIG\\))?\\.rule$",
             "isGenerated": true,
             "sourceGroupName": "CMake Rules",
             "compileGroupLanguage": null,
@@ -45,13 +45,13 @@
         {
             "name": "",
             "sourcePaths": [
-                "^.*/Tests/RunCMake/FileAPI/codemodel-v2-build/custom/CMakeFiles/custom_tgt$"
+                "^.*/Tests/RunCMake/FileAPI/codemodel-v2-build/custom/CMakeFiles/custom_tgt(-(Debug|Release|RelWithDebInfo|MinSizeRel))?$"
             ]
         },
         {
             "name": "CMake Rules",
             "sourcePaths": [
-                "^.*/Tests/RunCMake/FileAPI/codemodel-v2-build/(custom/)?CMakeFiles/([0-9a-f]+/)?custom_tgt\\.rule$"
+                "^.*/Tests/RunCMake/FileAPI/codemodel-v2-build/(custom/)?CMakeFiles/([0-9a-f]+/)?custom_tgt(-\\(CONFIG\\))?\\.rule$"
             ]
         }
     ],