浏览代码

add_custom_command: Add CODEGEN support

By specifying CODEGEN as an argument to add_custom_command the
custom command will be added to a codegen build target.

The intent is to provide a convenient way for users to get
their generated files without having to build the whole project.

This can be helpful for code analysis tools which can be useful
for IDEs and CI.
Juan Ramos 1 年之前
父节点
当前提交
197cb419d1
共有 64 个文件被更改,包括 733 次插入3 次删除
  1. 32 0
      Help/command/add_custom_command.rst
  2. 8 0
      Help/manual/cmake-policies.7.rst
  3. 26 0
      Help/policy/CMP0171.rst
  4. 10 0
      Help/release/dev/codegen.rst
  5. 29 1
      Source/cmAddCustomCommandCommand.cxx
  6. 1 0
      Source/cmAddDependenciesCommand.cxx
  7. 5 0
      Source/cmCustomCommand.h
  8. 49 0
      Source/cmGlobalGenerator.cxx
  9. 5 0
      Source/cmGlobalGenerator.h
  10. 86 0
      Source/cmGlobalNinjaGenerator.cxx
  11. 39 0
      Source/cmGlobalUnixMakefileGenerator3.cxx
  12. 3 0
      Source/cmGlobalUnixMakefileGenerator3.h
  13. 6 0
      Source/cmLocalGenerator.cxx
  14. 15 0
      Source/cmLocalUnixMakefileGenerator3.cxx
  15. 29 0
      Source/cmMakefileTargetGenerator.cxx
  16. 3 1
      Source/cmPolicies.h
  17. 11 0
      Source/cmTarget.cxx
  18. 5 0
      Source/cmTarget.h
  19. 11 0
      Source/cmTargetTraceDependencies.cxx
  20. 1 0
      Tests/RunCMake/CMP0171/CMP0171-NEW-result.txt
  21. 4 0
      Tests/RunCMake/CMP0171/CMP0171-NEW-stderr.txt
  22. 6 0
      Tests/RunCMake/CMP0171/CMP0171-NEW.cmake
  23. 1 0
      Tests/RunCMake/CMP0171/CMP0171-OLD-result.txt
  24. 4 0
      Tests/RunCMake/CMP0171/CMP0171-OLD-stderr.txt
  25. 9 0
      Tests/RunCMake/CMP0171/CMP0171-OLD.cmake
  26. 9 0
      Tests/RunCMake/CMP0171/CMP0171-WARN-stderr.txt
  27. 6 0
      Tests/RunCMake/CMP0171/CMP0171-WARN.cmake
  28. 1 0
      Tests/RunCMake/CMP0171/CMP0171-codegen-build-result.txt
  29. 0 0
      Tests/RunCMake/CMP0171/CMP0171-codegen.cmake
  30. 6 0
      Tests/RunCMake/CMP0171/CMakeLists.txt
  31. 18 0
      Tests/RunCMake/CMP0171/RunCMakeTest.cmake
  32. 3 1
      Tests/RunCMake/CMakeLists.txt
  33. 7 0
      Tests/RunCMake/Codegen/CMakeLists.txt
  34. 33 0
      Tests/RunCMake/Codegen/RunCMakeTest.cmake
  35. 5 0
      Tests/RunCMake/Codegen/add-custom-command-depends-build-check.cmake
  36. 16 0
      Tests/RunCMake/Codegen/add-custom-command-depends.cmake
  37. 5 0
      Tests/RunCMake/Codegen/add-dependencies-build-check.cmake
  38. 18 0
      Tests/RunCMake/Codegen/add-dependencies.cmake
  39. 1 0
      Tests/RunCMake/Codegen/append-implicit-depends-result.txt
  40. 2 0
      Tests/RunCMake/Codegen/append-implicit-depends-stderr.txt
  41. 19 0
      Tests/RunCMake/Codegen/append-implicit-depends.cmake
  42. 5 0
      Tests/RunCMake/Codegen/byproducts-build-check.cmake
  43. 19 0
      Tests/RunCMake/Codegen/byproducts.cmake
  44. 1 0
      Tests/RunCMake/Codegen/error.c
  45. 11 0
      Tests/RunCMake/Codegen/exclude-from-all.cmake
  46. 1 0
      Tests/RunCMake/Codegen/generated.h.in
  47. 1 0
      Tests/RunCMake/Codegen/implicit-depends-append-codegen-result.txt
  48. 4 0
      Tests/RunCMake/Codegen/implicit-depends-append-codegen-stderr.txt
  49. 18 0
      Tests/RunCMake/Codegen/implicit-depends-append-codegen.cmake
  50. 1 0
      Tests/RunCMake/Codegen/implicit-depends-result.txt
  51. 4 0
      Tests/RunCMake/Codegen/implicit-depends-stderr.txt
  52. 11 0
      Tests/RunCMake/Codegen/implicit-depends.cmake
  53. 4 0
      Tests/RunCMake/Codegen/main.c
  54. 13 0
      Tests/RunCMake/Codegen/min-graph-1-build-check.cmake
  55. 26 0
      Tests/RunCMake/Codegen/min-graph-1.cmake
  56. 5 0
      Tests/RunCMake/Codegen/min-graph-2-build-check.cmake
  57. 18 0
      Tests/RunCMake/Codegen/min-graph-2.cmake
  58. 5 0
      Tests/RunCMake/Codegen/min-graph-3-build-check.cmake
  59. 12 0
      Tests/RunCMake/Codegen/min-graph-3.cmake
  60. 5 0
      Tests/RunCMake/Codegen/no-codegen-check.cmake
  61. 6 0
      Tests/RunCMake/Codegen/no-codegen.cmake
  62. 1 0
      Tests/RunCMake/Codegen/no-output-result.txt
  63. 4 0
      Tests/RunCMake/Codegen/no-output-stderr.txt
  64. 11 0
      Tests/RunCMake/Codegen/no-output.cmake

+ 32 - 0
Help/command/add_custom_command.rst

@@ -26,6 +26,7 @@ The first signature is for adding a custom command to produce an output:
                      [JOB_POOL job_pool]
                      [JOB_SERVER_AWARE <bool>]
                      [VERBATIM] [APPEND] [USES_TERMINAL]
+                     [CODEGEN]
                      [COMMAND_EXPAND_LISTS]
                      [DEPENDS_EXPLICIT_ONLY])
 
@@ -203,6 +204,18 @@ The options are:
   ``${CC} "-I$<JOIN:$<TARGET_PROPERTY:foo,INCLUDE_DIRECTORIES>,;-I>" foo.cc``
   to be properly expanded.
 
