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 <danderson@tailscale.com>
This commit is contained in:
David Anderson 2023-03-01 16:51:48 -08:00 committed by Dave Anderson
parent f18beaa1e4
commit 0df11253ec

56
release/dist/dist.go vendored
View File

@ -5,6 +5,7 @@
package dist package dist
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"log" "log"
@ -146,11 +147,11 @@ func (b *Build) Extra(key any, constructor func() any) any {
// GoPkg returns the path on disk of pkg. // GoPkg returns the path on disk of pkg.
// The module of pkg must be imported in b.Repo's go.mod. // The module of pkg must be imported in b.Repo's go.mod.
func (b *Build) GoPkg(pkg string) (string, error) { 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 { if err != nil {
return "", fmt.Errorf("finding package %q: %w", pkg, err) 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. // 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 // and do other initialization. Running `go version` once takes care of
// all of that and avoids that initialization happening concurrently // all of that and avoids that initialization happening concurrently
// later on in builds. // later on in builds.
_, err := exec.Command(b.Go, "version").Output() _, err := b.Command(b.Repo, b.Go, "version").CombinedOutput()
return err return err
}) })
if err != nil { if err != nil {
@ -197,15 +198,10 @@ func (b *Build) BuildGoBinary(path string, env map[string]string) (string, error
sort.Strings(envStrs) sort.Strings(envStrs)
log.Printf("Building %s (with env %s)", path, strings.Join(envStrs, " ")) log.Printf("Building %s (with env %s)", path, strings.Join(envStrs, " "))
buildDir := b.TmpDir() buildDir := b.TmpDir()
cmd := exec.Command(b.Go, "build", "-o", buildDir, path) cmd := b.Command(b.Repo, b.Go, "build", "-o", buildDir, path)
cmd.Dir = b.Repo
cmd.Env = os.Environ()
for k, v := range env { 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 { if err := cmd.Run(); err != nil {
return "", err 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) { func findModRoot(path string) (string, error) {
for { for {
modpath := filepath.Join(path, "go.mod") modpath := filepath.Join(path, "go.mod")