Browse Source

Merge topic 'custom-command-output-genex'

c257c25419 add_custom_{command,target}: Add genex support to OUTPUT and BYPRODUCTS
f36af9228b cmLocalGenerator: Evaluate generator expressions in custom command outputs
c887cefd9a cmLocalGenerator: Simplify custom command output cmSourceFile creation
947ba01bf9 cmLocalGenerator: Factor out helper to expand custom command output paths
1902d28ebc cmLocalGenerator: Refactor UpdateOutputToSourceMap to avoid boolean trap
e4034eabe9 cmLocalGenerator: Re-order logic in CreateGeneratedSource
706c48301d cmCustomCommandGenerator: Treat relative outputs w.r.t. build dir
5d23c5446e cmCustomCommandGenerator: Refactor OUTPUT and DEPENDS path evaluation
...

Acked-by: Kitware Robot <[email protected]>
Acked-by: Kyle Edwards <[email protected]>
Acked-by: Pavel Solodovnikov <[email protected]>
Acked-by: Ben Boeckel <[email protected]>
Merge-request: !5402
Brad King 5 years ago
parent
commit
fedfe763ee

+ 86 - 0
Help/command/add_custom_command.rst

@@ -46,6 +46,12 @@ The options are:
   Append the ``COMMAND`` and ``DEPENDS`` option values to the custom
   command for the first output specified.  There must have already
   been a previous call to this command with the same output.
+
+  If the previous call specified the output via a generator expression,
+  the output specified by the current call must match in at least one
+  configuration after evaluating generator expressions.  In this case,
+  the appended commands and dependencies apply to all configurations.
+
   The ``COMMENT``, ``MAIN_DEPENDENCY``, and ``WORKING_DIRECTORY``
   options are currently ignored when APPEND is given, but may be
   used in the future.
@@ -73,6 +79,9 @@ The options are:
   The :ref:`Makefile Generators` will remove ``BYPRODUCTS`` and other
   :prop_sf:`GENERATED` files during ``make clean``.
 
+  Since CMake 3.20, arguments to ``BYPRODUCTS`` may use
+  :manual:`generator expressions <cmake-generator-expressions(7)>`.
+
 ``COMMAND``
   Specify the command-line(s) to execute at build time.
   If more than one ``COMMAND`` is specified they will be executed in order,
@@ -220,6 +229,9 @@ The options are:
   as a file on disk it should be marked with the :prop_sf:`SYMBOLIC`
   source file property.
 
+  Since CMake 3.20, arguments to ``OUTPUT`` may use
+  :manual:`generator expressions <cmake-generator-expressions(7)>`.
+
 ``USES_TERMINAL``
   .. versionadded:: 3.2
 
@@ -259,6 +271,44 @@ The options are:
   ``DEPFILE`` should also be relative to :variable:`CMAKE_CURRENT_BINARY_DIR`
   (see policy :policy:`CMP0116`.)
 
+Examples: Generating Files
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Custom commands may be used to generate source files.
+For example, the code:
+
+.. code-block:: cmake
+
+  add_custom_command(
+    OUTPUT out.c
+    COMMAND someTool -i ${CMAKE_CURRENT_SOURCE_DIR}/in.txt
+                     -o out.c
+    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/in.txt
+    VERBATIM)
+  add_library(myLib out.c)
+
+adds a custom command to run ``someTool`` to generate ``out.c`` and then
+compile the generated source as part of a library.  The generation rule
+will re-run whenever ``in.txt`` changes.
+
+Since CMake 3.20, one may use generator expressions to specify
+per-configuration outputs.  For example, the code:
+
+.. code-block:: cmake
+
+  add_custom_command(
+    OUTPUT "out-$<CONFIG>.c"
+    COMMAND someTool -i ${CMAKE_CURRENT_SOURCE_DIR}/in.txt
+                     -o "out-$<CONFIG>.c"
+                     -c "$<CONFIG>"
+    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/in.txt
+    VERBATIM)
+  add_library(myLib "out-$<CONFIG>.c")
+
+adds a custom command to run ``someTool`` to generate ``out-<config>.c``,
+where ``<config>`` is the build configuration, and then compile the generated
+source as part of a library.
+
 Build Events
 ^^^^^^^^^^^^
 
@@ -308,3 +358,39 @@ of the following is specified:
   configuration and no "empty-string-command" will be added.
 
   This allows to add individual build events for every configuration.
+
+Examples: Build Events
+^^^^^^^^^^^^^^^^^^^^^^
+
+A ``POST_BUILD`` event may be used to post-process a binary after linking.
+For example, the code:
+
+.. code-block:: cmake
+
+  add_executable(myExe myExe.c)
+  add_custom_command(
+    TARGET myExe POST_BUILD
+    COMMAND someHasher -i "$<TARGET_FILE:myExe>"
+                       -o "$<TARGET_FILE:myExe>.hash"
+    VERBATIM)
+
+will run ``someHasher`` to produce a ``.hash`` file next to the executable
+after linking.
+
+Since CMake 3.20, one may use generator expressions to specify
+per-configuration byproducts.  For example, the code:
+
+.. code-block:: cmake
+
+  add_library(myPlugin MODULE myPlugin.c)
+  add_custom_command(
+    TARGET myPlugin POST_BUILD
+    COMMAND someHasher -i "$<TARGET_FILE:myPlugin>"
+                       --as-code "myPlugin-hash-$<CONFIG>.c"
+    BYPRODUCTS "myPlugin-hash-$<CONFIG>.c"
+    VERBATIM)
+  add_executable(myExe myExe.c "myPlugin-hash-$<CONFIG>.c")
+
+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.

+ 3 - 0
Help/command/add_custom_target.rst

@@ -54,6 +54,9 @@ The options are:
   The :ref:`Makefile Generators` will remove ``BYPRODUCTS`` and other
   :prop_sf:`GENERATED` files during ``make clean``.
 
+  Since CMake 3.20, arguments to ``BYPRODUCTS`` may use
+  :manual:`generator expressions <cmake-generator-expressions(7)>`.
+
 ``COMMAND``
   Specify the command-line(s) to execute at build time.
   If more than one ``COMMAND`` is specified they will be executed in order,

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

@@ -0,0 +1,6 @@
+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.

+ 0 - 2
Source/CMakeLists.txt

