Merge topic 'AddCacheEntry-suppress-raw-pointer-usage'

4fc322bab4 AddCacheEntry: Suppress raw pointer usage

Acked-by: Kitware Robot <kwrobot@kitware.com>
Acked-by: buildbot <buildbot@kitware.com>
Merge-request: !8520
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9ec6267..d559c08 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -131,21 +131,21 @@
   endif()
 endif()
 
-# Check whether to build cppdap.
+# Check whether to build support for the debugger mode.
 if(NOT CMake_TEST_EXTERNAL_CMAKE)
-  if(NOT DEFINED CMake_ENABLE_CPPDAP)
-    # cppdap does not compile everywhere.
+  if(NOT DEFINED CMake_ENABLE_DEBUGGER)
+    # The debugger uses cppdap, which does not compile everywhere.
     if(CMAKE_SYSTEM_NAME MATCHES "Windows|Darwin|Linux|BSD|DragonFly|CYGWIN|MSYS"
         AND NOT (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 19.16)
         AND NOT (CMAKE_CXX_COMPILER_ID STREQUAL "XLClang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.1)
         )
-      set(CMake_ENABLE_CPPDAP 1)
+      set(CMake_ENABLE_DEBUGGER 1)
     else()
-      set(CMake_ENABLE_CPPDAP 0)
+      set(CMake_ENABLE_DEBUGGER 0)
     endif()
   endif()
 else()
-  set(CMake_ENABLE_CPPDAP 0)
+  set(CMake_ENABLE_DEBUGGER 0)
 endif()
 
 #-----------------------------------------------------------------------
@@ -186,7 +186,7 @@
 
   # Optionally use system utility libraries.
   option(CMAKE_USE_SYSTEM_LIBARCHIVE "Use system-installed libarchive" "${CMAKE_USE_SYSTEM_LIBRARY_LIBARCHIVE}")
-  if(CMake_ENABLE_CPPDAP)
+  if(CMake_ENABLE_DEBUGGER)
     option(CMAKE_USE_SYSTEM_CPPDAP "Use system-installed cppdap" "${CMAKE_USE_SYSTEM_LIBRARY_CPPDAP}")
   endif()
   option(CMAKE_USE_SYSTEM_CURL "Use system-installed curl" "${CMAKE_USE_SYSTEM_LIBRARY_CURL}")
diff --git a/Help/guide/importing-exporting/index.rst b/Help/guide/importing-exporting/index.rst
index 51a09c0..b1812c1 100644
--- a/Help/guide/importing-exporting/index.rst
+++ b/Help/guide/importing-exporting/index.rst
@@ -285,9 +285,9 @@
   :end-before: # include CMakePackageConfigHelpers macro
 
 This command generates the ``MathFunctionsTargets.cmake`` file and arranges
-to install it to ``lib/cmake``. The file contains code suitable for
-use by downstreams to import all targets listed in the install command from
-the installation tree.
+to install it to ``${CMAKE_INSTALL_LIBDIR}/cmake/MathFunctions``. The file
+contains code suitable for use by downstreams to import all targets listed in
+the install command from the installation tree.
 
 The ``NAMESPACE`` option will prepend ``MathFunctions::`` to  the target names
 as they are written to the export file. This convention of double-colons
@@ -317,7 +317,8 @@
 .. code-block:: cmake
   :linenos:
 
-   include(${INSTALL_PREFIX}/lib/cmake/MathFunctionTargets.cmake)
+   include(GNUInstallDirs)
+   include(${INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/cmake/MathFunctions/MathFunctionTargets.cmake)
    add_executable(myexe src1.c src2.c )
    target_link_libraries(myexe PRIVATE MathFunctions::MathFunctions)
 
diff --git a/Help/manual/cmake.1.rst b/Help/manual/cmake.1.rst
index 1ea7626..b5848f7 100644
--- a/Help/manual/cmake.1.rst
+++ b/Help/manual/cmake.1.rst
@@ -517,6 +517,53 @@
  If ``<type>`` is omitted, ``configure`` is assumed.  The current working
  directory must contain CMake preset files.
 
+.. option:: --debugger
+
+  Enables interactive debugging of the CMake language. CMake exposes a debugging
+  interface on the pipe named by :option:`--debugger-pipe <cmake --debugger-pipe>`
+  that conforms to the `Debug Adapter Protocol`_ specification with the following
+  modifications.
+
+  The ``initialize`` response includes an additional field named ``cmakeVersion``
+  which specifies the version of CMake being debugged.
+
+  .. code-block:: json
+    :caption: Debugger initialize response
+
+    {
+      "cmakeVersion": {
+        "major": 3,
+        "minor": 27,
+        "patch": 0,
+        "full": "3.27.0"
+      }
+    }
+
+  The members are:
+
+  ``major``
+    An integer specifying the major version number.
+
+  ``minor``
+    An integer specifying the minor version number.
+
+  ``patch``
+    An integer specifying the patch version number.
+
+  ``full``
+    A string specifying the full CMake version.
+
+.. _`Debug Adapter Protocol`: https://microsoft.github.io/debug-adapter-protocol/
+
+.. option:: --debugger-pipe <pipe name>, --debugger-pipe=<pipe name>
+
+  Name of the pipe (on Windows) or domain socket (on Unix) to use for
+  debugger communication.
+
+.. option:: --debugger-dap-log <log path>, --debugger-dap-log=<log path>
+
+  Logs all debugger communication to the specified file.
+
 .. _`Build Tool Mode`:
 
 Build a Project
@@ -809,6 +856,12 @@
 
     ``true`` if TLS support is enabled and ``false`` otherwise.
 
+  ``debugger``
+    .. versionadded:: 3.27
+
+    ``true`` if the :option:`--debugger <cmake --debugger>` mode
+    is supported and ``false`` otherwise.
+
 .. option:: cat [--] <files>...
 
   .. versionadded:: 3.18
diff --git a/Help/release/dev/cmake-debugger.rst b/Help/release/dev/cmake-debugger.rst
new file mode 100644
index 0000000..bfc4f6c
--- /dev/null
+++ b/Help/release/dev/cmake-debugger.rst
@@ -0,0 +1,5 @@
+cmake-debugger
+--------------
+
+* :manual:`cmake(1)` now supports interactive debugging of the CMake language.
+  See the :option:`--debugger <cmake --debugger>` option.
diff --git a/Help/release/dev/cmake-verbose-print-build-tool-command.rst b/Help/release/dev/cmake-verbose-print-build-tool-command.rst
new file mode 100644
index 0000000..4f13231
--- /dev/null
+++ b/Help/release/dev/cmake-verbose-print-build-tool-command.rst
@@ -0,0 +1,5 @@
+cmake-verbose-print-build-tool-command
+--------------------------------------
+
+* ``cmake --build $dir --verbose`` will now print the working directory and
+  command line used to perform the build.
diff --git a/Modules/CMakeDetermineCompilerABI.cmake b/Modules/CMakeDetermineCompilerABI.cmake
index d665cd1..13bfeec 100644
--- a/Modules/CMakeDetermineCompilerABI.cmake
+++ b/Modules/CMakeDetermineCompilerABI.cmake
@@ -42,7 +42,7 @@
     __TestCompiler_setTryCompileTargetType()
 
     # Avoid failing ABI detection on warnings.
-    string(REGEX REPLACE "(^| )-Werror([= ][^ ]*)?( |$)" " " CMAKE_${lang}_FLAGS "${CMAKE_${lang}_FLAGS}")
+    string(REGEX REPLACE "(^| )-Werror([= ][^-][^ ]*)?( |$)" " " CMAKE_${lang}_FLAGS "${CMAKE_${lang}_FLAGS}")
 
     # Save the current LC_ALL, LC_MESSAGES, and LANG environment variables
     # and set them to "C" that way GCC's "search starts here" text is in
diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index 2354f3d..bcaf890 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -762,6 +762,38 @@
     ZLIB::ZLIB
   )
 
+if(CMake_ENABLE_DEBUGGER)
+  target_sources(
+    CMakeLib
+    PRIVATE
+      cmDebuggerAdapter.cxx
+      cmDebuggerAdapter.h
+      cmDebuggerBreakpointManager.cxx
+      cmDebuggerBreakpointManager.h
+      cmDebuggerExceptionManager.cxx
+      cmDebuggerExceptionManager.h
+      cmDebuggerPipeConnection.cxx
+      cmDebuggerPipeConnection.h
+      cmDebuggerProtocol.cxx
+      cmDebuggerProtocol.h
+      cmDebuggerSourceBreakpoint.cxx
+      cmDebuggerSourceBreakpoint.h
+      cmDebuggerStackFrame.cxx
+      cmDebuggerStackFrame.h
+      cmDebuggerThread.cxx
+      cmDebuggerThread.h
+      cmDebuggerThreadManager.cxx
+      cmDebuggerThreadManager.h
+      cmDebuggerVariables.cxx
+      cmDebuggerVariables.h
+      cmDebuggerVariablesHelper.cxx
+      cmDebuggerVariablesHelper.h
+      cmDebuggerVariablesManager.cxx
+      cmDebuggerVariablesManager.h
+    )
+  target_link_libraries(CMakeLib PUBLIC cppdap::cppdap)
+endif()
+
 # Check if we can build the Mach-O parser.
 if(CMake_USE_MACH_PARSER)
   target_sources(
diff --git a/Source/CMakeVersion.cmake b/Source/CMakeVersion.cmake
index 95f4095..00d3236 100644
--- a/Source/CMakeVersion.cmake
+++ b/Source/CMakeVersion.cmake
@@ -1,7 +1,7 @@
 # CMake version number components.
 set(CMake_VERSION_MAJOR 3)
 set(CMake_VERSION_MINOR 26)
-set(CMake_VERSION_PATCH 20230530)
+set(CMake_VERSION_PATCH 20230531)
 #set(CMake_VERSION_RC 0)
 set(CMake_VERSION_IS_DIRTY 0)
 
diff --git a/Source/CTest/cmCTestBuildAndTestHandler.cxx b/Source/CTest/cmCTestBuildAndTestHandler.cxx
index cece98e..5feb953 100644
--- a/Source/CTest/cmCTestBuildAndTestHandler.cxx
+++ b/Source/CTest/cmCTestBuildAndTestHandler.cxx
@@ -246,7 +246,6 @@
         return 1;
       }
     }
-    std::string output;
     const char* config = nullptr;
     if (!this->CTest->GetConfigType().empty()) {
       config = this->CTest->GetConfigType().c_str();
@@ -259,9 +258,8 @@
                                 PackageResolveMode::Disable);
     int retVal = cm.GetGlobalGenerator()->Build(
       cmake::NO_BUILD_PARALLEL_LEVEL, this->SourceDir, this->BinaryDir,
-      this->BuildProject, { tar }, output, this->BuildMakeProgram, config,
+      this->BuildProject, { tar }, out, this->BuildMakeProgram, config,
       buildOptions, false, remainingTime);
