diff --git a/build/build.go b/build/build.go index 11d1c212..3b855ef1 100644 --- a/build/build.go +++ b/build/build.go @@ -26,6 +26,7 @@ import ( "github.com/docker/buildx/builder" "github.com/docker/buildx/driver" "github.com/docker/buildx/localstate" + "github.com/docker/buildx/util/desktop" "github.com/docker/buildx/util/dockerutil" "github.com/docker/buildx/util/imagetools" "github.com/docker/buildx/util/progress" @@ -822,6 +823,7 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s for i, dp := range dps { i, dp, so := i, dp, *dp.so + node := nodes[dp.driverIndex] if multiDriver { for i, e := range so.Exports { switch e.Type { @@ -940,6 +942,16 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opt map[s } else { rr, err = c.Build(ctx, so, "buildx", buildFunc, ch) } + if node.Driver.Features(ctx)[driver.HistoryAPI] && desktop.BuildBackendEnabled() { + buildRef := fmt.Sprintf("%s/%s/%s", node.Builder, node.Name, so.Ref) + if err != nil { + return &desktop.ErrorWithBuildRef{ + Ref: buildRef, + Err: err, + } + } + progress.WriteBuildRef(w, k, buildRef) + } if err != nil { return err } diff --git a/cmd/buildx/main.go b/cmd/buildx/main.go index 5bca7906..e300ae9c 100644 --- a/cmd/buildx/main.go +++ b/cmd/buildx/main.go @@ -5,6 +5,7 @@ import ( "os" "github.com/docker/buildx/commands" + "github.com/docker/buildx/util/desktop" "github.com/docker/buildx/version" "github.com/docker/cli/cli" "github.com/docker/cli/cli-plugins/manager" @@ -86,6 +87,9 @@ func main() { } else { fmt.Fprintf(cmd.Err(), "ERROR: %v\n", err) } + if ebr, ok := err.(*desktop.ErrorWithBuildRef); ok { + ebr.Print(cmd.Err()) + } os.Exit(1) } diff --git a/commands/bake.go b/commands/bake.go index 398c0fd9..8edd45e6 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -6,6 +6,7 @@ import ( "fmt" "os" + "github.com/containerd/console" "github.com/containerd/containerd/platforms" "github.com/docker/buildx/bake" "github.com/docker/buildx/build" @@ -13,6 +14,7 @@ import ( "github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/cobrautil/completion" "github.com/docker/buildx/util/confutil" + "github.com/docker/buildx/util/desktop" "github.com/docker/buildx/util/dockerutil" "github.com/docker/buildx/util/progress" "github.com/docker/buildx/util/tracing" @@ -117,6 +119,11 @@ func runBake(dockerCli command.Cli, targets []string, in bakeOptions, cFlags com progressTextDesc = fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver) } + var term bool + if _, err := console.ConsoleFromFile(os.Stderr); err == nil { + term = true + } + printer, err := progress.NewPrinter(ctx2, os.Stderr, os.Stderr, cFlags.progress, progress.WithDesc(progressTextDesc, progressConsoleDesc), ) @@ -130,6 +137,9 @@ func runBake(dockerCli command.Cli, targets []string, in bakeOptions, cFlags com if err == nil { err = err1 } + if err == nil && cFlags.progress != progress.PrinterModeQuiet { + desktop.PrintBuildDetails(os.Stderr, printer.BuildRefs(), term) + } } }() diff --git a/commands/build.go b/commands/build.go index 549bafd1..ed83b5f4 100644 --- a/commands/build.go +++ b/commands/build.go @@ -26,6 +26,7 @@ import ( "github.com/docker/buildx/store" "github.com/docker/buildx/store/storeutil" "github.com/docker/buildx/util/buildflags" + "github.com/docker/buildx/util/desktop" "github.com/docker/buildx/util/ioset" "github.com/docker/buildx/util/progress" "github.com/docker/buildx/util/tracing" @@ -238,6 +239,11 @@ func runBuild(dockerCli command.Cli, options buildOptions) (err error) { return err } + var term bool + if _, err := console.ConsoleFromFile(os.Stderr); err == nil { + term = true + } + ctx2, cancel := context.WithCancel(context.TODO()) defer cancel() progressMode, err := options.toProgress() @@ -273,7 +279,9 @@ func runBuild(dockerCli command.Cli, options buildOptions) (err error) { return retErr } - if progressMode == progress.PrinterModeQuiet { + if progressMode != progress.PrinterModeQuiet { + desktop.PrintBuildDetails(os.Stderr, printer.BuildRefs(), term) + } else { fmt.Println(getImageID(resp.ExporterResponse)) } if options.imageIDFile != "" { diff --git a/controller/pb/progress.go b/controller/pb/progress.go index 0b81aaa6..f2041236 100644 --- a/controller/pb/progress.go +++ b/controller/pb/progress.go @@ -19,6 +19,10 @@ func (w *writer) Write(status *client.SolveStatus) { w.ch <- ToControlStatus(status) } +func (w *writer) WriteBuildRef(target string, ref string) { + return +} + func (w *writer) ValidateLogSource(digest.Digest, interface{}) bool { return true } diff --git a/util/desktop/desktop.go b/util/desktop/desktop.go new file mode 100644 index 00000000..6ddf74a5 --- /dev/null +++ b/util/desktop/desktop.go @@ -0,0 +1,86 @@ +package desktop + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "sync" + + "github.com/containerd/console" +) + +var ( + bbEnabledOnce sync.Once + bbEnabled bool +) + +func BuildBackendEnabled() bool { + bbEnabledOnce.Do(func() { + home, err := os.UserHomeDir() + if err != nil { + return + } + _, err = os.Stat(filepath.Join(home, ".docker", "desktop-build", ".lastaccess")) + bbEnabled = err == nil + }) + return bbEnabled +} + +func BuildDetailsOutput(refs map[string]string, term bool) string { + if len(refs) == 0 { + return "" + } + refURL := func(ref string) string { + return fmt.Sprintf("docker-desktop://dashboard/build/%s", ref) + } + var out bytes.Buffer + out.WriteString("View build details: ") + multiTargets := len(refs) > 1 + for target, ref := range refs { + if multiTargets { + out.WriteString(fmt.Sprintf("\n %s: ", target)) + } + if term { + out.WriteString(hyperlink(refURL(ref))) + } else { + out.WriteString(refURL(ref)) + } + } + return out.String() +} + +func PrintBuildDetails(w io.Writer, refs map[string]string, term bool) { + if out := BuildDetailsOutput(refs, term); out != "" { + fmt.Fprintf(w, "\n%s\n", out) + } +} + +func hyperlink(url string) string { + // create an escape sequence using the OSC 8 format: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda + return fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", url, url) +} + +type ErrorWithBuildRef struct { + Ref string + Err error + Msg string +} + +func (e *ErrorWithBuildRef) Error() string { + return e.Err.Error() +} + +func (e *ErrorWithBuildRef) Unwrap() error { + return e.Err +} + +func (e *ErrorWithBuildRef) Print(w io.Writer) error { + var term bool + if _, err := console.ConsoleFromFile(os.Stderr); err == nil { + term = true + } + fmt.Fprintf(w, "\n%s", BuildDetailsOutput(map[string]string{"default": e.Ref}, term)) + return nil +} diff --git a/util/progress/printer.go b/util/progress/printer.go index bb962f53..fdc74d91 100644 --- a/util/progress/printer.go +++ b/util/progress/printer.go @@ -33,6 +33,11 @@ type Printer struct { warnings []client.VertexWarning logMu sync.Mutex logSourceMap map[digest.Digest]interface{} + + // TODO: remove once we can use result context to pass build ref + // see https://github.com/docker/buildx/pull/1861 + buildRefsMu sync.Mutex + buildRefs map[string]string } func (p *Printer) Wait() error { @@ -143,6 +148,19 @@ func NewPrinter(ctx context.Context, w io.Writer, out console.File, mode string, return pw, nil } +func (p *Printer) WriteBuildRef(target string, ref string) { + p.buildRefsMu.Lock() + defer p.buildRefsMu.Unlock() + if p.buildRefs == nil { + p.buildRefs = map[string]string{} + } + p.buildRefs[target] = ref +} + +func (p *Printer) BuildRefs() map[string]string { + return p.buildRefs +} + type printerOpts struct { displayOpts []progressui.DisplaySolveStatusOpt diff --git a/util/progress/writer.go b/util/progress/writer.go index 7cc63f6a..8e566f11 100644 --- a/util/progress/writer.go +++ b/util/progress/writer.go @@ -10,6 +10,7 @@ import ( type Writer interface { Write(*client.SolveStatus) + WriteBuildRef(string, string) ValidateLogSource(digest.Digest, interface{}) bool ClearLogSource(interface{}) } @@ -41,6 +42,10 @@ func Write(w Writer, name string, f func() error) { }) } +func WriteBuildRef(w Writer, target string, ref string) { + w.WriteBuildRef(target, ref) +} + func NewChannel(w Writer) (chan *client.SolveStatus, chan struct{}) { ch := make(chan *client.SolveStatus) done := make(chan struct{})