Răsfoiți Sursa

cmake_language: Add signature to DEFER calls to later times

Fixes: #19575
Brad King 5 ani în urmă
părinte
comite
e8b0359a43
100 a modificat fișierele cu 949 adăugiri și 25 ștergeri
  1. 119 0
      Help/command/cmake_language.rst
  2. 1 0
      Help/command/return.rst
  3. 6 1
      Help/manual/cmake.1.rst
  4. 5 0
      Help/release/dev/cmake_language-DEFER.rst
  5. 4 0
      Help/variable/CMAKE_CURRENT_LIST_LINE.rst
  6. 218 2
      Source/cmCMakeLanguageCommand.cxx
  7. 11 1
      Source/cmCommandArgumentParserHelper.cxx
  8. 7 0
      Source/cmGlobalGenerator.cxx
  9. 5 0
      Source/cmGlobalGenerator.h
  10. 5 2
      Source/cmListFileCache.cxx
  11. 8 2
      Source/cmListFileCache.h
  12. 179 11
      Source/cmMakefile.cxx
  13. 33 4
      Source/cmMakefile.h
  14. 15 0
      Source/cmState.cxx
  15. 2 0
      Source/cmState.h
  16. 1 0
      Source/cmStateTypes.h
  17. 1 1
      Source/cmake.cxx
  18. 1 1
      Tests/RunCMake/CommandLine/trace-json-v1-check.py
  19. 50 0
      Tests/RunCMake/cmake_language/RunCMakeTest.cmake
  20. 15 0
      Tests/RunCMake/cmake_language/defer_call-stderr.txt
  21. 8 0
      Tests/RunCMake/cmake_language/defer_call-stdout.txt
  22. 12 0
      Tests/RunCMake/cmake_language/defer_call.cmake
  23. 11 0
      Tests/RunCMake/cmake_language/defer_call/CMakeLists.txt
  24. 1 0
      Tests/RunCMake/cmake_language/defer_call/include.cmake
  25. 1 0
      Tests/RunCMake/cmake_language/defer_call_add_subdirectory-result.txt
  26. 9 0
      Tests/RunCMake/cmake_language/defer_call_add_subdirectory-stderr.txt
  27. 2 0
      Tests/RunCMake/cmake_language/defer_call_add_subdirectory.cmake
  28. 0 0
      Tests/RunCMake/cmake_language/defer_call_add_subdirectory/CMakeLists.txt
  29. 1 0
      Tests/RunCMake/cmake_language/defer_call_enable_language-result.txt
  30. 9 0
      Tests/RunCMake/cmake_language/defer_call_enable_language-stderr.txt
  31. 2 0
      Tests/RunCMake/cmake_language/defer_call_enable_language.cmake
  32. 1 0
      Tests/RunCMake/cmake_language/defer_call_error-result.txt
  33. 9 0
      Tests/RunCMake/cmake_language/defer_call_error-stderr.txt
  34. 3 0
      Tests/RunCMake/cmake_language/defer_call_error.cmake
  35. 2 0
      Tests/RunCMake/cmake_language/defer_call_error/CMakeLists.txt
  36. 13 0
      Tests/RunCMake/cmake_language/defer_call_ids-stdout.txt
  37. 14 0
      Tests/RunCMake/cmake_language/defer_call_ids.cmake
  38. 1 0
      Tests/RunCMake/cmake_language/defer_call_invalid_command-result.txt
  39. 4 0
      Tests/RunCMake/cmake_language/defer_call_invalid_command-stderr.txt
  40. 1 0
      Tests/RunCMake/cmake_language/defer_call_invalid_command.cmake
  41. 1 0
      Tests/RunCMake/cmake_language/defer_call_invalid_directory-result.txt
  42. 9 0
      Tests/RunCMake/cmake_language/defer_call_invalid_directory-stderr.txt
  43. 2 0
      Tests/RunCMake/cmake_language/defer_call_invalid_directory.cmake
  44. 0 0
      Tests/RunCMake/cmake_language/defer_call_invalid_directory/CMakeLists.txt
  45. 1 0
      Tests/RunCMake/cmake_language/defer_call_missing_directory-result.txt
  46. 9 0
      Tests/RunCMake/cmake_language/defer_call_missing_directory-stderr.txt
  47. 1 0
      Tests/RunCMake/cmake_language/defer_call_missing_directory.cmake
  48. 1 0
      Tests/RunCMake/cmake_language/defer_call_policy_PUSH-result.txt
  49. 2 0
      Tests/RunCMake/cmake_language/defer_call_policy_PUSH-stderr.txt
  50. 1 0
      Tests/RunCMake/cmake_language/defer_call_policy_PUSH.cmake
  51. 1 0
      Tests/RunCMake/cmake_language/defer_call_syntax_error-result.txt
  52. 13 0
      Tests/RunCMake/cmake_language/defer_call_syntax_error-stderr.txt
  53. 2 0
      Tests/RunCMake/cmake_language/defer_call_syntax_error.cmake
  54. 8 0
      Tests/RunCMake/cmake_language/defer_call_trace-stderr.txt
  55. 3 0
      Tests/RunCMake/cmake_language/defer_call_trace.cmake
  56. 5 0
      Tests/RunCMake/cmake_language/defer_call_trace_json-stderr.txt
  57. 3 0
      Tests/RunCMake/cmake_language/defer_call_trace_json.cmake
  58. 1 0
      Tests/RunCMake/cmake_language/defer_cancel_call_id-result.txt
  59. 4 0
      Tests/RunCMake/cmake_language/defer_cancel_call_id-stderr.txt
  60. 1 0
      Tests/RunCMake/cmake_language/defer_cancel_call_id.cmake
  61. 1 0
      Tests/RunCMake/cmake_language/defer_cancel_call_id_var-result.txt
  62. 4 0
      Tests/RunCMake/cmake_language/defer_cancel_call_id_var-stderr.txt
  63. 1 0
      Tests/RunCMake/cmake_language/defer_cancel_call_id_var.cmake
  64. 1 0
      Tests/RunCMake/cmake_language/defer_cancel_call_invalid_directory-result.txt
  65. 9 0
      Tests/RunCMake/cmake_language/defer_cancel_call_invalid_directory-stderr.txt
  66. 2 0
      Tests/RunCMake/cmake_language/defer_cancel_call_invalid_directory.cmake
  67. 0 0
      Tests/RunCMake/cmake_language/defer_cancel_call_invalid_directory/CMakeLists.txt
  68. 1 0
      Tests/RunCMake/cmake_language/defer_cancel_call_unknown_argument-result.txt
  69. 6 0
      Tests/RunCMake/cmake_language/defer_cancel_call_unknown_argument-stderr.txt
  70. 1 0
      Tests/RunCMake/cmake_language/defer_cancel_call_unknown_argument.cmake
  71. 1 0
      Tests/RunCMake/cmake_language/defer_directory_empty-result.txt
  72. 4 0
      Tests/RunCMake/cmake_language/defer_directory_empty-stderr.txt
  73. 1 0
      Tests/RunCMake/cmake_language/defer_directory_empty.cmake
  74. 1 0
      Tests/RunCMake/cmake_language/defer_directory_missing-result.txt
  75. 4 0
      Tests/RunCMake/cmake_language/defer_directory_missing-stderr.txt
  76. 1 0
      Tests/RunCMake/cmake_language/defer_directory_missing.cmake
  77. 1 0
      Tests/RunCMake/cmake_language/defer_directory_multiple-result.txt
  78. 4 0
      Tests/RunCMake/cmake_language/defer_directory_multiple-stderr.txt
  79. 1 0
      Tests/RunCMake/cmake_language/defer_directory_multiple.cmake
  80. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_id-result.txt
  81. 4 0
      Tests/RunCMake/cmake_language/defer_get_call_id-stderr.txt
  82. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_id.cmake
  83. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_id_empty-result.txt
  84. 4 0
      Tests/RunCMake/cmake_language/defer_get_call_id_empty-stderr.txt
  85. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_id_empty.cmake
  86. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_id_var-result.txt
  87. 4 0
      Tests/RunCMake/cmake_language/defer_get_call_id_var-stderr.txt
  88. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_id_var.cmake
  89. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_id-result.txt
  90. 4 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_id-stderr.txt
  91. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_id.cmake
  92. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_id_var-result.txt
  93. 4 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_id_var-stderr.txt
  94. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_id_var.cmake
  95. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_invalid_directory-result.txt
  96. 9 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_invalid_directory-stderr.txt
  97. 2 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_invalid_directory.cmake
  98. 0 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_invalid_directory/CMakeLists.txt
  99. 1 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_missing_var-result.txt
  100. 4 0
      Tests/RunCMake/cmake_language/defer_get_call_ids_missing_var-stderr.txt

+ 119 - 0
Help/command/cmake_language.rst

@@ -12,6 +12,7 @@ Synopsis
 
 
   cmake_language(`CALL`_ <command> [<arg>...])
   cmake_language(`CALL`_ <command> [<arg>...])
   cmake_language(`EVAL`_ CODE <code>...)
   cmake_language(`EVAL`_ CODE <code>...)
+  cmake_language(`DEFER`_ <options>... CALL <command> [<arg>...])
 
 
 Introduction
 Introduction
 ^^^^^^^^^^^^
 ^^^^^^^^^^^^
@@ -99,3 +100,121 @@ is equivalent to
   )
   )
 
 
   include(${CMAKE_CURRENT_BINARY_DIR}/eval.cmake)
   include(${CMAKE_CURRENT_BINARY_DIR}/eval.cmake)
