diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 56df7f125f..6c6606952a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,6 +6,10 @@ on: pull_request: schedule: - cron: '0 2 * * *' # Run every day, at 2AM UTC. + +permissions: + contents: read + env: GOPATH: ${{ github.workspace }} WORKING_DIR: ./src/github.com/google/pprof/ @@ -18,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - go: ['1.24', '1.25', 'tip'] + go: ['1.25', '1.26'] # Supported macOS versions can be found in # https://github.com/actions/virtual-environments#available-environments. os: ['macos-14', 'macos-15'] @@ -48,13 +52,12 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: ${{ env.WORKING_DIR }} - name: Update Go version using setup-go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - if: matrix.go != 'tip' + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: # Include cache directives to allow proper caching. Without them, we # get setup-go "Restore cache failed" warnings. @@ -62,29 +65,6 @@ jobs: cache: true cache-dependency-path: '**/go.sum' - - name: Install Go bootstrap compiler - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - if: matrix.go == 'tip' - with: - # Bootstrapping go tip requires 1.24 - # Include cache directives to allow proper caching. Without them, we - # get setup-go "Restore cache failed" warnings. - go-version: 1.24 - cache: true - cache-dependency-path: '**/go.sum' - - - name: Update Go version manually - if: matrix.go == 'tip' - working-directory: ${{ github.workspace }} - run: | - git clone https://go.googlesource.com/go $HOME/gotip - cd $HOME/gotip/src - ./make.bash - echo "GOROOT=$HOME/gotip" >> $GITHUB_ENV - echo "RUN_STATICCHECK=false" >> $GITHUB_ENV - echo "RUN_GOLANGCI_LINTER=false" >> $GITHUB_ENV - echo "$HOME/gotip/bin:$PATH" >> $GITHUB_PATH - - name: Set up Xcode uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: @@ -120,17 +100,16 @@ jobs: strategy: fail-fast: false matrix: - go: ['1.24', '1.25', 'tip'] + go: ['1.25', '1.26'] os: ['ubuntu-24.04', 'ubuntu-22.04'] steps: - name: Checkout the repo - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: ${{ env.WORKING_DIR }} - name: Update Go version using setup-go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - if: matrix.go != 'tip' + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: # Include cache directives to allow proper caching. Without them, we # get setup-go "Restore cache failed" warnings. @@ -138,29 +117,6 @@ jobs: cache: true cache-dependency-path: '**/go.sum' - - name: Install Go bootstrap compiler - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - if: matrix.go == 'tip' - with: - # Bootstrapping go tip requires 1.24 - # Include cache directives to allow proper caching. Without them, we - # get setup-go "Restore cache failed" warnings. - go-version: 1.24 - cache: true - cache-dependency-path: '**/go.sum' - - - name: Update Go version manually - if: matrix.go == 'tip' - working-directory: ${{ github.workspace }} - run: | - git clone https://go.googlesource.com/go $HOME/gotip - cd $HOME/gotip/src - ./make.bash - echo "GOROOT=$HOME/gotip" >> $GITHUB_ENV - echo "RUN_STATICCHECK=false" >> $GITHUB_ENV - echo "RUN_GOLANGCI_LINTER=false" >> $GITHUB_ENV - echo "$HOME/gotip/bin" >> $GITHUB_PATH - - name: Check chrome for browser tests run: | google-chrome --version @@ -214,15 +170,15 @@ jobs: strategy: fail-fast: false matrix: - go: ['1.24', '1.25'] + go: ['1.25', '1.26'] steps: - name: Checkout the repo - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: ${{ env.WORKING_DIR }} - name: Update Go version using setup-go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: # Include cache directives to allow proper caching. Without them, we # get setup-go "Restore cache failed" warnings. diff --git a/browsertests/go.mod b/browsertests/go.mod index 475c481489..e3b94583b9 100644 --- a/browsertests/go.mod +++ b/browsertests/go.mod @@ -1,8 +1,6 @@ module github.com/google/pprof/browsertests -go 1.24.0 - -toolchain go1.24.9 +go 1.25.0 // Use the version of pprof in this directory tree. replace github.com/google/pprof => ../ diff --git a/go.mod b/go.mod index 97386cfe13..09b01517d1 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/google/pprof -go 1.24.0 - -toolchain go1.24.9 +go 1.25.0 require ( github.com/chzyer/readline v1.5.1 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