From b526eaf3b7a6d2cb0d662fe82e20d631123b1a2d Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Sat, 5 Sep 2026 06:03:01 +0500 Subject: [PATCH] fix(build): don't fail bake progress when stdout is not a console #14090 hands the real stdout file to buildkit so Windows TTY progress works. that also passes a redirected stdout (`build >/dev/null`) into ConsoleFromFile, which errors with "provided file is not a console". only unwrap a terminal file, and fall back to plain progress otherwise. Fixes #14182 Signed-off-by: Dean Chen <862469039@qq.com> --- pkg/compose/build_bake.go | 19 +++++++++++--- pkg/compose/build_bake_test.go | 46 +++++++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index 255a64d8e8..6be9ed2b8f 100644 --- a/pkg/compose/build_bake.go +++ b/pkg/compose/build_bake.go @@ -140,7 +140,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project if out == nil { out = s.stdout() } - display, err := progressui.NewDisplay(makeConsole(out), displayMode) + display, err := newBakeDisplay(out, displayMode) if err != nil { return nil, err } @@ -509,14 +509,27 @@ func (s *composeService) getBuildxPlugin() (*manager.Plugin, error) { // matters, and falls back to plain elsewhere. func makeConsole(out io.Writer) io.Writer { if s, ok := out.(*streams.Out); ok { - if f, ok := s.File(); ok { + if f, ok := s.File(); ok && s.IsTerminal() { return f } - return &_console{s} + if s.IsTerminal() { + return &_console{s} + } + return s } return out } +// newBakeDisplay falls back to plain when TTY mode is requested but out is not a console +// (e.g. `docker compose build >/dev/null`, #14182). +func newBakeDisplay(out io.Writer, mode progressui.DisplayMode) (progressui.Display, error) { + d, err := progressui.NewDisplay(makeConsole(out), mode) + if err != nil && mode != progressui.PlainMode && mode != progressui.QuietMode { + d, err = progressui.NewDisplay(out, progressui.PlainMode) + } + return d, err +} + var _ console.File = &_console{} type _console struct { diff --git a/pkg/compose/build_bake_test.go b/pkg/compose/build_bake_test.go index ee814c2362..79f660ed58 100644 --- a/pkg/compose/build_bake_test.go +++ b/pkg/compose/build_bake_test.go @@ -24,6 +24,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/streams" + "github.com/moby/buildkit/util/progress/progressui" "gotest.tools/v3/assert" ) @@ -105,15 +106,37 @@ func TestToBakeAttest(t *testing.T) { // os.Stdin/Stdout/Stderr values, so a wrapper would disable the TTY progress // rendering entirely (#14086). func TestMakeConsole(t *testing.T) { - t.Run("stream wrapping a real file yields the file itself", func(t *testing.T) { - out := makeConsole(streams.NewOut(os.Stdout)) - assert.Equal(t, out, os.Stdout) + t.Run("terminal stdout yields the real file", func(t *testing.T) { + s := streams.NewOut(os.Stdout) + out := makeConsole(s) + if s.IsTerminal() { + assert.Equal(t, out, os.Stdout) + return + } + assert.Check(t, out != os.Stdout, "non-console stdout must not be passed to ConsoleFromFile") + }) + + t.Run("redirected file is not treated as a console", func(t *testing.T) { + r, w, err := os.Pipe() + assert.NilError(t, err) + t.Cleanup(func() { + _ = r.Close() + _ = w.Close() + }) + s := streams.NewOut(w) + out := makeConsole(s) + assert.Check(t, out != w, "pipe fd must not be handed to ConsoleFromFile") }) t.Run("file-less stream keeps the console.File wrapper", func(t *testing.T) { - out := makeConsole(streams.NewOut(&bytes.Buffer{})) - _, ok := out.(*_console) - assert.Check(t, ok, "expected a *_console, got %T", out) + s := streams.NewOut(&bytes.Buffer{}) + out := makeConsole(s) + if s.IsTerminal() { + _, ok := out.(*_console) + assert.Check(t, ok, "expected a *_console, got %T", out) + return + } + assert.Equal(t, out, io.Writer(s)) }) t.Run("plain writer is left untouched", func(t *testing.T) { @@ -121,3 +144,14 @@ func TestMakeConsole(t *testing.T) { assert.Equal(t, makeConsole(buf), io.Writer(buf)) }) } + +func TestNewBakeDisplayFallsBackWhenNotConsole(t *testing.T) { + r, w, err := os.Pipe() + assert.NilError(t, err) + t.Cleanup(func() { + _ = r.Close() + _ = w.Close() + }) + _, err = newBakeDisplay(streams.NewOut(w), progressui.TtyMode) + assert.NilError(t, err) +}