+``CODEGEN``
+  .. versionadded:: 3.31
+
+  Adds the custom command to a global ``codegen`` target that can be
+  used to execute the custom command while avoiding the majority of the
+  build graph.
+
+  This option is supported only by :ref:`Ninja Generators` and
+  :ref:`Makefile Generators`, and is ignored by other generators.
+  Furthermore, this option is allowed only if policy :policy:`CMP0171`
+  is set to ``NEW``.
+
 ``IMPLICIT_DEPENDS``
   Request scanning of implicit dependencies of an input file.
   The language given specifies the programming language whose
@@ -454,6 +467,25 @@ will re-run whenever ``in.txt`` changes.
   where ``<config>`` is the build configuration, and then compile the generated
   source as part of a library.
 
+.. versionadded:: 3.31
+  Use the ``CODEGEN`` option to add a custom command's outputs to the builtin
+  ``codegen`` target.  This is useful to make generated code available for
+   static analysis without building the entire project.  For example:
+
+  .. code-block:: cmake
+
+    add_executable(someTool someTool.c)
+
+    add_custom_command(
+      OUTPUT out.c
+      COMMAND someTool -o out.c
+      CODEGEN)
+
+    add_library(myLib out.c)
+
+  A user may build the ``codegen`` target to generate ``out.c``.
+  ``someTool`` is built as dependency, but ``myLib`` is not built at all.
+
 Example: Generating Files for Multiple Targets
 """"""""""""""""""""""""""""""""""""""""""""""
 

+ 8 - 0
Help/manual/cmake-policies.7.rst

@@ -51,6 +51,14 @@ The :variable:`CMAKE_MINIMUM_REQUIRED_VERSION` variable may also be used
 to determine whether to report an error on use of deprecated macros or
 functions.
 
+Policies Introduced by CMake 3.31
+=================================
+
+.. toctree::
+   :maxdepth: 1
+
+   CMP0171: 'codegen' is a reserved target name. </policy/CMP0171>
+
 Policies Introduced by CMake 3.30
 =================================
 

+ 26 - 0
Help/policy/CMP0171.rst

@@ -0,0 +1,26 @@
+CMP0171
+-------
+
+.. versionadded:: 3.31
+
+``codegen`` is a reserved target name.
+
+CMake 3.30 and earlier did not reserve ``codegen`` as a builtin target name,
+leaving projects free to create their own target with that name.
+CMake 3.31 and later prefer to reserve ``codegen`` as a builtin target name
+to drive custom commands created with the ``CODEGEN`` option to
+:command:`add_custom_command`.  In order to support building the ``codegen``
+target in scripted environments, e.g., ``cmake --build . --target codegen``,
+the ``codegen`` target needs to be generated even if no custom commands
+use the ``CODEGEN`` option.  This policy provides compatibility for projects
+that have not been updated to avoid creating a target named ``codegen``.
+
+The ``OLD`` behavior of this policy allows projects to create a target
+with the name ``codegen``.  The ``NEW`` behavior halts with a fatal error
+if a target with the name ``codegen`` is created.
+
+.. |INTRODUCED_IN_CMAKE_VERSION| replace:: 3.31
+.. |WARNS_OR_DOES_NOT_WARN| replace:: warns
+.. include:: STANDARD_ADVICE.txt
+
+.. include:: DEPRECATED.txt

+ 10 - 0
Help/release/dev/codegen.rst

@@ -0,0 +1,10 @@
+codegen
+-------
+
+* The :ref:`Ninja Generators` and :ref:`Makefile Generators` now produce
+  a ``codegen`` build target.  See policy :policy:`CMP0171`.  It drives a
+  subset of the build graph sufficient to run custom commands created with
+  :command:`add_custom_command`'s new ``CODEGEN`` option.
+
+* The :command:`add_custom_command` command gained a ``CODEGEN`` option
+  to mark a custom commands outputs as dependencies of a ``codegen`` target.

+ 29 - 1
Source/cmAddCustomCommandCommand.cxx

@@ -53,6 +53,7 @@ bool cmAddCustomCommandCommand(std::vector<std::string> const& args,
   bool command_expand_lists = false;
   bool depends_explicit_only =
     mf.IsOn("CMAKE_ADD_CUSTOM_COMMAND_DEPENDS_EXPLICIT_ONLY");
+  bool codegen = false;
   std::string implicit_depends_lang;
   cmImplicitDependsList implicit_depends;
 
@@ -111,6 +112,7 @@ bool cmAddCustomCommandCommand(std::vector<std::string> const& args,
   MAKE_STATIC_KEYWORD(VERBATIM);
   MAKE_STATIC_KEYWORD(WORKING_DIRECTORY);
   MAKE_STATIC_KEYWORD(DEPENDS_EXPLICIT_ONLY);
+  MAKE_STATIC_KEYWORD(CODEGEN);
 #undef MAKE_STATIC_KEYWORD
   static std::unordered_set<std::string> const keywords{
     keyAPPEND,
@@ -135,7 +137,8 @@ bool cmAddCustomCommandCommand(std::vector<std::string> const& args,
     keyUSES_TERMINAL,
     keyVERBATIM,
     keyWORKING_DIRECTORY,
-    keyDEPENDS_EXPLICIT_ONLY
+    keyDEPENDS_EXPLICIT_ONLY,
+    keyCODEGEN
   };
 
   for (std::string const& copy : args) {
@@ -166,6 +169,8 @@ bool cmAddCustomCommandCommand(std::vector<std::string> const& args,
         command_expand_lists = true;
       } else if (copy == keyDEPENDS_EXPLICIT_ONLY) {
         depends_explicit_only = true;
+      } else if (copy == keyCODEGEN) {
+        codegen = true;
       } else if (copy == keyTARGET) {
         doing = doing_target;
       } else if (copy == keyARGS) {
@@ -322,6 +327,28 @@ bool cmAddCustomCommandCommand(std::vector<std::string> const& args,
     return false;
   }
 
+  if (codegen) {
+    if (output.empty()) {
+      status.SetError("CODEGEN requires at least 1 OUTPUT.");
+      return false;
+    }
+
+    if (append) {
+      status.SetError("CODEGEN may not be used with APPEND.");
+      return false;
+    }
+
+    if (!implicit_depends.empty()) {
+      status.SetError("CODEGEN is not compatible with IMPLICIT_DEPENDS.");
+      return false;
+    }
+
+    if (mf.GetPolicyStatus(cmPolicies::CMP0171) != cmPolicies::NEW) {
+      status.SetError("CODEGEN option requires policy CMP0171 be set to NEW!");
+      return false;
+    }
+  }
+
   // Check for an append request.
   if (append) {
     mf.AppendCustomCommandToOutput(output[0], depends, implicit_depends,
@@ -355,6 +382,7 @@ bool cmAddCustomCommandCommand(std::vector<std::string> const& args,
     cc->SetOutputs(output);
     cc->SetMainDependency(main_dependency);
     cc->SetDepends(depends);
+    cc->SetCodegen(codegen);
     cc->SetImplicitDepends(implicit_depends);
     mf.AddCustomCommandToOutput(std::move(cc));
   } else {

+ 1 - 0
Source/cmAddDependenciesCommand.cxx

@@ -30,6 +30,7 @@ bool cmAddDependenciesCommand(std::vector<std::string> const& args,
     // skip over target_name
     for (std::string const& arg : cmMakeRange(args).advance(1)) {
       target->AddUtility(arg, false, &mf);
+      target->AddCodegenDependency(arg);
     }
   } else {
     mf.IssueMessage(

+ 5 - 0
Source/cmCustomCommand.h

@@ -132,6 +132,10 @@ public:
   const std::string& GetTarget() const;
   void SetTarget(const std::string& target);
 
+  /** Record if the custom command can be used for code generation. */
+  bool GetCodegen() const { return Codegen; }
+  void SetCodegen(bool b) { Codegen = b; }
+
 private:
   std::vector<std::string> Outputs;
   std::vector<std::string> Byproducts;
@@ -153,6 +157,7 @@ private:
   bool StdPipesUTF8 = false;
   bool HasMainDependency_ = false;
   bool DependsExplicitOnly = false;
+  bool Codegen = false;
 
 // Policies are NEW for synthesized custom commands, and set by cmMakefile for
 // user-created custom commands.

+ 49 - 0
Source/cmGlobalGenerator.cxx

@@ -1398,6 +1398,8 @@ void cmGlobalGenerator::Configure()
     }
   }
 
+  this->ReserveGlobalTargetCodegen();
+
   // update the cache entry for the number of local generators, this is used
   // for progress
   this->GetCMakeInstance()->AddCacheEntry(
@@ -2914,6 +2916,53 @@ void cmGlobalGenerator::AddGlobalTarget_Test(
   targets.push_back(std::move(gti));
 }
 
+void cmGlobalGenerator::ReserveGlobalTargetCodegen()
+{
+  // Read the policy value at the end of the top-level CMakeLists.txt file
+  // since it's a global policy that affects the whole project.
+  auto& mf = this->Makefiles[0];
+  const auto policyStatus = mf->GetPolicyStatus(cmPolicies::CMP0171);
+
+  this->AllowGlobalTargetCodegen = (policyStatus == cmPolicies::NEW);
+
+  cmTarget* tgt = this->FindTarget("codegen");
+  if (!tgt) {
+    return;
+  }
+
+  MessageType messageType = MessageType::AUTHOR_WARNING;
+  std::ostringstream e;
+  bool issueMessage = false;
+  switch (policyStatus) {
+    case cmPolicies::WARN:
+      e << cmPolicies::GetPolicyWarning(cmPolicies::CMP0171) << "\n";
+      issueMessage = true;
+      CM_FALLTHROUGH;
+    case cmPolicies::OLD:
+      break;
+    case cmPolicies::NEW:
+    case cmPolicies::REQUIRED_IF_USED:
+    case cmPolicies::REQUIRED_ALWAYS:
+      issueMessage = true;
+      messageType = MessageType::FATAL_ERROR;
+      break;
+  }
+  if (issueMessage) {
+    e << "The target name \"codegen\" is reserved.";
+    this->GetCMakeInstance()->IssueMessage(messageType, e.str(),
+                                           tgt->GetBacktrace());
+    if (messageType == MessageType::FATAL_ERROR) {
+      cmSystemTools::SetFatalErrorOccurred();
+      return;
+    }
+  }
+}
+
+bool cmGlobalGenerator::CheckCMP0171() const
+{
+  return this->AllowGlobalTargetCodegen;
+}
+
 void cmGlobalGenerator::AddGlobalTarget_EditCache(
   std::vector<GlobalTargetInfo>& targets) const
 {

+ 5 - 0
Source/cmGlobalGenerator.h

@@ -653,6 +653,8 @@ public:
 
   virtual std::string& EncodeLiteral(std::string& lit) { return lit; }
 
+  bool CheckCMP0171() const;
+
 protected:
   // for a project collect all its targets by following depend
   // information, and also collect all the targets
@@ -719,6 +721,8 @@ protected:
   void AddGlobalTarget_Install(std::vector<GlobalTargetInfo>& targets);
   void CreateGlobalTarget(GlobalTargetInfo const& gti, cmMakefile* mf);
 
+  void ReserveGlobalTargetCodegen();
+
   std::string FindMakeProgramFile;
   std::string ConfiguredFilesPath;
   cmake* CMakeInstance;
@@ -891,4 +895,5 @@ protected:
   bool ToolSupportsColor;
   bool InstallTargetEnabled;
   bool ConfigureDoneCMP0026AndCMP0024;
+  bool AllowGlobalTargetCodegen;
 };

+ 86 - 0
Source/cmGlobalNinjaGenerator.cxx

@@ -25,6 +25,7 @@
 
 #include "cmsys/FStream.hxx"
 
+#include "cmCustomCommand.h"
 #include "cmCxxModuleMapper.h"
 #include "cmDyndepCollation.h"
 #include "cmFortranParser.h"
@@ -43,6 +44,7 @@
 #include "cmOutputConverter.h"
 #include "cmRange.h"
 #include "cmScanDepFormat.h"
+#include "cmSourceFile.h"
 #include "cmState.h"
 #include "cmStateDirectory.h"
 #include "cmStateSnapshot.h"
@@ -1627,6 +1629,90 @@ void cmGlobalNinjaGenerator::WriteFolderTargets(std::ostream& os)
   std::map<std::string, DirectoryTarget> dirTargets =
     this->ComputeDirectoryTargets();
 
+  // Codegen target
+  if (this->CheckCMP0171()) {
+    for (auto const& it : dirTargets) {
+      cmNinjaBuild build("phony");
+      cmGlobalNinjaGenerator::WriteDivider(os);
+      std::string const& currentBinaryDir = it.first;
+      DirectoryTarget const& dt = it.second;
+      std::vector<std::string> configs =
+        dt.LG->GetMakefile()->GetGeneratorConfigs(
+          cmMakefile::IncludeEmptyConfig);
+
+      // Setup target
+      cmNinjaDeps configDeps;
+      build.Comment = cmStrCat("Folder: ", currentBinaryDir);
+      build.Outputs.emplace_back();
+      std::string const buildDirAllTarget =
+        this->ConvertToNinjaPath(cmStrCat(currentBinaryDir, "/codegen"));
+
+      cmNinjaDeps& explicitDeps = build.ExplicitDeps;
+
+      for (auto const& config : configs) {
+        explicitDeps.clear();
+
+        for (DirectoryTarget::Target const& t : dt.Targets) {
+          if (this->IsExcludedFromAllInConfig(t, config)) {
+            continue;
+          }
+
+          std::vector<cmSourceFile const*> customCommandSources;
+          t.GT->GetCustomCommands(customCommandSources, config);
+          for (cmSourceFile const* sf : customCommandSources) {
+            cmCustomCommand const* cc = sf->GetCustomCommand();
+            if (cc->GetCodegen()) {
+              auto const& outputs = cc->GetOutputs();
+
+              std::transform(outputs.begin(), outputs.end(),
+                             std::back_inserter(explicitDeps),
+                             this->MapToNinjaPath());
+            }
+          }
+        }
+
+        build.Outputs.front() = this->BuildAlias(buildDirAllTarget, config);
+        // Write target
+        this->WriteBuild(this->EnableCrossConfigBuild() &&
+                             this->CrossConfigs.count(config)
+                           ? os
+                           : *this->GetImplFileStream(config),
+                         build);
+      }
+
+      // Add shortcut target
+      if (this->IsMultiConfig()) {
+        for (auto const& config : configs) {
+          build.ExplicitDeps = { this->BuildAlias(buildDirAllTarget, config) };
+          build.Outputs.front() = buildDirAllTarget;
+          this->WriteBuild(*this->GetConfigFileStream(config), build);
+        }
+
+        if (!this->DefaultFileConfig.empty()) {
+          build.ExplicitDeps.clear();
+          for (auto const& config : this->DefaultConfigs) {
+            build.ExplicitDeps.push_back(
+              this->BuildAlias(buildDirAllTarget, config));
+          }
+          build.Outputs.front() = buildDirAllTarget;
+          this->WriteBuild(*this->GetDefaultFileStream(), build);
+        }
+      }
+
+      // Add target for all configs
+      if (this->EnableCrossConfigBuild()) {
+        build.ExplicitDeps.clear();
+        for (auto const& config : this->CrossConfigs) {
+          build.ExplicitDeps.push_back(
+            this->BuildAlias(buildDirAllTarget, config));
+        }
+        build.Outputs.front() = this->BuildAlias(buildDirAllTarget, "codegen");
+        this->WriteBuild(os, build);
+      }
+    }
+  }
+
+  // All target
   for (auto const& it : dirTargets) {
     cmNinjaBuild build("phony");
     cmGlobalNinjaGenerator::WriteDivider(os);

+ 39 - 0
Source/cmGlobalUnixMakefileGenerator3.cxx

@@ -23,6 +23,7 @@
 #include "cmStateTypes.h"
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
+#include "cmTarget.h"
 #include "cmTargetDepend.h"
 #include "cmValue.h"
 #include "cmake.h"
@@ -438,6 +439,10 @@ void cmGlobalUnixMakefileGenerator3::WriteDirectoryRules2(
   // Write directory-level rules for "all".
   this->WriteDirectoryRule2(ruleFileStream, rootLG, dt, "all", true, false);
 
+  // Write directory-level rules for "codegen".
+  this->WriteDirectoryRule2(ruleFileStream, rootLG, dt, "codegen", true,
+                            false);
+
   // Write directory-level rules for "preinstall".
   this->WriteDirectoryRule2(ruleFileStream, rootLG, dt, "preinstall", true,
                             true);
@@ -765,6 +770,17 @@ void cmGlobalUnixMakefileGenerator3::WriteConvenienceRules2(
                              depends, commands, true);
       }
 
+      // add the codegen rule
+      localName = lg.GetRelativeTargetDirectory(gtarget.get());
+      depends.clear();
+      commands.clear();
+      makeTargetName = cmStrCat(localName, "/codegen");
+      commands.push_back(
+        lg.GetRecursiveMakeCall(makefileName, makeTargetName));
+      this->AppendCodegenTargetDepends(depends, gtarget.get());
+      rootLG.WriteMakeRule(ruleFileStream, "codegen rule for target.",
+                           makeTargetName, depends, commands, true);
+
       // add the clean rule
       localName = lg.GetRelativeTargetDirectory(gtarget.get());
       makeTargetName = cmStrCat(localName, "/clean");
@@ -893,6 +909,29 @@ void cmGlobalUnixMakefileGenerator3::AppendGlobalTargetDepends(
   }
 }
 
+void cmGlobalUnixMakefileGenerator3::AppendCodegenTargetDepends(
+  std::vector<std::string>& depends, cmGeneratorTarget* target)
+{
+  const std::set<std::string>& codegen_depends =
+    target->Target->GetCodegenDeps();
+
+  for (cmTargetDepend const& i : this->GetTargetDirectDepends(target)) {
+    // Create the target-level dependency.
+    cmGeneratorTarget const* dep = i;
+    if (!dep->IsInBuildSystem()) {
+      continue;
+    }
+    if (codegen_depends.find(dep->GetName()) != codegen_depends.end()) {
+      cmLocalUnixMakefileGenerator3* lg3 =
+        static_cast<cmLocalUnixMakefileGenerator3*>(dep->GetLocalGenerator());
+      std::string tgtName = cmStrCat(
+        lg3->GetRelativeTargetDirectory(const_cast<cmGeneratorTarget*>(dep)),
+        "/all");
+      depends.push_back(tgtName);
+    }
+  }
+}
+
 void cmGlobalUnixMakefileGenerator3::WriteHelpRule(
   std::ostream& ruleFileStream, cmLocalUnixMakefileGenerator3* lg)
 {

+ 3 - 0
Source/cmGlobalUnixMakefileGenerator3.h

@@ -223,6 +223,9 @@ protected:
   void AppendGlobalTargetDepends(std::vector<std::string>& depends,
                                  cmGeneratorTarget* target);
 
+  void AppendCodegenTargetDepends(std::vector<std::string>& depends,
+                                  cmGeneratorTarget* target);
+
   // Target name hooks for superclass.
   const char* GetAllTargetName() const override { return "all"; }
   const char* GetInstallTargetName() const override { return "install"; }

+ 6 - 0
Source/cmLocalGenerator.cxx

@@ -4626,6 +4626,12 @@ void AppendCustomCommandToOutput(cmLocalGenerator& lg,
     if (cmCustomCommand* cc = sf->GetCustomCommand()) {
       cc->AppendCommands(commandLines);
       cc->AppendDepends(depends);
+      if (cc->GetCodegen() && !implicit_depends.empty()) {
+        lg.GetCMakeInstance()->IssueMessage(
+          MessageType::FATAL_ERROR,
+          "Cannot append IMPLICIT_DEPENDS to existing CODEGEN custom "
+          "command.");
+      }
       cc->AppendImplicitDepends(implicit_depends);
       return;
     }

+ 15 - 0
Source/cmLocalUnixMakefileGenerator3.cxx

@@ -1763,6 +1763,21 @@ void cmLocalUnixMakefileGenerator3::WriteLocalAllRules(
   this->WriteMakeRule(ruleFileStream, "The main all target", "all", depends,
                       commands, true);
 
+  // Write the codegen rule.
+  if (this->GetGlobalGenerator()->CheckCMP0171()) {
+    recursiveTarget = cmStrCat(this->GetCurrentBinaryDirectory(), "/codegen");
+    depends.clear();
+    commands.clear();
+    if (regenerate) {
+      depends.emplace_back("cmake_check_build_system");
+    }
+    commands.push_back(this->GetRecursiveMakeCall(mf2Dir, recursiveTarget));
+    AppendEcho(commands, "Finished generating code",
+               cmLocalUnixMakefileGenerator3::EchoColor::EchoGenerate);
+    this->WriteMakeRule(ruleFileStream, "The main codegen target", "codegen",
+                        depends, commands, true);
+  }
+
   // Write the clean rule.
   recursiveTarget = cmStrCat(this->GetCurrentBinaryDirectory(), "/clean");
   commands.clear();

+ 29 - 0
Source/cmMakefileTargetGenerator.cxx

@@ -251,6 +251,8 @@ void cmMakefileTargetGenerator::WriteTargetBuildRules()
   std::vector<cmSourceFile const*> customCommands;
   this->GeneratorTarget->GetCustomCommands(customCommands,
                                            this->GetConfigName());
+  std::vector<std::string> codegen_depends;
+  codegen_depends.reserve(customCommands.size());
   for (cmSourceFile const* sf : customCommands) {
     if (this->CMP0113New &&
         !this->LocalGenerator->GetCommandsVisited(this->GeneratorTarget)
@@ -273,6 +275,33 @@ void cmMakefileTargetGenerator::WriteTargetBuildRules()
           this->LocalGenerator->MaybeRelativeToCurBinDir(byproduct));
       }
     }
+
+    if (ccg.GetCC().GetCodegen()) {
+      std::string const& output = ccg.GetOutputs().front();
+
+      // We always attach the actual commands to the first output.
+      codegen_depends.emplace_back(output);
+    }
+  }
+
+  // Some make tools need a special dependency for an empty rule.
+  if (codegen_depends.empty()) {
+    std::string hack = this->GlobalGenerator->GetEmptyRuleHackDepends();
+    if (!hack.empty()) {
+      codegen_depends.emplace_back(std::move(hack));
+    }
+  }
+
+  // Construct the codegen target.
+  {
+    std::string const codegenTarget = cmStrCat(
+      this->LocalGenerator->GetRelativeTargetDirectory(this->GeneratorTarget),
+      "/codegen");
+
+    // Write the rule.
+    this->LocalGenerator->WriteMakeRule(*this->BuildFileStream, nullptr,
+                                        codegenTarget, codegen_depends, {},
+                                        true);
   }
 
   // Add byproducts from build events to the clean rules

+ 3 - 1
Source/cmPolicies.h

@@ -525,7 +525,9 @@ class cmMakefile;
          3, 30, 0, cmPolicies::WARN)                                          \
   SELECT(POLICY, CMP0170,                                                     \
          "FETCHCONTENT_FULLY_DISCONNECTED requirements are enforced.", 3, 30, \
-         0, cmPolicies::WARN)
+         0, cmPolicies::WARN)                                                 \
+  SELECT(POLICY, CMP0171, "'codegen' is a reserved target name.", 3, 31, 0,   \
+         cmPolicies::WARN)
 
 #define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
 #define CM_FOR_EACH_POLICY_ID(POLICY)                                         \

+ 11 - 0
Source/cmTarget.cxx

@@ -657,6 +657,7 @@ public:
   bool PerConfig;
   cmTarget::Visibility TargetVisibility;
   std::set<BT<std::pair<std::string, bool>>> Utilities;
+  std::set<std::string> CodegenDependencies;
   std::vector<cmCustomCommand> PreBuildCommands;
   std::vector<cmCustomCommand> PreLinkCommands;
   std::vector<cmCustomCommand> PostBuildCommands;
@@ -1238,6 +1239,16 @@ void cmTarget::AddUtility(BT<std::pair<std::string, bool>> util)
   this->impl->Utilities.emplace(std::move(util));
 }
 
+void cmTarget::AddCodegenDependency(std::string const& name)
+{
+  this->impl->CodegenDependencies.emplace(name);
+}
+
+std::set<std::string> const& cmTarget::GetCodegenDeps() const
+{
+  return this->impl->CodegenDependencies;
+}
+
 std::set<BT<std::pair<std::string, bool>>> const& cmTarget::GetUtilities()
   const
 {

+ 5 - 0
Source/cmTarget.h

@@ -173,6 +173,11 @@ public:
   void AddUtility(std::string const& name, bool cross,
                   cmMakefile const* mf = nullptr);
   void AddUtility(BT<std::pair<std::string, bool>> util);
+
+  void AddCodegenDependency(std::string const& name);
+
+  std::set<std::string> const& GetCodegenDeps() const;
+
   //! Get the utilities used by this target
   std::set<BT<std::pair<std::string, bool>>> const& GetUtilities() const;
 

+ 11 - 0
Source/cmTargetTraceDependencies.cxx

@@ -7,10 +7,12 @@
 
 #include <cmext/algorithm>
 
+#include "cmCustomCommand.h"
 #include "cmCustomCommandGenerator.h"
 #include "cmGeneratorTarget.h"
 #include "cmGlobalGenerator.h"
 #include "cmList.h"
+#include "cmListFileCache.h"
 #include "cmMakefile.h"
 #include "cmMessageType.h"
 #include "cmSourceFile.h"
@@ -132,6 +134,8 @@ void cmTargetTraceDependencies::FollowName(std::string const& name)
     // The name is a byproduct of a utility target or a PRE_BUILD, PRE_LINK, or
     // POST_BUILD command.
     this->GeneratorTarget->Target->AddUtility(t->GetName(), false);
+
+    this->GeneratorTarget->Target->AddCodegenDependency(t->GetName());
   }
   if (cmSourceFile* sf = i->second.Source) {
     // For now only follow the dependency if the source file is not a
@@ -213,6 +217,11 @@ void cmTargetTraceDependencies::CheckCustomCommand(cmCustomCommand const& cc)
       // Collect target-level dependencies referenced in command lines.
       for (auto const& util : ccg.GetUtilities()) {
         this->GeneratorTarget->Target->AddUtility(util);
+
+        if (ccg.GetCC().GetCodegen()) {
+          this->GeneratorTarget->Target->AddCodegenDependency(
+            util.Value.first);
+        }
       }
 
       // Collect file-level dependencies referenced in DEPENDS.
@@ -226,6 +235,8 @@ void cmTargetTraceDependencies::CheckCustomCommand(cmCustomCommand const& cc)
       // The dependency does not name a target and may be a file we
       // know how to generate.  Queue it.
       this->FollowName(dep);
+    } else {
+      this->GeneratorTarget->Target->AddCodegenDependency(dep);
     }
   }
 }

+ 1 - 0
Tests/RunCMake/CMP0171/CMP0171-NEW-result.txt

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

+ 4 - 0
Tests/RunCMake/CMP0171/CMP0171-NEW-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at CMP0171-NEW\.cmake:[0-9]+ \(add_custom_target\):
+  The target name "codegen" is reserved\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 6 - 0
Tests/RunCMake/CMP0171/CMP0171-NEW.cmake

@@ -0,0 +1,6 @@
+# codegen is now a reserved name and this will cause an error since the policy is new.
+add_custom_target(codegen
+  COMMAND ${CMAKE_COMMAND} -E copy
+    ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+)

+ 1 - 0
Tests/RunCMake/CMP0171/CMP0171-OLD-result.txt

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

+ 4 - 0
Tests/RunCMake/CMP0171/CMP0171-OLD-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at CMP0171-OLD\.cmake:[0-9]+ \(add_custom_command\):
+  add_custom_command CODEGEN option requires policy CMP0171 be set to NEW\!
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 9 - 0
Tests/RunCMake/CMP0171/CMP0171-OLD.cmake

@@ -0,0 +1,9 @@
+add_custom_command(
+  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated.hpp
+  COMMAND ${CMAKE_COMMAND} -E copy
+    ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.hpp
+  # This will cause an error since the CODEGEN option
+  # requires that CMP0171 is set to NEW
+  CODEGEN
+)

+ 9 - 0
Tests/RunCMake/CMP0171/CMP0171-WARN-stderr.txt

@@ -0,0 +1,9 @@
+CMake Warning \(dev\) at CMP0171-WARN\.cmake:[0-9]+ \(add_custom_target\):
+  Policy CMP0171 is not set: 'codegen' is a reserved target name\.  Run "cmake
+  --help-policy CMP0171" for policy details.  Use the cmake_policy command to
+  set the policy and suppress this warning\.
+
+  The target name "codegen" is reserved\.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+This warning is for project developers\.  Use -Wno-dev to suppress it\.

+ 6 - 0
Tests/RunCMake/CMP0171/CMP0171-WARN.cmake

@@ -0,0 +1,6 @@
+# CMake should warn the user if they have a target named codegen.
+add_custom_target(codegen
+  COMMAND ${CMAKE_COMMAND} -E copy
+    ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+)

+ 1 - 0
Tests/RunCMake/CMP0171/CMP0171-codegen-build-result.txt

@@ -0,0 +1 @@
+[^0]

+ 0 - 0
Tests/RunCMake/CMP0171/CMP0171-codegen.cmake


+ 6 - 0
Tests/RunCMake/CMP0171/CMakeLists.txt

@@ -0,0 +1,6 @@
+cmake_minimum_required(VERSION 3.29)
+project(${RunCMake_TEST} LANGUAGES C)
+
+include(${RunCMake_TEST}.cmake)
+
+enable_testing()

+ 18 - 0
Tests/RunCMake/CMP0171/RunCMakeTest.cmake

@@ -0,0 +1,18 @@
+include(RunCMake)
+
+run_cmake("CMP0171-WARN")
+
+run_cmake_with_options(CMP0171-OLD "-DCMAKE_POLICY_DEFAULT_CMP0171=OLD")
+
+run_cmake_with_options(CMP0171-NEW "-DCMAKE_POLICY_DEFAULT_CMP0171=NEW")
+
+# The entire point of this test is to ensure the codegen target is not created
+# unintentionally. It can only be created if CMP0171 is NEW.
+block()
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/CMP0171-codegen-build)
+  run_cmake(CMP0171-codegen)
+  set(RunCMake_TEST_NO_CLEAN 1)
+  set(RunCMake_TEST_OUTPUT_MERGE 1)
+  # This command will fail with either 1 or 2 depending.
+  run_cmake_command(CMP0171-codegen-build ${CMAKE_COMMAND} --build . --config Debug --target codegen)
+endblock()

+ 3 - 1
Tests/RunCMake/CMakeLists.txt

@@ -180,6 +180,7 @@ add_RunCMake_test(CMP0163)
 add_RunCMake_test(CMP0165)
 add_RunCMake_test(CMP0169)
 add_RunCMake_test(CMP0170)
+add_RunCMake_test(CMP0171)
 
 # The test for Policy 65 requires the use of the
 # CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS variable, which both the VS and Xcode
@@ -1228,7 +1229,8 @@ add_RunCMake_test(CMakePresetsWorkflow
 add_RunCMake_test(VerifyHeaderSets)
 add_RunCMake_test(set_tests_properties)
 
-if(${CMAKE_GENERATOR} MATCHES "Make|Ninja")
+if(CMAKE_GENERATOR MATCHES "Make|Ninja")
+  add_RunCMake_test(Codegen)
   add_RunCMake_test(TransformDepfile)
 endif()
 

+ 7 - 0
Tests/RunCMake/Codegen/CMakeLists.txt

@@ -0,0 +1,7 @@
+cmake_minimum_required(VERSION 3.29)
+project(${RunCMake_TEST} LANGUAGES C)
+
+# This value is read from the top level CMakeLists.txt
+cmake_policy(SET CMP0171 NEW)
+
+include(${RunCMake_TEST}.cmake)

+ 33 - 0
Tests/RunCMake/Codegen/RunCMakeTest.cmake

@@ -0,0 +1,33 @@
+include(RunCMake)
+
+function(run_codegen case)
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${case}-build)
+
+  run_cmake(${case})
+
+  set(RunCMake_TEST_NO_CLEAN 1)
+
+  run_cmake_command(${case}-build ${CMAKE_COMMAND} --build . --target codegen --config Debug)
+endfunction()
+
+# Builds codegen target when there are no custom commands marked codegen
+run_codegen("no-codegen")
+
+# We don't want codegen to drive parts of the project that are EXCLUDE_FROM_ALL
+run_codegen("exclude-from-all")
+
+# Ensures codegen builds minimal build graphs
+run_codegen("min-graph-1")
+run_codegen("min-graph-2")
+run_codegen("min-graph-3")
+
+# Handle specific cases that can affect codegen
+run_codegen("add-dependencies")
+run_codegen("add-custom-command-depends")
+run_codegen("byproducts")
+
+# Error handling
+run_cmake("implicit-depends")
+run_cmake("implicit-depends-append-codegen")
+run_cmake("append-implicit-depends")
+run_cmake("no-output")

+ 5 - 0
Tests/RunCMake/Codegen/add-custom-command-depends-build-check.cmake

@@ -0,0 +1,5 @@
+set(filename "${RunCMake_TEST_BINARY_DIR}/generated.hpp")
+if (NOT EXISTS "${filename}")
+  set(RunCMake_TEST_FAILED "expected file NOT created:\n ${filename}")
+  return()
+endif()

+ 16 - 0
Tests/RunCMake/Codegen/add-custom-command-depends.cmake

@@ -0,0 +1,16 @@
+add_custom_target(foobar
+  COMMAND ${CMAKE_COMMAND} -E copy
+    ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+)
+
+add_custom_command(
+  OUTPUT generated.hpp
+  # This test will fail if DEPENDS isn't accounted for in the codegen build graph
+  DEPENDS foobar
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+                                   ${CMAKE_CURRENT_BINARY_DIR}/generated.hpp
+  CODEGEN
+)
+
+add_custom_target(hpp_creator ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated.hpp)

+ 5 - 0
Tests/RunCMake/Codegen/add-dependencies-build-check.cmake

@@ -0,0 +1,5 @@
+set(filename "${RunCMake_TEST_BINARY_DIR}/generated.hpp")
+if (NOT EXISTS "${filename}")
+  set(RunCMake_TEST_FAILED "expected file NOT created:\n ${filename}")
+  return()
+endif()

+ 18 - 0
Tests/RunCMake/Codegen/add-dependencies.cmake

@@ -0,0 +1,18 @@
+add_custom_target(foobar
+  COMMAND ${CMAKE_COMMAND} -E copy
+    ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+)
+
+add_custom_command(
+  OUTPUT generated.hpp
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+                                   ${CMAKE_CURRENT_BINARY_DIR}/generated.hpp
+  CODEGEN
+)
+
+add_custom_target(hpp_creator ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated.hpp)
+
+# This test will fail if add_dependencies isn't account for in the
+# codegen build graph
+add_dependencies(hpp_creator foobar)

+ 1 - 0
Tests/RunCMake/Codegen/append-implicit-depends-result.txt

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

+ 2 - 0
Tests/RunCMake/Codegen/append-implicit-depends-stderr.txt

@@ -0,0 +1,2 @@
+CMake Error:
+  Cannot append IMPLICIT_DEPENDS to existing CODEGEN custom command\.

+ 19 - 0
Tests/RunCMake/Codegen/append-implicit-depends.cmake

@@ -0,0 +1,19 @@
+add_custom_command(
+  OUTPUT
+    ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
+  COMMAND
+    ${CMAKE_COMMAND} -E
+        copy ${CMAKE_CURRENT_SOURCE_DIR}/error.c
+        ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
+  CODEGEN
+)
+
+add_custom_command(
+  OUTPUT
+    ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
+
+  # ERROR out if IMPLICIT_DEPENDS is used with CODEGEN
+  IMPLICIT_DEPENDS C main.c
+
+  APPEND
+)

+ 5 - 0
Tests/RunCMake/Codegen/byproducts-build-check.cmake

@@ -0,0 +1,5 @@
+set(filename "${RunCMake_TEST_BINARY_DIR}/generated.hpp")
+if (NOT EXISTS "${filename}")
+  set(RunCMake_TEST_FAILED "expected file NOT created:\n ${filename}")
+  return()
+endif()

+ 19 - 0
Tests/RunCMake/Codegen/byproducts.cmake

@@ -0,0 +1,19 @@
+add_custom_target(foobar
+  COMMAND ${CMAKE_COMMAND} -E copy
+    ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+  BYPRODUCTS
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+)
+
+# This codegen step relies on the BYPRODUCTS of the previous command.
+# If foobar isn't properly accounted for as a dependency it will fail.
+add_custom_command(
+  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated.hpp
+  DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+  COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+                                   ${CMAKE_CURRENT_BINARY_DIR}/generated.hpp
+  CODEGEN
+)
+
+add_custom_target(hpp_creator ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated.hpp)

+ 1 - 0
Tests/RunCMake/Codegen/error.c

@@ -0,0 +1 @@
+#error "This file should not be compiled"

+ 11 - 0
Tests/RunCMake/Codegen/exclude-from-all.cmake

@@ -0,0 +1,11 @@
+add_custom_command(
+  OUTPUT
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+  COMMAND
+    ${CMAKE_COMMAND} -E false
+  CODEGEN
+)
+
+# We don't want codegen to drive parts of the project that are EXCLUDE_FROM_ALL.
+# This tests that foobar is properly excluded from the codegen build.
+add_executable(foobar EXCLUDE_FROM_ALL error.c ${CMAKE_CURRENT_BINARY_DIR}/generated.h)

+ 1 - 0
Tests/RunCMake/Codegen/generated.h.in

@@ -0,0 +1 @@
+// hello

+ 1 - 0
Tests/RunCMake/Codegen/implicit-depends-append-codegen-result.txt

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

+ 4 - 0
Tests/RunCMake/Codegen/implicit-depends-append-codegen-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at implicit-depends-append-codegen\.cmake:[0-9]+ \(add_custom_command\):
+  add_custom_command CODEGEN may not be used with APPEND\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)$

+ 18 - 0
Tests/RunCMake/Codegen/implicit-depends-append-codegen.cmake

@@ -0,0 +1,18 @@
+add_custom_command(
+  OUTPUT
+    ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
+
+  # ERROR out if IMPLICIT_DEPENDS is used with CODEGEN
+  IMPLICIT_DEPENDS C main.c
+)
+
+add_custom_command(
+  OUTPUT
+    ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
+  COMMAND
+    ${CMAKE_COMMAND} -E
+        copy ${CMAKE_CURRENT_SOURCE_DIR}/error.c
+        ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
+  CODEGEN
+  APPEND
+)

+ 1 - 0
Tests/RunCMake/Codegen/implicit-depends-result.txt

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

+ 4 - 0
Tests/RunCMake/Codegen/implicit-depends-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at implicit-depends\.cmake:[0-9]+ \(add_custom_command\):
+  add_custom_command CODEGEN is not compatible with IMPLICIT_DEPENDS\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 11 - 0
Tests/RunCMake/Codegen/implicit-depends.cmake

@@ -0,0 +1,11 @@
+add_custom_command(
+  OUTPUT
+    ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
+  COMMAND
+    ${CMAKE_COMMAND} -E
+        copy ${CMAKE_CURRENT_SOURCE_DIR}/error.c
+        ${CMAKE_CURRENT_BINARY_DIR}/main.cpp
+  CODEGEN
+  # ERROR out if IMPLICIT_DEPENDS is used with CODEGEN
+  IMPLICIT_DEPENDS C main.c
+)

+ 4 - 0
Tests/RunCMake/Codegen/main.c

@@ -0,0 +1,4 @@
+int main(void)
+{
+  return 0;
+}

+ 13 - 0
Tests/RunCMake/Codegen/min-graph-1-build-check.cmake

@@ -0,0 +1,13 @@
+set(filename "${RunCMake_TEST_BINARY_DIR}/generated.h")
+if (NOT EXISTS "${filename}")
+  set(RunCMake_TEST_FAILED "expected file NOT created:\n ${filename}")
+  return()
+endif()
+
+# foobar should be built since it was needed
+# by the code generation
+set(filename "${RunCMake_TEST_BINARY_DIR}/foobar.txt")
+if (NOT EXISTS "${filename}")
+  set(RunCMake_TEST_FAILED "expected file NOT created:\n ${filename}")
+  return()
+endif()

+ 26 - 0
Tests/RunCMake/Codegen/min-graph-1.cmake

@@ -0,0 +1,26 @@
+add_executable(foobar main.c)
+add_custom_command(
+  TARGET foobar POST_BUILD
+  COMMAND ${CMAKE_COMMAND} -E
+    copy ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in
+    ${CMAKE_CURRENT_BINARY_DIR}/foobar.txt
+)
+
+add_custom_command(
+  OUTPUT
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+  COMMAND
+    ${CMAKE_COMMAND} -E
+        copy ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in
+        ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+  COMMAND
+    # Generate a header file that requires foobar
+    foobar
+  CODEGEN
+)
+
+add_library(errorlib
+  # If this library is built error.c will cause the build to fail
+  error.c
+  ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+)

+ 5 - 0
Tests/RunCMake/Codegen/min-graph-2-build-check.cmake

@@ -0,0 +1,5 @@
+set(filename "${RunCMake_TEST_BINARY_DIR}/generated.h")
+if (NOT EXISTS "${filename}")
+  set(RunCMake_TEST_FAILED "expected file NOT created:\n ${filename}")
+  return()
+endif()

+ 18 - 0
Tests/RunCMake/Codegen/min-graph-2.cmake

@@ -0,0 +1,18 @@
+add_custom_command(
+  OUTPUT
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+  COMMAND
+    ${CMAKE_COMMAND} -E
+        copy ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in
+        ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+  CODEGEN
+)
+
+# This target should not be built. It has no reason
+# to be part of the codegen build graph
+add_custom_target(error_custom_target ALL
+  DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+
+  # Cause the build to fail
+  COMMAND ${CMAKE_COMMAND} -E false
+)

+ 5 - 0
Tests/RunCMake/Codegen/min-graph-3-build-check.cmake

@@ -0,0 +1,5 @@
+set(filename "${RunCMake_TEST_BINARY_DIR}/error_lib.c")
+if (NOT EXISTS "${filename}")
+  set(RunCMake_TEST_FAILED "expected file NOT created:\n ${filename}")
+  return()
+endif()

+ 12 - 0
Tests/RunCMake/Codegen/min-graph-3.cmake

@@ -0,0 +1,12 @@
+add_custom_command(
+  OUTPUT
+    ${CMAKE_CURRENT_BINARY_DIR}/error_lib.c
+  COMMAND
+    ${CMAKE_COMMAND} -E
+        copy ${CMAKE_CURRENT_SOURCE_DIR}/error.c
+        ${CMAKE_CURRENT_BINARY_DIR}/error_lib.c
+  CODEGEN
+)
+
+# This test will fail if error_lib.c is actually compiled
+add_executable(foobar ${CMAKE_CURRENT_BINARY_DIR}/error_lib.c)

+ 5 - 0
Tests/RunCMake/Codegen/no-codegen-check.cmake

@@ -0,0 +1,5 @@
+# Verify generated.hpp was NOT created
+set(unexpected "${RunCMake_TEST_BINARY_DIR}/generated.hpp")
+if(EXISTS "${unexpected}")
+  set(RunCMake_TEST_FAILED "unexpected file created:\n  ${unexpected}")
+endif()

+ 6 - 0
Tests/RunCMake/Codegen/no-codegen.cmake

@@ -0,0 +1,6 @@
+add_custom_command(
+  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated.hpp
+  COMMAND ${CMAKE_COMMAND} -E copy
+    ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.hpp
+)

+ 1 - 0
Tests/RunCMake/Codegen/no-output-result.txt

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

+ 4 - 0
Tests/RunCMake/Codegen/no-output-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at no-output\.cmake:[0-9]+ \(add_custom_command\):
+  add_custom_command CODEGEN requires at least 1 OUTPUT\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 11 - 0
Tests/RunCMake/Codegen/no-output.cmake

@@ -0,0 +1,11 @@
+add_custom_target(foobar
+  COMMAND ${CMAKE_COMMAND} -E copy
+    ${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in
+    ${CMAKE_CURRENT_BINARY_DIR}/generated.h
+)
+
+add_custom_command(TARGET foobar POST_BUILD
+  COMMAND
+    ${CMAKE_COMMAND} -E true
+  CODEGEN
+)