-    out << output;
     // if the build failed then return
     if (retVal) {
       if (outstring) {
diff --git a/Source/Modules/CMakeBuildUtilities.cmake b/Source/Modules/CMakeBuildUtilities.cmake
index 7d1e7da..c891fe9 100644
--- a/Source/Modules/CMakeBuildUtilities.cmake
+++ b/Source/Modules/CMakeBuildUtilities.cmake
@@ -379,7 +379,7 @@
 
 #---------------------------------------------------------------------
 # Build cppdap library.
-if(CMake_ENABLE_CPPDAP)
+if(CMake_ENABLE_DEBUGGER)
   if(CMAKE_USE_SYSTEM_CPPDAP)
     find_package(cppdap CONFIG)
     if(NOT cppdap_FOUND)
diff --git a/Source/cmConfigure.cmake.h.in b/Source/cmConfigure.cmake.h.in
index 3f19a11..de74716 100644
--- a/Source/cmConfigure.cmake.h.in
+++ b/Source/cmConfigure.cmake.h.in
@@ -20,6 +20,7 @@
 
 #cmakedefine HAVE_ENVIRON_NOT_REQUIRE_PROTOTYPE
 #cmakedefine HAVE_UNSETENV
+#cmakedefine CMake_ENABLE_DEBUGGER
 #cmakedefine CMake_USE_MACH_PARSER
 #cmakedefine CMake_USE_XCOFF_PARSER
 #cmakedefine CMAKE_USE_WMAKE
diff --git a/Source/cmDebuggerAdapter.cxx b/Source/cmDebuggerAdapter.cxx
new file mode 100644
index 0000000..d03f79d
--- /dev/null
+++ b/Source/cmDebuggerAdapter.cxx
@@ -0,0 +1,462 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include "cmDebuggerAdapter.h"
+
+#include <algorithm>
+#include <climits>
+#include <condition_variable>
+#include <cstdint>
+#include <functional>
+#include <iostream>
+#include <stdexcept>
+#include <utility>
+
+#include <cm/memory>
+#include <cm/optional>
+
+#include <cm3p/cppdap/io.h> // IWYU pragma: keep
+#include <cm3p/cppdap/protocol.h>
+#include <cm3p/cppdap/session.h>
+
+#include "cmDebuggerBreakpointManager.h"
+#include "cmDebuggerExceptionManager.h"
+#include "cmDebuggerProtocol.h"
+#include "cmDebuggerSourceBreakpoint.h" // IWYU pragma: keep
+#include "cmDebuggerStackFrame.h"
+#include "cmDebuggerThread.h"
+#include "cmDebuggerThreadManager.h"
+#include "cmListFileCache.h"
+#include "cmMakefile.h"
+#include "cmValue.h"
+#include "cmVersionConfig.h"
+#include <cmcppdap/include/dap/optional.h>
+#include <cmcppdap/include/dap/types.h>
+
+namespace cmDebugger {
+
+// Event provides a basic wait and signal synchronization primitive.
+class SyncEvent
+{
+public:
+  // Wait() blocks until the event is fired.
+  void Wait()
+  {
+    std::unique_lock<std::mutex> lock(Mutex);
+    Cv.wait(lock, [&] { return Fired; });
+  }
+
+  // Fire() sets signals the event, and unblocks any calls to Wait().
+  void Fire()
+  {
+    std::unique_lock<std::mutex> lock(Mutex);
+    Fired = true;
+    Cv.notify_all();
+  }
+
+private:
+  std::mutex Mutex;
+  std::condition_variable Cv;
+  bool Fired = false;
+};
+
+class Semaphore
+{
+public:
+  Semaphore(int count_ = 0)
+    : Count(count_)
+  {
+  }
+
+  inline void Notify()
+  {
+    std::unique_lock<std::mutex> lock(Mutex);
+    Count++;
+    // notify the waiting thread
+    Cv.notify_one();
+  }
+
+  inline void Wait()
+  {
+    std::unique_lock<std::mutex> lock(Mutex);
+    while (Count == 0) {
+      // wait on the mutex until notify is called
+      Cv.wait(lock);
+    }
+    Count--;
+  }
+
+private:
+  std::mutex Mutex;
+  std::condition_variable Cv;
+  int Count;
+};
+
+cmDebuggerAdapter::cmDebuggerAdapter(
+  std::shared_ptr<cmDebuggerConnection> connection,
+  std::string const& dapLogPath)
+  : cmDebuggerAdapter(std::move(connection),
+                      dapLogPath.empty()
+                        ? cm::nullopt
+                        : cm::optional<std::shared_ptr<dap::Writer>>(
+                            dap::file(dapLogPath.c_str())))
+{
+}
+
+cmDebuggerAdapter::cmDebuggerAdapter(
+  std::shared_ptr<cmDebuggerConnection> connection,
+  cm::optional<std::shared_ptr<dap::Writer>> logger)
+  : Connection(std::move(connection))
+  , SessionActive(true)
+  , DisconnectEvent(cm::make_unique<SyncEvent>())
+  , ConfigurationDoneEvent(cm::make_unique<SyncEvent>())
+  , ContinueSem(cm::make_unique<Semaphore>())
+  , ThreadManager(cm::make_unique<cmDebuggerThreadManager>())
+{
+  if (logger.has_value()) {
+    SessionLog = std::move(logger.value());
+  }
+  ClearStepRequests();
+
+  Session = dap::Session::create();
+  BreakpointManager =
+    cm::make_unique<cmDebuggerBreakpointManager>(Session.get());
+  ExceptionManager =
+    cm::make_unique<cmDebuggerExceptionManager>(Session.get());
+
+  // Handle errors reported by the Session. These errors include protocol
+  // parsing errors and receiving messages with no handler.
+  Session->onError([this](const char* msg) {
+    if (SessionLog) {
+      dap::writef(SessionLog, "dap::Session error: %s\n", msg);
+    }
+
+    std::cout << "[CMake Debugger] DAP session error: " << msg << std::endl;
+
+    BreakpointManager->ClearAll();
+    ExceptionManager->ClearAll();
+    ClearStepRequests();
+    ContinueSem->Notify();
+    DisconnectEvent->Fire();
+    SessionActive.store(false);
+  });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize
+  Session->registerHandler([this](const dap::CMakeInitializeRequest& req) {
+    SupportsVariableType = req.supportsVariableType.value(false);
+    dap::CMakeInitializeResponse response;
+    response.supportsConfigurationDoneRequest = true;
+    response.cmakeVersion.major = CMake_VERSION_MAJOR;
+    response.cmakeVersion.minor = CMake_VERSION_MINOR;
+    response.cmakeVersion.patch = CMake_VERSION_PATCH;
+    response.cmakeVersion.full = CMake_VERSION;
+    ExceptionManager->HandleInitializeRequest(response);
+    return response;
+  });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Events_Initialized
+  Session->registerSentHandler(
+    [&](const dap::ResponseOrError<dap::CMakeInitializeResponse>&) {
+      Session->send(dap::InitializedEvent());
+    });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Threads
+  Session->registerHandler([this](const dap::ThreadsRequest& req) {
+    (void)req;
+    std::unique_lock<std::mutex> lock(Mutex);
+    dap::ThreadsResponse response;
+    dap::Thread thread;
+    thread.id = DefaultThread->GetId();
+    thread.name = DefaultThread->GetName();
+    response.threads.push_back(thread);
+    return response;
+  });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StackTrace
+  Session->registerHandler([this](const dap::StackTraceRequest& request)
+                             -> dap::ResponseOrError<dap::StackTraceResponse> {
+    std::unique_lock<std::mutex> lock(Mutex);
+
+    cm::optional<dap::StackTraceResponse> response =
+      ThreadManager->GetThreadStackTraceResponse(request.threadId);
+    if (response.has_value()) {
+      return response.value();
+    }
+
+    return dap::Error("Unknown threadId '%d'", int(request.threadId));
+  });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Scopes
+  Session->registerHandler([this](const dap::ScopesRequest& request)
+                             -> dap::ResponseOrError<dap::ScopesResponse> {
+    std::unique_lock<std::mutex> lock(Mutex);
+    return DefaultThread->GetScopesResponse(request.frameId,
+                                            SupportsVariableType);
+  });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Variables
+  Session->registerHandler([this](const dap::VariablesRequest& request)
+                             -> dap::ResponseOrError<dap::VariablesResponse> {
+    return DefaultThread->GetVariablesResponse(request);
+  });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Pause
+  Session->registerHandler([this](const dap::PauseRequest& req) {
+    (void)req;
+    PauseRequest.store(true);
+    return dap::PauseResponse();
+  });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Continue
+  Session->registerHandler([this](const dap::ContinueRequest& req) {
+    (void)req;
+    ContinueSem->Notify();
+    return dap::ContinueResponse();
+  });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Next
+  Session->registerHandler([this](const dap::NextRequest& req) {
+    (void)req;
+    NextStepFrom.store(DefaultThread->GetStackFrameSize());
+    ContinueSem->Notify();
+    return dap::NextResponse();
+  });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepIn
+  Session->registerHandler([this](const dap::StepInRequest& req) {
+    (void)req;
+    // This would stop after stepped in, single line stepped or stepped out.
+    StepInRequest.store(true);
+    ContinueSem->Notify();
+    return dap::StepInResponse();
+  });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepOut
+  Session->registerHandler([this](const dap::StepOutRequest& req) {
+    (void)req;
+    StepOutDepth.store(DefaultThread->GetStackFrameSize() - 1);
+    ContinueSem->Notify();
+    return dap::StepOutResponse();
+  });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Launch
+  Session->registerHandler([](const dap::LaunchRequest& req) {
+    (void)req;
+    return dap::LaunchResponse();
+  });
+
+  // Handler for disconnect requests
+  Session->registerHandler([this](const dap::DisconnectRequest& request) {
+    (void)request;
+    BreakpointManager->ClearAll();
+    ExceptionManager->ClearAll();
+    ClearStepRequests();
+    ContinueSem->Notify();
+    DisconnectEvent->Fire();
+    SessionActive.store(false);
+    return dap::DisconnectResponse();
+  });
+
+  Session->registerHandler([this](const dap::EvaluateRequest& request) {
+    dap::EvaluateResponse response;
+    if (request.frameId.has_value()) {
+      std::shared_ptr<cmDebuggerStackFrame> frame =
+        DefaultThread->GetStackFrame(request.frameId.value());
+
+      auto var = frame->GetMakefile()->GetDefinition(request.expression);
+      if (var) {
+        response.type = "string";
+        response.result = var;
+        return response;
+      }
+    }
+
+    return response;
+  });
+
+  // The ConfigurationDone request is made by the client once all configuration
+  // requests have been made.
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ConfigurationDone
+  Session->registerHandler([this](const dap::ConfigurationDoneRequest& req) {
+    (void)req;
+    ConfigurationDoneEvent->Fire();
+    return dap::ConfigurationDoneResponse();
+  });
+
+  std::string errorMessage;
+  if (!Connection->StartListening(errorMessage)) {
+    throw std::runtime_error(errorMessage);
+  }
+
+  // Connect to the client. Write a well-known message to stdout so that
+  // clients know it is safe to attempt to connect.
+  std::cout << "Waiting for debugger client to connect..." << std::endl;
+  Connection->WaitForConnection();
+  std::cout << "Debugger client connected." << std::endl;
+
+  if (SessionLog) {
+    Session->connect(spy(Connection->GetReader(), SessionLog),
+                     spy(Connection->GetWriter(), SessionLog));
+  } else {
+    Session->connect(Connection->GetReader(), Connection->GetWriter());
+  }
+
+  // Start the processing thread.
+  SessionThread = std::thread([this] {
+    while (SessionActive.load()) {
+      if (auto payload = Session->getPayload()) {
+        payload();
+      }
+    }
+  });
+
+  ConfigurationDoneEvent->Wait();
+
+  DefaultThread = ThreadManager->StartThread("CMake script");
+  dap::ThreadEvent threadEvent;
+  threadEvent.reason = "started";
+  threadEvent.threadId = DefaultThread->GetId();
+  Session->send(threadEvent);
+}
+
+cmDebuggerAdapter::~cmDebuggerAdapter()
+{
+  if (SessionThread.joinable()) {
+    SessionThread.join();
+  }
+
+  Session.reset(nullptr);
+
+  if (SessionLog) {
+    SessionLog->close();
+  }
+}
+
+void cmDebuggerAdapter::ReportExitCode(int exitCode)
+{
+  ThreadManager->EndThread(DefaultThread);
+  dap::ThreadEvent threadEvent;
+  threadEvent.reason = "exited";
+  threadEvent.threadId = DefaultThread->GetId();
+  DefaultThread.reset();
+
+  dap::ExitedEvent exitEvent;
+  exitEvent.exitCode = exitCode;
+
+  dap::TerminatedEvent terminatedEvent;
+
+  if (SessionActive.load()) {
+    Session->send(threadEvent);
+    Session->send(exitEvent);
+    Session->send(terminatedEvent);
+  }
+
+  // Wait until disconnected or error.
+  DisconnectEvent->Wait();
+}
+
+void cmDebuggerAdapter::OnFileParsedSuccessfully(
+  std::string const& sourcePath,
+  std::vector<cmListFileFunction> const& functions)
+{
+  BreakpointManager->SourceFileLoaded(sourcePath, functions);
+}
+
+void cmDebuggerAdapter::OnBeginFunctionCall(cmMakefile* mf,
+                                            std::string const& sourcePath,
+                                            cmListFileFunction const& lff)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  DefaultThread->PushStackFrame(mf, sourcePath, lff);
+
+  if (lff.Line() == 0) {
+    // File just loaded, continue to first valid function call.
+    return;
+  }
+
+  auto hits = BreakpointManager->GetBreakpoints(sourcePath, lff.Line());
+  lock.unlock();
+
+  bool waitSem = false;
+  dap::StoppedEvent stoppedEvent;
+  stoppedEvent.allThreadsStopped = true;
+  stoppedEvent.threadId = DefaultThread->GetId();
+  if (!hits.empty()) {
+    ClearStepRequests();
+    waitSem = true;
+
+    dap::array<dap::integer> hitBreakpoints;
+    hitBreakpoints.resize(hits.size());
+    std::transform(hits.begin(), hits.end(), hitBreakpoints.begin(),
+                   [&](const int64_t& id) { return dap::integer(id); });
+    stoppedEvent.reason = "breakpoint";
+    stoppedEvent.hitBreakpointIds = hitBreakpoints;
+  }
+
+  if (long(DefaultThread->GetStackFrameSize()) <= NextStepFrom.load() ||
+      StepInRequest.load() ||
+      long(DefaultThread->GetStackFrameSize()) <= StepOutDepth.load()) {
+    ClearStepRequests();
+    waitSem = true;
+
+    stoppedEvent.reason = "step";
+  }
+
+  if (PauseRequest.load()) {
+    ClearStepRequests();
+    waitSem = true;
+
+    stoppedEvent.reason = "pause";
+  }
+
+  if (waitSem) {
+    Session->send(stoppedEvent);
+    ContinueSem->Wait();
+  }
+}
+
+void cmDebuggerAdapter::OnEndFunctionCall()
+{
+  DefaultThread->PopStackFrame();
+}
+
+static std::shared_ptr<cmListFileFunction> listFileFunction;
+
+void cmDebuggerAdapter::OnBeginFileParse(cmMakefile* mf,
+                                         std::string const& sourcePath)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+
+  listFileFunction = std::make_shared<cmListFileFunction>(
+    sourcePath, 0, 0, std::vector<cmListFileArgument>());
+  DefaultThread->PushStackFrame(mf, sourcePath, *listFileFunction);
+}
+
+void cmDebuggerAdapter::OnEndFileParse()
+{
+  DefaultThread->PopStackFrame();
+  listFileFunction = nullptr;
+}
+
+void cmDebuggerAdapter::OnMessageOutput(MessageType t, std::string const& text)
+{
+  cm::optional<dap::StoppedEvent> stoppedEvent =
+    ExceptionManager->RaiseExceptionIfAny(t, text);
+  if (stoppedEvent.has_value()) {
+    stoppedEvent->threadId = DefaultThread->GetId();
+    Session->send(*stoppedEvent);
+    ContinueSem->Wait();
+  }
+}
+
+void cmDebuggerAdapter::ClearStepRequests()
+{
+  NextStepFrom.store(INT_MIN);
+  StepInRequest.store(false);
+  StepOutDepth.store(INT_MIN);
+  PauseRequest.store(false);
+}
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerAdapter.h b/Source/cmDebuggerAdapter.h
new file mode 100644
index 0000000..f261d88
--- /dev/null
+++ b/Source/cmDebuggerAdapter.h
@@ -0,0 +1,93 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <atomic>
+#include <cstdint>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <cm/optional>
+
+#include <cm3p/cppdap/io.h> // IWYU pragma: keep
+
+#include "cmMessageType.h"
+
+class cmListFileFunction;
+class cmMakefile;
+
+namespace cmDebugger {
+class Semaphore;
+class SyncEvent;
+class cmDebuggerBreakpointManager;
+class cmDebuggerExceptionManager;
+class cmDebuggerThread;
+class cmDebuggerThreadManager;
+}
+
+namespace dap {
+class Session;
+}
+
+namespace cmDebugger {
+
+class cmDebuggerConnection
+{
+public:
+  virtual ~cmDebuggerConnection() = default;
+  virtual bool StartListening(std::string& errorMessage) = 0;
+  virtual void WaitForConnection() = 0;
+  virtual std::shared_ptr<dap::Reader> GetReader() = 0;
+  virtual std::shared_ptr<dap::Writer> GetWriter() = 0;
+};
+
+class cmDebuggerAdapter
+{
+public:
+  cmDebuggerAdapter(std::shared_ptr<cmDebuggerConnection> connection,
+                    std::string const& dapLogPath);
+  cmDebuggerAdapter(std::shared_ptr<cmDebuggerConnection> connection,
+                    cm::optional<std::shared_ptr<dap::Writer>> logger);
+  ~cmDebuggerAdapter();
+
+  void ReportExitCode(int exitCode);
+
+  void OnFileParsedSuccessfully(
+    std::string const& sourcePath,
+    std::vector<cmListFileFunction> const& functions);
+  void OnBeginFunctionCall(cmMakefile* mf, std::string const& sourcePath,
+                           cmListFileFunction const& lff);
+  void OnEndFunctionCall();
+  void OnBeginFileParse(cmMakefile* mf, std::string const& sourcePath);
+  void OnEndFileParse();
+
+  void OnMessageOutput(MessageType t, std::string const& text);
+
+private:
+  void ClearStepRequests();
+  std::shared_ptr<cmDebuggerConnection> Connection;
+  std::unique_ptr<dap::Session> Session;
+  std::shared_ptr<dap::Writer> SessionLog;
+  std::thread SessionThread;
+  std::atomic<bool> SessionActive;
+  std::mutex Mutex;
+  std::unique_ptr<SyncEvent> DisconnectEvent;
+  std::unique_ptr<SyncEvent> ConfigurationDoneEvent;
+  std::unique_ptr<Semaphore> ContinueSem;
+  std::atomic<int64_t> NextStepFrom;
+  std::atomic<bool> StepInRequest;
+  std::atomic<int64_t> StepOutDepth;
+  std::atomic<bool> PauseRequest;
+  std::unique_ptr<cmDebuggerThreadManager> ThreadManager;
+  std::shared_ptr<cmDebuggerThread> DefaultThread;
+  std::unique_ptr<cmDebuggerBreakpointManager> BreakpointManager;
+  std::unique_ptr<cmDebuggerExceptionManager> ExceptionManager;
+  bool SupportsVariableType;
+};
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerBreakpointManager.cxx b/Source/cmDebuggerBreakpointManager.cxx
new file mode 100644
index 0000000..152f0f5
--- /dev/null
+++ b/Source/cmDebuggerBreakpointManager.cxx
@@ -0,0 +1,200 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmDebuggerBreakpointManager.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+
+#include <cm3p/cppdap/optional.h>
+#include <cm3p/cppdap/session.h>
+#include <cm3p/cppdap/types.h>
+
+#include "cmDebuggerSourceBreakpoint.h"
+#include "cmListFileCache.h"
+#include "cmSystemTools.h"
+
+namespace cmDebugger {
+
+cmDebuggerBreakpointManager::cmDebuggerBreakpointManager(
+  dap::Session* dapSession)
+  : DapSession(dapSession)
+{
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetBreakpoints
+  DapSession->registerHandler([&](const dap::SetBreakpointsRequest& request) {
+    return HandleSetBreakpointsRequest(request);
+  });
+}
+
+int64_t cmDebuggerBreakpointManager::FindFunctionStartLine(
+  std::string const& sourcePath, int64_t line)
+{
+  auto location =
+    find_if(ListFileFunctionLines[sourcePath].begin(),
+            ListFileFunctionLines[sourcePath].end(),
+            [=](cmDebuggerFunctionLocation const& loc) {
+              return loc.StartLine <= line && loc.EndLine >= line;
+            });
+
+  if (location != ListFileFunctionLines[sourcePath].end()) {
+    return location->StartLine;
+  }
+
+  return 0;
+}
+
+int64_t cmDebuggerBreakpointManager::CalibrateBreakpointLine(
+  std::string const& sourcePath, int64_t line)
+{
+  auto location = find_if(ListFileFunctionLines[sourcePath].begin(),
+                          ListFileFunctionLines[sourcePath].end(),
+                          [=](cmDebuggerFunctionLocation const& loc) {
+                            return loc.StartLine >= line;
+                          });
+
+  if (location != ListFileFunctionLines[sourcePath].end()) {
+    return location->StartLine;
+  }
+
+  if (!ListFileFunctionLines[sourcePath].empty() &&
+      ListFileFunctionLines[sourcePath].back().EndLine <= line) {
+    // return last function start line for any breakpoints after.
+    return ListFileFunctionLines[sourcePath].back().StartLine;
+  }
+
+  return 0;
+}
+
+dap::SetBreakpointsResponse
+cmDebuggerBreakpointManager::HandleSetBreakpointsRequest(
+  dap::SetBreakpointsRequest const& request)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+
+  dap::SetBreakpointsResponse response;
+
+  auto sourcePath =
+    cmSystemTools::GetActualCaseForPath(request.source.path.value());
+  const dap::array<dap::SourceBreakpoint> defaultValue{};
+  const auto& breakpoints = request.breakpoints.value(defaultValue);
+  if (ListFileFunctionLines.find(sourcePath) != ListFileFunctionLines.end()) {
+    // The file has loaded, we can validate breakpoints.
+    if (Breakpoints.find(sourcePath) != Breakpoints.end()) {
+      Breakpoints[sourcePath].clear();
+    }
+    response.breakpoints.resize(breakpoints.size());
+    for (size_t i = 0; i < breakpoints.size(); i++) {
+      int64_t correctedLine =
+        CalibrateBreakpointLine(sourcePath, breakpoints[i].line);
+      if (correctedLine > 0) {
+        Breakpoints[sourcePath].emplace_back(NextBreakpointId++,
+                                             correctedLine);
+        response.breakpoints[i].id = Breakpoints[sourcePath].back().GetId();
+        response.breakpoints[i].line =
+          Breakpoints[sourcePath].back().GetLine();
+        response.breakpoints[i].verified = true;
+      } else {
+        response.breakpoints[i].verified = false;
+        response.breakpoints[i].line = breakpoints[i].line;
+      }
+      dap::Source dapSrc;
+      dapSrc.path = sourcePath;
+      response.breakpoints[i].source = dapSrc;
+    }
+  } else {
+    // The file has not loaded, validate breakpoints later.
+    ListFilePendingValidations.emplace(sourcePath);
+
+    response.breakpoints.resize(breakpoints.size());
+    for (size_t i = 0; i < breakpoints.size(); i++) {
+      Breakpoints[sourcePath].emplace_back(NextBreakpointId++,
+                                           breakpoints[i].line);
+      response.breakpoints[i].id = Breakpoints[sourcePath].back().GetId();
+      response.breakpoints[i].line = Breakpoints[sourcePath].back().GetLine();
+      response.breakpoints[i].verified = false;
+      dap::Source dapSrc;
+      dapSrc.path = sourcePath;
+      response.breakpoints[i].source = dapSrc;
+    }
+  }
+
+  return response;
+}
+
+void cmDebuggerBreakpointManager::SourceFileLoaded(
+  std::string const& sourcePath,
+  std::vector<cmListFileFunction> const& functions)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  if (ListFileFunctionLines.find(sourcePath) != ListFileFunctionLines.end()) {
+    // this is not expected.
+    return;
+  }
+
+  for (cmListFileFunction const& func : functions) {
+    ListFileFunctionLines[sourcePath].emplace_back(
+      cmDebuggerFunctionLocation{ func.Line(), func.LineEnd() });
+  }
+
+  if (ListFilePendingValidations.find(sourcePath) ==
+      ListFilePendingValidations.end()) {
+    return;
+  }
+
+  ListFilePendingValidations.erase(sourcePath);
+
+  for (size_t i = 0; i < Breakpoints[sourcePath].size(); i++) {
+    dap::BreakpointEvent breakpointEvent;
+    breakpointEvent.breakpoint.id = Breakpoints[sourcePath][i].GetId();
+    breakpointEvent.breakpoint.line = Breakpoints[sourcePath][i].GetLine();
+    auto source = dap::Source();
+    source.path = sourcePath;
+    breakpointEvent.breakpoint.source = source;
+    int64_t correctedLine = CalibrateBreakpointLine(
+      sourcePath, Breakpoints[sourcePath][i].GetLine());
+    if (correctedLine != Breakpoints[sourcePath][i].GetLine()) {
+      Breakpoints[sourcePath][i].ChangeLine(correctedLine);
+    }
+    breakpointEvent.reason = "changed";
+    breakpointEvent.breakpoint.verified = (correctedLine > 0);
+    if (breakpointEvent.breakpoint.verified) {
+      breakpointEvent.breakpoint.line = correctedLine;
+    } else {
+      Breakpoints[sourcePath][i].Invalid();
+    }
+
+    DapSession->send(breakpointEvent);
+  }
+}
+
+std::vector<int64_t> cmDebuggerBreakpointManager::GetBreakpoints(
+  std::string const& sourcePath, int64_t line)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  const auto& all = Breakpoints[sourcePath];
+  std::vector<int64_t> breakpoints;
+  if (all.empty()) {
+    return breakpoints;
+  }
+
+  auto it = all.begin();
+
+  while ((it = std::find_if(
+            it, all.end(), [&](const cmDebuggerSourceBreakpoint& breakpoint) {
+              return (breakpoint.GetIsValid() && breakpoint.GetLine() == line);
+            })) != all.end()) {
+    breakpoints.emplace_back(it->GetId());
+    ++it;
+  }
+
+  return breakpoints;
+}
+
+void cmDebuggerBreakpointManager::ClearAll()
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  Breakpoints.clear();
+}
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerBreakpointManager.h b/Source/cmDebuggerBreakpointManager.h
new file mode 100644
index 0000000..a4e5df5
--- /dev/null
+++ b/Source/cmDebuggerBreakpointManager.h
@@ -0,0 +1,61 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <cstdint>
+#include <mutex>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include <cm3p/cppdap/protocol.h>
+
+class cmListFileFunction;
+
+namespace cmDebugger {
+class cmDebuggerSourceBreakpoint;
+}
+
+namespace dap {
+class Session;
+}
+
+namespace cmDebugger {
+
+struct cmDebuggerFunctionLocation
+{
+  int64_t StartLine;
+  int64_t EndLine;
+};
+
+/** The breakpoint manager. */
+class cmDebuggerBreakpointManager
+{
+  dap::Session* DapSession;
+  std::mutex Mutex;
+  std::unordered_map<std::string, std::vector<cmDebuggerSourceBreakpoint>>
+    Breakpoints;
+  std::unordered_map<std::string,
+                     std::vector<struct cmDebuggerFunctionLocation>>
+    ListFileFunctionLines;
+  std::unordered_set<std::string> ListFilePendingValidations;
+  int64_t NextBreakpointId = 0;
+
+  dap::SetBreakpointsResponse HandleSetBreakpointsRequest(
+    dap::SetBreakpointsRequest const& request);
+  int64_t FindFunctionStartLine(std::string const& sourcePath, int64_t line);
+  int64_t CalibrateBreakpointLine(std::string const& sourcePath, int64_t line);
+
+public:
+  cmDebuggerBreakpointManager(dap::Session* dapSession);
+  void SourceFileLoaded(std::string const& sourcePath,
+                        std::vector<cmListFileFunction> const& functions);
+  std::vector<int64_t> GetBreakpoints(std::string const& sourcePath,
+                                      int64_t line);
+  void ClearAll();
+};
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerExceptionManager.cxx b/Source/cmDebuggerExceptionManager.cxx
new file mode 100644
index 0000000..a27426c
--- /dev/null
+++ b/Source/cmDebuggerExceptionManager.cxx
@@ -0,0 +1,129 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmDebuggerExceptionManager.h"
+
+#include <utility>
+#include <vector>
+
+#include <cm3p/cppdap/optional.h>
+#include <cm3p/cppdap/session.h>
+#include <cm3p/cppdap/types.h>
+
+#include "cmDebuggerProtocol.h"
+#include "cmMessageType.h"
+
+namespace cmDebugger {
+
+cmDebuggerExceptionManager::cmDebuggerExceptionManager(
+  dap::Session* dapSession)
+  : DapSession(dapSession)
+{
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetExceptionBreakpoints
+  DapSession->registerHandler(
+    [&](const dap::SetExceptionBreakpointsRequest& request) {
+      return HandleSetExceptionBreakpointsRequest(request);
+    });
+
+  // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ExceptionInfo
+  DapSession->registerHandler([&](const dap::ExceptionInfoRequest& request) {
+    (void)request;
+    return HandleExceptionInfoRequest();
+  });
+
+  ExceptionMap[MessageType::AUTHOR_WARNING] =
+    cmDebuggerExceptionFilter{ "AUTHOR_WARNING", "Warning (dev)" };
+  ExceptionMap[MessageType::AUTHOR_ERROR] =
+    cmDebuggerExceptionFilter{ "AUTHOR_ERROR", "Error (dev)" };
+  ExceptionMap[MessageType::FATAL_ERROR] =
+    cmDebuggerExceptionFilter{ "FATAL_ERROR", "Fatal error" };
+  ExceptionMap[MessageType::INTERNAL_ERROR] =
+    cmDebuggerExceptionFilter{ "INTERNAL_ERROR", "Internal error" };
+  ExceptionMap[MessageType::MESSAGE] =
+    cmDebuggerExceptionFilter{ "MESSAGE", "Other messages" };
+  ExceptionMap[MessageType::WARNING] =
+    cmDebuggerExceptionFilter{ "WARNING", "Warning" };
+  ExceptionMap[MessageType::LOG] =
+    cmDebuggerExceptionFilter{ "LOG", "Debug log" };
+  ExceptionMap[MessageType::DEPRECATION_ERROR] =
+    cmDebuggerExceptionFilter{ "DEPRECATION_ERROR", "Deprecation error" };
+  ExceptionMap[MessageType::DEPRECATION_WARNING] =
+    cmDebuggerExceptionFilter{ "DEPRECATION_WARNING", "Deprecation warning" };
+  RaiseExceptions["AUTHOR_ERROR"] = true;
+  RaiseExceptions["FATAL_ERROR"] = true;
+  RaiseExceptions["INTERNAL_ERROR"] = true;
+  RaiseExceptions["DEPRECATION_ERROR"] = true;
+}
+
+dap::SetExceptionBreakpointsResponse
+cmDebuggerExceptionManager::HandleSetExceptionBreakpointsRequest(
+  dap::SetExceptionBreakpointsRequest const& request)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  dap::SetExceptionBreakpointsResponse response;
+  RaiseExceptions.clear();
+  for (const auto& filter : request.filters) {
+    RaiseExceptions[filter] = true;
+  }
+
+  return response;
+}
+
+dap::ExceptionInfoResponse
+cmDebuggerExceptionManager::HandleExceptionInfoRequest()
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+
+  dap::ExceptionInfoResponse response;
+  if (TheException.has_value()) {
+    response.exceptionId = TheException->Id;
+    response.breakMode = "always";
+    response.description = TheException->Description;
+    TheException = {};
+  }
+  return response;
+}
+
+void cmDebuggerExceptionManager::HandleInitializeRequest(
+  dap::CMakeInitializeResponse& response)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  response.supportsExceptionInfoRequest = true;
+
+  dap::array<dap::ExceptionBreakpointsFilter> exceptionBreakpointFilters;
+  for (auto& pair : ExceptionMap) {
+    dap::ExceptionBreakpointsFilter filter;
+    filter.filter = pair.second.Filter;
+    filter.label = pair.second.Label;
+    filter.def = RaiseExceptions[filter.filter];
+    exceptionBreakpointFilters.emplace_back(filter);
+  }
+
+  response.exceptionBreakpointFilters = exceptionBreakpointFilters;
+}
+
+cm::optional<dap::StoppedEvent>
+cmDebuggerExceptionManager::RaiseExceptionIfAny(MessageType t,
+                                                std::string const& text)
+{
+  cm::optional<dap::StoppedEvent> maybeStoppedEvent;
+  std::unique_lock<std::mutex> lock(Mutex);
+  if (RaiseExceptions[ExceptionMap[t].Filter]) {
+    dap::StoppedEvent stoppedEvent;
+    stoppedEvent.allThreadsStopped = true;
+    stoppedEvent.reason = "exception";
+    stoppedEvent.description = "Pause on exception";
+    stoppedEvent.text = text;
+    TheException = cmDebuggerException{ ExceptionMap[t].Filter, text };
+    maybeStoppedEvent = std::move(stoppedEvent);
+  }
+
+  return maybeStoppedEvent;
+}
+
+void cmDebuggerExceptionManager::ClearAll()
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  RaiseExceptions.clear();
+}
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerExceptionManager.h b/Source/cmDebuggerExceptionManager.h
new file mode 100644
index 0000000..b819128
--- /dev/null
+++ b/Source/cmDebuggerExceptionManager.h
@@ -0,0 +1,70 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <cstddef>
+#include <functional>
+#include <mutex>
+#include <string>
+#include <unordered_map>
+
+#include <cm/optional>
+
+#include <cm3p/cppdap/protocol.h>
+
+#include "cmMessageType.h"
+
+namespace dap {
+class Session;
+struct CMakeInitializeResponse;
+}
+
+namespace cmDebugger {
+
+struct cmDebuggerException
+{
+  std::string Id;
+  std::string Description;
+};
+
+struct cmDebuggerExceptionFilter
+{
+  std::string Filter;
+  std::string Label;
+};
+
+/** The exception manager. */
+class cmDebuggerExceptionManager
+{
+  // Some older C++ standard libraries cannot hash an enum class by default.
+  struct MessageTypeHash
+  {
+    std::size_t operator()(MessageType t) const
+    {
+      return std::hash<int>{}(static_cast<int>(t));
+    }
+  };
+
+  dap::Session* DapSession;
+  std::mutex Mutex;
+  std::unordered_map<std::string, bool> RaiseExceptions;
+  std::unordered_map<MessageType, cmDebuggerExceptionFilter, MessageTypeHash>
+    ExceptionMap;
+  cm::optional<cmDebuggerException> TheException;
+
+  dap::SetExceptionBreakpointsResponse HandleSetExceptionBreakpointsRequest(
+    dap::SetExceptionBreakpointsRequest const& request);
+
+  dap::ExceptionInfoResponse HandleExceptionInfoRequest();
+
+public:
+  cmDebuggerExceptionManager(dap::Session* dapSession);
+  void HandleInitializeRequest(dap::CMakeInitializeResponse& response);
+  cm::optional<dap::StoppedEvent> RaiseExceptionIfAny(MessageType t,
+                                                      std::string const& text);
+  void ClearAll();
+};
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerPipeConnection.cxx b/Source/cmDebuggerPipeConnection.cxx
new file mode 100644
index 0000000..1b54346
--- /dev/null
+++ b/Source/cmDebuggerPipeConnection.cxx
@@ -0,0 +1,293 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmDebuggerPipeConnection.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cstring>
+#include <stdexcept>
+#include <utility>
+
+namespace cmDebugger {
+
+struct write_req_t
+{
+  uv_write_t req;
+  uv_buf_t buf;
+};
+
+cmDebuggerPipeBase::cmDebuggerPipeBase(std::string name)
+  : PipeName(std::move(name))
+{
+  Loop.init();
+  LoopExit.init(
+    *Loop, [](uv_async_t* handle) { uv_stop((uv_loop_t*)handle->data); },
+    Loop);
+  WriteEvent.init(
+    *Loop,
+    [](uv_async_t* handle) {
+      auto* conn = static_cast<cmDebuggerPipeBase*>(handle->data);
+      conn->WriteInternal();
+    },
+    this);
+  PipeClose.init(
+    *Loop,
+    [](uv_async_t* handle) {
+      auto* conn = static_cast<cmDebuggerPipeBase*>(handle->data);
+      if (conn->Pipe.get()) {
+        conn->Pipe->data = nullptr;
+        conn->Pipe.reset();
+      }
+    },
+    this);
+}
+
+void cmDebuggerPipeBase::WaitForConnection()
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  Connected.wait(lock, [this] { return isOpen() || FailedToOpen; });
+  if (FailedToOpen) {
+    throw std::runtime_error("Failed to open debugger connection.");
+  }
+}
+
+void cmDebuggerPipeBase::close()
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+
+  CloseConnection();
+  PipeClose.send();
+  lock.unlock();
+  ReadReady.notify_all();
+}
+
+size_t cmDebuggerPipeBase::read(void* buffer, size_t n)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  ReadReady.wait(lock, [this] { return !isOpen() || !ReadBuffer.empty(); });
+
+  if (!isOpen() && ReadBuffer.empty()) {
+    return 0;
+  }
+
+  auto size = std::min(n, ReadBuffer.size());
+  memcpy(buffer, ReadBuffer.data(), size);
+  ReadBuffer.erase(0, size);
+  return size;
+}
+
+bool cmDebuggerPipeBase::write(const void* buffer, size_t n)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  WriteBuffer.append(static_cast<const char*>(buffer), n);
+  lock.unlock();
+  WriteEvent.send();
+
+  lock.lock();
+  WriteComplete.wait(lock, [this] { return WriteBuffer.empty(); });
+  return true;
+}
+
+void cmDebuggerPipeBase::StopLoop()
+{
+  LoopExit.send();
+
+  if (LoopThread.joinable()) {
+    LoopThread.join();
+  }
+}
+
+void cmDebuggerPipeBase::BufferData(const std::string& data)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  ReadBuffer += data;
+  lock.unlock();
+  ReadReady.notify_all();
+}
+
+void cmDebuggerPipeBase::WriteInternal()
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  auto n = WriteBuffer.length();
+  assert(this->Pipe.get());
+  write_req_t* req = new write_req_t;
+  req->req.data = &WriteComplete;
+  char* rawBuffer = new char[n];
+  req->buf = uv_buf_init(rawBuffer, static_cast<unsigned int>(n));
+  memcpy(req->buf.base, WriteBuffer.data(), n);
+  WriteBuffer.clear();
+  lock.unlock();
+
+  uv_write(
+    reinterpret_cast<uv_write_t*>(req), this->Pipe, &req->buf, 1,
+    [](uv_write_t* cb_req, int status) {
+      (void)status; // We need to free memory even if the write failed.
+      write_req_t* wr = reinterpret_cast<write_req_t*>(cb_req);
+      reinterpret_cast<std::condition_variable*>(wr->req.data)->notify_all();
+      delete[] (wr->buf.base);
+      delete wr;
+    });
+
+#ifdef __clang_analyzer__
+  // Tell clang-analyzer that 'rawBuffer' does not leak.
+  // We pass ownership to the closure.
+  delete[] rawBuffer;
+#endif
+}
+
+cmDebuggerPipeConnection::cmDebuggerPipeConnection(std::string name)
+  : cmDebuggerPipeBase(std::move(name))
+{
+  ServerPipeClose.init(
+    *Loop,
+    [](uv_async_t* handle) {
+      auto* conn = static_cast<cmDebuggerPipeConnection*>(handle->data);
+      if (conn->ServerPipe.get()) {
+        conn->ServerPipe->data = nullptr;
+        conn->ServerPipe.reset();
+      }
+    },
+    this);
+}
+
+cmDebuggerPipeConnection::~cmDebuggerPipeConnection()
+{
+  StopLoop();
+}
+
+bool cmDebuggerPipeConnection::StartListening(std::string& errorMessage)
+{
+  this->ServerPipe.init(*Loop, 0,
+                        static_cast<cmDebuggerPipeConnection*>(this));
+
+  int r;
+  if ((r = uv_pipe_bind(this->ServerPipe, this->PipeName.c_str())) != 0) {
+    errorMessage =
+      "Internal Error with " + this->PipeName + ": " + uv_err_name(r);
+    return false;
+  }
+
+  r = uv_listen(this->ServerPipe, 1, [](uv_stream_t* stream, int status) {
+    if (status >= 0) {
+      auto* conn = static_cast<cmDebuggerPipeConnection*>(stream->data);
+      if (conn) {
+        conn->Connect(stream);
+      }
+    }
+  });
+
+  if (r != 0) {
+    errorMessage =
+      "Internal Error listening on " + this->PipeName + ": " + uv_err_name(r);
+    return false;
+  }
+
+  // Start the libuv event loop thread so that a client can connect.
+  LoopThread = std::thread([this] { uv_run(Loop, UV_RUN_DEFAULT); });
+
+  StartedListening.set_value();
+
+  return true;
+}
+
+std::shared_ptr<dap::Reader> cmDebuggerPipeConnection::GetReader()
+{
+  return std::static_pointer_cast<dap::Reader>(shared_from_this());
+}
+
+std::shared_ptr<dap::Writer> cmDebuggerPipeConnection::GetWriter()
+{
+  return std::static_pointer_cast<dap::Writer>(shared_from_this());
+}
+
+bool cmDebuggerPipeConnection::isOpen()
+{
+  return this->Pipe.get() != nullptr;
+}
+
+void cmDebuggerPipeConnection::CloseConnection()
+{
+  ServerPipeClose.send();
+}
+
+void cmDebuggerPipeConnection::Connect(uv_stream_t* server)
+{
+  if (this->Pipe.get()) {
+    // Accept and close all pipes but the first:
+    cm::uv_pipe_ptr rejectPipe;
+
+    rejectPipe.init(*Loop, 0);
+    uv_accept(server, rejectPipe);
+
+    return;
+  }
+
+  cm::uv_pipe_ptr ClientPipe;
+  ClientPipe.init(*Loop, 0, static_cast<cmDebuggerPipeConnection*>(this));
+
+  if (uv_accept(server, ClientPipe) != 0) {
+    return;
+  }
+
+  StartReading<cmDebuggerPipeConnection>(ClientPipe);
+
+  std::unique_lock<std::mutex> lock(Mutex);
+  Pipe = std::move(ClientPipe);
+  lock.unlock();
+  Connected.notify_all();
+}
+
+cmDebuggerPipeClient::~cmDebuggerPipeClient()
+{
+  StopLoop();
+}
+
+void cmDebuggerPipeClient::Start()
+{
+  this->Pipe.init(*Loop, 0, static_cast<cmDebuggerPipeClient*>(this));
+
+  uv_connect_t* connect = new uv_connect_t;
+  connect->data = this;
+  uv_pipe_connect(
+    connect, Pipe, PipeName.c_str(), [](uv_connect_t* cb_connect, int status) {
+      auto* conn = static_cast<cmDebuggerPipeClient*>(cb_connect->data);
+      if (status >= 0) {
+        conn->Connect();
+      } else {
+        conn->FailConnection();
+      }
+      delete cb_connect;
+    });
+
+  // Start the libuv event loop so that the pipe can connect.
+  LoopThread = std::thread([this] { uv_run(Loop, UV_RUN_DEFAULT); });
+}
+
+bool cmDebuggerPipeClient::isOpen()
+{
+  return IsConnected;
+}
+
+void cmDebuggerPipeClient::CloseConnection()
+{
+  IsConnected = false;
+}
+
+void cmDebuggerPipeClient::Connect()
+{
+  StartReading<cmDebuggerPipeClient>(Pipe);
+  std::unique_lock<std::mutex> lock(Mutex);
+  IsConnected = true;
+  lock.unlock();
+  Connected.notify_all();
+}
+
+void cmDebuggerPipeClient::FailConnection()
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  FailedToOpen = true;
+  lock.unlock();
+  Connected.notify_all();
+}
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerPipeConnection.h b/Source/cmDebuggerPipeConnection.h
new file mode 100644
index 0000000..0991ff7
--- /dev/null
+++ b/Source/cmDebuggerPipeConnection.h
@@ -0,0 +1,139 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <condition_variable>
+#include <cstddef>
+#include <future>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <thread>
+
+#include <cm3p/cppdap/io.h>
+#include <cm3p/uv.h>
+
+#include "cmDebuggerAdapter.h"
+#include "cmUVHandlePtr.h"
+
+namespace cmDebugger {
+
+class cmDebuggerPipeBase : public dap::ReaderWriter
+{
+public:
+  cmDebuggerPipeBase(std::string name);
+
+  void WaitForConnection();
+
+  // dap::ReaderWriter implementation
+
+  void close() final;
+  size_t read(void* buffer, size_t n) final;
+  bool write(const void* buffer, size_t n) final;
+
+protected:
+  virtual void CloseConnection(){};
+  template <typename T>
+  void StartReading(uv_stream_t* stream)
+  {
+    uv_read_start(
+      stream,
+      // alloc_cb
+      [](uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) {
+        (void)handle;
+        char* rawBuffer = new char[suggested_size];
+        *buf =
+          uv_buf_init(rawBuffer, static_cast<unsigned int>(suggested_size));
+      },
+      // read_cb
+      [](uv_stream_t* readStream, ssize_t nread, const uv_buf_t* buf) {
+        auto conn = static_cast<T*>(readStream->data);
+        if (conn) {
+          if (nread >= 0) {
+            conn->BufferData(std::string(buf->base, buf->base + nread));
+          } else {
+            conn->close();
+          }
+        }
+        delete[] (buf->base);
+      });
+  }
+  void StopLoop();
+
+  const std::string PipeName;
+  std::thread LoopThread;
+  cm::uv_loop_ptr Loop;
+  cm::uv_pipe_ptr Pipe;
+  std::mutex Mutex;
+  std::condition_variable Connected;
+  bool FailedToOpen = false;
+
+private:
+  void BufferData(const std::string& data);
+  void WriteInternal();
+
+  cm::uv_async_ptr LoopExit;
+  cm::uv_async_ptr WriteEvent;
+  cm::uv_async_ptr PipeClose;
+  std::string WriteBuffer;
+  std::string ReadBuffer;
+  std::condition_variable ReadReady;
+  std::condition_variable WriteComplete;
+};
+
+class cmDebuggerPipeConnection
+  : public cmDebuggerPipeBase
+  , public cmDebuggerConnection
+  , public std::enable_shared_from_this<cmDebuggerPipeConnection>
+{
+public:
+  cmDebuggerPipeConnection(std::string name);
+  ~cmDebuggerPipeConnection() override;
+
+  void WaitForConnection() override
+  {
+    cmDebuggerPipeBase::WaitForConnection();
+  }
+
+  bool StartListening(std::string& errorMessage) override;
+  std::shared_ptr<dap::Reader> GetReader() override;
+  std::shared_ptr<dap::Writer> GetWriter() override;
+
+  // dap::ReaderWriter implementation
+
+  bool isOpen() override;
+
+  // Used for unit test synchronization
+  std::promise<void> StartedListening;
+
+private:
+  void CloseConnection() override;
+  void Connect(uv_stream_t* server);
+
+  cm::uv_pipe_ptr ServerPipe;
+  cm::uv_async_ptr ServerPipeClose;
+};
+
+class cmDebuggerPipeClient : public cmDebuggerPipeBase
+{
+public:
+  using cmDebuggerPipeBase::cmDebuggerPipeBase;
+  ~cmDebuggerPipeClient() override;
+
+  void Start();
+
+  // dap::ReaderWriter implementation
+
+  bool isOpen() override;
+
+private:
+  void CloseConnection() override;
+  void Connect();
+  void FailConnection();
+
+  bool IsConnected = false;
+};
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerProtocol.cxx b/Source/cmDebuggerProtocol.cxx
new file mode 100644
index 0000000..505de35
--- /dev/null
+++ b/Source/cmDebuggerProtocol.cxx
@@ -0,0 +1,80 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmDebuggerProtocol.h"
+
+#include <string>
+
+namespace dap {
+DAP_IMPLEMENT_STRUCT_TYPEINFO(CMakeVersion, "", DAP_FIELD(major, "major"),
+                              DAP_FIELD(minor, "minor"),
+                              DAP_FIELD(patch, "patch"),
+                              DAP_FIELD(full, "full"));
+
+DAP_IMPLEMENT_STRUCT_TYPEINFO(
+  CMakeInitializeResponse, "",
+  DAP_FIELD(additionalModuleColumns, "additionalModuleColumns"),
+  DAP_FIELD(completionTriggerCharacters, "completionTriggerCharacters"),
+  DAP_FIELD(exceptionBreakpointFilters, "exceptionBreakpointFilters"),
+  DAP_FIELD(supportSuspendDebuggee, "supportSuspendDebuggee"),
+  DAP_FIELD(supportTerminateDebuggee, "supportTerminateDebuggee"),
+  DAP_FIELD(supportedChecksumAlgorithms, "supportedChecksumAlgorithms"),
+  DAP_FIELD(supportsBreakpointLocationsRequest,
+            "supportsBreakpointLocationsRequest"),
+  DAP_FIELD(supportsCancelRequest, "supportsCancelRequest"),
+  DAP_FIELD(supportsClipboardContext, "supportsClipboardContext"),
+  DAP_FIELD(supportsCompletionsRequest, "supportsCompletionsRequest"),
+  DAP_FIELD(supportsConditionalBreakpoints, "supportsConditionalBreakpoints"),
+  DAP_FIELD(supportsConfigurationDoneRequest,
+            "supportsConfigurationDoneRequest"),
+  DAP_FIELD(supportsDataBreakpoints, "supportsDataBreakpoints"),
+  DAP_FIELD(supportsDelayedStackTraceLoading,
+            "supportsDelayedStackTraceLoading"),
+  DAP_FIELD(supportsDisassembleRequest, "supportsDisassembleRequest"),
+  DAP_FIELD(supportsEvaluateForHovers, "supportsEvaluateForHovers"),
+  DAP_FIELD(supportsExceptionFilterOptions, "supportsExceptionFilterOptions"),
+  DAP_FIELD(supportsExceptionInfoRequest, "supportsExceptionInfoRequest"),
+  DAP_FIELD(supportsExceptionOptions, "supportsExceptionOptions"),
+  DAP_FIELD(supportsFunctionBreakpoints, "supportsFunctionBreakpoints"),
+  DAP_FIELD(supportsGotoTargetsRequest, "supportsGotoTargetsRequest"),
+  DAP_FIELD(supportsHitConditionalBreakpoints,
+            "supportsHitConditionalBreakpoints"),
+  DAP_FIELD(supportsInstructionBreakpoints, "supportsInstructionBreakpoints"),
+  DAP_FIELD(supportsLoadedSourcesRequest, "supportsLoadedSourcesRequest"),
+  DAP_FIELD(supportsLogPoints, "supportsLogPoints"),
+  DAP_FIELD(supportsModulesRequest, "supportsModulesRequest"),
+  DAP_FIELD(supportsReadMemoryRequest, "supportsReadMemoryRequest"),
+  DAP_FIELD(supportsRestartFrame, "supportsRestartFrame"),
+  DAP_FIELD(supportsRestartRequest, "supportsRestartRequest"),
+  DAP_FIELD(supportsSetExpression, "supportsSetExpression"),
+  DAP_FIELD(supportsSetVariable, "supportsSetVariable"),
+  DAP_FIELD(supportsSingleThreadExecutionRequests,
+            "supportsSingleThreadExecutionRequests"),
+  DAP_FIELD(supportsStepBack, "supportsStepBack"),
+  DAP_FIELD(supportsStepInTargetsRequest, "supportsStepInTargetsRequest"),
+  DAP_FIELD(supportsSteppingGranularity, "supportsSteppingGranularity"),
+  DAP_FIELD(supportsTerminateRequest, "supportsTerminateRequest"),
+  DAP_FIELD(supportsTerminateThreadsRequest,
+            "supportsTerminateThreadsRequest"),
+  DAP_FIELD(supportsValueFormattingOptions, "supportsValueFormattingOptions"),
+  DAP_FIELD(supportsWriteMemoryRequest, "supportsWriteMemoryRequest"),
+  DAP_FIELD(cmakeVersion, "cmakeVersion"));
+
+DAP_IMPLEMENT_STRUCT_TYPEINFO(
+  CMakeInitializeRequest, "initialize", DAP_FIELD(adapterID, "adapterID"),
+  DAP_FIELD(clientID, "clientID"), DAP_FIELD(clientName, "clientName"),
+  DAP_FIELD(columnsStartAt1, "columnsStartAt1"),
+  DAP_FIELD(linesStartAt1, "linesStartAt1"), DAP_FIELD(locale, "locale"),
+  DAP_FIELD(pathFormat, "pathFormat"),
+  DAP_FIELD(supportsArgsCanBeInterpretedByShell,
+            "supportsArgsCanBeInterpretedByShell"),
+  DAP_FIELD(supportsInvalidatedEvent, "supportsInvalidatedEvent"),
+  DAP_FIELD(supportsMemoryEvent, "supportsMemoryEvent"),
+  DAP_FIELD(supportsMemoryReferences, "supportsMemoryReferences"),
+  DAP_FIELD(supportsProgressReporting, "supportsProgressReporting"),
+  DAP_FIELD(supportsRunInTerminalRequest, "supportsRunInTerminalRequest"),
+  DAP_FIELD(supportsStartDebuggingRequest, "supportsStartDebuggingRequest"),
+  DAP_FIELD(supportsVariablePaging, "supportsVariablePaging"),
+  DAP_FIELD(supportsVariableType, "supportsVariableType"));
+
+} // namespace dap
diff --git a/Source/cmDebuggerProtocol.h b/Source/cmDebuggerProtocol.h
new file mode 100644
index 0000000..4334aed
--- /dev/null
+++ b/Source/cmDebuggerProtocol.h
@@ -0,0 +1,191 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <vector>
+
+#include <cm3p/cppdap/protocol.h>
+
+#include <cmcppdap/include/dap/optional.h>
+#include <cmcppdap/include/dap/typeof.h>
+#include <cmcppdap/include/dap/types.h>
+
+namespace dap {
+
+// Represents the cmake version.
+struct CMakeVersion : public InitializeResponse
+{
+  // The major version number.
+  integer major;
+  // The minor version number.
+  integer minor;
+  // The patch number.
+  integer patch;
+  // The full version string.
+  string full;
+};
+
+DAP_DECLARE_STRUCT_TYPEINFO(CMakeVersion);
+
+// Response to `initialize` request.
+struct CMakeInitializeResponse : public Response
+{
+  // The set of additional module information exposed by the debug adapter.
+  optional<array<ColumnDescriptor>> additionalModuleColumns;
+  // The set of characters that should trigger completion in a REPL. If not
+  // specified, the UI should assume the `.` character.
+  optional<array<string>> completionTriggerCharacters;
+  // Available exception filter options for the `setExceptionBreakpoints`
+  // request.
+  optional<array<ExceptionBreakpointsFilter>> exceptionBreakpointFilters;
+  // The debug adapter supports the `suspendDebuggee` attribute on the
+  // `disconnect` request.
+  optional<boolean> supportSuspendDebuggee;
+  // The debug adapter supports the `terminateDebuggee` attribute on the
+  // `disconnect` request.
+  optional<boolean> supportTerminateDebuggee;
+  // Checksum algorithms supported by the debug adapter.
+  optional<array<ChecksumAlgorithm>> supportedChecksumAlgorithms;
+  // The debug adapter supports the `breakpointLocations` request.
+  optional<boolean> supportsBreakpointLocationsRequest;
+  // The debug adapter supports the `cancel` request.
+  optional<boolean> supportsCancelRequest;
+  // The debug adapter supports the `clipboard` context value in the `evaluate`
+  // request.
+  optional<boolean> supportsClipboardContext;
+  // The debug adapter supports the `completions` request.
+  optional<boolean> supportsCompletionsRequest;
+  // The debug adapter supports conditional breakpoints.
+  optional<boolean> supportsConditionalBreakpoints;
+  // The debug adapter supports the `configurationDone` request.
+  optional<boolean> supportsConfigurationDoneRequest;
+  // The debug adapter supports data breakpoints.
+  optional<boolean> supportsDataBreakpoints;
+  // The debug adapter supports the delayed loading of parts of the stack,
+  // which requires that both the `startFrame` and `levels` arguments and the
+  // `totalFrames` result of the `stackTrace` request are supported.
+  optional<boolean> supportsDelayedStackTraceLoading;
+  // The debug adapter supports the `disassemble` request.
+  optional<boolean> supportsDisassembleRequest;
+  // The debug adapter supports a (side effect free) `evaluate` request for
+  // data hovers.
+  optional<boolean> supportsEvaluateForHovers;
+  // The debug adapter supports `filterOptions` as an argument on the
+  // `setExceptionBreakpoints` request.
+  optional<boolean> supportsExceptionFilterOptions;
+  // The debug adapter supports the `exceptionInfo` request.
+  optional<boolean> supportsExceptionInfoRequest;
+  // The debug adapter supports `exceptionOptions` on the
+  // `setExceptionBreakpoints` request.
+  optional<boolean> supportsExceptionOptions;
+  // The debug adapter supports function breakpoints.
+  optional<boolean> supportsFunctionBreakpoints;
+  // The debug adapter supports the `gotoTargets` request.
+  optional<boolean> supportsGotoTargetsRequest;
+  // The debug adapter supports breakpoints that break execution after a
+  // specified number of hits.
+  optional<boolean> supportsHitConditionalBreakpoints;
+  // The debug adapter supports adding breakpoints based on instruction
+  // references.
+  optional<boolean> supportsInstructionBreakpoints;
+  // The debug adapter supports the `loadedSources` request.
+  optional<boolean> supportsLoadedSourcesRequest;
+  // The debug adapter supports log points by interpreting the `logMessage`
+  // attribute of the `SourceBreakpoint`.
+  optional<boolean> supportsLogPoints;
+  // The debug adapter supports the `modules` request.
+  optional<boolean> supportsModulesRequest;
+  // The debug adapter supports the `readMemory` request.
+  optional<boolean> supportsReadMemoryRequest;
+  // The debug adapter supports restarting a frame.
+  optional<boolean> supportsRestartFrame;
+  // The debug adapter supports the `restart` request. In this case a client
+  // should not implement `restart` by terminating and relaunching the adapter
+  // but by calling the `restart` request.
+  optional<boolean> supportsRestartRequest;
+  // The debug adapter supports the `setExpression` request.
+  optional<boolean> supportsSetExpression;
+  // The debug adapter supports setting a variable to a value.
+  optional<boolean> supportsSetVariable;
+  // The debug adapter supports the `singleThread` property on the execution
+  // requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`,
+  // `stepBack`).
+  optional<boolean> supportsSingleThreadExecutionRequests;
+  // The debug adapter supports stepping back via the `stepBack` and
+  // `reverseContinue` requests.
+  optional<boolean> supportsStepBack;
+  // The debug adapter supports the `stepInTargets` request.
+  optional<boolean> supportsStepInTargetsRequest;
+  // The debug adapter supports stepping granularities (argument `granularity`)
+  // for the stepping requests.
+  optional<boolean> supportsSteppingGranularity;
+  // The debug adapter supports the `terminate` request.
+  optional<boolean> supportsTerminateRequest;
+  // The debug adapter supports the `terminateThreads` request.
+  optional<boolean> supportsTerminateThreadsRequest;
+  // The debug adapter supports a `format` attribute on the `stackTrace`,
+  // `variables`, and `evaluate` requests.
+  optional<boolean> supportsValueFormattingOptions;
+  // The debug adapter supports the `writeMemory` request.
+  optional<boolean> supportsWriteMemoryRequest;
+  // The CMake version.
+  CMakeVersion cmakeVersion;
+};
+
+DAP_DECLARE_STRUCT_TYPEINFO(CMakeInitializeResponse);
+
+// The `initialize` request is sent as the first request from the client to the
+// debug adapter in order to configure it with client capabilities and to
+// retrieve capabilities from the debug adapter. Until the debug adapter has
+// responded with an `initialize` response, the client must not send any
+// additional requests or events to the debug adapter. In addition the debug
+// adapter is not allowed to send any requests or events to the client until it
+// has responded with an `initialize` response. The `initialize` request may
+// only be sent once.
+struct CMakeInitializeRequest : public Request
+{
+  using Response = CMakeInitializeResponse;
+  // The ID of the debug adapter.
+  string adapterID;
+  // The ID of the client using this adapter.
+  optional<string> clientID;
+  // The human-readable name of the client using this adapter.
+  optional<string> clientName;
+  // If true all column numbers are 1-based (default).
+  optional<boolean> columnsStartAt1;
+  // If true all line numbers are 1-based (default).
+  optional<boolean> linesStartAt1;
+  // The ISO-639 locale of the client using this adapter, e.g. en-US or de-CH.
+  optional<string> locale;
+  // Determines in what format paths are specified. The default is `path`,
+  // which is the native format.
+  //
+  // May be one of the following enumeration values:
+  // 'path', 'uri'
+  optional<string> pathFormat;
+  // Client supports the `argsCanBeInterpretedByShell` attribute on the
+  // `runInTerminal` request.
+  optional<boolean> supportsArgsCanBeInterpretedByShell;
+  // Client supports the `invalidated` event.
+  optional<boolean> supportsInvalidatedEvent;
+  // Client supports the `memory` event.
+  optional<boolean> supportsMemoryEvent;
+  // Client supports memory references.
+  optional<boolean> supportsMemoryReferences;
+  // Client supports progress reporting.
+  optional<boolean> supportsProgressReporting;
+  // Client supports the `runInTerminal` request.
+  optional<boolean> supportsRunInTerminalRequest;
+  // Client supports the `startDebugging` request.
+  optional<boolean> supportsStartDebuggingRequest;
+  // Client supports the paging of variables.
+  optional<boolean> supportsVariablePaging;
+  // Client supports the `type` attribute for variables.
+  optional<boolean> supportsVariableType;
+};
+
+DAP_DECLARE_STRUCT_TYPEINFO(CMakeInitializeRequest);
+
+} // namespace dap
diff --git a/Source/cmDebuggerSourceBreakpoint.cxx b/Source/cmDebuggerSourceBreakpoint.cxx
new file mode 100644
index 0000000..d4665e6
--- /dev/null
+++ b/Source/cmDebuggerSourceBreakpoint.cxx
@@ -0,0 +1,14 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmDebuggerSourceBreakpoint.h"
+
+namespace cmDebugger {
+
+cmDebuggerSourceBreakpoint::cmDebuggerSourceBreakpoint(int64_t id,
+                                                       int64_t line)
+  : Id(id)
+  , Line(line)
+{
+}
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerSourceBreakpoint.h b/Source/cmDebuggerSourceBreakpoint.h
new file mode 100644
index 0000000..f6d6cac
--- /dev/null
+++ b/Source/cmDebuggerSourceBreakpoint.h
@@ -0,0 +1,26 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <cstdint>
+
+namespace cmDebugger {
+
+class cmDebuggerSourceBreakpoint
+{
+  int64_t Id;
+  int64_t Line;
+  bool IsValid = true;
+
+public:
+  cmDebuggerSourceBreakpoint(int64_t id, int64_t line);
+  int64_t GetId() const noexcept { return this->Id; }
+  int64_t GetLine() const noexcept { return this->Line; }
+  void ChangeLine(int64_t line) noexcept { this->Line = line; }
+  bool GetIsValid() const noexcept { return this->IsValid; }
+  void Invalid() noexcept { this->IsValid = false; }
+};
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerStackFrame.cxx b/Source/cmDebuggerStackFrame.cxx
new file mode 100644
index 0000000..789b0a5
--- /dev/null
+++ b/Source/cmDebuggerStackFrame.cxx
@@ -0,0 +1,28 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmDebuggerStackFrame.h"
+
+#include <utility>
+
+#include "cmListFileCache.h"
+
+namespace cmDebugger {
+
+std::atomic<int64_t> cmDebuggerStackFrame::NextId(1);
+
+cmDebuggerStackFrame::cmDebuggerStackFrame(cmMakefile* mf,
+                                           std::string sourcePath,
+                                           cmListFileFunction const& lff)
+  : Id(NextId.fetch_add(1))
+  , FileName(std::move(sourcePath))
+  , Function(lff)
+  , Makefile(mf)
+{
+}
+
+int64_t cmDebuggerStackFrame::GetLine() const noexcept
+{
+  return this->Function.Line();
+}
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerStackFrame.h b/Source/cmDebuggerStackFrame.h
new file mode 100644
index 0000000..dc3b2ab
--- /dev/null
+++ b/Source/cmDebuggerStackFrame.h
@@ -0,0 +1,33 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <atomic>
+#include <cstdint>
+#include <string>
+
+class cmListFileFunction;
+class cmMakefile;
+
+namespace cmDebugger {
+
+class cmDebuggerStackFrame
+{
+  static std::atomic<std::int64_t> NextId;
+  std::int64_t Id;
+  std::string FileName;
+  cmListFileFunction const& Function;
+  cmMakefile* Makefile;
+
+public:
+  cmDebuggerStackFrame(cmMakefile* mf, std::string sourcePath,
+                       cmListFileFunction const& lff);
+  int64_t GetId() const noexcept { return this->Id; }
+  std::string const& GetFileName() const noexcept { return this->FileName; }
+  int64_t GetLine() const noexcept;
+  cmMakefile* GetMakefile() const noexcept { return this->Makefile; }
+};
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerThread.cxx b/Source/cmDebuggerThread.cxx
new file mode 100644
index 0000000..fd52f5a
--- /dev/null
+++ b/Source/cmDebuggerThread.cxx
@@ -0,0 +1,150 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmDebuggerThread.h"
+
+#include <cstdint>
+#include <utility>
+
+#include <cm3p/cppdap/optional.h>
+#include <cm3p/cppdap/types.h>
+
+#include "cmDebuggerStackFrame.h"
+#include "cmDebuggerVariables.h"
+#include "cmDebuggerVariablesHelper.h"
+#include "cmDebuggerVariablesManager.h"
+
+namespace cmDebugger {
+
+cmDebuggerThread::cmDebuggerThread(int64_t id, std::string name)
+  : Id(id)
+  , Name(std::move(name))
+  , VariablesManager(std::make_shared<cmDebuggerVariablesManager>())
+{
+}
+
+void cmDebuggerThread::PushStackFrame(cmMakefile* mf,
+                                      std::string const& sourcePath,
+                                      cmListFileFunction const& lff)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  Frames.emplace_back(
+    std::make_shared<cmDebuggerStackFrame>(mf, sourcePath, lff));
+  FrameMap.insert({ Frames.back()->GetId(), Frames.back() });
+}
+
+void cmDebuggerThread::PopStackFrame()
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  FrameMap.erase(Frames.back()->GetId());
+  FrameScopes.erase(Frames.back()->GetId());
+  FrameVariables.erase(Frames.back()->GetId());
+  Frames.pop_back();
+}
+
+std::shared_ptr<cmDebuggerStackFrame> cmDebuggerThread::GetTopStackFrame()
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  if (!Frames.empty()) {
+    return Frames.back();
+  }
+
+  return {};
+}
+
+std::shared_ptr<cmDebuggerStackFrame> cmDebuggerThread::GetStackFrame(
+  int64_t frameId)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  auto it = FrameMap.find(frameId);
+
+  if (it == FrameMap.end()) {
+    return {};
+  }
+
+  return it->second;
+}
+
+dap::ScopesResponse cmDebuggerThread::GetScopesResponse(
+  int64_t frameId, bool supportsVariableType)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  auto it = FrameScopes.find(frameId);
+
+  if (it != FrameScopes.end()) {
+    dap::ScopesResponse response;
+    response.scopes = it->second;
+    return response;
+  }
+
+  auto it2 = FrameMap.find(frameId);
+  if (it2 == FrameMap.end()) {
+    return dap::ScopesResponse();
+  }
+
+  std::shared_ptr<cmDebuggerStackFrame> frame = it2->second;
+  std::shared_ptr<cmDebuggerVariables> localVariables =
+    cmDebuggerVariablesHelper::Create(VariablesManager, "Locals",
+                                      supportsVariableType, frame);
+
+  FrameVariables[frameId].emplace_back(localVariables);
+
+  dap::Scope scope;
+  scope.name = localVariables->GetName();
+  scope.presentationHint = "locals";
+  scope.variablesReference = localVariables->GetId();
+
+  dap::Source source;
+  source.name = frame->GetFileName();
+  source.path = source.name;
+  scope.source = source;
+
+  FrameScopes[frameId].push_back(scope);
+
+  dap::ScopesResponse response;
+  response.scopes.push_back(scope);
+  return response;
+}
+
+dap::VariablesResponse cmDebuggerThread::GetVariablesResponse(
+  dap::VariablesRequest const& request)
+{
+  std::unique_lock<std::mutex> lock(Mutex);
+  dap::VariablesResponse response;
+  response.variables = VariablesManager->HandleVariablesRequest(request);
+  return response;
+}
+
+dap::StackTraceResponse GetStackTraceResponse(
+  std::shared_ptr<cmDebuggerThread> const& thread)
+{
+  dap::StackTraceResponse response;
+  std::unique_lock<std::mutex> lock(thread->Mutex);
+  for (int i = static_cast<int>(thread->Frames.size()) - 1; i >= 0; --i) {
+    dap::Source source;
+    source.name = thread->Frames[i]->GetFileName();
+    source.path = source.name;
+
+#ifdef __GNUC__
+#  pragma GCC diagnostic push
+#  pragma GCC diagnostic ignored "-Warray-bounds"
+#endif
+    dap::StackFrame stackFrame;
+#ifdef __GNUC__
+#  pragma GCC diagnostic pop
+#endif
+    stackFrame.line = thread->Frames[i]->GetLine();
+    stackFrame.column = 1;
+    stackFrame.name = thread->Frames[i]->GetFileName() + " Line " +
+      std::to_string(stackFrame.line);
+    stackFrame.id = thread->Frames[i]->GetId();
+    stackFrame.source = source;
+
+    response.stackFrames.push_back(stackFrame);
+  }
+
+  response.totalFrames = response.stackFrames.size();
+  return response;
+}
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerThread.h b/Source/cmDebuggerThread.h
new file mode 100644
index 0000000..65ee2cf
--- /dev/null
+++ b/Source/cmDebuggerThread.h
@@ -0,0 +1,59 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <cm3p/cppdap/protocol.h>
+
+class cmListFileFunction;
+class cmMakefile;
+
+namespace cmDebugger {
+class cmDebuggerStackFrame;
+class cmDebuggerVariables;
+class cmDebuggerVariablesManager;
+}
+
+namespace cmDebugger {
+
+class cmDebuggerThread
+{
+  int64_t Id;
+  std::string Name;
+  std::vector<std::shared_ptr<cmDebuggerStackFrame>> Frames;
+  std::unordered_map<int64_t, std::shared_ptr<cmDebuggerStackFrame>> FrameMap;
+  std::mutex Mutex;
+  std::unordered_map<int64_t, std::vector<dap::Scope>> FrameScopes;
+  std::unordered_map<int64_t,
+                     std::vector<std::shared_ptr<cmDebuggerVariables>>>
+    FrameVariables;
+  std::shared_ptr<cmDebuggerVariablesManager> VariablesManager;
+
+public:
+  cmDebuggerThread(int64_t id, std::string name);
+  int64_t GetId() const { return this->Id; }
+  const std::string& GetName() const { return this->Name; }
+  void PushStackFrame(cmMakefile* mf, std::string const& sourcePath,
+                      cmListFileFunction const& lff);
+  void PopStackFrame();
+  std::shared_ptr<cmDebuggerStackFrame> GetTopStackFrame();
+  std::shared_ptr<cmDebuggerStackFrame> GetStackFrame(int64_t frameId);
+  size_t GetStackFrameSize() const { return this->Frames.size(); }
+  dap::ScopesResponse GetScopesResponse(int64_t frameId,
+                                        bool supportsVariableType);
+  dap::VariablesResponse GetVariablesResponse(
+    dap::VariablesRequest const& request);
+  friend dap::StackTraceResponse GetStackTraceResponse(
+    std::shared_ptr<cmDebuggerThread> const& thread);
+};
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerThreadManager.cxx b/Source/cmDebuggerThreadManager.cxx
new file mode 100644
index 0000000..0eb443b
--- /dev/null
+++ b/Source/cmDebuggerThreadManager.cxx
@@ -0,0 +1,47 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmDebuggerThreadManager.h"
+
+#include <algorithm>
+
+#include <cm3p/cppdap/protocol.h>
+
+#include "cmDebuggerThread.h"
+
+namespace cmDebugger {
+
+std::atomic<int64_t> cmDebuggerThreadManager::NextThreadId(1);
+
+std::shared_ptr<cmDebuggerThread> cmDebuggerThreadManager::StartThread(
+  std::string const& name)
+{
+  std::shared_ptr<cmDebuggerThread> thread =
+    std::make_shared<cmDebuggerThread>(
+      cmDebuggerThreadManager::NextThreadId.fetch_add(1), name);
+  Threads.emplace_back(thread);
+  return thread;
+}
+
+void cmDebuggerThreadManager::EndThread(
+  std::shared_ptr<cmDebuggerThread> const& thread)
+{
+  Threads.remove(thread);
+}
+
+cm::optional<dap::StackTraceResponse>
+cmDebuggerThreadManager::GetThreadStackTraceResponse(int64_t id)
+{
+  auto it = find_if(Threads.begin(), Threads.end(),
+                    [&](const std::shared_ptr<cmDebuggerThread>& t) {
+                      return t->GetId() == id;
+                    });
+
+  if (it == Threads.end()) {
+    return {};
+  }
+
+  return GetStackTraceResponse(*it);
+}
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerThreadManager.h b/Source/cmDebuggerThreadManager.h
new file mode 100644
index 0000000..934cf85
--- /dev/null
+++ b/Source/cmDebuggerThreadManager.h
@@ -0,0 +1,38 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <atomic>
+#include <cstdint>
+#include <list>
+#include <memory>
+#include <string>
+
+#include <cm/optional>
+
+namespace cmDebugger {
+class cmDebuggerThread;
+}
+
+namespace dap {
+struct StackTraceResponse;
+}
+
+namespace cmDebugger {
+
+class cmDebuggerThreadManager
+{
+  static std::atomic<std::int64_t> NextThreadId;
+  std::list<std::shared_ptr<cmDebuggerThread>> Threads;
+
+public:
+  cmDebuggerThreadManager() = default;
+  std::shared_ptr<cmDebuggerThread> StartThread(std::string const& name);
+  void EndThread(std::shared_ptr<cmDebuggerThread> const& thread);
+  cm::optional<dap::StackTraceResponse> GetThreadStackTraceResponse(
+    std::int64_t id);
+};
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerVariables.cxx b/Source/cmDebuggerVariables.cxx
new file mode 100644
index 0000000..40fe41f
--- /dev/null
+++ b/Source/cmDebuggerVariables.cxx
@@ -0,0 +1,133 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmDebuggerVariables.h"
+
+#include <algorithm>
+#include <vector>
+
+#include <cm3p/cppdap/optional.h>
+#include <cm3p/cppdap/protocol.h>
+#include <cm3p/cppdap/types.h>
+
+#include "cmDebuggerVariablesManager.h"
+
+namespace cmDebugger {
+
+namespace {
+const dap::VariablePresentationHint PrivatePropertyHint = { {},
+                                                            "property",
+                                                            {},
+                                                            "private" };
+const dap::VariablePresentationHint PrivateDataHint = { {},
+                                                        "data",
+                                                        {},
+                                                        "private" };
+}
+
+std::atomic<int64_t> cmDebuggerVariables::NextId(1);
+
+cmDebuggerVariables::cmDebuggerVariables(
+  std::shared_ptr<cmDebuggerVariablesManager> variablesManager,
+  std::string name, bool supportsVariableType)
+  : Id(NextId.fetch_add(1))
+  , Name(std::move(name))
+  , SupportsVariableType(supportsVariableType)
+  , VariablesManager(std::move(variablesManager))
+{
+  VariablesManager->RegisterHandler(
+    Id, [this](dap::VariablesRequest const& request) {
+      (void)request;
+      return this->HandleVariablesRequest();
+    });
+}
+
+cmDebuggerVariables::cmDebuggerVariables(
+  std::shared_ptr<cmDebuggerVariablesManager> variablesManager,
+  std::string name, bool supportsVariableType,
+  std::function<std::vector<cmDebuggerVariableEntry>()> getKeyValuesFunction)
+  : Id(NextId.fetch_add(1))
+  , Name(std::move(name))
+  , GetKeyValuesFunction(std::move(getKeyValuesFunction))
+  , SupportsVariableType(supportsVariableType)
+  , VariablesManager(std::move(variablesManager))
+{
+  VariablesManager->RegisterHandler(
+    Id, [this](dap::VariablesRequest const& request) {
+      (void)request;
+      return this->HandleVariablesRequest();
+    });
+}
+
+void cmDebuggerVariables::AddSubVariables(
+  std::shared_ptr<cmDebuggerVariables> const& variables)
+{
+  if (variables != nullptr) {
+    SubVariables.emplace_back(variables);
+  }
+}
+
+dap::array<dap::Variable> cmDebuggerVariables::HandleVariablesRequest()
+{
+  dap::array<dap::Variable> variables;
+
+  if (GetKeyValuesFunction != nullptr) {
+    auto values = GetKeyValuesFunction();
+    for (auto const& entry : values) {
+      if (IgnoreEmptyStringEntries && entry.Type == "string" &&
+          entry.Value.empty()) {
+        continue;
+      }
+      variables.push_back(dap::Variable{ {},
+                                         {},
+                                         {},
+                                         entry.Name,
+                                         {},
+                                         PrivateDataHint,
+                                         entry.Type,
+                                         entry.Value,
+                                         0 });
+    }
+  }
+
+  EnumerateSubVariablesIfAny(variables);
+
+  if (EnableSorting) {
+    std::sort(variables.begin(), variables.end(),
+              [](dap::Variable const& a, dap::Variable const& b) {
+                return a.name < b.name;
+              });
+  }
+  return variables;
+}
+
+void cmDebuggerVariables::EnumerateSubVariablesIfAny(
+  dap::array<dap::Variable>& toBeReturned) const
+{
+  dap::array<dap::Variable> ret;
+  for (auto const& variables : SubVariables) {
+    toBeReturned.emplace_back(
+      dap::Variable{ {},
+                     {},
+                     {},
+                     variables->GetName(),
+                     {},
+                     PrivatePropertyHint,
+                     SupportsVariableType ? "collection" : nullptr,
+                     variables->GetValue(),
+                     variables->GetId() });
+  }
+}
+
+void cmDebuggerVariables::ClearSubVariables()
+{
+  SubVariables.clear();
+}
+
+cmDebuggerVariables::~cmDebuggerVariables()
+{
+  ClearSubVariables();
+  VariablesManager->UnregisterHandler(Id);
+}
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerVariables.h b/Source/cmDebuggerVariables.h
new file mode 100644
index 0000000..eaaf2a8
--- /dev/null
+++ b/Source/cmDebuggerVariables.h
@@ -0,0 +1,124 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <atomic>
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <cm3p/cppdap/types.h> // IWYU pragma: keep
+
+namespace cmDebugger {
+class cmDebuggerVariablesManager;
+}
+
+namespace dap {
+struct Variable;
+}
+
+namespace cmDebugger {
+
+struct cmDebuggerVariableEntry
+{
+  cmDebuggerVariableEntry()
+    : cmDebuggerVariableEntry("", "", "")
+  {
+  }
+  cmDebuggerVariableEntry(std::string name, std::string value,
+                          std::string type)
+    : Name(std::move(name))
+    , Value(std::move(value))
+    , Type(std::move(type))
+  {
+  }
+  cmDebuggerVariableEntry(std::string name, std::string value)
+    : Name(std::move(name))
+    , Value(std::move(value))
+    , Type("string")
+  {
+  }
+  cmDebuggerVariableEntry(std::string name, const char* value)
+    : Name(std::move(name))
+    , Value(value == nullptr ? "" : value)
+    , Type("string")
+  {
+  }
+  cmDebuggerVariableEntry(std::string name, bool value)
+    : Name(std::move(name))
+    , Value(value ? "TRUE" : "FALSE")
+    , Type("bool")
+  {
+  }
+  cmDebuggerVariableEntry(std::string name, int64_t value)
+    : Name(std::move(name))
+    , Value(std::to_string(value))
+    , Type("int")
+  {
+  }
+  cmDebuggerVariableEntry(std::string name, int value)
+    : Name(std::move(name))
+    , Value(std::to_string(value))
+    , Type("int")
+  {
+  }
+  std::string const Name;
+  std::string const Value;
+  std::string const Type;
+};
+
+class cmDebuggerVariables
+{
+  static std::atomic<int64_t> NextId;
+  int64_t Id;
+  std::string Name;
+  std::string Value;
+
+  std::function<std::vector<cmDebuggerVariableEntry>()> GetKeyValuesFunction;
+  std::vector<std::shared_ptr<cmDebuggerVariables>> SubVariables;
+  bool IgnoreEmptyStringEntries = false;
+  bool EnableSorting = true;
+
+  virtual dap::array<dap::Variable> HandleVariablesRequest();
+  friend class cmDebuggerVariablesManager;
+
+protected:
+  const bool SupportsVariableType;
+  std::shared_ptr<cmDebuggerVariablesManager> VariablesManager;
+  void EnumerateSubVariablesIfAny(
+    dap::array<dap::Variable>& toBeReturned) const;
+  void ClearSubVariables();
+
+public:
+  cmDebuggerVariables(
+    std::shared_ptr<cmDebuggerVariablesManager> variablesManager,
+    std::string name, bool supportsVariableType);
+  cmDebuggerVariables(
+    std::shared_ptr<cmDebuggerVariablesManager> variablesManager,
+    std::string name, bool supportsVariableType,
+    std::function<std::vector<cmDebuggerVariableEntry>()> getKeyValuesFunc);
+  inline int64_t GetId() const noexcept { return this->Id; }
+  inline std::string GetName() const noexcept { return this->Name; }
+  inline std::string GetValue() const noexcept { return this->Value; }
+  inline void SetValue(std::string const& value) noexcept
+  {
+    this->Value = value;
+  }
+  void AddSubVariables(std::shared_ptr<cmDebuggerVariables> const& variables);
+  inline void SetIgnoreEmptyStringEntries(bool value) noexcept
+  {
+    this->IgnoreEmptyStringEntries = value;
+  }
+  inline void SetEnableSorting(bool value) noexcept
+  {
+    this->EnableSorting = value;
+  }
+  virtual ~cmDebuggerVariables();
+};
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerVariablesHelper.cxx b/Source/cmDebuggerVariablesHelper.cxx
new file mode 100644
index 0000000..42ce5e7
--- /dev/null
+++ b/Source/cmDebuggerVariablesHelper.cxx
@@ -0,0 +1,644 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmDebuggerVariablesHelper.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <functional>
+#include <iomanip>
+#include <map>
+#include <sstream>
+
+#include "cm_codecvt.hxx"
+
+#include "cmDebuggerStackFrame.h"
+#include "cmDebuggerVariables.h"
+#include "cmFileSet.h"
+#include "cmGlobalGenerator.h"
+#include "cmList.h"
+#include "cmListFileCache.h"
+#include "cmMakefile.h"
+#include "cmPropertyMap.h"
+#include "cmState.h"
+#include "cmStateSnapshot.h"
+#include "cmTarget.h"
+#include "cmTest.h"
+#include "cmValue.h"
+#include "cmake.h"
+
+namespace cmDebugger {
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::Create(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType,
+  cmPolicies::PolicyMap const& policyMap)
+{
+  static std::map<cmPolicies::PolicyStatus, std::string> policyStatusString = {
+    { cmPolicies::PolicyStatus::OLD, "OLD" },
+    { cmPolicies::PolicyStatus::WARN, "WARN" },
+    { cmPolicies::PolicyStatus::NEW, "NEW" },
+    { cmPolicies::PolicyStatus::REQUIRED_IF_USED, "REQUIRED_IF_USED" },
+    { cmPolicies::PolicyStatus::REQUIRED_ALWAYS, "REQUIRED_ALWAYS" }
+  };
+
+  return std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType, [=]() {
+      std::vector<cmDebuggerVariableEntry> ret;
+      ret.reserve(cmPolicies::CMPCOUNT);
+      for (int i = 0; i < cmPolicies::CMPCOUNT; ++i) {
+        if (policyMap.IsDefined(static_cast<cmPolicies::PolicyID>(i))) {
+          auto status = policyMap.Get(static_cast<cmPolicies::PolicyID>(i));
+          std::ostringstream ss;
+          ss << "CMP" << std::setfill('0') << std::setw(4) << i;
+          ret.emplace_back(ss.str(), policyStatusString[status]);
+        }
+      }
+      return ret;
+    });
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType,
+  std::vector<std::pair<std::string, std::string>> const& list)
+{
+  if (list.empty()) {
+    return {};
+  }
+
+  auto listVariables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType, [=]() {
+      std::vector<cmDebuggerVariableEntry> ret;
+      ret.reserve(list.size());
+      for (auto const& kv : list) {
+        ret.emplace_back(kv.first, kv.second);
+      }
+      return ret;
+    });
+
+  listVariables->SetValue(std::to_string(list.size()));
+  return listVariables;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType,
+  cmBTStringRange const& entries)
+{
+  if (entries.empty()) {
+    return {};
+  }
+
+  auto sourceEntries = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType);
+
+  for (auto const& entry : entries) {
+    auto arrayVariables = std::make_shared<cmDebuggerVariables>(
+      variablesManager, entry.Value, supportsVariableType, [=]() {
+        cmList items{ entry.Value };
+        std::vector<cmDebuggerVariableEntry> ret;
+        ret.reserve(items.size());
+        int i = 0;
+        for (std::string const& item : items) {
+          ret.emplace_back("[" + std::to_string(i++) + "]", item);
+        }
+        return ret;
+      });
+    arrayVariables->SetEnableSorting(false);
+    sourceEntries->AddSubVariables(arrayVariables);
+  }
+
+  sourceEntries->SetValue(std::to_string(entries.size()));
+  return sourceEntries;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType,
+  std::set<std::string> const& values)
+{
+  if (values.empty()) {
+    return {};
+  }
+
+  auto arrayVariables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType, [=]() {
+      std::vector<cmDebuggerVariableEntry> ret;
+      ret.reserve(values.size());
+      int i = 0;
+      for (std::string const& value : values) {
+        ret.emplace_back("[" + std::to_string(i++) + "]", value);
+      }
+      return ret;
+    });
+  arrayVariables->SetValue(std::to_string(values.size()));
+  arrayVariables->SetEnableSorting(false);
+  return arrayVariables;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType,
+  std::vector<std::string> const& values)
+{
+  if (values.empty()) {
+    return {};
+  }
+
+  auto arrayVariables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType, [=]() {
+      std::vector<cmDebuggerVariableEntry> ret;
+      ret.reserve(values.size());
+      int i = 0;
+      for (std::string const& value : values) {
+        ret.emplace_back("[" + std::to_string(i++) + "]", value);
+      }
+      return ret;
+    });
+
+  arrayVariables->SetValue(std::to_string(values.size()));
+  arrayVariables->SetEnableSorting(false);
+  return arrayVariables;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType,
+  std::vector<BT<std::string>> const& list)
+{
+  if (list.empty()) {
+    return {};
+  }
+
+  auto variables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType, [=]() {
+      std::vector<cmDebuggerVariableEntry> ret;
+      ret.reserve(list.size());
+      int i = 0;
+      for (auto const& item : list) {
+        ret.emplace_back("[" + std::to_string(i++) + "]", item.Value);
+      }
+
+      return ret;
+    });
+
+  variables->SetValue(std::to_string(list.size()));
+  variables->SetEnableSorting(false);
+  return variables;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType, cmFileSet* fileSet)
+{
+  if (fileSet == nullptr) {
+    return {};
+  }
+
+  static auto visibilityString = [](cmFileSetVisibility visibility) {
+    switch (visibility) {
+      case cmFileSetVisibility::Private:
+        return "Private";
+      case cmFileSetVisibility::Public:
+        return "Public";
+      case cmFileSetVisibility::Interface:
+        return "Interface";
+      default:
+        return "Unknown";
+    }
+  };
+
+  auto variables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType, [=]() {
+      std::vector<cmDebuggerVariableEntry> ret{
+        { "Name", fileSet->GetName() },
+        { "Type", fileSet->GetType() },
+        { "Visibility", visibilityString(fileSet->GetVisibility()) },
+      };
+
+      return ret;
+    });
+
+  variables->AddSubVariables(CreateIfAny(variablesManager, "Directories",
+                                         supportsVariableType,
+                                         fileSet->GetDirectoryEntries()));
+  variables->AddSubVariables(CreateIfAny(variablesManager, "Files",
+                                         supportsVariableType,
+                                         fileSet->GetFileEntries()));
+  return variables;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType,
+  std::vector<cmFileSet*> const& fileSets)
+{
+  if (fileSets.empty()) {
+    return {};
+  }
+
+  auto fileSetsVariables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType);
+
+  for (auto const& fileSet : fileSets) {
+    fileSetsVariables->AddSubVariables(CreateIfAny(
+      variablesManager, fileSet->GetName(), supportsVariableType, fileSet));
+  }
+
+  return fileSetsVariables;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType,
+  std::vector<cmTarget*> const& targets)
+{
+  if (targets.empty()) {
+    return {};
+  }
+
+  auto targetsVariables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType);
+
+  for (auto const& target : targets) {
+    auto targetVariables = std::make_shared<cmDebuggerVariables>(
+      variablesManager, target->GetName(), supportsVariableType, [=]() {
+        std::vector<cmDebuggerVariableEntry> ret = {
+          { "InstallPath", target->GetInstallPath() },
+          { "IsAIX", target->IsAIX() },
+          { "IsAndroidGuiExecutable", target->IsAndroidGuiExecutable() },
+          { "IsAppBundleOnApple", target->IsAppBundleOnApple() },
+          { "IsDLLPlatform", target->IsDLLPlatform() },
+          { "IsExecutableWithExports", target->IsExecutableWithExports() },
+          { "IsFrameworkOnApple", target->IsFrameworkOnApple() },
+          { "IsImported", target->IsImported() },
+          { "IsImportedGloballyVisible", target->IsImportedGloballyVisible() },
+          { "IsPerConfig", target->IsPerConfig() },
+          { "Name", target->GetName() },
+          { "RuntimeInstallPath", target->GetRuntimeInstallPath() },
+          { "Type", cmState::GetTargetTypeName(target->GetType()) },
+        };
+
+        return ret;
+      });
+    targetVariables->SetValue(cmState::GetTargetTypeName(target->GetType()));
+
+    targetVariables->AddSubVariables(Create(variablesManager, "PolicyMap",
+                                            supportsVariableType,
+                                            target->GetPolicyMap()));
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "Properties", supportsVariableType,
+                  target->GetProperties().GetList()));
+
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "IncludeDirectories", supportsVariableType,
+                  target->GetIncludeDirectoriesEntries()));
+    targetVariables->AddSubVariables(CreateIfAny(variablesManager, "Sources",
+                                                 supportsVariableType,
+                                                 target->GetSourceEntries()));
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "CompileDefinitions", supportsVariableType,
+                  target->GetCompileDefinitionsEntries()));
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "CompileFeatures", supportsVariableType,
+                  target->GetCompileFeaturesEntries()));
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "CompileOptions", supportsVariableType,
+                  target->GetCompileOptionsEntries()));
+    targetVariables->AddSubVariables(CreateIfAny(
+      variablesManager, "CxxModuleHeaderSets", supportsVariableType,
+      target->GetCxxModuleHeaderSetsEntries()));
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "CxxModuleSets", supportsVariableType,
+                  target->GetCxxModuleSetsEntries()));
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "HeaderSets", supportsVariableType,
+                  target->GetHeaderSetsEntries()));
+    targetVariables->AddSubVariables(CreateIfAny(
+      variablesManager, "InterfaceCxxModuleHeaderSets", supportsVariableType,
+      target->GetInterfaceCxxModuleHeaderSetsEntries()));
+    targetVariables->AddSubVariables(CreateIfAny(
+      variablesManager, "InterfaceHeaderSets", supportsVariableType,
+      target->GetInterfaceHeaderSetsEntries()));
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "LinkDirectories", supportsVariableType,
+                  target->GetLinkDirectoriesEntries()));
+    targetVariables->AddSubVariables(CreateIfAny(
+      variablesManager, "LinkImplementations", supportsVariableType,
+      target->GetLinkImplementationEntries()));
+    targetVariables->AddSubVariables(CreateIfAny(
+      variablesManager, "LinkInterfaceDirects", supportsVariableType,
+      target->GetLinkInterfaceDirectEntries()));
+    targetVariables->AddSubVariables(CreateIfAny(
+      variablesManager, "LinkInterfaceDirectExcludes", supportsVariableType,
+      target->GetLinkInterfaceDirectExcludeEntries()));
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "LinkInterfaces", supportsVariableType,
+                  target->GetLinkInterfaceEntries()));
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "LinkOptions", supportsVariableType,
+                  target->GetLinkOptionsEntries()));
+    targetVariables->AddSubVariables(CreateIfAny(
+      variablesManager, "SystemIncludeDirectories", supportsVariableType,
+      target->GetSystemIncludeDirectories()));
+    targetVariables->AddSubVariables(CreateIfAny(variablesManager, "Makefile",
+                                                 supportsVariableType,
+                                                 target->GetMakefile()));
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "GlobalGenerator", supportsVariableType,
+                  target->GetGlobalGenerator()));
+
+    std::vector<cmFileSet*> allFileSets;
+    auto allFileSetNames = target->GetAllFileSetNames();
+    allFileSets.reserve(allFileSetNames.size());
+    for (auto const& fileSetName : allFileSetNames) {
+      allFileSets.emplace_back(target->GetFileSet(fileSetName));
+    }
+    targetVariables->AddSubVariables(CreateIfAny(
+      variablesManager, "AllFileSets", supportsVariableType, allFileSets));
+
+    std::vector<cmFileSet*> allInterfaceFileSets;
+    auto allInterfaceFileSetNames = target->GetAllInterfaceFileSets();
+    allInterfaceFileSets.reserve(allInterfaceFileSetNames.size());
+    for (auto const& interfaceFileSetName : allInterfaceFileSetNames) {
+      allInterfaceFileSets.emplace_back(
+        target->GetFileSet(interfaceFileSetName));
+    }
+    targetVariables->AddSubVariables(
+      CreateIfAny(variablesManager, "AllInterfaceFileSets",
+                  supportsVariableType, allInterfaceFileSets));
+
+    targetVariables->SetIgnoreEmptyStringEntries(true);
+    targetsVariables->AddSubVariables(targetVariables);
+  }
+
+  targetsVariables->SetValue(std::to_string(targets.size()));
+  return targetsVariables;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::Create(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType,
+  std::shared_ptr<cmDebuggerStackFrame> const& frame)
+{
+  auto variables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType, [=]() {
+      return std::vector<cmDebuggerVariableEntry>{ { "CurrentLine",
+                                                     frame->GetLine() } };
+    });
+
+  auto closureKeys = frame->GetMakefile()->GetStateSnapshot().ClosureKeys();
+  auto locals = std::make_shared<cmDebuggerVariables>(
+    variablesManager, "Locals", supportsVariableType, [=]() {
+      std::vector<cmDebuggerVariableEntry> ret;
+      ret.reserve(closureKeys.size());
+      for (auto const& key : closureKeys) {
+        ret.emplace_back(
+          key, frame->GetMakefile()->GetStateSnapshot().GetDefinition(key));
+      }
+      return ret;
+    });
+  locals->SetValue(std::to_string(closureKeys.size()));
+  variables->AddSubVariables(locals);
+
+  std::function<bool(std::string const&)> isDirectory =
+    [](std::string const& key) {
+      size_t pos1 = key.rfind("_DIR");
+      size_t pos2 = key.rfind("_DIRECTORY");
+      return !((pos1 == std::string::npos || pos1 != key.size() - 4) &&
+               (pos2 == std::string::npos || pos2 != key.size() - 10));
+    };
+  auto directorySize =
+    std::count_if(closureKeys.begin(), closureKeys.end(), isDirectory);
+  auto directories = std::make_shared<cmDebuggerVariables>(
+    variablesManager, "Directories", supportsVariableType, [=]() {
+      std::vector<cmDebuggerVariableEntry> ret;
+      ret.reserve(directorySize);
+      for (auto const& key : closureKeys) {
+        if (isDirectory(key)) {
+          ret.emplace_back(
+            key, frame->GetMakefile()->GetStateSnapshot().GetDefinition(key));
+        }
+      }
+      return ret;
+    });
+  directories->SetValue(std::to_string(directorySize));
+  variables->AddSubVariables(directories);
+
+  auto cacheVariables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, "CacheVariables", supportsVariableType);
+  auto* state = frame->GetMakefile()->GetCMakeInstance()->GetState();
+  auto keys = state->GetCacheEntryKeys();
+  for (auto const& key : keys) {
+    auto entry = std::make_shared<cmDebuggerVariables>(
+      variablesManager,
+      key + ":" +
+        cmState::CacheEntryTypeToString(state->GetCacheEntryType(key)),
+      supportsVariableType, [=]() {
+        std::vector<cmDebuggerVariableEntry> ret;
+        auto properties = state->GetCacheEntryPropertyList(key);
+        ret.reserve(properties.size() + 2);
+        for (auto const& propertyName : properties) {
+          ret.emplace_back(propertyName,
+                           state->GetCacheEntryProperty(key, propertyName));
+        }
+
+        ret.emplace_back(
+          "TYPE",
+          cmState::CacheEntryTypeToString(state->GetCacheEntryType(key)));
+        ret.emplace_back("VALUE", state->GetCacheEntryValue(key));
+        return ret;
+      });
+
+    entry->SetValue(state->GetCacheEntryValue(key));
+    cacheVariables->AddSubVariables(entry);
+  }
+
+  cacheVariables->SetValue(std::to_string(keys.size()));
+  variables->AddSubVariables(cacheVariables);
+
+  auto targetVariables =
+    CreateIfAny(variablesManager, "Targets", supportsVariableType,
+                frame->GetMakefile()->GetOrderedTargets());
+
+  variables->AddSubVariables(targetVariables);
+  std::vector<cmTest*> tests;
+  frame->GetMakefile()->GetTests(
+    frame->GetMakefile()->GetDefaultConfiguration(), tests);
+  variables->AddSubVariables(
+    CreateIfAny(variablesManager, "Tests", supportsVariableType, tests));
+
+  return variables;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType, cmTest* test)
+{
+  if (test == nullptr) {
+    return {};
+  }
+
+  auto variables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType, [=]() {
+      std::vector<cmDebuggerVariableEntry> ret{
+        { "CommandExpandLists", test->GetCommandExpandLists() },
+        { "Name", test->GetName() },
+        { "OldStyle", test->GetOldStyle() },
+      };
+
+      return ret;
+    });
+
+  variables->AddSubVariables(CreateIfAny(
+    variablesManager, "Command", supportsVariableType, test->GetCommand()));
+
+  variables->AddSubVariables(CreateIfAny(variablesManager, "Properties",
+                                         supportsVariableType,
+                                         test->GetProperties().GetList()));
+  return variables;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType,
+  std::vector<cmTest*> const& tests)
+{
+  if (tests.empty()) {
+    return {};
+  }
+
+  auto variables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType);
+
+  for (auto const& test : tests) {
+    variables->AddSubVariables(CreateIfAny(variablesManager, test->GetName(),
+                                           supportsVariableType, test));
+  }
+  variables->SetValue(std::to_string(tests.size()));
+  return variables;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType, cmMakefile* mf)
+{
+  if (mf == nullptr) {
+    return {};
+  }
+
+  auto AppleSDKTypeString = [&](cmMakefile::AppleSDK sdk) {
+    switch (sdk) {
+      case cmMakefile::AppleSDK::MacOS:
+        return "MacOS";
+      case cmMakefile::AppleSDK::IPhoneOS:
+        return "IPhoneOS";
+      case cmMakefile::AppleSDK::IPhoneSimulator:
+        return "IPhoneSimulator";
+      case cmMakefile::AppleSDK::AppleTVOS:
+        return "AppleTVOS";
+      case cmMakefile::AppleSDK::AppleTVSimulator:
+        return "AppleTVSimulator";
+      default:
+        return "Unknown";
+    }
+  };
+
+  auto variables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType, [=]() {
+      std::vector<cmDebuggerVariableEntry> ret = {
+        { "DefineFlags", mf->GetDefineFlags() },
+        { "DirectoryId", mf->GetDirectoryId().String },
+        { "IsRootMakefile", mf->IsRootMakefile() },
+        { "HomeDirectory", mf->GetHomeDirectory() },
+        { "HomeOutputDirectory", mf->GetHomeOutputDirectory() },
+        { "CurrentSourceDirectory", mf->GetCurrentSourceDirectory() },
+        { "CurrentBinaryDirectory", mf->GetCurrentBinaryDirectory() },
+        { "PlatformIs32Bit", mf->PlatformIs32Bit() },
+        { "PlatformIs64Bit", mf->PlatformIs64Bit() },
+        { "PlatformIsx32", mf->PlatformIsx32() },
+        { "AppleSDKType", AppleSDKTypeString(mf->GetAppleSDKType()) },
+        { "PlatformIsAppleEmbedded", mf->PlatformIsAppleEmbedded() }
+      };
+
+      return ret;
+    });
+
+  variables->AddSubVariables(CreateIfAny(
+    variablesManager, "ListFiles", supportsVariableType, mf->GetListFiles()));
+  variables->AddSubVariables(CreateIfAny(variablesManager, "OutputFiles",
+                                         supportsVariableType,
+                                         mf->GetOutputFiles()));
+
+  variables->SetIgnoreEmptyStringEntries(true);
+  variables->SetValue(mf->GetDirectoryId().String);
+  return variables;
+}
+
+std::shared_ptr<cmDebuggerVariables> cmDebuggerVariablesHelper::CreateIfAny(
+  std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+  std::string const& name, bool supportsVariableType, cmGlobalGenerator* gen)
+{
+  if (gen == nullptr) {
+    return {};
+  }
+
+  auto makeFileEncodingString = [](codecvt::Encoding encoding) {
+    switch (encoding) {
+      case codecvt::Encoding::None:
+        return "None";
+      case codecvt::Encoding::UTF8:
+        return "UTF8";
+      case codecvt::Encoding::UTF8_WITH_BOM:
+        return "UTF8_WITH_BOM";
+      case codecvt::Encoding::ANSI:
+        return "ANSI";
+      case codecvt::Encoding::ConsoleOutput:
+        return "ConsoleOutput";
+      default:
+        return "Unknown";
+    }
+  };
+
+  auto variables = std::make_shared<cmDebuggerVariables>(
+    variablesManager, name, supportsVariableType, [=]() {
+      std::vector<cmDebuggerVariableEntry> ret = {
+        { "AllTargetName", gen->GetAllTargetName() },
+        { "CleanTargetName", gen->GetCleanTargetName() },
+        { "EditCacheCommand", gen->GetEditCacheCommand() },
+        { "EditCacheTargetName", gen->GetEditCacheTargetName() },
+        { "ExtraGeneratorName", gen->GetExtraGeneratorName() },
+        { "ForceUnixPaths", gen->GetForceUnixPaths() },
+        { "InstallLocalTargetName", gen->GetInstallLocalTargetName() },
+        { "InstallStripTargetName", gen->GetInstallStripTargetName() },
+        { "InstallTargetName", gen->GetInstallTargetName() },
+        { "IsMultiConfig", gen->IsMultiConfig() },
+        { "Name", gen->GetName() },
+        { "MakefileEncoding",
+          makeFileEncodingString(gen->GetMakefileEncoding()) },
+        { "PackageSourceTargetName", gen->GetPackageSourceTargetName() },
+        { "PackageTargetName", gen->GetPackageTargetName() },
+        { "PreinstallTargetName", gen->GetPreinstallTargetName() },
+        { "NeedSymbolicMark", gen->GetNeedSymbolicMark() },
+        { "RebuildCacheTargetName", gen->GetRebuildCacheTargetName() },
+        { "TestTargetName", gen->GetTestTargetName() },
+        { "UseLinkScript", gen->GetUseLinkScript() },
+      };
+
+      return ret;
+    });
+
+  if (gen->GetInstallComponents() != nullptr) {
+    variables->AddSubVariables(
+      CreateIfAny(variablesManager, "InstallComponents", supportsVariableType,
+                  *gen->GetInstallComponents()));
+  }
+
+  variables->SetIgnoreEmptyStringEntries(true);
+  variables->SetValue(gen->GetName());
+
+  return variables;
+}
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerVariablesHelper.h b/Source/cmDebuggerVariablesHelper.h
new file mode 100644
index 0000000..9b11eaf
--- /dev/null
+++ b/Source/cmDebuggerVariablesHelper.h
@@ -0,0 +1,106 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "cmAlgorithms.h"
+#include "cmPolicies.h"
+
+class cmFileSet;
+class cmGlobalGenerator;
+class cmMakefile;
+class cmTarget;
+class cmTest;
+
+namespace cmDebugger {
+class cmDebuggerStackFrame;
+class cmDebuggerVariables;
+class cmDebuggerVariablesManager;
+}
+
+template <typename T>
+class BT;
+
+namespace cmDebugger {
+
+class cmDebuggerVariablesHelper
+{
+  cmDebuggerVariablesHelper() = default;
+
+public:
+  static std::shared_ptr<cmDebuggerVariables> Create(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType,
+    cmPolicies::PolicyMap const& policyMap);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType,
+    std::vector<std::pair<std::string, std::string>> const& list);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType,
+    cmBTStringRange const& entries);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType,
+    std::set<std::string> const& values);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType,
+    std::vector<std::string> const& values);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType,
+    std::vector<BT<std::string>> const& list);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType, cmFileSet* fileSet);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType,
+    std::vector<cmFileSet*> const& fileSets);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType, cmTest* test);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType,
+    std::vector<cmTest*> const& tests);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType,
+    std::vector<cmTarget*> const& targets);
+
+  static std::shared_ptr<cmDebuggerVariables> Create(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType,
+    std::shared_ptr<cmDebuggerStackFrame> const& frame);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType, cmMakefile* mf);
+
+  static std::shared_ptr<cmDebuggerVariables> CreateIfAny(
+    std::shared_ptr<cmDebuggerVariablesManager> const& variablesManager,
+    std::string const& name, bool supportsVariableType,
+    cmGlobalGenerator* gen);
+};
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerVariablesManager.cxx b/Source/cmDebuggerVariablesManager.cxx
new file mode 100644
index 0000000..9b9b476
--- /dev/null
+++ b/Source/cmDebuggerVariablesManager.cxx
@@ -0,0 +1,38 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmDebuggerVariablesManager.h"
+
+#include <utility>
+
+#include <cm3p/cppdap/protocol.h>
+#include <cm3p/cppdap/types.h>
+
+namespace cmDebugger {
+
+void cmDebuggerVariablesManager::RegisterHandler(
+  int64_t id,
+  std::function<dap::array<dap::Variable>(dap::VariablesRequest const&)>
+    handler)
+{
+  VariablesHandlers[id] = std::move(handler);
+}
+
+void cmDebuggerVariablesManager::UnregisterHandler(int64_t id)
+{
+  VariablesHandlers.erase(id);
+}
+
+dap::array<dap::Variable> cmDebuggerVariablesManager::HandleVariablesRequest(
+  dap::VariablesRequest const& request)
+{
+  auto it = VariablesHandlers.find(request.variablesReference);
+
+  if (it != VariablesHandlers.end()) {
+    return it->second(request);
+  }
+
+  return dap::array<dap::Variable>();
+}
+
+} // namespace cmDebugger
diff --git a/Source/cmDebuggerVariablesManager.h b/Source/cmDebuggerVariablesManager.h
new file mode 100644
index 0000000..c219164
--- /dev/null
+++ b/Source/cmDebuggerVariablesManager.h
@@ -0,0 +1,40 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <cstdint>
+#include <functional>
+#include <unordered_map>
+#include <vector>
+
+#include <cm3p/cppdap/types.h> // IWYU pragma: keep
+
+namespace dap {
+struct Variable;
+struct VariablesRequest;
+}
+
+namespace cmDebugger {
+
+class cmDebuggerVariablesManager
+{
+  std::unordered_map<
+    int64_t,
+    std::function<dap::array<dap::Variable>(dap::VariablesRequest const&)>>
+    VariablesHandlers;
+  void RegisterHandler(
+    int64_t id,
+    std::function<dap::array<dap::Variable>(dap::VariablesRequest const&)>
+      handler);
+  void UnregisterHandler(int64_t id);
+  friend class cmDebuggerVariables;
+
+public:
+  cmDebuggerVariablesManager() = default;
+  dap::array<dap::Variable> HandleVariablesRequest(
+    dap::VariablesRequest const& request);
+};
+
+} // namespace cmDebugger
diff --git a/Source/cmGlobalGenerator.cxx b/Source/cmGlobalGenerator.cxx
index 68c74dd..f50c4cb 100644
--- a/Source/cmGlobalGenerator.cxx
+++ b/Source/cmGlobalGenerator.cxx
@@ -47,6 +47,7 @@
 #include "cmMSVC60LinkLineComputer.h"
 #include "cmMakefile.h"
 #include "cmMessageType.h"
