## Summary
Based on discussion from
[RFC](https://discourse.llvm.org/t/rfc-python-callback-for-source-file-resolution/83545),
this PR adds a new `SymbolLocatorScripted` plugin that allows Python
scripts to implement custom symbol and source file resolution logic.
This enables downstream users to build custom symbol servers, source
file remapping, and build artifact resolution entirely in Python.
### Changes
- Adds `LocateSourceFile()` to the SymbolLocator plugin interface,
called during source path resolution with a fully loaded `ModuleSP`, so
the plugin has access to the module's UUID, file paths, and symbols.
- Adds `SymbolLocatorScripted` plugin that delegates all four
SymbolLocator methods (`LocateExecutableObjectFile`,
`LocateExecutableSymbolFile`, `DownloadObjectAndSymbolFile`,
`LocateSourceFile`) to a user-provided Python class.
- Adds `ScriptedSymbolLocatorPythonInterface` to bridge C++ calls to
Python, with proper GIL management and error handling.
- Results for `LocateSourceFile` are cached per (module UUID, source
file) pair.
- The Python class is configured via: `settings set
plugin.symbol-locator.scripted.script-class module.ClassName`
### Python class interface
```python
class MyLocator:
def __init__(self, exe_ctx, args): ...
def locate_source_file(self, module, original_source_file):
...
def locate_executable_object_file(self, module_spec): ...
def locate_executable_symbol_file(self, module_spec,
default_search_paths): ...
def download_object_and_symbol_file(self, module_spec,
force_lookup, copy_executable): ...
```
### Test plan
```
Added TestScriptedSymbolLocator.py with 3 test cases:
- test_locate_source_file — verifies the locator resolves source
files, receives a valid SBModule with UUID, and remaps paths correctly
- test_locate_source_file_none_fallthrough — verifies returning
None falls through to default LLDB resolution, and that having no script
class set works normally
- test_invalid_script_class — verifies graceful handling of
invalid class names without crashing
```
Co-authored-by: Rahul Reddy Chamala <rachamal@fb.com>
221 lines
8.2 KiB
Python
221 lines
8.2 KiB
Python
from abc import ABCMeta, abstractmethod
|
|
import os
|
|
|
|
import lldb
|
|
|
|
|
|
class ScriptedSymbolLocator(metaclass=ABCMeta):
|
|
"""
|
|
The base class for a scripted symbol locator.
|
|
|
|
Most of the base class methods are optional and return ``None`` to fall
|
|
through to LLDB's default resolution. Override only the methods you need.
|
|
|
|
Configuration::
|
|
|
|
(lldb) command script import /path/to/my_locator.py
|
|
(lldb) target symbols scripted register -C my_locator.MyLocator \\
|
|
[-k key -v value ...]
|
|
"""
|
|
|
|
@abstractmethod
|
|
def __init__(self, exe_ctx, args):
|
|
"""Construct a scripted symbol locator.
|
|
|
|
Args:
|
|
exe_ctx (lldb.SBExecutionContext): The execution context for
|
|
the scripted symbol locator.
|
|
args (lldb.SBStructuredData): A Dictionary holding arbitrary
|
|
key/value pairs used by the scripted symbol locator.
|
|
"""
|
|
target = None
|
|
self.target = None
|
|
self.args = None
|
|
if isinstance(exe_ctx, lldb.SBExecutionContext):
|
|
target = exe_ctx.target
|
|
if isinstance(target, lldb.SBTarget) and target.IsValid():
|
|
self.target = target
|
|
self.dbg = target.GetDebugger()
|
|
if isinstance(args, lldb.SBStructuredData) and args.IsValid():
|
|
self.args = args
|
|
|
|
def locate_source_file(self, module, original_source_file):
|
|
"""Locate the source file for a given module.
|
|
|
|
Called when LLDB resolves source file paths during stack frame
|
|
display, breakpoint resolution, or source listing. This is the
|
|
primary method for implementing source file remapping based on
|
|
build IDs.
|
|
|
|
The module is a fully loaded ``SBModule`` (not an ``SBModuleSpec``),
|
|
so you can access its UUID, file path, platform file path,
|
|
symbol file path, sections, and symbols.
|
|
|
|
Results are cached per (module UUID, source file) pair, so this
|
|
method is called at most once per unique combination.
|
|
|
|
Args:
|
|
module (lldb.SBModule): The loaded module containing debug
|
|
info. Use ``module.GetUUIDString()`` to get the build ID
|
|
for looking up the correct source revision.
|
|
original_source_file (str): The original source file path
|
|
as recorded in the debug info.
|
|
|
|
Returns:
|
|
lldb.SBFileSpec: The resolved file spec, or None to fall
|
|
through to LLDB's default source resolution.
|
|
"""
|
|
return None
|
|
|
|
def locate_executable_object_file(self, module_spec):
|
|
"""Locate the executable (object) file for a given module.
|
|
|
|
Called when LLDB needs to find the binary for a module during
|
|
target creation or module loading. For example, when loading a
|
|
minidump, LLDB calls this for each shared library referenced
|
|
in the dump.
|
|
|
|
Args:
|
|
module_spec (lldb.SBModuleSpec): The module specification
|
|
containing the UUID, file path, architecture, and other
|
|
search criteria.
|
|
|
|
Returns:
|
|
lldb.SBFileSpec: The located executable, or None to fall
|
|
through to LLDB's default search.
|
|
"""
|
|
return None
|
|
|
|
def locate_executable_symbol_file(self, module_spec, default_search_paths):
|
|
"""Locate the symbol file for a given module.
|
|
|
|
Called when LLDB needs to find separate debug symbols (e.g.,
|
|
``.dSYM`` bundles on macOS, ``.debug`` files on Linux, ``.dwp``
|
|
files for split DWARF) for a module.
|
|
|
|
Args:
|
|
module_spec (lldb.SBModuleSpec): The module specification
|
|
containing the UUID and file path to search for.
|
|
default_search_paths (list): A list of default search paths
|
|
to check.
|
|
|
|
Returns:
|
|
lldb.SBFileSpec: The located symbol file, or None to fall
|
|
through to LLDB's default search.
|
|
"""
|
|
return None
|
|
|
|
def download_object_and_symbol_file(
|
|
self, module_spec, force_lookup, copy_executable
|
|
):
|
|
"""Download both the object file and symbol file for a module.
|
|
|
|
Called when LLDB needs to download a binary and its debug symbols
|
|
from a remote source (e.g., a symbol server, build artifact
|
|
store, or cloud storage). This is the last method called in the
|
|
resolution chain, typically as a fallback when local lookups
|
|
fail.
|
|
|
|
Args:
|
|
module_spec (lldb.SBModuleSpec): The module specification
|
|
containing the UUID and file path to download.
|
|
force_lookup (bool): If True, skip any cached results and
|
|
force a fresh lookup.
|
|
copy_executable (bool): If True, copy the executable to a
|
|
local path.
|
|
|
|
Returns:
|
|
bool: True if the download succeeded, False otherwise.
|
|
"""
|
|
return False
|
|
|
|
|
|
class LocalCacheSymbolLocator(ScriptedSymbolLocator):
|
|
"""Example locator that resolves files from a local cache directory.
|
|
|
|
Demonstrates how to subclass ``ScriptedSymbolLocator`` to implement
|
|
custom symbol and source file resolution. This locator looks up files
|
|
in a local directory structure organized by build ID (UUID)::
|
|
|
|
<cache_dir>/
|
|
<uuid>/
|
|
<binary_name>
|
|
<binary_name>.debug
|
|
src/
|
|
main.cpp
|
|
...
|
|
|
|
Usage::
|
|
|
|
(lldb) command script import scripted_symbol_locator
|
|
(lldb) target symbols scripted register \\
|
|
-C scripted_symbol_locator.LocalCacheSymbolLocator \\
|
|
-k cache_dir -v "/path/to/cache"
|
|
(lldb) target create --core /path/to/minidump.dmp
|
|
(lldb) bt
|
|
|
|
The locator searches for:
|
|
- Executables: ``<cache_dir>/<uuid>/<filename>``
|
|
- Symbol files: ``<cache_dir>/<uuid>/<filename>.debug``
|
|
- Source files: ``<cache_dir>/<uuid>/src/<basename>``
|
|
"""
|
|
|
|
cache_dir = None
|
|
|
|
def __init__(self, exe_ctx, args):
|
|
super().__init__(exe_ctx, args)
|
|
|
|
# Allow cache_dir to be set via structured data args.
|
|
if self.args:
|
|
cache_dir_val = self.args.GetValueForKey("cache_dir")
|
|
if cache_dir_val and cache_dir_val.IsValid():
|
|
val = cache_dir_val.GetStringValue(256)
|
|
if val:
|
|
LocalCacheSymbolLocator.cache_dir = val
|
|
|
|
def _get_cache_path(self, uuid_str, *components):
|
|
"""Build a path under the cache directory for a given UUID.
|
|
|
|
Args:
|
|
uuid_str (str): The module's UUID string.
|
|
*components: Additional path components (e.g., filename).
|
|
|
|
Returns:
|
|
str: The full path, or None if cache_dir is not set or the
|
|
UUID is empty.
|
|
"""
|
|
if not self.cache_dir or not uuid_str:
|
|
return None
|
|
return os.path.join(self.cache_dir, uuid_str, *components)
|
|
|
|
def locate_source_file(self, module, original_source_file):
|
|
"""Look up source files under ``<cache_dir>/<uuid>/src/``."""
|
|
uuid_str = module.GetUUIDString()
|
|
basename = os.path.basename(original_source_file)
|
|
path = self._get_cache_path(uuid_str, "src", basename)
|
|
if path and os.path.exists(path):
|
|
return lldb.SBFileSpec(path, True)
|
|
return None
|
|
|
|
def locate_executable_object_file(self, module_spec):
|
|
"""Look up executables under ``<cache_dir>/<uuid>/``."""
|
|
uuid_str = module_spec.GetUUIDString()
|
|
filename = os.path.basename(module_spec.GetFileSpec().GetFilename() or "")
|
|
if not filename:
|
|
return None
|
|
path = self._get_cache_path(uuid_str, filename)
|
|
if path and os.path.exists(path):
|
|
return lldb.SBFileSpec(path, True)
|
|
return None
|
|
|
|
def locate_executable_symbol_file(self, module_spec, default_search_paths):
|
|
"""Look up debug symbol files under ``<cache_dir>/<uuid>/``."""
|
|
uuid_str = module_spec.GetUUIDString()
|
|
filename = os.path.basename(module_spec.GetFileSpec().GetFilename() or "")
|
|
if not filename:
|
|
return None
|
|
debug_path = self._get_cache_path(uuid_str, filename + ".debug")
|
|
if debug_path and os.path.exists(debug_path):
|
|
return lldb.SBFileSpec(debug_path, True)
|
|
return None
|