From 9adbff4f2b3606854fcb3ade7bc520e3b5f63af5 Mon Sep 17 00:00:00 2001 From: Ross Light Date: Mon, 26 Apr 2021 08:22:52 -0700 Subject: [PATCH] Make output more compact and easier to read (#295) Build output now includes a target and command heading. Info-level log lines no longer have the unhelpful "INFO" prefix. All non-error build output now goes to stdout so someone who wants a more quiet experience for a successful build can redirect to `/dev/null`. Individual lines of command and log output now also include the target they are running for and the time the line was outputted. This serves to both visually group outputs by command as well as provide fine-grained timing information. Also propagate `NO_COLOR` to build environment. [Fixes ch4043] [Fixes ch4040] --- CHANGELOG.md | 6 ++ cmd/yb/build.go | 61 ++++++-------- cmd/yb/exec.go | 4 +- cmd/yb/main.go | 21 ++++- cmd/yb/output.go | 158 +++++++++++++++++++++++++---------- cmd/yb/output_test.go | 110 ++++++++++++++++++++++++ cmd/yb/run.go | 3 +- cmd/yb/styles.go | 91 ++++++++++++++++++++ internal/biome/biome.go | 5 +- internal/biome/docker.go | 5 +- internal/build/build.go | 7 +- internal/build/build_test.go | 2 +- 12 files changed, 384 insertions(+), 89 deletions(-) create mode 100644 cmd/yb/output_test.go create mode 100644 cmd/yb/styles.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a81210e2..346d429f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,11 +22,17 @@ The format is based on [Keep a Changelog][], and this project adheres to error if the targets have a dependency cycle. - yb attempts to detect some common Docker configuration issues and inform the user about them. +- yb now obeys the [`NO_COLOR` environment variable][] and propagates it to the + build environment. + +[`NO_COLOR` environment variable]: https://no-color.org/ ### Changed - Commands run as part of `build`, `exec`, or `run` now run without Docker by default. You can get the old behavior by running with `--mode=container`. +- Tool output has been changed to be more compact, to be easier to trace + command output, and to include more timing information. - `yb platform` is now an alias for `yb version`. ### Fixed diff --git a/cmd/yb/build.go b/cmd/yb/build.go index 732d91db..e92ced30 100644 --- a/cmd/yb/build.go +++ b/cmd/yb/build.go @@ -3,7 +3,7 @@ package main import ( "context" "fmt" - "io/ioutil" + "io" "os" "strings" "time" @@ -98,6 +98,7 @@ func (b *buildCmd) run(ctx context.Context) error { startTime := time.Now() ctx, span := ybtrace.Start(ctx, "Build", trace.WithNewRoot()) defer span.End() + ctx = withStdoutLogs(ctx) log.Infof(ctx, "Build started at %s", startTime.Format(longTimeFormat)) @@ -117,10 +118,10 @@ func (b *buildCmd) run(ctx context.Context) error { showDockerWarningsIfNeeded(ctx, b.mode, buildTargets) // Do the build! - startSection("BUILD") log.Debugf(ctx, "Building package %s in %s...", targetPackage.Name, targetPackage.Path) buildError := doTargetList(ctx, targetPackage, buildTargets, &doOptions{ + output: os.Stdout, executionMode: b.mode, dockerClient: dockerClient, dataDirs: dataDirs, @@ -132,27 +133,27 @@ func (b *buildCmd) run(ctx context.Context) error { }) if buildError != nil { span.SetStatus(codes.Unknown, buildError.Error()) + log.Errorf(ctx, "%v", buildError) } span.End() endTime := time.Now() buildTime := endTime.Sub(startTime) - log.Infof(ctx, "") - log.Infof(ctx, "Build finished at %s, taking %s", endTime.Format(longTimeFormat), buildTime) - log.Infof(ctx, "") - - log.Infof(ctx, "%s", buildTraces.dump()) + fmt.Printf("\nBuild finished at %s, taking %v\n\n", endTime.Format(longTimeFormat), buildTime.Truncate(time.Millisecond)) + fmt.Println(buildTraces.dump()) + style := termStylesFromEnv() if buildError != nil { - subSection("BUILD FAILED") - return buildError + fmt.Printf("%sBUILD FAILED%s ❌\n", style.buildResult(false), style.reset()) + return alreadyLoggedError{buildError} } - subSection("BUILD SUCCEEDED") + fmt.Printf("%sBUILD PASSED%s ️✔️\n", style.buildResult(true), style.reset()) return nil } type doOptions struct { + output io.Writer dataDirs *ybdata.Dirs downloader *ybdata.Downloader executionMode executionMode @@ -198,6 +199,11 @@ func doTargetList(ctx context.Context, pkg *yb.Package, targets []*yb.Target, op } func doTarget(ctx context.Context, pkg *yb.Package, target *yb.Target, opts *doOptions) error { + style := termStylesFromEnv() + fmt.Printf("\n🎯 %sTarget: %s%s\n", style.target(), target.Name, style.reset()) + + ctx = withLogPrefix(ctx, target.Name) + bio, err := newBiome(ctx, target, newBiomeOptions{ packageDir: pkg.Path, dataDirs: opts.dataDirs, @@ -216,15 +222,17 @@ func doTarget(ctx context.Context, pkg *yb.Package, target *yb.Target, opts *doO log.Warnf(ctx, "Clean up environment: %v", err) } }() + output := newLinePrefixWriter(opts.output, target.Name) sys := build.Sys{ Biome: bio, Downloader: opts.downloader, DockerClient: opts.dockerClient, DockerNetworkID: opts.dockerNetworkID, - Stdout: os.Stdout, - Stderr: os.Stderr, + + Stdout: output, + Stderr: output, } - execBiome, err := build.Setup(ctx, sys, target) + execBiome, err := build.Setup(withLogPrefix(ctx, setupLogPrefix), sys, target) if err != nil { return err } @@ -241,34 +249,15 @@ func doTarget(ctx context.Context, pkg *yb.Package, target *yb.Target, opts *doO return nil } - subSection(fmt.Sprintf("Build target: %s", target.Name)) - log.Infof(ctx, "Executing build steps...") - return build.Execute(ctx, sys, target) + return build.Execute(withStdoutLogs(ctx), sys, announceCommand, target) } -// Because, why not? -// Based on https://github.com/sindresorhus/is-docker/blob/master/index.js and https://github.com/moby/moby/issues/18355 -// Discussion is not settled yet: https://stackoverflow.com/questions/23513045/how-to-check-if-a-process-is-running-inside-docker-container#25518538 -func insideTheMatrix() bool { - hasDockerEnv := pathExists("/.dockerenv") - hasDockerCGroup := false - dockerCGroupPath := "/proc/self/cgroup" - if pathExists(dockerCGroupPath) { - contents, _ := ioutil.ReadFile(dockerCGroupPath) - hasDockerCGroup = strings.Count(string(contents), "docker") > 0 - } - return hasDockerEnv || hasDockerCGroup +func announceCommand(cmdString string) { + style := termStylesFromEnv() + fmt.Printf("%s> %s%s\n", style.command(), cmdString, style.reset()) } func pathExists(path string) bool { _, err := os.Lstat(path) return !os.IsNotExist(err) } - -func startSection(name string) { - fmt.Printf(" === %s ===\n", name) -} - -func subSection(name string) { - fmt.Printf(" -- %s -- \n", name) -} diff --git a/cmd/yb/exec.go b/cmd/yb/exec.go index c9bd6a7a..ee43cffb 100644 --- a/cmd/yb/exec.go +++ b/cmd/yb/exec.go @@ -99,7 +99,7 @@ func (b *execCmd) run(ctx context.Context) error { Stdout: os.Stdout, Stderr: os.Stderr, } - execBiome, err := build.Setup(ctx, sys, execTarget) + execBiome, err := build.Setup(withLogPrefix(ctx, execTarget.Name+setupLogPrefix), sys, execTarget) if err != nil { return err } @@ -109,5 +109,5 @@ func (b *execCmd) run(ctx context.Context) error { log.Errorf(ctx, "Clean up environment %s: %v", b.execEnvName, err) } }() - return build.Execute(ctx, sys, execTarget) + return build.Execute(ctx, sys, announceCommand, execTarget) } diff --git a/cmd/yb/main.go b/cmd/yb/main.go index a3e8538c..fa9e68fc 100644 --- a/cmd/yb/main.go +++ b/cmd/yb/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -87,12 +88,28 @@ func main() { err = rootCmd.ExecuteContext(ctx) cancel() if err != nil { - initLog(cfg, false) - log.Errorf(ctx, "%v", err) + if !errors.As(err, new(alreadyLoggedError)) { + initLog(cfg, false) + log.Errorf(ctx, "%v", err) + } os.Exit(1) } } +// alreadyLoggedError wraps another error to signal that it has already been +// shown to the user. If returned from a subcommand, then main() will not log it. +type alreadyLoggedError struct { + err error +} + +func (e alreadyLoggedError) Error() string { + return e.err.Error() +} + +func (e alreadyLoggedError) Unwrap() error { + return e.err +} + func displayOldDirectoryWarning(ctx context.Context) { home := os.Getenv("HOME") if home == "" { diff --git a/cmd/yb/output.go b/cmd/yb/output.go index 5c00e35c..07e270b2 100644 --- a/cmd/yb/output.go +++ b/cmd/yb/output.go @@ -17,26 +17,32 @@ package main import ( + "bytes" "context" "fmt" "io" "os" - "strconv" "strings" "sync" + "time" - "github.com/yourbase/commons/envvar" "github.com/yourbase/yb/internal/config" "go.opentelemetry.io/otel/api/trace" exporttrace "go.opentelemetry.io/otel/sdk/export/trace" "zombiezen.com/go/log" ) -const longTimeFormat = "15:04:05 MST" +const ( + shortTimeFormat = "15:04:05" + longTimeFormat = "15:04:05 MST" +) + +// setupLogPrefix is the log prefix used with withLogPrefix when running a +// target's setup. +const setupLogPrefix = ".deps" type logger struct { - color bool - showLevels bool + color termStyles mu sync.Mutex buf []byte @@ -49,50 +55,49 @@ func initLog(cfg config.Getter, showDebug bool) { log.SetDefault(&log.LevelFilter{ Min: configuredLogLevel(cfg, showDebug), Output: &logger{ - color: colorLogs(), - showLevels: showLogLevels(cfg), + color: termStylesFromEnv(), }, }) }) } func (l *logger) Log(ctx context.Context, entry log.Entry) { + logToStdout, _ := ctx.Value(logToStdoutKey{}).(bool) + logToStdout = logToStdout && entry.Level < log.Warn + var colorSequence string + switch { + case entry.Level < log.Info: + colorSequence = l.color.debug() + case entry.Level >= log.Error: + colorSequence = l.color.failure() + } + prefix, _ := ctx.Value(logPrefixKey{}).(string) + l.mu.Lock() defer l.mu.Unlock() l.buf = l.buf[:0] - if l.showLevels { - if l.color { - switch { - case entry.Level >= log.Error: - // Red text - l.buf = append(l.buf, "\x1b[31m"...) - case entry.Level >= log.Warn: - // Yellow text - l.buf = append(l.buf, "\x1b[33m"...) - default: - // Cyan text - l.buf = append(l.buf, "\x1b[36m"...) - } - } + l.buf = append(l.buf, colorSequence...) + for _, line := range strings.Split(strings.TrimSuffix(entry.Msg, "\n"), "\n") { + l.buf = appendLogPrefix(l.buf, entry.Time, prefix) switch { case entry.Level >= log.Error: - l.buf = append(l.buf, "ERROR"...) + l.buf = append(l.buf, "❌ "...) case entry.Level >= log.Warn: - l.buf = append(l.buf, "WARN"...) - case entry.Level >= log.Info: - l.buf = append(l.buf, "INFO"...) - default: - l.buf = append(l.buf, "DEBUG"...) + l.buf = append(l.buf, "⚠️ "...) } - if l.color { - l.buf = append(l.buf, "\x1b[0m"...) - } - l.buf = append(l.buf, ' ') + l.buf = append(l.buf, line...) + l.buf = append(l.buf, '\n') + } + if colorSequence != "" { + l.buf = append(l.buf, l.color.reset()...) + } + + out := os.Stderr + if logToStdout { + out = os.Stdout } - l.buf = append(l.buf, entry.Msg...) - l.buf = append(l.buf, '\n') - os.Stderr.Write(l.buf) + out.Write(l.buf) } func (l *logger) LogEnabled(entry log.Entry) bool { @@ -115,18 +120,83 @@ func configuredLogLevel(cfg config.Getter, showDebug bool) log.Level { return log.Info } -func colorLogs() bool { - b, _ := strconv.ParseBool(envvar.Get("CLICOLOR", "1")) - return b +type logToStdoutKey struct{} + +func withStdoutLogs(parent context.Context) context.Context { + if isPresent, _ := parent.Value(logToStdoutKey{}).(bool); isPresent { + return parent + } + return context.WithValue(parent, logToStdoutKey{}, true) +} + +type logPrefixKey struct{} + +func withLogPrefix(parent context.Context, prefix string) context.Context { + parentPrefix, _ := parent.Value(logPrefixKey{}).(string) + return context.WithValue(parent, logPrefixKey{}, parentPrefix+prefix) +} + +// linePrefixWriter prepends a timestamp and a prefix string to every line +// written to it and writes to an underlying writer. +type linePrefixWriter struct { + dst io.Writer + prefix string + buf []byte + wrote bool + now func() time.Time +} + +func newLinePrefixWriter(w io.Writer, prefix string) *linePrefixWriter { + return &linePrefixWriter{ + dst: w, + prefix: prefix, + now: time.Now, + } +} + +// Write writes p to the underlying writer, inserting line prefixes as required. +// Write will issue at most one Write call per line on the underlying writer. +func (lp *linePrefixWriter) Write(p []byte) (int, error) { + now := lp.now() + origLen := len(p) + for next := []byte(nil); len(p) > 0; p = next { + line := p + next = nil + lineEnd := bytes.IndexByte(p, '\n') + if lineEnd != -1 { + line, next = p[:lineEnd+1], p[lineEnd+1:] + } + if lp.wrote { + // Pass through rest of line if we already wrote the prefix. + n, err := lp.dst.Write(line) + lp.wrote = lineEnd == -1 + if err != nil { + return origLen - (len(p) + n), err + } + continue + } + lp.buf = appendLogPrefix(lp.buf[:0], now, lp.prefix) + lp.buf = append(lp.buf, line...) + n, err := lp.dst.Write(lp.buf) + lp.wrote = n > 0 && (n < len(lp.buf) || lineEnd == -1) + if err != nil { + n -= len(lp.buf) - len(line) // exclude prefix length + return origLen - (len(p) + n), err + } + } + return origLen, nil } -func showLogLevels(cfg config.Getter) bool { - out := config.Get(cfg, "defaults", "no-pretty-output") - if out != "" { - b, _ := strconv.ParseBool(out) - return !b +// appendLogPrefix formats the given timestamp and label and appends the result +// to dst. +func appendLogPrefix(dst []byte, t time.Time, prefix string) []byte { + if prefix == "" { + return dst } - return !envvar.Bool("YB_NO_PRETTY_OUTPUT") + dst = t.AppendFormat(dst, shortTimeFormat) + buf := bytes.NewBuffer(dst) + fmt.Fprintf(buf, " %-18s| ", prefix) + return buf.Bytes() } // A traceSink records spans in memory. The zero value is an empty sink. @@ -160,7 +230,7 @@ const ( // received. It is safe to call concurrently, including with ExportSpan. func (sink *traceSink) dump() string { sb := new(strings.Builder) - fmt.Fprintf(sb, "%-*s %-*s %-*s\n", + fmt.Fprintf(sb, "%-*s %-*s %*s\n", traceDumpStartWidth, "Start", traceDumpEndWidth, "End", traceDumpElapsedWidth, "Elapsed", diff --git a/cmd/yb/output_test.go b/cmd/yb/output_test.go new file mode 100644 index 00000000..ea86e8a3 --- /dev/null +++ b/cmd/yb/output_test.go @@ -0,0 +1,110 @@ +// Copyright 2021 YourBase Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "io" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestLinePrefixWriter(t *testing.T) { + start := time.Date(2021, time.April, 21, 14, 10, 23, 0, time.FixedZone("America/Los_Angeles", -7*60*60)) + const defaultPrefix = "default" + const defaultPaddedPrefix = "default |" + tests := []struct { + name string + prefix string + writes []string + want string + }{ + { + name: "Empty", + prefix: defaultPrefix, + writes: []string{""}, + want: "", + }, + { + name: "PartialLine", + prefix: defaultPrefix, + writes: []string{"foo"}, + want: "14:10:23 " + defaultPaddedPrefix + " foo", + }, + { + name: "FullLine", + prefix: defaultPrefix, + writes: []string{"foo\n"}, + want: "14:10:23 " + defaultPaddedPrefix + " foo\n", + }, + { + name: "MultipleLines/OneWrite", + prefix: defaultPrefix, + writes: []string{"foo\nbar\n"}, + want: "14:10:23 " + defaultPaddedPrefix + " foo\n" + + "14:10:23 " + defaultPaddedPrefix + " bar\n", + }, + { + name: "MultipleLines/MultipleWrites", + prefix: defaultPrefix, + writes: []string{"foo\n", "bar\n"}, + want: "14:10:23 " + defaultPaddedPrefix + " foo\n" + + "14:10:24 " + defaultPaddedPrefix + " bar\n", + }, + { + name: "MultipleLines/Split", + prefix: defaultPrefix, + writes: []string{"foo\nb", "ar\n"}, + want: "14:10:23 " + defaultPaddedPrefix + " foo\n" + + "14:10:23 " + defaultPaddedPrefix + " bar\n", + }, + { + name: "RSpecDots", + prefix: defaultPrefix, + writes: []string{".", ".", ".", ".", "\n"}, + want: "14:10:23 " + defaultPaddedPrefix + " ....\n", + }, + { + name: "LongPrefix", + prefix: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + writes: []string{"foo\n"}, + want: "14:10:23 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| foo\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + currTime := start + out := new(strings.Builder) + w := newLinePrefixWriter(out, test.prefix) + w.now = func() time.Time { + result := currTime + currTime = currTime.Add(1 * time.Second) + return result + } + for i, data := range test.writes { + if n, err := io.WriteString(w, data); n != len(data) || err != nil { + t.Errorf("Write[%d](%q) = %d, %v; want %d, ", i, data, n, err, len(data)) + } + } + if diff := cmp.Diff(test.want, out.String()); diff != "" { + t.Errorf("Output (-want +got):\n%s", diff) + } + }) + } +} diff --git a/cmd/yb/run.go b/cmd/yb/run.go index 5dac0c58..7639421e 100644 --- a/cmd/yb/run.go +++ b/cmd/yb/run.go @@ -82,6 +82,7 @@ func (b *runCmd) run(ctx context.Context, args []string) error { // Build dependencies before running command. err = doTargetList(ctx, pkg, targets[:len(targets)-1], &doOptions{ + output: os.Stderr, executionMode: b.mode, dockerClient: dockerClient, dockerNetworkID: dockerNetworkID, @@ -121,7 +122,7 @@ func (b *runCmd) run(ctx context.Context, args []string) error { Stdout: os.Stdout, Stderr: os.Stderr, } - execBiome, err := build.Setup(ctx, sys, execTarget) + execBiome, err := build.Setup(withLogPrefix(ctx, execTarget.Name+setupLogPrefix), sys, execTarget) if err != nil { return err } diff --git a/cmd/yb/styles.go b/cmd/yb/styles.go new file mode 100644 index 00000000..5e114e1c --- /dev/null +++ b/cmd/yb/styles.go @@ -0,0 +1,91 @@ +// Copyright 2021 YourBase Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "os" + "strconv" + + "github.com/yourbase/commons/envvar" +) + +// termStyles generates ANSI escape codes for specific styles. +// The zero value will not return any special escape codes. +// https://en.wikipedia.org/wiki/ANSI_escape_code +type termStyles bool + +func termStylesFromEnv() termStyles { + if os.Getenv("NO_COLOR") != "" { + // https://no-color.org/ + return false + } + b, _ := strconv.ParseBool(envvar.Get("CLICOLOR", "1")) + return termStyles(b) +} + +// reset returns the escape code to change the output to default settings. +func (style termStyles) reset() string { + if !style { + return "" + } + return "\x1b[0m" +} + +// target returns the escape code for formatting a target section heading. +func (style termStyles) target() string { + if !style { + return "" + } + // Bold + return "\x1b[1m" +} + +// command returns the escape code for formatting a command section heading. +func (style termStyles) command() string { + return style.target() +} + +// buildResult returns the escape code for formatting the final result message. +func (style termStyles) buildResult(success bool) string { + switch { + case !bool(style): + return "" + case !success: + return style.failure() + default: + // Bold + return "\x1b[1m" + } +} + +// failure returns the escape code for formatting error messages. +func (style termStyles) failure() string { + if !style { + return "" + } + // Red, but not bold to avoid using "bright red". + return "\x1b[31m" +} + +// debug returns the escape code for formatting debugging information. +func (style termStyles) debug() string { + if !style { + return "" + } + // Gray + return "\x1b[90m" +} diff --git a/internal/biome/biome.go b/internal/biome/biome.go index e74585dd..2326006d 100644 --- a/internal/biome/biome.go +++ b/internal/biome/biome.go @@ -183,7 +183,7 @@ func (l Local) Run(ctx context.Context, invoke *Invocation) error { if len(invoke.Argv) == 0 { return fmt.Errorf("local run: argv empty") } - log.Infof(ctx, "Run: %s", strings.Join(invoke.Argv, " ")) + log.Debugf(ctx, "Run: %s", strings.Join(invoke.Argv, " ")) log.Debugf(ctx, "Environment:\n%v", invoke.Env) dir := invoke.Dir if !filepath.IsAbs(invoke.Dir) { @@ -200,6 +200,9 @@ func (l Local) Run(ctx context.Context, invoke *Invocation) error { "LOGNAME=" + os.Getenv("LOGNAME"), "USER=" + os.Getenv("USER"), } + if v, ok := os.LookupEnv("NO_COLOR"); ok { + c.Env = append(c.Env, "NO_COLOR="+v) + } c.Env = appendStandardEnv(c.Env, runtime.GOOS) c.Env = invoke.Env.appendTo(c.Env, os.Getenv("PATH"), filepath.ListSeparator) c.Dir = dir diff --git a/internal/biome/docker.go b/internal/biome/docker.go index 9b432ff3..22b1245f 100644 --- a/internal/biome/docker.go +++ b/internal/biome/docker.go @@ -255,7 +255,7 @@ func (c *Container) Run(ctx context.Context, invoke *Invocation) error { return fmt.Errorf("run in container %s: argv empty", c.id) } - log.Infof(ctx, "Run (Docker): %s", strings.Join(invoke.Argv, " ")) + log.Debugf(ctx, "Run (Docker): %s", strings.Join(invoke.Argv, " ")) log.Debugf(ctx, "Running in container %s", c.id) log.Debugf(ctx, "Environment:\n%v", invoke.Env) @@ -272,6 +272,9 @@ func (c *Container) Run(ctx context.Context, invoke *Invocation) error { // TODO(light): Set LOGNAME and USER. "HOME=" + c.dirs.Home, } + if v, ok := os.LookupEnv("NO_COLOR"); ok { + opts.Env = append(opts.Env, "NO_COLOR="+v) + } opts.Env = appendStandardEnv(opts.Env, c.Describe().OS) opts.Env = invoke.Env.appendTo(opts.Env, c.path, ':') if slashpath.IsAbs(invoke.Dir) { diff --git a/internal/build/build.go b/internal/build/build.go index 312f5251..7faea5f2 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -38,7 +38,9 @@ type Sys = buildpack.Sys // Execute runs the given phase. It assumes that the phase's dependencies are // already available in the biome. -func Execute(ctx context.Context, sys Sys, target *yb.Target) (err error) { +// +// announce is called before every command run if not nil. +func Execute(ctx context.Context, sys Sys, announce func(string), target *yb.Target) (err error) { ctx, span := ybtrace.Start(ctx, "Build "+target.Name, trace.WithAttributes( label.String("target", target.Name), )) @@ -63,6 +65,9 @@ func Execute(ctx context.Context, sys Sys, target *yb.Target) (err error) { } } for _, cmdString := range target.Commands { + if announce != nil { + announce(cmdString) + } newWorkDir, err := runCommand(ctx, sys, workDir, cmdString) if err != nil { return fmt.Errorf("build %s: %w", target.Name, err) diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 12e033c5..d033dca2 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -164,7 +164,7 @@ func TestExecute(t *testing.T) { return nil }, } - err := Execute(ctx, Sys{Biome: bio}, test.target) + err := Execute(ctx, Sys{Biome: bio}, nil, test.target) if err != nil { if test.wantError { t.Logf("Build: %v (expected)", err)