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.
This commit is contained in:
Christopher Wellons
2026-04-15 10:03:02 -04:00
committed by Brad King
parent 577ad8b487
commit dbeee36ab6
+29 -15
View File
@@ -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<dap::SourceBreakpoint> const defaultValue{};
auto const& breakpoints = request.breakpoints.value(defaultValue);
@@ -126,42 +135,45 @@ void cmDebuggerBreakpointManager::SourceFileLoaded(
std::string const& sourcePath,
std::vector<cmListFileFunction> const& functions)
{
auto normalizedPath = NormalizePath(sourcePath);
std::unique_lock<std::mutex> 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<int64_t> cmDebuggerBreakpointManager::GetBreakpoints(
std::string const& sourcePath, int64_t line)
{
auto normalizedPath = NormalizePath(sourcePath);
std::unique_lock<std::mutex> lock(Mutex);
auto const& all = Breakpoints[sourcePath];
auto const& all = Breakpoints[normalizedPath];
std::vector<int64_t> breakpoints;
if (all.empty()) {
return breakpoints;