From dc1b730fc1cace998d39b3baeb2bded791baa1d2 Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Sat, 11 Apr 2026 14:17:48 -0400 Subject: [PATCH] cmake: normalize breakpoint paths The breakpoint manager normalizes incoming setBreakpoints paths with GetActualCaseForPath, but SourceFileLoaded and GetBreakpoints use paths from CollapseFullPath, which preserves whatever case the caller provided. On case-insensitive filesystems these can differ for the same file, so the unordered_map keys do not match and breakpoints are silently missed. On macOS (HFS+) GetActualCaseForPath is a no-op, so the setBreakpoints path passes through unmodified. If the DAP client sends "Test.cmake" but cmake was invoked with "cmake -P test.cmake", the breakpoint is stored under "Test.cmake" while GetBreakpoints looks up "test.cmake". On Windows GetActualCaseForPath *does* normalize, but only in HandleSetBreakpointsRequest. The breakpoint is stored under the canonical "Test.cmake", while SourceFileLoaded and GetBreakpoints still receive "test.cmake" from CollapseFullPath. The keys disagree from the opposite direction. Add a NormalizePath helper and apply it in all three public entry points so that every map key goes through the same transformation. On non-Windows it uses GetRealPath (wrapping realpath(3), which returns the actual filesystem case on macOS and resolves symlinks on Linux). On Windows the existing GetActualCaseForPath is retained. --- src/cmake-debugger-realpath.patch | 132 ++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/cmake-debugger-realpath.patch diff --git a/src/cmake-debugger-realpath.patch b/src/cmake-debugger-realpath.patch new file mode 100644 index 0000000..f28849d --- /dev/null +++ b/src/cmake-debugger-realpath.patch @@ -0,0 +1,132 @@ +debugger: normalize breakpoint paths on case-insensitive filesystems + +The breakpoint manager normalizes incoming setBreakpoints paths with +GetActualCaseForPath, but SourceFileLoaded and GetBreakpoints use +paths from CollapseFullPath, which preserves whatever case the caller +provided. On case-insensitive filesystems these can differ for the +same file, so the unordered_map keys do not match and breakpoints are +silently missed. + +On macOS (HFS+) GetActualCaseForPath is a no-op, so the setBreakpoints +path passes through unmodified. If the DAP client sends "Test.cmake" +but cmake was invoked with "cmake -P test.cmake", the breakpoint is +stored under "Test.cmake" while GetBreakpoints looks up "test.cmake". + +On Windows GetActualCaseForPath *does* normalize, but only in +HandleSetBreakpointsRequest. The breakpoint is stored under the +canonical "Test.cmake", while SourceFileLoaded and GetBreakpoints +still receive "test.cmake" from CollapseFullPath. The keys disagree +from the opposite direction. + +Add a NormalizePath helper and apply it in all three public entry +points so that every map key goes through the same transformation. +On non-Windows it uses GetRealPath (wrapping realpath(3), which +returns the actual filesystem case on macOS and resolves symlinks on +Linux). On Windows the existing GetActualCaseForPath is retained. + +--- a/Source/cmDebuggerBreakpointManager.cxx ++++ b/Source/cmDebuggerBreakpointManager.cxx +@@ -17,6 +17,22 @@ + + namespace cmDebugger { + ++// Resolve a source path to its canonical form so that breakpoint map ++// keys match regardless of the case used by the DAP client or by ++// CollapseFullPath. On macOS (case-insensitive HFS+) realpath() ++// returns the actual filesystem case; on Linux it resolves symlinks ++// (case is already significant); on Windows GetActualCaseForPath ++// walks path components with FindFirstFileW. ++static std::string NormalizePath(std::string const& sourcePath) ++{ ++#ifdef _WIN32 ++ return cmSystemTools::GetActualCaseForPath(sourcePath); ++#else ++ auto real = cmSystemTools::GetRealPath(sourcePath); ++ return real.empty() ? sourcePath : real; ++#endif ++} ++ + cmDebuggerBreakpointManager::cmDebuggerBreakpointManager( + dap::Session* dapSession) + : DapSession(dapSession) +@@ -73,8 +89,7 @@ + + dap::SetBreakpointsResponse response; + +- auto sourcePath = +- cmSystemTools::GetActualCaseForPath(request.source.path.value()); ++ auto sourcePath = NormalizePath(request.source.path.value()); + dap::array const defaultValue{}; + auto const& breakpoints = request.breakpoints.value(defaultValue); + +@@ -126,42 +141,46 @@ + std::string const& sourcePath, + std::vector const& functions) + { ++ auto normalizedPath = NormalizePath(sourcePath); ++ + std::unique_lock lock(Mutex); +- if (ListFileFunctionLines.find(sourcePath) != ListFileFunctionLines.end()) { ++ if (ListFileFunctionLines.find(normalizedPath) != ++ ListFileFunctionLines.end()) { + // this is not expected. + return; + } + + for (cmListFileFunction const& func : functions) { +- ListFileFunctionLines[sourcePath].emplace_back( ++ ListFileFunctionLines[normalizedPath].emplace_back( + cmDebuggerFunctionLocation{ func.Line(), func.LineEnd() }); + } + +- if (ListFilePendingValidations.find(sourcePath) == ++ if (ListFilePendingValidations.find(normalizedPath) == + ListFilePendingValidations.end()) { + return; + } + +- ListFilePendingValidations.erase(sourcePath); ++ ListFilePendingValidations.erase(normalizedPath); + +- for (size_t i = 0; i < Breakpoints[sourcePath].size(); i++) { ++ for (size_t i = 0; i < Breakpoints[normalizedPath].size(); i++) { + dap::BreakpointEvent breakpointEvent; +- breakpointEvent.breakpoint.id = Breakpoints[sourcePath][i].GetId(); +- breakpointEvent.breakpoint.line = Breakpoints[sourcePath][i].GetLine(); ++ breakpointEvent.breakpoint.id = Breakpoints[normalizedPath][i].GetId(); ++ breakpointEvent.breakpoint.line = ++ Breakpoints[normalizedPath][i].GetLine(); + auto source = dap::Source(); +- source.path = sourcePath; ++ source.path = normalizedPath; + breakpointEvent.breakpoint.source = source; + int64_t correctedLine = CalibrateBreakpointLine( +- sourcePath, Breakpoints[sourcePath][i].GetLine()); +- if (correctedLine != Breakpoints[sourcePath][i].GetLine()) { +- Breakpoints[sourcePath][i].ChangeLine(correctedLine); ++ normalizedPath, Breakpoints[normalizedPath][i].GetLine()); ++ if (correctedLine != Breakpoints[normalizedPath][i].GetLine()) { ++ Breakpoints[normalizedPath][i].ChangeLine(correctedLine); + } + breakpointEvent.reason = "changed"; + breakpointEvent.breakpoint.verified = (correctedLine > 0); + if (breakpointEvent.breakpoint.verified) { + breakpointEvent.breakpoint.line = correctedLine; + } else { +- Breakpoints[sourcePath][i].Invalid(); ++ Breakpoints[normalizedPath][i].Invalid(); + } + + DapSession->send(breakpointEvent); +@@ -171,8 +190,10 @@ + std::vector cmDebuggerBreakpointManager::GetBreakpoints( + std::string const& sourcePath, int64_t line) + { ++ auto normalizedPath = NormalizePath(sourcePath); ++ + std::unique_lock lock(Mutex); +- auto const& all = Breakpoints[sourcePath]; ++ auto const& all = Breakpoints[normalizedPath]; + std::vector breakpoints; + if (all.empty()) { + return breakpoints;