diff --git a/bake/hclparser/hclparser.go b/bake/hclparser/hclparser.go index 6933ad71..fc820e4f 100644 --- a/bake/hclparser/hclparser.go +++ b/bake/hclparser/hclparser.go @@ -579,9 +579,9 @@ func (p *parser) validateVariables(vars map[string]*variable, ectx *hcl.EvalCont } type Variable struct { - Name string - Description string - Value *string + Name string `json:"name"` + Description string `json:"description,omitempty"` + Value *string `json:"value,omitempty"` } type ParseMeta struct { diff --git a/commands/bake.go b/commands/bake.go index 12befc84..dcb5de5e 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -25,7 +25,6 @@ import ( "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/localstate" "github.com/docker/buildx/util/buildflags" - "github.com/docker/buildx/util/cobrautil" "github.com/docker/buildx/util/cobrautil/completion" "github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/desktop" @@ -38,24 +37,30 @@ import ( "github.com/moby/buildkit/util/progress/progressui" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/tonistiigi/go-csvvalue" "go.opentelemetry.io/otel/attribute" ) type bakeOptions struct { - files []string - overrides []string - printOnly bool - listTargets bool - listVars bool - sbom string - provenance string - allow []string + files []string + overrides []string + + sbom string + provenance string + allow []string builder string metadataFile string exportPush bool exportLoad bool callFunc string + + print bool + list string + + // TODO: remove deprecated flags + listTargets bool + listVars bool } func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in bakeOptions, cFlags commonFlags) (err error) { @@ -121,9 +126,13 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba var nodes []builder.Node var progressConsoleDesc, progressTextDesc string + if in.print && in.list != "" { + return errors.New("--print and --list are mutually exclusive") + } + // instance only needed for reading remote bake files or building var driverType string - if url != "" || !(in.printOnly || in.listTargets || in.listVars) { + if url != "" || !(in.print || in.list != "") { b, err := builder.New(dockerCli, builder.WithName(in.builder), builder.WithContextPathHash(contextPathHash), @@ -184,7 +193,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba "BAKE_LOCAL_PLATFORM": platforms.Format(platforms.DefaultSpec()), } - if in.listTargets || in.listVars { + if in.list != "" { cfg, pm, err := bake.ParseFiles(files, defaults) if err != nil { return err @@ -192,10 +201,15 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba if err = printer.Wait(); err != nil { return err } - if in.listTargets { - return printTargetList(dockerCli.Out(), cfg) - } else if in.listVars { - return printVars(dockerCli.Out(), pm.AllVariables) + list, err := parseList(in.list) + if err != nil { + return err + } + switch list.Type { + case "targets": + return printTargetList(dockerCli.Out(), list.Format, cfg) + case "variables": + return printVars(dockerCli.Out(), list.Format, pm.AllVariables) } } @@ -231,7 +245,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba Target: tgts, } - if in.printOnly { + if in.print { if err = printer.Wait(); err != nil { return err } @@ -427,6 +441,13 @@ func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { if !cmd.Flags().Lookup("pull").Changed { cFlags.pull = nil } + if options.list == "" { + if options.listTargets { + options.list = "targets" + } else if options.listVars { + options.list = "variables" + } + } options.builder = rootOpts.builder options.metadataFile = cFlags.metadataFile // Other common flags (noCache, pull and progress) are processed in runBake function. @@ -439,7 +460,6 @@ func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { flags.StringArrayVarP(&options.files, "file", "f", []string{}, "Build definition file") flags.BoolVar(&options.exportLoad, "load", false, `Shorthand for "--set=*.output=type=docker"`) - flags.BoolVar(&options.printOnly, "print", false, "Print the options without building") flags.BoolVar(&options.exportPush, "push", false, `Shorthand for "--set=*.output=type=registry"`) flags.StringVar(&options.sbom, "sbom", "", `Shorthand for "--set=*.attest=type=sbom"`) flags.StringVar(&options.provenance, "provenance", "", `Shorthand for "--set=*.attest=type=provenance"`) @@ -450,13 +470,16 @@ func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { flags.VarPF(callAlias(&options.callFunc, "check"), "check", "", `Shorthand for "--call=check"`) flags.Lookup("check").NoOptDefVal = "true" - flags.BoolVar(&options.listTargets, "list-targets", false, "List available targets") - cobrautil.MarkFlagsExperimental(flags, "list-targets") - flags.MarkHidden("list-targets") + flags.BoolVar(&options.print, "print", false, "Print the options without building") + flags.StringVar(&options.list, "list", "", "List targets or variables") + // TODO: remove deprecated flags + flags.BoolVar(&options.listTargets, "list-targets", false, "List available targets") + flags.MarkHidden("list-targets") + flags.MarkDeprecated("list-targets", "list-targets is deprecated, use list=targets instead") flags.BoolVar(&options.listVars, "list-variables", false, "List defined variables") - cobrautil.MarkFlagsExperimental(flags, "list-variables") flags.MarkHidden("list-variables") + flags.MarkDeprecated("list-variables", "list-variables is deprecated, use list=variables instead") commonBuildFlags(&cFlags, flags) @@ -557,10 +580,70 @@ func readBakeFiles(ctx context.Context, nodes []builder.Node, url string, names return } -func printVars(w io.Writer, vars []*hclparser.Variable) error { +type listEntry struct { + Type string + Format string +} + +func parseList(input string) (listEntry, error) { + res := listEntry{} + + fields, err := csvvalue.Fields(input, nil) + if err != nil { + return res, err + } + + if len(fields) == 1 && fields[0] == input && !strings.HasPrefix(input, "type=") { + res.Type = input + } + + if res.Type == "" { + for _, field := range fields { + key, value, ok := strings.Cut(field, "=") + if !ok { + return res, errors.Errorf("invalid value %s", field) + } + key = strings.TrimSpace(strings.ToLower(key)) + switch key { + case "type": + res.Type = value + case "format": + res.Format = value + default: + return res, errors.Errorf("unexpected key '%s' in '%s'", key, field) + } + } + } + if res.Format == "" { + res.Format = "table" + } + + switch res.Type { + case "targets", "variables": + default: + return res, errors.Errorf("invalid list type %q", res.Type) + } + + switch res.Format { + case "table", "json": + default: + return res, errors.Errorf("invalid list format %q", res.Format) + } + + return res, nil +} + +func printVars(w io.Writer, format string, vars []*hclparser.Variable) error { slices.SortFunc(vars, func(a, b *hclparser.Variable) int { return cmp.Compare(a.Name, b.Name) }) + + if format == "json" { + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(vars) + } + tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) defer tw.Flush() @@ -578,12 +661,7 @@ func printVars(w io.Writer, vars []*hclparser.Variable) error { return nil } -func printTargetList(w io.Writer, cfg *bake.Config) error { - tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) - defer tw.Flush() - - tw.Write([]byte("TARGET\tDESCRIPTION\n")) - +func printTargetList(w io.Writer, format string, cfg *bake.Config) error { type targetOrGroup struct { name string target *bake.Target @@ -602,6 +680,20 @@ func printTargetList(w io.Writer, cfg *bake.Config) error { return cmp.Compare(a.name, b.name) }) + var tw *tabwriter.Writer + if format == "table" { + tw = tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) + defer tw.Flush() + tw.Write([]byte("TARGET\tDESCRIPTION\n")) + } + + type targetList struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Group bool `json:"group,omitempty"` + } + var targetsList []targetList + for _, tgt := range list { if strings.HasPrefix(tgt.name, "_") { // convention for a private target @@ -610,9 +702,9 @@ func printTargetList(w io.Writer, cfg *bake.Config) error { var descr string if tgt.target != nil { descr = tgt.target.Description + targetsList = append(targetsList, targetList{Name: tgt.name, Description: descr}) } else if tgt.group != nil { descr = tgt.group.Description - if len(tgt.group.Targets) > 0 { slices.Sort(tgt.group.Targets) names := strings.Join(tgt.group.Targets, ", ") @@ -622,8 +714,17 @@ func printTargetList(w io.Writer, cfg *bake.Config) error { descr = names } } + targetsList = append(targetsList, targetList{Name: tgt.name, Description: descr, Group: true}) } - fmt.Fprintf(tw, "%s\t%s\n", tgt.name, descr) + if format == "table" { + fmt.Fprintf(tw, "%s\t%s\n", tgt.name, descr) + } + } + + if format == "json" { + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(targetsList) } return nil diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index 3e02b859..e4a81460 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -21,6 +21,7 @@ Build from a file | [`--check`](#check) | `bool` | | Shorthand for `--call=check` | | `-D`, `--debug` | `bool` | | Enable debug logging | | [`-f`](#file), [`--file`](#file) | `stringArray` | | Build definition file | +| `--list` | `string` | | List targets or variables | | `--load` | `bool` | | Shorthand for `--set=*.output=type=docker` | | [`--metadata-file`](#metadata-file) | `string` | | Write build result metadata to a file | | [`--no-cache`](#no-cache) | `bool` | | Do not use cache when building the image | diff --git a/tests/bake.go b/tests/bake.go index a4b0b3a5..f106f21b 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -1599,7 +1599,7 @@ target "abc" { out, err := bakeCmd( sb, withDir(dir), - withArgs("--list-targets"), + withArgs("--list=targets"), ) require.NoError(t, err, out) @@ -1628,7 +1628,7 @@ target "default" { out, err := bakeCmd( sb, withDir(dir), - withArgs("--list-variables"), + withArgs("--list=variables"), ) require.NoError(t, err, out)