+#include "cmOutputConverter.h"
 #include "cmPolicies.h"
 #include "cmRange.h"
 #include "cmSourceFile.h"
@@ -73,6 +74,23 @@
 
 class cmInstalledFile;
 
+namespace detail {
+std::string GeneratedMakeCommand::QuotedPrintable() const
+{
+  std::string output;
+  const char* sep = "";
+  int flags = 0;
+#if !defined(_WIN32)
+  flags |= cmOutputConverter::Shell_Flag_IsUnix;
+#endif
+  for (auto const& arg : this->PrimaryCommand) {
+    output += cmStrCat(sep, cmOutputConverter::EscapeForShell(arg, flags));
+    sep = " ";
+  }
+  return output;
+}
+}
+
 bool cmTarget::StrictTargetComparison::operator()(cmTarget const* t1,
                                                   cmTarget const* t2) const
 {
@@ -2056,9 +2074,12 @@
     mf->GetSafeDefinition("CMAKE_TRY_COMPILE_CONFIGURATION");
   cmBuildOptions defaultBuildOptions(false, fast, PackageResolveMode::Disable);
 
-  return this->Build(jobs, srcdir, bindir, projectName, newTarget, output, "",
-                     config, defaultBuildOptions, true,
-                     this->TryCompileTimeout);
+  std::stringstream ostr;
+  auto ret =
+    this->Build(jobs, srcdir, bindir, projectName, newTarget, ostr, "", config,
+                defaultBuildOptions, true, this->TryCompileTimeout);
+  output = ostr.str();
+  return ret;
 }
 
 std::vector<cmGlobalGenerator::GeneratedMakeCommand>