+
+Deferring Calls
+^^^^^^^^^^^^^^^
+
+.. versionadded:: 3.19
+
+.. _DEFER:
+
+.. code-block:: cmake
+
+  cmake_language(DEFER <options>... CALL <command> [<arg>...])
+
+Schedules a call to the named ``<command>`` with the given arguments (if any)
+to occur at a later time.  By default, deferred calls are executed as if
+written at the end of the current directory's ``CMakeLists.txt`` file,
+except that they run even after a :command:`return` call.  Variable
+references in arguments are evaluated at the time the deferred call is
+executed.
+
+The options are:
+
+``DIRECTORY <dir>``
+  Schedule the call for the end of the given directory instead of the
+  current directory.  The ``<dir>`` may reference either a source
+  directory or its corresponding binary directory.  Relative paths are
+  treated as relative to the current source directory.
+
+  The given directory must be known to CMake, being either the top-level
+  directory or one added by :command:`add_subdirectory`.  Furthermore,
+  the given directory must not yet be finished processing.  This means
+  it can be the current directory or one of its ancestors.
+
+``ID <id>``
+  Specify an identification for the deferred call.
+  The id may not be empty and may not begin in a capital letter ``A-Z``.
+  The id may begin in a ``_`` only if it was generated by another call
+  that used ``ID_VAR`` to get the id.
+
+``ID_VAR <var>``
+  Sepcify a variable in which to store the identification for the
+  deferred call.  If ``ID <id>`` is not given, a new identification
+  will be generated starting in a ``_``.
+
+The currently scheduled list of deferred calls may be retrieved:
+
+.. code-block:: cmake
+
+  cmake_language(DEFER [DIRECTORY <dir>] GET_CALL_IDS <var>)
+
+This will store in ``<var>`` a :ref:`Semicolon-separated list <CMake Language
+Lists>` of deferred call ids.
+
+Details of a specific call may be retrieved from its id:
+
+.. code-block:: cmake
+
+  cmake_language(DEFER [DIRECTORY <dir>] GET_CALL <id> <var>)
+
+This will store in ``<var>`` a :ref:`Semicolon-separated list <CMake Language
+Lists>` in which the first element is the name of the command to be
+called, and the remaining elements are its unevaluated arguments (any
+contained ``;`` characters are included literally and cannot be distinguished
+from multiple arguments).  If multiple calls are scheduled with the same id,
+this retrieves the first one.  If no call is scheduled with the given id,
+this stores an empty string in the variable.
+
+Deferred calls may be canceled by their id:
+
+.. code-block:: cmake
+
+  cmake_language(DEFER [DIRECTORY <dir>] CANCEL_CALL <id>...)
+
+This cancels all deferred calls matching any of the given ids.
+Unknown ids are silently ignored.
+
+Deferred Call Examples
+""""""""""""""""""""""
+
+For example, the code:
+
+.. code-block:: cmake
+
+  cmake_language(DEFER CALL message "${deferred_message}")
+  cmake_language(DEFER ID_VAR id CALL message "Cancelled Message")
+  cmake_language(DEFER CANCEL_CALL ${id})
+  message("Immediate Message")
+  set(deferred_message "Deferred Message")
+
+prints::
+
+  Immediate Message
+  Deferred Message
+
+The ``Cancelled Message`` is never printed because its command is
+cancelled.  The ``deferred_message`` variable reference is not evaluated
+until the call site, so it can be set after the deferred call is scheduled.
+
+In order to evaluate variable references immediately when scheduling a
+deferred call, wrap it using ``cmake_language(EVAL)``.  However, note that
+arguments will be re-evaluated in the deferred call, though that can be
+avoided by using bracket arguments.  For example:
+
+.. code-block:: cmake
+
+  set(deferred_message "Deferred Message 1")
+  set(re_evaluated [[${deferred_message}]])
+  cmake_language(EVAL CODE "
+    cmake_language(DEFER CALL message [[${deferred_message}]])
+    cmake_language(DEFER CALL message \"${re_evaluated}\")
+  ")
+  message("Immediate Message")
+  set(deferred_message "Deferred Message 2")
+
+also prints::
+
+  Immediate Message
+  Deferred Message 1
+  Deferred Message 2

+ 1 - 0
Help/command/return.rst

@@ -12,6 +12,7 @@ encountered in an included file (via :command:`include` or
 :command:`find_package`), it causes processing of the current file to stop
 :command:`find_package`), it causes processing of the current file to stop
 and control is returned to the including file.  If it is encountered in a
 and control is returned to the including file.  If it is encountered in a
 file which is not included by another file, e.g.  a ``CMakeLists.txt``,
 file which is not included by another file, e.g.  a ``CMakeLists.txt``,
+deferred calls scheduled by :command:`cmake_language(DEFER)` are invoked and
 control is returned to the parent directory if there is one.  If return is
 control is returned to the parent directory if there is one.  If return is
 called in a function, control is returned to the caller of the function.
 called in a function, control is returned to the caller of the function.
 
 

+ 6 - 1
Help/manual/cmake.1.rst

@@ -295,6 +295,11 @@ Options
      ``line``
      ``line``
        The line in ``file`` of the function call.
        The line in ``file`` of the function call.
 
 
+     ``defer``
+       Optional member that is present when the function call was deferred
+       by :command:`cmake_language(DEFER)`.  If present, its value is a
+       string containing the deferred call ``<id>``.
+
      ``cmd``
      ``cmd``
        The name of the function that was called.
        The name of the function that was called.
 
 
@@ -317,7 +322,7 @@ Options
        {
        {
          "version": {
          "version": {
            "major": 1,
            "major": 1,
-           "minor": 0
+           "minor": 1
          }
          }
        }
        }
 
 

+ 5 - 0
Help/release/dev/cmake_language-DEFER.rst

@@ -0,0 +1,5 @@
+cmake_language-DEFER
+--------------------
+
+* The :command:`cmake_language` command gained a ``DEFER`` mode to
+  schedule command calls to occur at the end of processing a directory.

+ 4 - 0
Help/variable/CMAKE_CURRENT_LIST_LINE.rst

@@ -5,3 +5,7 @@ The line number of the current file being processed.
 
 
 This is the line number of the file currently being processed by
 This is the line number of the file currently being processed by
 cmake.
 cmake.
+
+If CMake is currently processing deferred calls scheduled by
+the :command:`cmake_language(DEFER)` command, this variable
+evaluates to ``DEFERRED`` instead of a specific line number.

+ 218 - 2
Source/cmCMakeLanguageCommand.cxx

@@ -7,11 +7,14 @@
 #include <cstddef>
 #include <cstddef>
 #include <memory>
 #include <memory>
 #include <string>
 #include <string>
+#include <utility>
 
 
+#include <cm/optional>
 #include <cm/string_view>
 #include <cm/string_view>
 #include <cmext/string_view>
 #include <cmext/string_view>
 
 
 #include "cmExecutionStatus.h"
 #include "cmExecutionStatus.h"
+#include "cmGlobalGenerator.h"
 #include "cmListFileCache.h"
 #include "cmListFileCache.h"
 #include "cmMakefile.h"
 #include "cmMakefile.h"
 #include "cmRange.h"
 #include "cmRange.h"
@@ -37,9 +40,24 @@ std::array<cm::static_string_view, 12> InvalidCommands{
   } // clang-format on
   } // clang-format on
 };
 };
 
 
+std::array<cm::static_string_view, 1> InvalidDeferCommands{
+  {
+    // clang-format off
+  "return"_s,
+  } // clang-format on
+};
+
+struct Defer
+{
+  std::string Id;
+  std::string IdVar;
+  cmMakefile* Directory = nullptr;
+};
+
 bool cmCMakeLanguageCommandCALL(std::vector<cmListFileArgument> const& args,
 bool cmCMakeLanguageCommandCALL(std::vector<cmListFileArgument> const& args,
                                 std::string const& callCommand,
                                 std::string const& callCommand,
-                                size_t startArg, cmExecutionStatus& status)
+                                size_t startArg, cm::optional<Defer> defer,
+                                cmExecutionStatus& status)
 {
 {
   // ensure specified command is valid
   // ensure specified command is valid
   // start/end flow control commands are not allowed
   // start/end flow control commands are not allowed
@@ -49,6 +67,12 @@ bool cmCMakeLanguageCommandCALL(std::vector<cmListFileArgument> const& args,
     return FatalError(status,
     return FatalError(status,
                       cmStrCat("invalid command specified: "_s, callCommand));
                       cmStrCat("invalid command specified: "_s, callCommand));
   }
   }
+  if (defer &&
+      std::find(InvalidDeferCommands.cbegin(), InvalidDeferCommands.cend(),
+                cmd) != InvalidDeferCommands.cend()) {
+    return FatalError(status,
+                      cmStrCat("invalid command specified: "_s, callCommand));
+  }
 
 
   cmMakefile& makefile = status.GetMakefile();
   cmMakefile& makefile = status.GetMakefile();
   cmListFileContext context = makefile.GetBacktrace().Top();
   cmListFileContext context = makefile.GetBacktrace().Top();
@@ -66,9 +90,106 @@ bool cmCMakeLanguageCommandCALL(std::vector<cmListFileArgument> const& args,
     func.Arguments.emplace_back(lfarg);
     func.Arguments.emplace_back(lfarg);
   }
   }
 
 
+  if (defer) {
+    if (defer->Id.empty()) {
+      defer->Id = makefile.NewDeferId();
+    }
+    if (!defer->IdVar.empty()) {
+      makefile.AddDefinition(defer->IdVar, defer->Id);
+    }
+    cmMakefile* deferMakefile =
+      defer->Directory ? defer->Directory : &makefile;
+    if (!deferMakefile->DeferCall(defer->Id, context.FilePath, func)) {
+      return FatalError(
+        status,
+        cmStrCat("DEFER CALL may not be scheduled in directory:\n  "_s,
+                 deferMakefile->GetCurrentBinaryDirectory(),
+                 "\nat this time."_s));
+    }
+    return true;
+  }
   return makefile.ExecuteCommand(func, status);
   return makefile.ExecuteCommand(func, status);
 }
 }
 
 
+bool cmCMakeLanguageCommandDEFER(Defer const& defer,
+                                 std::vector<std::string> const& args,
+                                 size_t arg, cmExecutionStatus& status)
+{
+  cmMakefile* deferMakefile =
+    defer.Directory ? defer.Directory : &status.GetMakefile();
+  if (args[arg] == "CANCEL_CALL"_s) {
+    ++arg; // Consume CANCEL_CALL.
+    auto ids = cmMakeRange(args).advance(arg);
+    for (std::string const& id : ids) {
+      if (id[0] >= 'A' && id[0] <= 'Z') {
+        return FatalError(
+          status, cmStrCat("DEFER CANCEL_CALL unknown argument:\n  "_s, id));
+      }
+      if (!deferMakefile->DeferCancelCall(id)) {
+        return FatalError(
+          status,
+          cmStrCat("DEFER CANCEL_CALL may not update directory:\n  "_s,
+                   deferMakefile->GetCurrentBinaryDirectory(),
+                   "\nat this time."_s));
+      }
+    }
+    return true;
+  }
+  if (args[arg] == "GET_CALL_IDS"_s) {
+    ++arg; // Consume GET_CALL_IDS.
+    if (arg == args.size()) {
+      return FatalError(status, "DEFER GET_CALL_IDS missing output variable");
+    }
+    std::string const& var = args[arg++];
+    if (arg != args.size()) {
+      return FatalError(status, "DEFER GET_CALL_IDS given too many arguments");
+    }
+    cm::optional<std::string> ids = deferMakefile->DeferGetCallIds();
+    if (!ids) {
+      return FatalError(
+        status,
+        cmStrCat("DEFER GET_CALL_IDS may not access directory:\n  "_s,
+                 deferMakefile->GetCurrentBinaryDirectory(),
+                 "\nat this time."_s));
+    }
+    status.GetMakefile().AddDefinition(var, *ids);
+    return true;
+  }
+  if (args[arg] == "GET_CALL"_s) {
+    ++arg; // Consume GET_CALL.
+    if (arg == args.size()) {
+      return FatalError(status, "DEFER GET_CALL missing id");
+    }
+    std::string const& id = args[arg++];
+    if (arg == args.size()) {
+      return FatalError(status, "DEFER GET_CALL missing output variable");
+    }
+    std::string const& var = args[arg++];
+    if (arg != args.size()) {
+      return FatalError(status, "DEFER GET_CALL given too many arguments");
+    }
+    if (id.empty()) {
+      return FatalError(status, "DEFER GET_CALL id may not be empty");
+    }
+    if (id[0] >= 'A' && id[0] <= 'Z') {
+      return FatalError(status,
+                        cmStrCat("DEFER GET_CALL unknown argument:\n "_s, id));
+    }
+    cm::optional<std::string> call = deferMakefile->DeferGetCall(id);
+    if (!call) {
+      return FatalError(
+        status,
+        cmStrCat("DEFER GET_CALL may not access directory:\n  "_s,
+                 deferMakefile->GetCurrentBinaryDirectory(),
+                 "\nat this time."_s));
+    }
+    status.GetMakefile().AddDefinition(var, *call);
+    return true;
+  }
+  return FatalError(status,
+                    cmStrCat("DEFER operation unknown: "_s, args[arg]));
+}
+
 bool cmCMakeLanguageCommandEVAL(std::vector<cmListFileArgument> const& args,
 bool cmCMakeLanguageCommandEVAL(std::vector<cmListFileArgument> const& args,
                                 cmExecutionStatus& status)
                                 cmExecutionStatus& status)
 {
 {
@@ -118,11 +239,105 @@ bool cmCMakeLanguageCommand(std::vector<cmListFileArgument> const& args,
     }
     }
     return true;
     return true;
   };
   };
+  auto finishArgs = [&]() {
+    std::vector<cmListFileArgument> tmpArgs(args.begin() + rawArg, args.end());
+    status.GetMakefile().ExpandArguments(tmpArgs, expArgs);
+    rawArg = args.size();
+  };
 
 
   if (!moreArgs()) {
   if (!moreArgs()) {
     return FatalError(status, "called with incorrect number of arguments");
     return FatalError(status, "called with incorrect number of arguments");
   }
   }
 
 
+  cm::optional<Defer> maybeDefer;
+  if (expArgs[expArg] == "DEFER"_s) {
+    ++expArg; // Consume "DEFER".
+
+    if (!moreArgs()) {
+      return FatalError(status, "DEFER requires at least one argument");
+    }
+
+    Defer defer;
+
+    // Process optional arguments.
+    while (moreArgs()) {
+      if (expArgs[expArg] == "CALL"_s) {
+        break;
+      }
+      if (expArgs[expArg] == "CANCEL_CALL"_s ||
+          expArgs[expArg] == "GET_CALL_IDS"_s ||
+          expArgs[expArg] == "GET_CALL"_s) {
+        if (!defer.Id.empty() || !defer.IdVar.empty()) {
+          return FatalError(status,
+                            cmStrCat("DEFER "_s, expArgs[expArg],
+                                     " does not accept ID or ID_VAR."_s));
+        }
+        finishArgs();
+        return cmCMakeLanguageCommandDEFER(defer, expArgs, expArg, status);
+      }
+      if (expArgs[expArg] == "DIRECTORY"_s) {
+        ++expArg; // Consume "DIRECTORY".
+        if (defer.Directory) {
+          return FatalError(status,
+                            "DEFER given multiple DIRECTORY arguments");
+        }
+        if (!moreArgs()) {
+          return FatalError(status, "DEFER DIRECTORY missing value");
+        }
+        std::string dir = expArgs[expArg++];
+        if (dir.empty()) {
+          return FatalError(status, "DEFER DIRECTORY may not be empty");
+        }
+        dir = cmSystemTools::CollapseFullPath(
+          dir, status.GetMakefile().GetCurrentSourceDirectory());
+        defer.Directory =
+          status.GetMakefile().GetGlobalGenerator()->FindMakefile(dir);
+        if (!defer.Directory) {
+          return FatalError(status,
+                            cmStrCat("DEFER DIRECTORY:\n  "_s, dir,
+                                     "\nis not known.  "_s,
+                                     "It may not have been processed yet."_s));
+        }
+      } else if (expArgs[expArg] == "ID"_s) {
+        ++expArg; // Consume "ID".
+        if (!defer.Id.empty()) {
+          return FatalError(status, "DEFER given multiple ID arguments");
+        }
+        if (!moreArgs()) {
+          return FatalError(status, "DEFER ID missing value");
+        }
+        defer.Id = expArgs[expArg++];
+        if (defer.Id.empty()) {
+          return FatalError(status, "DEFER ID may not be empty");
+        }
+        if (defer.Id[0] >= 'A' && defer.Id[0] <= 'Z') {
+          return FatalError(status, "DEFER ID may not start in A-Z.");
+        }
+      } else if (expArgs[expArg] == "ID_VAR"_s) {
+        ++expArg; // Consume "ID_VAR".
+        if (!defer.IdVar.empty()) {
+          return FatalError(status, "DEFER given multiple ID_VAR arguments");
+        }
+        if (!moreArgs()) {
+          return FatalError(status, "DEFER ID_VAR missing variable name");
+        }
+        defer.IdVar = expArgs[expArg++];
+        if (defer.IdVar.empty()) {
+          return FatalError(status, "DEFER ID_VAR may not be empty");
+        }
+      } else {
+        return FatalError(
+          status, cmStrCat("DEFER unknown option:\n  "_s, expArgs[expArg]));
+      }
+    }
+
+    if (!(moreArgs() && expArgs[expArg] == "CALL"_s)) {
+      return FatalError(status, "DEFER must be followed by a CALL argument");
+    }
+
+    maybeDefer = std::move(defer);
+  }
+
   if (expArgs[expArg] == "CALL") {
   if (expArgs[expArg] == "CALL") {
     ++expArg; // Consume "CALL".
     ++expArg; // Consume "CALL".
 
 
@@ -138,7 +353,8 @@ bool cmCMakeLanguageCommand(std::vector<cmListFileArgument> const& args,
     }
     }
 
 
     // Run the CALL.
     // Run the CALL.
-    return cmCMakeLanguageCommandCALL(args, callCommand, rawArg, status);
+    return cmCMakeLanguageCommandCALL(args, callCommand, rawArg,
+                                      std::move(maybeDefer), status);
   }
   }
 
 
   if (expArgs[expArg] == "EVAL") {
   if (expArgs[expArg] == "EVAL") {

+ 11 - 1
Source/cmCommandArgumentParserHelper.cxx

@@ -8,8 +8,11 @@
 #include <utility>
 #include <utility>
 
 
 #include <cm/memory>
 #include <cm/memory>
+#include <cm/optional>
+#include <cmext/string_view>
 
 
 #include "cmCommandArgumentLexer.h"
 #include "cmCommandArgumentLexer.h"
+#include "cmListFileCache.h"
 #include "cmMakefile.h"
 #include "cmMakefile.h"
 #include "cmProperty.h"
 #include "cmProperty.h"
 #include "cmState.h"
 #include "cmState.h"
@@ -91,7 +94,14 @@ const char* cmCommandArgumentParserHelper::ExpandVariable(const char* var)
     return nullptr;
     return nullptr;
   }
   }
   if (this->FileLine >= 0 && strcmp(var, "CMAKE_CURRENT_LIST_LINE") == 0) {
   if (this->FileLine >= 0 && strcmp(var, "CMAKE_CURRENT_LIST_LINE") == 0) {
-    return this->AddString(std::to_string(this->FileLine));
+    std::string line;
+    cmListFileContext const& top = this->Makefile->GetBacktrace().Top();
+    if (top.DeferId) {
+      line = cmStrCat("DEFERRED:"_s, *top.DeferId);
+    } else {
+      line = std::to_string(this->FileLine);
+    }
+    return this->AddString(line);
   }
   }
   cmProp value = this->Makefile->GetDefinition(var);
   cmProp value = this->Makefile->GetDefinition(var);
   if (!value) {
   if (!value) {

+ 7 - 0
Source/cmGlobalGenerator.cxx

@@ -15,6 +15,7 @@
 
 
 #include <cm/memory>
 #include <cm/memory>
 #include <cmext/algorithm>
 #include <cmext/algorithm>
+#include <cmext/string_view>
 
 
 #include "cmsys/Directory.hxx"
 #include "cmsys/Directory.hxx"
 #include "cmsys/FStream.hxx"
 #include "cmsys/FStream.hxx"
@@ -1211,6 +1212,7 @@ void cmGlobalGenerator::Configure()
 {
 {
   this->FirstTimeProgress = 0.0f;
   this->FirstTimeProgress = 0.0f;
   this->ClearGeneratorMembers();
   this->ClearGeneratorMembers();
+  this->NextDeferId = 0;
 
 
   cmStateSnapshot snapshot = this->CMakeInstance->GetCurrentSnapshot();
   cmStateSnapshot snapshot = this->CMakeInstance->GetCurrentSnapshot();
 
 
@@ -3256,6 +3258,11 @@ const std::string& cmGlobalGenerator::GetRealPath(const std::string& dir)
   return i->second;
   return i->second;
 }
 }
 
 
+std::string cmGlobalGenerator::NewDeferId()
+{
+  return cmStrCat("__"_s, std::to_string(this->NextDeferId++));
+}
+
 void cmGlobalGenerator::ProcessEvaluationFiles()
 void cmGlobalGenerator::ProcessEvaluationFiles()
 {
 {
   std::vector<std::string> generatedFiles;
   std::vector<std::string> generatedFiles;

+ 5 - 0
Source/cmGlobalGenerator.h

@@ -508,6 +508,8 @@ public:
 
 
   std::string const& GetRealPath(std::string const& dir);
   std::string const& GetRealPath(std::string const& dir);
 
 
+  std::string NewDeferId();
+
 protected:
 protected:
   // for a project collect all its targets by following depend
   // for a project collect all its targets by following depend
   // information, and also collect all the targets
   // information, and also collect all the targets
@@ -633,6 +635,9 @@ private:
   std::map<std::string, int> LanguageToLinkerPreference;
   std::map<std::string, int> LanguageToLinkerPreference;
   std::map<std::string, std::string> LanguageToOriginalSharedLibFlags;
   std::map<std::string, std::string> LanguageToOriginalSharedLibFlags;
 
 
+  // Deferral id generation.
+  size_t NextDeferId = 0;
+
   // Record hashes for rules and outputs.
   // Record hashes for rules and outputs.
   struct RuleHash
   struct RuleHash
   {
   {

+ 5 - 2
Source/cmListFileCache.cxx

@@ -446,7 +446,8 @@ void cmListFileBacktrace::PrintCallStack(std::ostream& out) const
   cmStateSnapshot bottom = this->GetBottom();
   cmStateSnapshot bottom = this->GetBottom();
   for (Entry const* cur = this->TopEntry->Parent.get(); !cur->IsBottom();
   for (Entry const* cur = this->TopEntry->Parent.get(); !cur->IsBottom();
        cur = cur->Parent.get()) {
        cur = cur->Parent.get()) {
-    if (cur->Context.Name.empty()) {
+    if (cur->Context.Name.empty() &&
+        cur->Context.Line != cmListFileContext::DeferPlaceholderLine) {
       // Skip this whole-file scope.  When we get here we already will
       // Skip this whole-file scope.  When we get here we already will
       // have printed a more-specific context within the file.
       // have printed a more-specific context within the file.
       continue;
       continue;
@@ -483,11 +484,13 @@ bool cmListFileBacktrace::Empty() const
 std::ostream& operator<<(std::ostream& os, cmListFileContext const& lfc)
 std::ostream& operator<<(std::ostream& os, cmListFileContext const& lfc)
 {
 {
   os << lfc.FilePath;
   os << lfc.FilePath;
-  if (lfc.Line) {
+  if (lfc.Line > 0) {
     os << ":" << lfc.Line;
     os << ":" << lfc.Line;
     if (!lfc.Name.empty()) {
     if (!lfc.Name.empty()) {
       os << " (" << lfc.Name << ")";
       os << " (" << lfc.Name << ")";
     }
     }
+  } else if (lfc.Line == cmListFileContext::DeferPlaceholderLine) {
+    os << ":DEFERRED";
   }
   }
   return os;
   return os;
 }
 }

+ 8 - 2
Source/cmListFileCache.h

@@ -11,6 +11,8 @@
 #include <utility>
 #include <utility>
 #include <vector>
 #include <vector>
 
 
+#include <cm/optional>
+
 #include "cmStateSnapshot.h"
 #include "cmStateSnapshot.h"
 
 
 /** \class cmListFileCache
 /** \class cmListFileCache
@@ -72,6 +74,8 @@ public:
   std::string Name;
   std::string Name;
   std::string FilePath;
   std::string FilePath;
   long Line = 0;
   long Line = 0;
+  static long const DeferPlaceholderLine = -1;
+  cm::optional<std::string> DeferId;
 
 
   cmListFileContext() = default;
   cmListFileContext() = default;
   cmListFileContext(std::string name, std::string filePath, long line)
   cmListFileContext(std::string name, std::string filePath, long line)
@@ -81,13 +85,15 @@ public:
   {
   {
   }
   }
 
 
-  static cmListFileContext FromCommandContext(cmCommandContext const& lfcc,
-                                              std::string const& fileName)
+  static cmListFileContext FromCommandContext(
+    cmCommandContext const& lfcc, std::string const& fileName,
+    cm::optional<std::string> deferId = {})
   {
   {
     cmListFileContext lfc;
     cmListFileContext lfc;
     lfc.FilePath = fileName;
     lfc.FilePath = fileName;
     lfc.Line = lfcc.Line;
     lfc.Line = lfcc.Line;
     lfc.Name = lfcc.Name.Original;
     lfc.Name = lfcc.Name.Original;
+    lfc.DeferId = std::move(deferId);
     return lfc;
     return lfc;
   }
   }
 };
 };

+ 179 - 11
Source/cmMakefile.cxx

@@ -16,6 +16,7 @@
 #include <cm/iterator>
 #include <cm/iterator>
 #include <cm/memory>
 #include <cm/memory>
 #include <cm/optional>
 #include <cm/optional>
+#include <cm/type_traits> // IWYU pragma: keep
 #include <cm/vector>
 #include <cm/vector>
 #include <cmext/algorithm>
 #include <cmext/algorithm>
 #include <cmext/string_view>
 #include <cmext/string_view>
@@ -274,7 +275,9 @@ cmListFileBacktrace cmMakefile::GetBacktrace() const
   return this->Backtrace;
   return this->Backtrace;
 }
 }
 
 
-void cmMakefile::PrintCommandTrace(const cmListFileFunction& lff) const
+void cmMakefile::PrintCommandTrace(
+  cmListFileFunction const& lff,
+  cm::optional<std::string> const& deferId) const
 {
 {
   // Check if current file in the list of requested to trace...
   // Check if current file in the list of requested to trace...
   std::vector<std::string> const& trace_only_this_files =
   std::vector<std::string> const& trace_only_this_files =
@@ -322,6 +325,9 @@ void cmMakefile::PrintCommandTrace(const cmListFileFunction& lff) const
       builder["indentation"] = "";
       builder["indentation"] = "";
       val["file"] = full_path;
       val["file"] = full_path;
       val["line"] = static_cast<Json::Value::Int64>(lff.Line);
       val["line"] = static_cast<Json::Value::Int64>(lff.Line);
+      if (deferId) {
+        val["defer"] = *deferId;
+      }
       val["cmd"] = lff.Name.Original;
       val["cmd"] = lff.Name.Original;
       val["args"] = Json::Value(Json::arrayValue);
       val["args"] = Json::Value(Json::arrayValue);
       for (std::string const& arg : args) {
       for (std::string const& arg : args) {
@@ -335,8 +341,11 @@ void cmMakefile::PrintCommandTrace(const cmListFileFunction& lff) const
       break;
       break;
     }
     }
     case cmake::TraceFormat::TRACE_HUMAN:
     case cmake::TraceFormat::TRACE_HUMAN:
-      msg << full_path << "(" << lff.Line << "):  ";
-      msg << lff.Name.Original << "(";
+      msg << full_path << "(" << lff.Line << "):";
+      if (deferId) {
+        msg << "DEFERRED:" << *deferId << ":";
+      }
+      msg << "  " << lff.Name.Original << "(";
 
 
       for (std::string const& arg : args) {
       for (std::string const& arg : args) {
         msg << arg << " ";
         msg << arg << " ";
@@ -361,11 +370,12 @@ class cmMakefileCall
 {
 {
 public:
 public:
   cmMakefileCall(cmMakefile* mf, cmListFileFunction const& lff,
   cmMakefileCall(cmMakefile* mf, cmListFileFunction const& lff,
-                 cmExecutionStatus& status)
+                 cm::optional<std::string> deferId, cmExecutionStatus& status)
     : Makefile(mf)
     : Makefile(mf)
   {
   {
     cmListFileContext const& lfc = cmListFileContext::FromCommandContext(
     cmListFileContext const& lfc = cmListFileContext::FromCommandContext(
-      lff, this->Makefile->StateSnapshot.GetExecutionListFile());
+      lff, this->Makefile->StateSnapshot.GetExecutionListFile(),
+      std::move(deferId));
     this->Makefile->Backtrace = this->Makefile->Backtrace.Push(lfc);
     this->Makefile->Backtrace = this->Makefile->Backtrace.Push(lfc);
     ++this->Makefile->RecursionDepth;
     ++this->Makefile->RecursionDepth;
     this->Makefile->ExecutionStatusStack.push_back(&status);
     this->Makefile->ExecutionStatusStack.push_back(&status);
@@ -402,7 +412,8 @@ void cmMakefile::OnExecuteCommand(std::function<void()> callback)
 }
 }
 
 
 bool cmMakefile::ExecuteCommand(const cmListFileFunction& lff,
 bool cmMakefile::ExecuteCommand(const cmListFileFunction& lff,
-                                cmExecutionStatus& status)
+                                cmExecutionStatus& status,
+                                cm::optional<std::string> deferId)
 {
 {
   bool result = true;
   bool result = true;
 
 
@@ -417,7 +428,7 @@ bool cmMakefile::ExecuteCommand(const cmListFileFunction& lff,
   }
   }
 
 
   // Place this call on the call stack.
   // Place this call on the call stack.
-  cmMakefileCall stack_manager(this, lff, status);
+  cmMakefileCall stack_manager(this, lff, std::move(deferId), status);
   static_cast<void>(stack_manager);
   static_cast<void>(stack_manager);
 
 
   // Check for maximum recursion depth.
   // Check for maximum recursion depth.
@@ -445,7 +456,7 @@ bool cmMakefile::ExecuteCommand(const cmListFileFunction& lff,
     if (!cmSystemTools::GetFatalErrorOccured()) {
     if (!cmSystemTools::GetFatalErrorOccured()) {
       // if trace is enabled, print out invoke information
       // if trace is enabled, print out invoke information
       if (this->GetCMakeInstance()->GetTrace()) {
       if (this->GetCMakeInstance()->GetTrace()) {
-        this->PrintCommandTrace(lff);
+        this->PrintCommandTrace(lff, this->Backtrace.Top().DeferId);
       }
       }
       // Try invoking the command.
       // Try invoking the command.
       bool invokeSucceeded = command(lff.Arguments, status);
       bool invokeSucceeded = command(lff.Arguments, status);
@@ -663,6 +674,53 @@ private:
   bool ReportError;
   bool ReportError;
 };
 };
 
 
+class cmMakefile::DeferScope
+{
+public:
+  DeferScope(cmMakefile* mf, std::string const& deferredInFile)
+    : Makefile(mf)
+  {
+    cmListFileContext lfc;
+    lfc.Line = cmListFileContext::DeferPlaceholderLine;
+    lfc.FilePath = deferredInFile;
+    this->Makefile->Backtrace = this->Makefile->Backtrace.Push(lfc);
+    this->Makefile->DeferRunning = true;
+  }
+
+  ~DeferScope()
+  {
+    this->Makefile->DeferRunning = false;
+    this->Makefile->Backtrace = this->Makefile->Backtrace.Pop();
+  }
+
+  DeferScope(const DeferScope&) = delete;
+  DeferScope& operator=(const DeferScope&) = delete;
+
+private:
+  cmMakefile* Makefile;
+};
+
+class cmMakefile::DeferCallScope
+{
+public:
+  DeferCallScope(cmMakefile* mf, std::string const& deferredFromFile)
+    : Makefile(mf)
+  {
+    this->Makefile->StateSnapshot =
+      this->Makefile->GetState()->CreateDeferCallSnapshot(
+        this->Makefile->StateSnapshot, deferredFromFile);
+    assert(this->Makefile->StateSnapshot.IsValid());
+  }
+
+  ~DeferCallScope() { this->Makefile->PopSnapshot(); }
+
+  DeferCallScope(const DeferCallScope&) = delete;
+  DeferCallScope& operator=(const DeferCallScope&) = delete;
+
+private:
+  cmMakefile* Makefile;
+};
+
 bool cmMakefile::ReadListFile(const std::string& filename)
 bool cmMakefile::ReadListFile(const std::string& filename)
 {
 {
   std::string filenametoread = cmSystemTools::CollapseFullPath(
   std::string filenametoread = cmSystemTools::CollapseFullPath(
@@ -705,7 +763,8 @@ bool cmMakefile::ReadListFileAsString(const std::string& content,
 }
 }
 
 
 void cmMakefile::RunListFile(cmListFile const& listFile,
 void cmMakefile::RunListFile(cmListFile const& listFile,
-                             std::string const& filenametoread)
+                             std::string const& filenametoread,
+                             DeferCommands* defer)
 {
 {
   // add this list file to the list of dependencies
   // add this list file to the list of dependencies
   this->ListFiles.push_back(filenametoread);
   this->ListFiles.push_back(filenametoread);
@@ -736,6 +795,33 @@ void cmMakefile::RunListFile(cmListFile const& listFile,
     }
     }
   }
   }
 
 
+  // Run any deferred commands.
+  if (defer) {
+    // Add a backtrace level indicating calls are deferred.
+    DeferScope scope(this, filenametoread);
+
+    // Iterate by index in case one deferred call schedules another.
+    // NOLINTNEXTLINE(modernize-loop-convert)
+    for (size_t i = 0; i < defer->Commands.size(); ++i) {
+      DeferCommand& d = defer->Commands[i];
+      if (d.Id.empty()) {
+        // Cancelled.
+        continue;
+      }
+      // Mark as executed.
+      std::string id = std::move(d.Id);
+
+      // The deferred call may have come from another file.
+      DeferCallScope callScope(this, d.FilePath);
+
+      cmExecutionStatus status(*this);
+      this->ExecuteCommand(d.Command, status, std::move(id));
+      if (cmSystemTools::GetFatalErrorOccured()) {
+        break;
+      }
+    }
+  }
+
   this->AddDefinition("CMAKE_PARENT_LIST_FILE", currentParentFile);
   this->AddDefinition("CMAKE_PARENT_LIST_FILE", currentParentFile);
   this->AddDefinition("CMAKE_CURRENT_LIST_FILE", currentFile);
   this->AddDefinition("CMAKE_CURRENT_LIST_FILE", currentFile);
   this->AddDefinition("CMAKE_CURRENT_LIST_DIR",
   this->AddDefinition("CMAKE_CURRENT_LIST_DIR",
@@ -1678,7 +1764,9 @@ void cmMakefile::Configure()
     }
     }
   }
   }
 
 
-  this->RunListFile(listFile, currentStart);
+  this->Defer = cm::make_unique<DeferCommands>();
+  this->RunListFile(listFile, currentStart, this->Defer.get());
+  this->Defer.reset();
   if (cmSystemTools::GetFatalErrorOccured()) {
   if (cmSystemTools::GetFatalErrorOccured()) {
     scope.Quiet();
     scope.Quiet();
   }
   }
@@ -1753,6 +1841,13 @@ void cmMakefile::AddSubDirectory(const std::string& srcPath,
                                  const std::string& binPath,
                                  const std::string& binPath,
                                  bool excludeFromAll, bool immediate)
                                  bool excludeFromAll, bool immediate)
 {
 {
+  if (this->DeferRunning) {
+    this->IssueMessage(
+      MessageType::FATAL_ERROR,
+      "Subdirectories may not be created during deferred execution.");
+    return;
+  }
+
   // Make sure the binary directory is unique.
   // Make sure the binary directory is unique.
   if (!this->EnforceUniqueDir(srcPath, binPath)) {
   if (!this->EnforceUniqueDir(srcPath, binPath)) {
     return;
     return;
@@ -2960,6 +3055,68 @@ void cmMakefile::SetRecursionDepth(int recursionDepth)
   this->RecursionDepth = recursionDepth;
   this->RecursionDepth = recursionDepth;
 }
 }
 
 
+std::string cmMakefile::NewDeferId()
+{
+  return this->GetGlobalGenerator()->NewDeferId();
+}
+
+bool cmMakefile::DeferCall(std::string id, std::string file,
+                           cmListFileFunction lff)
+{
+  if (!this->Defer) {
+    return false;
+  }
+  this->Defer->Commands.emplace_back(
+    DeferCommand{ std::move(id), std::move(file), std::move(lff) });
+  return true;
+}
+
+bool cmMakefile::DeferCancelCall(std::string const& id)
+{
+  if (!this->Defer) {
+    return false;
+  }
+  for (DeferCommand& dc : this->Defer->Commands) {
+    if (dc.Id == id) {
+      dc.Id.clear();
+    }
+  }
+  return true;
+}
+
+cm::optional<std::string> cmMakefile::DeferGetCallIds() const
+{
+  cm::optional<std::string> ids;
+  if (this->Defer) {
+    ids = cmJoin(
+      cmMakeRange(this->Defer->Commands)
+        .filter([](DeferCommand const& dc) -> bool { return !dc.Id.empty(); })
+        .transform(
+          [](DeferCommand const& dc) -> std::string const& { return dc.Id; }),
+      ";");
+  }
+  return ids;
+}
+
+cm::optional<std::string> cmMakefile::DeferGetCall(std::string const& id) const
+{
+  cm::optional<std::string> call;
+  if (this->Defer) {
+    std::string tmp;
+    for (DeferCommand const& dc : this->Defer->Commands) {
+      if (dc.Id == id) {
+        tmp = dc.Command.Name.Original;
+        for (cmListFileArgument const& arg : dc.Command.Arguments) {
+          tmp = cmStrCat(tmp, ';', arg.Value);
+        }
+        break;
+      }
+    }
+    call = std::move(tmp);
+  }
+  return call;
+}
+
 MessageType cmMakefile::ExpandVariablesInStringNew(
 MessageType cmMakefile::ExpandVariablesInStringNew(
   std::string& errorstr, std::string& source, bool escapeQuotes,
   std::string& errorstr, std::string& source, bool escapeQuotes,
   bool noEscapes, bool atOnly, const char* filename, long line,
   bool noEscapes, bool atOnly, const char* filename, long line,
@@ -2997,7 +3154,12 @@ MessageType cmMakefile::ExpandVariablesInStringNew(
           switch (var.domain) {
           switch (var.domain) {
             case NORMAL:
             case NORMAL:
               if (filename && lookup == lineVar) {
               if (filename && lookup == lineVar) {
-                varresult = std::to_string(line);
+                cmListFileContext const& top = this->Backtrace.Top();
+                if (top.DeferId) {
+                  varresult = cmStrCat("DEFERRED:"_s, *top.DeferId);
+                } else {
+                  varresult = std::to_string(line);
+                }
               } else {
               } else {
                 value = this->GetDefinition(lookup);
                 value = this->GetDefinition(lookup);
               }
               }
@@ -3561,6 +3723,12 @@ void cmMakefile::AddTargetObject(std::string const& tgtName,
 void cmMakefile::EnableLanguage(std::vector<std::string> const& lang,
 void cmMakefile::EnableLanguage(std::vector<std::string> const& lang,
                                 bool optional)
                                 bool optional)
 {
 {
+  if (this->DeferRunning) {
+    this->IssueMessage(
+      MessageType::FATAL_ERROR,
+      "Languages may not be enabled during deferred execution.");
+    return;
+  }
   if (const char* def = this->GetGlobalGenerator()->GetCMakeCFGIntDir()) {
   if (const char* def = this->GetGlobalGenerator()->GetCMakeCFGIntDir()) {
     this->AddDefinition("CMAKE_CFG_INTDIR", def);
     this->AddDefinition("CMAKE_CFG_INTDIR", def);
   }
   }

+ 33 - 4
Source/cmMakefile.h

@@ -15,6 +15,7 @@
 #include <unordered_map>
 #include <unordered_map>
 #include <vector>
 #include <vector>
 
 
+#include <cm/optional>
 #include <cm/string_view>
 #include <cm/string_view>
 
 
 #include "cmsys/RegularExpression.hxx"
 #include "cmsys/RegularExpression.hxx"
@@ -695,7 +696,8 @@ public:
   /**
   /**
    * Print a command's invocation
    * Print a command's invocation
    */
    */
-  void PrintCommandTrace(const cmListFileFunction& lff) const;
+  void PrintCommandTrace(cmListFileFunction const& lff,
+                         cm::optional<std::string> const& deferId = {}) const;
 
 
   /**
   /**
    * Set a callback that is invoked whenever ExecuteCommand is called.
    * Set a callback that is invoked whenever ExecuteCommand is called.
@@ -706,8 +708,8 @@ public:
    * Execute a single CMake command.  Returns true if the command
    * Execute a single CMake command.  Returns true if the command
    * succeeded or false if it failed.
    * succeeded or false if it failed.
    */
    */
-  bool ExecuteCommand(const cmListFileFunction& lff,
-                      cmExecutionStatus& status);
+  bool ExecuteCommand(const cmListFileFunction& lff, cmExecutionStatus& status,
+                      cm::optional<std::string> deferId = {});
 
 
   //! Enable support for named language, if nil then all languages are
   //! Enable support for named language, if nil then all languages are
   /// enabled.
   /// enabled.
@@ -965,6 +967,12 @@ public:
   int GetRecursionDepth() const;
   int GetRecursionDepth() const;
   void SetRecursionDepth(int recursionDepth);
   void SetRecursionDepth(int recursionDepth);
 
 
+  std::string NewDeferId();
+  bool DeferCall(std::string id, std::string fileName, cmListFileFunction lff);
+  bool DeferCancelCall(std::string const& id);
+  cm::optional<std::string> DeferGetCallIds() const;
+  cm::optional<std::string> DeferGetCall(std::string const& id) const;
+
 protected:
 protected:
   // add link libraries and directories to the target
   // add link libraries and directories to the target
   void AddGlobalLinkInformation(cmTarget& target);
   void AddGlobalLinkInformation(cmTarget& target);
@@ -1026,10 +1034,25 @@ private:
   cmListFileBacktrace Backtrace;
   cmListFileBacktrace Backtrace;
   int RecursionDepth;
   int RecursionDepth;
 
 
+  struct DeferCommand
+  {
+    // Id is empty for an already-executed or cancelled operation.
+    std::string Id;
+    std::string FilePath;
+    cmListFileFunction Command;
+  };
+  struct DeferCommands
+  {
+    std::vector<DeferCommand> Commands;
+  };
+  std::unique_ptr<DeferCommands> Defer;
+  bool DeferRunning = false;
+
   void DoGenerate(cmLocalGenerator& lg);
   void DoGenerate(cmLocalGenerator& lg);
 
 
   void RunListFile(cmListFile const& listFile,
   void RunListFile(cmListFile const& listFile,
-                   const std::string& filenametoread);
+                   const std::string& filenametoread,
+                   DeferCommands* defer = nullptr);
 
 
   bool ParseDefineFlag(std::string const& definition, bool remove);
   bool ParseDefineFlag(std::string const& definition, bool remove);
 
 
@@ -1080,6 +1103,12 @@ private:
   class ListFileScope;
   class ListFileScope;
   friend class ListFileScope;
   friend class ListFileScope;
 
 
+  class DeferScope;
+  friend class DeferScope;
+
+  class DeferCallScope;
+  friend class DeferCallScope;
+
   class BuildsystemFileScope;
   class BuildsystemFileScope;
   friend class BuildsystemFileScope;
   friend class BuildsystemFileScope;
 
 

+ 15 - 0
Source/cmState.cxx

@@ -837,6 +837,21 @@ cmStateSnapshot cmState::CreateBuildsystemDirectorySnapshot(
   return snapshot;
   return snapshot;
 }
 }
 
 
+cmStateSnapshot cmState::CreateDeferCallSnapshot(
+  cmStateSnapshot const& originSnapshot, std::string const& fileName)
+{
+  cmStateDetail::PositionType pos =
+    this->SnapshotData.Push(originSnapshot.Position, *originSnapshot.Position);
+  pos->SnapshotType = cmStateEnums::DeferCallType;
+  pos->Keep = false;
+  pos->ExecutionListFile = this->ExecutionListFiles.Push(
+    originSnapshot.Position->ExecutionListFile, fileName);
+  assert(originSnapshot.Position->Vars.IsValid());
+  pos->BuildSystemDirectory->DirectoryEnd = pos;
+  pos->PolicyScope = originSnapshot.Position->Policies;
+  return { this, pos };
+}
+
 cmStateSnapshot cmState::CreateFunctionCallSnapshot(
 cmStateSnapshot cmState::CreateFunctionCallSnapshot(
   cmStateSnapshot const& originSnapshot, std::string const& fileName)
   cmStateSnapshot const& originSnapshot, std::string const& fileName)
 {
 {

+ 2 - 0
Source/cmState.h

@@ -55,6 +55,8 @@ public:
   cmStateSnapshot CreateBaseSnapshot();
   cmStateSnapshot CreateBaseSnapshot();
   cmStateSnapshot CreateBuildsystemDirectorySnapshot(
   cmStateSnapshot CreateBuildsystemDirectorySnapshot(
     cmStateSnapshot const& originSnapshot);
     cmStateSnapshot const& originSnapshot);
+  cmStateSnapshot CreateDeferCallSnapshot(
+    cmStateSnapshot const& originSnapshot, std::string const& fileName);
   cmStateSnapshot CreateFunctionCallSnapshot(
   cmStateSnapshot CreateFunctionCallSnapshot(
     cmStateSnapshot const& originSnapshot, std::string const& fileName);
     cmStateSnapshot const& originSnapshot, std::string const& fileName);
   cmStateSnapshot CreateMacroCallSnapshot(
   cmStateSnapshot CreateMacroCallSnapshot(

+ 1 - 0
Source/cmStateTypes.h

@@ -18,6 +18,7 @@ enum SnapshotType
 {
 {
   BaseType,
   BaseType,
   BuildsystemDirectoryType,
   BuildsystemDirectoryType,
+  DeferCallType,
   FunctionCallType,
   FunctionCallType,
   MacroCallType,
   MacroCallType,
   IncludeFileType,
   IncludeFileType,

+ 1 - 1
Source/cmake.cxx

@@ -983,7 +983,7 @@ void cmake::PrintTraceFormatVersion()
       Json::StreamWriterBuilder builder;
       Json::StreamWriterBuilder builder;
       builder["indentation"] = "";
       builder["indentation"] = "";
       version["major"] = 1;
       version["major"] = 1;
-      version["minor"] = 0;
+      version["minor"] = 1;
       val["version"] = version;
       val["version"] = version;
       msg = Json::writeString(builder, val);
       msg = Json::writeString(builder, val);
 #endif
 #endif

+ 1 - 1
Tests/RunCMake/CommandLine/trace-json-v1-check.py

@@ -56,7 +56,7 @@ with open(trace_file, 'r') as fp:
     assert sorted(vers.keys()) == ['version']
     assert sorted(vers.keys()) == ['version']
     assert sorted(vers['version'].keys()) == ['major', 'minor']
     assert sorted(vers['version'].keys()) == ['major', 'minor']
     assert vers['version']['major'] == 1
     assert vers['version']['major'] == 1
-    assert vers['version']['minor'] == 0
+    assert vers['version']['minor'] == 1
 
 
     for i in fp.readlines():
     for i in fp.readlines():
         line = json.loads(i)
         line = json.loads(i)

+ 50 - 0
Tests/RunCMake/cmake_language/RunCMakeTest.cmake

@@ -32,3 +32,53 @@ run_cmake(eval_message_fatal_error)
 run_cmake(eval_no_code)
 run_cmake(eval_no_code)
 run_cmake(eval_no_parameters)
 run_cmake(eval_no_parameters)
 run_cmake(eval_variable_outside_message)
 run_cmake(eval_variable_outside_message)
+run_cmake(defer_call)
+run_cmake(defer_call_add_subdirectory)
+run_cmake(defer_call_enable_language)
+run_cmake(defer_call_ids)
+foreach(command IN ITEMS
+    "function" "endfunction"
+    "macro" "endmacro"
+    "if" "elseif" "else" "endif"
+    "while" "endwhile"
+    "foreach" "endforeach"
+    "return"
+    )
+  message(STATUS "Running defer_call_invalid_command for ${command}...")
+  run_cmake_with_options(defer_call_invalid_command -Dcommand=${command})
+endforeach()
+run_cmake(defer_call_invalid_directory)
+run_cmake(defer_call_error)
+run_cmake(defer_call_missing_directory)
+run_cmake(defer_call_policy_PUSH)
+run_cmake(defer_call_syntax_error)
+run_cmake_with_options(defer_call_trace --trace-expand)
+run_cmake_with_options(defer_call_trace_json --trace --trace-format=json-v1)
+run_cmake(defer_cancel_call_unknown_argument)
+run_cmake(defer_cancel_call_invalid_directory)
+run_cmake(defer_cancel_call_id)
+run_cmake(defer_cancel_call_id_var)
+run_cmake(defer_directory_empty)
+run_cmake(defer_directory_missing)
+run_cmake(defer_directory_multiple)
+run_cmake(defer_id_empty)
+run_cmake(defer_id_missing)
+run_cmake(defer_id_multiple)
+run_cmake(defer_id_var_empty)
+run_cmake(defer_id_var_missing)
+run_cmake(defer_id_var_multiple)
+run_cmake(defer_get_call_ids_missing_var)
+run_cmake(defer_get_call_ids_too_many_args)
+run_cmake(defer_get_call_ids_invalid_directory)
+run_cmake(defer_get_call_ids_id)
+run_cmake(defer_get_call_ids_id_var)
+run_cmake(defer_get_call_missing_id)
+run_cmake(defer_get_call_missing_var)
+run_cmake(defer_get_call_too_many_args)
+run_cmake(defer_get_call_id_empty)
+run_cmake(defer_get_call_unknown_argument)
+run_cmake(defer_get_call_id)
+run_cmake(defer_get_call_id_var)
+run_cmake(defer_missing_arg)
+run_cmake(defer_missing_call)
+run_cmake(defer_unknown_option)

+ 15 - 0
Tests/RunCMake/cmake_language/defer_call-stderr.txt

@@ -0,0 +1,15 @@
+^CMake Deprecation Warning at defer_call/CMakeLists.txt:[0-9]+ \(cmake_policy\):
+  The OLD behavior for policy CMP0053 will be removed from a future version
+  of CMake.
+
+  The cmake-policies\(7\) manual explains that the OLD behaviors of all
+  policies are deprecated and that a policy should be set to OLD only under
+  specific short-term circumstances.  Projects should be ported to the NEW
+  behavior and not rely on setting a policy to OLD.
++
+CMake Warning at defer_call/CMakeLists.txt:3 \(message\):
+  Double-Deferred Warning In Subdirectory:
+
+   '[^']*/Tests/RunCMake/cmake_language/defer_call/CMakeLists.txt:DEFERRED:id3'
+Call Stack \(most recent call first\):
+  defer_call/CMakeLists.txt:DEFERRED$

+ 8 - 0
Tests/RunCMake/cmake_language/defer_call-stdout.txt

@@ -0,0 +1,8 @@
+-- Immediate Message In Subdirectory: ids='__0;__1'
+-- Deferred Message In Subdirectory: '[^']*/Tests/RunCMake/cmake_language/defer_call/CMakeLists.txt:DEFERRED:id1'
+-- Deferred Message In Included File: '[^']*/Tests/RunCMake/cmake_language/defer_call/include.cmake:1'
+-- Immediate Message: ids='__0;__1;__2;__3;__4'
+-- First Deferred Message
+-- Deferred Message From Subdirectory
+-- Deferred Message: ids='__4;__5'
+-- Final Deferred Message

+ 12 - 0
Tests/RunCMake/cmake_language/defer_call.cmake

@@ -0,0 +1,12 @@
+set(message_command "message")
+set(final_message "This should not be printed because variable evaluation is deferred too.")
+cmake_language(DEFER CALL ${message_command} STATUS "First Deferred Message")
+add_subdirectory(defer_call)
+cmake_language(DEFER CALL cmake_language DEFER CALL "${final_message_command}" STATUS "${final_message}")
+cmake_language(DEFER CALL cmake_language DEFER GET_CALL_IDS ids)
+cmake_language(DEFER CALL cmake_language EVAL CODE [[message(STATUS "Deferred Message: ids='${ids}'")]])
+cmake_language(DEFER GET_CALL_IDS ids)
+message(STATUS "Immediate Message: ids='${ids}'")
+set(final_message_command "message")
+set(final_message "Final Deferred Message")
+set(subdir_message "Deferred Message From Subdirectory")

+ 11 - 0
Tests/RunCMake/cmake_language/defer_call/CMakeLists.txt

@@ -0,0 +1,11 @@
+cmake_policy(SET CMP0053 OLD)
+cmake_language(DEFER ID id1 CALL message STATUS "Deferred Message In Subdirectory: '${CMAKE_CURRENT_LIST_FILE}:${CMAKE_CURRENT_LIST_LINE}'")
+cmake_language(DEFER ID id2 CALL
+  cmake_language DEFER ID id3 CALL
+  message WARNING "Double-Deferred Warning In Subdirectory:\n '${CMAKE_CURRENT_LIST_FILE}:${CMAKE_CURRENT_LIST_LINE}'")
+cmake_language(DEFER ID id4 CALL include "${CMAKE_CURRENT_LIST_DIR}/include.cmake")
+
+set(subdir_message "This should not be printed because variable evaluation is in deferred scope.")
+cmake_language(DEFER DIRECTORY .. CALL message STATUS "${subdir_message}")
+cmake_language(DEFER DIRECTORY .. GET_CALL_IDS ids)
+message(STATUS "Immediate Message In Subdirectory: ids='${ids}'")

+ 1 - 0
Tests/RunCMake/cmake_language/defer_call/include.cmake

@@ -0,0 +1 @@
+message(STATUS "Deferred Message In Included File: '${CMAKE_CURRENT_LIST_FILE}:${CMAKE_CURRENT_LIST_LINE}'")

+ 1 - 0
Tests/RunCMake/cmake_language/defer_call_add_subdirectory-result.txt

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

+ 9 - 0
Tests/RunCMake/cmake_language/defer_call_add_subdirectory-stderr.txt

@@ -0,0 +1,9 @@
+^CMake Error at defer_call_add_subdirectory.cmake:1 \(add_subdirectory\):
+  Subdirectories may not be created during deferred execution.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:DEFERRED
++
+CMake Error at defer_call_add_subdirectory.cmake:2 \(subdirs\):
+  Subdirectories may not be created during deferred execution.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:DEFERRED$

+ 2 - 0
Tests/RunCMake/cmake_language/defer_call_add_subdirectory.cmake

@@ -0,0 +1,2 @@
+cmake_language(DEFER CALL add_subdirectory defer_call_add_subdirectory)
+cmake_language(DEFER CALL subdirs defer_call_add_subdirectory)

+ 0 - 0
Tests/RunCMake/cmake_language/defer_call_add_subdirectory/CMakeLists.txt


+ 1 - 0
Tests/RunCMake/cmake_language/defer_call_enable_language-result.txt

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

+ 9 - 0
Tests/RunCMake/cmake_language/defer_call_enable_language-stderr.txt

@@ -0,0 +1,9 @@
+^CMake Error at defer_call_enable_language.cmake:1 \(enable_language\):
+  Languages may not be enabled during deferred execution.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:DEFERRED
++
+CMake Error at defer_call_enable_language.cmake:2 \(project\):
+  Languages may not be enabled during deferred execution.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:DEFERRED$

+ 2 - 0
Tests/RunCMake/cmake_language/defer_call_enable_language.cmake

@@ -0,0 +1,2 @@
+cmake_language(DEFER CALL enable_language C)
+cmake_language(DEFER CALL project foo C)

+ 1 - 0
Tests/RunCMake/cmake_language/defer_call_error-result.txt

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

+ 9 - 0
Tests/RunCMake/cmake_language/defer_call_error-stderr.txt

@@ -0,0 +1,9 @@
+^CMake Error at defer_call_error.cmake:2 \(message\):
+  Deferred Error
+Call Stack \(most recent call first\):
+  CMakeLists.txt:DEFERRED
++
+CMake Error at defer_call_error/CMakeLists.txt:2 \(message\):
+  Deferred Error from Subdirectory
+Call Stack \(most recent call first\):
+  CMakeLists.txt:DEFERRED$

+ 3 - 0
Tests/RunCMake/cmake_language/defer_call_error.cmake

@@ -0,0 +1,3 @@
+# Error message backtrace points here but call stack shows DEFERRED execution.
+cmake_language(DEFER CALL message SEND_ERROR "Deferred Error")
+add_subdirectory(defer_call_error)

+ 2 - 0
Tests/RunCMake/cmake_language/defer_call_error/CMakeLists.txt

@@ -0,0 +1,2 @@
+# Error message backtrace points here but call stack shows DEFERRED execution in parent.
+cmake_language(DEFER DIRECTORY .. CALL message SEND_ERROR "Deferred Error from Subdirectory")

+ 13 - 0
Tests/RunCMake/cmake_language/defer_call_ids-stdout.txt

@@ -0,0 +1,13 @@
+-- Immediate Message: ids='message0;getCallIds1;messageIds1;cancelCall;getCallIds2;messageIds2;toBeCancelled;message3'
+-- Immediate Message: message0='message;STATUS;First Deferred Message'
+-- Immediate Message: getCallIds1='cmake_language;DEFER;GET_CALL_IDS;ids'
+-- Immediate Message: messageIds1='cmake_language;EVAL;CODE;message\(STATUS "Deferred Message: ids='\${ids}'"\)'
+-- Immediate Message: cancelCall='cmake_language;DEFER;CANCEL_CALL;toBeCancelled'
+-- Immediate Message: getCallIds2='cmake_language;DEFER;GET_CALL_IDS;ids'
+-- Immediate Message: messageIds2='cmake_language;EVAL;CODE;message\(STATUS "Deferred Message: ids='\${ids}'"\)'
+-- Immediate Message: toBeCancelled='message;STATUS;Cancelled Message'
+-- Immediate Message: message3='message;STATUS;Final Deferred Message'
+-- First Deferred Message
+-- Deferred Message: ids='messageIds1;cancelCall;getCallIds2;messageIds2;toBeCancelled;message3'
+-- Deferred Message: ids='messageIds2;message3'
+-- Final Deferred Message

+ 14 - 0
Tests/RunCMake/cmake_language/defer_call_ids.cmake

@@ -0,0 +1,14 @@
+cmake_language(DEFER ID message0 CALL message STATUS "First Deferred Message")
+cmake_language(DEFER ID getCallIds1 CALL cmake_language DEFER GET_CALL_IDS ids)
+cmake_language(DEFER ID messageIds1 CALL cmake_language EVAL CODE [[message(STATUS "Deferred Message: ids='${ids}'")]])
+cmake_language(DEFER ID cancelCall CALL cmake_language DEFER CANCEL_CALL toBeCancelled)
+cmake_language(DEFER ID getCallIds2 CALL cmake_language DEFER GET_CALL_IDS ids)
+cmake_language(DEFER ID messageIds2 CALL cmake_language EVAL CODE [[message(STATUS "Deferred Message: ids='${ids}'")]])
+cmake_language(DEFER ID toBeCancelled CALL message STATUS "Cancelled Message")
+cmake_language(DEFER ID message3 CALL message STATUS "Final Deferred Message")
+cmake_language(DEFER GET_CALL_IDS ids)
+message(STATUS "Immediate Message: ids='${ids}'")
+foreach(id ${ids})
+  cmake_language(DEFER GET_CALL ${id} call)
+  message(STATUS "Immediate Message: ${id}='${call}'")
+endforeach()

+ 1 - 0
Tests/RunCMake/cmake_language/defer_call_invalid_command-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_call_invalid_command-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_call_invalid_command.cmake:1 \(cmake_language\):
+  cmake_language invalid command specified: [A-Za-z_]+
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_call_invalid_command.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER CALL ${command})

+ 1 - 0
Tests/RunCMake/cmake_language/defer_call_invalid_directory-result.txt

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

+ 9 - 0
Tests/RunCMake/cmake_language/defer_call_invalid_directory-stderr.txt

@@ -0,0 +1,9 @@
+^CMake Error at defer_call_invalid_directory.cmake:2 \(cmake_language\):
+  cmake_language DEFER CALL may not be scheduled in directory:
+
+    [^
+]*/Tests/RunCMake/cmake_language/defer_call_invalid_directory-build/defer_call_invalid_directory
+
+  at this time.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 2 - 0
Tests/RunCMake/cmake_language/defer_call_invalid_directory.cmake

@@ -0,0 +1,2 @@
+add_subdirectory(defer_call_invalid_directory)
+cmake_language(DEFER DIRECTORY defer_call_invalid_directory CALL message "Should not be allowed.")

+ 0 - 0
Tests/RunCMake/cmake_language/defer_call_invalid_directory/CMakeLists.txt


+ 1 - 0
Tests/RunCMake/cmake_language/defer_call_missing_directory-result.txt

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

+ 9 - 0
Tests/RunCMake/cmake_language/defer_call_missing_directory-stderr.txt

@@ -0,0 +1,9 @@
+^CMake Error at defer_call_missing_directory.cmake:1 \(cmake_language\):
+  cmake_language DEFER DIRECTORY:
+
+    [^
+]*/Tests/RunCMake/cmake_language/does_not_exist
+
+  is not known.  It may not have been processed yet.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_call_missing_directory.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER DIRECTORY does_not_exist CALL message "Should not be allowed.")

+ 1 - 0
Tests/RunCMake/cmake_language/defer_call_policy_PUSH-result.txt

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

+ 2 - 0
Tests/RunCMake/cmake_language/defer_call_policy_PUSH-stderr.txt

@@ -0,0 +1,2 @@
+^CMake Error at CMakeLists.txt:DEFERRED:
+  cmake_policy PUSH without matching POP$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_call_policy_PUSH.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER CALL cmake_policy PUSH)

+ 1 - 0
Tests/RunCMake/cmake_language/defer_call_syntax_error-result.txt

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

+ 13 - 0
Tests/RunCMake/cmake_language/defer_call_syntax_error-stderr.txt

@@ -0,0 +1,13 @@
+^CMake Error at defer_call_syntax_error.cmake:2 \(message\):
+  Syntax error in cmake code at
+
+    [^
+]*/Tests/RunCMake/cmake_language/defer_call_syntax_error.cmake:2
+
+  when parsing string
+
+    Deferred \\X Error
+
+  Invalid character escape '\\X'.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:DEFERRED$

+ 2 - 0
Tests/RunCMake/cmake_language/defer_call_syntax_error.cmake

@@ -0,0 +1,2 @@
+# Argument syntax error evaluated at deferred call site.
+cmake_language(DEFER CALL message "Deferred \X Error")

+ 8 - 0
Tests/RunCMake/cmake_language/defer_call_trace-stderr.txt

@@ -0,0 +1,8 @@
+[^
+]*/Tests/RunCMake/cmake_language/defer_call_trace.cmake\(2\):  cmake_language\(DEFER CALL message Deferred Message \)
+[^
+]*/Tests/RunCMake/cmake_language/defer_call_trace.cmake\(3\):  message\(Immediate Message \)
+Immediate Message
+[^
+]*/Tests/RunCMake/cmake_language/defer_call_trace.cmake\(2\):DEFERRED:__0:  message\(Deferred Message \)
+Deferred Message$

+ 3 - 0
Tests/RunCMake/cmake_language/defer_call_trace.cmake

@@ -0,0 +1,3 @@
+# The --trace and --trace-expand output point here for deferred call.
+cmake_language(DEFER CALL message "Deferred Message")
+message("Immediate Message")

+ 5 - 0
Tests/RunCMake/cmake_language/defer_call_trace_json-stderr.txt

@@ -0,0 +1,5 @@
+{"args":\["DEFER","CALL","message","Deferred Message"\],"cmd":"cmake_language","file":"[^"]*/Tests/RunCMake/cmake_language/defer_call_trace_json.cmake","frame":2,"line":2,"time":[0-9.]+}
+{"args":\["Immediate Message"\],"cmd":"message","file":"[^"]*/Tests/RunCMake/cmake_language/defer_call_trace_json.cmake","frame":2,"line":3,"time":[0-9.]+}
+Immediate Message
+{"args":\["Deferred Message"],"cmd":"message","defer":"__0","file":"[^"]*/Tests/RunCMake/cmake_language/defer_call_trace_json.cmake","frame":1,"line":2,"time":[0-9.]+}
+Deferred Message$

+ 3 - 0
Tests/RunCMake/cmake_language/defer_call_trace_json.cmake

@@ -0,0 +1,3 @@
+# The --trace and --trace-expand output point here for deferred call.
+cmake_language(DEFER CALL message "Deferred Message")
+message("Immediate Message")

+ 1 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_id-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_id-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_cancel_call_id.cmake:1 \(cmake_language\):
+  cmake_language DEFER CANCEL_CALL does not accept ID or ID_VAR.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_id.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER ID id CANCEL_CALL)

+ 1 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_id_var-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_id_var-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_cancel_call_id_var.cmake:1 \(cmake_language\):
+  cmake_language DEFER CANCEL_CALL does not accept ID or ID_VAR.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_id_var.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER ID_VAR id_var CANCEL_CALL)

+ 1 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_invalid_directory-result.txt

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

+ 9 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_invalid_directory-stderr.txt

@@ -0,0 +1,9 @@
+^CMake Error at defer_cancel_call_invalid_directory.cmake:2 \(cmake_language\):
+  cmake_language DEFER CANCEL_CALL may not update directory:
+
+    [^
+]*/Tests/RunCMake/cmake_language/defer_cancel_call_invalid_directory-build/defer_cancel_call_invalid_directory
+
+  at this time.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)

+ 2 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_invalid_directory.cmake

@@ -0,0 +1,2 @@
+add_subdirectory(defer_cancel_call_invalid_directory)
+cmake_language(DEFER DIRECTORY defer_cancel_call_invalid_directory CANCEL_CALL _)

+ 0 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_invalid_directory/CMakeLists.txt


+ 1 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_unknown_argument-result.txt

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

+ 6 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_unknown_argument-stderr.txt

@@ -0,0 +1,6 @@
+^CMake Error at defer_cancel_call_unknown_argument.cmake:1 \(cmake_language\):
+  cmake_language DEFER CANCEL_CALL unknown argument:
+
+    UNKNOWN
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_cancel_call_unknown_argument.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER CANCEL_CALL UNKNOWN)

+ 1 - 0
Tests/RunCMake/cmake_language/defer_directory_empty-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_directory_empty-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_directory_empty.cmake:1 \(cmake_language\):
+  cmake_language DEFER DIRECTORY may not be empty
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_directory_empty.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER DIRECTORY "")

+ 1 - 0
Tests/RunCMake/cmake_language/defer_directory_missing-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_directory_missing-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_directory_missing.cmake:1 \(cmake_language\):
+  cmake_language DEFER DIRECTORY missing value
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_directory_missing.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER DIRECTORY)

+ 1 - 0
Tests/RunCMake/cmake_language/defer_directory_multiple-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_directory_multiple-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_directory_multiple.cmake:1 \(cmake_language\):
+  cmake_language DEFER given multiple DIRECTORY arguments
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_directory_multiple.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER DIRECTORY . DIRECTORY x CALL message "Should not be allowed.")

+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_id-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_get_call_id-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_get_call_id.cmake:1 \(cmake_language\):
+  cmake_language DEFER GET_CALL does not accept ID or ID_VAR.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_id.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER ID id GET_CALL)

+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_id_empty-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_get_call_id_empty-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_get_call_id_empty.cmake:1 \(cmake_language\):
+  cmake_language DEFER GET_CALL id may not be empty
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_id_empty.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER GET_CALL "" var)

+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_id_var-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_get_call_id_var-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_get_call_id_var.cmake:1 \(cmake_language\):
+  cmake_language DEFER GET_CALL does not accept ID or ID_VAR.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_id_var.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER ID_VAR id_var GET_CALL)

+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_id-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_id-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_get_call_ids_id.cmake:1 \(cmake_language\):
+  cmake_language DEFER GET_CALL_IDS does not accept ID or ID_VAR.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_id.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER ID id GET_CALL_IDS)

+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_id_var-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_id_var-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_get_call_ids_id_var.cmake:1 \(cmake_language\):
+  cmake_language DEFER GET_CALL_IDS does not accept ID or ID_VAR.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_id_var.cmake

@@ -0,0 +1 @@
+cmake_language(DEFER ID_VAR id_var GET_CALL_IDS)

+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_invalid_directory-result.txt

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

+ 9 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_invalid_directory-stderr.txt

@@ -0,0 +1,9 @@
+^CMake Error at defer_get_call_ids_invalid_directory.cmake:2 \(cmake_language\):
+  cmake_language DEFER GET_CALL_IDS may not access directory:
+
+    [^
+]*/Tests/RunCMake/cmake_language/defer_get_call_ids_invalid_directory-build/defer_get_call_ids_invalid_directory
+
+  at this time.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

+ 2 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_invalid_directory.cmake

@@ -0,0 +1,2 @@
+add_subdirectory(defer_get_call_ids_invalid_directory)
+cmake_language(DEFER DIRECTORY defer_get_call_ids_invalid_directory GET_CALL_IDS var)

+ 0 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_invalid_directory/CMakeLists.txt


+ 1 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_missing_var-result.txt

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

+ 4 - 0
Tests/RunCMake/cmake_language/defer_get_call_ids_missing_var-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at defer_get_call_ids_missing_var.cmake:1 \(cmake_language\):
+  cmake_language DEFER GET_CALL_IDS missing output variable
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)$

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff