diff --git a/bake/bake.go b/bake/bake.go index 54c19a159..db0085208 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -702,7 +702,8 @@ type Target struct { NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"` ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"` Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"` - // IMPORTANT: if you add more fields here, do not forget to update newOverrides and docs/bake-reference.md. + Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"` + // IMPORTANT: if you add more fields here, do not forget to update newOverrides/AddOverrides and docs/bake-reference.md. // linked is a private field to mark a target used as a linked one linked bool @@ -776,6 +777,9 @@ func (t *Target) Merge(t2 *Target) { if t2.Target != nil { t.Target = t2.Target } + if t2.Call != nil { + t.Call = t2.Call + } if t2.Annotations != nil { // merge t.Annotations = append(t.Annotations, t2.Annotations...) } @@ -863,6 +867,8 @@ func (t *Target) AddOverrides(overrides map[string]Override) error { t.CacheTo = o.ArrValue case "target": t.Target = &value + case "call": + t.Call = &value case "secrets": t.Secrets = o.ArrValue case "ssh": @@ -1298,6 +1304,12 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { bo.Target = *t.Target } + if t.Call != nil { + bo.PrintFunc = &build.PrintFunc{ + Name: *t.Call, + } + } + cacheImports, err := buildflags.ParseCacheEntry(t.CacheFrom) if err != nil { return nil, err diff --git a/commands/bake.go b/commands/bake.go index 2e0164f64..ace78d96d 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -1,11 +1,13 @@ package commands import ( + "bytes" "context" "encoding/json" "fmt" "io" "os" + "slices" "strings" "github.com/containerd/console" @@ -13,6 +15,7 @@ import ( "github.com/docker/buildx/bake" "github.com/docker/buildx/build" "github.com/docker/buildx/builder" + "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/localstate" "github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/cobrautil/completion" @@ -40,6 +43,7 @@ type bakeOptions struct { metadataFile string exportPush bool exportLoad bool + callFunc string } func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in bakeOptions, cFlags commonFlags) (err error) { @@ -71,6 +75,11 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba targets = []string{"default"} } + callFunc, err := buildflags.ParsePrintFunc(in.callFunc) + if err != nil { + return err + } + overrides := in.overrides if in.exportPush { overrides = append(overrides, "*.push=true") @@ -78,6 +87,9 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba if in.exportLoad { overrides = append(overrides, "*.load=true") } + if callFunc != nil { + overrides = append(overrides, fmt.Sprintf("*.call=%s", callFunc.Name)) + } if cFlags.noCache != nil { overrides = append(overrides, fmt.Sprintf("*.no-cache=%t", *cFlags.noCache)) } @@ -146,19 +158,19 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba if err != nil { return } - if progressMode != progressui.QuietMode && progressMode != progressui.RawJSONMode { - desktop.PrintBuildDetails(os.Stderr, printer.BuildRefs(), term) + } + if progressMode != progressui.QuietMode && progressMode != progressui.RawJSONMode { + desktop.PrintBuildDetails(os.Stderr, printer.BuildRefs(), term) + } + if resp != nil && len(in.metadataFile) > 0 { + dt := make(map[string]interface{}) + for t, r := range resp { + dt[t] = decodeExporterResponse(r.ExporterResponse) } - if resp != nil && len(in.metadataFile) > 0 { - dt := make(map[string]interface{}) - for t, r := range resp { - dt[t] = decodeExporterResponse(r.ExporterResponse) - } - if warnings := printer.Warnings(); len(warnings) > 0 && confutil.MetadataWarningsEnabled() { - dt["buildx.build.warnings"] = warnings - } - err = writeMetadataFile(in.metadataFile, dt) + if warnings := printer.Warnings(); len(warnings) > 0 && confutil.MetadataWarningsEnabled() { + dt["buildx.build.warnings"] = warnings } + err = writeMetadataFile(in.metadataFile, dt) } }() @@ -222,6 +234,16 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba return nil } + for _, opt := range bo { + if opt.PrintFunc != nil { + cf, err := buildflags.ParsePrintFunc(opt.PrintFunc.Name) + if err != nil { + return err + } + opt.PrintFunc.Name = cf.Name + } + } + prm := confutil.MetadataProvenance() if len(in.metadataFile) == 0 { prm = confutil.MetadataProvenanceModeDisabled @@ -254,7 +276,113 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba return wrapBuildError(err, true) } - return + err = printer.Wait() + printer = nil + if err != nil { + return err + } + + var callFormatJSON bool + var jsonResults = map[string]map[string]any{} + if callFunc != nil { + callFormatJSON = callFunc.Format == "json" + } + var sep bool + var exitCode int + + names := make([]string, 0, len(bo)) + for name := range bo { + names = append(names, name) + } + slices.Sort(names) + + for _, name := range names { + req := bo[name] + if req.PrintFunc == nil { + continue + } + + pf := &pb.PrintFunc{ + Name: req.PrintFunc.Name, + Format: req.PrintFunc.Format, + IgnoreStatus: req.PrintFunc.IgnoreStatus, + } + + if callFunc != nil { + pf.Format = callFunc.Format + pf.IgnoreStatus = callFunc.IgnoreStatus + } + + var res map[string]string + if sp, ok := resp[name]; ok { + res = sp.ExporterResponse + } + + if callFormatJSON { + jsonResults[name] = map[string]any{} + buf := &bytes.Buffer{} + if code, err := printResult(buf, pf, res); err != nil { + jsonResults[name]["error"] = err.Error() + exitCode = 1 + } else if code != 0 && exitCode == 0 { + exitCode = code + } + m := map[string]*json.RawMessage{} + if err := json.Unmarshal(buf.Bytes(), &m); err == nil { + for k, v := range m { + jsonResults[name][k] = v + } + } else { + jsonResults[name][pf.Name] = json.RawMessage(buf.Bytes()) + } + } else { + if sep { + fmt.Fprintf(dockerCli.Out(), "\n\n") + } else { + sep = true + } + fmt.Fprintf(dockerCli.Out(), "%s\n", name) + if code, err := printResult(dockerCli.Out(), pf, res); err != nil { + fmt.Fprintf(dockerCli.Out(), "error: %v\n", err) + exitCode = 1 + } else if code != 0 && exitCode == 0 { + exitCode = code + } + } + } + if callFormatJSON { + out := struct { + Group map[string]*bake.Group `json:"group,omitempty"` + Target map[string]map[string]any `json:"target"` + }{ + Group: grps, + Target: map[string]map[string]any{}, + } + + for name, def := range tgts { + out.Target[name] = map[string]any{ + "build": def, + } + if res, ok := jsonResults[name]; ok { + printName := bo[name].PrintFunc.Name + if printName == "lint" { + printName = "check" + } + out.Target[name][printName] = res + } + } + dt, err := json.MarshalIndent(out, "", " ") + if err != nil { + return err + } + fmt.Fprintln(dockerCli.Out(), string(dt)) + } + + if exitCode != 0 { + os.Exit(exitCode) + } + + return nil } func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { @@ -290,6 +418,9 @@ func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { flags.StringVar(&options.sbom, "sbom", "", `Shorthand for "--set=*.attest=type=sbom"`) flags.StringVar(&options.provenance, "provenance", "", `Shorthand for "--set=*.attest=type=provenance"`) flags.StringArrayVar(&options.overrides, "set", nil, `Override target value (e.g., "targetpattern.key=value")`) + flags.StringVar(&options.callFunc, "call", "build", `Set method for evaluating build ("check", "outline", "targets")`) + flags.VarPF(callAlias(&options.callFunc, "check"), "check", "", `Shorthand for "--call=check"`) + flags.Lookup("check").NoOptDefVal = "true" commonBuildFlags(&cFlags, flags) diff --git a/commands/build.go b/commands/build.go index 1bf77dc81..9e5d3f005 100644 --- a/commands/build.go +++ b/commands/build.go @@ -9,7 +9,6 @@ import ( "encoding/json" "fmt" "io" - "log" "os" "path/filepath" "strconv" @@ -369,8 +368,10 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) } } if opts.PrintFunc != nil { - if err := printResult(opts.PrintFunc, resp.ExporterResponse); err != nil { + if exitcode, err := printResult(dockerCli.Out(), opts.PrintFunc, resp.ExporterResponse); err != nil { return err + } else if exitcode != 0 { + os.Exit(exitcode) } } else if options.metadataFile != "" { dt := decodeExporterResponse(resp.ExporterResponse) @@ -634,7 +635,7 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugConfig *debug.D } flags.StringVar(&options.printFunc, "call", "build", `Set method for evaluating build ("check", "outline", "targets")`) - flags.VarPF(callAlias(options, "check"), "check", "", `Shorthand for "--call=check"`) + flags.VarPF(callAlias(&options.printFunc, "check"), "check", "", `Shorthand for "--call=check"`) flags.Lookup("check").NoOptDefVal = "true" // hidden flags @@ -862,24 +863,24 @@ func printWarnings(w io.Writer, warnings []client.VertexWarning, mode progressui } } -func printResult(f *controllerapi.PrintFunc, res map[string]string) error { +func printResult(w io.Writer, f *controllerapi.PrintFunc, res map[string]string) (int, error) { switch f.Name { case "outline": - return printValue(outline.PrintOutline, outline.SubrequestsOutlineDefinition.Version, f.Format, res) + return 0, printValue(w, outline.PrintOutline, outline.SubrequestsOutlineDefinition.Version, f.Format, res) case "targets": - return printValue(targets.PrintTargets, targets.SubrequestsTargetsDefinition.Version, f.Format, res) + return 0, printValue(w, targets.PrintTargets, targets.SubrequestsTargetsDefinition.Version, f.Format, res) case "subrequests.describe": - return printValue(subrequests.PrintDescribe, subrequests.SubrequestsDescribeDefinition.Version, f.Format, res) + return 0, printValue(w, subrequests.PrintDescribe, subrequests.SubrequestsDescribeDefinition.Version, f.Format, res) case "lint": - err := printValue(lint.PrintLintViolations, lint.SubrequestLintDefinition.Version, f.Format, res) + err := printValue(w, lint.PrintLintViolations, lint.SubrequestLintDefinition.Version, f.Format, res) if err != nil { - return err + return 0, err } lintResults := lint.LintResults{} if result, ok := res["result.json"]; ok { if err := json.Unmarshal([]byte(result), &lintResults); err != nil { - return err + return 0, err } } if lintResults.Error != nil { @@ -889,52 +890,51 @@ func printResult(f *controllerapi.PrintFunc, res map[string]string) error { // but here we want to print the error in a way that's consistent with how // the lint warnings are printed via the `lint.PrintLintViolations` function, // which differs from the default error printing. - fmt.Println() - lintBuf := bytes.NewBuffer([]byte(lintResults.Error.Message)) if f.Format != "json" { - fmt.Fprintln(lintBuf) + fmt.Fprintln(w) } + lintBuf := bytes.NewBuffer([]byte(lintResults.Error.Message + "\n")) sourceInfo := lintResults.Sources[lintResults.Error.Location.SourceIndex] source := errdefs.Source{ Info: sourceInfo, Ranges: lintResults.Error.Location.Ranges, } source.Print(lintBuf) - return errors.New(lintBuf.String()) + return 0, errors.New(lintBuf.String()) } else if len(lintResults.Warnings) == 0 && f.Format != "json" { - fmt.Println("Check complete, no warnings found.") + fmt.Fprintln(w, "Check complete, no warnings found.") } default: if dt, ok := res["result.json"]; ok && f.Format == "json" { - fmt.Println(dt) + fmt.Fprintln(w, dt) } else if dt, ok := res["result.txt"]; ok { - fmt.Print(dt) + fmt.Fprint(w, dt) } else { - log.Printf("%s %+v", f, res) + fmt.Fprintf(w, "%s %+v\n", f, res) } } if v, ok := res["result.statuscode"]; !f.IgnoreStatus && ok { if n, err := strconv.Atoi(v); err == nil && n != 0 { - os.Exit(n) + return n, nil } } - return nil + return 0, nil } type printFunc func([]byte, io.Writer) error -func printValue(printer printFunc, version string, format string, res map[string]string) error { +func printValue(w io.Writer, printer printFunc, version string, format string, res map[string]string) error { if format == "json" { - fmt.Fprintln(os.Stdout, res["result.json"]) + fmt.Fprintln(w, res["result.json"]) return nil } if res["version"] != "" && versions.LessThan(version, res["version"]) && res["result.txt"] != "" { // structure is too new and we don't know how to print it - fmt.Fprint(os.Stdout, res["result.txt"]) + fmt.Fprint(w, res["result.txt"]) return nil } - return printer([]byte(res["result.json"]), os.Stdout) + return printer([]byte(res["result.json"]), w) } type invokeConfig struct { @@ -1042,7 +1042,7 @@ func maybeJSONArray(v string) []string { return []string{v} } -func callAlias(options *buildOptions, value string) cobrautil.BoolFuncValue { +func callAlias(target *string, value string) cobrautil.BoolFuncValue { return func(s string) error { v, err := strconv.ParseBool(s) if err != nil { @@ -1050,7 +1050,7 @@ func callAlias(options *buildOptions, value string) cobrautil.BoolFuncValue { } if v { - options.printFunc = value + *target = value } return nil }