From c5cec6710e927457c3c29d6c156415e8539a5111 Mon Sep 17 00:00:00 2001 From: Heschi Kreinick Date: Mon, 24 Feb 2020 16:46:08 -0500 Subject: all: consolidate invokeGo implementations Over time we have accumulated a disturbing number of ways to run the Go command, each of which supported slightly different features and workarounds. Combine all of them. This unavoidably introduces some changes in debug logging behavior; I think we should try to fix them incrementally and consistently. Updates golang/go#37368. Change-Id: I664ca8685bf247a226be3cb807789c2fcddf233d Reviewed-on: https://go-review.googlesource.com/c/tools/+/220737 Run-TryBot: Heschi Kreinick Reviewed-by: Rebecca Stambler --- internal/gocommand/invoke.go | 92 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 internal/gocommand/invoke.go (limited to 'internal/gocommand/invoke.go') diff --git a/internal/gocommand/invoke.go b/internal/gocommand/invoke.go new file mode 100644 index 000000000..26cf7b18e --- /dev/null +++ b/internal/gocommand/invoke.go @@ -0,0 +1,92 @@ +// Package gocommand is a helper for calling the go command. +package gocommand + +import ( + "bytes" + "context" + "fmt" + "os/exec" + "strings" + "time" +) + +// An Invocation represents a call to the go command. +type Invocation struct { + Verb string + Args []string + BuildFlags []string + Env []string + WorkingDir string + Logf func(format string, args ...interface{}) +} + +// Run runs the invocation, returning its stdout and an error suitable for +// human consumption, including stderr. +func (i *Invocation) Run(ctx context.Context) (*bytes.Buffer, error) { + stdout, _, friendly, _ := i.RunRaw(ctx) + return stdout, friendly +} + +// RunRaw is like Run, but also returns the raw stderr and error for callers +// that want to do low-level error handling/recovery. +func (i *Invocation) RunRaw(ctx context.Context) (stdout *bytes.Buffer, stderr *bytes.Buffer, friendlyError error, rawError error) { + log := i.Logf + if log == nil { + log = func(string, ...interface{}) {} + } + + goArgs := []string{i.Verb} + switch i.Verb { + case "mod": + // mod needs the sub-verb before build flags. + goArgs = append(goArgs, i.Args[0]) + goArgs = append(goArgs, i.BuildFlags...) + goArgs = append(goArgs, i.Args[1:]...) + case "env": + // env doesn't take build flags. + goArgs = append(goArgs, i.Args...) + default: + goArgs = append(goArgs, i.BuildFlags...) + goArgs = append(goArgs, i.Args...) + } + cmd := exec.CommandContext(ctx, "go", goArgs...) + stdout = &bytes.Buffer{} + stderr = &bytes.Buffer{} + cmd.Stdout = stdout + cmd.Stderr = stderr + // On darwin the cwd gets resolved to the real path, which breaks anything that + // expects the working directory to keep the original path, including the + // go command when dealing with modules. + // The Go stdlib has a special feature where if the cwd and the PWD are the + // same node then it trusts the PWD, so by setting it in the env for the child + // process we fix up all the paths returned by the go command. + cmd.Env = append(append([]string{}, i.Env...), "PWD="+i.WorkingDir) + cmd.Dir = i.WorkingDir + + defer func(start time.Time) { log("%s for %v", time.Since(start), cmdDebugStr(cmd)) }(time.Now()) + + rawError = cmd.Run() + friendlyError = rawError + if rawError != nil { + // Check for 'go' executable not being found. + if ee, ok := rawError.(*exec.Error); ok && ee.Err == exec.ErrNotFound { + friendlyError = fmt.Errorf("go command required, not found: %v", ee) + } + if ctx.Err() != nil { + friendlyError = ctx.Err() + } + friendlyError = fmt.Errorf("err: %v: stderr: %s", rawError, stderr) + } + return +} + +func cmdDebugStr(cmd *exec.Cmd) string { + env := make(map[string]string) + for _, kv := range cmd.Env { + split := strings.Split(kv, "=") + k, v := split[0], split[1] + env[k] = v + } + + return fmt.Sprintf("GOROOT=%v GOPATH=%v GO111MODULE=%v GOPROXY=%v PWD=%v go %v", env["GOROOT"], env["GOPATH"], env["GO111MODULE"], env["GOPROXY"], env["PWD"], cmd.Args) +} -- cgit v1.2.3