proc,service: return build informations for each package

Adds an API call that returns a list of packages contained in the
program and the files that were used to build them, and also a best
guess at which filesystem directory contained the package when it was
built.

This can be used by IDEs to map file paths if the debugging environment
doesn't match the build environment exactly.
This commit is contained in:
aarzilli 2019-12-03 14:00:30 +01:00 committed by Alessandro Arzilli
parent 0e0d689246
commit a8606afb0b
8 changed files with 177 additions and 5 deletions

@ -43,6 +43,7 @@ functions(Filter) | Equivalent to API call [ListFunctions](https://godoc.org/git
goroutines(Start, Count) | Equivalent to API call [ListGoroutines](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListGoroutines)
local_vars(Scope, Cfg) | Equivalent to API call [ListLocalVars](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListLocalVars)
package_vars(Filter, Cfg) | Equivalent to API call [ListPackageVars](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListPackageVars)
packages_build_info(IncludeFiles) | Equivalent to API call [ListPackagesBuildInfo](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListPackagesBuildInfo)
registers(ThreadID, IncludeFp) | Equivalent to API call [ListRegisters](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListRegisters)
sources(Filter) | Equivalent to API call [ListSources](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListSources)
threads() | Equivalent to API call [ListThreads](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListThreads)

@ -385,13 +385,28 @@ func (lineInfo *DebugLineInfo) FirstStmtForLine(start, end uint64) (pc uint64, f
}
if err := sm.next(); err != nil {
if lineInfo.Logf != nil {
lineInfo.Logf("StmtAfter error: %v", err)
lineInfo.Logf("FirstStmtForLine error: %v", err)
}
return 0, "", 0, false
}
}
}
func (lineInfo *DebugLineInfo) FirstFile() string {
sm := newStateMachine(lineInfo, lineInfo.Instructions)
for {
if sm.valid {
return sm.file
}
if err := sm.next(); err != nil {
if lineInfo.Logf != nil {
lineInfo.Logf("FirstFile error: %v", err)
}
return ""
}
}
}
func (sm *StateMachine) next() error {
sm.started = true
if sm.valid {

@ -351,7 +351,7 @@ func (bi *BinaryInfo) LineToPC(filename string, lineno int) (pcs []uint64, err e
fileFound := false
var pc uint64
for _, cu := range bi.compileUnits {
if cu.lineInfo.Lookup[filename] == nil {
if cu.lineInfo != nil && cu.lineInfo.Lookup[filename] == nil {
continue
}
fileFound = true
@ -411,7 +411,7 @@ func (bi *BinaryInfo) AllPCsForFileLines(filename string, linenos []int) map[int
r[line] = make([]uint64, 0, 1)
}
for _, cu := range bi.compileUnits {
if cu.lineInfo.Lookup[filename] != nil {
if cu.lineInfo != nil && cu.lineInfo.Lookup[filename] != nil {
cu.lineInfo.AllPCsForFileLines(filename, r)
}
}
@ -1317,8 +1317,8 @@ func (bi *BinaryInfo) loadDebugInfoMaps(image *Image, debugLineBytes []byte, wg
if len(cu.ranges) >= 1 {
cu.lowPC = cu.ranges[0][0]
}
lineInfoOffset, _ := entry.Val(dwarf.AttrStmtList).(int64)
if lineInfoOffset >= 0 && lineInfoOffset < int64(len(debugLineBytes)) {
lineInfoOffset, hasLineInfo := entry.Val(dwarf.AttrStmtList).(int64)
if hasLineInfo && lineInfoOffset >= 0 && lineInfoOffset < int64(len(debugLineBytes)) {
var logfn func(string, ...interface{})
if logflags.DebugLineErrors() {
logger := logrus.New().WithFields(logrus.Fields{"layer": "dwarf-line"})
@ -1756,3 +1756,52 @@ func (bi *BinaryInfo) symLookup(addr uint64) (string, uint64) {
}
return "", 0
}
type PackageBuildInfo struct {
ImportPath string
DirectoryPath string
Files map[string]struct{}
}
// ListPackagesBuildInfo returns the list of packages used by the program along with
// the directory where each package was compiled and optionally the list of
// files constituting the package.
func (bi *BinaryInfo) ListPackagesBuildInfo(includeFiles bool) []*PackageBuildInfo {
m := make(map[string]*PackageBuildInfo)
for _, cu := range bi.compileUnits {
if cu.image != bi.Images[0] || !cu.isgo || cu.lineInfo == nil {
//TODO(aarzilli): what's the correct thing to do for plugins?
continue
}
ip := strings.Replace(cu.name, "\\", "/", -1)
if _, ok := m[ip]; !ok {
path := cu.lineInfo.FirstFile()
if ext := filepath.Ext(path); ext != ".go" && ext != ".s" {
continue
}
dp := filepath.Dir(path)
m[ip] = &PackageBuildInfo{
ImportPath: ip,
DirectoryPath: dp,
Files: make(map[string]struct{}),
}
}
if includeFiles {
pbi := m[ip]
for _, file := range cu.lineInfo.FileNames {
pbi.Files[file.Path] = struct{}{}
}
}
}
r := make([]*PackageBuildInfo, 0, len(m))
for _, pbi := range m {
r = append(r, pbi)
}
sort.Slice(r, func(i, j int) bool { return r[i].ImportPath < r[j].ImportPath })
return r
}

@ -4525,3 +4525,24 @@ func TestIssue1817(t *testing.T) {
setFileBreakpoint(p, t, fixture.Source, 16)
})
}
func TestListPackagesBuildInfo(t *testing.T) {
withTestProcess("pkgrenames", t, func(p proc.Process, fixture protest.Fixture) {
pkgs := p.BinInfo().ListPackagesBuildInfo(true)
t.Logf("returned %d", len(pkgs))
if len(pkgs) < 10 {
t.Errorf("very few packages returned")
}
for _, pkg := range pkgs {
t.Logf("%q %q", pkg.ImportPath, pkg.DirectoryPath)
const _fixtures = "_fixtures"
fidx := strings.Index(pkg.ImportPath, _fixtures)
if fidx < 0 {
continue
}
if !strings.HasSuffix(strings.Replace(pkg.DirectoryPath, "\\", "/", -1), pkg.ImportPath[fidx:]) {
t.Errorf("unexpected suffix: %q %q", pkg.ImportPath, pkg.DirectoryPath)
}
}
})
}

@ -848,6 +848,36 @@ func (env *Env) starlarkPredeclare() starlark.StringDict {
}
return env.interfaceToStarlarkValue(rpcRet), nil
})
r["packages_build_info"] = starlark.NewBuiltin("packages_build_info", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if err := isCancelled(thread); err != nil {
return starlark.None, decorateError(thread, err)
}
var rpcArgs rpc2.ListPackagesBuildInfoIn
var rpcRet rpc2.ListPackagesBuildInfoOut
if len(args) > 0 && args[0] != starlark.None {
err := unmarshalStarlarkValue(args[0], &rpcArgs.IncludeFiles, "IncludeFiles")
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
for _, kv := range kwargs {
var err error
switch kv[0].(starlark.String) {
case "IncludeFiles":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.IncludeFiles, "IncludeFiles")
default:
err = fmt.Errorf("unknown argument %q", kv[0])
}
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
err := env.ctx.Client().CallAPI("ListPackagesBuildInfo", &rpcArgs, &rpcRet)
if err != nil {
return starlark.None, err
}
return env.interfaceToStarlarkValue(rpcRet), nil
})
r["registers"] = starlark.NewBuiltin("registers", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if err := isCancelled(thread); err != nil {
return starlark.None, decorateError(thread, err)

@ -509,3 +509,10 @@ const (
// values saved in the runtime.g structure.
StacktraceG
)
// ImportPathToDirectoryPath maps an import path to a directory path.
type PackageBuildInfo struct {
ImportPath string
DirectoryPath string
Files []string
}

@ -1337,6 +1337,35 @@ func (d *Debugger) GetVersion(out *api.GetVersionOut) error {
return nil
}
// ListPackagesBuildInfo returns the list of packages used by the program along with
// the directory where each package was compiled and optionally the list of
// files constituting the package.
func (d *Debugger) ListPackagesBuildInfo(includeFiles bool) []api.PackageBuildInfo {
d.processMutex.Lock()
defer d.processMutex.Unlock()
pkgs := d.target.BinInfo().ListPackagesBuildInfo(includeFiles)
r := make([]api.PackageBuildInfo, 0, len(pkgs))
for _, pkg := range pkgs {
var files []string
if len(pkg.Files) > 0 {
files = make([]string, 0, len(pkg.Files))
for file := range pkg.Files {
files = append(files, file)
}
}
sort.Strings(files)
r = append(r, api.PackageBuildInfo{
ImportPath: pkg.ImportPath,
DirectoryPath: pkg.DirectoryPath,
Files: files,
})
}
return r
}
func go11DecodeErrorCheck(err error) error {
if _, isdecodeerr := err.(dwarf.DecodeError); !isdecodeerr {
return err

@ -731,3 +731,23 @@ func (s *RPCServer) ListDynamicLibraries(in ListDynamicLibrariesIn, out *ListDyn
out.List = s.debugger.ListDynamicLibraries()
return nil
}
// ListPackagesBuildInfoIn holds the arguments of ListPackages.
type ListPackagesBuildInfoIn struct {
IncludeFiles bool
}
// ListPackagesBuildInfoOut holds the return values of ListPackages.
type ListPackagesBuildInfoOut struct {
List []api.PackageBuildInfo
}
// ListPackagesBuildInfo returns the list of packages used by the program along with
// the directory where each package was compiled and optionally the list of
// files constituting the package.
// Note that the directory path is a best guess and may be wrong is a tool
// other than cmd/go is used to perform the build.
func (s *RPCServer) ListPackagesBuildInfo(in ListPackagesBuildInfoIn, out *ListPackagesBuildInfoOut) error {
out.List = s.debugger.ListPackagesBuildInfo(in.IncludeFiles)
return nil
}