@@ -181,8 +181,6 @@ set(SRCS
   cmBinUtilsWindowsPEObjdumpGetRuntimeDependenciesTool.h
   cmCacheManager.cxx
   cmCacheManager.h
-  cmCheckCustomOutputs.h
-  cmCheckCustomOutputs.cxx
   cmCLocaleEnvironmentScope.h
   cmCLocaleEnvironmentScope.cxx
   cmCMakePath.h

+ 3 - 9
Source/cmAddCustomCommandCommand.cxx

@@ -5,11 +5,11 @@
 #include <sstream>
 #include <unordered_set>
 
-#include "cmCheckCustomOutputs.h"
 #include "cmCustomCommand.h"
 #include "cmCustomCommandLines.h"
 #include "cmCustomCommandTypes.h"
 #include "cmExecutionStatus.h"
+#include "cmGeneratorExpression.h"
 #include "cmGlobalGenerator.h"
 #include "cmMakefile.h"
 #include "cmMessageType.h"
@@ -188,7 +188,8 @@ bool cmAddCustomCommandCommand(std::vector<std::string> const& args,
         case doing_output:
         case doing_outputs:
         case doing_byproducts:
-          if (!cmSystemTools::FileIsFullPath(copy)) {
+          if (!cmSystemTools::FileIsFullPath(copy) &&
+              cmGeneratorExpression::Find(copy) != 0) {
             // This is an output to be generated, so it should be
             // under the build tree.
             filename = cmStrCat(mf.GetCurrentBinaryDirectory(), '/');
@@ -296,13 +297,6 @@ bool cmAddCustomCommandCommand(std::vector<std::string> const& args,
     return false;
   }
 
-  // Make sure the output names and locations are safe.
-  if (!cmCheckCustomOutputs(output, "OUTPUT", status) ||
-      !cmCheckCustomOutputs(outputs, "OUTPUTS", status) ||
-      !cmCheckCustomOutputs(byproducts, "BYPRODUCTS", status)) {
-    return false;
-  }
-
   // Check for an append request.
   if (append) {
     mf.AppendCustomCommandToOutput(output[0], depends, implicit_depends,

+ 6 - 8
Source/cmAddCustomTargetCommand.cxx

@@ -4,7 +4,6 @@
 
 #include <utility>
 
-#include "cmCheckCustomOutputs.h"
 #include "cmCustomCommandLines.h"
 #include "cmExecutionStatus.h"
 #include "cmGeneratorExpression.h"
@@ -120,12 +119,16 @@ bool cmAddCustomTargetCommand(std::vector<std::string> const& args,
           break;
         case doing_byproducts: {
           std::string filename;
-          if (!cmSystemTools::FileIsFullPath(copy)) {
+          if (!cmSystemTools::FileIsFullPath(copy) &&
+              cmGeneratorExpression::Find(copy) != 0) {
             filename = cmStrCat(mf.GetCurrentBinaryDirectory(), '/');
           }
           filename += copy;
           cmSystemTools::ConvertToUnixSlashes(filename);
-          byproducts.push_back(cmSystemTools::CollapseFullPath(filename));
+          if (cmSystemTools::FileIsFullPath(filename)) {
+            filename = cmSystemTools::CollapseFullPath(filename);
+          }
+          byproducts.push_back(filename);
         } break;
         case doing_depends: {
           std::string dep = copy;
@@ -206,11 +209,6 @@ bool cmAddCustomTargetCommand(std::vector<std::string> const& args,
     return false;
   }
 
-  // Make sure the byproduct names and locations are safe.
-  if (!cmCheckCustomOutputs(byproducts, "BYPRODUCTS", status)) {
-    return false;
-  }
-
   // Add the utility target to the makefile.
   bool escapeOldStyle = !verbatim;
   cmTarget* target = mf.AddUtilityCommand(

+ 0 - 36
Source/cmCheckCustomOutputs.cxx

@@ -1,36 +0,0 @@
-/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
-   file Copyright.txt or https://cmake.org/licensing for details.  */
-#include "cmCheckCustomOutputs.h"
-
-#include "cmExecutionStatus.h"
-#include "cmMakefile.h"
-#include "cmStringAlgorithms.h"
-#include "cmSystemTools.h"
-
-bool cmCheckCustomOutputs(const std::vector<std::string>& outputs,
-                          cm::string_view keyword, cmExecutionStatus& status)
-{
-  cmMakefile& mf = status.GetMakefile();
-
-  for (std::string const& o : outputs) {
-    // Make sure the file will not be generated into the source
-    // directory during an out of source build.
-    if (!mf.CanIWriteThisFile(o)) {
-      status.SetError(
-        cmStrCat("attempted to have a file\n  ", o,
-                 "\nin a source directory as an output of custom command."));
-      cmSystemTools::SetFatalErrorOccured();
-      return false;
-    }
-
-    // Make sure the output file name has no invalid characters.
-    std::string::size_type pos = o.find_first_of("#<>");
-    if (pos != std::string::npos) {
-      status.SetError(cmStrCat("called with ", keyword, " containing a \"",
-                               o[pos], "\".  This character is not allowed."));
-      return false;
-    }
-  }
-
-  return true;
-}

+ 0 - 15
Source/cmCheckCustomOutputs.h

@@ -1,15 +0,0 @@
-/* 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 <string>
-#include <vector>
-
-#include <cm/string_view>
-
-class cmExecutionStatus;
-
-bool cmCheckCustomOutputs(const std::vector<std::string>& outputs,
-                          cm::string_view keyword, cmExecutionStatus& status);

+ 34 - 17
Source/cmCustomCommandGenerator.cxx

@@ -24,22 +24,38 @@
 #include "cmTransformDepfile.h"
 
 namespace {
-void AppendPaths(const std::vector<std::string>& inputs,
-                 cmGeneratorExpression const& ge, cmLocalGenerator* lg,
-                 std::string const& config, std::vector<std::string>& output)
+std::vector<std::string> EvaluateDepends(std::vector<std::string> const& paths,
+                                         cmGeneratorExpression const& ge,
+                                         cmLocalGenerator* lg,
+                                         std::string const& config)
 {
-  for (std::string const& in : inputs) {
-    std::unique_ptr<cmCompiledGeneratorExpression> cge = ge.Parse(in);
-    std::vector<std::string> result =
-      cmExpandedList(cge->Evaluate(lg, config));
-    for (std::string& it : result) {
-      cmSystemTools::ConvertToUnixSlashes(it);
-      if (cmSystemTools::FileIsFullPath(it)) {
-        it = cmSystemTools::CollapseFullPath(it);
-      }
+  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);
+    cm::append(depends, cmExpandedList(ep));
+  }
+  for (std::string& p : depends) {
+    if (cmSystemTools::FileIsFullPath(p)) {
+      p = cmSystemTools::CollapseFullPath(p);
+    } else {
+      cmSystemTools::ConvertToUnixSlashes(p);
     }
-    cm::append(output, result);
   }
+  return depends;
+}
+
+std::vector<std::string> EvaluateOutputs(std::vector<std::string> const& paths,
+                                         cmGeneratorExpression const& ge,
+                                         cmLocalGenerator* lg,
+                                         std::string const& config)
+{
+  std::vector<std::string> outputs;
+  for (std::string const& p : paths) {
+    std::unique_ptr<cmCompiledGeneratorExpression> cge = ge.Parse(p);
+    cm::append(outputs, lg->ExpandCustomCommandOutputPaths(*cge, config));
+  }
+  return outputs;
 }
 }
 
@@ -121,9 +137,10 @@ cmCustomCommandGenerator::cmCustomCommandGenerator(cmCustomCommand const& cc,
     this->CommandLines.push_back(std::move(argv));
   }
 
-  AppendPaths(cc.GetByproducts(), ge, this->LG, this->Config,
-              this->Byproducts);
-  AppendPaths(cc.GetDepends(), ge, this->LG, this->Config, this->Depends);
+  this->Outputs = EvaluateOutputs(cc.GetOutputs(), ge, this->LG, this->Config);
+  this->Byproducts =
+    EvaluateOutputs(cc.GetByproducts(), ge, this->LG, this->Config);
+  this->Depends = EvaluateDepends(cc.GetDepends(), ge, this->LG, this->Config);
 
   const std::string& workingdirectory = this->CC->GetWorkingDirectory();
   if (!workingdirectory.empty()) {
@@ -326,7 +343,7 @@ std::string cmCustomCommandGenerator::GetWorkingDirectory() const
 
 std::vector<std::string> const& cmCustomCommandGenerator::GetOutputs() const
 {
-  return this->CC->GetOutputs();
+  return this->Outputs;
 }
 
 std::vector<std::string> const& cmCustomCommandGenerator::GetByproducts() const

+ 1 - 0
Source/cmCustomCommandGenerator.h

@@ -24,6 +24,7 @@ class cmCustomCommandGenerator
   bool MakeVars;
   cmCustomCommandLines CommandLines;
   std::vector<std::vector<std::string>> EmulatorsWithArguments;
+  std::vector<std::string> Outputs;
   std::vector<std::string> Byproducts;
   std::vector<std::string> Depends;
   std::string WorkingDirectory;

+ 174 - 49
Source/cmLocalGenerator.cxx

@@ -17,9 +17,11 @@
 #include <cm/memory>
 #include <cm/string_view>
 #include <cmext/algorithm>
+#include <cmext/string_view>
 
 #include "cmsys/RegularExpression.hxx"
 
+#include "cmAlgorithms.h"
 #include "cmComputeLinkInformation.h"
 #include "cmCustomCommand.h"
 #include "cmCustomCommandGenerator.h"
@@ -3812,38 +3814,95 @@ void cmLocalGenerator::GenerateFrameworkInfoPList(
 }
 
 namespace {
+cm::string_view CustomOutputRoleKeyword(cmLocalGenerator::OutputRole role)
+{
+  return (role == cmLocalGenerator::OutputRole::Primary ? "OUTPUT"_s
+                                                        : "BYPRODUCTS"_s);
+}
+
 void CreateGeneratedSource(cmLocalGenerator& lg, const std::string& output,
+                           cmLocalGenerator::OutputRole role,
                            cmCommandOrigin origin,
                            const cmListFileBacktrace& lfbt)
 {
-  if (cmGeneratorExpression::Find(output) == std::string::npos) {
-    // Outputs without generator expressions from the project are already
-    // created and marked as generated.  Do not mark them again, because
-    // other commands might have overwritten the property.
-    if (origin == cmCommandOrigin::Generator) {
-      lg.GetMakefile()->GetOrCreateGeneratedSource(output);
-    }
-  } else {
+  if (cmGeneratorExpression::Find(output) != std::string::npos) {
     lg.GetCMakeInstance()->IssueMessage(
       MessageType::FATAL_ERROR,
       "Generator expressions in custom command outputs are not implemented!",
       lfbt);
+    return;
+  }
+
+  // Make sure the file will not be generated into the source
+  // directory during an out of source build.
+  if (!lg.GetMakefile()->CanIWriteThisFile(output)) {
+    lg.GetCMakeInstance()->IssueMessage(
+      MessageType::FATAL_ERROR,
+      cmStrCat(CustomOutputRoleKeyword(role), " path\n  ", output,
+               "\nin a source directory as an output of custom command."),
+      lfbt);
+    return;
+  }
+
+  // Make sure the output file name has no invalid characters.
+  std::string::size_type pos = output.find_first_of("#<>");
+  if (pos != std::string::npos) {
+    lg.GetCMakeInstance()->IssueMessage(
+      MessageType::FATAL_ERROR,
+      cmStrCat(CustomOutputRoleKeyword(role), " containing a \"", output[pos],
+               "\" is not allowed."),
+      lfbt);
+    return;
+  }
+
+  // Outputs without generator expressions from the project are already
+  // created and marked as generated.  Do not mark them again, because
+  // other commands might have overwritten the property.
+  if (origin == cmCommandOrigin::Generator) {
+    lg.GetMakefile()->GetOrCreateGeneratedSource(output);
   }
 }
 
-void CreateGeneratedSources(cmLocalGenerator& lg,
-                            const std::vector<std::string>& outputs,
-                            cmCommandOrigin origin,
-                            const cmListFileBacktrace& lfbt)
+std::string ComputeCustomCommandRuleFileName(cmLocalGenerator& lg,
+                                             cmListFileBacktrace const& bt,
+                                             std::string const& output)
 {
-  for (std::string const& o : outputs) {
-    CreateGeneratedSource(lg, o, origin, lfbt);
+  // If the output path has no generator expressions, use it directly.
+  if (cmGeneratorExpression::Find(output) == std::string::npos) {
+    return output;
   }
+
+  // The output path contains a generator expression, but we must choose
+  // a single source file path to which to attach the custom command.
+  // Use some heuristics to provie a nice-looking name when possible.
+
+  // If the only genex is $<CONFIG>, replace that gracefully.
+  {
+    std::string simple = output;
+    cmSystemTools::ReplaceString(simple, "$<CONFIG>", "(CONFIG)");
+    if (cmGeneratorExpression::Find(simple) == std::string::npos) {
+      return simple;
+    }
+  }
+
+  // If the genex evaluates to the same value in all configurations, use that.
+  {
+    std::vector<std::string> allConfigOutputs =
+      lg.ExpandCustomCommandOutputGenex(output, bt);
+    if (allConfigOutputs.size() == 1) {
+      return allConfigOutputs.front();
+    }
+  }
+
+  // Fall back to a deterministic unique name.
+  cmCryptoHash h(cmCryptoHash::AlgoSHA256);
+  return cmStrCat(lg.GetCurrentBinaryDirectory(), "/CMakeFiles/",
+                  h.HashString(output).substr(0, 16));
 }
 
 cmSourceFile* AddCustomCommand(
   cmLocalGenerator& lg, const cmListFileBacktrace& lfbt,
-  const std::vector<std::string>& outputs,
+  cmCommandOrigin origin, const std::vector<std::string>& outputs,
   const std::vector<std::string>& byproducts,
   const std::vector<std::string>& depends, const std::string& main_dependency,
   const cmImplicitDependsList& implicit_depends,
@@ -3880,7 +3939,8 @@ cmSourceFile* AddCustomCommand(
     cmGlobalGenerator* gg = lg.GetGlobalGenerator();
 
     // Construct a rule file associated with the first output produced.
-    std::string outName = gg->GenerateRuleFile(outputs[0]);
+    std::string outName = gg->GenerateRuleFile(
+      ComputeCustomCommandRuleFileName(lg, lfbt, outputs[0]));
 
     // Check if the rule file already exists.
     file = mf->GetSource(outName, cmSourceFileLocationKind::Known);
@@ -3923,7 +3983,10 @@ cmSourceFile* AddCustomCommand(
     cc->SetJobPool(job_pool);
     file->SetCustomCommand(std::move(cc));
 
-    lg.AddSourceOutputs(file, outputs, byproducts);
+    lg.AddSourceOutputs(file, outputs, cmLocalGenerator::OutputRole::Primary,
+                        lfbt, origin);
+    lg.AddSourceOutputs(file, byproducts,
+                        cmLocalGenerator::OutputRole::Byproduct, lfbt, origin);
   }
   return file;
 }
@@ -3967,9 +4030,6 @@ void AddCustomCommandToTarget(cmLocalGenerator& lg,
                               const std::string& job_pool,
                               bool command_expand_lists, bool stdPipesUTF8)
 {
-  // Always create the byproduct sources and mark them generated.
-  CreateGeneratedSources(lg, byproducts, origin, lfbt);
-
   // Add the command to the appropriate build step for the target.
   std::vector<std::string> no_output;
   cmCustomCommand cc(no_output, byproducts, depends, commandLines, lfbt,
@@ -3992,7 +4052,7 @@ void AddCustomCommandToTarget(cmLocalGenerator& lg,
       break;
   }
 
-  lg.AddTargetByproducts(target, byproducts);
+  lg.AddTargetByproducts(target, byproducts, lfbt, origin);
 }
 
 cmSourceFile* AddCustomCommandToOutput(
@@ -4006,14 +4066,11 @@ cmSourceFile* AddCustomCommandToOutput(
   bool uses_terminal, bool command_expand_lists, const std::string& depfile,
   const std::string& job_pool, bool stdPipesUTF8)
 {
-  // Always create the output sources and mark them generated.
-  CreateGeneratedSources(lg, outputs, origin, lfbt);
-  CreateGeneratedSources(lg, byproducts, origin, lfbt);
-
-  return AddCustomCommand(
-    lg, lfbt, outputs, byproducts, depends, main_dependency, implicit_depends,
-    commandLines, comment, workingDir, replace, escapeOldStyle, uses_terminal,
-    command_expand_lists, depfile, job_pool, stdPipesUTF8);
+  return AddCustomCommand(lg, lfbt, origin, outputs, byproducts, depends,
+                          main_dependency, implicit_depends, commandLines,
+                          comment, workingDir, replace, escapeOldStyle,
+                          uses_terminal, command_expand_lists, depfile,
+                          job_pool, stdPipesUTF8);
 }
 
 void AppendCustomCommandToOutput(cmLocalGenerator& lg,
@@ -4024,7 +4081,22 @@ void AppendCustomCommandToOutput(cmLocalGenerator& lg,
                                  const cmCustomCommandLines& commandLines)
 {
   // Lookup an existing command.
-  if (cmSourceFile* sf = lg.GetSourceFileWithOutput(output)) {
+  cmSourceFile* sf = nullptr;
+  if (cmGeneratorExpression::Find(output) == std::string::npos) {
+    sf = lg.GetSourceFileWithOutput(output);
+  } else {
+    // This output path has a generator expression.  Evaluate it to
+    // find the output for any configurations.
+    for (std::string const& out :
+         lg.ExpandCustomCommandOutputGenex(output, lfbt)) {
+      sf = lg.GetSourceFileWithOutput(out);
+      if (sf) {
+        break;
+      }
+    }
+  }
+
+  if (sf) {
     if (cmCustomCommand* cc = sf->GetCustomCommand()) {
       cc->AppendCommands(commandLines);
       cc->AppendDepends(depends);
@@ -4051,10 +4123,6 @@ void AddUtilityCommand(cmLocalGenerator& lg, const cmListFileBacktrace& lfbt,
                        bool uses_terminal, bool command_expand_lists,
                        const std::string& job_pool, bool stdPipesUTF8)
 {
-  // Always create the byproduct sources and mark them generated.
-  CreateGeneratedSource(lg, force.Name, origin, lfbt);
-  CreateGeneratedSources(lg, byproducts, origin, lfbt);
-
   // Use an empty comment to avoid generation of default comment.
   if (!comment) {
     comment = "";
@@ -4063,12 +4131,12 @@ void AddUtilityCommand(cmLocalGenerator& lg, const cmListFileBacktrace& lfbt,
   std::string no_main_dependency;
   cmImplicitDependsList no_implicit_depends;
   cmSourceFile* rule = AddCustomCommand(
-    lg, lfbt, { force.Name }, byproducts, depends, no_main_dependency,
+    lg, lfbt, origin, { force.Name }, byproducts, depends, no_main_dependency,
     no_implicit_depends, commandLines, comment, workingDir,
     /*replace=*/false, escapeOldStyle, uses_terminal, command_expand_lists,
     /*depfile=*/"", job_pool, stdPipesUTF8);
   if (rule) {
-    lg.AddTargetByproducts(target, byproducts);
+    lg.AddTargetByproducts(target, byproducts, lfbt, origin);
   }
 
   if (!force.NameCMP0049.empty()) {
@@ -4166,34 +4234,87 @@ cmSourceFile* cmLocalGenerator::GetSourceFileWithOutput(
   return nullptr;
 }
 
+std::vector<std::string> cmLocalGenerator::ExpandCustomCommandOutputPaths(
+  cmCompiledGeneratorExpression const& cge, std::string const& config)
+{
+  std::vector<std::string> paths = cmExpandedList(cge.Evaluate(this, config));
+  for (std::string& p : paths) {
+    p = cmSystemTools::CollapseFullPath(p, this->GetCurrentBinaryDirectory());
+  }
+  return paths;
+}
+
+std::vector<std::string> cmLocalGenerator::ExpandCustomCommandOutputGenex(
+  std::string const& o, cmListFileBacktrace const& bt)
+{
+  std::vector<std::string> allConfigOutputs;
+  cmGeneratorExpression ge(bt);
+  std::unique_ptr<cmCompiledGeneratorExpression> cge = ge.Parse(o);
+  std::vector<std::string> configs =
+    this->Makefile->GetGeneratorConfigs(cmMakefile::IncludeEmptyConfig);
+  for (std::string const& config : configs) {
+    std::vector<std::string> configOutputs =
+      this->ExpandCustomCommandOutputPaths(*cge, config);
+    allConfigOutputs.reserve(allConfigOutputs.size() + configOutputs.size());
+    std::move(configOutputs.begin(), configOutputs.end(),
+              std::back_inserter(allConfigOutputs));
+  }
+  auto endUnique =
+    cmRemoveDuplicates(allConfigOutputs.begin(), allConfigOutputs.end());
+  allConfigOutputs.erase(endUnique, allConfigOutputs.end());
+  return allConfigOutputs;
+}
+
 void cmLocalGenerator::AddTargetByproducts(
-  cmTarget* target, const std::vector<std::string>& byproducts)
+  cmTarget* target, const std::vector<std::string>& byproducts,
+  cmListFileBacktrace const& bt, cmCommandOrigin origin)
 {
   for (std::string const& o : byproducts) {
-    this->UpdateOutputToSourceMap(o, target);
+    if (cmGeneratorExpression::Find(o) == std::string::npos) {
+      this->UpdateOutputToSourceMap(o, target, bt, origin);
+      continue;
+    }
+
+    // This byproduct path has a generator expression.  Evaluate it to
+    // register the byproducts for all configurations.
+    for (std::string const& b : this->ExpandCustomCommandOutputGenex(o, bt)) {
+      this->UpdateOutputToSourceMap(b, target, bt, cmCommandOrigin::Generator);
+    }
   }
 }
 
 void cmLocalGenerator::AddSourceOutputs(
   cmSourceFile* source, const std::vector<std::string>& outputs,
-  const std::vector<std::string>& byproducts)
+  OutputRole role, cmListFileBacktrace const& bt, cmCommandOrigin origin)
 {
   for (std::string const& o : outputs) {
-    this->UpdateOutputToSourceMap(o, source, false);
-  }
-  for (std::string const& o : byproducts) {
-    this->UpdateOutputToSourceMap(o, source, true);
+    if (cmGeneratorExpression::Find(o) == std::string::npos) {
+      this->UpdateOutputToSourceMap(o, source, role, bt, origin);
+      continue;
+    }
+
+    // This output path has a generator expression.  Evaluate it to
+    // register the outputs for all configurations.
+    for (std::string const& out :
+         this->ExpandCustomCommandOutputGenex(o, bt)) {
+      this->UpdateOutputToSourceMap(out, source, role, bt,
+                                    cmCommandOrigin::Generator);
+    }
   }
 }
 
 void cmLocalGenerator::UpdateOutputToSourceMap(std::string const& byproduct,
-                                               cmTarget* target)
+                                               cmTarget* target,
+                                               cmListFileBacktrace const& bt,
+                                               cmCommandOrigin origin)
 {
   SourceEntry entry;
   entry.Sources.Target = target;
 
   auto pr = this->OutputToSource.emplace(byproduct, entry);
-  if (!pr.second) {
+  if (pr.second) {
+    CreateGeneratedSource(*this, byproduct, OutputRole::Byproduct, origin, bt);
+  } else {
     SourceEntry& current = pr.first->second;
     // Has the target already been set?
     if (!current.Sources.Target) {
@@ -4210,18 +4331,22 @@ void cmLocalGenerator::UpdateOutputToSourceMap(std::string const& byproduct,
 
 void cmLocalGenerator::UpdateOutputToSourceMap(std::string const& output,
                                                cmSourceFile* source,
-                                               bool byproduct)
+                                               OutputRole role,
+                                               cmListFileBacktrace const& bt,
+                                               cmCommandOrigin origin)
 {
   SourceEntry entry;
   entry.Sources.Source = source;
-  entry.Sources.SourceIsByproduct = byproduct;
+  entry.Sources.SourceIsByproduct = role == OutputRole::Byproduct;
 
   auto pr = this->OutputToSource.emplace(output, entry);
-  if (!pr.second) {
+  if (pr.second) {
+    CreateGeneratedSource(*this, output, role, origin, bt);
+  } else {
     SourceEntry& current = pr.first->second;
     // Outputs take precedence over byproducts
     if (!current.Sources.Source ||
-        (current.Sources.SourceIsByproduct && !byproduct)) {
+        (current.Sources.SourceIsByproduct && role == OutputRole::Primary)) {
       current.Sources.Source = source;
       current.Sources.SourceIsByproduct = false;
     } else {

+ 23 - 5
Source/cmLocalGenerator.h

@@ -22,6 +22,7 @@
 #include "cmProperty.h"
 #include "cmStateSnapshot.h"
 
+class cmCompiledGeneratorExpression;
 class cmComputeLinkInformation;
 class cmCustomCommandGenerator;
 class cmCustomCommandLines;
@@ -362,18 +363,32 @@ public:
     bool command_expand_lists = false, const std::string& job_pool = "",
     bool stdPipesUTF8 = false);
 
+  std::vector<std::string> ExpandCustomCommandOutputPaths(
+    cmCompiledGeneratorExpression const& cge, std::string const& config);
+  std::vector<std::string> ExpandCustomCommandOutputGenex(
+    std::string const& o, cmListFileBacktrace const& bt);
+
   /**
    * Add target byproducts.
    */
   void AddTargetByproducts(cmTarget* target,
-                           const std::vector<std::string>& byproducts);
+                           const std::vector<std::string>& byproducts,
+                           cmListFileBacktrace const& bt,
+                           cmCommandOrigin origin);
+
+  enum class OutputRole
+  {
+    Primary,
+    Byproduct,
+  };
 
   /**
    * Add source file outputs.
    */
   void AddSourceOutputs(cmSourceFile* source,
-                        const std::vector<std::string>& outputs,
-                        const std::vector<std::string>& byproducts);
+                        std::vector<std::string> const& outputs,
+                        OutputRole role, cmListFileBacktrace const& bt,
+                        cmCommandOrigin origin);
 
   /**
    * Return the target if the provided source name is a byproduct of a utility
@@ -607,9 +622,12 @@ private:
   using OutputToSourceMap = std::unordered_map<std::string, SourceEntry>;
   OutputToSourceMap OutputToSource;
 
-  void UpdateOutputToSourceMap(std::string const& byproduct, cmTarget* target);
+  void UpdateOutputToSourceMap(std::string const& byproduct, cmTarget* target,
+                               cmListFileBacktrace const& bt,
+                               cmCommandOrigin origin);
   void UpdateOutputToSourceMap(std::string const& output, cmSourceFile* source,
-                               bool byproduct);
+                               OutputRole role, cmListFileBacktrace const& bt,
+                               cmCommandOrigin origin);
 
   void AddSharedFlags(std::string& flags, const std::string& lang,
                       bool shared);

+ 69 - 2
Tests/ConfigSources/CMakeLists.txt

@@ -16,6 +16,72 @@ void config_$<CONFIG>() {}
 ]]
   )
 
+# Custom command outputs named with the configuration(s).
+add_custom_command(
+  OUTPUT "custom1_$<CONFIG>.cpp"
+  COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/custom1.cpp.in" "custom1_$<CONFIG>.cpp"
+  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/custom1.cpp.in
+  VERBATIM
+  )
+# Output path starts in a generator expression.
+add_custom_command(
+  OUTPUT "$<1:custom2_$<CONFIG>.cpp>"
+  COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/custom2.cpp.in" "custom2_$<CONFIG>.cpp"
+  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/custom2.cpp.in
+  VERBATIM
+  )
+# Source file generated as a custom command's byproduct.
+add_custom_command(
+  OUTPUT custom3.txt
+  BYPRODUCTS "$<1:custom3_$<CONFIG>.cpp>"
+  COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/custom3.cpp.in" "custom3_$<CONFIG>.cpp"
+  COMMAND ${CMAKE_COMMAND} -E touch custom3.txt
+  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/custom3.cpp.in
+  VERBATIM
+  )
+# Source file generated as a custom target's byproduct.
+add_custom_target(custom4
+  BYPRODUCTS "custom4_$<CONFIG>.cpp"
+  COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/custom4.cpp.in" "custom4_$<CONFIG>.cpp"
+  VERBATIM
+  )
+# Source file generated by appended custom command.
+add_custom_command(
+  OUTPUT "custom5_$<CONFIG>.cpp"
+  COMMAND ${CMAKE_COMMAND} -E echo custom5_$<CONFIG>.cpp
+  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/custom5.cpp.in
+  VERBATIM
+  )
+add_custom_command(APPEND
+  OUTPUT "custom5_$<CONFIG>.cpp"
+  COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/custom5.cpp.in" "custom5_$<CONFIG>.cpp.in"
+  VERBATIM
+  )
+# Appending through any configuration's output affects all configurations.
+if(CMAKE_CONFIGURATION_TYPES MATCHES ";([^;]+)$")
+  set(last_config "${CMAKE_MATCH_1}")
+else()
+  set(last_config ${CMAKE_BUILD_TYPE})
+endif()
+add_custom_command(APPEND
+  OUTPUT "custom5_${last_config}.cpp"
+  COMMAND ${CMAKE_COMMAND} -E copy "custom5_$<CONFIG>.cpp.in" "custom5_$<CONFIG>.cpp"
+  VERBATIM
+  )
+foreach(n RANGE 1 5)
+  set_property(SOURCE custom${n}_Debug.cpp PROPERTY COMPILE_DEFINITIONS CUSTOM_CFG_DEBUG)
+  foreach(other Release RelWithDebInfo MinSizeRel)
+    set_property(SOURCE custom${n}_${other}.cpp PROPERTY COMPILE_DEFINITIONS CUSTOM_CFG_OTHER)
+  endforeach()
+endforeach()
+add_library(Custom STATIC
+  custom1_$<CONFIG>.cpp
+  custom2_$<CONFIG>.cpp
+  custom3_$<CONFIG>.cpp custom3.txt
+  custom4_$<CONFIG>.cpp
+  custom5_$<CONFIG>.cpp
+  )
+
 # Per-config sources via INTERFACE_SOURCES.
 add_library(iface INTERFACE)
 target_sources(iface INTERFACE
@@ -34,7 +100,7 @@ add_executable(ConfigSources
   $<$<CONFIG:NotAConfig>:does_not_exist.cpp>
   ${CMAKE_CURRENT_BINARY_DIR}/config_$<CONFIG>.cpp
   )
-target_link_libraries(ConfigSources iface)
+target_link_libraries(ConfigSources Custom iface)
 
 # Per-config sources via LINK_LIBRARIES.
 add_library(iface_debug INTERFACE)
@@ -53,6 +119,7 @@ target_compile_definitions(ConfigSourcesLink PRIVATE
   "$<$<NOT:$<CONFIG:Debug>>:CFG_OTHER>"
   )
 target_link_libraries(ConfigSourcesLink PRIVATE
+  Custom
   "$<$<CONFIG:Debug>:iface_debug>"
   "$<$<NOT:$<CONFIG:Debug>>:iface_other>"
   "$<$<CONFIG:NotAConfig>:iface_does_not_exist>"
@@ -70,7 +137,7 @@ target_compile_definitions(ConfigSourcesLinkIface PRIVATE
   "$<$<CONFIG:Debug>:CFG_DEBUG>"
   "$<$<NOT:$<CONFIG:Debug>>:CFG_OTHER>"
   )
-target_link_libraries(ConfigSourcesLinkIface ConfigSourcesIface)
+target_link_libraries(ConfigSourcesLinkIface Custom ConfigSourcesIface)
 
 # A target with sources in only one configuration that is not the
 # first in CMAKE_CONFIGURATION_TYPES.

+ 13 - 0
Tests/ConfigSources/custom1.cpp.in

@@ -0,0 +1,13 @@
+#ifdef CUSTOM_CFG_DEBUG
+int custom1_debug()
+{
+  return 0;
+}
+#endif
+
+#ifdef CUSTOM_CFG_OTHER
+int custom1_other()
+{
+  return 0;
+}
+#endif

+ 13 - 0
Tests/ConfigSources/custom2.cpp.in

@@ -0,0 +1,13 @@
+#ifdef CUSTOM_CFG_DEBUG
+int custom2_debug()
+{
+  return 0;
+}
+#endif
+
+#ifdef CUSTOM_CFG_OTHER
+int custom2_other()
+{
+  return 0;
+}
+#endif

+ 13 - 0
Tests/ConfigSources/custom3.cpp.in

@@ -0,0 +1,13 @@
+#ifdef CUSTOM_CFG_DEBUG
+int custom3_debug()
+{
+  return 0;
+}
+#endif
+
+#ifdef CUSTOM_CFG_OTHER
+int custom3_other()
+{
+  return 0;
+}
+#endif

+ 13 - 0
Tests/ConfigSources/custom4.cpp.in

@@ -0,0 +1,13 @@
+#ifdef CUSTOM_CFG_DEBUG
+int custom4_debug()
+{
+  return 0;
+}
+#endif
+
+#ifdef CUSTOM_CFG_OTHER
+int custom4_other()
+{
+  return 0;
+}
+#endif

+ 13 - 0
Tests/ConfigSources/custom5.cpp.in

@@ -0,0 +1,13 @@
+#ifdef CUSTOM_CFG_DEBUG
+int custom5_debug()
+{
+  return 0;
+}
+#endif
+
+#ifdef CUSTOM_CFG_OTHER
+int custom5_other()
+{
+  return 0;
+}
+#endif

+ 8 - 1
Tests/ConfigSources/main_debug.cpp

@@ -7,7 +7,14 @@
 
 #include "iface.h"
 
+extern int custom1_debug();
+extern int custom2_debug();
+extern int custom3_debug();
+extern int custom4_debug();
+extern int custom5_debug();
+
 int main(int argc, char** argv)
 {
-  return iface_src() + iface_debug();
+  return iface_src() + iface_debug() + custom1_debug() + custom2_debug() +
+    custom3_debug() + custom4_debug() + custom5_debug();
 }

+ 8 - 1
Tests/ConfigSources/main_other.cpp

@@ -7,7 +7,14 @@
 
 #include "iface.h"
 
+extern int custom1_other();
+extern int custom2_other();
+extern int custom3_other();
+extern int custom4_other();
+extern int custom5_other();
+
 int main(int argc, char** argv)
 {
-  return iface_src() + iface_other();
+  return iface_src() + iface_other() + custom1_other() + custom2_other() +
+    custom3_other() + custom4_other() + custom5_other();
 }

+ 37 - 0
Tests/RunCMake/VS10Project/CustomCommandGenex-check.cmake

@@ -0,0 +1,37 @@
+set(vcProjectFile "${RunCMake_TEST_BINARY_DIR}/foo.vcxproj")
+if(NOT EXISTS "${vcProjectFile}")
+  set(RunCMake_TEST_FAILED "Project file ${vcProjectFile} does not exist.")
+  return()
+endif()
+
+set(found_CustomBuild_out 0)
+set(found_CustomBuild_out_CONFIG 0)
+set(found_CustomBuild_out_CONFIG_CONFIG 0)
+set(found_CustomBuild_out_HASH 0)
+file(STRINGS "${vcProjectFile}" lines)
+foreach(line IN LISTS lines)
+  if(line MATCHES [[<CustomBuild Include=".*\\out\.txt\.rule">]])
+    set(found_CustomBuild_out 1)
+  endif()
+  if(line MATCHES [[<CustomBuild Include=".*\\out-\(CONFIG\)\.txt\.rule">]])
+    set(found_CustomBuild_out_CONFIG 1)
+  endif()
+  if(line MATCHES [[<CustomBuild Include=".*\\out-\(CONFIG\)-\(CONFIG\)\.txt\.rule">]])
+    set(found_CustomBuild_out_CONFIG_CONFIG 1)
+  endif()
+  if(line MATCHES [[<CustomBuild Include=".*\\[0-9A-Fa-f]+\.rule">]])
+    set(found_CustomBuild_out_HASH 1)
+  endif()
+endforeach()
+if(NOT found_CustomBuild_out)
+  string(APPEND RunCMake_TEST_FAILED "CustomBuild for out.txt.rule not found in\n  ${vcProjectFile}\n")
+endif()
+if(NOT found_CustomBuild_out_CONFIG)
+  string(APPEND RunCMake_TEST_FAILED "CustomBuild for out-(CONFIG).txt.rule not found in\n  ${vcProjectFile}\n")
+endif()
+if(NOT found_CustomBuild_out_CONFIG_CONFIG)
+  string(APPEND RunCMake_TEST_FAILED "CustomBuild for out-(CONFIG)-(CONFIG).txt.rule not found in\n  ${vcProjectFile}\n")
+endif()
+if(NOT found_CustomBuild_out_HASH)
+  string(APPEND RunCMake_TEST_FAILED "CustomBuild for <hash>.rule not found in\n  ${vcProjectFile}\n")
+endif()

+ 21 - 0
Tests/RunCMake/VS10Project/CustomCommandGenex.cmake

@@ -0,0 +1,21 @@
+add_custom_command(
+  OUTPUT "$<1:out.txt>"
+  COMMAND ${CMAKE_COMMAND} -E touch "out.txt"
+  VERBATIM
+  )
+add_custom_command(
+  OUTPUT "out-$<CONFIG>.txt"
+  COMMAND ${CMAKE_COMMAND} -E touch "out-$<CONFIG>.txt"
+  VERBATIM
+  )
+add_custom_command(
+  OUTPUT "out-$<CONFIG>-$<CONFIG>.txt"
+  COMMAND ${CMAKE_COMMAND} -E touch "out-$<CONFIG>-$<CONFIG>.txt"
+  VERBATIM
+  )
+add_custom_command(
+  OUTPUT "out-$<CONFIG>-$<CONFIG:Debug>.txt"
+  COMMAND ${CMAKE_COMMAND} -E touch "out-$<CONFIG>-$<CONFIG:Debug>.txt"
+  VERBATIM
+  )
+add_custom_target(foo DEPENDS "out.txt" "out-$<CONFIG>.txt" "out-$<CONFIG>-$<CONFIG>.txt" "out-$<CONFIG>-$<CONFIG:Debug>.txt")

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

@@ -7,6 +7,7 @@ if(CMAKE_C_COMPILER_ID STREQUAL "MSVC" AND CMAKE_C_COMPILER_VERSION VERSION_GREA
   run_cmake(LanguageStandard)
 endif()
 
+run_cmake(CustomCommandGenex)
 run_cmake(VsCsharpSourceGroup)
 run_cmake(VsCSharpCompilerOpts)
 run_cmake(ExplicitCMakeLists)

+ 22 - 11
Tests/RunCMake/add_custom_command/BadByproduct-stderr.txt

@@ -1,36 +1,47 @@
 CMake Error at BadByproduct.cmake:2 \(add_custom_command\):
-  add_custom_command called with BYPRODUCTS containing a "#".  This character
-  is not allowed.
+  BYPRODUCTS containing a "#" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
 
 CMake Error at BadByproduct.cmake:3 \(add_custom_command\):
-  add_custom_command called with BYPRODUCTS containing a "<".  This character
-  is not allowed.
+  BYPRODUCTS containing a "<" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
 
 CMake Error at BadByproduct.cmake:4 \(add_custom_command\):
-  add_custom_command called with BYPRODUCTS containing a ">".  This character
-  is not allowed.
+  BYPRODUCTS containing a ">" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
-
+(
 CMake Error at BadByproduct.cmake:5 \(add_custom_command\):
-  add_custom_command called with BYPRODUCTS containing a "<".  This character
-  is not allowed.
+  BYPRODUCTS containing a "#" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
-
+)+
 CMake Error at BadByproduct.cmake:6 \(add_custom_command\):
-  add_custom_command attempted to have a file
+  BYPRODUCTS path
 
     .*RunCMake/add_custom_command/f
 
   in a source directory as an output of custom command.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
+
+(
+CMake Error at BadByproduct.cmake:7 \(add_custom_command\):
+  Error evaluating generator expression:
+
+    \$<TARGET_PROPERTY:prop>
+
+  \$<TARGET_PROPERTY:prop> may only be used with binary targets.  It may not
+  be used with add_custom_command or add_custom_target.  Specify the target
+  to read a property from using the \$<TARGET_PROPERTY:tgt,prop> signature
+  instead.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)
+
+)+

+ 1 - 0
Tests/RunCMake/add_custom_command/BadByproduct.cmake

@@ -4,3 +4,4 @@ add_custom_command(OUTPUT b BYPRODUCTS "a<")
 add_custom_command(OUTPUT c BYPRODUCTS "a>")
 add_custom_command(OUTPUT d BYPRODUCTS "$<CONFIG>/#")
 add_custom_command(OUTPUT e BYPRODUCTS ${CMAKE_CURRENT_SOURCE_DIR}/f)
+add_custom_command(OUTPUT f BYPRODUCTS "$<TARGET_PROPERTY:prop>")

+ 22 - 11
Tests/RunCMake/add_custom_command/BadOutput-stderr.txt

@@ -1,36 +1,47 @@
 CMake Error at BadOutput.cmake:2 \(add_custom_command\):
-  add_custom_command called with OUTPUT containing a "#".  This character is
-  not allowed.
+  OUTPUT containing a "#" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
 
 CMake Error at BadOutput.cmake:3 \(add_custom_command\):
-  add_custom_command called with OUTPUT containing a "<".  This character is
-  not allowed.
+  OUTPUT containing a "<" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
 
 CMake Error at BadOutput.cmake:4 \(add_custom_command\):
-  add_custom_command called with OUTPUT containing a ">".  This character is
-  not allowed.
+  OUTPUT containing a ">" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
-
+(
 CMake Error at BadOutput.cmake:5 \(add_custom_command\):
-  add_custom_command called with OUTPUT containing a "<".  This character is
-  not allowed.
+  OUTPUT containing a "#" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
-
+)+
 CMake Error at BadOutput.cmake:6 \(add_custom_command\):
-  add_custom_command attempted to have a file
+  OUTPUT path
 
     .*RunCMake/add_custom_command/e
 
   in a source directory as an output of custom command.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
+
+(
+CMake Error at BadOutput.cmake:7 \(add_custom_command\):
+  Error evaluating generator expression:
+
+    \$<TARGET_PROPERTY:prop>
+
+  \$<TARGET_PROPERTY:prop> may only be used with binary targets.  It may not
+  be used with add_custom_command or add_custom_target.  Specify the target
+  to read a property from using the \$<TARGET_PROPERTY:tgt,prop> signature
+  instead.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)
+
+)+

+ 1 - 0
Tests/RunCMake/add_custom_command/BadOutput.cmake

@@ -4,3 +4,4 @@ add_custom_command(OUTPUT "a<" COMMAND b)
 add_custom_command(OUTPUT "a>" COMMAND c)
 add_custom_command(OUTPUT "$<CONFIG>/#" COMMAND d)
 add_custom_command(OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/e COMMAND f)
+add_custom_command(OUTPUT "$<TARGET_PROPERTY:prop>" COMMAND g)

+ 22 - 11
Tests/RunCMake/add_custom_target/BadByproduct-stderr.txt

@@ -1,36 +1,47 @@
 CMake Error at BadByproduct.cmake:2 \(add_custom_target\):
-  add_custom_target called with BYPRODUCTS containing a "#".  This character
-  is not allowed.
+  BYPRODUCTS containing a "#" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
 
 CMake Error at BadByproduct.cmake:3 \(add_custom_target\):
-  add_custom_target called with BYPRODUCTS containing a "<".  This character
-  is not allowed.
+  BYPRODUCTS containing a "<" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
 
 CMake Error at BadByproduct.cmake:4 \(add_custom_target\):
-  add_custom_target called with BYPRODUCTS containing a ">".  This character
-  is not allowed.
+  BYPRODUCTS containing a ">" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
-
+(
 CMake Error at BadByproduct.cmake:5 \(add_custom_target\):
-  add_custom_target called with BYPRODUCTS containing a "<".  This character
-  is not allowed.
+  BYPRODUCTS containing a "#" is not allowed.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
 
-
+)+
 CMake Error at BadByproduct.cmake:6 \(add_custom_target\):
-  add_custom_target attempted to have a file
+  BYPRODUCTS path
 
     .*RunCMake/add_custom_target/j
 
   in a source directory as an output of custom command.
 Call Stack \(most recent call first\):
   CMakeLists.txt:3 \(include\)
+
+(
+CMake Error at BadByproduct.cmake:7 \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<TARGET_PROPERTY:prop>
+
+  \$<TARGET_PROPERTY:prop> may only be used with binary targets.  It may not
+  be used with add_custom_command or add_custom_target.  Specify the target
+  to read a property from using the \$<TARGET_PROPERTY:tgt,prop> signature
+  instead.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)
+
+)+

+ 1 - 0
Tests/RunCMake/add_custom_target/BadByproduct.cmake

@@ -4,3 +4,4 @@ add_custom_target(c BYPRODUCTS "a<" COMMAND d)
 add_custom_target(e BYPRODUCTS "a>" COMMAND f)
 add_custom_target(g BYPRODUCTS "$<CONFIG>/#" COMMAND h)
 add_custom_target(i BYPRODUCTS ${CMAKE_CURRENT_SOURCE_DIR}/j COMMAND k)
+add_custom_target(l BYPRODUCTS "$<TARGET_PROPERTY:prop>" COMMAND m)

+ 0 - 1
bootstrap

@@ -293,7 +293,6 @@ CMAKE_CXX_SOURCES="\
   cmCMakePolicyCommand \
   cmCPackPropertiesGenerator \
   cmCacheManager \
-  cmCheckCustomOutputs \
   cmCommand \
   cmCommandArgumentParserHelper \
   cmCommands \