From dbeee36ab683ee4d6bcf3dbe3b04360c76167c86 Mon Sep 17 00:00:00 2001 From: Christopher Wellons Date: Mon, 13 Apr 2026 12:37:21 -0400 Subject: [PATCH] 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 that calls ToNormalizedPathOnDisk and apply it in all three public entry points so that every map key goes through the same transformation. ToNormalizedPathOnDisk loads the on-disk capitalization on macOS and Windows while preserving symbolic links in logical paths. --- Source/cmDebuggerBreakpointManager.cxx | 44 +++++++++++++++++--------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/Source/cmDebuggerBreakpointManager.cxx b/Source/cmDebuggerBreakpointManager.cxx index b33985b408..555426de49 100644 --- a/Source/cmDebuggerBreakpointManager.cxx +++ b/Source/cmDebuggerBreakpointManager.cxx @@ -17,6 +17,16 @@ 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 case-insensitive filesystems (macOS, Windows) +// ToNormalizedPathOnDisk loads the on-disk capitalization while +// preserving symbolic links in logical paths. +static std::string NormalizePath(std::string const& sourcePath) +{ + return cmSystemTools::ToNormalizedPathOnDisk(sourcePath); +} + cmDebuggerBreakpointManager::cmDebuggerBreakpointManager( dap::Session* dapSession) : DapSession(dapSession) @@ -73,8 +83,7 @@ cmDebuggerBreakpointManager::HandleSetBreakpointsRequest( 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 +135,45 @@ void cmDebuggerBreakpointManager::SourceFileLoaded( 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 +183,10 @@ void cmDebuggerBreakpointManager::SourceFileLoaded( 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;