@@ -2083,7 +2104,7 @@
 int cmGlobalGenerator::Build(
   int jobs, const std::string& /*unused*/, const std::string& bindir,
   const std::string& projectName, const std::vector<std::string>& targets,
-  std::string& output, const std::string& makeCommandCSTR,
+  std::ostream& ostr, const std::string& makeCommandCSTR,
   const std::string& config, const cmBuildOptions& buildOptions, bool verbose,
   cmDuration timeout, cmSystemTools::OutputOption outputflag,
   std::vector<std::string> const& nativeOptions)
@@ -2094,16 +2115,13 @@
    * Run an executable command and put the stdout in output.
    */
   cmWorkingDirectory workdir(bindir);
-  output += "Change Dir: ";
-  output += bindir;
-  output += "\n";
+  ostr << "Change Dir: '" << bindir << '\'' << std::endl;
   if (workdir.Failed()) {
     cmSystemTools::SetRunCommandHideConsole(hideconsole);
     std::string err = cmStrCat("Failed to change directory: ",
                                std::strerror(workdir.GetLastResult()));
     cmSystemTools::Error(err);
-    output += err;
-    output += "\n";
+    ostr << err << std::endl;
     return 1;
   }
   std::string realConfig = config;
@@ -2132,9 +2150,8 @@
       this->GenerateBuildCommand(makeCommandCSTR, projectName, bindir,
                                  { "clean" }, realConfig, jobs, verbose,
                                  buildOptions);
-    output += "\nRun Clean Command:";
-    output += cleanCommand.front().Printable();
-    output += "\n";
+    ostr << "\nRun Clean Command: " << cleanCommand.front().QuotedPrintable()
+         << std::endl;
     if (cleanCommand.size() != 1) {
       this->GetCMakeInstance()->IssueMessage(MessageType::INTERNAL_ERROR,
                                              "The generator did not produce "
@@ -2147,27 +2164,33 @@
                                          nullptr, outputflag, timeout)) {
       cmSystemTools::SetRunCommandHideConsole(hideconsole);
       cmSystemTools::Error("Generator: execution of make clean failed.");
-      output += *outputPtr;
-      output += "\nGenerator: execution of make clean failed.\n";
+      ostr << *outputPtr << "\nGenerator: execution of make clean failed."
+           << std::endl;
 
       return 1;
     }
-    output += *outputPtr;
+    ostr << *outputPtr;
   }
 
   // now build
   std::string makeCommandStr;
-  output += "\nRun Build Command(s):";
+  std::string outputMakeCommandStr;
+  bool isWatcomWMake = this->CMakeInstance->GetState()->UseWatcomWMake();
+  bool needBuildOutput = isWatcomWMake;
+  std::string buildOutput;
+  ostr << "\nRun Build Command(s): ";
 
   retVal = 0;
   for (auto command = makeCommand.begin();
        command != makeCommand.end() && retVal == 0; ++command) {
     makeCommandStr = command->Printable();
-    if (command != makeCommand.end()) {
+    outputMakeCommandStr = command->QuotedPrintable();
+    if ((command + 1) != makeCommand.end()) {
       makeCommandStr += " && ";
+      outputMakeCommandStr += " && ";
     }
 
-    output += makeCommandStr;
+    ostr << outputMakeCommandStr << std::endl;
     if (!cmSystemTools::RunSingleCommand(command->PrimaryCommand, outputPtr,
                                          outputPtr, &retVal, nullptr,
                                          outputflag, timeout)) {
@@ -2175,21 +2198,24 @@
       cmSystemTools::Error(
         "Generator: execution of make failed. Make command was: " +
         makeCommandStr);
-      output += *outputPtr;
-      output += "\nGenerator: execution of make failed. Make command was: " +
-        makeCommandStr + "\n";
+      ostr << *outputPtr
+           << "\nGenerator: execution of make failed. Make command was: "
+           << outputMakeCommandStr << std::endl;
 
       return 1;
     }
-    output += *outputPtr;
+    ostr << *outputPtr << std::flush;
+    if (needBuildOutput) {
+      buildOutput += *outputPtr;
+    }
   }
-  output += "\n";
+  ostr << std::endl;
   cmSystemTools::SetRunCommandHideConsole(hideconsole);
 
   // The OpenWatcom tools do not return an error code when a link
   // library is not found!
-  if (this->CMakeInstance->GetState()->UseWatcomWMake() && retVal == 0 &&
-      output.find("W1008: cannot open") != std::string::npos) {
+  if (isWatcomWMake && retVal == 0 &&
+      buildOutput.find("W1008: cannot open") != std::string::npos) {
     retVal = 1;
   }
 
diff --git a/Source/cmGlobalGenerator.h b/Source/cmGlobalGenerator.h
index d657fc8..01afabd 100644
--- a/Source/cmGlobalGenerator.h
+++ b/Source/cmGlobalGenerator.h
@@ -85,6 +85,7 @@
   }
 
   std::string Printable() const { return cmJoin(this->PrimaryCommand, " "); }
+  std::string QuotedPrintable() const;
 
   std::vector<std::string> PrimaryCommand;
   bool RequiresOutputForward = false;
@@ -233,7 +234,7 @@
   int Build(
     int jobs, const std::string& srcdir, const std::string& bindir,
     const std::string& projectName,
-    std::vector<std::string> const& targetNames, std::string& output,
+    std::vector<std::string> const& targetNames, std::ostream& ostr,
     const std::string& makeProgram, const std::string& config,
     const cmBuildOptions& buildOptions, bool verbose, cmDuration timeout,
     cmSystemTools::OutputOption outputflag = cmSystemTools::OUTPUT_NONE,
diff --git a/Source/cmMakefile.cxx b/Source/cmMakefile.cxx
index 4acb283..0af0ed0 100644
--- a/Source/cmMakefile.cxx
+++ b/Source/cmMakefile.cxx
@@ -68,6 +68,10 @@
 #  include "cmVariableWatch.h"
 #endif
 
+#ifdef CMake_ENABLE_DEBUGGER
+#  include "cmDebuggerAdapter.h"
+#endif
+
 #ifndef __has_feature
 #  define __has_feature(x) 0
 #endif
@@ -424,6 +428,13 @@
           return argsValue;
         });
 #endif
+#ifdef CMake_ENABLE_DEBUGGER
+    if (this->Makefile->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+      this->Makefile->GetCMakeInstance()
+        ->GetDebugAdapter()
+        ->OnBeginFunctionCall(mf, lfc.FilePath, lff);
+    }
+#endif
   }
 
   ~cmMakefileCall()
@@ -434,6 +445,13 @@
     this->Makefile->ExecutionStatusStack.pop_back();
     --this->Makefile->RecursionDepth;
     this->Makefile->Backtrace = this->Makefile->Backtrace.Pop();
+#ifdef CMake_ENABLE_DEBUGGER
+    if (this->Makefile->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+      this->Makefile->GetCMakeInstance()
+        ->GetDebugAdapter()
+        ->OnEndFunctionCall();
+    }
+#endif
   }
 
   cmMakefileCall(const cmMakefileCall&) = delete;
@@ -663,12 +681,33 @@
 
   IncludeScope incScope(this, filenametoread, noPolicyScope);
 
+#ifdef CMake_ENABLE_DEBUGGER
+  if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+    this->GetCMakeInstance()->GetDebugAdapter()->OnBeginFileParse(
+      this, filenametoread);
+  }
+#endif
+
   cmListFile listFile;
   if (!listFile.ParseFile(filenametoread.c_str(), this->GetMessenger(),
                           this->Backtrace)) {
+#ifdef CMake_ENABLE_DEBUGGER
+    if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+      this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse();
+    }
+#endif
+
     return false;
   }
 
+#ifdef CMake_ENABLE_DEBUGGER
+  if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+    this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse();
+    this->GetCMakeInstance()->GetDebugAdapter()->OnFileParsedSuccessfully(
+      filenametoread, listFile.Functions);
+  }
+#endif
+
   this->RunListFile(listFile, filenametoread);
   if (cmSystemTools::GetFatalErrorOccurred()) {
     incScope.Quiet();
@@ -764,12 +803,33 @@
 
   ListFileScope scope(this, filenametoread);
 
+#ifdef CMake_ENABLE_DEBUGGER
+  if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+    this->GetCMakeInstance()->GetDebugAdapter()->OnBeginFileParse(
+      this, filenametoread);
+  }
+#endif
+
   cmListFile listFile;
   if (!listFile.ParseFile(filenametoread.c_str(), this->GetMessenger(),
                           this->Backtrace)) {
+#ifdef CMake_ENABLE_DEBUGGER
+    if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+      this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse();
+    }
+#endif
+
     return false;
   }
 
+#ifdef CMake_ENABLE_DEBUGGER
+  if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+    this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse();
+    this->GetCMakeInstance()->GetDebugAdapter()->OnFileParsedSuccessfully(
+      filenametoread, listFile.Functions);
+  }
+#endif
+
   this->RunListFile(listFile, filenametoread);
   if (cmSystemTools::GetFatalErrorOccurred()) {
     scope.Quiet();
@@ -791,6 +851,13 @@
     return false;
   }
 
+#ifdef CMake_ENABLE_DEBUGGER
+  if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+    this->GetCMakeInstance()->GetDebugAdapter()->OnFileParsedSuccessfully(
+      filenametoread, listFile.Functions);
+  }
+#endif
+
   this->RunListFile(listFile, filenametoread);
   if (cmSystemTools::GetFatalErrorOccurred()) {
     scope.Quiet();
@@ -1658,11 +1725,33 @@
   assert(cmSystemTools::FileExists(currentStart, true));
   this->AddDefinition("CMAKE_PARENT_LIST_FILE", currentStart);
 
+#ifdef CMake_ENABLE_DEBUGGER
+  if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+    this->GetCMakeInstance()->GetDebugAdapter()->OnBeginFileParse(
+      this, currentStart);
+  }
+#endif
+
   cmListFile listFile;
   if (!listFile.ParseFile(currentStart.c_str(), this->GetMessenger(),
                           this->Backtrace)) {
+#ifdef CMake_ENABLE_DEBUGGER
+    if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+      this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse();
+    }
+#endif
+
     return;
   }
+
+#ifdef CMake_ENABLE_DEBUGGER
+  if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+    this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse();
+    this->GetCMakeInstance()->GetDebugAdapter()->OnFileParsedSuccessfully(
+      currentStart, listFile.Functions);
+  }
+#endif
+
   if (this->IsRootMakefile()) {
     bool hasVersion = false;
     // search for the right policy command
@@ -3767,6 +3856,12 @@
     return;
   }
   cm->UpdateProgress(message, s);
