From 0df11253eca9c0cd00d873e3314d7e3028446c06 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Wed, 1 Mar 2023 16:51:48 -0800 Subject: [PATCH] release/dist: add a helper to run commands The helper suppresses output if the command runs successfully. If the command fails, it dumps the buffered output to stdout before returning the error. This means the happy path isn't swamped by debug noise or xcode being intensely verbose about what kind of day it's having, but you still get debug output when something goes wrong. Updates tailscale/corp#9045 Signed-off-by: David Anderson --- release/dist/dist.go | 56 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/release/dist/dist.go b/release/dist/dist.go index 348d396d5..2a88c2e2d 100644 --- a/release/dist/dist.go +++ b/release/dist/dist.go @@ -5,6 +5,7 @@ package dist import ( + "bytes" "errors" "fmt" "log" @@ -146,11 +147,11 @@ func (b *Build) Extra(key any, constructor func() any) any { // GoPkg returns the path on disk of pkg. // The module of pkg must be imported in b.Repo's go.mod. func (b *Build) GoPkg(pkg string) (string, error) { - bs, err := exec.Command(b.Go, "list", "-f", "{{.Dir}}", pkg).Output() + out, err := b.Command(b.Repo, b.Go, "list", "-f", "{{.Dir}}", pkg).CombinedOutput() if err != nil { return "", fmt.Errorf("finding package %q: %w", pkg, err) } - return strings.TrimSpace(string(bs)), nil + return strings.TrimSpace(out), nil } // TmpDir creates and returns a new empty temporary directory. @@ -178,7 +179,7 @@ func (b *Build) BuildGoBinary(path string, env map[string]string) (string, error // and do other initialization. Running `go version` once takes care of // all of that and avoids that initialization happening concurrently // later on in builds. - _, err := exec.Command(b.Go, "version").Output() + _, err := b.Command(b.Repo, b.Go, "version").CombinedOutput() return err }) if err != nil { @@ -197,15 +198,10 @@ func (b *Build) BuildGoBinary(path string, env map[string]string) (string, error sort.Strings(envStrs) log.Printf("Building %s (with env %s)", path, strings.Join(envStrs, " ")) buildDir := b.TmpDir() - cmd := exec.Command(b.Go, "build", "-o", buildDir, path) - cmd.Dir = b.Repo - cmd.Env = os.Environ() + cmd := b.Command(b.Repo, b.Go, "build", "-o", buildDir, path) for k, v := range env { - cmd.Env = append(cmd.Env, k+"="+v) + cmd.Cmd.Env = append(cmd.Cmd.Env, k+"="+v) } - cmd.Env = append(cmd.Env, "TS_USE_GOCROSS=1") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return "", err } @@ -217,6 +213,46 @@ func (b *Build) BuildGoBinary(path string, env map[string]string) (string, error }) } +// Command prepares an exec.Cmd to run [cmd, args...] in dir. +func (b *Build) Command(dir, cmd string, args ...string) *Command { + ret := &Command{ + Cmd: exec.Command(cmd, args...), + } + ret.Cmd.Stdout = &ret.Output + ret.Cmd.Stderr = &ret.Output + // dist always wants to use gocross if any Go is involved. + ret.Cmd.Env = append(os.Environ(), "TS_USE_GOCROSS=1") + ret.Cmd.Dir = dir + return ret +} + +// Command runs an exec.Cmd and returns its exit status. If the command fails, +// its output is printed to os.Stdout, otherwise it's suppressed. +type Command struct { + Cmd *exec.Cmd + Output bytes.Buffer +} + +// Run is like c.Cmd.Run, but if the command fails, its output is printed to +// os.Stdout before returning the error. +func (c *Command) Run() error { + err := c.Cmd.Run() + if err != nil { + // Command failed, dump its output. + os.Stdout.Write(c.Output.Bytes()) + } + return err +} + +// CombinedOutput is like c.Cmd.CombinedOutput, but returns the output as a +// string instead of a byte slice. +func (c *Command) CombinedOutput() (string, error) { + c.Cmd.Stdout = nil + c.Cmd.Stderr = nil + bs, err := c.Cmd.CombinedOutput() + return string(bs), err +} + func findModRoot(path string) (string, error) { for { modpath := filepath.Join(path, "go.mod")