Skip to content

Commit

Permalink
Add ENABLE_BAZEL_PACKAGES_LOAD_HACK=true for poor Bazel users (#40)
Browse files Browse the repository at this point in the history
Bazel build systems try to keep hermeticity by setting PATH="." - but Go does not like this as
it is a security concern; almost all Go tooling relies on golang.org/x/tools/go/packages.Load
which behind the scenes must invoke `go` and uses the secure version of x/sys/execabs, but
ultimately this means Go tools like autogold cannot be run in Bazel:

golang/go#57304

Autogold relies on `packages.Load` in order to determine the Go package name / path when writing
out a Go AST representation of the value passed in; but the issue above means autogold cannot be
used with Bazel without removing "." from your PATH, which Bazel claims breaks hermeticity (one
of the whole reasons people use Bazel.)

For Bazel users, we allow them to set ENABLE_BAZEL_PACKAGES_LOAD_HACK=true which causes autogold to guess/infer
package names and paths using stack trace information and import paths. This is not perfect, it
doesn't respect packages whose import paths donot match their defined `package foo` statement for
example - but it's sufficient to enable autogold to be used in Bazel build environments where the
above Go/Bazel bug is found.

Signed-off-by: Stephen Gutekanst <stephen@sourcegraph.com>
  • Loading branch information
emidoots authored Feb 20, 2023
1 parent bae639c commit f2fed4f
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 7 deletions.
108 changes: 108 additions & 0 deletions bazel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package autogold

import (
"errors"
"os"
"runtime"
"strconv"
"strings"
)

// Bazel build systems try to keep hermeticity by setting PATH="." - but Go does not like this as
// it is a security concern; almost all Go tooling relies on golang.org/x/tools/go/packages.Load
// which behind the scenes must invoke `go` and uses the secure version of x/sys/execabs, but
// ultimately this means Go tools like autogold cannot be run in Bazel:
//
// https://github.com/golang/go/issues/57304
//
// Autogold relies on `packages.Load` in order to determine the Go package name / path when writing
// out a Go AST representation of the value passed in; but the issue above means autogold cannot be
// used with Bazel without removing "." from your PATH, which Bazel claims breaks hermeticity (one
// of the whole reasons people use Bazel.)
//
// For Bazel users, we allow them to set ENABLE_BAZEL_PACKAGES_LOAD_HACK=true which causes autogold
// to guess/infer package names and paths using stack trace information and import paths. This is
// not perfect, it doesn't respect packages whose import paths donot match their defined
// `package foo` statement for example - but it's sufficient to enable autogold to be used in Bazel
// build environments where the above Go/Bazel bug is found.

func isBazel() bool {
hacks, _ := strconv.ParseBool(os.Getenv("ENABLE_BAZEL_PACKAGES_LOAD_HACK"))
return hacks
}

// Guesses a package name and import path using Go debug stack trace information.
//
// It looks at the current goroutine's stack, finds the most recent function call in a `_test.go`
// file, and then guesses the package name and path based on the function name.
//
// This does not respect packages whose import path does not match their defined `package autogold_test`
// statement.
//
// This does not respect packages
func bazelGetPackageNameAndPath(dir string) (name, path string, err error) {
// Guesses an import path based on a function name like:
//
// github.com/hexops/autogold/v2.getPackageNameAndPath
// github.com/hexops/autogold/v2.Expect.func1
//
guessPkgPathFromFuncName := func(funcName string) string {
components := strings.Split(funcName, ".")
pkgPath := []string{}
for _, comp := range components {
pkgPath = append(pkgPath, comp)
if strings.Contains(comp, "/") {
break
}
}
return strings.Join(pkgPath, ".")
}

var (
file string
ok bool
pc uintptr
)
for caller := 1; caller < 10000; caller++ {
pc, file, _, ok = runtime.Caller(caller)
if !ok {
break
}
if !strings.Contains(file, "_test.go") {
continue
}
pkgPath := guessPkgPathFromFuncName(runtime.FuncForPC(pc).Name())
pkgName, _ := bazelPackagePathToName(pkgPath)
return pkgName, pkgPath, nil
}
return "", "", errors.New("unable to guess package name/path due to BAZEL_BAD=true")
}

// Guesses a Go package name based on the last component of a Go package path. e.g.:
//
// github.com/hexops/autogold/v2 -> autogold
// github.com/hexops/autogold -> autogold
// cmd/blobstore/internal/blobstore/blobstore_test_test -> blobstore_test
//
// Note that in the third case, Bazel appears to do some reckless renaming of Go package paths,
// where that package would otherwise have path "github.com/sourcegraph/sourcegraph/cmd/blobstore/internal/blobstore"
// and "package blobstore_test" as its name.
//
// This does not respect packages whose import path does not match their defined `package autogold_test`
// statement.
func bazelPackagePathToName(path string) (string, error) {
components := strings.Split(path, "/")
last := components[len(components)-1]
if !strings.Contains(path, ".") {
// Third case.
return strings.TrimSuffix(last, "_test"), nil
}
if strings.HasPrefix(last, "v") {
if _, err := strconv.ParseUint(last[1:], 10, 32); err == nil {
// Package path has a version suffix, e.g. github.com/hexops/autogold/v2
// and we want the "autogold" component not "v2"
last = components[len(components)-2]
}
}
return last, nil
}
3 changes: 3 additions & 0 deletions diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ func stringify(v interface{}, opts []Option) string {
if opt.forPackagePath != "" {
valastOpt.PackagePath = opt.forPackagePath
}
if isBazel() {
valastOpt.PackagePathToName = bazelPackagePathToName
}
if opt.allowRaw {
allowRaw = true
}
Expand Down
15 changes: 9 additions & 6 deletions expect.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ var (
)

func getPackageNameAndPath(dir string) (name, path string, err error) {
if isBazel() {
return bazelGetPackageNameAndPath(dir)
}
// If it is cached, fetch it from the cache. This prevents us from doing a semi-costly package
// load for every test that runs, instead requiring we only do it once per _test.go directory.
getPackageNameAndPathCacheMu.RLock()
Expand Down Expand Up @@ -122,16 +125,11 @@ func Expect(want interface{}) Value {
writeProfile()
t.Fatal(err)
}
testPath, err := filepath.Rel(pwd, file)
if err != nil {
writeProfile()
t.Fatal(err)
}

// Determine the package name and path of the test file, so we can unqualify types in
// that package.
start := time.Now()
pkgName, pkgPath, err := getPackageNameAndPath(filepath.Dir(testPath))
pkgName, pkgPath, err := getPackageNameAndPath(pwd)
profGetPackageNameAndPath = time.Since(start)
if err != nil {
writeProfile()
Expand Down Expand Up @@ -171,6 +169,11 @@ func Expect(want interface{}) Value {
// Replace the autogold.Expect(...) call's `want` parameter with the expression for
// the value we got.
start = time.Now()
testPath, err := filepath.Rel(pwd, file)
if err != nil {
writeProfile()
t.Fatal(err)
}
_, err = replaceExpect(t, testPath, testName, line, gotString, true)
profReplaceExpect = time.Since(start)
if err != nil {
Expand Down
6 changes: 5 additions & 1 deletion expect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,11 @@ func Test_getPackageNameAndPath_subdir(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if want := "test"; pkgName != want {
want := "test"
if isBazel() {
want = "autogold"
}
if pkgName != want {
t.Fatal("\ngot:\n", pkgName, "\nwant:\n", want)
}
if want := "github.com/hexops/autogold/v2/internal/test"; pkgPath != want {
Expand Down

0 comments on commit f2fed4f

Please sign in to comment.