diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index e547356df4815e8144bedcf46ae9c5dd133d95c3..e23b07056669e2993b772882e677c5e964677393 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -246,6 +246,8 @@ set(SRCS
   cmGlobalGeneratorFactory.h
   cmGlobalUnixMakefileGenerator3.cxx
   cmGlobalUnixMakefileGenerator3.h
+  cmGlobVerificationManager.cxx
+  cmGlobVerificationManager.h
   cmGraphAdjacencyList.h
   cmGraphVizWriter.cxx
   cmGraphVizWriter.h
diff --git a/Source/cmGlobVerificationManager.cxx b/Source/cmGlobVerificationManager.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..e23b6eaeaceeb98c36722d730c9f0491ed595fe2
--- /dev/null
+++ b/Source/cmGlobVerificationManager.cxx
@@ -0,0 +1,172 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmGlobVerificationManager.h"
+
+#include "cmsys/FStream.hxx"
+#include <sstream>
+
+#include "cmGeneratedFileStream.h"
+#include "cmListFileCache.h"
+#include "cmSystemTools.h"
+#include "cmVersion.h"
+#include "cmake.h"
+
+bool cmGlobVerificationManager::SaveVerificationScript(const std::string& path)
+{
+  if (this->Cache.empty()) {
+    return true;
+  }
+
+  std::string scriptFile = path;
+  scriptFile += cmake::GetCMakeFilesDirectory();
+  std::string stampFile = scriptFile;
+  cmSystemTools::MakeDirectory(scriptFile);
+  scriptFile += "/VerifyGlobs.cmake";
+  stampFile += "/cmake.verify_globs";
+  cmGeneratedFileStream verifyScriptFile(scriptFile.c_str());
+  verifyScriptFile.SetCopyIfDifferent(true);
+  if (!verifyScriptFile) {
+    cmSystemTools::Error("Unable to open verification script file for save. ",
+                         scriptFile.c_str());
+    cmSystemTools::ReportLastSystemError("");
+    return false;
+  }
+
+  verifyScriptFile << std::boolalpha;
+  verifyScriptFile << "# CMAKE generated file: DO NOT EDIT!\n"
+                   << "# Generated by CMake Version "
+                   << cmVersion::GetMajorVersion() << "."
+                   << cmVersion::GetMinorVersion() << "\n";
+
+  for (auto const& i : this->Cache) {
+    CacheEntryKey k = std::get<0>(i);
+    CacheEntryValue v = std::get<1>(i);
+
+    if (!v.Initialized) {
+      continue;
+    }
+
+    verifyScriptFile << "\n";
+
+    for (auto const& bt : v.Backtraces) {
+      verifyScriptFile << "# " << std::get<0>(bt);
+      std::get<1>(bt).PrintTitle(verifyScriptFile);
+      verifyScriptFile << "\n";
+    }
+
+    k.PrintGlobCommand(verifyScriptFile, "NEW_GLOB");
+    verifyScriptFile << "\n";
+
+    verifyScriptFile << "set(OLD_GLOB\n";
+    for (const std::string& file : v.Files) {
+      verifyScriptFile << "  \"" << file << "\"\n";
+    }
+    verifyScriptFile << "  )\n";
+
+    verifyScriptFile << "if(NOT \"${NEW_GLOB}\" STREQUAL \"${OLD_GLOB}\")\n"
+                     << "  message(\"-- GLOB mismatch!\")\n"
+                     << "  file(TOUCH_NOCREATE \"" << stampFile << "\")\n"
+                     << "endif()\n";
+  }
+  verifyScriptFile.Close();
+
+  cmsys::ofstream verifyStampFile(stampFile.c_str());
+  if (!verifyStampFile) {
+    cmSystemTools::Error("Unable to open verification stamp file for write. ",
+                         stampFile.c_str());
+    return false;
+  }
+  verifyStampFile << "# This file is generated by CMake for checking of the "
+                     "VerifyGlobs.cmake file\n";
+  this->VerifyScript = scriptFile;
+  this->VerifyStamp = stampFile;
+  return true;
+}
+
+bool cmGlobVerificationManager::DoWriteVerifyTarget() const
+{
+  return !this->VerifyScript.empty() && !this->VerifyStamp.empty();
+}
+
+bool cmGlobVerificationManager::CacheEntryKey::operator<(
+  const CacheEntryKey& r) const
+{
+  if (this->Recurse < r.Recurse) {
+    return true;
+  }
+  if (this->Recurse > r.Recurse) {
+    return false;
+  }
+  if (this->ListDirectories < r.ListDirectories) {
+    return true;
+  }
+  if (this->ListDirectories > r.ListDirectories) {
+    return false;
+  }
+  if (this->FollowSymlinks < r.FollowSymlinks) {
+    return true;
+  }
+  if (this->FollowSymlinks > r.FollowSymlinks) {
+    return false;
+  }
+  if (this->Relative < r.Relative) {
+    return true;
+  }
+  if (this->Relative > r.Relative) {
+    return false;
+  }
+  if (this->Expression < r.Expression) {
+    return true;
+  }
+  if (this->Expression > r.Expression) {
+    return false;
+  }
+  return false;
+}
+
+void cmGlobVerificationManager::CacheEntryKey::PrintGlobCommand(
+  std::ostream& out, const std::string& cmdVar)
+{
+  out << "file(GLOB" << (this->Recurse ? "_RECURSE " : " ");
+  out << cmdVar << " ";
+  if (this->Recurse && this->FollowSymlinks) {
+    out << "FOLLOW_SYMLINKS ";
+  }
+  out << "LIST_DIRECTORIES " << this->ListDirectories << " ";
+  if (!this->Relative.empty()) {
+    out << "RELATIVE \"" << this->Relative << "\" ";
+  }
+  out << "\"" << this->Expression << "\")";
+}
+
+void cmGlobVerificationManager::AddCacheEntry(
+  const bool recurse, const bool listDirectories, const bool followSymlinks,
+  const std::string& relative, const std::string& expression,
+  const std::vector<std::string>& files, const std::string& variable,
+  const cmListFileBacktrace& backtrace)
+{
+  CacheEntryKey key = CacheEntryKey(recurse, listDirectories, followSymlinks,
+                                    relative, expression);
+  CacheEntryValue& value = this->Cache[key];
+  if (!value.Initialized) {
+    value.Files = files;
+    value.Initialized = true;
+    value.Backtraces.emplace_back(variable, backtrace);
+  } else if (value.Initialized && value.Files != files) {
+    std::ostringstream message;
+    message << std::boolalpha;
+    message << "The glob expression\n";
+    key.PrintGlobCommand(message, variable);
+    backtrace.PrintTitle(message);
+    message << "\nwas already present in the glob cache but the directory\n"
+               "contents have changed during the configuration run.\n";
+    message << "Matching glob expressions:";
+    for (auto const& bt : value.Backtraces) {
+      message << "\n  " << std::get<0>(bt);
+      std::get<1>(bt).PrintTitle(message);
+    }
+    cmSystemTools::Error(message.str().c_str());
+  } else {
+    value.Backtraces.emplace_back(variable, backtrace);
+  }
+}
diff --git a/Source/cmGlobVerificationManager.h b/Source/cmGlobVerificationManager.h
new file mode 100644
index 0000000000000000000000000000000000000000..4508602fedd2c6c822cce4045dd4b81ca8f1c01b
--- /dev/null
+++ b/Source/cmGlobVerificationManager.h
@@ -0,0 +1,89 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#ifndef cmGlobVerificationManager_h
+#define cmGlobVerificationManager_h
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include "cmListFileCache.h"
+
+#include <iosfwd>
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+/** \class cmGlobVerificationManager
+ * \brief Class for expressing build-time dependencies on glob expressions.
+ *
+ * Generates a CMake script which verifies glob outputs during prebuild.
+ *
+ */
+class cmGlobVerificationManager
+{
+public:
+  cmGlobVerificationManager() {}
+
+protected:
+  ///! Save verification script for given makefile.
+  ///! Saves to output <path>/<CMakeFilesDirectory>/VerifyGlobs.cmake
+  bool SaveVerificationScript(const std::string& path);
+
+  ///! Add an entry into the glob cache
+  void AddCacheEntry(bool recurse, bool listDirectories, bool followSymlinks,
+                     const std::string& relative,
+                     const std::string& expression,
+                     const std::vector<std::string>& files,
+                     const std::string& variable,
+                     const cmListFileBacktrace& bt);
+
+  ///! Check targets should be written in generated build system.
+  bool DoWriteVerifyTarget() const;
+
+  ///! Get the paths to the generated script and stamp files
+  std::string const& GetVerifyScript() const { return this->VerifyScript; }
+  std::string const& GetVerifyStamp() const { return this->VerifyStamp; }
+
+private:
+  struct CacheEntryKey
+  {
+    const bool Recurse;
+    const bool ListDirectories;
+    const bool FollowSymlinks;
+    const std::string Relative;
+    const std::string Expression;
+    CacheEntryKey(const bool rec, const bool l, const bool s,
+                  const std::string& rel, const std::string& e)
+      : Recurse(rec)
+      , ListDirectories(l)
+      , FollowSymlinks(s)
+      , Relative(rel)
+      , Expression(e)
+    {
+    }
+    bool operator<(const CacheEntryKey& r) const;
+    void PrintGlobCommand(std::ostream& out, const std::string& cmdVar);
+  };
+
+  struct CacheEntryValue
+  {
+    bool Initialized;
+    std::vector<std::string> Files;
+    std::vector<std::pair<std::string, cmListFileBacktrace>> Backtraces;
+    CacheEntryValue()
+      : Initialized(false)
+    {
+    }
+  };
+
+  typedef std::map<CacheEntryKey, CacheEntryValue> CacheEntryMap;
+  CacheEntryMap Cache;
+  std::string VerifyScript;
+  std::string VerifyStamp;
+
+  // Only cmState should be able to add cache values.
+  // cmGlobVerificationManager should never be used directly.
+  friend class cmState; // allow access to add cache values
+};
+
+#endif
diff --git a/Source/cmState.cxx b/Source/cmState.cxx
index bb891b579273a4c5d9276ed916a1a10e89ec2295..a93fb11857d9cc4d1fa8f1c694e7b724a3b72361 100644
--- a/Source/cmState.cxx
+++ b/Source/cmState.cxx
@@ -13,6 +13,7 @@
 #include "cmCommand.h"
 #include "cmDefinitions.h"
 #include "cmDisallowedCommand.h"
+#include "cmGlobVerificationManager.h"
 #include "cmListFileCache.h"
 #include "cmStatePrivate.h"
 #include "cmStateSnapshot.h"
@@ -31,11 +32,13 @@ cmState::cmState()
   , MSYSShell(false)
 {
   this->CacheManager = new cmCacheManager;
+  this->GlobVerificationManager = new cmGlobVerificationManager;
 }
 
 cmState::~cmState()
 {
   delete this->CacheManager;
+  delete this->GlobVerificationManager;
   cmDeleteAll(this->BuiltinCommands);
   cmDeleteAll(this->ScriptedCommands);
 }
@@ -207,6 +210,39 @@ void cmState::AddCacheEntry(const std::string& key, const char* value,
   this->CacheManager->AddCacheEntry(key, value, helpString, type);
 }
 
+bool cmState::DoWriteGlobVerifyTarget() const
+{
+  return this->GlobVerificationManager->DoWriteVerifyTarget();
+}
+
+std::string const& cmState::GetGlobVerifyScript() const
+{
+  return this->GlobVerificationManager->GetVerifyScript();
+}
+
+std::string const& cmState::GetGlobVerifyStamp() const
+{
+  return this->GlobVerificationManager->GetVerifyStamp();
+}
+
+bool cmState::SaveVerificationScript(const std::string& path)
+{
+  return this->GlobVerificationManager->SaveVerificationScript(path);
+}
+
+void cmState::AddGlobCacheEntry(bool recurse, bool listDirectories,
+                                bool followSymlinks,
+                                const std::string& relative,
+                                const std::string& expression,
+                                const std::vector<std::string>& files,
+                                const std::string& variable,
+                                cmListFileBacktrace const& backtrace)
+{
+  this->GlobVerificationManager->AddCacheEntry(
+    recurse, listDirectories, followSymlinks, relative, expression, files,
+    variable, backtrace);
+}
+
 void cmState::RemoveCacheEntry(std::string const& key)
 {
   this->CacheManager->RemoveCacheEntry(key);
diff --git a/Source/cmState.h b/Source/cmState.h
index 6cbf82d5c2bdec88568a99bacc358749eb2e12b9..4c6fc69943124a30b0123428589e5a8f4c731b16 100644
--- a/Source/cmState.h
+++ b/Source/cmState.h
@@ -12,6 +12,7 @@
 
 #include "cmDefinitions.h"
 #include "cmLinkedTree.h"
+#include "cmListFileCache.h"
 #include "cmPolicies.h"
 #include "cmProperty.h"
 #include "cmPropertyDefinitionMap.h"
@@ -21,6 +22,7 @@
 
 class cmCacheManager;
 class cmCommand;
+class cmGlobVerificationManager;
 class cmPropertyDefinition;
 class cmStateSnapshot;
 class cmMessenger;
@@ -165,12 +167,24 @@ private:
                      const char* helpString,
                      cmStateEnums::CacheEntryType type);
 
+  bool DoWriteGlobVerifyTarget() const;
+  std::string const& GetGlobVerifyScript() const;
+  std::string const& GetGlobVerifyStamp() const;
+  bool SaveVerificationScript(const std::string& path);
+  void AddGlobCacheEntry(bool recurse, bool listDirectories,
+                         bool followSymlinks, const std::string& relative,
+                         const std::string& expression,
+                         const std::vector<std::string>& files,
+                         const std::string& variable,
+                         cmListFileBacktrace const& bt);
+
   std::map<cmProperty::ScopeType, cmPropertyDefinitionMap> PropertyDefinitions;
   std::vector<std::string> EnabledLanguages;
   std::map<std::string, cmCommand*> BuiltinCommands;
   std::map<std::string, cmCommand*> ScriptedCommands;
   cmPropertyMap GlobalProperties;
   cmCacheManager* CacheManager;
+  cmGlobVerificationManager* GlobVerificationManager;
 
   cmLinkedTree<cmStateDetail::BuildsystemDirectoryStateType>
     BuildsystemDirectory;
diff --git a/Source/cmake.cxx b/Source/cmake.cxx
index 323bcf6ca6b11b63f86221f836c32d9c8045c760..41a3a2203f340dd151a831e2490e09a5a5fa7f77 100644
--- a/Source/cmake.cxx
+++ b/Source/cmake.cxx
@@ -1434,6 +1434,7 @@ int cmake::ActualConfigure()
 
   // only save the cache if there were no fatal errors
   if (this->GetWorkingMode() == NORMAL_MODE) {
+    this->State->SaveVerificationScript(this->GetHomeOutputDirectory());
     this->SaveCache(this->GetHomeOutputDirectory());
   }
   if (cmSystemTools::GetErrorOccuredFlag()) {
@@ -1649,6 +1650,33 @@ void cmake::AddCacheEntry(const std::string& key, const char* value,
   this->UnwatchUnusedCli(key);
 }
 
+bool cmake::DoWriteGlobVerifyTarget() const
+{
+  return this->State->DoWriteGlobVerifyTarget();
+}
+
+std::string const& cmake::GetGlobVerifyScript() const
+{
+  return this->State->GetGlobVerifyScript();
+}
+
+std::string const& cmake::GetGlobVerifyStamp() const
+{
+  return this->State->GetGlobVerifyStamp();
+}
+
+void cmake::AddGlobCacheEntry(bool recurse, bool listDirectories,
+                              bool followSymlinks, const std::string& relative,
+                              const std::string& expression,
+                              const std::vector<std::string>& files,
+                              const std::string& variable,
+                              cmListFileBacktrace const& backtrace)
+{
+  this->State->AddGlobCacheEntry(recurse, listDirectories, followSymlinks,
+                                 relative, expression, files, variable,
+                                 backtrace);
+}
+
 std::string cmake::StripExtension(const std::string& file) const
 {
   auto dotpos = file.rfind('.');
diff --git a/Source/cmake.h b/Source/cmake.h
index 1ac549b9092c1a081e698f82f4880b75fe6a77dd..5e4930d1b30fd75a6a3d71c21e25de75c40fcfd0 100644
--- a/Source/cmake.h
+++ b/Source/cmake.h
@@ -255,6 +255,16 @@ public:
   void AddCacheEntry(const std::string& key, const char* value,
                      const char* helpString, int type);
 
+  bool DoWriteGlobVerifyTarget() const;
+  std::string const& GetGlobVerifyScript() const;
+  std::string const& GetGlobVerifyStamp() const;
+  void AddGlobCacheEntry(bool recurse, bool listDirectories,
+                         bool followSymlinks, const std::string& relative,
+                         const std::string& expression,
+                         const std::vector<std::string>& files,
+                         const std::string& variable,
+                         cmListFileBacktrace const& bt);
+
   /**
    * Get the system information and write it to the file specified
    */
diff --git a/bootstrap b/bootstrap
index 3d5b0d0e7001ca0531271c7743e6406e538b8c06..3bcab6024c1dfc4483d76bbb9b64adf4fe1cde1c 100755
--- a/bootstrap
+++ b/bootstrap
@@ -332,6 +332,7 @@ CMAKE_CXX_SOURCES="\
   cmGlobalCommonGenerator \
   cmGlobalGenerator \
   cmGlobalUnixMakefileGenerator3 \
+  cmGlobVerificationManager \
   cmHexFileConverter \
   cmIfCommand \
   cmIncludeCommand \