+
+#ifdef CMake_ENABLE_DEBUGGER
+  if (cm->GetDebugAdapter() != nullptr) {
+    cm->GetDebugAdapter()->OnMessageOutput(MessageType::MESSAGE, message);
+  }
+#endif
 }
 
 std::string cmMakefile::GetModulesFile(const std::string& filename,
diff --git a/Source/cmMessageCommand.cxx b/Source/cmMessageCommand.cxx
index baf40f8..68b3a5d 100644
--- a/Source/cmMessageCommand.cxx
+++ b/Source/cmMessageCommand.cxx
@@ -3,6 +3,7 @@
 #include "cmMessageCommand.h"
 
 #include <cassert>
+#include <memory>
 #include <utility>
 
 #include <cm/string_view>
@@ -19,6 +20,10 @@
 #include "cmSystemTools.h"
 #include "cmake.h"
 
+#ifdef CMake_ENABLE_DEBUGGER
+#  include "cmDebuggerAdapter.h"
+#endif
+
 namespace {
 
 enum class CheckingType
@@ -202,6 +207,12 @@
 
     case Message::LogLevel::LOG_NOTICE:
       cmSystemTools::Message(IndentText(message, mf));
+#ifdef CMake_ENABLE_DEBUGGER
+      if (mf.GetCMakeInstance()->GetDebugAdapter() != nullptr) {
+        mf.GetCMakeInstance()->GetDebugAdapter()->OnMessageOutput(type,
+                                                                  message);
+      }
+#endif
       break;
 
     case Message::LogLevel::LOG_STATUS:
diff --git a/Source/cmMessenger.cxx b/Source/cmMessenger.cxx
index 7de8936..4e975d1 100644
--- a/Source/cmMessenger.cxx
+++ b/Source/cmMessenger.cxx
@@ -16,6 +16,10 @@
 
 #include "cmsys/Terminal.h"
 
+#ifdef CMake_ENABLE_DEBUGGER
+#  include "cmDebuggerAdapter.h"
+#endif
+
 MessageType cmMessenger::ConvertMessageType(MessageType t) const
 {
   if (t == MessageType::AUTHOR_WARNING || t == MessageType::AUTHOR_ERROR) {
@@ -207,6 +211,12 @@
   PrintCallStack(msg, backtrace, this->TopSource);
 
   displayMessage(t, msg);
+
+#ifdef CMake_ENABLE_DEBUGGER
+  if (DebuggerAdapter != nullptr) {
+    DebuggerAdapter->OnMessageOutput(t, msg.str());
+  }
+#endif
 }
 
 void cmMessenger::PrintBacktraceTitle(std::ostream& out,
diff --git a/Source/cmMessenger.h b/Source/cmMessenger.h
index 451add0..bdefb00 100644
--- a/Source/cmMessenger.h
+++ b/Source/cmMessenger.h
@@ -5,6 +5,7 @@
 #include "cmConfigure.h" // IWYU pragma: keep
 
 #include <iosfwd>
+#include <memory>
 #include <string>
 
 #include <cm/optional>
@@ -12,6 +13,12 @@
 #include "cmListFileCache.h"
 #include "cmMessageType.h"
 
+#ifdef CMake_ENABLE_DEBUGGER
+namespace cmDebugger {
+class cmDebuggerAdapter;
+}
+#endif
+
 class cmMessenger
 {
 public:
@@ -55,6 +62,13 @@
   // Print the top of a backtrace.
   void PrintBacktraceTitle(std::ostream& out,
                            cmListFileBacktrace const& bt) const;
+#ifdef CMake_ENABLE_DEBUGGER
+  void SetDebuggerAdapter(
+    std::shared_ptr<cmDebugger::cmDebuggerAdapter> const& debuggerAdapter)
+  {
+    DebuggerAdapter = debuggerAdapter;
+  }
+#endif
 
 private:
   bool IsMessageTypeVisible(MessageType t) const;
@@ -66,4 +80,7 @@
   bool SuppressDeprecatedWarnings = false;
   bool DevWarningsAsErrors = false;
   bool DeprecatedWarningsAsErrors = false;
+#ifdef CMake_ENABLE_DEBUGGER
+  std::shared_ptr<cmDebugger::cmDebuggerAdapter> DebuggerAdapter;
+#endif
 };
diff --git a/Source/cmOutputConverter.cxx b/Source/cmOutputConverter.cxx
index 53cb21e..02981ae 100644
--- a/Source/cmOutputConverter.cxx
+++ b/Source/cmOutputConverter.cxx
@@ -243,11 +243,6 @@
                                               bool unescapeNinjaConfiguration,
                                               bool forResponse) const
 {
-  // Do not escape shell operators.
-  if (cmOutputConverterIsShellOperator(str)) {
-    return std::string(str);
-  }
-
   // Compute the flags for the target shell environment.
   int flags = 0;
   if (this->GetState()->UseWindowsVSIDE()) {
@@ -283,6 +278,16 @@
     flags |= Shell_Flag_IsUnix;
   }
 
+  return cmOutputConverter::EscapeForShell(str, flags);
+}
+
+std::string cmOutputConverter::EscapeForShell(cm::string_view str, int flags)
+{
+  // Do not escape shell operators.
+  if (cmOutputConverterIsShellOperator(str)) {
+    return std::string(str);
+  }
+
   return Shell_GetArgument(str, flags);
 }
 
diff --git a/Source/cmOutputConverter.h b/Source/cmOutputConverter.h
index 625d897..0ee7afb 100644
--- a/Source/cmOutputConverter.h
+++ b/Source/cmOutputConverter.h
@@ -107,6 +107,7 @@
                              bool forEcho = false, bool useWatcomQuote = false,
                              bool unescapeNinjaConfiguration = false,
                              bool forResponse = false) const;
+  static std::string EscapeForShell(cm::string_view str, int flags);
 
   enum class WrapQuotes
   {
diff --git a/Source/cmQtAutoGenInitializer.cxx b/Source/cmQtAutoGenInitializer.cxx
index 76bb0cd..2c48e78 100644
--- a/Source/cmQtAutoGenInitializer.cxx
+++ b/Source/cmQtAutoGenInitializer.cxx
@@ -344,7 +344,7 @@
 
   // Verbosity
   {
-    std::string def =
+    std::string const def =
       this->Makefile->GetSafeDefinition("CMAKE_AUTOGEN_VERBOSE");
     if (!def.empty()) {
       unsigned long iVerb = 0;
@@ -546,7 +546,7 @@
       this->Moc.MacroNames.erase(cmRemoveDuplicates(this->Moc.MacroNames),
                                  this->Moc.MacroNames.end());
       {
-        cmList filterList = { this->GenTarget->GetSafeProperty(
+        cmList const filterList = { this->GenTarget->GetSafeProperty(
           "AUTOMOC_DEPEND_FILTERS") };
         if ((filterList.size() % 2) != 0) {
           cmSystemTools::Error(
@@ -650,7 +650,7 @@
 
   // Moc includes
   {
-    SearchPathSanitizer sanitizer(this->Makefile);
+    SearchPathSanitizer const sanitizer(this->Makefile);
     auto getDirs =
       [this, &sanitizer](std::string const& cfg) -> std::vector<std::string> {
       // Get the include dirs for this target, without stripping the implicit
@@ -662,8 +662,6 @@
       return sanitizer(dirs);
     };
 
-    // Default configuration include directories
-    this->Moc.Includes.Default = getDirs(this->ConfigDefault);
     // Other configuration settings
     if (this->MultiConfig) {
       for (std::string const& cfg : this->ConfigsList) {
@@ -673,6 +671,9 @@
         }
         this->Moc.Includes.Config[cfg] = std::move(dirs);
       }
+    } else {
+      // Default configuration include directories
+      this->Moc.Includes.Default = getDirs(this->ConfigDefault);
     }
   }
 
@@ -690,8 +691,6 @@
       return defines;
     };
 
-    // Default configuration defines
-    this->Moc.Defines.Default = getDefs(this->ConfigDefault);
     // Other configuration defines
     if (this->MultiConfig) {
       for (std::string const& cfg : this->ConfigsList) {
@@ -701,6 +700,9 @@
         }
         this->Moc.Defines.Config[cfg] = std::move(defines);
       }
+    } else {
+      // Default configuration defines
+      this->Moc.Defines.Default = getDefs(this->ConfigDefault);
     }
   }
 
@@ -1024,8 +1026,24 @@
   if (this->MocOrUicEnabled() && !this->AutogenTarget.FilesGenerated.empty()) {
     if (this->CMP0071Accept) {
       // Let the autogen target depend on the GENERATED files
-      for (MUFile* muf : this->AutogenTarget.FilesGenerated) {
-        this->AutogenTarget.DependFiles.insert(muf->FullPath);
+      if (this->MultiConfig &&
+          this->Makefile->GetSafeDefinition("CMAKE_CROSS_CONFIGS").empty()) {
+        for (MUFile const* muf : this->AutogenTarget.FilesGenerated) {
+          if (muf->Configs.empty()) {
+            this->AutogenTarget.DependFiles.insert(muf->FullPath);
+          } else {
+            for (size_t ci : muf->Configs) {
+              std::string const& config = this->ConfigsList[ci];
+              std::string const& pathWithConfig =
+                cmStrCat("$<$<CONFIG:", config, ">:", muf->FullPath, '>');
+              this->AutogenTarget.DependFiles.insert(pathWithConfig);
+            }
+          }
+        }
+      } else {
+        for (MUFile const* muf : this->AutogenTarget.FilesGenerated) {
+          this->AutogenTarget.DependFiles.insert(muf->FullPath);
+        }
       }
     } else if (this->CMP0071Warn) {
       cm::string_view property;
@@ -1037,7 +1055,7 @@
         property = "SKIP_AUTOUIC";
       }
       std::string files;
-      for (MUFile* muf : this->AutogenTarget.FilesGenerated) {
+      for (MUFile const* muf : this->AutogenTarget.FilesGenerated) {
         files += cmStrCat("  ", Quoted(muf->FullPath), '\n');
       }
       this->Makefile->IssueMessage(
@@ -1068,7 +1086,7 @@
       property = "SKIP_AUTOUIC";
     }
     std::string files;
-    for (cmSourceFile* sf : this->AutogenTarget.CMP0100HeadersWarn) {
+    for (cmSourceFile const* sf : this->AutogenTarget.CMP0100HeadersWarn) {
       files += cmStrCat("  ", Quoted(sf->GetFullPath()), '\n');
     }
     this->Makefile->IssueMessage(
@@ -1089,7 +1107,7 @@
   if (!this->Rcc.Qrcs.empty()) {
     const bool modernQt = (this->QtVersion.Major >= 5);
     // Target rcc options
-    cmList optionsTarget{ this->GenTarget->GetSafeProperty(
+    cmList const optionsTarget{ this->GenTarget->GetSafeProperty(
       kw.AUTORCC_OPTIONS) };
 
     // Check if file name is unique
@@ -1168,7 +1186,8 @@
   if (this->Moc.Enabled) {
     this->AddGeneratedSource(this->Moc.CompilationFile, this->Moc, true);
     if (useNinjaDepfile) {
-      if (this->MultiConfig) {
+      if (this->MultiConfig &&
+          !this->Makefile->GetSafeDefinition("CMAKE_CROSS_CONFIGS").empty()) {
         // Make all mocs_compilation_<CONFIG>.cpp files byproducts of the
         // ${target}_autogen/timestamp custom command.
         // We cannot just use Moc.CompilationFileGenex here, because that
@@ -1213,12 +1232,25 @@
   // instead of fiddling with the include directories
   std::vector<std::string> configs;
   this->GlobalGen->GetQtAutoGenConfigs(configs);
-  bool stdPipesUTF8 = true;
+  bool constexpr stdPipesUTF8 = true;
   cmCustomCommandLines commandLines;
-  for (auto const& config : configs) {
+  if (this->Makefile->GetSafeDefinition("CMAKE_CROSS_CONFIGS").empty()) {
+    std::string autugenInfoFileconfig;
+    if (this->MultiConfig) {
+      autugenInfoFileconfig = "$<CONFIG>";
+    } else {
+      autugenInfoFileconfig = configs[0];
+    }
     commandLines.push_back(cmMakeCommandLine(
       { cmSystemTools::GetCMakeCommand(), "-E", "cmake_autogen",
-        this->AutogenTarget.InfoFile, config }));
+        this->AutogenTarget.InfoFile, autugenInfoFileconfig }));
+
+  } else {
+    for (auto const& config : configs) {
+      commandLines.push_back(cmMakeCommandLine(
+        { cmSystemTools::GetCMakeCommand(), "-E", "cmake_autogen",
+          this->AutogenTarget.InfoFile, config }));
+    }
   }
 
   // Use PRE_BUILD on demand
@@ -1244,7 +1276,7 @@
   // Create the autogen target/command
   if (usePRE_BUILD) {
     // Add additional autogen target dependencies to origin target
-    for (cmTarget* depTarget : this->AutogenTarget.DependTargets) {
+    for (cmTarget const* depTarget : this->AutogenTarget.DependTargets) {
       this->GenTarget->Target->AddUtility(depTarget->GetName(), false,
                                           this->Makefile);
     }
@@ -1724,10 +1756,21 @@
       this->GenTarget, "AUTOMOC_MACRO_NAMES", nullptr, nullptr);
     EvaluatedTargetPropertyEntries InterfaceAutoMocMacroNamesEntries;
 
-    AddInterfaceEntries(this->GenTarget, this->ConfigDefault,
-                        "INTERFACE_AUTOMOC_MACRO_NAMES", "CXX", &dagChecker,
-                        InterfaceAutoMocMacroNamesEntries,
-                        IncludeRuntimeInterface::Yes);
+    if (this->MultiConfig) {
+      for (auto const& cfg : this->ConfigsList) {
+        if (!cfg.empty()) {
+          AddInterfaceEntries(this->GenTarget, cfg,
+                              "INTERFACE_AUTOMOC_MACRO_NAMES", "CXX",
+                              &dagChecker, InterfaceAutoMocMacroNamesEntries,
+                              IncludeRuntimeInterface::Yes);
+        }
+      }
+    } else {
+      AddInterfaceEntries(this->GenTarget, this->ConfigDefault,
+                          "INTERFACE_AUTOMOC_MACRO_NAMES", "CXX", &dagChecker,
+                          InterfaceAutoMocMacroNamesEntries,
+                          IncludeRuntimeInterface::Yes);
+    }
 
     for (auto const& entry : InterfaceAutoMocMacroNamesEntries.Entries) {
       this->Moc.MacroNames.insert(this->Moc.MacroNames.end(),
@@ -1747,11 +1790,11 @@
     info.SetConfig("MOC_COMPILATION_FILE", this->Moc.CompilationFile);
     info.SetConfig("MOC_PREDEFS_FILE", this->Moc.PredefsFile);
 
-    cmStandardLevelResolver resolver{ this->Makefile };
-    auto CompileOptionFlag =
+    cmStandardLevelResolver const resolver{ this->Makefile };
+    auto const CompileOptionFlag =
       resolver.GetCompileOptionDef(this->GenTarget, "CXX", "");
 
-    auto CompileOptionValue =
+    auto const CompileOptionValue =
       this->GenTarget->Makefile->GetSafeDefinition(CompileOptionFlag);
 
     if (!CompileOptionValue.empty()) {
@@ -1995,13 +2038,13 @@
   return parseMocVersion(capturedStdOut);
 }
 
-static std::string FindMocExecutableFromMocTarget(cmMakefile* makefile,
+static std::string FindMocExecutableFromMocTarget(cmMakefile const* makefile,
                                                   unsigned int qtMajorVersion)
 {
   std::string result;
   const std::string mocTargetName =
     "Qt" + std::to_string(qtMajorVersion) + "::moc";
-  cmTarget* mocTarget = makefile->FindTargetToUse(mocTargetName);
+  cmTarget const* mocTarget = makefile->FindTargetToUse(mocTargetName);
   if (mocTarget) {
     result = mocTarget->GetSafeProperty("IMPORTED_LOCATION");
   }
diff --git a/Source/cmake.cxx b/Source/cmake.cxx
index 3694ea8..f30d4d3 100644
--- a/Source/cmake.cxx
+++ b/Source/cmake.cxx
@@ -38,6 +38,10 @@
 #include "cmCMakePresetsGraph.h"
 #include "cmCommandLineArgument.h"
 #include "cmCommands.h"
+#ifdef CMake_ENABLE_DEBUGGER
+#  include "cmDebuggerAdapter.h"
+#  include "cmDebuggerPipeConnection.h"
+#endif
 #include "cmDocumentation.h"
 #include "cmDocumentationEntry.h"
 #include "cmDuration.h"
@@ -411,6 +415,11 @@
   obj["fileApi"] = cmFileAPI::ReportCapabilities();
   obj["serverMode"] = false;
   obj["tls"] = static_cast<bool>(curlVersion->features & CURL_VERSION_SSL);
+#  ifdef CMake_ENABLE_DEBUGGER
+  obj["debugger"] = true;
+#  else
+  obj["debugger"] = false;
+#  endif
 
   return obj;
 }
@@ -617,6 +626,13 @@
   };
 
   auto ScriptLambda = [&](std::string const& path, cmake* state) -> bool {
+#ifdef CMake_ENABLE_DEBUGGER
+    // Script mode doesn't hit the usual code path in cmake::Run() that starts
+    // the debugger, so start it manually here instead.
+    if (!this->StartDebuggerIfEnabled()) {
+      return false;
+    }
+#endif
     // Register fake project commands that hint misuse in script mode.
     GetProjectCommandsInScriptMode(state->GetState());
     // Documented behavior of CMAKE{,_CURRENT}_{SOURCE,BINARY}_DIR is to be
@@ -1233,7 +1249,52 @@
                      "CMAKE_COMPILE_WARNING_AS_ERROR variable.\n";
         state->SetIgnoreWarningAsError(true);
         return true;
-      } }
+      } },
+    CommandArgument{ "--debugger", CommandArgument::Values::Zero,
+                     [](std::string const&, cmake* state) -> bool {
+#ifdef CMake_ENABLE_DEBUGGER
+                       std::cout << "Running with debugger on.\n";
+                       state->SetDebuggerOn(true);
+                       return true;
+#else
+                       static_cast<void>(state);
+                       cmSystemTools::Error(
+                         "CMake was not built with support for --debugger");
+                       return false;
+#endif
+                     } },
+    CommandArgument{ "--debugger-pipe",
+                     "No path specified for --debugger-pipe",
+                     CommandArgument::Values::One,
+                     [](std::string const& value, cmake* state) -> bool {
+#ifdef CMake_ENABLE_DEBUGGER
+                       state->DebuggerPipe = value;
+                       return true;
+#else
+                       static_cast<void>(value);
+                       static_cast<void>(state);
+                       cmSystemTools::Error("CMake was not built with support "
+                                            "for --debugger-pipe");
+                       return false;
+#endif
+                     } },
+    CommandArgument{
+      "--debugger-dap-log", "No file specified for --debugger-dap-log",
+      CommandArgument::Values::One,
+      [](std::string const& value, cmake* state) -> bool {
+#ifdef CMake_ENABLE_DEBUGGER
+        std::string path = cmSystemTools::CollapseFullPath(value);
+        cmSystemTools::ConvertToUnixSlashes(path);
+        state->DebuggerDapLogFile = path;
+        return true;
+#else
+        static_cast<void>(value);
+        static_cast<void>(state);
+        cmSystemTools::Error(
+          "CMake was not built with support for --debugger-dap-log");
+        return false;
+#endif
+      } },
   };
 
 #if defined(CMAKE_HAVE_VS_GENERATORS)
@@ -2618,6 +2679,52 @@
   }
 }
 
+#ifdef CMake_ENABLE_DEBUGGER
+
+bool cmake::StartDebuggerIfEnabled()
+{
+  if (!this->GetDebuggerOn()) {
+    return true;
+  }
+
+  if (DebugAdapter == nullptr) {
+    if (this->GetDebuggerPipe().empty()) {
+      std::cerr
+        << "Error: --debugger-pipe must be set when debugging is enabled.\n";
+      return false;
+    }
+
+    try {
+      DebugAdapter = std::make_shared<cmDebugger::cmDebuggerAdapter>(
+        std::make_shared<cmDebugger::cmDebuggerPipeConnection>(
+          this->GetDebuggerPipe()),
+        this->GetDebuggerDapLogFile());
+    } catch (const std::runtime_error& error) {
+      std::cerr << "Error: Failed to create debugger adapter.\n";
+      std::cerr << error.what() << "\n";
+      return false;
+    }
+    Messenger->SetDebuggerAdapter(DebugAdapter);
+  }
+
+  return true;
+}
+
+void cmake::StopDebuggerIfNeeded(int exitCode)
+{
+  if (!this->GetDebuggerOn()) {
+    return;
+  }
+
+  // The debug adapter may have failed to start (e.g. invalid pipe path).
+  if (DebugAdapter != nullptr) {
+    DebugAdapter->ReportExitCode(exitCode);
+    DebugAdapter.reset();
+  }
+}
+
+#endif
+
 // handle a command line invocation
 int cmake::Run(const std::vector<std::string>& args, bool noconfigure)
 {
@@ -2707,6 +2814,12 @@
     return 0;
   }
 
+#ifdef CMake_ENABLE_DEBUGGER
+  if (!this->StartDebuggerIfEnabled()) {
+    return -1;
+  }
+#endif
+
   int ret = this->Configure();
   if (ret) {
 #if defined(CMAKE_HAVE_VS_GENERATORS)
@@ -3619,7 +3732,6 @@
       return 1;
     }
   }
-  std::string output;
   std::string projName;
   cmValue cachedProjectName =
     this->State->GetCacheEntryValue("CMAKE_PROJECT_NAME");
@@ -3693,10 +3805,17 @@
   }
 
   this->GlobalGenerator->PrintBuildCommandAdvice(std::cerr, jobs);
-  return this->GlobalGenerator->Build(
-    jobs, "", dir, projName, targets, output, "", config, buildOptions,
+  std::stringstream ostr;
+  // `cmGlobalGenerator::Build` logs metadata about what directory and commands
+  // are being executed to the `output` parameter. If CMake is verbose, print
+  // this out.
+  std::ostream& verbose_ostr = verbose ? std::cout : ostr;
+  int buildresult = this->GlobalGenerator->Build(
+    jobs, "", dir, projName, targets, verbose_ostr, "", config, buildOptions,
     verbose, cmDuration::zero(), cmSystemTools::OUTPUT_PASSTHROUGH,
     nativeOptions);
+
+  return buildresult;
 }
 
 bool cmake::Open(const std::string& dir, bool dryRun)
diff --git a/Source/cmake.h b/Source/cmake.h
index 9da0295..2f5ea24 100644
--- a/Source/cmake.h
+++ b/Source/cmake.h
@@ -37,6 +37,13 @@
 #endif
 
 class cmConfigureLog;
+
+#ifdef CMake_ENABLE_DEBUGGER
+namespace cmDebugger {
+class cmDebuggerAdapter;
+}
+#endif
+
 class cmExternalMakefileProjectGeneratorFactory;
 class cmFileAPI;
 class cmFileTimeCache;
@@ -660,6 +667,23 @@
   }
 #endif
 
+#ifdef CMake_ENABLE_DEBUGGER
+  bool GetDebuggerOn() const { return this->DebuggerOn; }
+  std::string GetDebuggerPipe() const { return this->DebuggerPipe; }
+  std::string GetDebuggerDapLogFile() const
+  {
+    return this->DebuggerDapLogFile;
+  }
+  void SetDebuggerOn(bool b) { this->DebuggerOn = b; }
+  bool StartDebuggerIfEnabled();
+  void StopDebuggerIfNeeded(int exitCode);
+  std::shared_ptr<cmDebugger::cmDebuggerAdapter> GetDebugAdapter()
+    const noexcept
+  {
+    return this->DebugAdapter;
+  }
+#endif
+
 protected:
   void RunCheckForUnusedVariables();
   int HandleDeleteCacheVariables(const std::string& var);
@@ -800,6 +824,13 @@
   std::unique_ptr<cmMakefileProfilingData> ProfilingOutput;
 #endif
 
+#ifdef CMake_ENABLE_DEBUGGER
+  std::shared_ptr<cmDebugger::cmDebuggerAdapter> DebugAdapter;
+  bool DebuggerOn = false;
+  std::string DebuggerPipe;
+  std::string DebuggerDapLogFile;
+#endif
+
 public:
   static cmDocumentationEntry CMAKE_STANDARD_OPTIONS_TABLE[18];
 };
diff --git a/Source/cmakemain.cxx b/Source/cmakemain.cxx
index ad27443..ced83dc 100644
--- a/Source/cmakemain.cxx
+++ b/Source/cmakemain.cxx
@@ -392,8 +392,14 @@
   // Always return a non-negative value.  Windows tools do not always
   // interpret negative return values as errors.
   if (res != 0) {
+#ifdef CMake_ENABLE_DEBUGGER
+    cm.StopDebuggerIfNeeded(1);
+#endif
     return 1;
   }
+#ifdef CMake_ENABLE_DEBUGGER
+  cm.StopDebuggerIfNeeded(0);
+#endif
   return 0;
 }
 
diff --git a/Tests/CMakeLib/CMakeLists.txt b/Tests/CMakeLib/CMakeLists.txt
index 944b328..5c14de2 100644
--- a/Tests/CMakeLib/CMakeLists.txt
+++ b/Tests/CMakeLib/CMakeLists.txt
@@ -32,6 +32,16 @@
   testCMExtEnumSet.cxx
   testList.cxx
   )
+if(CMake_ENABLE_DEBUGGER)
+  list(APPEND CMakeLib_TESTS
+    testDebuggerAdapter.cxx
+    testDebuggerAdapterPipe.cxx
+    testDebuggerBreakpointManager.cxx
+    testDebuggerVariables.cxx
+    testDebuggerVariablesHelper.cxx
+    testDebuggerVariablesManager.cxx
+    )
+endif()
 if (CMake_TEST_FILESYSTEM_PATH OR NOT CMake_HAVE_CXX_FILESYSTEM)
   list(APPEND CMakeLib_TESTS testCMFilesystemPath.cxx)
 endif()
@@ -78,3 +88,18 @@
 
 add_executable(testAffinity testAffinity.cxx)
 target_link_libraries(testAffinity CMakeLib)
+
+if(CMake_ENABLE_DEBUGGER)
+  add_executable(testDebuggerNamedPipe testDebuggerNamedPipe.cxx)
+  target_link_libraries(testDebuggerNamedPipe PRIVATE CMakeLib)
+  set(testDebuggerNamedPipe_Project_ARGS
+    "$<TARGET_FILE:cmake>" ${CMAKE_CURRENT_SOURCE_DIR}/DebuggerSample ${CMAKE_CURRENT_BINARY_DIR}/DebuggerSample
+    )
+  set(testDebuggerNamedPipe_Script_ARGS
+    "$<TARGET_FILE:cmake>" ${CMAKE_CURRENT_SOURCE_DIR}/DebuggerSample/script.cmake
+    )
+  foreach(case Project Script)
+    add_test(NAME CMakeLib.testDebuggerNamedPipe-${case} COMMAND testDebuggerNamedPipe ${testDebuggerNamedPipe_${case}_ARGS})
+    set_property(TEST CMakeLib.testDebuggerNamedPipe-${case} PROPERTY TIMEOUT 300)
+  endforeach()
+endif()
diff --git a/Tests/CMakeLib/DebuggerSample/CMakeLists.txt b/Tests/CMakeLib/DebuggerSample/CMakeLists.txt
new file mode 100644
index 0000000..8f8603a
--- /dev/null
+++ b/Tests/CMakeLib/DebuggerSample/CMakeLists.txt
@@ -0,0 +1,9 @@
+cmake_minimum_required(VERSION 3.26)
+project(DebuggerSample NONE)
+message("Hello CMake Debugger")
+
+# There are concerns that because the debugger uses libuv for pipe
+# communication, libuv may register a SIGCHILD handler that interferes with
+# the existing handler used by kwsys process management. Test this case with a
+# simple external process.
+execute_process(COMMAND "${CMAKE_COMMAND}" -E echo test)
diff --git a/Tests/CMakeLib/DebuggerSample/script.cmake b/Tests/CMakeLib/DebuggerSample/script.cmake
new file mode 100644
index 0000000..4c0c00a
--- /dev/null
+++ b/Tests/CMakeLib/DebuggerSample/script.cmake
@@ -0,0 +1 @@
+message(STATUS "This is an example script")
diff --git a/Tests/CMakeLib/testCommon.h b/Tests/CMakeLib/testCommon.h
new file mode 100644
index 0000000..bd2d54e
--- /dev/null
+++ b/Tests/CMakeLib/testCommon.h
@@ -0,0 +1,30 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include <functional>
+#include <iostream>
+#include <vector>
+
+#define ASSERT_TRUE(x)                                                        \
+  do {                                                                        \
+    if (!(x)) {                                                               \
+      std::cout << "ASSERT_TRUE(" #x ") failed on line " << __LINE__ << "\n"; \
+      return false;                                                           \
+    }                                                                         \
+  } while (false)
+
+inline int runTests(std::vector<std::function<bool()>> const& tests)
+{
+  for (auto const& test : tests) {
+    if (!test()) {
+      return 1;
+    }
+    std::cout << ".";
+  }
+
+  std::cout << " Passed" << std::endl;
+  return 0;
+}
+
+#define BOOL_STRING(b) ((b) ? "TRUE" : "FALSE")
diff --git a/Tests/CMakeLib/testDebugger.h b/Tests/CMakeLib/testDebugger.h
new file mode 100644
index 0000000..8ba21f6
--- /dev/null
+++ b/Tests/CMakeLib/testDebugger.h
@@ -0,0 +1,99 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include <memory>
+#include <vector>
+
+#include "cmDebuggerAdapter.h"
+#include "cmDebuggerProtocol.h"
+#include "cmListFileCache.h"
+#include "cmMessenger.h"
+#include <cmcppdap/include/dap/io.h>
+#include <cmcppdap/include/dap/session.h>
+#include <cmcppdap/include/dap/types.h>
+
+#include "testCommon.h"
+
+#define ASSERT_VARIABLE(x, expectedName, expectedValue, expectedType)         \
+  do {                                                                        \
+    ASSERT_TRUE(x.name == expectedName);                                      \
+    ASSERT_TRUE(x.value == expectedValue);                                    \
+    ASSERT_TRUE(x.type.value() == expectedType);                              \
+    ASSERT_TRUE(x.evaluateName.has_value() == false);                         \
+    if (std::string(expectedType) == "collection") {                          \
+      ASSERT_TRUE(x.variablesReference != 0);                                 \
+    }                                                                         \
+  } while (false)
+
+#define ASSERT_VARIABLE_REFERENCE(x, expectedName, expectedValue,             \
+                                  expectedType, expectedReference)            \
+  do {                                                                        \
+    ASSERT_VARIABLE(x, expectedName, expectedValue, expectedType);            \
+    ASSERT_TRUE(x.variablesReference == (expectedReference));                 \
+  } while (false)
+
+#define ASSERT_VARIABLE_REFERENCE_NOT_ZERO(x, expectedName, expectedValue,    \
+                                           expectedType)                      \
+  do {                                                                        \
+    ASSERT_VARIABLE(x, expectedName, expectedValue, expectedType);            \
+    ASSERT_TRUE(x.variablesReference != 0);                                   \
+  } while (false)
+
+#define ASSERT_BREAKPOINT(x, expectedId, expectedLine, sourcePath,            \
+                          isVerified)                                         \
+  do {                                                                        \
+    ASSERT_TRUE(x.id.has_value());                                            \
+    ASSERT_TRUE(x.id.value() == expectedId);                                  \
+    ASSERT_TRUE(x.line.has_value());                                          \
+    ASSERT_TRUE(x.line.value() == expectedLine);                              \
+    ASSERT_TRUE(x.source.has_value());                                        \
+    ASSERT_TRUE(x.source.value().path.has_value());                           \
+    ASSERT_TRUE(x.source.value().path.value() == sourcePath);                 \
+    ASSERT_TRUE(x.verified == isVerified);                                    \
+  } while (false)
+
+class DebuggerTestHelper
+{
+  std::shared_ptr<dap::ReaderWriter> Client2Debugger = dap::pipe();
+  std::shared_ptr<dap::ReaderWriter> Debugger2Client = dap::pipe();
+
+public:
+  std::unique_ptr<dap::Session> Client = dap::Session::create();
+  std::unique_ptr<dap::Session> Debugger = dap::Session::create();
+  void bind()
+  {
+    auto client2server = dap::pipe();
+    auto server2client = dap::pipe();
+    Client->bind(server2client, client2server);
+    Debugger->bind(client2server, server2client);
+  }
+  std::vector<cmListFileFunction> CreateListFileFunctions(const char* str,
+                                                          const char* filename)
+  {
+    cmMessenger messenger;
+    cmListFileBacktrace backtrace;
+    cmListFile listfile;
+    listfile.ParseString(str, filename, &messenger, backtrace);
+    return listfile.Functions;
+  }
+};
+
+class ScopedThread
+{
+public:
+  template <class... Args>
+  explicit ScopedThread(Args&&... args)
+    : Thread(std::forward<Args>(args)...)
+  {
+  }
+
+  ~ScopedThread()
+  {
+    if (Thread.joinable())
+      Thread.join();
+  }
+
+private:
+  std::thread Thread;
+};
diff --git a/Tests/CMakeLib/testDebuggerAdapter.cxx b/Tests/CMakeLib/testDebuggerAdapter.cxx
new file mode 100644
index 0000000..394986b
--- /dev/null
+++ b/Tests/CMakeLib/testDebuggerAdapter.cxx
@@ -0,0 +1,173 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include <chrono>
+#include <cstdio>
+#include <functional>
+#include <future>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <cm3p/cppdap/future.h>
+#include <cm3p/cppdap/io.h>
+#include <cm3p/cppdap/optional.h>
+#include <cm3p/cppdap/protocol.h>
+#include <cm3p/cppdap/session.h>
+#include <cm3p/cppdap/types.h>
+
+#include "cmDebuggerAdapter.h"
+#include "cmDebuggerProtocol.h"
+#include "cmVersionConfig.h"
+
+#include "testCommon.h"
+#include "testDebugger.h"
+
+class DebuggerLocalConnection : public cmDebugger::cmDebuggerConnection
+{
+public:
+  DebuggerLocalConnection()
+    : ClientToDebugger(dap::pipe())
+    , DebuggerToClient(dap::pipe())
+  {
+  }
+
+  bool StartListening(std::string& errorMessage) override
+  {
+    errorMessage = "";
+    return true;
+  }
+  void WaitForConnection() override {}
+
+  std::shared_ptr<dap::Reader> GetReader() override
+  {
+    return ClientToDebugger;
+  };
+
+  std::shared_ptr<dap::Writer> GetWriter() override
+  {
+    return DebuggerToClient;
+  }
+
+  std::shared_ptr<dap::ReaderWriter> ClientToDebugger;
+  std::shared_ptr<dap::ReaderWriter> DebuggerToClient;
+};
+
+bool testBasicProtocol()
+{
+  std::promise<bool> debuggerAdapterInitializedPromise;
+  std::future<bool> debuggerAdapterInitializedFuture =
+    debuggerAdapterInitializedPromise.get_future();
+
+  std::promise<bool> initializedEventReceivedPromise;
+  std::future<bool> initializedEventReceivedFuture =
+    initializedEventReceivedPromise.get_future();
+
+  std::promise<bool> exitedEventReceivedPromise;
+  std::future<bool> exitedEventReceivedFuture =
+    exitedEventReceivedPromise.get_future();
+
+  std::promise<bool> terminatedEventReceivedPromise;
+  std::future<bool> terminatedEventReceivedFuture =
+    terminatedEventReceivedPromise.get_future();
+
+  std::promise<bool> threadStartedPromise;
+  std::future<bool> threadStartedFuture = threadStartedPromise.get_future();
+
+  std::promise<bool> threadExitedPromise;
+  std::future<bool> threadExitedFuture = threadExitedPromise.get_future();
+
+  std::promise<bool> disconnectResponseReceivedPromise;
+  std::future<bool> disconnectResponseReceivedFuture =
+    disconnectResponseReceivedPromise.get_future();
+
+  auto futureTimeout = std::chrono::seconds(60);
+
+  auto connection = std::make_shared<DebuggerLocalConnection>();
+  std::unique_ptr<dap::Session> client = dap::Session::create();
+  client->registerHandler([&](const dap::InitializedEvent& e) {
+    (void)e;
+    initializedEventReceivedPromise.set_value(true);
+  });
+  client->registerHandler([&](const dap::ExitedEvent& e) {
+    (void)e;
+    exitedEventReceivedPromise.set_value(true);
+  });
+  client->registerHandler([&](const dap::TerminatedEvent& e) {
+    (void)e;
+    terminatedEventReceivedPromise.set_value(true);
+  });
+  client->registerHandler([&](const dap::ThreadEvent& e) {
+    if (e.reason == "started") {
+      threadStartedPromise.set_value(true);
+    } else if (e.reason == "exited") {
+      threadExitedPromise.set_value(true);
+    }
+  });
+
+  client->bind(connection->DebuggerToClient, connection->ClientToDebugger);
+
+  ScopedThread debuggerThread([&]() -> int {
+    std::shared_ptr<cmDebugger::cmDebuggerAdapter> debuggerAdapter =
+      std::make_shared<cmDebugger::cmDebuggerAdapter>(
+        connection, dap::file(stdout, false));
+
+    debuggerAdapterInitializedPromise.set_value(true);
+    debuggerAdapter->ReportExitCode(0);
+
+    // Ensure the disconnectResponse has been received before
+    // destructing debuggerAdapter.
+    ASSERT_TRUE(disconnectResponseReceivedFuture.wait_for(futureTimeout) ==
+                std::future_status::ready);
+    return 0;
+  });
+
+  dap::CMakeInitializeRequest initializeRequest;
+  auto initializeResponse = client->send(initializeRequest).get();
+  ASSERT_TRUE(initializeResponse.response.cmakeVersion.full == CMake_VERSION);
+  ASSERT_TRUE(initializeResponse.response.cmakeVersion.major ==
+              CMake_VERSION_MAJOR);
+  ASSERT_TRUE(initializeResponse.response.cmakeVersion.minor ==
+              CMake_VERSION_MINOR);
+  ASSERT_TRUE(initializeResponse.response.cmakeVersion.patch ==
+              CMake_VERSION_PATCH);
+  ASSERT_TRUE(initializeResponse.response.supportsExceptionInfoRequest);
+  ASSERT_TRUE(
+    initializeResponse.response.exceptionBreakpointFilters.has_value());
+
+  dap::LaunchRequest launchRequest;
+  auto launchResponse = client->send(launchRequest).get();
+  ASSERT_TRUE(!launchResponse.error);
+
+  dap::ConfigurationDoneRequest configurationDoneRequest;
+  auto configurationDoneResponse =
+    client->send(configurationDoneRequest).get();
+  ASSERT_TRUE(!configurationDoneResponse.error);
+
+  ASSERT_TRUE(debuggerAdapterInitializedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+  ASSERT_TRUE(initializedEventReceivedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+  ASSERT_TRUE(threadStartedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+  ASSERT_TRUE(threadExitedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+  ASSERT_TRUE(exitedEventReceivedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+  ASSERT_TRUE(terminatedEventReceivedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+
+  dap::DisconnectRequest disconnectRequest;
+  auto disconnectResponse = client->send(disconnectRequest).get();
+  disconnectResponseReceivedPromise.set_value(true);
+  ASSERT_TRUE(!disconnectResponse.error);
+
+  return true;
+}
+
+int testDebuggerAdapter(int, char*[])
+{
+  return runTests(std::vector<std::function<bool()>>{
+    testBasicProtocol,
+  });
+}
diff --git a/Tests/CMakeLib/testDebuggerAdapterPipe.cxx b/Tests/CMakeLib/testDebuggerAdapterPipe.cxx
new file mode 100644
index 0000000..643661d
--- /dev/null
+++ b/Tests/CMakeLib/testDebuggerAdapterPipe.cxx
@@ -0,0 +1,184 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include <chrono>
+#include <cstdio>
+#include <functional>
+#include <future>
+#include <iostream>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+#include <cm3p/cppdap/future.h>
+#include <cm3p/cppdap/io.h>
+#include <cm3p/cppdap/optional.h>
+#include <cm3p/cppdap/protocol.h>
+#include <cm3p/cppdap/session.h>
+#include <cm3p/cppdap/types.h>
+
+#include "cmDebuggerAdapter.h"
+#include "cmDebuggerPipeConnection.h"
+#include "cmDebuggerProtocol.h"
+#include "cmVersionConfig.h"
+
+#ifdef _WIN32
+#  include "cmCryptoHash.h"
+#  include "cmSystemTools.h"
+#endif
+
+#include "testCommon.h"
+#include "testDebugger.h"
+
+bool testProtocolWithPipes()
+{
+  std::promise<void> debuggerConnectionCreatedPromise;
+  std::future<void> debuggerConnectionCreatedFuture =
+    debuggerConnectionCreatedPromise.get_future();
+
+  std::future<void> startedListeningFuture;
+
+  std::promise<bool> debuggerAdapterInitializedPromise;
+  std::future<bool> debuggerAdapterInitializedFuture =
+    debuggerAdapterInitializedPromise.get_future();
+
+  std::promise<bool> initializedEventReceivedPromise;
+  std::future<bool> initializedEventReceivedFuture =
+    initializedEventReceivedPromise.get_future();
+
+  std::promise<bool> exitedEventReceivedPromise;
+  std::future<bool> exitedEventReceivedFuture =
+    exitedEventReceivedPromise.get_future();
+
+  std::promise<bool> terminatedEventReceivedPromise;
+  std::future<bool> terminatedEventReceivedFuture =
+    terminatedEventReceivedPromise.get_future();
+
+  std::promise<bool> threadStartedPromise;
+  std::future<bool> threadStartedFuture = threadStartedPromise.get_future();
+
+  std::promise<bool> threadExitedPromise;
+  std::future<bool> threadExitedFuture = threadExitedPromise.get_future();
+
+  std::promise<bool> disconnectResponseReceivedPromise;
+  std::future<bool> disconnectResponseReceivedFuture =
+    disconnectResponseReceivedPromise.get_future();
+
+  auto futureTimeout = std::chrono::seconds(60);
+
+#ifdef _WIN32
+  std::string namedPipe = R"(\\.\pipe\LOCAL\CMakeDebuggerPipe2_)" +
+    cmCryptoHash(cmCryptoHash::AlgoSHA256)
+      .HashString(cmSystemTools::GetCurrentWorkingDirectory());
+#else
+  std::string namedPipe = "CMakeDebuggerPipe2";
+#endif
+
+  std::unique_ptr<dap::Session> client = dap::Session::create();
+  client->registerHandler([&](const dap::InitializedEvent& e) {
+    (void)e;
+    initializedEventReceivedPromise.set_value(true);
+  });
+  client->registerHandler([&](const dap::ExitedEvent& e) {
+    (void)e;
+    exitedEventReceivedPromise.set_value(true);
+  });
+  client->registerHandler([&](const dap::TerminatedEvent& e) {
+    (void)e;
+    terminatedEventReceivedPromise.set_value(true);
+  });
+  client->registerHandler([&](const dap::ThreadEvent& e) {
+    if (e.reason == "started") {
+      threadStartedPromise.set_value(true);
+    } else if (e.reason == "exited") {
+      threadExitedPromise.set_value(true);
+    }
+  });
+
+  ScopedThread debuggerThread([&]() -> int {
+    try {
+      auto connection =
+        std::make_shared<cmDebugger::cmDebuggerPipeConnection>(namedPipe);
+      startedListeningFuture = connection->StartedListening.get_future();
+      debuggerConnectionCreatedPromise.set_value();
+      std::shared_ptr<cmDebugger::cmDebuggerAdapter> debuggerAdapter =
+        std::make_shared<cmDebugger::cmDebuggerAdapter>(
+          connection, dap::file(stdout, false));
+
+      debuggerAdapterInitializedPromise.set_value(true);
+      debuggerAdapter->ReportExitCode(0);
+
+      // Ensure the disconnectResponse has been received before
+      // destructing debuggerAdapter.
+      ASSERT_TRUE(disconnectResponseReceivedFuture.wait_for(futureTimeout) ==
+                  std::future_status::ready);
+      return 0;
+    } catch (const std::runtime_error& error) {
+      std::cerr << "Error: Failed to create debugger adapter.\n";
+      std::cerr << error.what() << "\n";
+      return -1;
+    }
+  });
+
+  ASSERT_TRUE(debuggerConnectionCreatedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+  ASSERT_TRUE(startedListeningFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+
+  auto client2Debugger =
+    std::make_shared<cmDebugger::cmDebuggerPipeClient>(namedPipe);
+  client2Debugger->Start();
+  client2Debugger->WaitForConnection();
+  client->bind(client2Debugger, client2Debugger);
+
+  dap::CMakeInitializeRequest initializeRequest;
+  auto response = client->send(initializeRequest);
+  auto initializeResponse = response.get();
+  ASSERT_TRUE(!initializeResponse.error);
+  ASSERT_TRUE(initializeResponse.response.cmakeVersion.full == CMake_VERSION);
+  ASSERT_TRUE(initializeResponse.response.cmakeVersion.major ==
+              CMake_VERSION_MAJOR);
+  ASSERT_TRUE(initializeResponse.response.cmakeVersion.minor ==
+              CMake_VERSION_MINOR);
+  ASSERT_TRUE(initializeResponse.response.cmakeVersion.patch ==
+              CMake_VERSION_PATCH);
+  ASSERT_TRUE(initializeResponse.response.supportsExceptionInfoRequest);
+  ASSERT_TRUE(
+    initializeResponse.response.exceptionBreakpointFilters.has_value());
+  dap::LaunchRequest launchRequest;
+  auto launchResponse = client->send(launchRequest).get();
+  ASSERT_TRUE(!launchResponse.error);
+
+  dap::ConfigurationDoneRequest configurationDoneRequest;
+  auto configurationDoneResponse =
+    client->send(configurationDoneRequest).get();
+  ASSERT_TRUE(!configurationDoneResponse.error);
+
+  ASSERT_TRUE(debuggerAdapterInitializedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+  ASSERT_TRUE(initializedEventReceivedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+  ASSERT_TRUE(terminatedEventReceivedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+  ASSERT_TRUE(threadStartedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+  ASSERT_TRUE(threadExitedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+  ASSERT_TRUE(exitedEventReceivedFuture.wait_for(futureTimeout) ==
+              std::future_status::ready);
+
+  dap::DisconnectRequest disconnectRequest;
+  auto disconnectResponse = client->send(disconnectRequest).get();
+  disconnectResponseReceivedPromise.set_value(true);
+  ASSERT_TRUE(!disconnectResponse.error);
+
+  return true;
+}
+
+int testDebuggerAdapterPipe(int, char*[])
+{
+  return runTests(std::vector<std::function<bool()>>{
+    testProtocolWithPipes,
+  });
+}
diff --git a/Tests/CMakeLib/testDebuggerBreakpointManager.cxx b/Tests/CMakeLib/testDebuggerBreakpointManager.cxx
new file mode 100644
index 0000000..83734ea
--- /dev/null
+++ b/Tests/CMakeLib/testDebuggerBreakpointManager.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 <atomic>
+#include <chrono>
+#include <functional>
+#include <future>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <cm3p/cppdap/future.h>
+#include <cm3p/cppdap/optional.h>
+#include <cm3p/cppdap/protocol.h>
+#include <cm3p/cppdap/session.h>
+#include <cm3p/cppdap/types.h>
+
+#include "cmDebuggerBreakpointManager.h"
+#include "cmDebuggerSourceBreakpoint.h" // IWYU pragma: keep
+#include "cmListFileCache.h"
+
+#include "testCommon.h"
+#include "testDebugger.h"
+
+static bool testHandleBreakpointRequestBeforeFileIsLoaded()
+{
+  // Arrange
+  DebuggerTestHelper helper;
+  cmDebugger::cmDebuggerBreakpointManager breakpointManager(
+    helper.Debugger.get());
+  helper.bind();
+  dap::SetBreakpointsRequest setBreakpointRequest;
+  std::string sourcePath = "C:/CMakeLists.txt";
+  setBreakpointRequest.source.path = sourcePath;
+  dap::array<dap::SourceBreakpoint> sourceBreakpoints(3);
+  sourceBreakpoints[0].line = 1;
+  sourceBreakpoints[1].line = 2;
+  sourceBreakpoints[2].line = 3;
+  setBreakpointRequest.breakpoints = sourceBreakpoints;
+
+  // Act
+  auto got = helper.Client->send(setBreakpointRequest).get();
+
+  // Assert
+  auto& response = got.response;
+  ASSERT_TRUE(!got.error);
+  ASSERT_TRUE(response.breakpoints.size() == sourceBreakpoints.size());
+  ASSERT_BREAKPOINT(response.breakpoints[0], 0, sourceBreakpoints[0].line,
+                    sourcePath, false);
+  ASSERT_BREAKPOINT(response.breakpoints[1], 1, sourceBreakpoints[1].line,
+                    sourcePath, false);
+  ASSERT_BREAKPOINT(response.breakpoints[2], 2, sourceBreakpoints[2].line,
+                    sourcePath, false);
+  return true;
+}
+
+static bool testHandleBreakpointRequestAfterFileIsLoaded()
+{
+  // Arrange
+  DebuggerTestHelper helper;
+  std::atomic<bool> notExpectBreakpointEvents(true);
+  helper.Client->registerHandler([&](const dap::BreakpointEvent&) {
+    notExpectBreakpointEvents.store(false);
+  });
+
+  cmDebugger::cmDebuggerBreakpointManager breakpointManager(
+    helper.Debugger.get());
+  helper.bind();
+  std::string sourcePath = "C:/CMakeLists.txt";
+  std::vector<cmListFileFunction> functions = helper.CreateListFileFunctions(
+    "# Comment1\nset(var1 foo)\n# Comment2\nset(var2\nbar)\n",
+    sourcePath.c_str());
+
+  breakpointManager.SourceFileLoaded(sourcePath, functions);
+  dap::SetBreakpointsRequest setBreakpointRequest;
+  setBreakpointRequest.source.path = sourcePath;
+  dap::array<dap::SourceBreakpoint> sourceBreakpoints(5);
+  sourceBreakpoints[0].line = 1;
+  sourceBreakpoints[1].line = 2;
+  sourceBreakpoints[2].line = 3;
+  sourceBreakpoints[3].line = 4;
+  sourceBreakpoints[4].line = 5;
+  setBreakpointRequest.breakpoints = sourceBreakpoints;
+
+  // Act
+  auto got = helper.Client->send(setBreakpointRequest).get();
+
+  // Assert
+  auto& response = got.response;
+  ASSERT_TRUE(!got.error);
+  ASSERT_TRUE(response.breakpoints.size() == sourceBreakpoints.size());
+  // Line 1 is a comment. Move it to next valid function, which is line 2.
+  ASSERT_BREAKPOINT(response.breakpoints[0], 0, 2, sourcePath, true);
+  ASSERT_BREAKPOINT(response.breakpoints[1], 1, sourceBreakpoints[1].line,
+                    sourcePath, true);
+  // Line 3 is a comment. Move it to next valid function, which is line 4.
+  ASSERT_BREAKPOINT(response.breakpoints[2], 2, 4, sourcePath, true);
+  ASSERT_BREAKPOINT(response.breakpoints[3], 3, sourceBreakpoints[3].line,
+                    sourcePath, true);
+  // Line 5 is the 2nd part of line 4 function. No valid function after line 5,
+  // show the breakpoint at line 4.
+  ASSERT_BREAKPOINT(response.breakpoints[4], 4, sourceBreakpoints[3].line,
+                    sourcePath, true);
+
+  ASSERT_TRUE(notExpectBreakpointEvents.load());
+
+  return true;
+}
+
+static bool testSourceFileLoadedAfterHandleBreakpointRequest()
+{
+  // Arrange
+  DebuggerTestHelper helper;
+  std::vector<dap::BreakpointEvent> breakpointEvents;
+  std::atomic<int> remainingBreakpointEvents(5);
+  std::promise<void> allBreakpointEventsReceivedPromise;
+  std::future<void> allBreakpointEventsReceivedFuture =
+    allBreakpointEventsReceivedPromise.get_future();
+  helper.Client->registerHandler([&](const dap::BreakpointEvent& event) {
+    breakpointEvents.emplace_back(event);
+    if (--remainingBreakpointEvents == 0) {
+      allBreakpointEventsReceivedPromise.set_value();
+    }
+  });
+  cmDebugger::cmDebuggerBreakpointManager breakpointManager(
+    helper.Debugger.get());
+  helper.bind();
+  dap::SetBreakpointsRequest setBreakpointRequest;
+  std::string sourcePath = "C:/CMakeLists.txt";
+  setBreakpointRequest.source.path = sourcePath;
+  dap::array<dap::SourceBreakpoint> sourceBreakpoints(5);
+  sourceBreakpoints[0].line = 1;
+  sourceBreakpoints[1].line = 2;
+  sourceBreakpoints[2].line = 3;
+  sourceBreakpoints[3].line = 4;
+  sourceBreakpoints[4].line = 5;
+  setBreakpointRequest.breakpoints = sourceBreakpoints;
+  std::vector<cmListFileFunction> functions = helper.CreateListFileFunctions(
+    "# Comment1\nset(var1 foo)\n# Comment2\nset(var2\nbar)\n",
+    sourcePath.c_str());
+  auto got = helper.Client->send(setBreakpointRequest).get();
+
+  // Act
+  breakpointManager.SourceFileLoaded(sourcePath, functions);
+  ASSERT_TRUE(allBreakpointEventsReceivedFuture.wait_for(
+                std::chrono::seconds(10)) == std::future_status::ready);
+
+  // Assert
+  ASSERT_TRUE(breakpointEvents.size() > 0);
+  // Line 1 is a comment. Move it to next valid function, which is line 2.
+  ASSERT_BREAKPOINT(breakpointEvents[0].breakpoint, 0, 2, sourcePath, true);
+  ASSERT_BREAKPOINT(breakpointEvents[1].breakpoint, 1,
+                    sourceBreakpoints[1].line, sourcePath, true);
+  // Line 3 is a comment. Move it to next valid function, which is line 4.
+  ASSERT_BREAKPOINT(breakpointEvents[2].breakpoint, 2, 4, sourcePath, true);
+  ASSERT_BREAKPOINT(breakpointEvents[3].breakpoint, 3,
+                    sourceBreakpoints[3].line, sourcePath, true);
+  // Line 5 is the 2nd part of line 4 function. No valid function after line 5,
+  // show the breakpoint at line 4.
+  ASSERT_BREAKPOINT(breakpointEvents[4].breakpoint, 4,
+                    sourceBreakpoints[3].line, sourcePath, true);
+  return true;
+}
+
+int testDebuggerBreakpointManager(int, char*[])
+{
+  return runTests(std::vector<std::function<bool()>>{
+    testHandleBreakpointRequestBeforeFileIsLoaded,
+    testHandleBreakpointRequestAfterFileIsLoaded,
+    testSourceFileLoadedAfterHandleBreakpointRequest,
+  });
+}
diff --git a/Tests/CMakeLib/testDebuggerNamedPipe.cxx b/Tests/CMakeLib/testDebuggerNamedPipe.cxx
new file mode 100644
index 0000000..d2b0728
--- /dev/null
+++ b/Tests/CMakeLib/testDebuggerNamedPipe.cxx
@@ -0,0 +1,218 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include <chrono>
+#include <cstdio>
+#include <exception>
+#include <iostream>
+#include <memory>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <cm3p/cppdap/io.h>
+
+#include "cmsys/RegularExpression.hxx"
+
+#include "cmDebuggerPipeConnection.h"
+#include "cmSystemTools.h"
+
+#ifdef _WIN32
+#  include "cmCryptoHash.h"
+#endif
+
+static void sendCommands(std::shared_ptr<dap::ReaderWriter> const& debugger,
+                         int delayMs,
+                         std::vector<std::string> const& initCommands)
+{
+  for (const auto& command : initCommands) {
+    std::string contentLength = "Content-Length:";
+    contentLength += std::to_string(command.size()) + "\r\n\r\n";
+    debugger->write(contentLength.c_str(), contentLength.size());
+    if (!debugger->write(command.c_str(), command.size())) {
+      std::cout << "debugger write error" << std::endl;
+      break;
+    }
+    std::this_thread::sleep_for(std::chrono::milliseconds(delayMs));
+  }
+}
+
+/** \brief Test CMake debugger named pipe.
+ *
+ * Test CMake debugger named pipe by
+ * 1. Create a named pipe for DAP traffic between the client and the debugger.
+ * 2. Create a client thread to wait for the debugger connection.
+ *    - Once the debugger is connected, send the minimum required commands to
+ *      get debugger going.
+ *    - Wait for the CMake to complete the cache generation
+ *    - Send the disconnect command.
+ *    - Read and store the debugger's responses for validation.
+ * 3. Run the CMake command with debugger on and wait for it to complete.
+ * 4. Validate the response to ensure we are getting the expected responses.
+ *
+ */
+int runTest(int argc, char* argv[])
+{
+  if (argc < 3) {
+    std::cout << "Usage:\n";
+    std::cout << "\t(project mode) TestDebuggerNamedPipe <CMakePath> "
+                 "<SourceFolder> <OutputFolder>\n";
+    std::cout << "\t(script mode) TestDebuggerNamedPipe <CMakePath> "
+                 "<ScriptPath>\n";
+    return 1;
+  }
+
+  bool scriptMode = argc == 3;
+
+#ifdef _WIN32
+  std::string namedPipe = R"(\\.\pipe\LOCAL\CMakeDebuggerPipe_)" +
+    cmCryptoHash(cmCryptoHash::AlgoSHA256)
+      .HashString(scriptMode ? argv[2] : argv[3]);
+#else
+  std::string namedPipe =
+    std::string("CMakeDebuggerPipe") + (scriptMode ? "Script" : "Project");
+#endif
+
+  std::vector<std::string> cmakeCommand;
+  cmakeCommand.emplace_back(argv[1]);
+  cmakeCommand.emplace_back("--debugger");
+  cmakeCommand.emplace_back("--debugger-pipe");
+  cmakeCommand.emplace_back(namedPipe);
+
+  if (scriptMode) {
+    cmakeCommand.emplace_back("-P");
+    cmakeCommand.emplace_back(argv[2]);
+  } else {
+    cmakeCommand.emplace_back("-S");
+    cmakeCommand.emplace_back(argv[2]);
+    cmakeCommand.emplace_back("-B");
+    cmakeCommand.emplace_back(argv[3]);
+  }
+
+  // Capture debugger response stream.
+  std::stringstream debuggerResponseStream;
+
+  // Start the debugger client process.
+  std::thread clientThread([&]() {
+    // Poll until the pipe server is running. Clients can also look for a magic
+    // string in the CMake output, but this is easier for the test case.
+    std::shared_ptr<cmDebugger::cmDebuggerPipeClient> client;
+    int attempt = 0;
+    do {
+      attempt++;
+      try {
+        client = std::make_shared<cmDebugger::cmDebuggerPipeClient>(namedPipe);
+        client->Start();
+        client->WaitForConnection();
+        std::cout << "cmDebuggerPipeClient connected.\n";
+        break;
+      } catch (std::runtime_error&) {
+        std::cout << "Failed attempt " << attempt
+                  << " to connect to pipe server. Retrying.\n";
+        client.reset();
+        std::this_thread::sleep_for(std::chrono::milliseconds(200));
+      }
+    } while (attempt < 50); // 10 seconds
+
+    if (attempt >= 50) {
+      return -1;
+    }
+
+    // Send init commands to get debugger going.
+    sendCommands(
+      client, 400,
+      { "{\"arguments\":{\"adapterID\":\"\"},\"command\":\"initialize\","
+        "\"seq\":"
+        "1,\"type\":\"request\"}",
+        "{\"arguments\":{},\"command\":\"launch\",\"seq\":2,\"type\":"
+        "\"request\"}",
+        "{\"arguments\":{},\"command\":\"configurationDone\",\"seq\":3,"
+        "\"type\":"
+        "\"request\"}" });
+
+    // Look for "exitCode" as a sign that configuration has completed and
+    // it's now safe to disconnect.
+    for (;;) {
+      char buffer[1];
+      size_t result = client->read(buffer, 1);
+      if (result != 1) {
+        std::cout << "debugger read error: " << result << std::endl;
+        break;
+      }
+      debuggerResponseStream << buffer[0];
+      if (debuggerResponseStream.str().find("exitCode") != std::string::npos) {
+        break;
+      }
+    }
+
+    // Send disconnect command.
+    sendCommands(
+      client, 200,
+      { "{\"arguments\":{},\"command\":\"disconnect\",\"seq\":4,\"type\":"
+        "\"request\"}" });
+
+    // Read any remaining debugger responses.
+    for (;;) {
+      char buffer[1];
+      size_t result = client->read(buffer, 1);
+      if (result != 1) {
+        std::cout << "debugger read error: " << result << std::endl;
+        break;
+      }
+      debuggerResponseStream << buffer[0];
+    }
+
+    client->close();
+
+    return 0;
+  });
+
+  if (!cmSystemTools::RunSingleCommand(cmakeCommand, nullptr, nullptr, nullptr,
+                                       nullptr, cmSystemTools::OUTPUT_MERGE)) {
+    std::cout << "Error running command" << std::endl;
+    return -1;
+  }
+
+  clientThread.join();
+
+  auto debuggerResponse = debuggerResponseStream.str();
+
+  std::vector<std::string> expectedResponses = {
+    R"("event" : "initialized".*"type" : "event")",
+    R"("command" : "launch".*"success" : true.*"type" : "response")",
+    R"("command" : "configurationDone".*"success" : true.*"type" : "response")",
+    R"("reason" : "started".*"threadId" : 1.*"event" : "thread".*"type" : "event")",
+    R"("reason" : "exited".*"threadId" : 1.*"event" : "thread".*"type" : "event")",
+    R"("exitCode" : 0.*"event" : "exited".*"type" : "event")",
+    R"("command" : "disconnect".*"success" : true.*"type" : "response")"
+  };
+
+  for (auto& regexString : expectedResponses) {
+    cmsys::RegularExpression regex(regexString);
+    if (!regex.find(debuggerResponse)) {
+      std::cout << "Expected response not found: " << regexString << std::endl;
+      std::cout << debuggerResponse << std::endl;
+      return -1;
+    }
+  }
+
+  return 0;
+}
+
+int main(int argc, char* argv[])
+{
+  try {
+    return runTest(argc, argv);
+  } catch (const std::exception& ex) {
+    std::cout << "An exception occurred: " << ex.what() << std::endl;
+    return -1;
+  } catch (const std::string& ex) {
+    std::cout << "An exception occurred: " << ex << std::endl;
+    return -1;
+  } catch (...) {
+    std::cout << "An unknown exception occurred" << std::endl;
+    return -1;
+  }
+}
diff --git a/Tests/CMakeLib/testDebuggerVariables.cxx b/Tests/CMakeLib/testDebuggerVariables.cxx
new file mode 100644
index 0000000..6c19baa
--- /dev/null
+++ b/Tests/CMakeLib/testDebuggerVariables.cxx
@@ -0,0 +1,185 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <string>
+#include <unordered_set>
+#include <vector>
+
+#include <cm3p/cppdap/protocol.h>
+#include <cm3p/cppdap/types.h>
+
+#include "cmDebuggerVariables.h"
+#include "cmDebuggerVariablesManager.h"
+
+#include "testCommon.h"
+#include "testDebugger.h"
+
+static dap::VariablesRequest CreateVariablesRequest(int64_t reference)
+{
+  dap::VariablesRequest variableRequest;
+  variableRequest.variablesReference = reference;
+  return variableRequest;
+}
+
+static bool testUniqueIds()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  std::unordered_set<int64_t> variableIds;
+  bool noDuplicateIds = true;
+  for (int i = 0; i < 10000 && noDuplicateIds; ++i) {
+    auto variable =
+      cmDebugger::cmDebuggerVariables(variablesManager, "Locals", true, []() {
+        return std::vector<cmDebugger::cmDebuggerVariableEntry>();
+      });
+
+    if (variableIds.find(variable.GetId()) != variableIds.end()) {
+      noDuplicateIds = false;
+    }
+    variableIds.insert(variable.GetId());
+  }
+
+  ASSERT_TRUE(noDuplicateIds);
+
+  return true;
+}
+
+static bool testConstructors()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  auto parent = std::make_shared<cmDebugger::cmDebuggerVariables>(
+    variablesManager, "Parent", true, [=]() {
+      return std::vector<cmDebugger::cmDebuggerVariableEntry>{
+        { "ParentKey", "ParentValue", "ParentType" }
+      };
+    });
+
+  auto children1 = std::make_shared<cmDebugger::cmDebuggerVariables>(
+    variablesManager, "Children1", true, [=]() {
+      return std::vector<cmDebugger::cmDebuggerVariableEntry>{
+        { "ChildKey1", "ChildValue1", "ChildType1" },
+        { "ChildKey2", "ChildValue2", "ChildType2" }
+      };
+    });
+
+  parent->AddSubVariables(children1);
+
+  auto children2 = std::make_shared<cmDebugger::cmDebuggerVariables>(
+    variablesManager, "Children2", true);
+
+  auto grandChildren21 = std::make_shared<cmDebugger::cmDebuggerVariables>(
+    variablesManager, "GrandChildren21", true);
+  grandChildren21->SetValue("GrandChildren21 Value");
+  children2->AddSubVariables(grandChildren21);
+  parent->AddSubVariables(children2);
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(parent->GetId()));
+  ASSERT_TRUE(variables.size() == 3);
+  ASSERT_VARIABLE_REFERENCE(variables[0], "Children1", "", "collection",
+                            children1->GetId());
+  ASSERT_VARIABLE_REFERENCE(variables[1], "Children2", "", "collection",
+                            children2->GetId());
+  ASSERT_VARIABLE(variables[2], "ParentKey", "ParentValue", "ParentType");
+
+  variables = variablesManager->HandleVariablesRequest(
+    CreateVariablesRequest(children1->GetId()));
+  ASSERT_TRUE(variables.size() == 2);
+  ASSERT_VARIABLE(variables[0], "ChildKey1", "ChildValue1", "ChildType1");
+  ASSERT_VARIABLE(variables[1], "ChildKey2", "ChildValue2", "ChildType2");
+
+  variables = variablesManager->HandleVariablesRequest(
+    CreateVariablesRequest(children2->GetId()));
+  ASSERT_TRUE(variables.size() == 1);
+  ASSERT_VARIABLE_REFERENCE(variables[0], "GrandChildren21",
+                            "GrandChildren21 Value", "collection",
+                            grandChildren21->GetId());
+
+  return true;
+}
+
+static bool testIgnoreEmptyStringEntries()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  auto vars = std::make_shared<cmDebugger::cmDebuggerVariables>(
+    variablesManager, "Variables", true, []() {
+      return std::vector<cmDebugger::cmDebuggerVariableEntry>{
+        { "IntValue1", 5 },           { "StringValue1", "" },
+        { "StringValue2", "foo" },    { "StringValue3", "" },
+        { "StringValue4", "bar" },    { "StringValue5", "" },
+        { "IntValue2", int64_t(99) }, { "BooleanTrue", true },
+        { "BooleanFalse", false },
+      };
+    });
+
+  vars->SetIgnoreEmptyStringEntries(true);
+  vars->SetEnableSorting(false);
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+  ASSERT_TRUE(variables.size() == 6);
+  ASSERT_VARIABLE(variables[0], "IntValue1", "5", "int");
+  ASSERT_VARIABLE(variables[1], "StringValue2", "foo", "string");
+  ASSERT_VARIABLE(variables[2], "StringValue4", "bar", "string");
+  ASSERT_VARIABLE(variables[3], "IntValue2", "99", "int");
+  ASSERT_VARIABLE(variables[4], "BooleanTrue", "TRUE", "bool");
+  ASSERT_VARIABLE(variables[5], "BooleanFalse", "FALSE", "bool");
+
+  return true;
+}
+
+static bool testSortTheResult()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  auto vars = std::make_shared<cmDebugger::cmDebuggerVariables>(
+    variablesManager, "Variables", true, []() {
+      return std::vector<cmDebugger::cmDebuggerVariableEntry>{
+        { "4", "4" }, { "2", "2" }, { "1", "1" }, { "3", "3" }, { "5", "5" },
+      };
+    });
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+  ASSERT_TRUE(variables.size() == 5);
+  ASSERT_VARIABLE(variables[0], "1", "1", "string");
+  ASSERT_VARIABLE(variables[1], "2", "2", "string");
+  ASSERT_VARIABLE(variables[2], "3", "3", "string");
+  ASSERT_VARIABLE(variables[3], "4", "4", "string");
+  ASSERT_VARIABLE(variables[4], "5", "5", "string");
+
+  vars->SetEnableSorting(false);
+
+  variables = variablesManager->HandleVariablesRequest(
+    CreateVariablesRequest(vars->GetId()));
+  ASSERT_TRUE(variables.size() == 5);
+  ASSERT_VARIABLE(variables[0], "4", "4", "string");
+  ASSERT_VARIABLE(variables[1], "2", "2", "string");
+  ASSERT_VARIABLE(variables[2], "1", "1", "string");
+  ASSERT_VARIABLE(variables[3], "3", "3", "string");
+  ASSERT_VARIABLE(variables[4], "5", "5", "string");
+
+  return true;
+}
+
+int testDebuggerVariables(int, char*[])
+{
+  return runTests(std::vector<std::function<bool()>>{
+    testUniqueIds,
+    testConstructors,
+    testIgnoreEmptyStringEntries,
+    testSortTheResult,
+  });
+}
diff --git a/Tests/CMakeLib/testDebuggerVariablesHelper.cxx b/Tests/CMakeLib/testDebuggerVariablesHelper.cxx
new file mode 100644
index 0000000..e0bbdf0
--- /dev/null
+++ b/Tests/CMakeLib/testDebuggerVariablesHelper.cxx
@@ -0,0 +1,587 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include <functional>
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <cm3p/cppdap/protocol.h>
+#include <cm3p/cppdap/types.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "cmDebuggerStackFrame.h"
+#include "cmDebuggerVariables.h"
+#include "cmDebuggerVariablesHelper.h"
+#include "cmDebuggerVariablesManager.h"
+#include "cmFileSet.h"
+#include "cmGlobalGenerator.h"
+#include "cmListFileCache.h"
+#include "cmMakefile.h"
+#include "cmPolicies.h"
+#include "cmPropertyMap.h"
+#include "cmState.h"
+#include "cmStateDirectory.h"
+#include "cmStateSnapshot.h"
+#include "cmStateTypes.h"
+#include "cmTarget.h"
+#include "cmTest.h"
+#include "cmake.h"
+
+#include "testCommon.h"
+#include "testDebugger.h"
+
+static dap::VariablesRequest CreateVariablesRequest(int64_t reference)
+{
+  dap::VariablesRequest variableRequest;
+  variableRequest.variablesReference = reference;
+  return variableRequest;
+}
+
+struct Dummies
+{
+  std::shared_ptr<cmake> CMake;
+  std::shared_ptr<cmMakefile> Makefile;
+  std::shared_ptr<cmGlobalGenerator> GlobalGenerator;
+};
+
+static Dummies CreateDummies(
+  std::string targetName,
+  std::string currentSourceDirectory = "c:/CurrentSourceDirectory",
+  std::string currentBinaryDirectory = "c:/CurrentBinaryDirectory")
+{
+  Dummies dummies;
+  dummies.CMake =
+    std::make_shared<cmake>(cmake::RoleProject, cmState::Project);
+  cmState* state = dummies.CMake->GetState();
+  dummies.GlobalGenerator =
+    std::make_shared<cmGlobalGenerator>(dummies.CMake.get());
+  cmStateSnapshot snapshot = state->CreateBaseSnapshot();
+  snapshot.GetDirectory().SetCurrentSource(currentSourceDirectory);
+  snapshot.GetDirectory().SetCurrentBinary(currentBinaryDirectory);
+  dummies.Makefile =
+    std::make_shared<cmMakefile>(dummies.GlobalGenerator.get(), snapshot);
+  dummies.Makefile->CreateNewTarget(targetName, cmStateEnums::EXECUTABLE);
+  return dummies;
+}
+
+static bool testCreateFromPolicyMap()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  cmPolicies::PolicyMap policyMap;
+  policyMap.Set(cmPolicies::CMP0000, cmPolicies::NEW);
+  policyMap.Set(cmPolicies::CMP0003, cmPolicies::WARN);
+  policyMap.Set(cmPolicies::CMP0005, cmPolicies::OLD);
+  auto vars = cmDebugger::cmDebuggerVariablesHelper::Create(
+    variablesManager, "Locals", true, policyMap);
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+  ASSERT_TRUE(variables.size() == 3);
+  ASSERT_VARIABLE(variables[0], "CMP0000", "NEW", "string");
+  ASSERT_VARIABLE(variables[1], "CMP0003", "WARN", "string");
+  ASSERT_VARIABLE(variables[2], "CMP0005", "OLD", "string");
+
+  return true;
+}
+
+static bool testCreateFromPairVector()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  std::vector<std::pair<std::string, std::string>> pairs = {
+    { "Foo1", "Bar1" }, { "Foo2", "Bar2" }
+  };
+
+  auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, pairs);
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+
+  ASSERT_TRUE(vars->GetValue() == std::to_string(pairs.size()));
+  ASSERT_TRUE(variables.size() == 2);
+  ASSERT_VARIABLE(variables[0], "Foo1", "Bar1", "string");
+  ASSERT_VARIABLE(variables[1], "Foo2", "Bar2", "string");
+
+  auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true,
+    std::vector<std::pair<std::string, std::string>>());
+
+  ASSERT_TRUE(none == nullptr);
+
+  return true;
+}
+
+static bool testCreateFromSet()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  std::set<std::string> set = { "Foo", "Bar" };
+
+  auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, set);
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+
+  ASSERT_TRUE(vars->GetValue() == std::to_string(set.size()));
+  ASSERT_TRUE(variables.size() == 2);
+  ASSERT_VARIABLE(variables[0], "[0]", "Bar", "string");
+  ASSERT_VARIABLE(variables[1], "[1]", "Foo", "string");
+
+  auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, std::set<std::string>());
+
+  ASSERT_TRUE(none == nullptr);
+
+  return true;
+}
+
+static bool testCreateFromStringVector()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  std::vector<std::string> list = { "Foo", "Bar" };
+
+  auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, list);
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+
+  ASSERT_TRUE(vars->GetValue() == std::to_string(list.size()));
+  ASSERT_TRUE(variables.size() == 2);
+  ASSERT_VARIABLE(variables[0], "[0]", "Foo", "string");
+  ASSERT_VARIABLE(variables[1], "[1]", "Bar", "string");
+
+  auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, std::vector<std::string>());
+
+  ASSERT_TRUE(none == nullptr);
+
+  return true;
+}
+
+static bool testCreateFromTarget()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  auto dummies = CreateDummies("Foo");
+
+  auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, dummies.Makefile->GetOrderedTargets());
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+
+  ASSERT_TRUE(variables.size() == 1);
+  ASSERT_VARIABLE(variables[0], "Foo", "EXECUTABLE", "collection");
+
+  variables = variablesManager->HandleVariablesRequest(
+    CreateVariablesRequest(variables[0].variablesReference));
+
+  ASSERT_TRUE(variables.size() == 15);
+  ASSERT_VARIABLE(variables[0], "GlobalGenerator", "Generic", "collection");
+  ASSERT_VARIABLE(variables[1], "IsAIX", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[2], "IsAndroidGuiExecutable", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[3], "IsAppBundleOnApple", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[4], "IsDLLPlatform", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[5], "IsExecutableWithExports", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[6], "IsFrameworkOnApple", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[7], "IsImported", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[8], "IsImportedGloballyVisible", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[9], "IsPerConfig", "TRUE", "bool");
+  ASSERT_VARIABLE(variables[10], "Makefile",
+                  dummies.Makefile->GetDirectoryId().String, "collection");
+  ASSERT_VARIABLE(variables[11], "Name", "Foo", "string");
+  ASSERT_VARIABLE(variables[12], "PolicyMap", "", "collection");
+  ASSERT_VARIABLE(variables[13], "Properties",
+                  std::to_string(dummies.Makefile->GetOrderedTargets()[0]
+                                   ->GetProperties()
+                                   .GetList()
+                                   .size()),
+                  "collection");
+  ASSERT_VARIABLE(variables[14], "Type", "EXECUTABLE", "string");
+
+  auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, std::vector<cmTarget*>());
+
+  ASSERT_TRUE(none == nullptr);
+
+  return true;
+}
+
+static bool testCreateFromGlobalGenerator()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  auto dummies = CreateDummies("Foo");
+
+  auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, dummies.GlobalGenerator.get());
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+
+  ASSERT_TRUE(variables.size() == 10);
+  ASSERT_VARIABLE(variables[0], "AllTargetName", "ALL_BUILD", "string");
+  ASSERT_VARIABLE(variables[1], "ForceUnixPaths", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[2], "InstallTargetName", "INSTALL", "string");
+  ASSERT_VARIABLE(variables[3], "IsMultiConfig", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[4], "MakefileEncoding", "None", "string");
+  ASSERT_VARIABLE(variables[5], "Name", "Generic", "string");
+  ASSERT_VARIABLE(variables[6], "NeedSymbolicMark", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[7], "PackageTargetName", "PACKAGE", "string");
+  ASSERT_VARIABLE(variables[8], "TestTargetName", "RUN_TESTS", "string");
+  ASSERT_VARIABLE(variables[9], "UseLinkScript", "FALSE", "bool");
+
+  auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true,
+    static_cast<cmGlobalGenerator*>(nullptr));
+
+  ASSERT_TRUE(none == nullptr);
+
+  return true;
+}
+
+static bool testCreateFromTests()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  auto dummies = CreateDummies("Foo");
+  cmTest test1 = cmTest(dummies.Makefile.get());
+  test1.SetName("Test1");
+  test1.SetOldStyle(false);
+  test1.SetCommandExpandLists(true);
+  test1.SetCommand(std::vector<std::string>{ "Foo1", "arg1" });
+  test1.SetProperty("Prop1", "Prop1");
+  cmTest test2 = cmTest(dummies.Makefile.get());
+  test2.SetName("Test2");
+  test2.SetOldStyle(false);
+  test2.SetCommandExpandLists(false);
+  test2.SetCommand(std::vector<std::string>{ "Bar1", "arg1", "arg2" });
+  test2.SetProperty("Prop2", "Prop2");
+  test2.SetProperty("Prop3", "Prop3");
+
+  std::vector<cmTest*> tests = { &test1, &test2 };
+
+  auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, tests);
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+
+  ASSERT_TRUE(vars->GetValue() == std::to_string(tests.size()));
+  ASSERT_TRUE(variables.size() == 2);
+  ASSERT_VARIABLE_REFERENCE_NOT_ZERO(variables[0], test1.GetName(), "",
+                                     "collection");
+  ASSERT_VARIABLE_REFERENCE_NOT_ZERO(variables[1], test2.GetName(), "",
+                                     "collection");
+
+  dap::array<dap::Variable> testVariables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(variables[0].variablesReference));
+  ASSERT_TRUE(testVariables.size() == 5);
+  ASSERT_VARIABLE_REFERENCE_NOT_ZERO(testVariables[0], "Command",
+                                     std::to_string(test1.GetCommand().size()),
+                                     "collection");
+  ASSERT_VARIABLE(testVariables[1], "CommandExpandLists",
+                  BOOL_STRING(test1.GetCommandExpandLists()), "bool");
+  ASSERT_VARIABLE(testVariables[2], "Name", test1.GetName(), "string");
+  ASSERT_VARIABLE(testVariables[3], "OldStyle",
+                  BOOL_STRING(test1.GetOldStyle()), "bool");
+  ASSERT_VARIABLE_REFERENCE_NOT_ZERO(testVariables[4], "Properties", "1",
+                                     "collection");
+
+  dap::array<dap::Variable> commandVariables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(testVariables[0].variablesReference));
+  ASSERT_TRUE(commandVariables.size() == test1.GetCommand().size());
+  for (size_t i = 0; i < commandVariables.size(); ++i) {
+    ASSERT_VARIABLE(commandVariables[i], "[" + std::to_string(i) + "]",
+                    test1.GetCommand()[i], "string");
+  }
+
+  dap::array<dap::Variable> propertiesVariables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(testVariables[4].variablesReference));
+  ASSERT_TRUE(propertiesVariables.size() == 1);
+  ASSERT_VARIABLE(propertiesVariables[0], "Prop1", "Prop1", "string");
+
+  testVariables = variablesManager->HandleVariablesRequest(
+    CreateVariablesRequest(variables[1].variablesReference));
+  ASSERT_TRUE(testVariables.size() == 5);
+  ASSERT_VARIABLE_REFERENCE_NOT_ZERO(testVariables[0], "Command",
+                                     std::to_string(test2.GetCommand().size()),
+                                     "collection");
+  ASSERT_VARIABLE(testVariables[1], "CommandExpandLists",
+                  BOOL_STRING(test2.GetCommandExpandLists()), "bool");
+  ASSERT_VARIABLE(testVariables[2], "Name", test2.GetName(), "string");
+  ASSERT_VARIABLE(testVariables[3], "OldStyle",
+                  BOOL_STRING(test2.GetOldStyle()), "bool");
+  ASSERT_VARIABLE_REFERENCE_NOT_ZERO(testVariables[4], "Properties", "2",
+                                     "collection");
+
+  commandVariables = variablesManager->HandleVariablesRequest(
+    CreateVariablesRequest(testVariables[0].variablesReference));
+  ASSERT_TRUE(commandVariables.size() == test2.GetCommand().size());
+  for (size_t i = 0; i < commandVariables.size(); ++i) {
+    ASSERT_VARIABLE(commandVariables[i], "[" + std::to_string(i) + "]",
+                    test2.GetCommand()[i], "string");
+  }
+
+  propertiesVariables = variablesManager->HandleVariablesRequest(
+    CreateVariablesRequest(testVariables[4].variablesReference));
+  ASSERT_TRUE(propertiesVariables.size() == 2);
+  ASSERT_VARIABLE(propertiesVariables[0], "Prop2", "Prop2", "string");
+  ASSERT_VARIABLE(propertiesVariables[1], "Prop3", "Prop3", "string");
+
+  auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, std::vector<cmTest*>());
+
+  ASSERT_TRUE(none == nullptr);
+
+  return true;
+}
+
+static bool testCreateFromMakefile()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  auto dummies = CreateDummies("Foo");
+  auto snapshot = dummies.Makefile->GetStateSnapshot();
+  auto state = dummies.Makefile->GetState();
+  state->SetSourceDirectory("c:/HomeDirectory");
+  state->SetBinaryDirectory("c:/HomeOutputDirectory");
+  auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, dummies.Makefile.get());
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+
+  ASSERT_TRUE(variables.size() == 12);
+  ASSERT_VARIABLE(variables[0], "AppleSDKType", "MacOS", "string");
+  ASSERT_VARIABLE(variables[1], "CurrentBinaryDirectory",
+                  snapshot.GetDirectory().GetCurrentBinary(), "string");
+  ASSERT_VARIABLE(variables[2], "CurrentSourceDirectory",
+                  snapshot.GetDirectory().GetCurrentSource(), "string");
+  ASSERT_VARIABLE(variables[3], "DefineFlags", " ", "string");
+  ASSERT_VARIABLE(variables[4], "DirectoryId",
+                  dummies.Makefile->GetDirectoryId().String, "string");
+  ASSERT_VARIABLE(variables[5], "HomeDirectory", state->GetSourceDirectory(),
+                  "string");
+  ASSERT_VARIABLE(variables[6], "HomeOutputDirectory",
+                  state->GetBinaryDirectory(), "string");
+  ASSERT_VARIABLE(variables[7], "IsRootMakefile", "TRUE", "bool");
+  ASSERT_VARIABLE(variables[8], "PlatformIs32Bit", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[9], "PlatformIs64Bit", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[10], "PlatformIsAppleEmbedded", "FALSE", "bool");
+  ASSERT_VARIABLE(variables[11], "PlatformIsx32", "FALSE", "bool");
+
+  auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, static_cast<cmMakefile*>(nullptr));
+
+  ASSERT_TRUE(none == nullptr);
+
+  return true;
+}
+
+static bool testCreateFromStackFrame()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+  auto dummies = CreateDummies("Foo");
+
+  cmListFileFunction lff = cmListFileFunction("set", 99, 99, {});
+  auto frame = std::make_shared<cmDebugger::cmDebuggerStackFrame>(
+    dummies.Makefile.get(), "c:/CMakeLists.txt", lff);
+
+  dummies.CMake->AddCacheEntry("CMAKE_BUILD_TYPE", "Debug", "Build Type",
+                               cmStateEnums::CacheEntryType::STRING);
+
+  auto locals = cmDebugger::cmDebuggerVariablesHelper::Create(
+    variablesManager, "Locals", true, frame);
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(locals->GetId()));
+
+  ASSERT_TRUE(variables.size() == 5);
+  ASSERT_VARIABLE(variables[0], "CacheVariables", "1", "collection");
+  ASSERT_VARIABLE(variables[1], "CurrentLine", std::to_string(lff.Line()),
+                  "int");
+  ASSERT_VARIABLE(variables[2], "Directories", "2", "collection");
+  ASSERT_VARIABLE(variables[3], "Locals", "2", "collection");
+  ASSERT_VARIABLE(variables[4], "Targets", "1", "collection");
+
+  dap::array<dap::Variable> cacheVariables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(variables[0].variablesReference));
+  ASSERT_TRUE(cacheVariables.size() == 1);
+  ASSERT_VARIABLE(cacheVariables[0], "CMAKE_BUILD_TYPE:STRING", "Debug",
+                  "collection");
+
+  dap::array<dap::Variable> directoriesVariables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(variables[2].variablesReference));
+  ASSERT_TRUE(directoriesVariables.size() == 2);
+  ASSERT_VARIABLE(
+    directoriesVariables[0], "CMAKE_CURRENT_BINARY_DIR",
+    dummies.Makefile->GetStateSnapshot().GetDirectory().GetCurrentBinary(),
+    "string");
+  ASSERT_VARIABLE(
+    directoriesVariables[1], "CMAKE_CURRENT_SOURCE_DIR",
+    dummies.Makefile->GetStateSnapshot().GetDirectory().GetCurrentSource(),
+    "string");
+
+  dap::array<dap::Variable> propertiesVariables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(cacheVariables[0].variablesReference));
+  ASSERT_TRUE(propertiesVariables.size() == 3);
+  ASSERT_VARIABLE(propertiesVariables[0], "HELPSTRING", "Build Type",
+                  "string");
+  ASSERT_VARIABLE(propertiesVariables[1], "TYPE", "STRING", "string");
+  ASSERT_VARIABLE(propertiesVariables[2], "VALUE", "Debug", "string");
+
+  return true;
+}
+
+static bool testCreateFromBTStringVector()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  std::vector<BT<std::string>> list(2);
+  list[0].Value = "Foo";
+  list[1].Value = "Bar";
+
+  auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, list);
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+
+  ASSERT_TRUE(vars->GetValue() == std::to_string(list.size()));
+  ASSERT_TRUE(variables.size() == 2);
+  ASSERT_VARIABLE(variables[0], "[0]", "Foo", "string");
+  ASSERT_VARIABLE(variables[1], "[1]", "Bar", "string");
+
+  auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, std::vector<std::string>());
+
+  ASSERT_TRUE(none == nullptr);
+
+  return true;
+}
+
+static bool testCreateFromFileSet()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  cmake cm(cmake::RoleScript, cmState::Unknown);
+  cmFileSet fileSet(cm, "Foo", "HEADERS", cmFileSetVisibility::Public);
+  BT<std::string> directory;
+  directory.Value = "c:/";
+  fileSet.AddDirectoryEntry(directory);
+  BT<std::string> file;
+  file.Value = "c:/foo.cxx";
+  fileSet.AddFileEntry(file);
+
+  auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, &fileSet);
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+
+  ASSERT_TRUE(variables.size() == 5);
+  ASSERT_VARIABLE_REFERENCE_NOT_ZERO(variables[0], "Directories", "1",
+                                     "collection");
+  ASSERT_VARIABLE_REFERENCE_NOT_ZERO(variables[1], "Files", "1", "collection");
+  ASSERT_VARIABLE(variables[2], "Name", "Foo", "string");
+  ASSERT_VARIABLE(variables[3], "Type", "HEADERS", "string");
+  ASSERT_VARIABLE(variables[4], "Visibility", "Public", "string");
+
+  dap::array<dap::Variable> directoriesVariables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(variables[0].variablesReference));
+  ASSERT_TRUE(directoriesVariables.size() == 1);
+  ASSERT_VARIABLE(directoriesVariables[0], "[0]", directory.Value, "string");
+
+  dap::array<dap::Variable> filesVariables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(variables[1].variablesReference));
+  ASSERT_TRUE(filesVariables.size() == 1);
+  ASSERT_VARIABLE(filesVariables[0], "[0]", file.Value, "string");
+
+  return true;
+}
+
+static bool testCreateFromFileSets()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  cmake cm(cmake::RoleScript, cmState::Unknown);
+  cmFileSet fileSet(cm, "Foo", "HEADERS", cmFileSetVisibility::Public);
+  BT<std::string> directory;
+  directory.Value = "c:/";
+  fileSet.AddDirectoryEntry(directory);
+  BT<std::string> file;
+  file.Value = "c:/foo.cxx";
+  fileSet.AddFileEntry(file);
+
+  auto fileSets = std::vector<cmFileSet*>{ &fileSet };
+  auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny(
+    variablesManager, "Locals", true, fileSets);
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(
+      CreateVariablesRequest(vars->GetId()));
+
+  ASSERT_TRUE(variables.size() == 1);
+  ASSERT_VARIABLE_REFERENCE_NOT_ZERO(variables[0], "Foo", "", "collection");
+
+  return true;
+}
+
+int testDebuggerVariablesHelper(int, char*[])
+{
+  return runTests(std::vector<std::function<bool()>>{
+    testCreateFromPolicyMap,
+    testCreateFromPairVector,
+    testCreateFromSet,
+    testCreateFromStringVector,
+    testCreateFromTarget,
+    testCreateFromGlobalGenerator,
+    testCreateFromMakefile,
+    testCreateFromStackFrame,
+    testCreateFromTests,
+    testCreateFromBTStringVector,
+    testCreateFromFileSet,
+    testCreateFromFileSets,
+  });
+}
diff --git a/Tests/CMakeLib/testDebuggerVariablesManager.cxx b/Tests/CMakeLib/testDebuggerVariablesManager.cxx
new file mode 100644
index 0000000..3013b9f
--- /dev/null
+++ b/Tests/CMakeLib/testDebuggerVariablesManager.cxx
@@ -0,0 +1,50 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include <functional>
+#include <memory>
+#include <vector>
+
+#include <cm3p/cppdap/protocol.h>
+#include <cm3p/cppdap/types.h>
+#include <stdint.h>
+
+#include "cmDebuggerVariables.h"
+#include "cmDebuggerVariablesManager.h"
+
+#include "testCommon.h"
+
+static bool testVariablesRegistration()
+{
+  auto variablesManager =
+    std::make_shared<cmDebugger::cmDebuggerVariablesManager>();
+
+  int64_t line = 5;
+  auto local = std::make_shared<cmDebugger::cmDebuggerVariables>(
+    variablesManager, "Local", true, [=]() {
+      return std::vector<cmDebugger::cmDebuggerVariableEntry>{ { "CurrentLine",
+                                                                 line } };
+    });
+
+  dap::VariablesRequest variableRequest;
+  variableRequest.variablesReference = local->GetId();
+
+  dap::array<dap::Variable> variables =
+    variablesManager->HandleVariablesRequest(variableRequest);
+
+  ASSERT_TRUE(variables.size() == 1);
+
+  local.reset();
+
+  variables = variablesManager->HandleVariablesRequest(variableRequest);
+  ASSERT_TRUE(variables.size() == 0);
+
+  return true;
+}
+
+int testDebuggerVariablesManager(int, char*[])
+{
+  return runTests(std::vector<std::function<bool()>>{
+    testVariablesRegistration,
+  });
+}
diff --git a/Tests/RunCMake/Autogen/MocGeneratedFile.cmake b/Tests/RunCMake/Autogen/MocGeneratedFile.cmake
new file mode 100644
index 0000000..7bb55e9
--- /dev/null
+++ b/Tests/RunCMake/Autogen/MocGeneratedFile.cmake
@@ -0,0 +1,15 @@
+enable_language(CXX)
+
+find_package(Qt${with_qt_version} REQUIRED COMPONENTS Core)
+
+set(CMAKE_AUTOMOC ON)
+
+set(GEN_SRC "class_$<CONFIG>.cpp")
+add_custom_command(
+  OUTPUT "${GEN_SRC}"
+  COMMAND ${CMAKE_COMMAND} -E echo "// cpp src" > "${GEN_SRC}"
+  VERBATIM
+)
+
+add_library(libgen STATIC ${GEN_SRC})
+target_link_libraries(libgen Qt${with_qt_version}::Core)
diff --git a/Tests/RunCMake/Autogen/RunCMakeTest.cmake b/Tests/RunCMake/Autogen/RunCMakeTest.cmake
index 97b64ed..4fe9406 100644
--- a/Tests/RunCMake/Autogen/RunCMakeTest.cmake
+++ b/Tests/RunCMake/Autogen/RunCMakeTest.cmake
@@ -103,4 +103,23 @@
       endblock()
     endif()
   endif()
