Skip to content

Commit

Permalink
Add ENABLE_BAZEL_HACKS=true for poor Bazel users
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_HACKS=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
slimsag committed Feb 16, 2023
1 parent bae639c commit 52c04f4
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 6 deletions.
96 changes: 96 additions & 0 deletions bazel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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_HACKS=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_HACKS"))
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++ {
pc, file, _, ok = runtime.Caller(caller)
if !ok || strings.Contains(file, "_test.go") {
break
}
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
//
// 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.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

0 comments on commit 52c04f4

Please sign in to comment.