-
Notifications
You must be signed in to change notification settings - Fork 465
Adding support for .bzl file operations #1464
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,26 @@ macros. | |
| input instead of from a local file in the package directory: `-:all_tests`. | ||
| (It is presumably not useful to both use a `-` package name and use the `-f | ||
| -` flag to read commands from the standard input.) | ||
| * In `.bzl` files, use a label to refer to a top-level global variable: | ||
| `//pkg:my_patches` (see [Global variables in .bzl files](#global-variables-in-bzl-files)). | ||
|
|
||
| ### Global variables in .bzl files | ||
|
|
||
| Buildozer can edit top-level global variables in `.bzl` files. Target the | ||
| variable by name, for example `//pkg:my_patches`, and use `add`, `remove`, `set`, | ||
| `print`, or `delete`. Other commands return an error on variable targets. | ||
|
|
||
| Only simple assignments of the form `name = value` at module scope are | ||
| supported. Variables assigned via tuple unpacking (e.g. `(A, B) = ...`) or `for` | ||
| loop variables are not detected. Assignments inside `def` bodies are ignored. | ||
|
|
||
| Example: | ||
|
|
||
| ```bash | ||
| # In defs.bzl: my_patches = ["a.patch"] | ||
| buildozer 'add patches b.patch' //pkg:my_patches | ||
| buildozer 'remove patches a.patch' //pkg:my_patches | ||
| ``` | ||
|
|
||
| ### Options | ||
|
|
||
|
|
@@ -85,7 +105,7 @@ See `buildozer -help` for the full list. | |
| ### Edit commands | ||
|
|
||
| Buildozer supports the following commands(`'command args'`): | ||
|
|
||
| * `add <attr>[:<type>] <value(s)>`: Adds value(s) to a list attribute of a rule. | ||
| If a value is already present in the list, it is not added. `type` specifies | ||
| the type of values being added. See [supported types](#supported-types) for | ||
|
|
@@ -253,7 +273,7 @@ buildozer 'new cc_binary new_bin before tests' //:__pkg__ | |
| # Copy an attribute from `protolib` to `py_protolib`. | ||
| buildozer 'copy testonly protolib' //pkg:py_protolib | ||
|
|
||
| # Set two attributes in the same rule | ||
| # Set two attributes in the same rule | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please fix |
||
| buildozer 'set compile 1' 'set srcmap 1' //pkg:rule | ||
|
|
||
| # Make a default explicit in all soy_js rules in a package | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -115,6 +115,20 @@ func parseAttr(attrName string) (attr string, attrType AttrType, err error) { | |
| // The cmdXXX functions implement the various commands. | ||
|
|
||
| func cmdAdd(opts *Options, env CmdEnvironment) (*build.File, error) { | ||
| // Variable targets are represented by a dummy Rule with a nil Call. | ||
| if env.Rule != nil && env.Rule.Call == nil { | ||
| varName := env.Rule.ImplicitName | ||
| varAssign, ok := env.Vars[varName] | ||
| if !ok { | ||
| return nil, fmt.Errorf("variable %s is not defined", varName) | ||
| } | ||
| for _, val := range env.Args[1:] { | ||
| strVal := getLabelStringExpr(val, env.Pkg) | ||
| varAssign.RHS = AddValueToList(varAssign.RHS, env.Pkg, strVal, true) | ||
| } | ||
|
|
||
| return env.File, nil | ||
| } | ||
|
mydatascience marked this conversation as resolved.
|
||
| attr, attrType, err := parseAttr(env.Args[0]) | ||
| if err != nil { | ||
| return nil, err | ||
|
|
@@ -245,6 +259,23 @@ func cmdPrintComment(opts *Options, env CmdEnvironment) (*build.File, error) { | |
| } | ||
|
|
||
| func cmdDelete(opts *Options, env CmdEnvironment) (*build.File, error) { | ||
| // Variable targets are represented by a dummy Rule with a nil Call. | ||
| if env.Rule != nil && env.Rule.Call == nil { | ||
| varName := env.Rule.ImplicitName | ||
| varAssign, ok := env.Vars[varName] | ||
| if !ok { | ||
| return nil, fmt.Errorf("variable %s is not defined", varName) | ||
| } | ||
| var all []build.Expr | ||
| for _, stmt := range env.File.Stmt { | ||
| if stmt == varAssign { | ||
| continue | ||
| } | ||
| all = append(all, stmt) | ||
| } | ||
| env.File.Stmt = all | ||
| return env.File, nil | ||
| } | ||
| return DeleteRule(env.File, env.Rule), nil | ||
| } | ||
|
|
||
|
|
@@ -365,6 +396,47 @@ func cmdSubstituteLoad(opts *Options, env CmdEnvironment) (*build.File, error) { | |
| } | ||
|
|
||
| func cmdPrint(opts *Options, env CmdEnvironment) (*build.File, error) { | ||
| // Variable targets are represented by a dummy Rule with a nil Call. | ||
| if env.Rule != nil && env.Rule.Call == nil { | ||
| varName := env.Rule.ImplicitName | ||
| varAssign, ok := env.Vars[varName] | ||
| if !ok { | ||
| return nil, fmt.Errorf("variable %s is not defined", varName) | ||
| } | ||
| format := env.Args | ||
| if len(format) == 0 { | ||
| format = []string{"value"} | ||
| } | ||
| fields := make([]*apipb.Output_Record_Field, len(format)) | ||
| for i, str := range format { | ||
| switch str { | ||
| case "name": | ||
| fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{Text: varName}} | ||
| case "kind": | ||
| fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{Text: "var"}} | ||
| case "label": | ||
| label := labels.Label{Package: env.Pkg, Target: varName} | ||
| fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{Text: label.Format()}} | ||
| case "path": | ||
| fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{Text: env.File.Path}} | ||
| case "startline": | ||
| start, _ := varAssign.Span() | ||
| fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Number{Number: int32(start.Line)}} | ||
| case "endline": | ||
| _, end := varAssign.Span() | ||
| fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Number{Number: int32(end.Line)}} | ||
| case "rule": | ||
| fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{Text: build.FormatString(varAssign)}} | ||
| case "value": | ||
| fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{Text: build.FormatString(varAssign.RHS)}} | ||
| default: | ||
| fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Error{Error: apipb.Output_Record_Field_MISSING}} | ||
| } | ||
| } | ||
| env.output.Fields = fields | ||
| return nil, nil | ||
| } | ||
|
|
||
| format := env.Args | ||
| if len(format) == 0 { | ||
| format = []string{"name", "kind"} | ||
|
|
@@ -453,6 +525,18 @@ func attrKeysForPattern(rule *build.Rule, pattern string) []string { | |
| } | ||
|
|
||
| func cmdRemove(opts *Options, env CmdEnvironment) (*build.File, error) { | ||
| // Variable targets are represented by a dummy Rule with a nil Call. | ||
| if env.Rule != nil && env.Rule.Call == nil { | ||
| varName := env.Rule.ImplicitName | ||
| varAssign, ok := env.Vars[varName] | ||
| if !ok { | ||
| return nil, fmt.Errorf("variable %s is not defined", varName) | ||
| } | ||
| for _, val := range env.Args[1:] { | ||
| ListDelete(varAssign.RHS, val, env.Pkg) | ||
| } | ||
|
mydatascience marked this conversation as resolved.
|
||
| return env.File, nil | ||
| } | ||
| if len(env.Args) == 1 { // Remove the attribute | ||
| if env.Args[0] == "*" { | ||
| didDelete := false | ||
|
|
@@ -597,6 +681,16 @@ func cmdSet(opts *Options, env CmdEnvironment) (*build.File, error) { | |
| return nil, err | ||
| } | ||
| args := env.Args[1:] | ||
| // Variable targets are represented by a dummy Rule with a nil Call. | ||
| if env.Rule != nil && env.Rule.Call == nil { | ||
| varName := env.Rule.ImplicitName | ||
| varAssign, ok := env.Vars[varName] | ||
| if !ok { | ||
| return nil, fmt.Errorf("variable %s is not defined", varName) | ||
| } | ||
| varAssign.RHS = getAttrValueExpr(attr, attrType, args, env) | ||
| return env.File, nil | ||
| } | ||
| if attr == "kind" { | ||
| env.Rule.SetKind(args[0]) | ||
| } else { | ||
|
|
@@ -1020,7 +1114,7 @@ var readonlyCommands = map[string]bool{ | |
| "print_comment": true, | ||
| } | ||
|
|
||
| func expandTargets(f *build.File, rule string) ([]*build.Rule, error) { | ||
| func expandTargets(f *build.File, rule string, vars map[string]*build.AssignExpr) ([]*build.Rule, error) { | ||
| if r := FindRuleByName(f, rule); r != nil { | ||
| return []*build.Rule{r}, nil | ||
| } else if r := FindExportedFile(f, rule); r != nil { | ||
|
|
@@ -1040,6 +1134,12 @@ func expandTargets(f *build.File, rule string) ([]*build.Rule, error) { | |
| } else { | ||
| return f.Rules(kind), nil | ||
| } | ||
| } else if vars != nil { | ||
| // When variable editing is enabled, allow targeting global variables directly, | ||
| // but only if the variable is actually defined in the file. | ||
| if _, ok := vars[rule]; ok { | ||
| return []*build.Rule{{ImplicitName: rule}}, nil | ||
| } | ||
| } | ||
| return nil, fmt.Errorf("rule '%s' not found", rule) | ||
| } | ||
|
|
@@ -1190,11 +1290,19 @@ type rewriteResult struct { | |
| func getGlobalVariables(exprs []build.Expr) (vars map[string]*build.AssignExpr) { | ||
| vars = make(map[string]*build.AssignExpr) | ||
| for _, expr := range exprs { | ||
| if as, ok := expr.(*build.AssignExpr); ok { | ||
| if lhs, ok := as.LHS.(*build.Ident); ok { | ||
| vars[lhs.Name] = as | ||
| build.Walk(expr, func(x build.Expr, stk []build.Expr) { | ||
| //Skip variables defined inside functions | ||
| for _, frame := range stk { | ||
| if _, ok := frame.(*build.DefStmt); ok { | ||
| return | ||
| } | ||
| } | ||
| } | ||
| if as, ok := x.(*build.AssignExpr); ok { | ||
| if lhs, ok := as.LHS.(*build.Ident); ok { | ||
| vars[lhs.Name] = as | ||
| } | ||
| } | ||
|
Comment on lines
+1300
to
+1304
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implementation only detects global variables assigned via simple identifiers (e.g.,
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes — that's a real limitation. Intended. Added to README |
||
| }) | ||
| } | ||
| return vars | ||
| } | ||
|
|
@@ -1296,7 +1404,7 @@ func rewrite(opts *Options, commandsForFile commandsForFile) *rewriteResult { | |
| absPkg = f.Pkg | ||
| } | ||
|
|
||
| targets, err := expandTargets(f, rule) | ||
| targets, err := expandTargets(f, rule, vars) | ||
| if err != nil { | ||
| cerr := commandError(cft.commands, cft.target, err) | ||
| errs = append(errs, cerr) | ||
|
|
@@ -1356,6 +1464,7 @@ func executeCommandsInFile( | |
| ) (*build.File, error) { | ||
| changed := false | ||
| for _, cmd := range cft.commands { | ||
| cmdName := cmd.tokens[0] | ||
| cmdInfo := AllCommands[cmd.tokens[0]] | ||
| // Depending on whether a transformation is rule-specific or not, it should be applied to | ||
| // every rule that satisfies the filter or just once to the file. | ||
|
|
@@ -1364,6 +1473,22 @@ func executeCommandsInFile( | |
| cmdTargets = []*build.Rule{nil} | ||
| } | ||
| for _, r := range cmdTargets { | ||
| // Variable targets are represented by a dummy Rule with a nil Call. | ||
| // Most rule-specific commands assume a non-nil Call, so guard centrally to avoid panics. | ||
| if r != nil && r.Call == nil { | ||
| switch cmdName { | ||
| case "add", "remove", "set", "print", "delete": | ||
| // Supported variable-target commands. | ||
| default: | ||
| err := fmt.Errorf("command %q does not support variable targets", cmdName) | ||
| cerr := commandError([]command{cmd}, cft.target, err) | ||
| if opts.KeepGoing { | ||
| *errs = append(*errs, cerr) | ||
| continue | ||
| } | ||
| return nil, cerr | ||
| } | ||
| } | ||
| record := &apipb.Output_Record{} | ||
| newf, err := cmdInfo.Fn(opts, CmdEnvironment{f, r, vars, absPkg, cmd.tokens[1:], record}) | ||
| if len(record.Fields) != 0 { | ||
|
|
@@ -1749,7 +1874,7 @@ func ExecuteCommandsOnInlineFile(fileContent []byte, commands []string) ([]byte, | |
| f.Type = build.TypeBuild | ||
| } | ||
| for _, cft := range commandsByTargetName { | ||
| rules, err := expandTargets(f, cft.target) | ||
| rules, err := expandTargets(f, cft.target, nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you happen to have some further reasoning / requirements for the variable support?
It has been an intentional decision for the buildtools to identify targets using the "name" attribute since it generally simplifies BUILD file management as well as large scale changes across the google monorepo.
Use of variables also generally tends to have poor effects on build health, are not really aligned with https://bazel.build/build/style-guide#prefer-damp-build-files-over-dry, and is something we generally discourage.
So it would be interesting to learn what the specific need is, from what I can tell
patcheswould generally be part of a target with anameas well?Additionally the change to support variables seems to add some non-insignificant complexity across many of the commands.