diff --git a/lldb/include/lldb/Breakpoint/BreakpointResolverFileLine.h b/lldb/include/lldb/Breakpoint/BreakpointResolverFileLine.h --- a/lldb/include/lldb/Breakpoint/BreakpointResolverFileLine.h +++ b/lldb/include/lldb/Breakpoint/BreakpointResolverFileLine.h @@ -21,9 +21,10 @@ class BreakpointResolverFileLine : public BreakpointResolver { public: - BreakpointResolverFileLine(const lldb::BreakpointSP &bkpt, - lldb::addr_t offset, bool skip_prologue, - const SourceLocationSpec &location_spec); + BreakpointResolverFileLine( + const lldb::BreakpointSP &bkpt, lldb::addr_t offset, bool skip_prologue, + const SourceLocationSpec &location_spec, + llvm::Optional removed_prefix_opt = llvm::None); static BreakpointResolver * CreateFromStructuredData(const lldb::BreakpointSP &bkpt, @@ -57,10 +58,14 @@ protected: void FilterContexts(SymbolContextList &sc_list); + void DeduceSourceMapping(SymbolContextList &sc_list); friend class Breakpoint; SourceLocationSpec m_location_spec; bool m_skip_prologue; + // Any previously removed file path prefix by reverse source mapping. + // This is used to auto deduce source map if needed. + llvm::Optional m_removed_prefix_opt; private: BreakpointResolverFileLine(const BreakpointResolverFileLine &) = delete; diff --git a/lldb/include/lldb/Target/PathMappingList.h b/lldb/include/lldb/Target/PathMappingList.h --- a/lldb/include/lldb/Target/PathMappingList.h +++ b/lldb/include/lldb/Target/PathMappingList.h @@ -37,6 +37,9 @@ void Append(const PathMappingList &rhs, bool notify); + void AppendUnique(llvm::StringRef path, llvm::StringRef replacement, + bool notify); + void Clear(bool notify); // By default, dump all pairs. @@ -88,7 +91,22 @@ bool only_if_exists = false) const; bool RemapPath(const char *, std::string &) const = delete; - bool ReverseRemapPath(const FileSpec &file, FileSpec &fixed) const; + /// Perform reverse source path remap for input \a file. + /// Source maps contains a list of mappings. + /// Reverse remap means locating a matching entry prefix using "to_new_path" + /// part and replacing it with "from_original_path" part if found. + /// + /// \param[in] file + /// The source path to reverse remap. + /// \param[in] fixed + /// The reversed mapped new path. + /// + /// \return + /// llvm::None if no remapping happens, otherwise, the matching source map + /// entry's ""to_new_pathto"" part (which is the prefix of \a file) is + /// returned. + llvm::Optional ReverseRemapPath(const FileSpec &file, + FileSpec &fixed) const; /// Finds a source file given a file spec using the path remappings. /// diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h --- a/lldb/include/lldb/Target/Target.h +++ b/lldb/include/lldb/Target/Target.h @@ -141,6 +141,8 @@ PathMappingList &GetSourcePathMap() const; + bool GetAutoDeduceSourceMap() const; + FileSpecList GetExecutableSearchPaths(); void AppendExecutableSearchPaths(const FileSpec &); diff --git a/lldb/source/Breakpoint/BreakpointResolverFileLine.cpp b/lldb/source/Breakpoint/BreakpointResolverFileLine.cpp --- a/lldb/source/Breakpoint/BreakpointResolverFileLine.cpp +++ b/lldb/source/Breakpoint/BreakpointResolverFileLine.cpp @@ -12,6 +12,7 @@ #include "lldb/Core/Module.h" #include "lldb/Symbol/CompileUnit.h" #include "lldb/Symbol/Function.h" +#include "lldb/Target/Target.h" #include "lldb/Utility/LLDBLog.h" #include "lldb/Utility/Log.h" #include "lldb/Utility/StreamString.h" @@ -22,9 +23,11 @@ // BreakpointResolverFileLine: BreakpointResolverFileLine::BreakpointResolverFileLine( const BreakpointSP &bkpt, lldb::addr_t offset, bool skip_prologue, - const SourceLocationSpec &location_spec) + const SourceLocationSpec &location_spec, + llvm::Optional removed_prefix_opt) : BreakpointResolver(bkpt, BreakpointResolver::FileLineResolver, offset), - m_location_spec(location_spec), m_skip_prologue(skip_prologue) {} + m_location_spec(location_spec), m_skip_prologue(skip_prologue), + m_removed_prefix_opt(removed_prefix_opt) {} BreakpointResolver *BreakpointResolverFileLine::CreateFromStructuredData( const BreakpointSP &bkpt, const StructuredData::Dictionary &options_dict, @@ -187,6 +190,85 @@ } } +void BreakpointResolverFileLine::DeduceSourceMapping( + SymbolContextList &sc_list) { + Target &target = GetBreakpoint()->GetTarget(); + if (!target.GetAutoDeduceSourceMap()) + return; + + Log *log = GetLog(LLDBLog::Breakpoints); + const llvm::StringRef path_separator = llvm::sys::path::get_separator( + m_location_spec.GetFileSpec().GetPathStyle()); + // Check if "b" is a suffix of "a". + // And return llvm::None if not or the new path + // of "a" after consuming "b" from the back. + auto check_suffix = + [path_separator](llvm::StringRef a, llvm::StringRef b, + bool case_sensitive) -> llvm::Optional { + if (case_sensitive ? a.consume_back(b) : a.consume_back_insensitive(b)) { + if (a.empty() || a.endswith(path_separator)) { + return a; + } + } + return llvm::None; + }; + + FileSpec request_file = m_location_spec.GetFileSpec(); + + // Only auto deduce source map if breakpoint is full path. + // Note: an existing source map reverse mapping (m_removed_prefix_opt has + // value) may make request_file relative. + if (!m_removed_prefix_opt.has_value() && request_file.IsRelative()) + return; + + const bool case_sensitive = request_file.IsCaseSensitive(); + const bool full = !request_file.GetDirectory().IsEmpty(); + for (uint32_t i = 0; i < sc_list.GetSize(); ++i) { + SymbolContext sc; + sc_list.GetContextAtIndex(i, sc); + + FileSpec sc_file = sc.line_entry.file; + + if (FileSpec::Equal(sc_file, request_file, full)) + continue; + + llvm::StringRef sc_file_dir = sc_file.GetDirectory().GetStringRef(); + llvm::StringRef request_file_dir = + request_file.GetDirectory().GetStringRef(); + + llvm::StringRef new_mapping_from; + llvm::SmallString<256> new_mapping_to; + + // Adding back any potentially reverse mapping stripped prefix. + // for new_mapping_to. + if (m_removed_prefix_opt.hasValue()) + llvm::sys::path::append(new_mapping_to, m_removed_prefix_opt.getValue()); + + llvm::Optional new_mapping_from_opt = + check_suffix(sc_file_dir, request_file_dir, case_sensitive); + if (new_mapping_from_opt) { + new_mapping_from = new_mapping_from_opt.getValue(); + if (new_mapping_to.empty()) + new_mapping_to = "."; + } else { + llvm::Optional new_mapping_to_opt = + check_suffix(request_file_dir, sc_file_dir, case_sensitive); + if (new_mapping_to_opt) { + new_mapping_from = "."; + llvm::sys::path::append(new_mapping_to, ".", + new_mapping_to_opt.getValue()); + } + } + + if (!new_mapping_from.empty() && !new_mapping_to.empty()) { + LLDB_LOG(log, "generating auto source map from {0} to {1}", + new_mapping_from, new_mapping_to); + target.GetSourcePathMap().AppendUnique(new_mapping_from, new_mapping_to, + /*notify*/ true); + } + } +} + Searcher::CallbackReturn BreakpointResolverFileLine::SearchCallback( SearchFilter &filter, SymbolContext &context, Address *addr) { SymbolContextList sc_list; @@ -222,6 +304,9 @@ FilterContexts(sc_list); + if (GetBreakpoint()->GetTarget().GetAutoDeduceSourceMap()) + DeduceSourceMapping(sc_list); + StreamString s; s.Printf("for %s:%d ", m_location_spec.GetFileSpec().GetFilename().AsCString(""), diff --git a/lldb/source/Target/PathMappingList.cpp b/lldb/source/Target/PathMappingList.cpp --- a/lldb/source/Target/PathMappingList.cpp +++ b/lldb/source/Target/PathMappingList.cpp @@ -76,6 +76,18 @@ } } +void PathMappingList::AppendUnique(llvm::StringRef path, + llvm::StringRef replacement, bool notify) { + auto normalized_path = NormalizePath(path); + auto normalized_replacement = NormalizePath(replacement); + for (const auto &pair : m_pairs) { + if (pair.first.GetStringRef().equals(normalized_path) && + pair.second.GetStringRef().equals(normalized_replacement)) + return; + } + Append(path, replacement, notify); +} + void PathMappingList::Insert(llvm::StringRef path, llvm::StringRef replacement, uint32_t index, bool notify) { ++m_mod_id; @@ -207,20 +219,22 @@ return {}; } -bool PathMappingList::ReverseRemapPath(const FileSpec &file, FileSpec &fixed) const { +llvm::Optional +PathMappingList::ReverseRemapPath(const FileSpec &file, FileSpec &fixed) const { std::string path = file.GetPath(); llvm::StringRef path_ref(path); for (const auto &it : m_pairs) { + llvm::StringRef removed_prefix = it.second.GetStringRef(); if (!path_ref.consume_front(it.second.GetStringRef())) continue; auto orig_file = it.first.GetStringRef(); - auto orig_style = FileSpec::GuessPathStyle(orig_file).value_or( + auto orig_style = FileSpec::GuessPathStyle(orig_file).getValueOr( llvm::sys::path::Style::native); fixed.SetFile(orig_file, orig_style); AppendPathComponents(fixed, path_ref, orig_style); - return true; + return removed_prefix; } - return false; + return llvm::None; } llvm::Optional PathMappingList::FindFile(const FileSpec &orig_spec) const { diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -356,7 +356,9 @@ bool hardware, LazyBool move_to_nearest_code) { FileSpec remapped_file; - if (!GetSourcePathMap().ReverseRemapPath(file, remapped_file)) + llvm::Optional removed_prefix_opt = + GetSourcePathMap().ReverseRemapPath(file, remapped_file); + if (!removed_prefix_opt) remapped_file = file; if (check_inlines == eLazyBoolCalculate) { @@ -400,7 +402,7 @@ return nullptr; BreakpointResolverSP resolver_sp(new BreakpointResolverFileLine( - nullptr, offset, skip_prologue, location_spec)); + nullptr, offset, skip_prologue, location_spec, removed_prefix_opt)); return CreateBreakpoint(filter_sp, resolver_sp, internal, hardware, true); } @@ -4270,6 +4272,12 @@ return option_value->GetCurrentValue(); } +bool TargetProperties::GetAutoDeduceSourceMap() const { + const uint32_t idx = ePropertyAutoDeduceSourceMap; + return m_collection_sp->GetPropertyAtIndexAsBoolean( + nullptr, idx, g_target_properties[idx].default_uint_value != 0); +} + void TargetProperties::AppendExecutableSearchPaths(const FileSpec &dir) { const uint32_t idx = ePropertyExecutableSearchPaths; OptionValueFileSpecList *option_value = diff --git a/lldb/source/Target/TargetProperties.td b/lldb/source/Target/TargetProperties.td --- a/lldb/source/Target/TargetProperties.td +++ b/lldb/source/Target/TargetProperties.td @@ -37,6 +37,9 @@ def SourceMap: Property<"source-map", "PathMap">, DefaultStringValue<"">, Desc<"Source path remappings apply substitutions to the paths of source files, typically needed to debug from a different host than the one that built the target. The source-map property consists of an array of pairs, the first element is a path prefix, and the second is its replacement. The syntax is `prefix1 replacement1 prefix2 replacement2...`. The pairs are checked in order, the first prefix that matches is used, and that prefix is substituted with the replacement. A common pattern is to use source-map in conjunction with the clang -fdebug-prefix-map flag. In the build, use `-fdebug-prefix-map=/path/to/build_dir=.` to rewrite the host specific build directory to `.`. Then for debugging, use `settings set target.source-map . /path/to/local_dir` to convert `.` to a valid local path.">; + def AutoDeduceSourceMap: Property<"auto-deduce-source-map", "Boolean">, + DefaultFalse, + Desc<"Automatically deduce source path mappings based on source file breakpoint resolution. It only deduces source mapping if source file breakpoint request is using full path.">; def ExecutableSearchPaths: Property<"exec-search-paths", "FileSpecList">, DefaultStringValue<"">, Desc<"Executable search paths to use when locating executable files whose paths don't match the local file system.">; diff --git a/lldb/test/API/functionalities/breakpoint/breakpoint_command/TestBreakpointCommand.py b/lldb/test/API/functionalities/breakpoint/breakpoint_command/TestBreakpointCommand.py --- a/lldb/test/API/functionalities/breakpoint/breakpoint_command/TestBreakpointCommand.py +++ b/lldb/test/API/functionalities/breakpoint/breakpoint_command/TestBreakpointCommand.py @@ -8,6 +8,7 @@ from lldbsuite.test.decorators import * from lldbsuite.test.lldbtest import * from lldbsuite.test import lldbutil +import json import os import side_effect @@ -428,3 +429,69 @@ bp_3 = target.FindBreakpointByID(bp_id_3) self.assertFalse(bp_3.IsValid(), "Didn't delete disabled breakpoint 3") + + + def get_source_map_json(self): + stream = lldb.SBStream() + self.dbg.GetSetting("target.source-map").GetAsJSON(stream) + return json.loads(stream.GetData()) + + def verify_source_map_entry_pair(self, entry, original, replacement): + self.assertEquals(entry[0], original, + "source map entry 'original' does not match") + self.assertEquals(entry[1], replacement, + "source map entry 'replacement' does not match") + + @skipIf(oslist=["windows"]) + @no_debug_info_test + def test_breakpoints_auto_deduce_source_map(self): + """ + Test that with target.auto-deduce-source-map settings. + + The "relative.yaml" contains a line table that is: + + Line table for a/b/c/main.cpp in `a.out + 0x0000000100003f94: a/b/c/main.cpp:1 + 0x0000000100003fb0: a/b/c/main.cpp:2:3 + 0x0000000100003fb8: a/b/c/main.cpp:2:3 + """ + src_dir = self.getSourceDir() + yaml_path = os.path.join(src_dir, "relative.yaml") + yaml_base, ext = os.path.splitext(yaml_path) + obj_path = self.getBuildArtifact("a.out") + self.yaml2obj(yaml_path, obj_path) + + # Create a target with the object file we just created from YAML + target = self.dbg.CreateTarget(obj_path) + # We now have debug information with line table paths that start are + # "./a/b/c/main.cpp". + + source_map_json = self.get_source_map_json() + self.assertEquals(len(source_map_json), 0, "source map should be empty initially") + self.runCmd("settings set target.auto-deduce-source-map true") + + # Verify auto deduced source map when file path in debug info + # is a suffix of request breakpoint file path + path = "/x/y/a/b/c/main.cpp" + bp = target.BreakpointCreateByLocation(path, 2) + self.assertTrue(bp.GetNumLocations() > 0, + 'Couldn\'t resolve breakpoint using full path "%s" in executate "%s" with ' + 'debug info that has relative path with matching suffix' % (path, self.getBuildArtifact("a.out"))) + + source_map_json = self.get_source_map_json() + self.assertEquals(len(source_map_json), 1, "source map should not be empty") + self.verify_source_map_entry_pair(source_map_json[0], ".", "/x/y") + + # Reset source map. + self.runCmd("settings clear target.source-map") + + # Verify source map will not auto deduced when file path of request breakpoint + # equals the file path in debug info. + path = "a/b/c/main.cpp" + bp = target.BreakpointCreateByLocation(path, 2) + self.assertTrue(bp.GetNumLocations() > 0, + 'Couldn\'t resolve breakpoint using full path "%s" in executate "%s" with ' + 'debug info that has relative path with matching suffix' % (path, self.getBuildArtifact("a.out"))) + + source_map_json = self.get_source_map_json() + self.assertEquals(len(source_map_json), 0, "source map should not be deduced") diff --git a/lldb/unittests/Target/PathMappingListTest.cpp b/lldb/unittests/Target/PathMappingListTest.cpp --- a/lldb/unittests/Target/PathMappingListTest.cpp +++ b/lldb/unittests/Target/PathMappingListTest.cpp @@ -40,7 +40,7 @@ map.RemapPath(ConstString(match.original.GetPath()), actual_remapped)); EXPECT_EQ(FileSpec(actual_remapped.GetStringRef()), match.remapped); FileSpec unmapped_spec; - EXPECT_TRUE(map.ReverseRemapPath(match.remapped, unmapped_spec)); + EXPECT_TRUE(map.ReverseRemapPath(match.remapped, unmapped_spec).hasValue()); std::string unmapped_path = unmapped_spec.GetPath(); EXPECT_EQ(unmapped_path, orig_normalized); }