diff --git a/internal/report/source.go b/internal/report/source.go index 771f500ec1..8a8284f7ab 100644 --- a/internal/report/source.go +++ b/internal/report/source.go @@ -25,6 +25,7 @@ import ( "os" "path/filepath" "regexp" + "runtime" "slices" "sort" "strconv" @@ -1007,28 +1008,95 @@ func openSourceFile(path, searchPath, trim string) (*os.File, error) { path = trimPath(path, trim, searchPath) // If file is still absolute, require file to exist. if filepath.IsAbs(path) { - f, err := os.Open(path) - return f, err - } - // Scan each component of the path. - for _, dir := range filepath.SplitList(searchPath) { - // Search up for every parent of each possible path. - for { - filename := filepath.Join(dir, path) - if f, err := os.Open(filename); err == nil { - return f, nil - } - parent := filepath.Dir(dir) - if parent == dir { - break + if f, err := tryOpenFile(path); err == nil { + return f, nil + } + } else { + // Scan each component of the path. + for _, dir := range filepath.SplitList(searchPath) { + // Search up for every parent of each possible path. + for { + filename := filepath.Join(dir, path) + if f, err := tryOpenFile(filename); err == nil { + return f, nil + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent } - dir = parent } } + // Fall back to looking for Go standard library sources under the local + // $GOROOT/src, since profiles from Go programs refer to standard library + // files under the GOROOT of the machine where the program was built. + if f, err := openGorootSourceFile(path, gorootSrc); err == nil { + return f, nil + } return nil, fmt.Errorf("could not find file %s on path %s", path, searchPath) } +// tryOpenFile opens the file at filename if it exists and is not a directory, +// which os.Open would also happily open. +func tryOpenFile(filename string) (*os.File, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + stat, err := f.Stat() + if err != nil { + f.Close() + return nil, err + } + if stat.IsDir() { + f.Close() + return nil, fmt.Errorf("%s is a directory", filename) + } + return f, nil +} + +// gorootSrc is the directory holding the Go standard library sources, if +// known. runtime.GOROOT honors the $GOROOT environment variable and otherwise +// reports the GOROOT this binary was built with, which is the Go installation +// used to `go install` pprof in the common case. +var gorootSrc = func() string { + if goroot := runtime.GOROOT(); goroot != "" { + return filepath.Join(goroot, "src") + } + return "" +}() + +// openGorootSourceFile tries to open a Go standard library source file under +// gorootSrc. Binaries built without -trimpath record standard library files +// under the build machine's GOROOT (e.g. /usr/local/go/src/runtime/proc.go), +// which may not exist locally, so resolve the path components after a "/src/" +// component against gorootSrc. Binaries built with -trimpath record them +// relative to $GOROOT/src (e.g. runtime/proc.go), so resolve relative paths +// against gorootSrc directly. +func openGorootSourceFile(path, gorootSrc string) (*os.File, error) { + if gorootSrc == "" { + return nil, fmt.Errorf("GOROOT is not known") + } + if !filepath.IsAbs(path) { + if f, err := tryOpenFile(filepath.Join(gorootSrc, path)); err == nil { + return f, nil + } + } + sPath := filepath.ToSlash(path) + for { + i := strings.Index(sPath, "/src/") + if i == -1 { + return nil, fmt.Errorf("could not find file %s under %s", path, gorootSrc) + } + sPath = sPath[i+len("/src/"):] + if f, err := tryOpenFile(filepath.Join(gorootSrc, filepath.FromSlash(sPath))); err == nil { + return f, nil + } + } +} + // trimPath cleans up a path by removing prefixes that are commonly // found on profiles plus configured prefixes. // TODO(aalexand): Consider optimizing out the redundant work done in this diff --git a/internal/report/source_test.go b/internal/report/source_test.go index dce64bc438..5ef39aabeb 100644 --- a/internal/report/source_test.go +++ b/internal/report/source_test.go @@ -180,6 +180,12 @@ func TestOpenSourceFile(t *testing.T) { desc: "error when not found", path: "foo.cc", }, + { + desc: "directory is not matched", + searchPath: "$dir", + fs: []string{"foo/bar.cc"}, + path: "foo", + }, } { t.Run(tc.desc, func(t *testing.T) { defer func() { @@ -215,6 +221,131 @@ func TestOpenSourceFile(t *testing.T) { } } +func TestOpenGorootSourceFile(t *testing.T) { + gorootSrc := filepath.Join(t.TempDir(), "goroot", "src") + for _, f := range []string{"runtime/proc.go", "fmt/print.go"} { + path := filepath.Join(gorootSrc, filepath.FromSlash(f)) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatalf("failed to create dir for %q: %v", path, err) + } + if err := os.WriteFile(path, nil, 0644); err != nil { + t.Fatalf("failed to create file %q: %v", path, err) + } + } + for _, tc := range []struct { + desc string + path string + noGoroot bool + wantPath string // Relative to gorootSrc. If empty, error is wanted. + }{ + { + desc: "absolute path under a foreign GOROOT", + path: "/usr/local/go/src/runtime/proc.go", + wantPath: "runtime/proc.go", + }, + { + desc: "absolute path with multiple src components", + path: "/home/user/src/go/src/fmt/print.go", + wantPath: "fmt/print.go", + }, + { + desc: "relative path from a -trimpath build", + path: "runtime/proc.go", + wantPath: "runtime/proc.go", + }, + { + desc: "absolute path without a src component", + path: "/usr/local/go/runtime/proc.go", + }, + { + desc: "file missing from GOROOT", + path: "/usr/local/go/src/runtime/missing.go", + }, + { + desc: "unknown GOROOT", + path: "/usr/local/go/src/runtime/proc.go", + noGoroot: true, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + src := gorootSrc + if tc.noGoroot { + src = "" + } + path := filepath.FromSlash(tc.path) + f, err := openGorootSourceFile(path, src) + if tc.wantPath == "" { + if err == nil { + gotPath := f.Name() + f.Close() + t.Fatalf("openGorootSourceFile(%q) = %q, want error", path, gotPath) + } + return + } + if err != nil { + t.Fatalf("openGorootSourceFile(%q) = err %v, want path %q", path, err, tc.wantPath) + } + defer f.Close() + if want := filepath.Join(src, filepath.FromSlash(tc.wantPath)); f.Name() != want { + t.Errorf("openGorootSourceFile(%q) = %q, want %q", path, f.Name(), want) + } + }) + } +} + +func TestOpenSourceFileGorootFallback(t *testing.T) { + fakeGorootSrc := filepath.Join(t.TempDir(), "goroot", "src") + path := filepath.Join(fakeGorootSrc, "runtime", "proc.go") + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatalf("failed to create dir for %q: %v", path, err) + } + if err := os.WriteFile(path, nil, 0644); err != nil { + t.Fatalf("failed to create file %q: %v", path, err) + } + savedGorootSrc := gorootSrc + gorootSrc = fakeGorootSrc + defer func() { gorootSrc = savedGorootSrc }() + + for _, tc := range []struct { + desc string + path string + wantErr bool + }{ + { + desc: "absolute path under a foreign GOROOT", + path: "/usr/local/go/src/runtime/proc.go", + }, + { + desc: "relative path from a -trimpath build", + path: "runtime/proc.go", + }, + { + desc: "not found under GOROOT falls through to error", + path: "runtime/missing.go", + wantErr: true, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + f, err := openSourceFile(filepath.FromSlash(tc.path), "", "") + if tc.wantErr { + if err == nil { + gotPath := f.Name() + f.Close() + t.Fatalf("openSourceFile(%q) = %q, want error", tc.path, gotPath) + } + return + } + if err != nil { + t.Fatalf("openSourceFile(%q) = err %v, want path %q", tc.path, err, path) + } + defer f.Close() + if f.Name() != path { + t.Errorf("openSourceFile(%q) = %q, want %q", tc.path, f.Name(), path) + } + }) + } +} + func TestIndentation(t *testing.T) { for _, c := range []struct { str string