+
+  if(RunCMake_GENERATOR_IS_MULTI_CONFIG AND NOT RunCMake_GENERATOR MATCHES "Xcode")
+    block()
+      set(RunCMake_TEST_BINARY_DIR  ${RunCMake_BINARY_DIR}/MocGeneratedFile-build)
+      run_cmake(MocGeneratedFile)
+      set(RunCMake_TEST_NO_CLEAN 1)
+      run_cmake_command(MocGeneratedFile-build ${CMAKE_COMMAND} --build . --config Debug --verbose)
+    endblock()
+    if(RunCMake_GENERATOR MATCHES "Ninja Multi-Config")
+      block()
+        set(RunCMake_TEST_BINARY_DIR  ${RunCMake_BINARY_DIR}/MocGeneratedFile-cross-config-build)
+        list(APPEND RunCMake_TEST_OPTIONS -DCMAKE_CROSS_CONFIGS=all)
+        run_cmake(MocGeneratedFile)
+        set(RunCMake_TEST_NO_CLEAN 1)
+        run_cmake_command(MocGeneratedFile-cross-config-build ${CMAKE_COMMAND} --build . --config Release --target libgen:Debug)
+        run_cmake_command(MocGeneratedFile-cross-config-build ${CMAKE_COMMAND} --build . --config Debug --target libgen:Release)
+      endblock()
+    endif()
+  endif()
 endif ()
diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-result.txt b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-stderr.txt b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-stderr.txt
new file mode 100644
index 0000000..6269c19
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: No file specified for --debugger-dap-log
+CMake Error: Run 'cmake --help' for all supported options\.$
diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog.cmake b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog.cmake
new file mode 100644
index 0000000..6ddce8b
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog.cmake
@@ -0,0 +1 @@
+message(FATAL_ERROR "This should not be reached.")
diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-result.txt b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-stderr.txt b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-stderr.txt
new file mode 100644
index 0000000..947cb00
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: No path specified for --debugger-pipe
+CMake Error: Run 'cmake --help' for all supported options\.$
diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe.cmake b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe.cmake
new file mode 100644
index 0000000..6ddce8b
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe.cmake
@@ -0,0 +1 @@
+message(FATAL_ERROR "This should not be reached.")
diff --git a/Tests/RunCMake/CommandLine/DebuggerCapabilityInspect-check.cmake b/Tests/RunCMake/CommandLine/DebuggerCapabilityInspect-check.cmake
new file mode 100644
index 0000000..75769f2
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerCapabilityInspect-check.cmake
@@ -0,0 +1,5 @@
+if(actual_stdout MATCHES [["debugger" *: *true]])
+  set_property(DIRECTORY PROPERTY CMake_ENABLE_DEBUGGER 1)
+else()
+  set_property(DIRECTORY PROPERTY CMake_ENABLE_DEBUGGER 0)
+endif()
diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupported-result.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupported-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerNotSupported-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupported-stderr.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupported-stderr.txt
new file mode 100644
index 0000000..5845bb3
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerNotSupported-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: CMake was not built with support for --debugger
+CMake Error: Run 'cmake --help' for all supported options\.$
diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupported.cmake b/Tests/RunCMake/CommandLine/DebuggerNotSupported.cmake
new file mode 100644
index 0000000..6ddce8b
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerNotSupported.cmake
@@ -0,0 +1 @@
+message(FATAL_ERROR "This should not be reached.")
diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-result.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-stderr.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-stderr.txt
new file mode 100644
index 0000000..84c2200
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: CMake was not built with support for --debugger-dap-log
+CMake Error: Run 'cmake --help' for all supported options\.$
diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog.cmake b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog.cmake
new file mode 100644
index 0000000..6ddce8b
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog.cmake
@@ -0,0 +1 @@
+message(FATAL_ERROR "This should not be reached.")
diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-result.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-stderr.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-stderr.txt
new file mode 100644
index 0000000..5684f4c
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-stderr.txt
@@ -0,0 +1,2 @@
+^CMake Error: CMake was not built with support for --debugger-pipe
+CMake Error: Run 'cmake --help' for all supported options\.$
diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe.cmake b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe.cmake
new file mode 100644
index 0000000..6ddce8b
--- /dev/null
+++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe.cmake
@@ -0,0 +1 @@
+message(FATAL_ERROR "This should not be reached.")
diff --git a/Tests/RunCMake/CommandLine/E_capabilities-stdout.txt b/Tests/RunCMake/CommandLine/E_capabilities-stdout.txt
index e2f63cd..c01f414 100644
--- a/Tests/RunCMake/CommandLine/E_capabilities-stdout.txt
+++ b/Tests/RunCMake/CommandLine/E_capabilities-stdout.txt
@@ -1 +1 @@
-^{"fileApi":{"requests":\[{"kind":"codemodel","version":\[{"major":2,"minor":6}]},{"kind":"configureLog","version":\[{"major":1,"minor":0}]},{"kind":"cache","version":\[{"major":2,"minor":0}]},{"kind":"cmakeFiles","version":\[{"major":1,"minor":0}]},{"kind":"toolchains","version":\[{"major":1,"minor":0}]}]},"generators":\[.*\],"serverMode":false,"tls":(true|false),"version":{.*}}$
+^{"debugger":(true|false),"fileApi":{"requests":\[{"kind":"codemodel","version":\[{"major":2,"minor":6}]},{"kind":"configureLog","version":\[{"major":1,"minor":0}]},{"kind":"cache","version":\[{"major":2,"minor":0}]},{"kind":"cmakeFiles","version":\[{"major":1,"minor":0}]},{"kind":"toolchains","version":\[{"major":1,"minor":0}]}]},"generators":\[.*\],"serverMode":false,"tls":(true|false),"version":{.*}}$
diff --git a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake b/Tests/RunCMake/CommandLine/RunCMakeTest.cmake
index 205949b..45b4c0e 100644
--- a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake
+++ b/Tests/RunCMake/CommandLine/RunCMakeTest.cmake
@@ -125,6 +125,17 @@
 run_cmake_command(cache-empty-entry
   ${CMAKE_COMMAND} --build ${RunCMake_SOURCE_DIR}/cache-empty-entry/)
 
+run_cmake_command(DebuggerCapabilityInspect ${CMAKE_COMMAND} -E capabilities)
+get_property(CMake_ENABLE_DEBUGGER DIRECTORY PROPERTY CMake_ENABLE_DEBUGGER)
+if(CMake_ENABLE_DEBUGGER)
+  run_cmake_with_options(DebuggerArgMissingPipe --debugger-pipe)
+  run_cmake_with_options(DebuggerArgMissingDapLog --debugger-dap-log)
+else()
+  run_cmake_with_options(DebuggerNotSupported --debugger)
+  run_cmake_with_options(DebuggerNotSupportedPipe --debugger-pipe pipe)
+  run_cmake_with_options(DebuggerNotSupportedDapLog --debugger-dap-log dap-log)
+endif()
+
 function(run_ExplicitDirs)
   set(RunCMake_TEST_NO_CLEAN 1)
   set(RunCMake_TEST_NO_SOURCE_DIR 1)
diff --git a/Tests/RunCMake/Ninja/VerboseBuild-nowork-stdout.txt b/Tests/RunCMake/Ninja/VerboseBuild-nowork-stdout.txt
index 60a9228..40b4527 100644
--- a/Tests/RunCMake/Ninja/VerboseBuild-nowork-stdout.txt
+++ b/Tests/RunCMake/Ninja/VerboseBuild-nowork-stdout.txt
@@ -1 +1 @@
-^ninja: no work to do
+ninja: no work to do
diff --git a/Utilities/IWYU/mapping.imp b/Utilities/IWYU/mapping.imp
index 366c517..6c12ada 100644
--- a/Utilities/IWYU/mapping.imp
+++ b/Utilities/IWYU/mapping.imp
@@ -22,6 +22,7 @@
 
   # HACK: check whether this can be removed with next iwyu release.
   { include: [ "<bits/cxxabi_forced.h>", private, "<ctime>", public ] },
+  { include: [ "<bits/exception.h>", private, "<exception>", public ] },
   { include: [ "<bits/shared_ptr.h>", private, "<memory>", public ] },
   { include: [ "<bits/std_function.h>", private, "<functional>", public ] },
   { include: [ "<bits/refwrap.h>", private, "<functional>", public ] },
@@ -101,6 +102,7 @@
   { symbol: [ "__gnu_cxx::__enable_if<true, bool>::__type", private, "\"cmConfigure.h\"", public ] },
   { symbol: [ "std::remove_reference<std::basic_string<char, std::char_traits<char>, std::allocator<char> > &>::type", private, "\"cmConfigure.h\"", public ] },
   { symbol: [ "std::remove_reference<Defer &>::type", private, "\"cmConfigure.h\"", public ] },
+  { symbol: [ "std::remove_reference<dap::StoppedEvent &>::type", private, "\"cmConfigure.h\"", public ] },
 
   # Wrappers for 3rd-party libraries
   { include: [ "@<.*curl/curlver.h>", private, "<cm3p/curl/curl.h>", public ] },
diff --git a/bootstrap b/bootstrap
index a056edf..109e450 100755
--- a/bootstrap
+++ b/bootstrap
@@ -80,6 +80,7 @@
 cmake_bootstrap_system_libs=""
 cmake_bootstrap_qt_gui=""
 cmake_bootstrap_qt_qmake=""
+cmake_bootstrap_debugger=""
 cmake_sphinx_info=""
 cmake_sphinx_man=""
 cmake_sphinx_html=""
@@ -697,6 +698,9 @@
   --no-qt-gui             do not build the Qt-based GUI (default)
   --qt-qmake=<qmake>      use <qmake> as the qmake executable to find Qt
 
+  --debugger              enable debugger support (default if supported)
+  --no-debugger           disable debugger support
+
   --sphinx-info           build Info manual with Sphinx
   --sphinx-man            build man pages with Sphinx
   --sphinx-html           build html help with Sphinx
@@ -962,6 +966,8 @@
   --qt-gui) cmake_bootstrap_qt_gui="1" ;;
   --no-qt-gui) cmake_bootstrap_qt_gui="0" ;;
   --qt-qmake=*) cmake_bootstrap_qt_qmake=`cmake_arg "$1"` ;;
+  --debugger) cmake_bootstrap_debugger="1" ;;
+  --no-debugger) cmake_bootstrap_debugger="0" ;;
   --sphinx-info) cmake_sphinx_info="1" ;;
   --sphinx-man) cmake_sphinx_man="1" ;;
   --sphinx-html) cmake_sphinx_html="1" ;;
@@ -1987,6 +1993,11 @@
 set (QT_QMAKE_EXECUTABLE "'"${cmake_bootstrap_qt_qmake}"'" CACHE FILEPATH "Location of Qt qmake" FORCE)
 ' >> "${cmake_bootstrap_dir}/InitialCacheFlags.cmake"
 fi
+if test "x${cmake_bootstrap_debugger}" != "x"; then
+  echo '
+set (CMake_ENABLE_DEBUGGER '"${cmake_bootstrap_debugger}"' CACHE BOOL "Enable CMake debugger support" FORCE)
+' >> "${cmake_bootstrap_dir}/InitialCacheFlags.cmake"
+fi
 if test "x${cmake_sphinx_info}" != "x"; then
   echo '
 set (SPHINX_INFO "'"${cmake_sphinx_info}"'" CACHE BOOL "Build Info manual with Sphinx" FORCE)